From a4c03af4e9461cec99f7dd00cea12ec7587983a8 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+im-fran@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:23:41 -0300 Subject: [PATCH 001/194] patch: ordenado algunos archivos * Se han movido algunos archivos a sus carpetas correspondientes para tener un mejor orden. --- ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme | 7 +++++++ lib/config/router.dart | 6 +++--- .../{ => asignatura}/asignatura_controller.dart | 0 .../{ => asignatura}/asignaturas_controller.dart | 2 +- .../{ => asignatura}/asignatura_detalle_screen.dart | 8 ++++---- .../{ => asignatura}/asignatura_estudiantes_tab.dart | 0 lib/screens/{ => asignatura}/asignatura_notas_tab.dart | 2 +- lib/screens/{ => asignatura}/asignatura_resumen_tab.dart | 0 .../{ => asignatura}/asignaturas_lista_screen.dart | 2 +- 9 files changed, 17 insertions(+), 10 deletions(-) rename lib/controllers/{ => asignatura}/asignatura_controller.dart (100%) rename lib/controllers/{ => asignatura}/asignaturas_controller.dart (95%) rename lib/screens/{ => asignatura}/asignatura_detalle_screen.dart (92%) rename lib/screens/{ => asignatura}/asignatura_estudiantes_tab.dart (100%) rename lib/screens/{ => asignatura}/asignatura_notas_tab.dart (99%) rename lib/screens/{ => asignatura}/asignatura_resumen_tab.dart (100%) rename lib/screens/{ => asignatura}/asignaturas_lista_screen.dart (98%) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme index 77d0e8a..70cb7d0 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme @@ -50,6 +50,13 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + Date: Thu, 2 Nov 2023 12:25:23 -0300 Subject: [PATCH 002/194] =?UTF-8?q?patch:=20reduccion=20de=20tama=C3=B1o?= =?UTF-8?q?=20para=20notas=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Se ha reducido el tamaño de asignatura notas tab al mover distintas secciones de la clase a sus propios widgets como las notas de presentacion y examen, ademas de la nota final. --- .../asignatura/asignatura_notas_tab.dart | 259 +++++------------- .../notas_tab/labeled_nota_display.dart | 54 ++++ .../notas_tab/nota_final_display.dart | 33 +++ .../asignatura/notas_tab/notas_display.dart | 78 ++++++ 4 files changed, 235 insertions(+), 189 deletions(-) create mode 100644 lib/widgets/asignatura/notas_tab/labeled_nota_display.dart create mode 100644 lib/widgets/asignatura/notas_tab/nota_final_display.dart create mode 100644 lib/widgets/asignatura/notas_tab/notas_display.dart diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index 318238a..fd0fe03 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,199 +1,80 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_masked_text/flutter_masked_text.dart'; -import 'package:get/get_state_manager/get_state_manager.dart'; -import 'package:mi_utem/controllers/asignatura/asignatura_controller.dart'; -import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/themes/theme.dart'; -import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; -import 'package:mi_utem/widgets/nota_list_item.dart'; -import 'package:mi_utem/widgets/pull_to_refresh.dart'; + import 'package:flutter/material.dart'; + import 'package:flutter_masked_text/flutter_masked_text.dart'; + import 'package:get/get_state_manager/get_state_manager.dart'; + import 'package:mi_utem/controllers/asignatura/asignatura_controller.dart'; + import 'package:mi_utem/models/evaluacion.dart'; + import 'package:mi_utem/themes/theme.dart'; + import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; + import 'package:mi_utem/widgets/custom_error_widget.dart'; + import 'package:mi_utem/widgets/loading_indicator.dart'; + import 'package:mi_utem/widgets/nota_list_item.dart'; + import 'package:mi_utem/widgets/pull_to_refresh.dart'; -class AsignaturaNotasTab extends GetView { - final String asignaturaId; + class AsignaturaNotasTab extends GetView { + final String asignaturaId; - AsignaturaNotasTab({ - Key? key, - required this.asignaturaId, - }) : super(key: key); + AsignaturaNotasTab({ + Key? key, + required this.asignaturaId, + }) : super(key: key); - @override - String get tag => asignaturaId; + @override + String get tag => asignaturaId; - Future _onRefresh() async { - controller.refreshData(); - } - - @override - Widget build(BuildContext context) { - return PullToRefresh( - onRefresh: _onRefresh, - child: controller.obx( - (asignatura) { - final examGradeController = MaskedTextController( - mask: '0.0', - text: asignatura?.grades?.notaExamen?.toStringAsFixed(1) ?? "", - ); - final presentationGradeController = MaskedTextController( - mask: '0.0', - text: - asignatura?.grades?.notaPresentacion?.toStringAsFixed(1) ?? "", - ); + Future _onRefresh() async { + controller.refreshData(); + } - return ListView( - physics: AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.all(10), - children: [ - Card( - child: Row( - children: [ - Container( - height: 130, - width: 10, - color: asignatura?.colorPorEstado, - ), - Expanded( - child: Container( - padding: EdgeInsets.fromLTRB(15, 20, 20, 20), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - children: [ - Text( - asignatura?.grades?.notaFinal - ?.toStringAsFixed(1) ?? - "S/N", - style: TextStyle( - fontSize: 40, - fontWeight: FontWeight.bold, - ), - ), - Text( - asignatura?.estado ?? "---", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - Container(width: 10), - Container( - height: 80, - width: 0.5, - color: Colors.grey, - ), - Container(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "Examen", - style: TextStyle(fontSize: 16), - ), - Container( - width: 60, - margin: EdgeInsets.only(left: 15), - child: TextField( - controller: examGradeController, - textAlign: TextAlign.center, - enabled: false, - decoration: InputDecoration( - disabledBorder: MainTheme.theme - .inputDecorationTheme.border! - .copyWith( - borderSide: BorderSide( - color: Colors.transparent, - )), - ), - keyboardType: - TextInputType.numberWithOptions( - decimal: true), - ), - ), - ], - ), - Container(height: 10), - Row( - children: [ - Text( - "Presentación", - style: TextStyle(fontSize: 16), - ), - Container( - width: 60, - margin: EdgeInsets.only(left: 15), - child: TextField( - controller: presentationGradeController, - textAlign: TextAlign.center, - enabled: false, - decoration: InputDecoration( - hintText: "--", - disabledBorder: MainTheme.theme - .inputDecorationTheme.border! - .copyWith( - borderSide: BorderSide( - color: Colors.transparent, - )), - ), - keyboardType: - TextInputType.numberWithOptions( - decimal: true), - ), - ), - ], - ) - ], - ), - ], - ), - ), - ), - ], + @override + Widget build(BuildContext context) { + return PullToRefresh( + onRefresh: _onRefresh, + child: controller.obx((asignatura) => ListView( + physics: AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.all(10), + children: [ + NotasDisplayWidget( + notaFinal: asignatura?.grades?.notaFinal, + notaExamen: asignatura?.grades?.notaExamen, + notaPresentacion: asignatura?.grades?.notaPresentacion, + estado: asignatura?.estado, + colorPorEstado: asignatura?.colorPorEstado, ), - ), - Card( - child: Container( - padding: EdgeInsets.all(20), - child: asignatura?.grades?.notasParciales.isNotEmpty == true - ? ListView.builder( - shrinkWrap: true, - physics: ClampingScrollPhysics(), - itemBuilder: (context, i) { - REvaluacion evaluacion = - asignatura!.grades!.notasParciales[i]; - return NotaListItem( - evaluacion: IEvaluacion.fromRemote(evaluacion), - /* onChanged: (evaluacion) { - _controller.chag(evaluacion, nota); - } */ - ); - }, - itemCount: asignatura!.grades!.notasParciales.length, - ) - : CustomErrorWidget( - emoji: "🤔", - title: "Parece que aún no hay notas ni ponderadores", - ), + Card( + child: Container( + padding: EdgeInsets.all(20), + child: asignatura?.grades?.notasParciales.isNotEmpty == true + ? ListView.builder( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemBuilder: (context, i) { + REvaluacion evaluacion = asignatura!.grades!.notasParciales[i]; + return NotaListItem( + evaluacion: IEvaluacion.fromRemote(evaluacion), + /* onChanged: (evaluacion) { + _controller.chag(evaluacion, nota); + } */ + ); + }, + itemCount: asignatura!.grades!.notasParciales.length, + ) + : CustomErrorWidget( + emoji: "🤔", + title: "Parece que aún no hay notas ni ponderadores", + ), + ), ), - ), - ], - ); - }, - onLoading: LoadingIndicator(), - onError: (error) => SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: CustomErrorWidget( - title: "Ocurrió un error al cargar las notas", - error: '', + ], + ), + onLoading: LoadingIndicator(), + onError: (error) => SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: CustomErrorWidget( + title: "Ocurrió un error al cargar las notas", + error: '', + ), ), ), - ), - ); + ); + } } -} diff --git a/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart b/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart new file mode 100644 index 0000000..76d228a --- /dev/null +++ b/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart @@ -0,0 +1,54 @@ + import 'package:flutter/material.dart'; + import 'package:flutter_masked_text/flutter_masked_text.dart'; + import 'package:mi_utem/themes/theme.dart'; + + class LabeledNotaDisplayWidget extends StatelessWidget { + + final String label; + final num? nota; + final String? hint; + + LabeledNotaDisplayWidget({ + Key? key, + required this.label, + this.nota, + this.hint, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final notaController = MaskedTextController( + mask: '0.0', + text: nota?.toStringAsFixed(1) ?? "", + ); + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(label, + style: TextStyle(fontSize: 16), + ), + Container( + width: 60, + margin: EdgeInsets.only(left: 15), + child: TextField( + controller: notaController, + textAlign: TextAlign.center, + enabled: false, + decoration: InputDecoration( + hintText: hint, + disabledBorder: MainTheme.theme + .inputDecorationTheme.border! + .copyWith(borderSide: BorderSide(color: Colors.transparent)), + ), + keyboardType: + TextInputType.numberWithOptions( + decimal: true, + ), + ), + ), + ], + ); + } + + } \ No newline at end of file diff --git a/lib/widgets/asignatura/notas_tab/nota_final_display.dart b/lib/widgets/asignatura/notas_tab/nota_final_display.dart new file mode 100644 index 0000000..35a7da8 --- /dev/null +++ b/lib/widgets/asignatura/notas_tab/nota_final_display.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class NotaFinalDisplayWidget extends StatelessWidget { + + final num? notaFinal; + final String? estado; + + NotaFinalDisplayWidget({ + Key? key, + this.notaFinal, + this.estado, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(notaFinal?.toStringAsFixed(1) ?? "S/N", + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + ), + ), + Text(estado ?? "---", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/asignatura/notas_tab/notas_display.dart b/lib/widgets/asignatura/notas_tab/notas_display.dart new file mode 100644 index 0000000..7705e21 --- /dev/null +++ b/lib/widgets/asignatura/notas_tab/notas_display.dart @@ -0,0 +1,78 @@ + import 'package:flutter/material.dart'; + import 'package:flutter_masked_text/flutter_masked_text.dart'; + import 'package:mi_utem/themes/theme.dart'; + import 'package:mi_utem/widgets/asignatura/notas_tab/labeled_nota_display.dart'; + import 'package:mi_utem/widgets/asignatura/notas_tab/nota_final_display.dart'; + + class NotasDisplayWidget extends StatelessWidget { + + final num? notaFinal; + final num? notaExamen; + final num? notaPresentacion; + final String? estado; + final Color? colorPorEstado; + + NotasDisplayWidget({ + Key? key, + this.notaFinal, + this.notaExamen, + this.notaPresentacion, + this.estado, + this.colorPorEstado, + }) : super(key: key); + + + @override + Widget build(BuildContext context) { + return Card( + child: Row( + children: [ + Container( + height: 130, + width: 10, + color: colorPorEstado, + ), + Expanded( + child: Container( + padding: EdgeInsets.fromLTRB(15, 20, 20, 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + NotaFinalDisplayWidget( + notaFinal: notaFinal, + estado: estado, + ), + Container(width: 10), + Container( + height: 80, + width: 0.5, + color: Colors.grey, + ), + Container(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + LabeledNotaDisplayWidget( + label: "Examen", + nota: notaExamen, + ), + Container(height: 10), + LabeledNotaDisplayWidget( + label: "Presentación", + nota: notaPresentacion, + hint: "--" + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + + } \ No newline at end of file From b5256adfcabd4bdabaaa33cdce85943fb321b724 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+im-fran@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:25:58 -0300 Subject: [PATCH 003/194] rem: removido algunos imports no utilizados --- lib/screens/asignatura/asignatura_notas_tab.dart | 2 -- lib/widgets/asignatura/notas_tab/notas_display.dart | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index fd0fe03..1730e74 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; - import 'package:flutter_masked_text/flutter_masked_text.dart'; import 'package:get/get_state_manager/get_state_manager.dart'; import 'package:mi_utem/controllers/asignatura/asignatura_controller.dart'; import 'package:mi_utem/models/evaluacion.dart'; - import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; diff --git a/lib/widgets/asignatura/notas_tab/notas_display.dart b/lib/widgets/asignatura/notas_tab/notas_display.dart index 7705e21..8786058 100644 --- a/lib/widgets/asignatura/notas_tab/notas_display.dart +++ b/lib/widgets/asignatura/notas_tab/notas_display.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_masked_text/flutter_masked_text.dart'; - import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/labeled_nota_display.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/nota_final_display.dart'; From 274f0fd76ab78b54a035e41a26d6bcc084d61b5e Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 7 Nov 2023 22:41:14 -0300 Subject: [PATCH 004/194] patch: error al refrescar token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Había un error que al intentar refrescar la token y las credenciales son nulas no pasa nada. Ahora el usuario es redirigido al login informando del error. --- ios/Runner/Runner.entitlements | 4 ++++ lib/services/auth_service.dart | 23 +++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 903def2..a8b7773 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.developer.associated-domains + + webcredentials:mi.utem.cl + diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 29d756f..c871591 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,6 +1,9 @@ import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get/get.dart' as GetX; import 'package:get_storage/get_storage.dart'; +import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/helpers/snackbars.dart'; import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/services/perfil_service.dart'; import 'package:mi_utem/utils/dio_miutem_client.dart'; @@ -116,17 +119,21 @@ class AuthService { String? correo = box.read("correoUtem"); String? contrasenia = await secureStorage.read(key: "contrasenia"); - if (correo != null && contrasenia != null) { - dynamic data = {'correo': correo, 'contrasenia': contrasenia}; + if(correo == null || contrasenia == null) { + await logOut(); + GetX.Get.toNamed(Routes.login); + showDefaultSnackbar("Error", "Lo sentimos, pero parece que tus credenciales no son válidas. Por favor, vuelve a iniciar sesión."); + throw Exception("No se pudo refrescar el token (no hay credenciales)"); + } + + dynamic data = {'correo': correo, 'contrasenia': contrasenia}; - Response response = await DioMiUtemClient.authDio.post(uri, data: data); + Response response = await DioMiUtemClient.authDio.post(uri, data: data); - String token = response.data['token']; - _storeToken(token); + String token = response.data['token']; + _storeToken(token); - return token; - } - throw Exception("No se pudo refrescar el token"); + return token; } catch (e) { print(e.toString()); rethrow; From 85a13b191d441acdb0a44a74298b7a3a3d1b2fbf Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:56:40 -0300 Subject: [PATCH 005/194] patch: actualizadas dependencias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Se han actualizado varias dependencias para soportar la última version de flutter * Se ha cambiado charts_flutter (descontinuado) a community_charts_flutter (una version de la comunidad con arreglos y que sigue en desarrollo activo) * Se actualizó en android/build.gradle la version sdk de 33 a 34. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- android/build.gradle | 4 +-- lib/widgets/asistencia_chart.dart | 4 +-- pubspec.lock | 57 +++++++++++++++---------------- pubspec.yaml | 11 +++--- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 74e4842..ca02cfb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,8 +1,8 @@ buildscript { ext.kotlin_version = '1.7.21' ext { - compileSdkVersion = 33 // or latest - targetSdkVersion = 33 // or latest + compileSdkVersion = 34 // or latest + targetSdkVersion = 34 // or latest appCompatVersion = "1.4.2" // or latest } repositories { diff --git a/lib/widgets/asistencia_chart.dart b/lib/widgets/asistencia_chart.dart index 34a33f1..da2c689 100644 --- a/lib/widgets/asistencia_chart.dart +++ b/lib/widgets/asistencia_chart.dart @@ -1,9 +1,7 @@ import 'dart:math'; +import 'package:community_charts_flutter/community_charts_flutter.dart' as charts; import 'package:flutter/material.dart'; - -import 'package:charts_flutter/flutter.dart' as charts; - import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/themes/theme.dart'; diff --git a/pubspec.lock b/pubspec.lock index 994e81d..a5e0cb1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: awesome_notifications - sha256: "2b430c75cc879d6cfd52bb6eb2b5c1591ed425347816408cdcbd3f6916bba14c" + sha256: "327162c36a1476978f89638741da618cac42356aabc39a5035b6c206d8fca978" url: "https://pub.dev" source: hosted - version: "0.7.4+1" + version: "0.7.7" awesome_notifications_core: dependency: "direct main" description: @@ -177,23 +177,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charts_common: - dependency: transitive - description: - name: charts_common - sha256: "7b8922f9b0d9b134122756a787dab1c3946ae4f3fc5022ff323ba0014998ea02" - url: "https://pub.dev" - source: hosted - version: "0.12.0" - charts_flutter: - dependency: "direct main" - description: - path: charts_flutter - ref: HEAD - resolved-ref: fcd0c524ac573ae4d0b79f3bdd6491fbbcdbfb61 - url: "https://github.com/google/charts" - source: git - version: "0.12.0" checked_yaml: dependency: transitive description: @@ -266,6 +249,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.2" + community_charts_common: + dependency: transitive + description: + name: community_charts_common + sha256: "20697244c826df0545237ebe01d61caa96a2a2e4d23c6f88890441636a4d5220" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + community_charts_flutter: + dependency: "direct main" + description: + name: community_charts_flutter + sha256: ca5bd07337e162daee13c19679f602cd8b3f704520d242beeebbc2e312f84f89 + url: "https://pub.dev" + source: hosted + version: "1.0.2" convert: dependency: transitive description: @@ -454,10 +453,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: dcf54c170c5371ad0e79229d0fb372c58262ae0968b7de222bf28e51dd236be0 + sha256: "675c209c94a1817649137cbd113fc4c9ae85e48d03dd578629abbec6d8a4d93d" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.16.0" firebase_core_platform_interface: dependency: transitive description: @@ -470,10 +469,10 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "4cf4d2161530332ddc3c562f19823fb897ff37a9a774090d28df99f47370e973" + sha256: e8c408923cd3a25bd342c576a114f2126769cd1a57106a4edeaa67ea4a84e962 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.8.0" firebase_in_app_messaging: dependency: "direct main" description: @@ -926,10 +925,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" js: dependency: transitive description: @@ -1294,18 +1293,18 @@ packages: dependency: transitive description: name: sentry - sha256: "830667eadc0398fea3a3424ed1b74568e2db603a42758d0922e2f2974ce55a60" + sha256: "9cfd325611ab54b57d5e26957466823f05bea9d6cfcc8d48f11817b8bcedf0d1" url: "https://pub.dev" source: hosted - version: "7.10.1" + version: "7.12.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "6730f41b304c6fb0fa590dacccaf73ba11082fc64b274cfe8a79776f2b95309c" + sha256: "0cd7d622cb63c94fd1b2f87ab508e158b950bd281e2a80f327ebf73bb217eaf3" url: "https://pub.dev" source: hosted - version: "7.10.1" + version: "7.12.0" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2bef81c..de7b3c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,10 +17,7 @@ dependencies: barcode_image: ^2.0.2 cached_network_image: ^3.2.0 carousel_slider: ^4.0.0 - charts_flutter: - git: - url: https://github.com/google/charts - path: charts_flutter + community_charts_flutter: ^1.0.2 circular_profile_avatar: ^2.0.5 clipboard: ^0.1.3 cloud_firestore: ^4.3.1 @@ -30,7 +27,7 @@ dependencies: dio_cache_interceptor_hive_store: ^3.1.1 dotted_border: ^2.0.0+2 extended_image: ^8.1.1 - firebase_core: 2.11.0 + firebase_core: 2.16.0 firebase_analytics: 10.2.1 firebase_in_app_messaging: ^0.7.0+10 firebase_remote_config: ^3.0.9 @@ -63,14 +60,14 @@ dependencies: recase: ^4.0.0 responsive_framework: ^0.2.0 screenshot: ^2.1.0 - sentry_flutter: ^7.10.1 + sentry_flutter: ^7.12.0 share_plus: ^7.2.1 simple_gesture_detector: ^0.2.0 url_launcher: ^6.1.7 video_player: ^2.6.1 dio_http_cache: ^0.3.0 image: ^3.1.0 - intl: ^0.17.0 + intl: ^0.18.1 get_storage: ^2.0.3 hexcolor: ^3.0.1 shared_preferences: ^2.0.20 From 87cba841c59195d0f796a4a30944ab89caa1ade5 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+im-fran@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:52:39 -0300 Subject: [PATCH 006/194] patch: actualizadas unas dependencias para reparar cocoapods install. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Podfile.lock | 89 ++++++++++++++++++++++++++++++++++-------------- pubspec.lock | 28 +++++++++++---- pubspec.yaml | 4 +-- 3 files changed, 88 insertions(+), 33 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1a54867..e00ae90 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -596,18 +596,18 @@ PODS: - abseil/base/base_internal - abseil/base/config - abseil/meta/type_traits - - awesome_notifications (0.0.5): + - awesome_notifications (0.8.1): - Flutter - - IosAwnCore (= 0.7.3) + - IosAwnCore (~> 0.8.0) - awesome_notifications_core (0.0.1): - Flutter - - awesome_notifications_fcm (0.7.3): + - awesome_notifications_fcm (0.8.1): - awesome_notifications - - Firebase - Firebase/Messaging + - FirebaseCore - Flutter - - IosAwnCore (= 0.7.3) - - IosAwnFcmDist (= 0.7.5) + - IosAwnCore (~> 0.8.0) + - IosAwnFcmDist (~> 0.8.0) - background_fetch (1.2.1): - Flutter - BoringSSL-GRPC (0.0.24): @@ -621,8 +621,6 @@ PODS: - firebase_core - Flutter - nanopb (< 2.30910.0, >= 2.30908.0) - - Firebase (10.7.0): - - Firebase/Core (= 10.7.0) - Firebase/Analytics (10.7.0): - Firebase/Core - Firebase/Core (10.7.0): @@ -630,6 +628,9 @@ PODS: - FirebaseAnalytics (~> 10.7.0) - Firebase/CoreOnly (10.7.0): - FirebaseCore (= 10.7.0) + - Firebase/Crashlytics (10.7.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 10.7.0) - Firebase/Firestore (10.7.0): - Firebase/CoreOnly - FirebaseFirestore (~> 10.7.0) @@ -646,9 +647,13 @@ PODS: - Firebase/Analytics (= 10.7.0) - firebase_core - Flutter - - firebase_core (2.11.0): + - firebase_core (2.16.0): - Firebase/CoreOnly (= 10.7.0) - Flutter + - firebase_crashlytics (3.3.6): + - Firebase/Crashlytics (= 10.7.0) + - firebase_core + - Flutter - firebase_in_app_messaging (0.7.0-15): - Firebase/InAppMessaging (= 10.7.0) - firebase_core @@ -681,8 +686,18 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) + - FirebaseCoreExtension (10.17.0): + - FirebaseCore (~> 10.0) - FirebaseCoreInternal (10.16.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseCrashlytics (10.7.0): + - FirebaseCore (~> 10.5) + - FirebaseInstallations (~> 10.0) + - FirebaseSessions (~> 10.5) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.8) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (~> 2.1) - FirebaseFirestore (10.7.0): - abseil/algorithm (~> 1.20211102.0) - abseil/base (~> 1.20211102.0) @@ -722,6 +737,14 @@ PODS: - FirebaseInstallations (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseSessions (10.17.0): + - FirebaseCore (~> 10.5) + - FirebaseCoreExtension (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.10) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesSwift (~> 2.1) - Flutter (1.0.0) - flutter_keyboard_visibility (0.0.1): - Flutter @@ -833,9 +856,11 @@ PODS: - Flutter - in_app_review (0.2.0): - Flutter - - IosAwnCore (0.7.3) - - IosAwnFcmDist (0.7.5): - - IosAwnCore (= 0.7.3) + - IosAwnCore (0.8.0) + - IosAwnFcmDist (0.8.0): + - FirebaseCore + - FirebaseMessaging + - IosAwnCore (~> 0.8.0) - leveldb-library (1.22.2) - Libuv-gRPC (0.0.10): - Libuv-gRPC/Implementation (= 0.0.10) @@ -856,13 +881,15 @@ PODS: - permission_handler_apple (9.1.1): - Flutter - PromisesObjC (2.3.1) - - Sentry/HybridSDK (8.11.0): - - SentryPrivate (= 8.11.0) + - PromisesSwift (2.3.1): + - PromisesObjC (= 2.3.1) + - Sentry/HybridSDK (8.14.2): + - SentryPrivate (= 8.14.2) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.11.0) - - SentryPrivate (8.11.0) + - Sentry/HybridSDK (= 8.14.2) + - SentryPrivate (8.14.2) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -885,6 +912,7 @@ DEPENDENCIES: - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - firebase_in_app_messaging (from `.symlinks/plugins/firebase_in_app_messaging/ios`) - firebase_remote_config (from `.symlinks/plugins/firebase_remote_config/ios`) - Flutter (from `Flutter`) @@ -912,12 +940,15 @@ SPEC REPOS: - FirebaseABTesting - FirebaseAnalytics - FirebaseCore + - FirebaseCoreExtension - FirebaseCoreInternal + - FirebaseCrashlytics - FirebaseFirestore - FirebaseInAppMessaging - FirebaseInstallations - FirebaseMessaging - FirebaseRemoteConfig + - FirebaseSessions - FMDB - GoogleAppMeasurement - GoogleDataTransport @@ -930,6 +961,7 @@ SPEC REPOS: - Libuv-gRPC - nanopb - PromisesObjC + - PromisesSwift - Sentry - SentryPrivate - UXCam @@ -949,6 +981,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_analytics/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" + firebase_crashlytics: + :path: ".symlinks/plugins/firebase_crashlytics/ios" firebase_in_app_messaging: :path: ".symlinks/plugins/firebase_in_app_messaging/ios" firebase_remote_config: @@ -988,26 +1022,30 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: abseil: ebe5b5529fb05d93a8bdb7951607be08b7fa71bc - awesome_notifications: d63d9a25f126860f9a600850d99772237895b3ba + awesome_notifications: db394d2e061e4583ba0f738ddea611e3986cc3fb awesome_notifications_core: d02eed89738fa362d56cbd372850e9adcd2c6bef - awesome_notifications_fcm: 7e2d7ab4ca1826fe3a9a5ca96771ace73e05db48 + awesome_notifications_fcm: 554a9088f81a91dbe8a318abee6b48f43e4a9417 background_fetch: 896944864b038d2837fc750d470e9841e1e6a363 BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33 cloud_firestore: e0da2eedba479416c908433ac8879fdd81f61157 Firebase: 0219acf760880eeec8ce479895bd7767466d9f81 firebase_analytics: e8e294333de66e5429d4aac365966281b4dbfb7d - firebase_core: dee76ded6c693fdb38b8ea39aef7129e32e587a3 + firebase_core: 27bc73423698d0960be189ccf29bdc2d373f2746 + firebase_crashlytics: 1b3768f2e118b0281ad30f2931369e0d1921789c firebase_in_app_messaging: 2b36a1746f4fefbd6f578a2f6659358a0d3f2c94 firebase_remote_config: e5f1ed5b29191424280b7b249228f0ed64bc90ee FirebaseABTesting: 03f0a8b88cf618350527f2c6a2234e29b9c65064 FirebaseAnalytics: f8133442ee6f8512e28ff19e62ce15398bfaeace FirebaseCore: e317665b9d744727a97e623edbbed009320afdd7 + FirebaseCoreExtension: 47720bb330d7041047c0935a34a3a4b92f818074 FirebaseCoreInternal: 26233f705cc4531236818a07ac84d20c333e505a + FirebaseCrashlytics: 35fdd1a433b31e28adcf5c8933f4c526691a1e0b FirebaseFirestore: 3963a6edd1c84b4748dab3e2c62624a29d03eca1 FirebaseInAppMessaging: d04732fe9c37c3d026d66435abba60120087a7f5 FirebaseInstallations: b822f91a61f7d1ba763e5ccc9d4f2e6f2ed3b3ee FirebaseMessaging: ac9062bcc35ed56e15a0241d8fd317022499baf8 FirebaseRemoteConfig: d5de62211e2eaa2152d8ee85a23c301b70887a74 + FirebaseSessions: 49f39e5c10e3f9fdd38d01b748329bae2a2fa8ed Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be @@ -1021,8 +1059,8 @@ SPEC CHECKSUMS: image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d - IosAwnCore: 6494e0e174d49f04f513e8a002187be226889a37 - IosAwnFcmDist: 47578dd46472304b20f1965ad12cc893c877e57c + IosAwnCore: ed1b2b6d84962a758354dbacd9ce525c72ce28a9 + IosAwnFcmDist: 0ac4da8f9ef8f2db9e5ec5d0c10c8db793ce447d leveldb-library: f03246171cce0484482ec291f88b6d563699ee06 Libuv-gRPC: 55e51798e14ef436ad9bc45d12d43b77b49df378 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 @@ -1030,9 +1068,10 @@ SPEC CHECKSUMS: path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - Sentry: 39d57e691e311bdb73bc1ab5bbebbd6bc890050d - sentry_flutter: b2feefdad5b0f06602347172bc7257e8e9da5562 - SentryPrivate: 48712023cdfd523735c2edb6b06bedf26c4730a3 + PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 + Sentry: e0ea366f95ebb68f26d6030d8c22d6b2e6d23dd0 + sentry_flutter: 9a04c51c373d76ee22167bf1e65bc468c0a91fed + SentryPrivate: 949a21fa59872427edc73b524c3ec8456761d97f share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a @@ -1042,4 +1081,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 816280d49dfc997733647f095bf1e4c52c291937 -COCOAPODS: 1.13.0 +COCOAPODS: 1.14.2 diff --git a/pubspec.lock b/pubspec.lock index a5e0cb1..0f9c2fd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "867b77e2367bc502dcd4d5a66302615409f04eb20ed82ba1c0ba073f9107e018" + sha256: "2d8e8e123ca3675625917f535fcc0d3a50092eef44334168f9b18adc050d4c6e" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.6" analyzer: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: awesome_notifications - sha256: "327162c36a1476978f89638741da618cac42356aabc39a5035b6c206d8fca978" + sha256: "65f730f9c0e73a346039ef746384bcff1773f9f03821b859705a7ab8db977b23" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.8.2" awesome_notifications_core: dependency: "direct main" description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: awesome_notifications_fcm - sha256: "3bcfc54360128bd9a83f558c6652681d58ba158a5237f15536fd4c6da46d99f2" + sha256: "0e44272fc734a5f42af063688fa172bac58f79fcc98d8930cc3604a0ecba0bea" url: "https://pub.dev" source: hosted - version: "0.7.4+1" + version: "0.8.0" background_fetch: dependency: "direct main" description: @@ -473,6 +473,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.8.0" + firebase_crashlytics: + dependency: transitive + description: + name: firebase_crashlytics + sha256: f4a4b046606e306b589bef5c1e268afbfab2e5fddde6de7e4340400465c8d231 + url: "https://pub.dev" + source: hosted + version: "3.3.6" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + sha256: "8666b935e29b143297e2923dc8112663854f828d10954a92b8215e7249b55d59" + url: "https://pub.dev" + source: hosted + version: "3.6.6" firebase_in_app_messaging: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index de7b3c8..4a125cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,8 +10,8 @@ environment: dependencies: awesome_notifications_core: ^0.8.1 - awesome_notifications: any - awesome_notifications_fcm: any + awesome_notifications: ^0.8.2 + awesome_notifications_fcm: ^0.8.0 badges: ^2.0.2 barcode_widget: ^2.0.3 barcode_image: ^2.0.2 From d42ae1e98643e82b20e56e5e88765bee80fa42cd Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+im-fran@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:33:33 -0300 Subject: [PATCH 007/194] patch: mejorado widget acerca_screen * se ha separado el acerca de en una carpeta propia * se ha separado en otros widgets acerca del club y acerca de los desarrolladores. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/config/router.dart | 2 +- lib/screens/login_screen/login_screen.dart | 2 +- .../acerca_aplicacion_content.dart | 0 lib/widgets/{ => acerca}/acerca_dialog.dart | 2 +- lib/widgets/acerca/club/acerca_club.dart | 55 ++++++++ .../club/acerca_club_desarrolladores.dart | 133 ++++++++++++++++++ .../acerca/club/acerca_club_redes.dart | 51 +++++++ 7 files changed, 242 insertions(+), 3 deletions(-) rename lib/widgets/{ => acerca}/acerca_aplicacion_content.dart (100%) rename lib/widgets/{ => acerca}/acerca_dialog.dart (98%) create mode 100644 lib/widgets/acerca/club/acerca_club.dart create mode 100644 lib/widgets/acerca/club/acerca_club_desarrolladores.dart create mode 100644 lib/widgets/acerca/club/acerca_club_redes.dart diff --git a/lib/config/router.dart b/lib/config/router.dart index 9aa0b0e..ea9f974 100644 --- a/lib/config/router.dart +++ b/lib/config/router.dart @@ -15,7 +15,7 @@ import 'package:mi_utem/screens/splash_screen.dart'; import 'package:mi_utem/screens/usuario_screen.dart'; import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/services/perfil_service.dart'; -import 'package:mi_utem/widgets/acerca_screen.dart'; +import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; final _loginPage = GetPage( name: Routes.login, diff --git a/lib/screens/login_screen/login_screen.dart b/lib/screens/login_screen/login_screen.dart index ca11827..f2c976c 100644 --- a/lib/screens/login_screen/login_screen.dart +++ b/lib/screens/login_screen/login_screen.dart @@ -14,7 +14,7 @@ import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/widgets/acerca_dialog.dart'; +import 'package:mi_utem/widgets/acerca/acerca_dialog.dart'; import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; import 'package:mi_utem/widgets/dialogs/not_ready_dialog.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; diff --git a/lib/widgets/acerca_aplicacion_content.dart b/lib/widgets/acerca/acerca_aplicacion_content.dart similarity index 100% rename from lib/widgets/acerca_aplicacion_content.dart rename to lib/widgets/acerca/acerca_aplicacion_content.dart diff --git a/lib/widgets/acerca_dialog.dart b/lib/widgets/acerca/acerca_dialog.dart similarity index 98% rename from lib/widgets/acerca_dialog.dart rename to lib/widgets/acerca/acerca_dialog.dart index 9aba895..ca1e8f2 100644 --- a/lib/widgets/acerca_dialog.dart +++ b/lib/widgets/acerca/acerca_dialog.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/widgets/acerca_aplicacion_content.dart'; +import 'package:mi_utem/widgets/acerca/acerca_aplicacion_content.dart'; class AcercaDialog extends StatefulWidget { AcercaDialog({ diff --git a/lib/widgets/acerca/club/acerca_club.dart b/lib/widgets/acerca/club/acerca_club.dart new file mode 100644 index 0000000..18c16d9 --- /dev/null +++ b/lib/widgets/acerca/club/acerca_club.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; + +import '../../../services/remote_config/remote_config.dart'; +import '../../default_network_image.dart'; +import 'acerca_club_redes.dart'; + +class AcercaClub extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + child: Container( + padding: EdgeInsets.all(20), + child: Column( + children: [ + Container( + width: 150, + height: 150, + child: DefaultNetworkImage( + url: RemoteConfigService.clubLogo, + )), + Container(height: 20), + Text( + RemoteConfigService.clubNombre, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + color: Colors.grey[700], + fontWeight: FontWeight.bold, + ), + ), + Container(height: 20), + MarkdownBody( + selectable: false, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: TextStyle( + fontSize: 16, + color: Colors.grey[700], + ), + ), + data: RemoteConfigService.clubDescripcion, + ), + Container(height: 20), + AcercaClubRedes(), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart new file mode 100644 index 0000000..9f06b87 --- /dev/null +++ b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../config/routes.dart'; +import '../../../models/usuario.dart'; +import '../../../services/analytics_service.dart'; +import '../../../services/remote_config/remote_config.dart'; +import '../../image_view_screen.dart'; +import '../../profile_photo.dart'; + +class AcercaClubDesarrolladores extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + child: Container( + padding: EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Text( + "Desarrolladores", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + color: Colors.grey[700], + fontWeight: FontWeight.bold, + ), + ), + ), + Container(height: 10), + ...jsonDecode(RemoteConfigService.miutemDesarrolladores).map((creador) => Container ( + padding: EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + ProfilePhoto( + usuario: Usuario(nombres: creador['nombre'], fotoUrl: creador['fotoUrl']), + onImageTap: (context, imageProvider) { + AnalyticsService.logEvent("acerca_person_image_tap", parameters: { + "persona": creador['nombre'], + }); + Get.to(() => ImageViewScreen(imageProvider: imageProvider), + routeName: Routes.imageView, + ); + }), + Container(width: 20), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + creador["nombre"], + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey[800], + fontSize: 16, + ), + ), + Text( + creador["rol"], + style: TextStyle( + fontSize: 16, + color: Colors.grey[700], + ), + ), + Container(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: creador['redes'].map((red) => Container( + margin: EdgeInsets.only(right: 8), + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Color(red["color"]), + ), + child: InkWell( + customBorder: + CircleBorder(), + onTap: () async { + AnalyticsService.logEvent("acerca_person_social_tap", + parameters: { + "persona": + creador['nombre'], + "red": red['nombre'], + }, + ); + await launchUrl( + Uri.parse(red["url"]), + ); + }, + child: Container( + padding: + const EdgeInsets.all( + 8), + decoration: + new BoxDecoration( + shape: BoxShape.circle, + ), + child: Icon( + IconDataBrands(red["icono"]), + size: 15, + color: Colors.white, + ), + ), + ), + ), + ) + .toList(), + ), + ], + ), + ), + ], + ), + ), + ) + .toList() + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/acerca/club/acerca_club_redes.dart b/lib/widgets/acerca/club/acerca_club_redes.dart new file mode 100644 index 0000000..6049ae0 --- /dev/null +++ b/lib/widgets/acerca/club/acerca_club_redes.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AcercaClubRedes extends StatelessWidget { + + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: jsonDecode(RemoteConfigService.clubRedes) + .map( + (red) => Container( + margin: EdgeInsets.symmetric(horizontal: 5), + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Color(red["color"]), + ), + child: InkWell( + customBorder: CircleBorder(), + onTap: () async { + AnalyticsService.logEvent("acerca_club_social_tap", + parameters: { + "red": red['nombre'], + }, + ); + await launchUrl(Uri.parse(red["url"])); + }, + child: Container( + padding: const EdgeInsets.all(10), + decoration: new BoxDecoration( + shape: BoxShape.circle, + ), + child: Icon( + IconDataBrands(red["icono"]), + size: 20, + color: Colors.white, + ), + ), + ), + ), + ).toList() + ); + } + +} \ No newline at end of file From f8774622ac73956cb440ec4f6d64c47b3a3d111c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:34:43 -0300 Subject: [PATCH 008/194] patch: movido un archivo a su carpeta Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/acerca/acerca_screen.dart | 44 +++++ lib/widgets/acerca_screen.dart | 259 -------------------------- 2 files changed, 44 insertions(+), 259 deletions(-) create mode 100644 lib/widgets/acerca/acerca_screen.dart delete mode 100644 lib/widgets/acerca_screen.dart diff --git a/lib/widgets/acerca/acerca_screen.dart b/lib/widgets/acerca/acerca_screen.dart new file mode 100644 index 0000000..7de53a0 --- /dev/null +++ b/lib/widgets/acerca/acerca_screen.dart @@ -0,0 +1,44 @@ + +import 'package:flutter/material.dart'; +import 'package:mi_utem/widgets/acerca/acerca_aplicacion_content.dart'; +import 'package:mi_utem/widgets/acerca/club/acerca_club.dart'; +import 'package:mi_utem/widgets/acerca/club/acerca_club_desarrolladores.dart'; +import 'package:mi_utem/widgets/custom_app_bar.dart'; + +class AcercaScreen extends StatelessWidget { + AcercaScreen({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[200], + appBar: CustomAppBar( + title: Text( + "Acerca de Mi UTEM", + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + AcercaClub(), + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + child: AcercaAplicacionContent(), + ), + AcercaClubDesarrolladores() + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/acerca_screen.dart b/lib/widgets/acerca_screen.dart deleted file mode 100644 index 30a7613..0000000 --- a/lib/widgets/acerca_screen.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/models/usuario.dart'; -import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/widgets/acerca_aplicacion_content.dart'; -import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:mi_utem/widgets/default_network_image.dart'; -import 'package:mi_utem/widgets/image_view_screen.dart'; -import 'package:mi_utem/widgets/profile_photo.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class AcercaScreen extends StatelessWidget { - AcercaScreen({ - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey[200], - appBar: CustomAppBar( - title: Text( - "Acerca de Mi UTEM", - ), - ), - body: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(10), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - child: Container( - padding: EdgeInsets.all(20), - child: Column( - children: [ - Container( - width: 150, - height: 150, - child: DefaultNetworkImage( - url: RemoteConfigService.clubLogo, - )), - Container(height: 20), - Text( - RemoteConfigService.clubNombre, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - color: Colors.grey[700], - fontWeight: FontWeight.bold, - ), - ), - Container(height: 20), - MarkdownBody( - selectable: false, - styleSheet: MarkdownStyleSheet( - textAlign: WrapAlignment.center, - p: TextStyle( - fontSize: 16, - color: Colors.grey[700], - ), - ), - data: RemoteConfigService.clubDescripcion, - ), - Container(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: jsonDecode(RemoteConfigService.clubRedes) - .map( - (red) => Container( - margin: EdgeInsets.symmetric(horizontal: 5), - decoration: new BoxDecoration( - shape: BoxShape.circle, - color: Color(red["color"]), - ), - child: InkWell( - customBorder: CircleBorder(), - onTap: () async { - AnalyticsService.logEvent( - "acerca_club_social_tap", - parameters: { - "red": red['nombre'], - }, - ); - await launchUrl(Uri.parse(red["url"])); - }, - child: Container( - padding: const EdgeInsets.all(10), - decoration: new BoxDecoration( - shape: BoxShape.circle, - ), - child: Icon( - IconDataBrands(red["icono"]), - size: 20, - color: Colors.white, - ), - ), - ), - ), - ) - .toList(), - ), - ], - ), - ), - ), - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - child: AcercaAplicacionContent(), - ), - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - child: Container( - padding: EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Text( - "Desarrolladores", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - color: Colors.grey[700], - fontWeight: FontWeight.bold, - ), - ), - ), - Container(height: 10), - ...jsonDecode(RemoteConfigService.miutemDesarrolladores) - .map( - (creador) => Container( - padding: EdgeInsets.symmetric(vertical: 10), - child: Row( - children: [ - ProfilePhoto( - usuario: Usuario( - nombres: creador['nombre'], - fotoUrl: creador['fotoUrl']), - onImageTap: (context, imageProvider) { - AnalyticsService.logEvent( - "acerca_person_image_tap", - parameters: { - "persona": creador['nombre'], - }, - ); - Get.to( - () => ImageViewScreen( - imageProvider: imageProvider, - ), - routeName: Routes.imageView, - ); - }), - Container(width: 20), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - creador["nombre"], - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.grey[800], - fontSize: 16, - ), - ), - Text( - creador["rol"], - style: TextStyle( - fontSize: 16, - color: Colors.grey[700], - ), - ), - Container(height: 5), - Row( - mainAxisAlignment: - MainAxisAlignment.start, - children: creador['redes'] - .map( - (red) => Container( - margin: - EdgeInsets.only(right: 8), - decoration: new BoxDecoration( - shape: BoxShape.circle, - color: Color(red["color"]), - ), - child: InkWell( - customBorder: - CircleBorder(), - onTap: () async { - AnalyticsService.logEvent( - "acerca_person_social_tap", - parameters: { - "persona": - creador['nombre'], - "red": red['nombre'], - }, - ); - await launchUrl( - Uri.parse(red["url"]), - ); - }, - child: Container( - padding: - const EdgeInsets.all( - 8), - decoration: - new BoxDecoration( - shape: BoxShape.circle, - ), - child: Icon( - IconDataBrands( - red["icono"]), - size: 15, - color: Colors.white, - ), - ), - ), - ), - ) - .toList(), - ), - ], - ), - ), - ], - ), - ), - ) - .toList() - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} From 07f62203a6b736e1dcb1b937afa3fb8631ef7cb9 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:57:35 -0300 Subject: [PATCH 009/194] wip: trabajando en reduccion de login_screen.dart y otros widgets. * Reducido login_screen * Reducido acerca de dialog. * Se necesita revisar otros widgets que modifique (creo que utilice stateless widgets para algunos que debieron ser stateful) Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/login_screen/login_screen.dart | 126 +++--------------- lib/widgets/acerca/acerca_screen.dart | 2 +- .../acerca_aplicacion_content.dart | 0 .../acerca/{ => dialog}/acerca_dialog.dart | 16 +-- lib/widgets/login_screen/creditos_app.dart | 51 +++++++ .../login_screen/formulario_credenciales.dart | 68 ++++++++++ 6 files changed, 144 insertions(+), 119 deletions(-) rename lib/widgets/acerca/{ => dialog}/acerca_aplicacion_content.dart (100%) rename lib/widgets/acerca/{ => dialog}/acerca_dialog.dart (88%) create mode 100644 lib/widgets/login_screen/creditos_app.dart create mode 100644 lib/widgets/login_screen/formulario_credenciales.dart diff --git a/lib/screens/login_screen/login_screen.dart b/lib/screens/login_screen/login_screen.dart index f2c976c..25c2027 100644 --- a/lib/screens/login_screen/login_screen.dart +++ b/lib/screens/login_screen/login_screen.dart @@ -1,24 +1,21 @@ -import 'dart:convert'; -import 'dart:math'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/helpers/snackbars.dart'; import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/auth_service.dart'; -import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/widgets/acerca/acerca_dialog.dart'; +import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; import 'package:mi_utem/widgets/dialogs/not_ready_dialog.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; -import 'package:mi_utem/widgets/login_text_form_field.dart'; +import 'package:mi_utem/widgets/login_screen/creditos_app.dart'; +import 'package:mi_utem/widgets/login_screen/formulario_credenciales.dart'; import 'package:video_player/video_player.dart'; part '_background.dart'; @@ -31,8 +28,8 @@ class LoginScreen extends StatefulWidget { } class _LoginScreenState extends State { - TextEditingController _correoController = TextEditingController(); - TextEditingController _contraseniaController = TextEditingController(); + TextEditingController correoController = TextEditingController(); + TextEditingController contraseniaController = TextEditingController(); GlobalKey _formKey = GlobalKey(); @@ -50,8 +47,8 @@ class _LoginScreenState extends State { ), ); - _correoController.text = ""; - _contraseniaController.text = ""; + correoController.text = ""; + contraseniaController.text = ""; SchedulerBinding.instance.addPostFrameCallback((_) { _checkAndPerformUpdate(); @@ -94,14 +91,6 @@ class _LoginScreenState extends State { } */ } - String get _creditText { - List texts = jsonDecode(RemoteConfigService.creditos); - - Random random = new Random(); - - return texts[random.nextInt(texts.length)]; - } - @override Widget build(BuildContext context) { return Scaffold( @@ -143,84 +132,14 @@ class _LoginScreenState extends State { ), ), Container(height: constraints.maxHeight * 0.1), - AutofillGroup( - child: Column( - children: [ - LoginTextFormField( - controller: _correoController, - hintText: 'nombre@utem.cl', - labelText: 'Correo UTEM', - textCapitalization: TextCapitalization.none, - keyboardType: TextInputType.emailAddress, - inputFormatters: [ - FilteringTextInputFormatter.deny( - RegExp(" ")), - ], - icon: Icons.person, - autofillHints: [AutofillHints.username], - validator: (String value) { - if (value.isEmpty) { - return 'Debe ingresar un correo UTEM'; - } else if (value.contains("@") && - !value.endsWith("@utem.cl")) { - return 'Debe ingresar un correo UTEM'; - } - }, - ), - LoginTextFormField( - controller: _contraseniaController, - hintText: '• • • • • • • • •', - labelText: 'Contraseña', - textCapitalization: TextCapitalization.none, - icon: Icons.lock, - obscureText: true, - autofillHints: [AutofillHints.password], - validator: (String value) { - if (value.isEmpty) { - return 'Debe ingresar una contraseña'; - } - }, - ) - ], - ), - ), + FormularioCredenciales(correoController: correoController, contraseniaController: contraseniaController), TextButton( onPressed: () => _login(), child: Text("Iniciar"), ), Container(height: constraints.maxHeight * 0.1), if (!isKeyboardVisible) - Expanded( - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: EdgeInsets.all(10), - child: SafeArea( - child: GestureDetector( - child: MarkdownBody( - selectable: false, - styleSheet: MarkdownStyleSheet( - textAlign: WrapAlignment.center, - p: TextStyle( - color: Colors.white, - ), - ), - data: _creditText, - ), - onTap: () { - Get.toNamed( - Routes.about, - ); - }, - ), - ), - ), - ], - ), - ), - ), + CreditosApp(), ], ), ), @@ -236,8 +155,8 @@ class _LoginScreenState extends State { } Future _login() async { - final correo = _correoController.text; - final contrasenia = _contraseniaController.text; + final correo = correoController.text; + final contrasenia = contraseniaController.text; if (correo == "error@utem.cl") { Get.dialog(MonkeyErrorDialog()); @@ -260,14 +179,10 @@ class _LoginScreenState extends State { AnalyticsService.logEvent('login'); AnalyticsService.setUser(usuario); - Get.toNamed( - Routes.home, - ); + Get.toNamed(Routes.home); - if (esPrimeraVez) { - Get.dialog( - AcercaDialog(), - ); + if (esPrimeraVez || true) { + Get.dialog(AcercaDialog()); } } on DioError catch (e) { print(e.message); @@ -276,21 +191,14 @@ class _LoginScreenState extends State { if (e.response?.data["codigoInterno"]?.toString() == "4") { Get.dialog(NotReadyDialog()); } else { - showDefaultSnackbar( - "Error", - "Usuario o contraseña incorrecta", - ); + showDefaultSnackbar("Error", "Usuario o contraseña incorrecta"); } - } else if (e.response?.statusCode != null && - e.response!.statusCode.toString().startsWith("5")) { + } else if (e.response?.statusCode != null && e.response!.statusCode.toString().startsWith("5")) { print(e.response?.data); Get.dialog(MonkeyErrorDialog()); } else { print(e.response?.data); - showDefaultSnackbar( - "Error", - "Ocurrió un error inesperado 😢", - ); + showDefaultSnackbar("Error", "Ocurrió un error inesperado 😢"); } } catch (e) { print(e.toString()); diff --git a/lib/widgets/acerca/acerca_screen.dart b/lib/widgets/acerca/acerca_screen.dart index 7de53a0..7a05433 100644 --- a/lib/widgets/acerca/acerca_screen.dart +++ b/lib/widgets/acerca/acerca_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/acerca/acerca_aplicacion_content.dart'; +import 'package:mi_utem/widgets/acerca/dialog/acerca_aplicacion_content.dart'; import 'package:mi_utem/widgets/acerca/club/acerca_club.dart'; import 'package:mi_utem/widgets/acerca/club/acerca_club_desarrolladores.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; diff --git a/lib/widgets/acerca/acerca_aplicacion_content.dart b/lib/widgets/acerca/dialog/acerca_aplicacion_content.dart similarity index 100% rename from lib/widgets/acerca/acerca_aplicacion_content.dart rename to lib/widgets/acerca/dialog/acerca_aplicacion_content.dart diff --git a/lib/widgets/acerca/acerca_dialog.dart b/lib/widgets/acerca/dialog/acerca_dialog.dart similarity index 88% rename from lib/widgets/acerca/acerca_dialog.dart rename to lib/widgets/acerca/dialog/acerca_dialog.dart index ca1e8f2..9a74f48 100644 --- a/lib/widgets/acerca/acerca_dialog.dart +++ b/lib/widgets/acerca/dialog/acerca_dialog.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/widgets/acerca/acerca_aplicacion_content.dart'; +import 'package:mi_utem/widgets/acerca/dialog/acerca_aplicacion_content.dart'; class AcercaDialog extends StatefulWidget { AcercaDialog({ @@ -90,14 +90,12 @@ class _AcercaDialogState extends State { : "Saber más", style: TextStyle(color: Colors.white), ), - onPressed: _isActive - ? null - : () { - Get.back(); - Get.toNamed( - Routes.about, - ); - }, + onPressed: () { + if (!_isActive) { + Get.back(); + Get.toNamed(Routes.about); + } + } ), ], ), diff --git a/lib/widgets/login_screen/creditos_app.dart b/lib/widgets/login_screen/creditos_app.dart new file mode 100644 index 0000000..71c5247 --- /dev/null +++ b/lib/widgets/login_screen/creditos_app.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; + +import '../../services/remote_config/remote_config.dart'; + +class CreditosApp extends StatelessWidget { + + String get _creditText { + List texts = jsonDecode(RemoteConfigService.creditos); + + Random random = new Random(); + + return texts[random.nextInt(texts.length)]; + } + + @override + Widget build(BuildContext context) { + return Expanded( + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: EdgeInsets.all(10), + child: SafeArea( + child: GestureDetector( + child: MarkdownBody( + selectable: false, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: TextStyle(color: Colors.white), + ), + data: _creditText, + ), + onTap: () { + Get.dialog(AcercaDialog()); + }, + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/login_screen/formulario_credenciales.dart b/lib/widgets/login_screen/formulario_credenciales.dart new file mode 100644 index 0000000..c8328d8 --- /dev/null +++ b/lib/widgets/login_screen/formulario_credenciales.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../login_text_form_field.dart'; + +class FormularioCredenciales extends StatefulWidget { + + final TextEditingController correoController; + final TextEditingController contraseniaController; + + + FormularioCredenciales({ required this.correoController, required this.contraseniaController }); + + @override + State createState() => _FormularioCredencialesState(correoController: correoController, contraseniaController: contraseniaController); +} + +class _FormularioCredencialesState extends State { + + final TextEditingController correoController; + final TextEditingController contraseniaController; + + _FormularioCredencialesState({ required this.correoController, required this.contraseniaController }); + + @override + Widget build(BuildContext context) { + return AutofillGroup( + child: Column( + children: [ + LoginTextFormField( + controller: correoController, + hintText: 'nombre@utem.cl', + labelText: 'Correo UTEM', + textCapitalization: TextCapitalization.none, + keyboardType: TextInputType.emailAddress, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(" ")), + ], + icon: Icons.person, + autofillHints: [AutofillHints.username], + validator: (String value) { + if (value.isEmpty) { + return 'Debe ingresar un correo UTEM'; + } else if (value.contains("@") && + !value.endsWith("@utem.cl")) { + return 'Debe ingresar un correo UTEM'; + } + }, + ), + LoginTextFormField( + controller: contraseniaController, + hintText: '• • • • • • • • •', + labelText: 'Contraseña', + textCapitalization: TextCapitalization.none, + icon: Icons.lock, + obscureText: true, + autofillHints: [AutofillHints.password], + validator: (String value) { + if (value.isEmpty) { + return 'Debe ingresar una contraseña'; + } + }, + ) + ], + ), + ); + } +} \ No newline at end of file From 1b4f6ecf251fc5a3885379b1743603753119b8bf Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 16 Nov 2023 08:41:30 -0300 Subject: [PATCH 010/194] patch: reduccion de login_screen, acerca_dialog y reparado algunos widgets. * Se ha reducido login_screen a distintos widgets * Se ha reducido acerca_dialog a distintos widgets * Se han reparado algunos widgets que configure como stateless siendo stateful. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/login_screen/login_screen.dart | 139 ++--------------- lib/services/update_service.dart | 46 ++++++ lib/widgets/acerca/acerca_screen.dart | 4 +- lib/widgets/acerca/dialog/acerca_dialog.dart | 29 +--- .../dialog/acerca_dialog_action_button.dart | 44 ++++++ .../notas_tab/labeled_nota_display.dart | 95 ++++++------ .../notas_tab/nota_final_display.dart | 27 ++-- .../asignatura/notas_tab/notas_display.dart | 143 +++++++++--------- lib/widgets/login_screen/creditos_app.dart | 4 +- .../login_screen/formulario_credenciales.dart | 17 +-- lib/widgets/login_screen/login_button.dart | 98 ++++++++++++ 11 files changed, 352 insertions(+), 294 deletions(-) create mode 100644 lib/services/update_service.dart create mode 100644 lib/widgets/acerca/dialog/acerca_dialog_action_button.dart create mode 100644 lib/widgets/login_screen/login_button.dart diff --git a/lib/screens/login_screen/login_screen.dart b/lib/screens/login_screen/login_screen.dart index 25c2027..7d0a4e8 100644 --- a/lib/screens/login_screen/login_screen.dart +++ b/lib/screens/login_screen/login_screen.dart @@ -1,26 +1,23 @@ -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/helpers/snackbars.dart'; -import 'package:mi_utem/models/usuario.dart'; -import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/auth_service.dart'; -import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; -import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; -import 'package:mi_utem/widgets/dialogs/not_ready_dialog.dart'; -import 'package:mi_utem/widgets/loading_dialog.dart'; +import 'package:mi_utem/services/update_service.dart'; import 'package:mi_utem/widgets/login_screen/creditos_app.dart'; import 'package:mi_utem/widgets/login_screen/formulario_credenciales.dart'; +import 'package:mi_utem/widgets/login_screen/login_button.dart'; import 'package:video_player/video_player.dart'; part '_background.dart'; class LoginScreen extends StatefulWidget { + + final TextEditingController _correoController = TextEditingController(); + final TextEditingController _contraseniaController = TextEditingController(); + + final GlobalKey _formKey = GlobalKey(); + LoginScreen({Key? key}) : super(key: key); @override @@ -28,10 +25,6 @@ class LoginScreen extends StatefulWidget { } class _LoginScreenState extends State { - TextEditingController correoController = TextEditingController(); - TextEditingController contraseniaController = TextEditingController(); - - GlobalKey _formKey = GlobalKey(); @override void initState() { @@ -47,48 +40,10 @@ class _LoginScreenState extends State { ), ); - correoController.text = ""; - contraseniaController.text = ""; - - SchedulerBinding.instance.addPostFrameCallback((_) { - _checkAndPerformUpdate(); - }); - } - - Future _checkAndPerformUpdate() async { - /* try { - VersionStatus status = - await NewVersion(context: context).getVersionStatus(); - print("status.localVersion ${status.localVersion}"); - print("status.storeVersion ${status.storeVersion}"); - - var localVersion = status.localVersion.split("."); - var storeVersion = status.storeVersion.split("."); - if (storeVersion[0].compareTo(localVersion[0]) > 0) { - if (Platform.isAndroid) { - AppUpdateInfo info = await InAppUpdate.checkForUpdate(); - - if (info.updateAvailable == true) { - await InAppUpdate.performImmediateUpdate(); - } - } - } else if (storeVersion[1].compareTo(localVersion[1]) > 0) { - if (Platform.isAndroid) { - AppUpdateInfo info = await InAppUpdate.checkForUpdate(); - - if (info.updateAvailable == true) { - await InAppUpdate.startFlexibleUpdate(); - await InAppUpdate.completeFlexibleUpdate(); - } - } - } else if (storeVersion[2].compareTo(localVersion[2]) > 0) { - print("MINOR"); - } + widget._correoController.text = ""; + widget._contraseniaController.text = ""; - return; - } catch (error) { - print("_checkAndPerformUpdate Error: ${error.toString()}"); - } */ + UpdateService(); } @override @@ -109,7 +64,7 @@ class _LoginScreenState extends State { ), child: IntrinsicHeight( child: Form( - key: _formKey, + key: widget._formKey, child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -122,21 +77,15 @@ class _LoginScreenState extends State { children: [ Hero( tag: 'utemLogo', - child: Image.asset( - 'assets/images/utem_logo_color_blanco.png', - width: 250, - ), + child: Image.asset('assets/images/utem_logo_color_blanco.png', width: 250), ), ], ), ), ), Container(height: constraints.maxHeight * 0.1), - FormularioCredenciales(correoController: correoController, contraseniaController: contraseniaController), - TextButton( - onPressed: () => _login(), - child: Text("Iniciar"), - ), + FormularioCredenciales(correoController: widget._correoController, contraseniaController: widget._contraseniaController), + LoginButton(correoController: widget._correoController, contraseniaController: widget._contraseniaController, formKey: widget._formKey), Container(height: constraints.maxHeight * 0.1), if (!isKeyboardVisible) CreditosApp(), @@ -153,62 +102,4 @@ class _LoginScreenState extends State { ), ); } - - Future _login() async { - final correo = correoController.text; - final contrasenia = contraseniaController.text; - - if (correo == "error@utem.cl") { - Get.dialog(MonkeyErrorDialog()); - } else if (correo == "test@utem.cl" && contrasenia == "test") { - showDefaultSnackbar( - "Error", - "Usuario o contraseña incorrecta", - ); - } else { - if (_formKey.currentState?.validate() ?? false) { - Get.dialog( - LoadingDialog(), - barrierDismissible: false, - ); - - try { - bool esPrimeraVez = await AuthService.esPrimeraVez(); - Usuario usuario = await AuthService.login(correo, contrasenia, true); - - AnalyticsService.logEvent('login'); - AnalyticsService.setUser(usuario); - - Get.toNamed(Routes.home); - - if (esPrimeraVez || true) { - Get.dialog(AcercaDialog()); - } - } on DioError catch (e) { - print(e.message); - Get.back(); - if (e.response?.statusCode == 403) { - if (e.response?.data["codigoInterno"]?.toString() == "4") { - Get.dialog(NotReadyDialog()); - } else { - showDefaultSnackbar("Error", "Usuario o contraseña incorrecta"); - } - } else if (e.response?.statusCode != null && e.response!.statusCode.toString().startsWith("5")) { - print(e.response?.data); - Get.dialog(MonkeyErrorDialog()); - } else { - print(e.response?.data); - showDefaultSnackbar("Error", "Ocurrió un error inesperado 😢"); - } - } catch (e) { - print(e.toString()); - Get.back(); - showDefaultSnackbar( - "Error", - "Ocurrió un error inesperado 😢", - ); - } - } - } - } } diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart new file mode 100644 index 0000000..2f9c8c7 --- /dev/null +++ b/lib/services/update_service.dart @@ -0,0 +1,46 @@ +import 'package:flutter/scheduler.dart'; + +class UpdateService { + + UpdateService(){ + SchedulerBinding.instance.addPostFrameCallback((_) => _checkAndPerformUpdate()); + } + + /* try { + VersionStatus status = + await NewVersion(context: context).getVersionStatus(); + print("status.localVersion ${status.localVersion}"); + print("status.storeVersion ${status.storeVersion}"); + + var localVersion = status.localVersion.split("."); + var storeVersion = status.storeVersion.split("."); + if (storeVersion[0].compareTo(localVersion[0]) > 0) { + if (Platform.isAndroid) { + AppUpdateInfo info = await InAppUpdate.checkForUpdate(); + + if (info.updateAvailable == true) { + await InAppUpdate.performImmediateUpdate(); + } + } + } else if (storeVersion[1].compareTo(localVersion[1]) > 0) { + if (Platform.isAndroid) { + AppUpdateInfo info = await InAppUpdate.checkForUpdate(); + + if (info.updateAvailable == true) { + await InAppUpdate.startFlexibleUpdate(); + await InAppUpdate.completeFlexibleUpdate(); + } + } + } else if (storeVersion[2].compareTo(localVersion[2]) > 0) { + print("MINOR"); + } + + return; + } catch (error) { + print("_checkAndPerformUpdate Error: ${error.toString()}"); + } */ + + Future _checkAndPerformUpdate() async { + + } +} \ No newline at end of file diff --git a/lib/widgets/acerca/acerca_screen.dart b/lib/widgets/acerca/acerca_screen.dart index 7a05433..55971c7 100644 --- a/lib/widgets/acerca/acerca_screen.dart +++ b/lib/widgets/acerca/acerca_screen.dart @@ -15,9 +15,7 @@ class AcercaScreen extends StatelessWidget { return Scaffold( backgroundColor: Colors.grey[200], appBar: CustomAppBar( - title: Text( - "Acerca de Mi UTEM", - ), + title: Text("Acerca de Mi UTEM"), ), body: SingleChildScrollView( child: Padding( diff --git a/lib/widgets/acerca/dialog/acerca_dialog.dart b/lib/widgets/acerca/dialog/acerca_dialog.dart index 9a74f48..fab64e0 100644 --- a/lib/widgets/acerca/dialog/acerca_dialog.dart +++ b/lib/widgets/acerca/dialog/acerca_dialog.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_aplicacion_content.dart'; +import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog_action_button.dart'; class AcercaDialog extends StatefulWidget { AcercaDialog({ @@ -76,33 +76,10 @@ class _AcercaDialogState extends State { preTitulo: "Antes de empezar...", titulo: "Bienvenido a Mi UTEM", ), - Wrap( - spacing: 10, - runSpacing: 10, - crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - TextButton( - child: Text( - _isActive - ? "Podrás cerrar en $_timeLeft" - : "Saber más", - style: TextStyle(color: Colors.white), - ), - onPressed: () { - if (!_isActive) { - Get.back(); - Get.toNamed(Routes.about); - } - } - ), - ], - ), + AcercaDialogActionButton(isActive: _isActive, timeLeft: _timeLeft), if (!_isActive) OutlinedButton( - child: Text( - "Cerrar", + child: Text("Cerrar", style: TextStyle(color: Get.theme.primaryColor), ), onPressed: () { diff --git a/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart b/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart new file mode 100644 index 0000000..26db494 --- /dev/null +++ b/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../config/routes.dart'; + +class AcercaDialogActionButton extends StatefulWidget { + + final bool isActive; + final int timeLeft; + + const AcercaDialogActionButton({ + Key? key, + required this.isActive, + required this.timeLeft, + }) : super(key: key); + + @override + State createState() => _AcercaDialogActionButtonState(); +} + +class _AcercaDialogActionButtonState extends State { + + @override + Widget build(BuildContext context) => Wrap( + spacing: 10, + runSpacing: 10, + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + TextButton( + child: Text(widget.isActive ? "Podrás cerrar en ${widget.timeLeft}" : "Saber más", + style: TextStyle(color: Colors.white), + ), + onPressed: () { + if (!widget.isActive) { + Get.back(); + Get.toNamed(Routes.about); + } + } + ), + ], + ); +} \ No newline at end of file diff --git a/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart b/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart index 76d228a..16dab8f 100644 --- a/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart +++ b/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart @@ -1,54 +1,55 @@ - import 'package:flutter/material.dart'; - import 'package:flutter_masked_text/flutter_masked_text.dart'; - import 'package:mi_utem/themes/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_masked_text/flutter_masked_text.dart'; +import 'package:mi_utem/themes/theme.dart'; - class LabeledNotaDisplayWidget extends StatelessWidget { +class LabeledNotaDisplayWidget extends StatefulWidget { + final String _label; + final num? _nota; + final String? _hint; - final String label; - final num? nota; - final String? hint; + LabeledNotaDisplayWidget({required String label, num? nota, String? hint}) + : _label = label, + _nota = nota, + _hint = hint; - LabeledNotaDisplayWidget({ - Key? key, - required this.label, - this.nota, - this.hint, - }) : super(key: key); + @override + _LabeledNotaDisplayWidgetState createState() => + _LabeledNotaDisplayWidgetState(); +} - @override - Widget build(BuildContext context) { - final notaController = MaskedTextController( - mask: '0.0', - text: nota?.toStringAsFixed(1) ?? "", - ); +class _LabeledNotaDisplayWidgetState extends State { + @override + Widget build(BuildContext context) { + final notaController = MaskedTextController( + mask: '0.0', + text: widget._nota?.toStringAsFixed(1) ?? "", + ); - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(label, - style: TextStyle(fontSize: 16), - ), - Container( - width: 60, - margin: EdgeInsets.only(left: 15), - child: TextField( - controller: notaController, - textAlign: TextAlign.center, - enabled: false, - decoration: InputDecoration( - hintText: hint, - disabledBorder: MainTheme.theme - .inputDecorationTheme.border! - .copyWith(borderSide: BorderSide(color: Colors.transparent)), - ), - keyboardType: - TextInputType.numberWithOptions( - decimal: true, - ), + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget._label, + style: TextStyle(fontSize: 16), + ), + Container( + width: 60, + margin: EdgeInsets.only(left: 15), + child: TextField( + controller: notaController, + textAlign: TextAlign.center, + enabled: false, + decoration: InputDecoration( + hintText: widget._hint, + disabledBorder: MainTheme.theme.inputDecorationTheme.border! + .copyWith(borderSide: BorderSide(color: Colors.transparent)), + ), + keyboardType: TextInputType.numberWithOptions( + decimal: true, ), ), - ], - ); - } - - } \ No newline at end of file + ), + ], + ); + } +} diff --git a/lib/widgets/asignatura/notas_tab/nota_final_display.dart b/lib/widgets/asignatura/notas_tab/nota_final_display.dart index 35a7da8..527c187 100644 --- a/lib/widgets/asignatura/notas_tab/nota_final_display.dart +++ b/lib/widgets/asignatura/notas_tab/nota_final_display.dart @@ -1,27 +1,32 @@ import 'package:flutter/material.dart'; -class NotaFinalDisplayWidget extends StatelessWidget { +class NotaFinalDisplayWidget extends StatefulWidget { + final num? _notaFinal; + final String? _estado; - final num? notaFinal; - final String? estado; + NotaFinalDisplayWidget({Key? key, num? notaFinal, String? estado}) + : _notaFinal = notaFinal, + _estado = estado, + super(key: key); - NotaFinalDisplayWidget({ - Key? key, - this.notaFinal, - this.estado, - }) : super(key: key); + @override + _NotaFinalDisplayWidgetState createState() => _NotaFinalDisplayWidgetState(); +} +class _NotaFinalDisplayWidgetState extends State { @override Widget build(BuildContext context) { return Column( children: [ - Text(notaFinal?.toStringAsFixed(1) ?? "S/N", + Text( + widget._notaFinal?.toStringAsFixed(1) ?? "S/N", style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, ), ), - Text(estado ?? "---", + Text( + widget._estado ?? "---", style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -30,4 +35,4 @@ class NotaFinalDisplayWidget extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/widgets/asignatura/notas_tab/notas_display.dart b/lib/widgets/asignatura/notas_tab/notas_display.dart index 8786058..c0c16ef 100644 --- a/lib/widgets/asignatura/notas_tab/notas_display.dart +++ b/lib/widgets/asignatura/notas_tab/notas_display.dart @@ -1,76 +1,79 @@ - import 'package:flutter/material.dart'; - import 'package:mi_utem/widgets/asignatura/notas_tab/labeled_nota_display.dart'; - import 'package:mi_utem/widgets/asignatura/notas_tab/nota_final_display.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/widgets/asignatura/notas_tab/labeled_nota_display.dart'; +import 'package:mi_utem/widgets/asignatura/notas_tab/nota_final_display.dart'; - class NotasDisplayWidget extends StatelessWidget { +class NotasDisplayWidget extends StatefulWidget { + final num? _notaFinal; + final num? _notaExamen; + final num? _notaPresentacion; + final String? _estado; + final Color? _colorPorEstado; - final num? notaFinal; - final num? notaExamen; - final num? notaPresentacion; - final String? estado; - final Color? colorPorEstado; + NotasDisplayWidget({ + notaFinal, + notaExamen, + notaPresentacion, + estado, + colorPorEstado, + }) : _notaFinal = notaFinal, + _notaExamen = notaExamen, + _notaPresentacion = notaPresentacion, + _estado = estado, + _colorPorEstado = colorPorEstado; - NotasDisplayWidget({ - Key? key, - this.notaFinal, - this.notaExamen, - this.notaPresentacion, - this.estado, - this.colorPorEstado, - }) : super(key: key); + @override + State createState() => NotasDisplayWidgetState(); +} - - @override - Widget build(BuildContext context) { - return Card( - child: Row( - children: [ - Container( - height: 130, - width: 10, - color: colorPorEstado, - ), - Expanded( - child: Container( - padding: EdgeInsets.fromLTRB(15, 20, 20, 20), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - NotaFinalDisplayWidget( - notaFinal: notaFinal, - estado: estado, - ), - Container(width: 10), - Container( - height: 80, - width: 0.5, - color: Colors.grey, - ), - Container(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - LabeledNotaDisplayWidget( - label: "Examen", - nota: notaExamen, - ), - Container(height: 10), - LabeledNotaDisplayWidget( - label: "Presentación", - nota: notaPresentacion, - hint: "--" - ), - ], - ), - ], - ), +class NotasDisplayWidgetState extends State { + @override + Widget build(BuildContext context) { + return Card( + child: Row( + children: [ + Container( + height: 130, + width: 10, + color: widget._colorPorEstado, + ), + Expanded( + child: Container( + padding: EdgeInsets.fromLTRB(15, 20, 20, 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + NotaFinalDisplayWidget( + notaFinal: widget._notaFinal, + estado: widget._estado, + ), + Container(width: 10), + Container( + height: 80, + width: 0.5, + color: Colors.grey, + ), + Container(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + LabeledNotaDisplayWidget( + label: "Examen", + nota: widget._notaExamen, + ), + Container(height: 10), + LabeledNotaDisplayWidget( + label: "Presentación", + nota: widget._notaPresentacion, + hint: "--"), + ], + ), + ], ), ), - ], - ), - ); - } - - - } \ No newline at end of file + ), + ], + ), + ); + } +} diff --git a/lib/widgets/login_screen/creditos_app.dart b/lib/widgets/login_screen/creditos_app.dart index 71c5247..c28c3cb 100644 --- a/lib/widgets/login_screen/creditos_app.dart +++ b/lib/widgets/login_screen/creditos_app.dart @@ -4,8 +4,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; +import '../../config/routes.dart'; import '../../services/remote_config/remote_config.dart'; class CreditosApp extends StatelessWidget { @@ -38,7 +38,7 @@ class CreditosApp extends StatelessWidget { data: _creditText, ), onTap: () { - Get.dialog(AcercaDialog()); + Get.toNamed(Routes.about); }, ), ), diff --git a/lib/widgets/login_screen/formulario_credenciales.dart b/lib/widgets/login_screen/formulario_credenciales.dart index c8328d8..58e1d91 100644 --- a/lib/widgets/login_screen/formulario_credenciales.dart +++ b/lib/widgets/login_screen/formulario_credenciales.dart @@ -5,30 +5,25 @@ import '../login_text_form_field.dart'; class FormularioCredenciales extends StatefulWidget { - final TextEditingController correoController; - final TextEditingController contraseniaController; + final TextEditingController _correoController; + final TextEditingController _contraseniaController; - FormularioCredenciales({ required this.correoController, required this.contraseniaController }); + FormularioCredenciales({ required TextEditingController correoController, required TextEditingController contraseniaController }) : _contraseniaController = contraseniaController, _correoController = correoController; @override - State createState() => _FormularioCredencialesState(correoController: correoController, contraseniaController: contraseniaController); + State createState() => _FormularioCredencialesState(); } class _FormularioCredencialesState extends State { - final TextEditingController correoController; - final TextEditingController contraseniaController; - - _FormularioCredencialesState({ required this.correoController, required this.contraseniaController }); - @override Widget build(BuildContext context) { return AutofillGroup( child: Column( children: [ LoginTextFormField( - controller: correoController, + controller: widget._correoController, hintText: 'nombre@utem.cl', labelText: 'Correo UTEM', textCapitalization: TextCapitalization.none, @@ -48,7 +43,7 @@ class _FormularioCredencialesState extends State { }, ), LoginTextFormField( - controller: contraseniaController, + controller: widget._contraseniaController, hintText: '• • • • • • • • •', labelText: 'Contraseña', textCapitalization: TextCapitalization.none, diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart new file mode 100644 index 0000000..849e78b --- /dev/null +++ b/lib/widgets/login_screen/login_button.dart @@ -0,0 +1,98 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/helpers/snackbars.dart'; +import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; +import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; +import 'package:mi_utem/widgets/dialogs/not_ready_dialog.dart'; +import 'package:mi_utem/widgets/loading_dialog.dart'; + + +class LoginButton extends StatefulWidget { + + final TextEditingController _correoController; + final TextEditingController _contraseniaController; + + final GlobalKey _formKey; + + LoginButton({ + required TextEditingController correoController, + required TextEditingController contraseniaController, + required GlobalKey formKey, + }) : + _correoController = correoController, + _contraseniaController = contraseniaController, + _formKey = formKey; + + @override + _LoginButtonState createState() => _LoginButtonState(); + +} + +class _LoginButtonState extends State { + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: () => _login(), + child: Text("Iniciar"), + ); + } + + Future _login() async { + final correo = widget._correoController.text; + final contrasenia = widget._contraseniaController.text; + + if (correo == "error@utem.cl") { + Get.dialog(MonkeyErrorDialog()); + return; + } else if (correo == "test@utem.cl" && contrasenia == "test") { + showDefaultSnackbar("Error", "Usuario o contraseña incorrecta"); + return; + } + + if(widget._formKey.currentState?.validate() == false) { + return; + } + + Get.dialog(LoadingDialog(), barrierDismissible: false); + + try { + bool esPrimeraVez = await AuthService.esPrimeraVez(); + Usuario usuario = await AuthService.login(correo, contrasenia, true); + + AnalyticsService.logEvent('login'); + AnalyticsService.setUser(usuario); + + Get.toNamed(Routes.home); + + if (esPrimeraVez) { + Get.dialog(AcercaDialog()); + } + } on DioError catch (e) { + print(e.message); + Get.back(); + if (e.response?.statusCode == 403) { + if (e.response?.data["codigoInterno"]?.toString() == "4") { + Get.dialog(NotReadyDialog()); + } else { + showDefaultSnackbar("Error", "Usuario o contraseña incorrecta"); + } + } else if (e.response?.statusCode != null && e.response!.statusCode.toString().startsWith("5")) { + print(e.response?.data); + Get.dialog(MonkeyErrorDialog()); + } else { + print(e.response?.data); + showDefaultSnackbar("Error", "Ocurrió un error inesperado 😢"); + } + } catch (e) { + print(e.toString()); + Get.back(); + showDefaultSnackbar("Error", "Ocurrió un error inesperado 😢"); + } + } +} \ No newline at end of file From b351354f408b87098be31fbc180176aca0f8923a Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:04:45 -0300 Subject: [PATCH 011/194] =?UTF-8?q?wip:=20trabajando=20en=20actualizaci?= =?UTF-8?q?=C3=B3n=20dentro=20de=20la=20app.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/update_service.dart | 18 ++++++++++++++++-- pubspec.lock | 8 ++++++++ pubspec.yaml | 3 ++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index 2f9c8c7..6013ba0 100644 --- a/lib/services/update_service.dart +++ b/lib/services/update_service.dart @@ -1,5 +1,14 @@ -import 'package:flutter/scheduler.dart'; +import 'dart:io'; +import 'package:flutter/scheduler.dart'; +import 'package:in_app_update/in_app_update.dart'; + +/* + * Clase que se encarga de verificar si hay una nueva versión de la aplicación + * y de actualizarla si es necesario. + * + * Para iOS muestra una notificación de que hay una nueva versión disponible. + */ class UpdateService { UpdateService(){ @@ -41,6 +50,11 @@ class UpdateService { } */ Future _checkAndPerformUpdate() async { - + if (Platform.isAndroid) { + final AppUpdateInfo appUpdateInfo = await InAppUpdate.checkForUpdate(); + if (appUpdateInfo.immediateUpdateAllowed) { + await InAppUpdate.performImmediateUpdate(); + } + } } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 0f9c2fd..80babec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -937,6 +937,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + in_app_update: + dependency: "direct main" + description: + name: in_app_update + sha256: b6ccb757281a96a4b18536f68fe2567aeca865134218719364212da8fe94615c + url: "https://pub.dev" + source: hosted + version: "4.2.2" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4a125cb..f680e9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Plataforma académica para estudiantes de la Universidad Tecnológi Metropolitana (UTEM) publish_to: none -version: 2.11.2 +version: 2.11.10 environment: sdk: ">=2.14.4 <3.0.0" @@ -73,6 +73,7 @@ dependencies: shared_preferences: ^2.0.20 background_fetch: ^1.1.5 flutter_uxcam: ^2.3.0 + in_app_update: ^4.2.2 dependency_overrides: qr: ^3.0.0 From 9c1e896639f427a72e8c384ab5740f28165f5f00 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:30:29 -0300 Subject: [PATCH 012/194] patch: reducido lista de asignaturas y usuario_screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Se redujo la lista de asignaturas a distintos widgets * Se mejoró la pantalla de usuario. * Se arreglaron algunos widgets que importaban archivos relativos en lugar de usar el package:mi_utem Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../asignatura/asignatura_resumen_tab.dart | 45 +--- .../asignatura/asignaturas_lista_screen.dart | 95 +------ lib/screens/usuario_screen.dart | 235 ++++++++---------- lib/widgets/acerca/club/acerca_club.dart | 4 +- .../club/acerca_club_desarrolladores.dart | 13 +- .../dialog/acerca_dialog_action_button.dart | 3 +- .../lista/asignatura_list_tile.dart | 53 ++++ .../asignatura/lista/lista_asignaturas.dart | 19 ++ .../lista/sin_asignaturas_mensaje.dart | 24 ++ lib/widgets/field_list_tile.dart | 46 ++-- lib/widgets/login_screen/creditos_app.dart | 5 +- .../login_screen/formulario_credenciales.dart | 3 +- 12 files changed, 244 insertions(+), 301 deletions(-) create mode 100644 lib/widgets/asignatura/lista/asignatura_list_tile.dart create mode 100644 lib/widgets/asignatura/lista/lista_asignaturas.dart create mode 100644 lib/widgets/asignatura/lista/sin_asignaturas_mensaje.dart diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/asignatura_resumen_tab.dart index 497f019..c93b79b 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/asignatura_resumen_tab.dart @@ -57,39 +57,18 @@ class AsignaturaResumenTab extends StatelessWidget { ), ], Divider(height: 5, indent: 20, endIndent: 20), - FieldListTile( - title: "Docente", - value: asignatura?.docente ?? "Sin docente", - // trailing: _asignatura.docente != null - // ? Badge( - // shape: BadgeShape.square, - // borderRadius: BorderRadius.circular(10), - // padding: EdgeInsets.symmetric( - // horizontal: 6, vertical: 3), - // elevation: 0, - // badgeContent: Text( - // 'Nuevo', - // style: TextStyle( - // color: Colors.white, - // fontSize: 10, - // fontWeight: FontWeight.bold, - // ), - // ), - // ) - // : null, - // onTap: _asignatura.docente != null - // ? () async { - // await Get.to(() => - // UsuarioScreen( - // tipo: 2, - // query: { - // "nombre": _asignatura.docente - // }, - // asignatura: widget.asignatura, - // ), - // ); - // } - // : null, + GestureDetector( + child: FieldListTile( + title: "Docente", + value: asignatura?.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ") ?? "Sin docente", // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. + ), + onTap: () async { + /*await Get.to(() => UsuarioScreen( + tipo: 2, + query: { "nombre": asignatura?.docente }, + asignatura: asignatura, + ));*/ + }, ), Divider(height: 5, indent: 20, endIndent: 20), if (asignatura?.tipoAsignatura != null) ...[ diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index cbad3f2..f343080 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -3,11 +3,10 @@ import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/controllers/asignatura/asignaturas_controller.dart'; -import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/widgets/asignatura/lista/lista_asignaturas.dart'; +import 'package:mi_utem/widgets/asignatura/lista/sin_asignaturas_mensaje.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; @@ -32,47 +31,16 @@ class AsignaturasListaScreen extends GetView { IconButton( icon: Icon(Mdi.calculator), tooltip: "Calculadora", - onPressed: () { - Get.toNamed( - Routes.calculadoraNotas, - ); - }, + onPressed: () => Get.toNamed(Routes.calculadoraNotas), ), ] : [], ), body: PullToRefresh( - onRefresh: () async { - await _onRefresh(); - }, + onRefresh: () async => await _onRefresh(), child: controller.obx( - (asignaturas) => asignaturas == null || asignaturas.isEmpty - ? Center( - child: SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: CustomErrorWidget( - emoji: "🤔", - title: "Parece que no se encontraron asignaturas", - ), - ), - ) - : ListView.builder( - physics: AlwaysScrollableScrollPhysics(), - itemBuilder: (BuildContext context, int i) { - Asignatura asignatura = asignaturas[i]; - return AsignaturaListTile(asignatura: asignatura); - }, - itemCount: asignaturas.length, - ), - onError: (error) => Center( - child: SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: CustomErrorWidget( - emoji: "🤔", - title: "Ocurrió un error al obtener las asignaturas", - ), - ), - ), + (asignaturas) => asignaturas == null || asignaturas.isEmpty ? SinAsignaturasMensaje(mensaje: "Parece que no se encontraron asignaturas.", emoji: "🤔") : ListaAsignaturas(asignaturas: asignaturas), + onError: (error) => SinAsignaturasMensaje(mensaje: "Ocurrió un error al obtener las asignaturas", emoji: "😢"), onLoading: Container( padding: EdgeInsets.all(20), child: Column( @@ -91,54 +59,3 @@ class AsignaturasListaScreen extends GetView { ); } } - -class AsignaturaListTile extends StatelessWidget { - const AsignaturaListTile({ - Key? key, - required this.asignatura, - }) : super(key: key); - - final Asignatura asignatura; - - @override - Widget build(BuildContext context) { - final ThemeData theme = MainTheme.theme; - - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - ), - child: Card( - child: InkWell( - onTap: () => Get.toNamed('${Routes.asignatura}/${asignatura.id}'), - child: Container( - padding: EdgeInsets.all(20), - width: double.infinity, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - asignatura.nombre!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleMedium, - textAlign: TextAlign.start, - ), - Container(height: 10), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(asignatura.codigo!), - Text(asignatura.tipoHora!), - ], - ) - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index 33e7fdc..d8c2589 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -101,141 +101,47 @@ class _UsuarioScreenState extends State { List get _datosPersonales { List lista = []; - if (_usuario != null) { - if (_usuario!.nombre != null && _usuario!.nombre!.isNotEmpty) { - lista.add(ListTile( - title: Text( - "Nombre", - style: TextStyle( - color: Colors.grey, - ), - ), - subtitle: Text( - _usuario!.nombreCompleto!, - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), + if(_usuario == null) { + return lista; + } + + if (_usuario?.nombre?.isEmpty == false) { + lista.add(ListTile( + title: Text("Nombre", + style: TextStyle(color: Colors.grey), + ), + subtitle: Text(_usuario!.nombreCompleto!, + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, ), - )); - } else { - if (_usuario!.nombres != null && _usuario!.nombres!.isNotEmpty) { - lista.add( - ListTile( - title: Text( - "Nombres", - style: TextStyle( - color: Colors.grey, - ), - ), - subtitle: Text( - _usuario!.nombres!, - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), - ), + ), + )); + } else { + if (_usuario?.nombres?.isEmpty == false) { + lista.add( + ListTile( + title: Text("Nombres", + style: TextStyle(color: Colors.grey), ), - ); - } - - if (_usuario!.apellidos != null && _usuario!.apellidos!.isNotEmpty) { - lista.add(Divider(height: 1)); - lista.add( - ListTile( - title: Text( - "Apellidos", - style: TextStyle( - color: Colors.grey, - ), - ), - subtitle: Text( - _usuario!.apellidos!, - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), + subtitle: Text(_usuario!.nombres!, + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, ), ), - ); - } - } - - if (_usuario!.correoUtem != null && _usuario!.correoUtem!.isNotEmpty) { - lista.add(Divider(height: 1)); - lista.add(ListTile( - title: Text( - "Correo", - style: TextStyle( - color: Colors.grey, - ), - ), - onLongPress: widget.tipo != 0 - ? () async { - await FlutterClipboard.copy(_usuario!.correoUtem!); - Get.snackbar( - "¡Copiado!", - "Correo copiado al portapapeles", - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); - } - : null, - onTap: widget.tipo != 0 - ? () async { - await launchUrl( - Uri.parse( - "mailto:${_usuario!.correoUtem}", - ), - ); - } - : null, - subtitle: Text( - _usuario!.correoUtem ?? "", - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), ), - )); + ); } - if (_usuario!.correoPersonal != null && - _usuario!.correoPersonal!.isNotEmpty) { + + if (_usuario?.apellidos?.isEmpty == false) { lista.add(Divider(height: 1)); lista.add( ListTile( - title: Text( - "Correo", - style: TextStyle( - color: Colors.grey, - ), + title: Text("Apellidos", + style: TextStyle(color: Colors.grey), ), - onLongPress: widget.tipo != 0 - ? () async { - await FlutterClipboard.copy(_usuario!.correoPersonal!); - Get.snackbar( - "¡Copiado!", - "Correo copiado al portapapeles", - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); - } - : null, - onTap: widget.tipo != 0 - ? () async { - await launchUrl( - Uri.parse( - "mailto:${_usuario!.correoPersonal}", - ), - ); - } - : null, - subtitle: Text( - _usuario!.correoPersonal ?? "", + subtitle: Text(_usuario!.apellidos!, style: TextStyle( color: Colors.grey[900], fontSize: 18, @@ -244,25 +150,80 @@ class _UsuarioScreenState extends State { ), ); } + } - if (widget.tipo == 0 && _usuario!.rut != null) { - lista.add(Divider(height: 1)); - lista.add(ListTile( - title: Text( - "RUT", - style: TextStyle( - color: Colors.grey, - ), + if (_usuario?.correoUtem?.isEmpty == false) { + lista.add(Divider(height: 1)); + lista.add(ListTile( + title: Text("Correo Institucional", + style: TextStyle(color: Colors.grey), + ), + onLongPress: widget.tipo != 0 ? () async { + await FlutterClipboard.copy(_usuario!.correoUtem!); + Get.snackbar( + "¡Copiado!", + "Correo copiado al portapapeles", + colorText: Colors.white, + backgroundColor: Get.theme.primaryColor, + snackPosition: SnackPosition.BOTTOM, + margin: EdgeInsets.all(20), + ); + } : null, + onTap: widget.tipo != 0 ? () async { + await launchUrl(Uri.parse("mailto:${_usuario?.correoUtem ?? ""}")); + } : null, + subtitle: Text(_usuario!.correoUtem ?? "", + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, ), - subtitle: Text( - _usuario!.rut!.formateado(true), + ), + )); + } + if (_usuario?.correoPersonal?.isEmpty == false) { + lista.add(Divider(height: 1)); + lista.add( + ListTile( + title: Text("Correo Personal", + style: TextStyle(color: Colors.grey), + ), + onLongPress: widget.tipo != 0 ? () async { + await FlutterClipboard.copy(_usuario!.correoPersonal!); + Get.snackbar( + "¡Copiado!", + "Correo copiado al portapapeles", + colorText: Colors.white, + backgroundColor: Get.theme.primaryColor, + snackPosition: SnackPosition.BOTTOM, + margin: EdgeInsets.all(20), + ); + } : null, + onTap: widget.tipo != 0 ? () async { + await launchUrl(Uri.parse("mailto:${_usuario?.correoPersonal ?? ""}")); + } : null, + subtitle: Text(_usuario!.correoPersonal ?? "", style: TextStyle( color: Colors.grey[900], fontSize: 18, ), ), - )); - } + ), + ); + } + + if (widget.tipo == 0 && _usuario!.rut != null) { + lista.add(Divider(height: 1)); + lista.add(ListTile( + title: Text("RUT", + style: TextStyle(color: Colors.grey), + ), + subtitle: Text(_usuario!.rut!.formateado(true), + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + )); } return lista; diff --git a/lib/widgets/acerca/club/acerca_club.dart b/lib/widgets/acerca/club/acerca_club.dart index 18c16d9..218e978 100644 --- a/lib/widgets/acerca/club/acerca_club.dart +++ b/lib/widgets/acerca/club/acerca_club.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:mi_utem/widgets/default_network_image.dart'; -import '../../../services/remote_config/remote_config.dart'; -import '../../default_network_image.dart'; import 'acerca_club_redes.dart'; class AcercaClub extends StatelessWidget { diff --git a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart index 9f06b87..1e9dd22 100644 --- a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart +++ b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart @@ -3,15 +3,14 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:mi_utem/widgets/image_view_screen.dart'; +import 'package:mi_utem/widgets/profile_photo.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../config/routes.dart'; -import '../../../models/usuario.dart'; -import '../../../services/analytics_service.dart'; -import '../../../services/remote_config/remote_config.dart'; -import '../../image_view_screen.dart'; -import '../../profile_photo.dart'; - class AcercaClubDesarrolladores extends StatelessWidget { @override diff --git a/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart b/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart index 26db494..ac961dd 100644 --- a/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart +++ b/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; - -import '../../../config/routes.dart'; +import 'package:mi_utem/config/routes.dart'; class AcercaDialogActionButton extends StatefulWidget { diff --git a/lib/widgets/asignatura/lista/asignatura_list_tile.dart b/lib/widgets/asignatura/lista/asignatura_list_tile.dart new file mode 100644 index 0000000..2cbda6e --- /dev/null +++ b/lib/widgets/asignatura/lista/asignatura_list_tile.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/themes/theme.dart'; + +class AsignaturaListTile extends StatelessWidget { + + const AsignaturaListTile({ + Key? key, + required this.asignatura, + }) : super(key: key); + + final Asignatura asignatura; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10.0, + ), + child: Card( + child: InkWell( + onTap: () => Get.toNamed('${Routes.asignatura}/${asignatura.id}'), + child: Container( + padding: EdgeInsets.all(20), + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + asignatura.nombre!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: MainTheme.theme.textTheme.titleMedium, + textAlign: TextAlign.start, + ), + Container(height: 10), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(asignatura.codigo!), + Text(asignatura.tipoHora!), + ], + ) + ], + ), + ), + ), + ), + ); +} \ No newline at end of file diff --git a/lib/widgets/asignatura/lista/lista_asignaturas.dart b/lib/widgets/asignatura/lista/lista_asignaturas.dart new file mode 100644 index 0000000..5679e37 --- /dev/null +++ b/lib/widgets/asignatura/lista/lista_asignaturas.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/widgets/asignatura/lista/asignatura_list_tile.dart'; + +class ListaAsignaturas extends StatelessWidget { + final List asignaturas; + + const ListaAsignaturas({ + Key? key, + required this.asignaturas, + }) : super(key: key); + + @override + Widget build(BuildContext context) => ListView.builder( + physics: AlwaysScrollableScrollPhysics(), + itemBuilder: (BuildContext context, int i) => AsignaturaListTile(asignatura: asignaturas[i]), + itemCount: asignaturas.length, + ); +} diff --git a/lib/widgets/asignatura/lista/sin_asignaturas_mensaje.dart b/lib/widgets/asignatura/lista/sin_asignaturas_mensaje.dart new file mode 100644 index 0000000..a161909 --- /dev/null +++ b/lib/widgets/asignatura/lista/sin_asignaturas_mensaje.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/widgets/custom_error_widget.dart'; + +class SinAsignaturasMensaje extends StatelessWidget { + + final String mensaje, emoji; + + const SinAsignaturasMensaje({ + Key? key, + required this.mensaje, + required this.emoji, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Center( + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: CustomErrorWidget( + emoji: emoji, + title: mensaje, + ), + ), + ); +} \ No newline at end of file diff --git a/lib/widgets/field_list_tile.dart b/lib/widgets/field_list_tile.dart index 6074ca2..5f3bfd5 100644 --- a/lib/widgets/field_list_tile.dart +++ b/lib/widgets/field_list_tile.dart @@ -17,31 +17,25 @@ class FieldListTile extends StatelessWidget { final EdgeInsetsGeometry padding; @override - Widget build(BuildContext context) { - return Padding( - padding: padding, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title.toUpperCase(), - maxLines: 2, - style: Get.textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.bold, - ), - ), - Text( - value ?? "Sin información", - style: Get.textTheme.bodyMedium, - ), - ], - ), + Widget build(BuildContext context) => Padding( + padding: padding, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title.toUpperCase(), + maxLines: 2, + style: Get.textTheme.bodySmall!.copyWith(fontWeight: FontWeight.bold), + ), + Text(value ?? "Sin información", + style: Get.textTheme.bodyMedium, + ), + ], ), - ], - ), - ); - } + ), + ], + ), + ); } diff --git a/lib/widgets/login_screen/creditos_app.dart b/lib/widgets/login_screen/creditos_app.dart index c28c3cb..1b2fd64 100644 --- a/lib/widgets/login_screen/creditos_app.dart +++ b/lib/widgets/login_screen/creditos_app.dart @@ -4,9 +4,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:get/get.dart'; - -import '../../config/routes.dart'; -import '../../services/remote_config/remote_config.dart'; +import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/services/remote_config/remote_config.dart'; class CreditosApp extends StatelessWidget { diff --git a/lib/widgets/login_screen/formulario_credenciales.dart b/lib/widgets/login_screen/formulario_credenciales.dart index 58e1d91..19b3fbf 100644 --- a/lib/widgets/login_screen/formulario_credenciales.dart +++ b/lib/widgets/login_screen/formulario_credenciales.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import '../login_text_form_field.dart'; +import 'package:mi_utem/widgets/login_text_form_field.dart'; class FormularioCredenciales extends StatefulWidget { From 1dd2555867ede7a264d359d779da65e5b21e78e1 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 15 Dec 2023 12:57:30 -0300 Subject: [PATCH 013/194] patch: actualizados servicios de noticias y widgets * Se ha actualizado a nuestra API de noticias * Se han mejorado y reducido los widgets de noticias * Se ha cambiado el servicio de noticias para usar GetConnect Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/config/constants.dart | 6 +- lib/models/noticia.dart | 80 +++++-------------- lib/screens/main_screen.dart | 4 +- lib/services/noticias_service.dart | 37 ++------- lib/widgets/noticia_card.dart | 75 ----------------- lib/widgets/noticias/NoticiaCardWidget.dart | 77 ++++++++++++++++++ .../NoticiasCarruselWidget.dart} | 43 +++------- 7 files changed, 116 insertions(+), 206 deletions(-) delete mode 100644 lib/widgets/noticia_card.dart create mode 100644 lib/widgets/noticias/NoticiaCardWidget.dart rename lib/widgets/{noticias_carrusel.dart => noticias/NoticiasCarruselWidget.dart} (57%) diff --git a/lib/config/constants.dart b/lib/config/constants.dart index 08a447a..1568527 100644 --- a/lib/config/constants.dart +++ b/lib/config/constants.dart @@ -1,6 +1,8 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + class Constants { - static const String sentryDsn = - 'https://0af59b2ad2b44f4e8c9cad4ea8d5f32e@o507661.ingest.sentry.io/5599080'; + static const String sentryDsn = 'https://0af59b2ad2b44f4e8c9cad4ea8d5f32e@o507661.ingest.sentry.io/5599080'; static const String uxCamDevKey = '0y6p88obpgiug1g'; static const String uxCamProdKey = 'fxkjj5ulr7vb4yf'; + static String apiUrl = bool.fromEnvironment('dart.vm.product') ? 'https://api.exdev.cl' : (dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl'); } diff --git a/lib/models/noticia.dart b/lib/models/noticia.dart index 461b4e2..dfc1fa2 100644 --- a/lib/models/noticia.dart +++ b/lib/models/noticia.dart @@ -1,67 +1,25 @@ -import 'package:html/parser.dart'; - class Noticia { - int? id, featuredMediaId; - String? titulo, subtitulo, link; - FeaturedMedia? featuredMedia; - - - Noticia( - this.id, - this.titulo, - this.subtitulo, - this.link, - this.featuredMediaId - ); - - Noticia.empty() - : id = null, - titulo = "", - subtitulo = "", - link = "", - featuredMedia = FeaturedMedia.empty(), - featuredMediaId = null; - - factory Noticia.fromJson(Map json) { - return Noticia( - json['id'], - parse(json['title']['rendered']).body!.text.trim(), - parse(json['excerpt']['rendered']).body!.text.trim(), - json['link'], - json['featured_media'] - ); - } + int? id; + String? titulo, subtitulo, link, imagen; - static List fromJsonList(List json) { - List lista = []; - for (var elemento in json) { - lista.add(Noticia.fromJson(elemento)); - } - return lista; - } -} + Noticia({this.id, this.titulo, this.subtitulo, this.link, this.imagen}); -class FeaturedMedia { - int? id; - String? guid; - - - FeaturedMedia({ - this.id, - this.guid - }); + Noticia.empty() + : id = null, + titulo = "", + subtitulo = "", + link = "", + imagen = ""; - factory FeaturedMedia.empty() { - return FeaturedMedia( - id: null, - guid: "https://noticias.utem.cl/wp-content/uploads/2017/07/en-preparacion.jpg" - ); - } + factory Noticia.fromApiJson(Map json) => + Noticia( + id: json["id"], + titulo: json["titulo"], + subtitulo: json["subtitulo"], + link: json["link"], + imagen: json["imagen"], + ); - factory FeaturedMedia.fromJson(Map json) { - return FeaturedMedia( - id: json['id'], - guid: json['guid'] != null && json['guid']['rendered'] != null && json['guid']['rendered'] != "" ? json['guid']['rendered'] : "https://noticias.utem.cl/wp-content/uploads/2017/07/en-preparacion.jpg" - ); - } + static List fromApiJsonList(List json) => + json.map((e) => Noticia.fromApiJson(e)).toList(); } diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index bd0759d..7639dd7 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -14,7 +14,7 @@ import "package:mi_utem/services/review_service.dart"; import 'package:mi_utem/widgets/banners_section.dart'; import "package:mi_utem/widgets/custom_app_bar.dart"; import "package:mi_utem/widgets/custom_drawer.dart"; -import "package:mi_utem/widgets/noticias_carrusel.dart"; +import "package:mi_utem/widgets/noticias/NoticiasCarruselWidget.dart"; import "package:mi_utem/widgets/permisos_section.dart"; import "package:mi_utem/widgets/quick_menu_section.dart"; @@ -105,7 +105,7 @@ class _MainScreenState extends State { ), Container(height: 20), ], - NoticiasSection(), + NoticiasCarruselWidget(), ], ), ), diff --git a/lib/services/noticias_service.dart b/lib/services/noticias_service.dart index 3ae6931..aaee8e4 100644 --- a/lib/services/noticias_service.dart +++ b/lib/services/noticias_service.dart @@ -1,38 +1,11 @@ -import 'package:dio/dio.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/utils/dio_wordpress_client.dart'; -class NoticiasService { - static final Dio _dio = DioWordpressClient.initDio; +class NoticiasService extends GetConnect { - static Future> getNoticias() async { - String uri = "/posts"; + Future> getNoticias() async => + get("${Constants.apiUrl}/v1/noticias").then((response) => response.statusCode == 200 ? Noticia.fromApiJsonList(response.body) : []); - try { - Response response = await _dio.get(uri); - - List noticias = Noticia.fromJsonList(response.data); - - return await Future.wait(noticias.map((noticia) async { - String uri = '/media/${noticia.featuredMediaId}'; - try { - if (noticia.featuredMediaId != null && noticia.featuredMediaId != 0) { - Response response = await _dio.get(uri); - noticia.featuredMedia = FeaturedMedia.fromJson(response.data); - } else { - noticia.featuredMedia = FeaturedMedia.empty(); - } - return noticia; - } catch (e) { - print(e); - noticia.featuredMedia = FeaturedMedia.empty(); - return noticia; - } - }).toList()); - } on DioError catch (e) { - print(e.message); - throw e; - } - } } diff --git a/lib/widgets/noticia_card.dart b/lib/widgets/noticia_card.dart deleted file mode 100644 index a2c97f1..0000000 --- a/lib/widgets/noticia_card.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; -import 'package:mdi/mdi.dart'; - -class NoticiaCard extends StatelessWidget { - const NoticiaCard( - {Key? key, this.titulo, this.subtitulo, this.imagenUrl, this.onTap}) - : super(key: key); - - final String? titulo, subtitulo, imagenUrl; - final Function? onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 200, - width: 200, - child: Card( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => this.onTap!(), - borderRadius: BorderRadius.all(Radius.circular(15)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - imagenUrl != null - ? Image.network( - imagenUrl!, - height: 110, - fit: BoxFit.cover, - ) - : Container( - height: 110, - width: double.infinity, - color: Colors.grey, - child: Icon( - Mdi.imageOff, - color: Colors.white, - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - height: 70, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Spacer(), - Text( - titulo!, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Get.theme.textTheme.bodyLarge, - ), - - /* Text( - subtitulo, - overflow: TextOverflow.ellipsis, - style: Get.theme.textTheme.bodyText2, - maxLines: 2 - ) */ - Spacer(), - ], - ), - ) - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/widgets/noticias/NoticiaCardWidget.dart b/lib/widgets/noticias/NoticiaCardWidget.dart new file mode 100644 index 0000000..0793f0f --- /dev/null +++ b/lib/widgets/noticias/NoticiaCardWidget.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mdi/mdi.dart'; +import 'package:mi_utem/models/noticia.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class NoticiaCardWidget extends StatefulWidget { + + final Noticia _noticia; + + NoticiaCardWidget(this._noticia); + + @override + State createState() => _NoticiaCardWidgetState(); +} + +class _NoticiaCardWidgetState extends State { + + @override + Widget build(BuildContext context) => SizedBox( + height: 200, + width: 200, + child: Card( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + AnalyticsService.logEvent("noticia_card_tap"); + this._launchURL(widget._noticia.link!); + }, + borderRadius: BorderRadius.all(Radius.circular(15)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + widget._noticia.imagen != null ? Image.network(widget._noticia.imagen!, height: 110, fit: BoxFit.cover) : Container( + height: 110, + width: double.infinity, + color: Colors.grey, + child: Icon( + Mdi.imageOff, + color: Colors.white, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + height: 70, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Spacer(), + Text( + widget._noticia.titulo!, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Get.theme.textTheme.bodyLarge, + ), + Spacer(), + ], + ), + ) + ], + ), + ), + ), + ), + ); + + Future _launchURL(String url) async { + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + } else { + throw 'Could not launch $url'; + } + } +} diff --git a/lib/widgets/noticias_carrusel.dart b/lib/widgets/noticias/NoticiasCarruselWidget.dart similarity index 57% rename from lib/widgets/noticias_carrusel.dart rename to lib/widgets/noticias/NoticiasCarruselWidget.dart index fa0ec57..56eb8d0 100644 --- a/lib/widgets/noticias_carrusel.dart +++ b/lib/widgets/noticias/NoticiasCarruselWidget.dart @@ -2,21 +2,19 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/noticias_service.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; -import 'package:mi_utem/widgets/noticia_card.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:mi_utem/widgets/noticias/NoticiaCardWidget.dart'; -class NoticiasSection extends StatefulWidget { - NoticiasSection({Key? key}) : super(key: key); +class NoticiasCarruselWidget extends StatefulWidget { + NoticiasCarruselWidget({Key? key}) : super(key: key); @override - State createState() => _NoticiasSectionState(); + State createState() => _NoticiasCarruselWidgetState(); } -class _NoticiasSectionState extends State { +class _NoticiasCarruselWidgetState extends State { Future>? _noticiasFuture; @override @@ -25,18 +23,7 @@ class _NoticiasSectionState extends State { _noticiasFuture = _getNoticias(); } - Future> _getNoticias() async { - List noticias = await NoticiasService.getNoticias(); - return noticias; - } - - Future _launchURL(String url) async { - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } else { - throw 'Could not launch $url'; - } - } + Future> _getNoticias() async => await NoticiasService().getNoticias(); @override Widget build(BuildContext context) { @@ -46,8 +33,7 @@ class _NoticiasSectionState extends State { children: [ Container( padding: EdgeInsets.symmetric(horizontal: 20), - child: Text( - "Noticias".toUpperCase(), + child: Text("Noticias".toUpperCase(), style: Get.textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, ), @@ -58,9 +44,7 @@ class _NoticiasSectionState extends State { future: _noticiasFuture, builder: (context, snapshot) { if (snapshot.hasError) { - return CustomErrorWidget( - title: "Ocurrió un error al obtener las noticias", - error: snapshot.error); + return CustomErrorWidget(title: "Ocurrió un error al obtener las noticias", error: snapshot.error); } else { if (snapshot.hasData && snapshot.data!.length > 0) { List noticias = snapshot.data!; @@ -71,16 +55,7 @@ class _NoticiasSectionState extends State { viewportFraction: 0.5, initialPage: 0, ), - itemBuilder: (BuildContext context, int i, int rI) => - NoticiaCard( - titulo: noticias[i].titulo, - subtitulo: noticias[i].subtitulo, - imagenUrl: noticias[i].featuredMedia?.guid, - onTap: () { - AnalyticsService.logEvent("noticia_tap"); - _launchURL(noticias[i].link!); - }, - ), + itemBuilder: (BuildContext context, int i, int rI) => NoticiaCardWidget(noticias[i]), itemCount: noticias.length, ); } else { From 434a6e2cfdd2da1776994199cebb8ed7f7e0fc17 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 15 Dec 2023 16:26:40 -0300 Subject: [PATCH 014/194] patch: reducido calculadora de notas * Se redujo la calculadora de notas a varios widgets Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/calculadora_notas_screen.dart | 235 +----------------- .../calculadora_notas/DisplayNotasWidget.dart | 51 ++++ .../calculadora_notas/EditarNotasWidget.dart | 52 ++++ .../ModoSimulacionWidget.dart | 22 ++ .../NotaExamenDisplayWidget.dart | 52 ++++ .../NotaFinalDisplayWidget.dart | 24 ++ .../NotaPresentacionDisplayWidget.dart | 45 ++++ .../NotasCalculadoraWidget.dart | 40 +++ 8 files changed, 290 insertions(+), 231 deletions(-) create mode 100644 lib/widgets/calculadora_notas/DisplayNotasWidget.dart create mode 100644 lib/widgets/calculadora_notas/EditarNotasWidget.dart create mode 100644 lib/widgets/calculadora_notas/ModoSimulacionWidget.dart create mode 100644 lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart create mode 100644 lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart create mode 100644 lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart create mode 100644 lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index 3324e29..53a5fc4 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/controllers/calculator_controller.dart'; -import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/widgets/calculadora_notas/DisplayNotasWidget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/EditarNotasWidget.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:mi_utem/widgets/nota_list_item.dart'; class CalculadoraNotasScreen extends StatelessWidget { CalculadoraNotasScreen({ @@ -25,234 +22,10 @@ class CalculadoraNotasScreen extends StatelessWidget { body: ListView( padding: EdgeInsets.all(10), children: [ - Card( - child: Stack( - alignment: Alignment.center, - children: [ - Container( - padding: EdgeInsets.all(20), - width: double.infinity, - child: RotationTransition( - turns: AlwaysStoppedAnimation(-20 / 360), - child: Text( - "Modo simulación".toUpperCase(), - style: TextStyle( - color: Colors.grey[200], - fontWeight: FontWeight.bold, - fontSize: 25, - ), - textAlign: TextAlign.center, - ), - ), - ), - Container( - padding: EdgeInsets.symmetric(vertical: 20, horizontal: 30), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - children: [ - Obx( - () => Text( - controller.calculatedFinalGrade - ?.toStringAsFixed(1) ?? - "--", - style: TextStyle( - fontSize: 40, - fontWeight: FontWeight.bold, - ), - ), - ), - /* Text( - _asignatura!.estadoCalculado, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), */ - ], - ), - Container(width: 10), - Container( - height: 80, - width: 0.5, - color: Colors.grey, - ), - Container(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "Examen", - style: TextStyle(fontSize: 16), - ), - Container( - width: 80, - margin: EdgeInsets.only(left: 15), - child: Obx( - () => TextField( - controller: - controller.examGradeTextFieldController, - textAlign: TextAlign.center, - onChanged: (String value) { - controller.examGrade.value = - double.tryParse( - value.replaceAll(",", "."), - ); - }, - enabled: controller.canTakeExam, - decoration: InputDecoration( - hintText: controller - .minimumRequiredExamGrade - ?.toStringAsFixed(1) ?? - "", - filled: !controller.canTakeExam, - fillColor: Colors.grey.withOpacity(0.2), - disabledBorder: MainTheme - .theme.inputDecorationTheme.border! - .copyWith( - borderSide: BorderSide( - color: Colors.grey[300]!, - ), - ), - ), - keyboardType: - TextInputType.numberWithOptions( - decimal: true, - ), - ), - ), - ), - ], - ), - Container(height: 10), - Obx( - () => Row( - children: [ - Text( - "Pres.", - style: TextStyle(fontSize: 16), - ), - Container( - width: 80, - margin: EdgeInsets.only(left: 15), - child: TextField( - controller: TextEditingController( - text: controller - .calculatedPresentationGrade - ?.toStringAsFixed(1) ?? - "", - ), - textAlign: TextAlign.center, - enabled: false, - decoration: InputDecoration( - hintText: "Nota", - disabledBorder: MainTheme - .theme.inputDecorationTheme.border! - .copyWith( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - ), - keyboardType: - TextInputType.numberWithOptions( - decimal: true, - ), - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - Card( - child: Stack( - alignment: Alignment.center, - children: [ - Container( - padding: EdgeInsets.all(20), - width: double.infinity, - child: RotationTransition( - turns: AlwaysStoppedAnimation(-20 / 360), - child: Text( - "Modo simulación".toUpperCase(), - style: TextStyle( - color: Colors.grey[200], - fontWeight: FontWeight.bold, - fontSize: 25, - ), - textAlign: TextAlign.center, - ), - ), - ), - Container( - padding: EdgeInsets.all(20), - child: Column( - children: [ - Obx( - () => ListView.separated( - shrinkWrap: true, - physics: ClampingScrollPhysics(), - separatorBuilder: (context, index) => - SizedBox(height: 10), - itemBuilder: (context, i) { - REvaluacion evaluacion = - controller.partialGrades[i]; - return NotaListItem( - evaluacion: IEvaluacion.fromRemote(evaluacion), - editable: true, - gradeController: - controller.gradeTextFieldControllers[i], - percentageController: - controller.percentageTextFieldControllers[i], - onChanged: (evaluacion) { - controller.changeGradeAt(i, evaluacion); - }, - onDelete: () => _deleteGrade(controller, i), - ); - }, - itemCount: controller.partialGrades.length, - ), - ), - SizedBox(height: 16), - TextButton( - onPressed: () => _addGrade(controller), - child: Text("Agregar nota"), - ), - ], - ), - ), - ], - ), - ), + DisplayNotasWidget(calculatorController: controller), + EditarNotasWidget(calculatorController: controller) ], ), ); } - - void _deleteGrade(CalculatorController controller, int index) { - AnalyticsService.logEvent("calculator_delete_grade"); - controller.removeGradeAt(index); - } - - void _addGrade(CalculatorController controller) { - AnalyticsService.logEvent("calculator_add_grade"); - controller.addGrade( - IEvaluacion( - nota: null, - porcentaje: null, - ), - ); - } } diff --git a/lib/widgets/calculadora_notas/DisplayNotasWidget.dart b/lib/widgets/calculadora_notas/DisplayNotasWidget.dart new file mode 100644 index 0000000..f001304 --- /dev/null +++ b/lib/widgets/calculadora_notas/DisplayNotasWidget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/widgets/calculadora_notas/ModoSimulacionWidget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/NotaExamenDisplayWidget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/NotaFinalDisplayWidget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart'; + +class DisplayNotasWidget extends StatelessWidget { + final CalculatorController _calculatorController; + + const DisplayNotasWidget({ + Key? key, + required CalculatorController calculatorController, + }) : _calculatorController = calculatorController, + super(key: key); + + @override + Widget build(BuildContext context) => Card( + child: Stack( + alignment: Alignment.center, + children: [ + ModoSimulacionWidget(), + Container( + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 30), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + NotaFinalDisplayWidget(calculatorController: _calculatorController), + Container(width: 10), + Container( + height: 80, + width: 0.5, + color: Colors.grey, + ), + Container(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + NotaExamenDisplayWidget(calculatorController: _calculatorController), + Container(height: 10), + NotaPresentacionDisplayWidget(calculatorController: _calculatorController), + ], + ), + ], + ), + ), + ], + ), + ); +} diff --git a/lib/widgets/calculadora_notas/EditarNotasWidget.dart b/lib/widgets/calculadora_notas/EditarNotasWidget.dart new file mode 100644 index 0000000..9a008bd --- /dev/null +++ b/lib/widgets/calculadora_notas/EditarNotasWidget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/widgets/calculadora_notas/ModoSimulacionWidget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/NotasCalculadoraWidget.dart'; + +class EditarNotasWidget extends StatelessWidget { + final CalculatorController _calculatorController; + + const EditarNotasWidget({ + Key? key, + required CalculatorController calculatorController, + }) : _calculatorController = calculatorController, + super(key: key); + + @override + Widget build(BuildContext context) => Card( + child: Stack( + alignment: Alignment.center, + children: [ + ModoSimulacionWidget(), + Container( + padding: EdgeInsets.all(20), + child: Column( + children: [ + NotasCalculadoraWidget(calculatorController: _calculatorController, onDelete: (controller, index) => _deleteGrade(controller, index)), + SizedBox(height: 16), + TextButton( + onPressed: () => _addGrade(_calculatorController), + child: Text("Agregar nota"), + ), + ], + ), + ), + ], + ), + ); + + void _deleteGrade(CalculatorController controller, int index) { + AnalyticsService.logEvent("calculator_delete_grade"); + controller.removeGradeAt(index); + } + + void _addGrade(CalculatorController controller) { + AnalyticsService.logEvent("calculator_add_grade"); + controller.addGrade(IEvaluacion( + nota: null, + porcentaje: null, + )); + } +} diff --git a/lib/widgets/calculadora_notas/ModoSimulacionWidget.dart b/lib/widgets/calculadora_notas/ModoSimulacionWidget.dart new file mode 100644 index 0000000..9bd03a6 --- /dev/null +++ b/lib/widgets/calculadora_notas/ModoSimulacionWidget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class ModoSimulacionWidget extends StatelessWidget { + + @override + Widget build(BuildContext context) => Container( + padding: EdgeInsets.all(20), + width: double.infinity, + child: RotationTransition( + turns: AlwaysStoppedAnimation(-20 / 360), + child: Text( + "Modo simulación".toUpperCase(), + style: TextStyle( + color: Colors.grey[200], + fontWeight: FontWeight.bold, + fontSize: 25, + ), + textAlign: TextAlign.center, + ), + ), + ); +} diff --git a/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart b/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart new file mode 100644 index 0000000..766cd4c --- /dev/null +++ b/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/themes/theme.dart'; + +class NotaExamenDisplayWidget extends StatelessWidget { + final CalculatorController _calculatorController; + + const NotaExamenDisplayWidget({ + Key? key, + required CalculatorController calculatorController, + }) : _calculatorController = calculatorController, + super(key: key); + + @override + Widget build(BuildContext context) => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Examen", + style: TextStyle(fontSize: 16), + ), + Container( + width: 80, + margin: EdgeInsets.only(left: 15), + child: Obx(() => TextField( + controller: _calculatorController.examGradeTextFieldController, + textAlign: TextAlign.center, + onChanged: (String value) { + _calculatorController.examGrade.value = double.tryParse(value.replaceAll(",", ".")); + }, + enabled: _calculatorController.canTakeExam, + decoration: InputDecoration( + hintText: _calculatorController.minimumRequiredExamGrade?.toStringAsFixed(1) ?? "", + filled: !_calculatorController.canTakeExam, + fillColor: Colors.grey.withOpacity(0.2), + disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( + borderSide: BorderSide( + color: Colors.grey[300]!, + ), + ), + ), + keyboardType: + TextInputType.numberWithOptions( + decimal: true, + ), + ), + ), + ), + ], + ); +} diff --git a/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart b/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart new file mode 100644 index 0000000..3ded3ff --- /dev/null +++ b/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; + +class NotaFinalDisplayWidget extends StatelessWidget { + final CalculatorController _calculatorController; + + const NotaFinalDisplayWidget({ + Key? key, + required CalculatorController calculatorController, + }) : _calculatorController = calculatorController, + super(key: key); + + @override + Widget build(BuildContext context) => Column( + children: [ + Obx(() => + Text(_calculatorController.calculatedFinalGrade?.toStringAsFixed(1) ?? "--", + style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold), + ), + ), + ], + ); +} diff --git a/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart b/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart new file mode 100644 index 0000000..10e41c2 --- /dev/null +++ b/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/themes/theme.dart'; + +class NotaPresentacionDisplayWidget extends StatelessWidget { + final CalculatorController _calculatorController; + + const NotaPresentacionDisplayWidget({ + Key? key, + required CalculatorController calculatorController, + }) : _calculatorController = calculatorController, + super(key: key); + + @override + Widget build(BuildContext context) => Obx( + () => Row( + children: [ + Text( + "Pres.", + style: TextStyle(fontSize: 16), + ), + Container( + width: 80, + margin: EdgeInsets.only(left: 15), + child: TextField( + controller: TextEditingController(text: _calculatorController.calculatedPresentationGrade?.toStringAsFixed(1) ?? ""), + textAlign: TextAlign.center, + enabled: false, + decoration: InputDecoration( + hintText: "Nota", + disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + ), + keyboardType: + TextInputType.numberWithOptions(decimal: true), + ), + ), + ], + ), + ); +} diff --git a/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart b/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart new file mode 100644 index 0000000..dfc14b0 --- /dev/null +++ b/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/widgets/nota_list_item.dart'; + +class NotasCalculadoraWidget extends StatelessWidget { + final CalculatorController _calculatorController; + final Function onDelete; + + const NotasCalculadoraWidget({ + Key? key, + required CalculatorController calculatorController, + required this.onDelete, + }) : _calculatorController = calculatorController, + super(key: key); + + + @override + Widget build(BuildContext context) => Obx(() => ListView.separated( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + separatorBuilder: (context, index) => SizedBox(height: 10), + itemBuilder: (context, i) { + REvaluacion evaluacion = _calculatorController.partialGrades[i]; + return NotaListItem( + evaluacion: IEvaluacion.fromRemote(evaluacion), + editable: true, + gradeController: _calculatorController.gradeTextFieldControllers[i], + percentageController: _calculatorController.percentageTextFieldControllers[i], + onChanged: (evaluacion) { + _calculatorController.changeGradeAt(i, evaluacion); + }, + onDelete: () => Function.apply(onDelete, [_calculatorController, i]), + ); + }, + itemCount: _calculatorController.partialGrades.length, + ), + ); +} From 3a0fcc2cca0bc97dd1fc2d86d9e70b46b054857f Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:07:19 -0300 Subject: [PATCH 015/194] patch: cambio de versiones para funcionar con flutter v3.7.2 --- .env.example | 2 +- lib/controllers/carreras_controller.dart | 8 +- pubspec.lock | 94 ++++++++++++------------ pubspec.yaml | 12 +-- 4 files changed, 60 insertions(+), 56 deletions(-) diff --git a/.env.example b/.env.example index e66bfee..f301e49 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -MI_UTEM_API_DEBUG=https://api-mi-utem.herokuapp.com/ \ No newline at end of file +MI_UTEM_API_DEBUG=https://api.exdev.cl \ No newline at end of file diff --git a/lib/controllers/carreras_controller.dart b/lib/controllers/carreras_controller.dart index 20eb963..a26aa56 100644 --- a/lib/controllers/carreras_controller.dart +++ b/lib/controllers/carreras_controller.dart @@ -17,10 +17,12 @@ class CarrerasController extends GetxController { } void getCarreras() async { - final carreras = await CarreraService.getCarreras(forceRefresh: true); + try { + final carreras = await CarreraService.getCarreras(forceRefresh: true); - this.carreras.value = carreras; - _autoSelectCarreraActiva(carreras); + this.carreras.value = carreras; + _autoSelectCarreraActiva(carreras); + } catch(_) {} } void _autoSelectCarreraActiva(List carreras) { diff --git a/pubspec.lock b/pubspec.lock index 80babec..2347809 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.10.0" awesome_notifications: dependency: "direct main" description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.1" checked_yaml: dependency: transitive description: @@ -245,10 +245,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.17.0" community_charts_common: dependency: transitive description: @@ -357,18 +357,18 @@ packages: dependency: "direct main" description: name: extended_image - sha256: b4d72a27851751cfadaf048936d42939db7cd66c08fdcfe651eeaa1179714ee6 + sha256: "75e4b0ad0f8f63eed7935ff2506c809a670f5e6dd0f61304525879d53fc41a17" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "7.0.2" extended_image_library: dependency: transitive description: name: extended_image_library - sha256: "8bf87c0b14dcb59200c923a9a3952304e4732a0901e40811428834ef39018ee1" + sha256: "550743b43ab093aed35ef234500fcc7a304cbac1eca47b0cc991e07e88750758" url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.4.2" fake_async: dependency: transitive description: @@ -794,21 +794,29 @@ packages: source: hosted version: "0.15.4" http: - dependency: transitive + dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "0.13.6" http_client_helper: dependency: transitive description: name: http_client_helper - sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" + sha256: "14c6e756644339f561321dab021215475ba4779aa962466f59ccb3ecf66b36c3" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.0.4" + http_interceptor: + dependency: "direct main" + description: + name: http_interceptor + sha256: "5f3dde028e67789339c250252c09510a74aff21ce16b06d07d9096bda6582bab" + url: "https://pub.dev" + source: hosted + version: "1.0.2" http_parser: dependency: transitive description: @@ -957,10 +965,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.6.5" json_annotation: dependency: "direct overridden" description: @@ -1013,18 +1021,18 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.2.0" mdi: dependency: "direct main" description: @@ -1037,10 +1045,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.8.0" mime: dependency: transitive description: @@ -1085,10 +1093,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.8.2" path_drawing: dependency: transitive description: @@ -1309,26 +1317,26 @@ packages: dependency: "direct main" description: name: screenshot - sha256: "455284ff1f5b911d94a43c25e1385485cf6b4f288293eba68f15dad711c7b81c" + sha256: "30bb9fade6eb2578a1fc2e84f6b184141fc86883cda10988d4500ff00eb728e2" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "1.3.0" sentry: dependency: transitive description: name: sentry - sha256: "9cfd325611ab54b57d5e26957466823f05bea9d6cfcc8d48f11817b8bcedf0d1" + sha256: d2ee9c850d876d285f22e2e662f400ec2438df9939fe4acd5d780df9841794ce url: "https://pub.dev" source: hosted - version: "7.12.0" + version: "7.16.1" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "0cd7d622cb63c94fd1b2f87ab508e158b950bd281e2a80f327ebf73bb217eaf3" + sha256: "5b428c189c825f16fb14e9166529043f06b965d5b59bfc3a1415e39c082398c0" url: "https://pub.dev" source: hosted - version: "7.12.0" + version: "7.16.1" share_plus: dependency: "direct main" description: @@ -1426,10 +1434,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" sqflite: dependency: transitive description: @@ -1490,10 +1498,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.4.16" tint: dependency: transitive description: @@ -1646,22 +1654,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - web: - dependency: transitive - description: - name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 - url: "https://pub.dev" - source: hosted - version: "0.1.4-beta" win32: dependency: transitive description: name: win32 - sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "4.1.4" xdg_directories: dependency: transitive description: @@ -1687,5 +1687,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.13.0" + dart: ">=2.19.0 <3.0.0" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index f680e9b..21ebfa4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,10 +3,10 @@ description: Plataforma académica para estudiantes de la Universidad Tecnológi Metropolitana (UTEM) publish_to: none -version: 2.11.10 +version: 3.0.0 environment: - sdk: ">=2.14.4 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: awesome_notifications_core: ^0.8.1 @@ -15,7 +15,6 @@ dependencies: badges: ^2.0.2 barcode_widget: ^2.0.3 barcode_image: ^2.0.2 - cached_network_image: ^3.2.0 carousel_slider: ^4.0.0 community_charts_flutter: ^1.0.2 circular_profile_avatar: ^2.0.5 @@ -26,7 +25,6 @@ dependencies: dio_cache_interceptor: ^3.2.6 dio_cache_interceptor_hive_store: ^3.1.1 dotted_border: ^2.0.0+2 - extended_image: ^8.1.1 firebase_core: 2.16.0 firebase_analytics: 10.2.1 firebase_in_app_messaging: ^0.7.0+10 @@ -59,7 +57,6 @@ dependencies: qr_flutter: ^4.0.0 recase: ^4.0.0 responsive_framework: ^0.2.0 - screenshot: ^2.1.0 sentry_flutter: ^7.12.0 share_plus: ^7.2.1 simple_gesture_detector: ^0.2.0 @@ -74,6 +71,11 @@ dependencies: background_fetch: ^1.1.5 flutter_uxcam: ^2.3.0 in_app_update: ^4.2.2 + http: ^0.13.6 + http_interceptor: ^1.0.2 + screenshot: ^1.3.0 + extended_image: ^7.0.2 + cached_network_image: ^3.2.3 dependency_overrides: qr: ^3.0.0 From 7a580046fb93342001725e16560e0dc8f6888f06 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 12 Mar 2024 07:58:47 -0300 Subject: [PATCH 016/194] feat: .fvmrc para usar flutter version manager --- .fvmrc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..2b19323 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.7.12", + "flavors": {} +} \ No newline at end of file From 64b16c26bf54608bbb3ddf967132325b5696a447 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 12 Mar 2024 07:59:47 -0300 Subject: [PATCH 017/194] =?UTF-8?q?patch:=20agregada=20configuraci=C3=B3n?= =?UTF-8?q?=20de=20vscode=20para=20fvm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7100e0a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": ".fvm/versions/3.7.12" +} \ No newline at end of file From c0cae280760cbaae2bbbe3745813f6fb706d1e29 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:04:12 -0300 Subject: [PATCH 018/194] feat: se ordenaron muchas clases y se implementa nuevo sistema de servicios. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Se ordenaron muchas clases y widgets (ya no se cuales eran xD) * Se cambió a un nuevo sistema de servicios usando GetIt que usa servicios bajo demanda de manera "floja" (lazy). De esta forma se mejora el rendimiento. * Se cambia de dio a http como se recomienda en flutter.dev * Se agrega constante de "secureStorage" para un fácil acceso al storage encriptado. --- lib/config/constants.dart | 2 + lib/config/http_client.dart | 52 ++++ lib/config/router.dart | 106 -------- lib/config/routes.dart | 17 -- lib/config/secure_storage.dart | 3 + .../asignatura/asignatura_controller.dart | 62 ----- .../asignatura/asignaturas_controller.dart | 67 ----- lib/controllers/calculator_controller.dart | 209 -------------- lib/controllers/carreras_controller.dart | 50 ---- .../grades_changes_controller.dart | 249 ----------------- lib/controllers/horario_controller.dart | 16 +- lib/controllers/notification_controller.dart | 17 +- lib/controllers/qr_pass_controller.dart | 28 -- lib/controllers/qr_passes_controller.dart | 20 +- lib/main.dart | 38 +-- lib/models/evaluacion.dart | 60 ++--- lib/models/exceptions/custom_exception.dart | 35 +++ lib/models/grades.dart | 28 +- lib/models/permiso_covid.dart | 42 ++- lib/models/user/credential.dart | 25 ++ lib/models/user/rut.dart | 35 +++ lib/models/user/user.dart | 80 ++++++ .../asignatura/asignatura_detalle_screen.dart | 157 ++++++----- .../asignatura/asignatura_notas_tab.dart | 159 ++++++----- .../asignatura/asignaturas_lista_screen.dart | 117 +++++--- lib/screens/calculadora_notas_screen.dart | 24 +- lib/screens/credencial_screen.dart | 147 +++++----- lib/screens/docentes_screen.dart | 157 +++++------ lib/screens/login_screen/login_screen.dart | 105 ++------ lib/screens/main_screen.dart | 66 ++--- lib/screens/permiso_covid_screen.dart | 178 ++++++------ lib/screens/splash_screen.dart | 134 +++++---- lib/screens/usuario_screen.dart | 189 ++++++------- lib/services/analytics_service.dart | 102 ++----- lib/services/asignaturas_service.dart | 63 ----- lib/services/auth_service.dart | 157 ----------- lib/services/background_service.dart | 5 +- lib/services/carreras_service.dart | 27 -- lib/services/docentes_service.dart | 40 ++- lib/services/grades_service.dart | 40 --- lib/services/horarios_service.dart | 37 --- lib/services/noticias_service.dart | 11 - lib/services/perfil_service.dart | 109 -------- lib/services/permisos_covid_service.dart | 64 ----- .../implementations/asignaturas_service.dart | 73 +++++ .../implementations/auth_service.dart | 189 +++++++++++++ .../implementations/calculator_service.dart | 254 ++++++++++++++++++ .../implementations/carreras_service.dart | 66 +++++ .../implementations/credential_service.dart | 25 ++ .../implementations/grades_service.dart | 246 +++++++++++++++++ .../implementations/horario_service.dart | 39 +++ .../implementations/noticias_service.dart | 28 ++ .../implementations/qr_pass_service.dart | 72 +++++ .../interfaces/asignaturas_service.dart | 8 + lib/services_new/interfaces/auth_service.dart | 23 ++ .../interfaces/calculator_service.dart | 84 ++++++ .../interfaces/carreras_service.dart | 15 ++ .../interfaces/credential_service.dart | 10 + .../interfaces/grades_service.dart | 22 ++ .../interfaces/horario_service.dart | 6 + .../interfaces/noticias_service.dart | 6 + .../interfaces/qr_pass_service.dart | 8 + lib/services_new/service_manager.dart | 31 +++ lib/utils/dio_miutem_client.dart | 7 +- .../club/acerca_club_desarrolladores.dart | 91 +++---- .../dialog/acerca_dialog_action_button.dart | 23 +- .../lista/asignatura_list_tile.dart | 20 +- .../calculadora_notas/DisplayNotasWidget.dart | 26 +- .../calculadora_notas/EditarNotasWidget.dart | 28 +- .../ModoSimulacionWidget.dart | 11 +- .../NotaExamenDisplayWidget.dart | 54 ++-- .../NotaFinalDisplayWidget.dart | 28 +- .../NotaPresentacionDisplayWidget.dart | 33 +-- .../NotasCalculadoraWidget.dart | 56 ++-- lib/widgets/credencial_card.dart | 90 +++---- lib/widgets/custom_drawer.dart | 206 +++++++------- lib/widgets/custom_error_widget.dart | 27 +- lib/widgets/default_network_image.dart | 11 +- lib/widgets/dialogs/not_ready_dialog.dart | 120 ++++----- lib/widgets/loading_indicator.dart | 40 ++- .../login_screen/background.dart} | 16 +- lib/widgets/login_screen/creditos_app.dart | 61 +++-- .../login_screen/formulario_credenciales.dart | 82 +++--- lib/widgets/login_screen/login_button.dart | 85 +++--- lib/widgets/login_screen/login_form.dart | 101 +++++++ lib/widgets/login_text_form_field.dart | 3 + lib/widgets/nota_list_item.dart | 28 +- .../noticias/NoticiasCarruselWidget.dart | 93 ++++--- lib/widgets/permiso_card.dart | 10 +- lib/widgets/permisos_section.dart | 31 ++- lib/widgets/profile_photo.dart | 199 +++++++------- lib/widgets/pull_to_refresh.dart | 95 +++---- lib/widgets/quick_menu_card.dart | 28 +- lib/widgets/sad_dialog.dart | 8 +- pubspec.lock | 34 ++- pubspec.yaml | 3 + 96 files changed, 3170 insertions(+), 3112 deletions(-) create mode 100644 lib/config/http_client.dart delete mode 100644 lib/config/router.dart delete mode 100644 lib/config/routes.dart create mode 100644 lib/config/secure_storage.dart delete mode 100644 lib/controllers/asignatura/asignatura_controller.dart delete mode 100644 lib/controllers/asignatura/asignaturas_controller.dart delete mode 100644 lib/controllers/calculator_controller.dart delete mode 100644 lib/controllers/carreras_controller.dart delete mode 100644 lib/controllers/grades_changes_controller.dart delete mode 100644 lib/controllers/qr_pass_controller.dart create mode 100644 lib/models/exceptions/custom_exception.dart create mode 100644 lib/models/user/credential.dart create mode 100644 lib/models/user/rut.dart create mode 100644 lib/models/user/user.dart delete mode 100644 lib/services/asignaturas_service.dart delete mode 100644 lib/services/auth_service.dart delete mode 100644 lib/services/carreras_service.dart delete mode 100644 lib/services/grades_service.dart delete mode 100644 lib/services/horarios_service.dart delete mode 100644 lib/services/noticias_service.dart delete mode 100644 lib/services/perfil_service.dart delete mode 100644 lib/services/permisos_covid_service.dart create mode 100644 lib/services_new/implementations/asignaturas_service.dart create mode 100644 lib/services_new/implementations/auth_service.dart create mode 100644 lib/services_new/implementations/calculator_service.dart create mode 100644 lib/services_new/implementations/carreras_service.dart create mode 100644 lib/services_new/implementations/credential_service.dart create mode 100644 lib/services_new/implementations/grades_service.dart create mode 100644 lib/services_new/implementations/horario_service.dart create mode 100644 lib/services_new/implementations/noticias_service.dart create mode 100644 lib/services_new/implementations/qr_pass_service.dart create mode 100644 lib/services_new/interfaces/asignaturas_service.dart create mode 100644 lib/services_new/interfaces/auth_service.dart create mode 100644 lib/services_new/interfaces/calculator_service.dart create mode 100644 lib/services_new/interfaces/carreras_service.dart create mode 100644 lib/services_new/interfaces/credential_service.dart create mode 100644 lib/services_new/interfaces/grades_service.dart create mode 100644 lib/services_new/interfaces/horario_service.dart create mode 100644 lib/services_new/interfaces/noticias_service.dart create mode 100644 lib/services_new/interfaces/qr_pass_service.dart create mode 100644 lib/services_new/service_manager.dart rename lib/{screens/login_screen/_background.dart => widgets/login_screen/background.dart} (81%) create mode 100644 lib/widgets/login_screen/login_form.dart diff --git a/lib/config/constants.dart b/lib/config/constants.dart index 1568527..11a0e8a 100644 --- a/lib/config/constants.dart +++ b/lib/config/constants.dart @@ -1,5 +1,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; +final apiUrl = bool.fromEnvironment('dart.vm.product') ? 'https://api.exdev.cl' : (dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl'); + class Constants { static const String sentryDsn = 'https://0af59b2ad2b44f4e8c9cad4ea8d5f32e@o507661.ingest.sentry.io/5599080'; static const String uxCamDevKey = '0y6p88obpgiug1g'; diff --git a/lib/config/http_client.dart b/lib/config/http_client.dart new file mode 100644 index 0000000..475e061 --- /dev/null +++ b/lib/config/http_client.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:watch_it/watch_it.dart'; + +class AuthClient extends http.BaseClient { + + final _client = http.Client(); + + @override + Future send(http.BaseRequest request) async { + if(!request.headers.containsKey('user-agent')) { + request.headers['user-agent'] = "App/MiUTEM"; + } + + if(!request.headers.containsKey('content-type')) { + request.headers['content-type'] = "application/json"; + } + + if (!request.headers.containsKey('authorization')) { + final user = await di.get().getUser(); + final token = user?.token; + if (token != null) { + request.headers['authorization'] = 'Bearer $token'; + } + } + + var response = await _client.send(request); + final json = jsonDecode(await response.stream.bytesToString()); + if(response.statusCode == 401 && json is Map && json.containsKey("codigoInterno") && json["codigoInterno"] == 12) { + // Refrescar el token + final _authService = di.get(); + await _authService.isLoggedIn(); + final user = await _authService.getUser(); + final token = user?.token; + if (token != null) { + request.headers['authorization'] = 'Bearer $token'; + response = await _client.send(request); + } + } + + return response; + } + + @override + void close() { + _client.close(); + super.close(); + } + +} \ No newline at end of file diff --git a/lib/config/router.dart b/lib/config/router.dart deleted file mode 100644 index ea9f974..0000000 --- a/lib/config/router.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/route_manager.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; -import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; -import 'package:mi_utem/controllers/asignatura/asignaturas_controller.dart'; -import 'package:mi_utem/controllers/qr_passes_controller.dart'; -import 'package:mi_utem/screens/calculadora_notas_screen.dart'; -import 'package:mi_utem/screens/credencial_screen.dart'; -import 'package:mi_utem/screens/horario/horario_screen.dart'; -import 'package:mi_utem/screens/login_screen/login_screen.dart'; -import 'package:mi_utem/screens/main_screen.dart'; -import 'package:mi_utem/screens/permiso_covid_screen.dart'; -import 'package:mi_utem/screens/splash_screen.dart'; -import 'package:mi_utem/screens/usuario_screen.dart'; -import 'package:mi_utem/services/auth_service.dart'; -import 'package:mi_utem/services/perfil_service.dart'; -import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; - -final _loginPage = GetPage( - name: Routes.login, - page: () => LoginScreen(), - middlewares: [OnlyNoAuthMiddleware()], -); - -final _homePage = GetPage( - name: Routes.home, - bindings: [QrPassesBinding()], - page: () { - final usuario = PerfilService.getLocalUsuario(); - - return MainScreen(usuario: usuario); - }, - middlewares: [OnlyAuthMiddleware()], -); - -final pages = [ - GetPage( - name: Routes.splash, - page: () => SplashScreen(), - ), - GetPage( - name: Routes.about, - page: () => AcercaScreen(), - ), - _loginPage, - _homePage, - GetPage( - name: Routes.perfil, - page: () => UsuarioScreen(), - middlewares: [OnlyAuthMiddleware()], - ), - GetPage( - name: Routes.credencial, - page: () => CredencialScreen(), - middlewares: [OnlyAuthMiddleware()], - ), - GetPage( - name: Routes.calculadoraNotas, - page: () => CalculadoraNotasScreen(), - middlewares: [OnlyAuthMiddleware()], - ), - GetPage( - name: Routes.horario, - page: () => HorarioScreen(), - binding: HorarioBinding(), - middlewares: [OnlyAuthMiddleware()], - ), - GetPage( - name: Routes.asignaturas, - page: () => AsignaturasListaScreen(), - middlewares: [OnlyAuthMiddleware()], - binding: AsignaturasBinding(), - ), - GetPage( - name: '${Routes.asignatura}/:asignaturaId', - page: () => AsignaturaDetalleScreen(), - ), - GetPage( - name: Routes.pass, - page: () => PermisoCovidScreen(), - middlewares: [OnlyAuthMiddleware()], - ), -]; - -class OnlyAuthMiddleware extends GetMiddleware { - @override - RouteSettings? redirect(String? page) { - final isLoggedIn = AuthService.isLoggedIn(); - if (!isLoggedIn) { - return const RouteSettings(name: Routes.login); - } - return null; - } -} - -class OnlyNoAuthMiddleware extends GetMiddleware { - @override - RouteSettings? redirect(String? page) { - final isLoggedIn = AuthService.isLoggedIn(); - if (isLoggedIn) { - return const RouteSettings(name: Routes.home); - } - return null; - } -} diff --git a/lib/config/routes.dart b/lib/config/routes.dart deleted file mode 100644 index 1884123..0000000 --- a/lib/config/routes.dart +++ /dev/null @@ -1,17 +0,0 @@ -class Routes { - static const splash = '/splash'; - static const login = '/login'; - static const home = '/'; - static const about = '/about'; - static const horario = '/horario'; - static const asignaturas = '/asignaturas'; - static const perfil = '/perfil'; - static const credencial = '/credencial'; - static const calculadoraNotas = '/calculadora-notas'; - static const imageView = '/image-view'; - static const asignatura = '/asignatura'; - - static const passBase = '/pass'; - static const passParameter = 'passId'; - static const pass = '$passBase/:$passParameter'; -} diff --git a/lib/config/secure_storage.dart b/lib/config/secure_storage.dart new file mode 100644 index 0000000..5366ad9 --- /dev/null +++ b/lib/config/secure_storage.dart @@ -0,0 +1,3 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +final secureStorage = FlutterSecureStorage(); \ No newline at end of file diff --git a/lib/controllers/asignatura/asignatura_controller.dart b/lib/controllers/asignatura/asignatura_controller.dart deleted file mode 100644 index 544e732..0000000 --- a/lib/controllers/asignatura/asignatura_controller.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:get/get.dart'; -import 'package:mi_utem/controllers/carreras_controller.dart'; -import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/models/grades.dart'; -import 'package:mi_utem/services/grades_service.dart'; - -class AsignaturaController extends GetxController with StateMixin { - late final String asignaturaId; - late final Asignatura? _initialAsignatura; - - Carrera? _selectedCarrera; - - AsignaturaController(this.asignaturaId, {Asignatura? asignatura}) { - if (asignatura != null) { - _initialAsignatura = asignatura; - } - } - - @override - void onInit() { - _selectedCarrera = Get.find().selectedCarrera.value; - if (_selectedCarrera != null) { - getAsignaturaDetail(Get.find().selectedCarrera.value); - } - - ever( - Get.find().selectedCarrera, - (carrera) { - _selectedCarrera = carrera; - getAsignaturaDetail(carrera, forceRefresh: true); - }, - ); - super.onInit(); - } - - Future refreshData() async { - await getAsignaturaDetail(_selectedCarrera, forceRefresh: true); - } - - Future getAsignaturaDetail(Carrera? carrera, - {bool forceRefresh = false}) async { - final carreraId = carrera?.id; - if (carreraId != null) { - change(null, status: RxStatus.loading()); - try { - Grades grades = await GradesService.getGrades( - carreraId, - asignaturaId, - forceRefresh: forceRefresh, - ); - - Asignatura asignatura = _initialAsignatura!; - asignatura.grades = grades; - - change(asignatura, status: RxStatus.success()); - } catch (e) { - change(null, status: RxStatus.error(e.toString())); - } - } - } -} diff --git a/lib/controllers/asignatura/asignaturas_controller.dart b/lib/controllers/asignatura/asignaturas_controller.dart deleted file mode 100644 index d3d5be6..0000000 --- a/lib/controllers/asignatura/asignaturas_controller.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:get/get.dart'; -import 'package:mi_utem/controllers/asignatura/asignatura_controller.dart'; -import 'package:mi_utem/controllers/carreras_controller.dart'; -import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/services/asignaturas_service.dart'; - -class AsignaturasController extends GetxController - with StateMixin> { - @override - void onInit() { - change(null, status: RxStatus.loading()); - - if (Get.find().selectedCarrera.value != null) { - getAsignaturas(Get.find().selectedCarrera.value); - } - - ever( - Get.find().selectedCarrera, - (carrera) => getAsignaturas(carrera, forceRefresh: true), - ); - super.onInit(); - } - - void getAsignaturas( - Carrera? carrera, { - bool forceRefresh = false, - }) async { - change(null, status: RxStatus.loading()); - - final carreraId = carrera?.id; - - if (carreraId == null) { - change(null, status: RxStatus.error("No hay carrera seleccionada")); - return; - } - - List response = await AsignaturasService.getAsignaturas( - carreraId, - forceRefresh: forceRefresh, - ); - - change(response, status: RxStatus.success()); - - for (var asignatura in response) { - if (asignatura.id != null) { - Get.put( - AsignaturaController(asignatura.id!, asignatura: asignatura), - tag: asignatura.id, - permanent: true, - ); - } - } - } - - void refreshAsignaturas() { - final carrera = Get.find().selectedCarrera.value; - getAsignaturas(carrera, forceRefresh: true); - } -} - -class AsignaturasBinding extends Bindings { - @override - void dependencies() { - Get.put(AsignaturasController(), permanent: true); - } -} diff --git a/lib/controllers/calculator_controller.dart b/lib/controllers/calculator_controller.dart deleted file mode 100644 index c042466..0000000 --- a/lib/controllers/calculator_controller.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:flutter_masked_text/flutter_masked_text.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/models/grades.dart'; - -class CalculatorController extends GetxController { - static const maxPercentage = 100; - static const maxGrade = 7; - static const minimumGradeForExam = 2.95; - static const passingGrade = 3.95; - static const examFinalWeight = 0.4; - static const presentationFinalWeight = 1 - examFinalWeight; - - final partialGrades = [].obs; - final percentageTextFieldControllers = [].obs; - final gradeTextFieldControllers = [].obs; - final examGrade = Rxn(); - final examGradeTextFieldController = MaskedTextController(mask: "0.0"); - final freeEditable = false.obs; - - static CalculatorController get to => Get.find(); - - double? get calculatedFinalGrade { - if (calculatedPresentationGrade != null) { - if (examGrade.value != null) { - final weightedFinalGrade = - calculatedPresentationGrade! * presentationFinalWeight; - final weightedExamGrade = examGrade.value! * examFinalWeight; - - return weightedFinalGrade + weightedExamGrade; - } - return calculatedPresentationGrade; - } - return null; - } - - double? get calculatedPresentationGrade { - double presentationGrade = 0; - for (var partialGrade in partialGrades) { - final weight = (partialGrade.porcentaje ?? 0) / maxPercentage; - presentationGrade += (partialGrade.nota ?? 0) * weight; - } - - return presentationGrade != 0 ? presentationGrade : null; - } - - int get numOfPartialGradesWithoutGrade { - return partialGrades - .where((partialGrade) => partialGrade.nota == null) - .length; - } - - bool get hasMissingPartialGrade { - return numOfPartialGradesWithoutGrade > 0; - } - - bool get canTakeExam { - return !hasMissingPartialGrade && - calculatedPresentationGrade != null && - calculatedPresentationGrade! >= minimumGradeForExam && - calculatedPresentationGrade! < passingGrade; - } - - double? get minimumRequiredExamGrade { - if (canTakeExam) { - final weightedPresentationGrade = - calculatedPresentationGrade! * presentationFinalWeight; - return (passingGrade - weightedPresentationGrade) / examFinalWeight; - } - - return null; - } - - double get percentageOfPartialGrades { - double percentage = 0; - for (var partialGrade in partialGrades) { - percentage += (partialGrade.porcentaje ?? 0); - } - return percentage; - } - - double get missingPercentage { - return maxPercentage - percentageOfPartialGrades; - } - - int get numOfPartialGradesWithoutPercentage { - return partialGrades - .where((partialGrade) => partialGrade.porcentaje == null) - .length; - } - - bool get hasMissingPercentage { - return numOfPartialGradesWithoutPercentage > 0; - } - - double? get suggestedPercentage { - final percentage = missingPercentage / numOfPartialGradesWithoutPercentage; - return 0 <= percentage && percentage <= maxPercentage ? percentage : null; - } - - double? get suggestedPresentationGrade { - double presentationGrade = 0; - for (var partialGrade in partialGrades) { - final weight = (partialGrade.porcentaje ?? (suggestedPercentage ?? 0)) / - maxPercentage; - presentationGrade += (partialGrade.nota ?? 0) * weight; - } - return 0 <= presentationGrade && presentationGrade <= maxGrade - ? presentationGrade - : null; - } - - double get percentageWithoutGrade { - double percentage = 0; - for (var partialGrade in partialGrades) { - if (partialGrade.nota == null) { - percentage += (partialGrade.porcentaje ?? (suggestedPercentage ?? 0)); - } - } - return percentage; - } - - bool get hasCorrectPercentage { - return percentageOfPartialGrades == maxPercentage; - } - - double? get suggestedGrade { - if (hasMissingPartialGrade && percentageWithoutGrade > 0) { - final weightOfMissingGrades = percentageWithoutGrade / maxPercentage; - final requiredGradeValue = - passingGrade - (suggestedPresentationGrade ?? 0); - final missingGradesValue = requiredGradeValue / weightOfMissingGrades; - return missingGradesValue; - } - return null; - } - - void makeEditable() { - freeEditable.value = true; - } - - void makeNonEditable() { - freeEditable.value = false; - } - - void loadGrades(Grades grades) { - partialGrades.clear(); - percentageTextFieldControllers.clear(); - gradeTextFieldControllers.clear(); - - for (var evaluacion in grades.notasParciales) { - final partialGrade = IEvaluacion.fromRemote(evaluacion); - addGrade(partialGrade); - } - - setExamGrade(grades.notaExamen); - } - - void changeGradeAt(int index, IEvaluacion changedGrade) { - final grade = partialGrades[index]; - if (grade.editable || freeEditable.value) { - partialGrades[index] = changedGrade; - - if (hasMissingPartialGrade) { - clearExamGrade(); - } - } else { - throw Exception("No se puede editar una nota que está asignada"); - } - } - - void clearExamGrade() { - examGrade.value = null; - examGradeTextFieldController.text = ""; - } - - void setExamGrade(num? grade) { - examGrade.value = grade?.toDouble(); - examGradeTextFieldController.text = - grade?.toDouble().toStringAsFixed(1) ?? ""; - } - - void addGrade(IEvaluacion grade) { - partialGrades.add(grade); - percentageTextFieldControllers.add( - MaskedTextController( - mask: "000", - text: grade.porcentaje?.toDouble().toStringAsFixed(0) ?? "", - ), - ); - gradeTextFieldControllers.add( - MaskedTextController( - mask: "0.0", - text: grade.nota?.toDouble().toStringAsFixed(1) ?? "", - ), - ); - } - - void removeGradeAt(int index) { - final grade = partialGrades[index]; - if (grade.editable || freeEditable.value) { - partialGrades.removeRange(index, index + 1); - percentageTextFieldControllers.removeRange(index, index + 1); - gradeTextFieldControllers.removeRange(index, index + 1); - } else { - throw Exception("No se puede eliminar una nota que está asignada"); - } - } -} diff --git a/lib/controllers/carreras_controller.dart b/lib/controllers/carreras_controller.dart deleted file mode 100644 index a26aa56..0000000 --- a/lib/controllers/carreras_controller.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:get/get.dart'; -import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/carreras_service.dart'; - -class CarrerasController extends GetxController { - final carreras = [].obs; - final selectedCarrera = Rxn(); - - static CarrerasController get to => Get.find(); - - @override - void onInit() { - getCarreras(); - - super.onInit(); - } - - void getCarreras() async { - try { - final carreras = await CarreraService.getCarreras(forceRefresh: true); - - this.carreras.value = carreras; - _autoSelectCarreraActiva(carreras); - } catch(_) {} - } - - void _autoSelectCarreraActiva(List carreras) { - final estados = ["Regular", "Causal de Eliminacion"] - .reversed - .map((e) => e.toLowerCase()) - .toList(); - - carreras.sort( - (a, b) => estados.indexOf(b.estado!.toLowerCase()).compareTo( - estados.indexOf(a.estado!.toLowerCase()), - ), - ); - - Carrera activa = carreras.first; - - AnalyticsService.setCarreraToUser(activa); - - changeSelectedCarrera(activa); - } - - void changeSelectedCarrera(Carrera carrera) { - selectedCarrera.value = carrera; - } -} diff --git a/lib/controllers/grades_changes_controller.dart b/lib/controllers/grades_changes_controller.dart deleted file mode 100644 index 0be8fb7..0000000 --- a/lib/controllers/grades_changes_controller.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'dart:developer'; - -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/controllers/carreras_controller.dart'; -import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/models/grades.dart'; -import 'package:mi_utem/services/asignaturas_service.dart'; -import 'package:mi_utem/services/auth_service.dart'; -import 'package:mi_utem/services/grades_service.dart'; -import 'package:mi_utem/services/notification_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - -class GradesChangesController { - static const savedGradesPrefix = 'savedGrades_'; - static const suscribedAsignaturasPrefix = 'suscribedAsignaturas_'; - - static final GetStorage box = GetStorage(); - - static Future saveGrades(String asignaturaId, Grades grades) async { - final jsonGrades = grades.toJson(); - jsonGrades['lastUpdate'] = DateTime.now().toIso8601String(); - - log('Saving grades for $asignaturaId: $jsonGrades'); - - return box.write('$savedGradesPrefix$asignaturaId', jsonGrades); - } - - static GradeChangeType _getGradeValueChangeType( - Grades oldGrades, - Grades updatedGrades, - ) { - final oldGradesLength = oldGrades.notasParciales.length; - final updatedGradesLength = updatedGrades.notasParciales.length; - - if (oldGradesLength == updatedGradesLength) { - GradeChangeType? currentChange; - - for (int i = 0; i < oldGradesLength; i++) { - final oldValue = oldGrades.notasParciales[i]; - final updatedValue = updatedGrades.notasParciales[i]; - - if (oldValue.nota != updatedValue.nota) { - if (oldValue.nota == null && updatedValue.nota != null) { - Sentry.configureScope( - (scope) => scope.setExtra('newGrade', updatedValue.nota), - ); - currentChange = GradeChangeType.gradeSetted; - } else if (oldValue.nota != null && updatedValue.nota == null) { - currentChange = currentChange ?? GradeChangeType.gradeDeleted; - } else { - currentChange = currentChange ?? GradeChangeType.gradeUpdated; - } - } - } - - if (currentChange != null) { - return currentChange; - } - } else { - Sentry.captureMessage( - 'Asignatura $oldGrades.id has a different number of weighters in _getGradeValueChangeType function', - level: SentryLevel.warning, - ); - } - return GradeChangeType.noChange; - } - - static bool _hasAWeighterDiferrence( - Grades oldGrades, - Grades updatedGrades, - ) { - final oldGradesLength = oldGrades.notasParciales.length; - final updatedGradesLength = updatedGrades.notasParciales.length; - - if (oldGradesLength == updatedGradesLength) { - for (int i = 0; i < oldGradesLength; i++) { - final oldWeighter = oldGrades.notasParciales[i]; - final updatedWeighter = updatedGrades.notasParciales[i]; - - if (oldWeighter.porcentaje != updatedWeighter.porcentaje) { - return true; - } - } - } else { - Sentry.captureMessage( - 'Asignatura $oldGrades.id has a different number of weighters in _hasAWeighterDiferrence function', - level: SentryLevel.warning, - ); - } - return false; - } - - static bool _hasAGradeWithValue(Grades asignatura) { - return asignatura.notasParciales.any((element) => element.nota != null); - } - - static GradeChangeType compareGrades( - String asignaturaId, - Grades updatedGrades, - ) { - final oldGradesJson = box.read('$savedGradesPrefix$asignaturaId'); - - if (oldGradesJson != null) { - final oldGrades = Grades.fromJson(oldGradesJson); - - log(oldGrades.toString()); - - final oldGradesLength = oldGrades.notasParciales.length; - final updatedGradesLength = updatedGrades.notasParciales.length; - - if (oldGradesLength == 0) { - if (updatedGradesLength == 0) { - return GradeChangeType.noChange; - } else { - if (_hasAGradeWithValue(updatedGrades)) { - return GradeChangeType.gradeSetted; - } else { - return GradeChangeType.weightersSetted; - } - } - } else { - if (updatedGradesLength == 0) { - return GradeChangeType.weightersDeleted; - } else { - if (oldGradesLength != updatedGradesLength) { - return GradeChangeType.weightersUpdated; - } else { - if (_hasAWeighterDiferrence(oldGrades, updatedGrades)) { - return GradeChangeType.weightersUpdated; - } else { - return _getGradeValueChangeType(oldGrades, updatedGrades); - } - } - } - } - } else { - log('compareGrades oldGradesJson was null'); - } - - return GradeChangeType.noChange; - } - - static Future> checkIfGradesHasChange() async { - final isLogged = AuthService.isLoggedIn(); - - if (isLogged) { - final carrera = CarrerasController.to.selectedCarrera.value; - final carreraId = carrera?.id; - - if (carreraId != null) { - final suscribedAsignaturasJson = - box.read('$suscribedAsignaturasPrefix$carreraId'); - List? suscribedAsignaturas; - - if (suscribedAsignaturasJson == null) { - final asignaturas = - await AsignaturasService.getAsignaturas(carreraId); - final asignaturasJson = asignaturas.map((e) => e.toJson()).toList(); - suscribedAsignaturas = asignaturas; - box.write('$suscribedAsignaturasPrefix$carreraId', asignaturasJson); - } else { - suscribedAsignaturas = - Asignatura.fromJsonList(suscribedAsignaturasJson); - } - - for (Asignatura? asignatura in suscribedAsignaturas) { - final asignaturaId = asignatura?.id; - if (asignatura != null && asignaturaId != null) { - final updatedGrades = await GradesService.getGrades( - carreraId, - asignaturaId, - forceRefresh: true, - saveGrades: false, - ); - - final changeType = compareGrades( - asignaturaId, - updatedGrades, - ); - - await saveGrades(asignaturaId, updatedGrades); - - _notificateChange(asignatura, changeType); - } - } - } - } - - return {}; - } - - static void _notificateChange(Asignatura asignatura, GradeChangeType change) { - final asignaturaName = asignatura.nombre ?? asignatura.codigo; - - String? title; - String? body; - - switch (change) { - case GradeChangeType.gradeSetted: - title = 'Tienes una nueva nota'; - body = '$asignaturaName: se ha agregado una nota'; - break; - case GradeChangeType.gradeUpdated: - title = 'Una nota ha cambiado'; - body = '$asignaturaName: se ha actualizado una nota'; - break; - case GradeChangeType.gradeDeleted: - title = 'Una nota se ha borrado'; - body = '$asignaturaName: se ha eliminado una nota'; - break; - default: - break; - } - - if (title != null && body != null) { - Sentry.captureMessage( - 'Asignatura has changed and notificated', - level: SentryLevel.debug, - withScope: (scope) { - scope.setTag('asignaturaId', asignatura.id.toString()); - scope.setTag('asignaturaCodigo', asignatura.codigo.toString()); - scope.setTag('change', change.toString()); - }, - ); - - NotificationService.showGradeChangeNotification(title, body, asignatura); - } else if (change != GradeChangeType.noChange) { - Sentry.captureMessage( - 'Asignatura has changed but not notificated', - level: SentryLevel.debug, - withScope: (scope) { - scope.setTag('asignaturaId', asignatura.id.toString()); - scope.setTag('asignaturaCodigo', asignatura.codigo.toString()); - scope.setTag('change', change.toString()); - }, - ); - } - } -} - -enum GradeChangeType { - weightersSetted, - weightersUpdated, - weightersDeleted, - gradeSetted, - gradeUpdated, - gradeDeleted, - noChange -} diff --git a/lib/controllers/horario_controller.dart b/lib/controllers/horario_controller.dart index aac0bb3..ad66efb 100644 --- a/lib/controllers/horario_controller.dart +++ b/lib/controllers/horario_controller.dart @@ -3,14 +3,15 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/controllers/carreras_controller.dart'; import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; -import 'package:mi_utem/services/horarios_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; +import 'package:mi_utem/services_new/interfaces/horario_service.dart'; import 'package:vector_math/vector_math_64.dart' as vector; +import 'package:watch_it/watch_it.dart'; class HorarioController extends GetxController { static const daysCount = 6; @@ -85,14 +86,11 @@ class HorarioController extends GetxController { @override onInit() { - if (Get.find().selectedCarrera.value != null) { - getHorarioData(Get.find().selectedCarrera.value); + final selectedCarrera = watchValue((CarrerasService service) => service.selectedCarrera); + if (selectedCarrera != null) { + getHorarioData(selectedCarrera); } - ever( - Get.find().selectedCarrera, - (carrera) => getHorarioData(carrera), - ); _init(); super.onInit(); } @@ -112,7 +110,7 @@ class HorarioController extends GetxController { return; } loadingHorario.value = true; - horario.value = await HorarioService.getHorario(carreraId); + horario.value = await di.get().getHorario(carreraId); _setRandomColorsByHorario(); loadingHorario.value = false; } diff --git a/lib/controllers/notification_controller.dart b/lib/controllers/notification_controller.dart index 7e2950f..4f50951 100644 --- a/lib/controllers/notification_controller.dart +++ b/lib/controllers/notification_controller.dart @@ -4,9 +4,9 @@ import 'dart:developer'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/main.dart'; import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; class NotificationController { @@ -35,9 +35,7 @@ class NotificationController { /// Use this method to detect when the user taps on a notifications or action button @pragma("vm:entry-point") - static Future onActionReceivedMethod( - ReceivedAction receivedAction, - ) async { + static Future onActionReceivedMethod(ReceivedAction receivedAction) async { log("onActionReceivedMethod: ${receivedAction.id} ${receivedAction.payload}"); AnalyticsService.logEvent( @@ -50,12 +48,9 @@ class NotificationController { if (type == 'grade_change') { final asignaturaJsonString = payload?['asignatura']; if (asignaturaJsonString != null) { - AnalyticsService.logEvent( - 'notification_tap_grade_change', - ); - final asignatura = - Asignatura.fromJson(jsonDecode(asignaturaJsonString)); - Get.toNamed('${Routes.asignatura}/${asignatura.id}'); + AnalyticsService.logEvent('notification_tap_grade_change'); + final asignatura = Asignatura.fromJson(jsonDecode(asignaturaJsonString)); + navigatorKey.currentState?.push(MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignaturaId: asignatura.id))); } } } diff --git a/lib/controllers/qr_pass_controller.dart b/lib/controllers/qr_pass_controller.dart deleted file mode 100644 index e879f6a..0000000 --- a/lib/controllers/qr_pass_controller.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:get/get.dart'; -import 'package:mi_utem/models/permiso_covid.dart'; -import 'package:mi_utem/services/permisos_covid_service.dart'; - -class QrPassController extends GetxController with StateMixin { - late final String passId; - - QrPassController(this.passId); - - @override - void onInit() { - getPassDetails(); - super.onInit(); - } - - void getPassDetails({bool refresh = false}) async { - change(null, status: RxStatus.loading()); - try { - PermisoCovid pass = await PermisosCovidService.getDetallesPermiso( - passId, - forceRefresh: refresh, - ); - change(pass, status: RxStatus.success()); - } catch (e) { - change(null, status: RxStatus.error(e.toString())); - } - } -} diff --git a/lib/controllers/qr_passes_controller.dart b/lib/controllers/qr_passes_controller.dart index c92b8d0..ac2fe86 100644 --- a/lib/controllers/qr_passes_controller.dart +++ b/lib/controllers/qr_passes_controller.dart @@ -1,7 +1,7 @@ import 'package:get/get.dart'; -import 'package:mi_utem/controllers/qr_pass_controller.dart'; import 'package:mi_utem/models/permiso_covid.dart'; -import 'package:mi_utem/services/permisos_covid_service.dart'; +import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; +import 'package:watch_it/watch_it.dart'; class QrPassesController extends GetxController { final passes = [].obs; @@ -15,18 +15,20 @@ class QrPassesController extends GetxController { void getPasses({bool refresh = false}) async { isLoading.value = true; - List response = - await PermisosCovidService.getPermisos(forceRefresh: refresh); + List? response = await di.get().getPermisos(forceRefresh: refresh); + if(response == null) { + return; + } passes.value = response; isLoading.value = false; for (var pass in passes) { if (pass.id != null) { - Get.put( - QrPassController(pass.id!), - tag: pass.id, - permanent: true, - ); + // Get.put( + // QrPassController(pass.id!), + // tag: pass.id, + // permanent: true, + // ); } } } diff --git a/lib/main.dart b/lib/main.dart index e48103c..91cf003 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,21 +9,22 @@ import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/router.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; -import 'package:mi_utem/controllers/carreras_controller.dart'; +import 'package:mi_utem/screens/splash_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/services/background_service.dart'; import 'package:mi_utem/services/notification_service.dart'; -import 'package:mi_utem/services/perfil_service.dart'; +import 'package:mi_utem/services_new/service_manager.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart' as AuthServiceNew; import 'package:mi_utem/themes/theme.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:watch_it/watch_it.dart'; import 'services/remote_config/remote_config.dart'; +final GlobalKey navigatorKey = GlobalKey(); + + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -34,6 +35,7 @@ void main() async { await RemoteConfigService.initialize(); await NotificationService.initialize(); await BackgroundService.initAndStart(); + await registerServices(); await SentryFlutter.init( (options) { options.dsn = Constants.sentryDsn; @@ -52,37 +54,19 @@ class MiUtem extends StatefulWidget { class _MiUtemState extends State { final FirebaseAnalytics analytics = FirebaseAnalytics.instance; - final calculatorController = Get.put(CalculatorController()); - - @override - void initState() { - if (AuthService.isLoggedIn()) { - final user = PerfilService.getLocalUsuario(); - AnalyticsService.setUser(user); - } else { - AnalyticsService.removeUser(); - } - super.initState(); - } - @override Widget build(BuildContext context) { FlutterUxcam.optIntoSchematicRecordings(); - FlutterUxConfig config = FlutterUxConfig( + FlutterUxcam.startWithConfiguration(FlutterUxConfig( userAppKey: kDebugMode ? Constants.uxCamDevKey : Constants.uxCamProdKey, enableAutomaticScreenNameTagging: true, enableMultiSessionRecord: true, - ); - FlutterUxcam.startWithConfiguration(config); + )); return GetMaterialApp( - getPages: pages, - initialRoute: Routes.splash, + home: SplashScreen(), debugShowCheckedModeBanner: false, title: 'Mi UTEM', - initialBinding: BindingsBuilder(() { - Get.put(CarrerasController(), permanent: true); - }), theme: MainTheme.theme, navigatorObservers: [ FirebaseAnalyticsObserver(analytics: analytics), diff --git a/lib/models/evaluacion.dart b/lib/models/evaluacion.dart index 4acb870..ce66c14 100644 --- a/lib/models/evaluacion.dart +++ b/lib/models/evaluacion.dart @@ -13,35 +13,21 @@ class REvaluacion { this.nota, }); - factory REvaluacion.fromJson(Map? json) { - if (json == null) { - return REvaluacion(); - } - return REvaluacion( - porcentaje: json[porcentajeKey], - descripcion: json[descripcionKey], - nota: json[notaKey], - ); - } - - static List fromJsonList(dynamic json) { - if (json == null) { - return []; - } - List list = []; - for (var item in json) { - list.add(REvaluacion.fromJson(item)); - } - return list; - } - - Map toJson() { - return { - porcentajeKey: porcentaje, - descripcionKey: descripcion, - notaKey: nota, - }; - } + factory REvaluacion.fromJson(Map? json) => + json != null ? REvaluacion( + porcentaje: json[porcentajeKey], + descripcion: json[descripcionKey], + nota: json[notaKey], + ) : REvaluacion(); + + static List fromJsonList(List? json) => + json?.map((it) => REvaluacion.fromJson(it)).toList() ?? []; + + Map toJson() => { + porcentajeKey: porcentaje, + descripcionKey: descripcion, + notaKey: nota, + }; } class IEvaluacion extends REvaluacion { @@ -66,13 +52,11 @@ class IEvaluacion extends REvaluacion { ); } - IEvaluacion copyWith( - {bool? editable, String? descripcion, num? porcentaje, num? nota}) { - return IEvaluacion( - editable: editable ?? this.editable, - descripcion: descripcion ?? this.descripcion, - porcentaje: porcentaje ?? this.porcentaje, - nota: nota ?? this.nota, - ); - } + IEvaluacion copyWith({bool? editable, String? descripcion, num? porcentaje, num? nota}) => + IEvaluacion( + editable: editable ?? this.editable, + descripcion: descripcion ?? this.descripcion, + porcentaje: porcentaje ?? this.porcentaje, + nota: nota ?? this.nota, + ); } diff --git a/lib/models/exceptions/custom_exception.dart b/lib/models/exceptions/custom_exception.dart new file mode 100644 index 0000000..2a309bd --- /dev/null +++ b/lib/models/exceptions/custom_exception.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; + +class CustomException implements Exception { + + final String message; + final String? error; + final int? statusCode; + final int? internalCode; + + CustomException({ + this.message = 'Ocurrió un error inesperado. Por favor, inténtalo nuevamente.', + this.error, + this.statusCode, + this.internalCode, + }); + + factory CustomException.custom(String? errorMessage) => CustomException(message: "Ha ocurrido un error inesperado. ${errorMessage ?? "Por favor intenta más tarde."}"); + + factory CustomException.fromJson(Map json) => CustomException( + message: json['mensaje'] as String, + error: json['error'] as String?, + statusCode: json['codigoHttp'] as int?, + internalCode: json['codigoInterno'] as int?, + ); + + toJson() => { + 'mensaje': message, + 'error': error, + 'codigoHttp': statusCode, + 'codigoInterno': internalCode, + }; + + @override + String toString() => jsonEncode(toJson()); +} \ No newline at end of file diff --git a/lib/models/grades.dart b/lib/models/grades.dart index 9719262..761cb2d 100644 --- a/lib/models/grades.dart +++ b/lib/models/grades.dart @@ -13,21 +13,17 @@ class Grades { this.notaExamen, }); - factory Grades.fromJson(Map json) { - return Grades( - notasParciales: REvaluacion.fromJsonList(json['notasParciales']), - notaFinal: json['notaFinal'] as num?, - notaPresentacion: json['notaPresentacion'] as num?, - notaExamen: json['notaExamen'] as num?, - ); - } + factory Grades.fromJson(Map json) => Grades( + notasParciales: REvaluacion.fromJsonList(json['notasParciales']), + notaFinal: json['notaFinal'] as num?, + notaPresentacion: json['notaPresentacion'] as num?, + notaExamen: json['notaExamen'] as num?, + ); - Map toJson() { - return { - 'notasParciales': notasParciales.map((nota) => nota.toJson()).toList(), - 'notaFinal': notaFinal, - 'notaPresentacion': notaPresentacion, - 'notaExamen': notaExamen, - }; - } + Map toJson() => { + 'notasParciales': notasParciales.map((nota) => nota.toJson()).toList(), + 'notaFinal': notaFinal, + 'notaPresentacion': notaPresentacion, + 'notaExamen': notaExamen, + }; } diff --git a/lib/models/permiso_covid.dart b/lib/models/permiso_covid.dart index 336aae0..a2556d2 100644 --- a/lib/models/permiso_covid.dart +++ b/lib/models/permiso_covid.dart @@ -1,8 +1,8 @@ -import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/models/user/user.dart'; class PermisoCovid { String? id; - Usuario? usuario; + User? user; String? codigoQr; String? perfil; String? motivo; @@ -14,7 +14,7 @@ class PermisoCovid { PermisoCovid({ this.id, - this.usuario, + this.user, this.codigoQr, this.perfil, this.motivo, @@ -25,26 +25,24 @@ class PermisoCovid { this.fechaSolicitud, }); - factory PermisoCovid.fromJson(Map json) { - return PermisoCovid( - id: json['id'], - usuario: Usuario.fromJson(json['usuario']), - codigoQr: json['codigoQr'], - perfil: json['perfil'], - motivo: json['motivo'], - campus: json['campus'], - dependencia: json['dependencia'], - jornada: json['jornada'], - vigencia: json['vigencia'], - fechaSolicitud: DateTime.tryParse(json['fechaSolicitud']), - ); - } + factory PermisoCovid.fromJson(Map json) => PermisoCovid( + id: json['id'], + user: json.containsKey("usuario") ? User.fromJson(json['usuario']) : null, + codigoQr: json['codigoQr'], + perfil: json['perfil'], + motivo: json['motivo'], + campus: json['campus'], + dependencia: json['dependencia'], + jornada: json['jornada'], + vigencia: json['vigencia'], + fechaSolicitud: DateTime.tryParse(json['fechaSolicitud']), + ); - static List fromJsonList(List json) { - List lista = []; - for (var elemento in json) { - lista.add(PermisoCovid.fromJson(elemento)); + static List fromJsonList(List? json) { + if(json == null) { + return []; } - return lista; + + return json.map((it) => PermisoCovid.fromJson(it)).toList(); } } diff --git a/lib/models/user/credential.dart b/lib/models/user/credential.dart new file mode 100644 index 0000000..c9fcb17 --- /dev/null +++ b/lib/models/user/credential.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; + +class Credentials { + + final String email; + final String password; + + const Credentials({ + required this.email, + required this.password, + }); + + toJson() => { + 'correo': email, + 'contrasenia': password, + }; + + @override + String toString() => jsonEncode(toJson()); + + factory Credentials.fromJson(Map json) => Credentials( + email: json['correo'] as String, + password: json['contrasenia'] as String, + ); +} \ No newline at end of file diff --git a/lib/models/user/rut.dart b/lib/models/user/rut.dart new file mode 100644 index 0000000..06f9c5d --- /dev/null +++ b/lib/models/user/rut.dart @@ -0,0 +1,35 @@ +class Rut { + int rut; + + get dv => _calcularDV(); + + Rut(this.rut); + + String _calcularDV() { + var rutString = rut.toString(); + var suma = 0; + var multiplo = 2; + for (var i = rutString.length - 1; i >= 0; i--) { + suma += int.parse(rutString[i]) * multiplo; + multiplo++; + if (multiplo > 7) { + multiplo = 2; + } + } + + final dv = 11 - (suma % 11); + return dv == 11 ? "0" : (dv == 10 ? "K" : dv.toString()); + } + + static Rut fromString(String rut) { + if(rut.contains("-")) { + rut = rut.split("-")[0]; + } + + return Rut(int.parse(rut)); + } + + @override + String toString() => "$rut-${dv.toUpperCase()}"; + +} \ No newline at end of file diff --git a/lib/models/user/user.dart b/lib/models/user/user.dart new file mode 100644 index 0000000..a39cc35 --- /dev/null +++ b/lib/models/user/user.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:mi_utem/models/user/rut.dart'; + +class User { + + String? token; + Rut? rut; + + String correoPersonal; + String? correoUtem; + + String? fotoBase64; + List perfiles; + + String nombreCompleto; + String? nombres; + String? apellidos; + + String? username; + String? fotoUrl; + + get nombreCompletoCapitalizado => nombreCompleto.toLowerCase().split(' ').map((it) => it[0].toUpperCase() + it.substring(1)).join(' '); + get nombreDisplayCapitalizado => "${nombres?.split(' ')[0]} $apellidos".toLowerCase().split(' ').map((it) => it[0].toUpperCase() + it.substring(1)).join(' '); + get primerNombre => nombreCompletoCapitalizado.split(' ')[0]; + get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0].toUpperCase()).join(''); + + User({ + this.token, + this.rut, + this.correoPersonal = "N/N", + this.correoUtem, + this.fotoBase64, + this.perfiles = const [], + this.nombreCompleto = "N/N", + this.nombres, + this.apellidos, + this.username, + this.fotoUrl + }); + + static List fromJsonList(List? list) { + if(list == null) { + return []; + } + + return list.map((json) => User.fromJson(json as Map)).toList(); + } + + factory User.fromJson(Map json) => User( + token: json['token'], + rut: json['rut'] is int ? Rut(json['rut'] as int) : Rut.fromString("${json['rut']}"), + correoPersonal: json['correoPersonal'], + correoUtem: json['correoUtem'], + fotoBase64: json['fotoBase64'], + perfiles: ((json['perfiles'] as List)).map((it) => it.toString()).toList(), + nombreCompleto: json['nombreCompleto'], + nombres: json['nombres'], + apellidos: json['apellidos'], + username: json['username'], + fotoUrl: json['fotoUrl'] + ); + + Map toJson() => { + 'token': token, + 'rut': rut?.rut, + 'correoPersonal': correoPersonal, + 'correoUtem': correoUtem, + 'fotoBase64': fotoBase64, + 'perfiles': perfiles, + 'nombreCompleto': nombreCompleto, + 'nombres': nombres, + 'apellidos': apellidos, + 'username': username, + 'fotoUrl': fotoUrl + }; + + @override + String toString() => jsonEncode(toJson()); +} \ No newline at end of file diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 8183c32..0d98f25 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/controllers/asignatura/asignatura_controller.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/screens/asignatura/asignatura_estudiantes_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; +import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; +import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; +import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:watch_it/watch_it.dart'; class _ITabs { final String label; @@ -25,32 +27,33 @@ class _ITabs { }); } -class AsignaturaDetalleScreen extends GetView { - AsignaturaDetalleScreen({Key? key}) : super(key: key); +class AsignaturaDetalleScreen extends StatelessWidget { + final String? asignaturaId; - String? get tag => Get.parameters['asignaturaId']; + AsignaturaDetalleScreen({ + super.key, + required this.asignaturaId, + }); List<_ITabs> _getTabs(Asignatura asignatura) => [ - _ITabs( - label: "Resumen", - child: AsignaturaResumenTab(asignatura: asignatura), - ), - _ITabs( - label: "Notas", - child: AsignaturaNotasTab(asignaturaId: asignatura.id!), - initial: true, - ), - if (asignatura.estudiantes != null && - asignatura.estudiantes!.length > 0) - _ITabs( - label: "Estudiantes", - child: AsignaturaEstudiantesTab(asignatura: asignatura), - ), - ]; + _ITabs( + label: "Resumen", + child: AsignaturaResumenTab(asignatura: asignatura), + ), + _ITabs( + label: "Notas", + child: AsignaturaNotasTab(asignaturaId: asignatura.id!), + initial: true, + ), + if (asignatura.estudiantes != null && + asignatura.estudiantes!.length > 0) + _ITabs( + label: "Estudiantes", + child: AsignaturaEstudiantesTab(asignatura: asignatura), + ), + ]; - bool get _mostrarCalculadora { - return RemoteConfigService.calculadoraMostrar; - } + bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; int _getInitialIndex(Asignatura asignatura) { final index = _getTabs(asignatura).indexWhere((tab) => tab.initial); @@ -61,51 +64,63 @@ class AsignaturaDetalleScreen extends GetView { Widget build(BuildContext context) { ReviewService.addScreen("AsignaturaScreen"); - return controller.obx( - (asignatura) => DefaultTabController( - initialIndex: asignatura != null ? _getInitialIndex(asignatura) : 0, - length: asignatura != null ? _getTabs(asignatura).length : 1, - child: Scaffold( - appBar: CustomAppBar( - title: Text(asignatura?.nombre ?? "Asigntura sin nombre"), - actions: _mostrarCalculadora - ? [ - IconButton( - icon: Icon(Mdi.calculator), - tooltip: "Calculadora", - onPressed: () { - Get.toNamed(Routes.calculadoraNotas); - if (asignatura?.grades != null) { - final calculatorController = CalculatorController.to; - calculatorController.loadGrades(asignatura!.grades!); - } - }, - ), - ] - : [], - bottom: asignatura != null - ? TabBar( - indicatorColor: Colors.white.withOpacity(0.8), - tabs: _getTabs(asignatura) - .map((tab) => Tab(text: tab.label)) - .toList(), - ) - : null, - ), - body: asignatura != null - ? TabBarView( - children: - _getTabs(asignatura).map((tab) => tab.child).toList(), - ) - : Container(), - ), - ), - onLoading: Scaffold( - appBar: CustomAppBar(), - body: Center( - child: LoadingIndicator(), - ), - ), + return FutureBuilder( + future: di.get().getDetalleAsignatura(asignaturaId), + builder: (context, snapshot) { + if(snapshot.connectionState == ConnectionState.waiting) { + return Scaffold( + appBar: CustomAppBar(), + body: Center( + child: LoadingIndicator(), + ), + ); + } else if (snapshot.hasError) { + final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "No sabemos que pasó, pero no pudimos cargar la asignatura."; + return CustomErrorWidget( + title: "Ocurrió un error", + error: error, + ); + } else if (snapshot.hasData) { + final asignatura = snapshot.data; + return DefaultTabController( + initialIndex: asignatura != null ? _getInitialIndex(asignatura) : 0, + length: asignatura != null ? _getTabs(asignatura).length : 1, + child: Scaffold( + appBar: CustomAppBar( + title: Text(asignatura?.nombre ?? "Asigntura sin nombre"), + actions: _mostrarCalculadora ? [ + IconButton( + icon: Icon(Mdi.calculator), + tooltip: "Calculadora", + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())); + if (asignatura?.grades != null) { + di.get().updateWithGrades(asignatura!.grades!); + } + }, + ), + ] : [], + bottom: asignatura != null ? TabBar( + indicatorColor: Colors.white.withOpacity(0.8), + tabs: _getTabs(asignatura) + .map((tab) => Tab(text: tab.label)) + .toList(), + ) : null, + ), + body: asignatura != null ? TabBarView( + children: _getTabs(asignatura).map((tab) => tab.child).toList(), + ) : Container(), + ), + ); + } + + return CustomErrorWidget( + title: "Ocurrió un error", + error: "No sabemos que pasó, pero no pudimos cargar la asignatura.", + ); + } ); } + + } diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index 1730e74..e6c2496 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,78 +1,95 @@ - import 'package:flutter/material.dart'; - import 'package:get/get_state_manager/get_state_manager.dart'; - import 'package:mi_utem/controllers/asignatura/asignatura_controller.dart'; - import 'package:mi_utem/models/evaluacion.dart'; - import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; - import 'package:mi_utem/widgets/custom_error_widget.dart'; - import 'package:mi_utem/widgets/loading_indicator.dart'; - import 'package:mi_utem/widgets/nota_list_item.dart'; - import 'package:mi_utem/widgets/pull_to_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; +import 'package:mi_utem/widgets/custom_error_widget.dart'; +import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/nota_list_item.dart'; +import 'package:mi_utem/widgets/pull_to_refresh.dart'; +import 'package:watch_it/watch_it.dart'; - class AsignaturaNotasTab extends GetView { - final String asignaturaId; +class AsignaturaNotasTab extends StatefulWidget { + final String asignaturaId; - AsignaturaNotasTab({ - Key? key, - required this.asignaturaId, - }) : super(key: key); + const AsignaturaNotasTab({ + super.key, + required this.asignaturaId, + }); - @override - String get tag => asignaturaId; + @override + State createState() => _AsignaturaNotasTabState(); +} - Future _onRefresh() async { - controller.refreshData(); - } +class _AsignaturaNotasTabState extends State { - @override - Widget build(BuildContext context) { - return PullToRefresh( - onRefresh: _onRefresh, - child: controller.obx((asignatura) => ListView( - physics: AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.all(10), - children: [ - NotasDisplayWidget( - notaFinal: asignatura?.grades?.notaFinal, - notaExamen: asignatura?.grades?.notaExamen, - notaPresentacion: asignatura?.grades?.notaPresentacion, - estado: asignatura?.estado, - colorPorEstado: asignatura?.colorPorEstado, - ), - Card( - child: Container( - padding: EdgeInsets.all(20), - child: asignatura?.grades?.notasParciales.isNotEmpty == true - ? ListView.builder( - shrinkWrap: true, - physics: ClampingScrollPhysics(), - itemBuilder: (context, i) { - REvaluacion evaluacion = asignatura!.grades!.notasParciales[i]; - return NotaListItem( - evaluacion: IEvaluacion.fromRemote(evaluacion), - /* onChanged: (evaluacion) { - _controller.chag(evaluacion, nota); - } */ - ); - }, - itemCount: asignatura!.grades!.notasParciales.length, - ) - : CustomErrorWidget( - emoji: "🤔", - title: "Parece que aún no hay notas ni ponderadores", - ), + Future _future = Future.value(null); + + @override + void initState() { + _future = di.get().getDetalleAsignatura(widget.asignaturaId, forceRefresh: true); + super.initState(); + } + + @override + Widget build(BuildContext context) => PullToRefresh( + onRefresh: () async { + setState(() { + _future = di.get().getDetalleAsignatura(widget.asignaturaId, forceRefresh: true); + }); + }, + child: FutureBuilder( + future: _future, + builder: (context, snapshot) { + final asignatura = snapshot.data; + if (snapshot.connectionState == ConnectionState.waiting) { + return LoadingIndicator(); + } else if (snapshot.hasError) { + final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "No sabemos que pasó, pero no pudimos cargar tus notas."; + return CustomErrorWidget( + title: "Ocurrió un error al cargar las notas", + error: error, + ); + } else if (snapshot.hasData) { + return ListView( + physics: AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.all(10), + children: [ + NotasDisplayWidget( + notaFinal: asignatura?.grades?.notaFinal, + notaExamen: asignatura?.grades?.notaExamen, + notaPresentacion: asignatura?.grades?.notaPresentacion, + estado: asignatura?.estado, + colorPorEstado: asignatura?.colorPorEstado, + ), + Card( + child: Container( + padding: EdgeInsets.all(20), + child: asignatura?.grades?.notasParciales.isNotEmpty == true ? ListView.builder( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemBuilder: (context, i) { + REvaluacion evaluacion = asignatura!.grades!.notasParciales[i]; + return NotaListItem(evaluacion: IEvaluacion.fromRemote(evaluacion)); + }, + itemCount: asignatura!.grades!.notasParciales.length, + ) : CustomErrorWidget( + emoji: "🤔", + title: "Parece que aún no hay notas ni ponderadores", ), ), - ], - ), - onLoading: LoadingIndicator(), - onError: (error) => SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: CustomErrorWidget( - title: "Ocurrió un error al cargar las notas", - error: '', - ), - ), - ), - ); - } - } + ), + ], + ); + } + + return CustomErrorWidget( + title: "Ocurrió un error al cargar las notas", + error: '', + ); + }, + ), + ); +} + diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index f343080..2992a5d 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -1,61 +1,104 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/controllers/asignatura/asignaturas_controller.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; import 'package:mi_utem/widgets/asignatura/lista/lista_asignaturas.dart'; import 'package:mi_utem/widgets/asignatura/lista/sin_asignaturas_mensaje.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; +import 'package:watch_it/watch_it.dart'; -class AsignaturasListaScreen extends GetView { - AsignaturasListaScreen({Key? key}) : super(key: key); +class AsignaturasListaScreen extends StatefulWidget with WatchItStatefulWidgetMixin { + const AsignaturasListaScreen({super.key}); - Future _onRefresh() async { - controller.refreshAsignaturas(); - } + @override + State createState() => _AsignaturasListaScreenState(); +} - bool get _mostrarCalculadora { - return RemoteConfigService.calculadoraMostrar; - } +class _AsignaturasListaScreenState extends State { + final _asignaturasService = di.get(); + + bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; @override Widget build(BuildContext context) { + final _carreraSeleccionada = watchValue((CarrerasService service) => service.selectedCarrera); + if(_carreraSeleccionada?.id == null) { + logger.d("Carrera seleccionada es nula! Refrescando..."); + di.get().getCarreras(forceRefresh: true); + } + return Scaffold( appBar: CustomAppBar( title: Text("Asignaturas"), - actions: _mostrarCalculadora - ? [ - IconButton( - icon: Icon(Mdi.calculator), - tooltip: "Calculadora", - onPressed: () => Get.toNamed(Routes.calculadoraNotas), - ), - ] - : [], + actions: _mostrarCalculadora ? [ + IconButton( + icon: Icon(Mdi.calculator), + tooltip: "Calculadora", + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())), + ), + ] : [], ), body: PullToRefresh( - onRefresh: () async => await _onRefresh(), - child: controller.obx( - (asignaturas) => asignaturas == null || asignaturas.isEmpty ? SinAsignaturasMensaje(mensaje: "Parece que no se encontraron asignaturas.", emoji: "🤔") : ListaAsignaturas(asignaturas: asignaturas), - onError: (error) => SinAsignaturasMensaje(mensaje: "Ocurrió un error al obtener las asignaturas", emoji: "😢"), - onLoading: Container( - padding: EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Center( - child: LoadingIndicator(), - ), - ), - ], - ), - ), + onRefresh: () async { + setState(() => {}); + }, + child: FutureBuilder?>( + future: _asignaturasService.getAsignaturas(_carreraSeleccionada?.id), + builder: (context, snapshot) { + if(snapshot.hasError) { + final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al obtener las asignaturas"; + return _errorWidget(error); + } + + if(snapshot.connectionState == ConnectionState.waiting) { + return _loadingWidget(); + } + + final asignaturas = snapshot.data ?? []; + if(asignaturas.isEmpty) { + return _errorWidget("No encontramos asignaturas. Por favor intenta más tarde."); + } + + return ListaAsignaturas(asignaturas: asignaturas); + }, ), ), ); } + + Widget _loadingWidget() => Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: LoadingIndicator(), + ), + ), + ], + ), + ); + + Widget _errorWidget(String mensaje) => Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: SinAsignaturasMensaje(mensaje: mensaje, emoji: "\u{1F622}"), + ), + ), + ], + ), + ); } diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index 53a5fc4..9fb822e 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -1,29 +1,31 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; import 'package:mi_utem/widgets/calculadora_notas/DisplayNotasWidget.dart'; import 'package:mi_utem/widgets/calculadora_notas/EditarNotasWidget.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; +import 'package:watch_it/watch_it.dart'; class CalculadoraNotasScreen extends StatelessWidget { - CalculadoraNotasScreen({ - Key? key, - }) : super(key: key); + + const CalculadoraNotasScreen({ + super.key, + }); @override Widget build(BuildContext context) { - final controller = CalculatorController.to; + final _calculatorService = di.get(); - controller.makeEditable(); + _calculatorService.makeEditable(); return Scaffold( appBar: CustomAppBar( - title: Text("Calculadora de notas"), + title: const Text("Calculadora de notas"), ), body: ListView( - padding: EdgeInsets.all(10), - children: [ - DisplayNotasWidget(calculatorController: controller), - EditarNotasWidget(calculatorController: controller) + padding: const EdgeInsets.all(10), + children: const [ + DisplayNotasWidget(), + EditarNotasWidget() ], ), ); diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index 998afc1..a8b347a 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_windowmanager/flutter_windowmanager.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/controllers/carreras_controller.dart'; import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/perfil_service.dart'; import 'package:mi_utem/services/review_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; import 'package:mi_utem/widgets/credencial_card.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/flip_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:watch_it/watch_it.dart'; class CredencialScreen extends StatefulWidget { CredencialScreen({ @@ -23,15 +24,13 @@ class CredencialScreen extends StatefulWidget { } class _CredencialScreenState extends State { - Future? _future; - Usuario? _usuario; + User? _usuario; FlipController _flipController = FlipController(); @override void initState() { ReviewService.addScreen("CredencialScreen"); _secureScreen(); - _future = _getData(); super.initState(); } @@ -49,30 +48,16 @@ class _CredencialScreenState extends State { super.dispose(); } - Future _getData() async { - try { - Usuario usuario = PerfilService.getLocalUsuario(); - setState(() { - _usuario = usuario; - }); - return usuario; - } catch (e) { - throw e; - } - } - @override Widget build(BuildContext context) { - Carrera? carreraActiva = CarrerasController.to.selectedCarrera.value; + Carrera? carreraActiva = watchValue((CarrerasService service) => service.selectedCarrera); return Scaffold( appBar: CustomAppBar( - title: Text("Credencial universitaria"), + title: const Text("Credencial universitaria"), actions: [ IconButton( - icon: Icon(_flipController.actualFace == FlipController.front - ? Icons.info - : Mdi.accountCircle), + icon: Icon(_flipController.actualFace == FlipController.front ? Icons.info : Mdi.accountCircle), onPressed: () { _flipController.flip!(); }, @@ -81,74 +66,70 @@ class _CredencialScreenState extends State { ), backgroundColor: Colors.grey[200], body: FutureBuilder( - future: _future, + future: di.get().getUser(), builder: (context, snapshot) { if (snapshot.hasError) { return CustomErrorWidget( - title: "Ocurrió un error al generar tu crendencial", - error: snapshot.error); - } else { - if (snapshot.hasData) { - if (_usuario!.rut != null && - carreraActiva!.nombre != null && - carreraActiva.nombre!.isNotEmpty) { - return Center( - child: SafeArea( - child: CredencialCard( - usuario: _usuario, - carrera: carreraActiva, - controller: _flipController, - onFlip: (direction) => _onFlip(), - ), - ), - ); - } else { - return Container( - padding: EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "😕", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 50, - ), - ), - Container(height: 15), - Text( - "Ocurrió un error al generar tu credencial", - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - Container(height: 15), - Text( - snapshot.error.toString(), - textAlign: TextAlign.center, - ), - ], + title: "Ocurrió un error al generar tu crendencial", + error: snapshot.error, + ); + } + + if (snapshot.hasData) { + if (_usuario!.rut != null && carreraActiva!.nombre != null && carreraActiva.nombre!.isNotEmpty) { + return Center( + child: SafeArea( + child: CredencialCard( + user: _usuario, + carrera: carreraActiva, + controller: _flipController, + onFlip: (direction) => _onFlip(), ), - ); - } - } else { - return Container( - padding: EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Center( - child: LoadingIndicator(), - ), - ), - ], ), ); } + + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("😕", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 50, + ), + ), + const SizedBox(height: 15), + const Text("Ocurrió un error al generar tu credencial", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 15), + Text(snapshot.error.toString(), + textAlign: TextAlign.center, + ), + ], + ), + ); } + + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Expanded( + child: Center( + child: LoadingIndicator(), + ), + ), + ], + ), + ); }, ), ); diff --git a/lib/screens/docentes_screen.dart b/lib/screens/docentes_screen.dart index 5875a03..2828276 100644 --- a/lib/screens/docentes_screen.dart +++ b/lib/screens/docentes_screen.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/screens/usuario_screen.dart'; import 'package:mi_utem/services/docentes_service.dart'; import 'package:mi_utem/utils/debounce.dart'; @@ -11,15 +9,17 @@ import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; class DocentesScreen extends StatefulWidget { - DocentesScreen({Key? key}) : super(key: key); + const DocentesScreen({ + super.key, + }); @override _DocentesScreenState createState() => _DocentesScreenState(); } class _DocentesScreenState extends State { - Future>? _futureDocentes; - late List _docentes; + Future>? _futureDocentes; + late List _docentes; late Debounce d; TextEditingController _controller = TextEditingController(); @@ -32,13 +32,13 @@ class _DocentesScreenState extends State { }); } - Future> _getDocentes(String nombre) async { + Future> _getDocentes(String nombre) async { setState(() { _docentes = []; _futureDocentes = null; _futureDocentes = DocentesService.buscarDocentes(nombre); }); - List docentes = await _futureDocentes!; + List docentes = await _futureDocentes!; setState(() => _docentes = docentes); return docentes; } @@ -58,100 +58,83 @@ class _DocentesScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar( - title: Text("Docentes"), + title: const Text("Docentes"), ), body: SingleChildScrollView( - padding: EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.symmetric(vertical: 20), child: Column( children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 15), + padding: const EdgeInsets.symmetric(horizontal: 15), child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( hintText: "Escribe para buscar un docente", prefixIcon: Icon(Icons.search), ), keyboardType: TextInputType.name, - onSubmitted: (query) { - _search(query); - }, + onSubmitted: _search, textCapitalization: TextCapitalization.words, textInputAction: TextInputAction.search, controller: _controller, - onChanged: (query) { - _search(query); - }, + onChanged: _search, ), ), - Container(height: 20), - _futureDocentes != null - ? FutureBuilder>( - future: _futureDocentes, - builder: (context, snapshot) { - if (snapshot.hasError) { - return CustomErrorWidget( - title: "Ocurrió un error al obtener los docentes", - error: snapshot.error, - ); - } else { - if (snapshot.hasData && snapshot.data != null) { - if (snapshot.data!.length > 0) { - return ListView.separated( - physics: ScrollPhysics(), - shrinkWrap: true, - separatorBuilder: (context, index) => - Divider(height: 5, indent: 20, endIndent: 20), - itemBuilder: (BuildContext context, int i) { - Usuario docente = _docentes[i]; - return ListTile( - leading: ProfilePhoto( - usuario: docente, - radius: 20, - editable: false, - ), - title: Text( - docente.nombreCompleto!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(docente.correoUtem ?? - docente.correoPersonal ?? - ""), - onTap: () async { - await Get.to( - () => UsuarioScreen( - tipo: 2, - query: {"nombre": docente.nombre}, - ), - routeName: Routes.perfil, - ); - }, - ); - }, - itemCount: _docentes.length, - ); - } else { - return CustomErrorWidget( - emoji: "🤔", - title: - "Parece que no se encontraron docentes que coincidan con tu búsqueda", - ); - } - } else { - return Container( - padding: EdgeInsets.all(20), - child: Center( - child: LoadingIndicator(), - ), - ); - } - } - }, - ) - : CustomErrorWidget( - emoji: "💅", - title: "Escribe para buscar un docente", - ), + const SizedBox(height: 20), + _futureDocentes != null ? FutureBuilder>( + future: _futureDocentes, + builder: (context, snapshot) { + if (snapshot.hasError) { + return CustomErrorWidget( + title: "Ocurrió un error al obtener los docentes", + error: snapshot.error, + ); + } + + final listaDocentes = snapshot.data; + + if(!snapshot.hasData || listaDocentes == null) { + return const Padding( + padding: EdgeInsets.all(20), + child: Center( + child: LoadingIndicator(), + ), + ); + } + + if (listaDocentes.isEmpty) { + return const CustomErrorWidget( + emoji: "\u{1F914}", // Emoji de cara pensando (mejor usar códigos unicode) + title: "Parece que no se encontraron docentes que coincidan con tu búsqueda", + ); + } + + return ListView.separated( + physics: ScrollPhysics(), + shrinkWrap: true, + itemCount: _docentes.length, + separatorBuilder: (context, index) => const Divider(height: 5, indent: 20, endIndent: 20), + itemBuilder: (BuildContext context, int i) { + User docente = _docentes[i]; + return ListTile( + leading: ProfilePhoto( + user: docente, + radius: 20, + editable: false, + ), + title: Text(docente.nombreCompleto ?? "N/N", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(docente.correoUtem ?? docente.correoPersonal ?? "N/N"), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => UsuarioScreen(tipo: 2, query: {"nombre": docente.nombreDisplayCapitalizado}))), + ); + }, + ); + }, + ) : CustomErrorWidget( + emoji: "💅", + title: "Escribe para buscar un docente", + ), ], ), ), diff --git a/lib/screens/login_screen/login_screen.dart b/lib/screens/login_screen/login_screen.dart index 7d0a4e8..e5912c7 100644 --- a/lib/screens/login_screen/login_screen.dart +++ b/lib/screens/login_screen/login_screen.dart @@ -4,102 +4,27 @@ import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get.dart'; import 'package:mi_utem/services/update_service.dart'; +import 'package:mi_utem/widgets/login_screen/background.dart'; import 'package:mi_utem/widgets/login_screen/creditos_app.dart'; import 'package:mi_utem/widgets/login_screen/formulario_credenciales.dart'; import 'package:mi_utem/widgets/login_screen/login_button.dart'; +import 'package:mi_utem/widgets/login_screen/login_form.dart'; import 'package:video_player/video_player.dart'; -part '_background.dart'; - -class LoginScreen extends StatefulWidget { - - final TextEditingController _correoController = TextEditingController(); - final TextEditingController _contraseniaController = TextEditingController(); - - final GlobalKey _formKey = GlobalKey(); - - LoginScreen({Key? key}) : super(key: key); - - @override - _LoginScreenState createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State { +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); @override - void initState() { - super.initState(); - - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: Brightness.light, - statusBarIconBrightness: Brightness.light, - systemNavigationBarColor: Colors.black, - systemNavigationBarIconBrightness: Brightness.light, + Widget build(BuildContext context) => Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.black, + body: LoginBackground( + child: LayoutBuilder( + builder: (context, BoxConstraints constraints) => LoginForm( + constraints: constraints, + ), ), - ); - - widget._correoController.text = ""; - widget._contraseniaController.text = ""; - - UpdateService(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - body: KeyboardVisibilityBuilder( - builder: (context, isKeyboardVisible) { - return _Background( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20), - child: ConstrainedBox( - constraints: constraints.copyWith( - minHeight: constraints.maxHeight, - maxHeight: double.infinity, - ), - child: IntrinsicHeight( - child: Form( - key: widget._formKey, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container(height: constraints.maxHeight * 0.1), - Expanded( - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Hero( - tag: 'utemLogo', - child: Image.asset('assets/images/utem_logo_color_blanco.png', width: 250), - ), - ], - ), - ), - ), - Container(height: constraints.maxHeight * 0.1), - FormularioCredenciales(correoController: widget._correoController, contraseniaController: widget._contraseniaController), - LoginButton(correoController: widget._correoController, contraseniaController: widget._contraseniaController, formKey: widget._formKey), - Container(height: constraints.maxHeight * 0.1), - if (!isKeyboardVisible) - CreditosApp(), - ], - ), - ), - ), - ), - ); - }, - ), - ); - }, - ), - ); - } + ), + ); } + diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 7639dd7..915446b 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -6,31 +6,37 @@ import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_markdown/flutter_markdown.dart"; import "package:get/get.dart"; -import 'package:mi_utem/controllers/grades_changes_controller.dart'; -import "package:mi_utem/models/usuario.dart"; -import "package:mi_utem/services/perfil_service.dart"; +import "package:mi_utem/models/user/user.dart"; import "package:mi_utem/services/remote_config/remote_config.dart"; import "package:mi_utem/services/review_service.dart"; +import "package:mi_utem/services_new/interfaces/auth_service.dart"; +import "package:mi_utem/services_new/interfaces/grades_service.dart"; import 'package:mi_utem/widgets/banners_section.dart'; import "package:mi_utem/widgets/custom_app_bar.dart"; import "package:mi_utem/widgets/custom_drawer.dart"; import "package:mi_utem/widgets/noticias/NoticiasCarruselWidget.dart"; import "package:mi_utem/widgets/permisos_section.dart"; import "package:mi_utem/widgets/quick_menu_section.dart"; +import "package:watch_it/watch_it.dart"; class MainScreen extends StatefulWidget { - final Usuario usuario; - MainScreen({Key? key, required this.usuario}) : super(key: key); + MainScreen({ + super.key, + }); @override _MainScreenState createState() => _MainScreenState(); } class _MainScreenState extends State { + + User? _user; + final _authService = di.get(); + @override void initState() { super.initState(); - PerfilService.saveFcmToken(); + _authService.saveFCMToken(); SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle( statusBarColor: Colors.transparent, @@ -43,20 +49,13 @@ class _MainScreenState extends State { ReviewService.addScreen("MainScreen"); ReviewService.checkAndRequestReview(); - } - @override - void dispose() { - super.dispose(); + _authService.getUser().then((user) => setState(() => _user = user)); } String get _greetingText { List texts = jsonDecode(RemoteConfigService.greetings); - - Random random = new Random(); - String text = texts[random.nextInt(texts.length)]; - text = text.replaceAll("%name", widget.usuario.primerNombre); - return text; + return texts[Random().nextInt(texts.length)].replaceAll("%name", _user?.primerNombre ?? "N/N"); } @override @@ -65,24 +64,19 @@ class _MainScreenState extends State { return Scaffold( appBar: CustomAppBar(title: Text("Inicio")), - drawer: CustomDrawer(usuario: widget.usuario), - floatingActionButton: kDebugMode - ? FloatingActionButton( - onPressed: () { - GradesChangesController.checkIfGradesHasChange(); - }, - tooltip: "Probar notificaciones de notas", - child: Icon( - Icons.notifications, - color: Colors.white, - ), - ) - : null, + drawer: CustomDrawer(), + floatingActionButton: kDebugMode ? FloatingActionButton( + onPressed: () => di.get().lookForGradeUpdates(), + tooltip: "Probar notificaciones de notas", + child: Icon(Icons.notifications, + color: Colors.white, + ), + ) : null, body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container(height: 20), + children: [ + const SizedBox(height: 20), Container( padding: EdgeInsets.symmetric(horizontal: 20), width: double.infinity, @@ -94,16 +88,14 @@ class _MainScreenState extends State { ), ), ), - Container(height: 20), + const SizedBox(height: 20), PermisosCovidSection(), - Container(height: 20), + const SizedBox(height: 20), QuickMenuSection(), - Container(height: 20), + const SizedBox(height: 20), if (banners.isNotEmpty) ...[ - BannersSection( - banners: banners, - ), - Container(height: 20), + BannersSection(banners: banners), + const SizedBox(height: 20), ], NoticiasCarruselWidget(), ], diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index ade51a8..adb64e5 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -7,40 +7,57 @@ import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:get/get.dart'; import 'package:image/image.dart' as dartImage; import 'package:intl/intl.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/controllers/qr_pass_controller.dart'; import 'package:mi_utem/models/permiso_covid.dart'; -import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; import 'package:mi_utem/widgets/image_view_screen.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; +import 'package:watch_it/watch_it.dart'; -class PermisoCovidScreen extends GetView { - const PermisoCovidScreen({ - Key? key, - }) : super(key: key); +class PermisoCovidScreen extends StatelessWidget { - @override - String? get tag => Get.parameters[Routes.passParameter]; + final String passId; + + const PermisoCovidScreen({ + super.key, + required this.passId, + }); @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar(title: Text("Permiso de ingreso")), - body: controller.obx( - (pass) => SingleChildScrollView( - child: LoadedScreen(permiso: pass!), - ), - onLoading: Center( - child: LoadingIndicator( - message: "Esto tardará un poco, paciencia...", - ), - ), - onEmpty: Text('No data found'), - onError: (error) => CustomErrorWidget(), + body: FutureBuilder( + future: di.get().getDetallesPermiso(passId), + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return const CustomErrorWidget(); + } + + final permiso = snapshot.data; + if(!snapshot.hasData) { + return Center( + child: LoadingIndicator( + message: "Esto tardará un poco, paciencia...", + ), + ); + } + + if(permiso == null) { + return const CustomErrorWidget( + title: "Permiso no encontrado", + emoji: "\u{1F914}", + ); + } + + return SingleChildScrollView( + child: LoadedScreen(permiso: permiso), + ); + }, ), ); } @@ -54,7 +71,7 @@ class LoadedScreen extends StatelessWidget { final PermisoCovid permiso; - _openQr(String heroTag) { + _openQr(BuildContext context, String heroTag) { final image = dartImage.Image(500, 500); dartImage.fill(image, dartImage.getColor(255, 255, 255)); @@ -70,78 +87,71 @@ class LoadedScreen extends StatelessWidget { Uint8List data = Uint8List.fromList(dartImage.encodePng(image)); - Get.to( - () => ImageViewScreen( - imageProvider: MemoryImage(data), - heroTag: heroTag, - occlude: true, - ), - routeName: Routes.imageView, - ); + Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen( + imageProvider: MemoryImage(data), + heroTag: heroTag, + occlude: true, + ))); } @override - Widget build(BuildContext context) { - final f = new DateFormat('dd/MM/yyyy'); - return Padding( - padding: const EdgeInsets.all(20.0), - child: Card( - child: Column( - children: [ - UsuarioDetalle( - usuario: permiso.usuario!, - ), - Divider(thickness: 1, color: Color(0xFFFEEEEE)), - DetallesPermiso( - campus: permiso.campus, - dependencias: permiso.dependencia, - jornada: permiso.jornada, - vigencia: permiso.vigencia, - motivo: permiso.motivo, - ), - Divider(thickness: 1, color: Color(0xFFFEEEEE)), - Container(height: 20), - Center( - child: InkWell( - onTap: () => _openQr("qr_${permiso.codigoQr!}"), - child: Hero( - tag: "qr_${permiso.codigoQr!}", - child: Container( - color: Colors.white, - padding: EdgeInsets.all(10), - child: OccludeWrapper( - child: BarcodeWidget( - barcode: Barcode.qrCode(), - height: 200, - width: 200, - data: permiso.codigoQr!, - drawText: false, - ), + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(20.0), + child: Card( + child: Column( + children: [ + UsuarioDetalle( + user: permiso.user!, + ), + Divider(thickness: 1, color: Color(0xFFFEEEEE)), + DetallesPermiso( + campus: permiso.campus, + dependencias: permiso.dependencia, + jornada: permiso.jornada, + vigencia: permiso.vigencia, + motivo: permiso.motivo, + ), + Divider(thickness: 1, color: Color(0xFFFEEEEE)), + Container(height: 20), + Center( + child: InkWell( + onTap: () => _openQr(context, "qr_${permiso.codigoQr!}"), + child: Hero( + tag: "qr_${permiso.codigoQr!}", + child: Container( + color: Colors.white, + padding: EdgeInsets.all(10), + child: OccludeWrapper( + child: BarcodeWidget( + barcode: Barcode.qrCode(), + height: 200, + width: 200, + data: permiso.codigoQr!, + drawText: false, ), ), ), ), ), - Container(height: 20), - Text( - "Permiso generado el ${f.format(permiso.fechaSolicitud!)}", - style: Get.textTheme.bodySmall, - ), - Container(height: 20), - ], - ), + ), + Container(height: 20), + Text("Permiso generado el ${DateFormat('dd/MM/yyyy').format(permiso.fechaSolicitud!)}", + style: Get.textTheme.bodySmall, + ), + Container(height: 20), + ], ), - ); - } + ), + ); } class UsuarioDetalle extends StatelessWidget { - const UsuarioDetalle({ - Key? key, - required this.usuario, - }) : super(key: key); + final User user; - final Usuario usuario; + const UsuarioDetalle({ + super.key, + required this.user, + }); @override Widget build(BuildContext context) { @@ -149,20 +159,18 @@ class UsuarioDetalle extends StatelessWidget { padding: const EdgeInsets.all(20.0), child: Row( children: [ - ProfilePhoto(usuario: usuario), + ProfilePhoto(user: user), Container(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - usuario.nombre!, + Text(user.nombreDisplayCapitalizado, maxLines: 2, style: Get.textTheme.bodyLarge, ), Container(height: 4), - Text( - usuario.rut!.formateado(true), + Text(user.rut.toString(), style: Get.textTheme.bodyMedium, ), ], diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 7cb983b..4e042c3 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -2,9 +2,14 @@ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/screens/login_screen/login_screen.dart'; +import 'package:mi_utem/screens/main_screen.dart'; +import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/notification_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:watch_it/watch_it.dart'; class SplashScreen extends StatefulWidget { SplashScreen({Key? key}) : super(key: key); @@ -14,7 +19,8 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { - var delayed; + + final _authService = di.get(); @override void initState() { @@ -31,81 +37,63 @@ class _SplashScreenState extends State { ); } - bool _terminoAnimacion = false; - - void _onEndAnimacion() async { - _terminoAnimacion = true; - _cambiarPantalla(); - } - - void _cambiarPantalla() async { - if (_terminoAnimacion) { - Get.offAllNamed( - Routes.home, - ); - await NotificationService.requestUserPermissionIfNecessary(); - } - } - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Get.theme.primaryColor, - body: Stack( - children: [ - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topRight, - end: Alignment.bottomLeft, - colors: [Color(0xff1d8e5c), Color(0xff06607a)], - ), + Widget build(BuildContext context) => Scaffold( + backgroundColor: Get.theme.primaryColor, + body: Stack( + children: [ + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [Color(0xff1d8e5c), Color(0xff06607a)], ), ), - Column( - children: [ - Expanded( - child: Padding( - padding: EdgeInsets.all(20), - child: Hero( - tag: 'utemLogo', - child: FlareActor( - "assets/animations/utem.flr", - alignment: Alignment.center, - fit: BoxFit.contain, - animation: "default", - callback: (String val) { - _onEndAnimacion(); - }, - ), + ), + Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(20), + child: Hero( + tag: 'utemLogo', + child: FlareActor("assets/animations/utem.flr", + alignment: Alignment.center, + fit: BoxFit.contain, + animation: "default", + callback: (String val) async { + await NotificationService.requestUserPermissionIfNecessary(); + final isLoggedIn = await _authService.isLoggedIn(); + if(!isLoggedIn) { + AnalyticsService.removeUser(); + } else { + final user = await _authService.getUser(); + if(user != null) { + AnalyticsService.setUser(user); + } + } + Navigator.pushReplacement(context, MaterialPageRoute(builder: (ctx) => isLoggedIn ? MainScreen() : LoginScreen())); + }, ), ), ), - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, snapshot) { - if (!snapshot.hasError && - snapshot.hasData && - snapshot.data != null) { - PackageInfo packageInfo = snapshot.data!; - return Container( - padding: EdgeInsets.all(10), - child: Text( - "Versión: ${packageInfo.version}", - style: TextStyle( - color: Colors.white.withOpacity(0.5), - ), - ), - ); - } else { - return Container(); - } - }, - ), - ], - ), - ], - ), - ); - } + ), + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) => !snapshot.hasError && snapshot.hasData && snapshot.data != null ? Padding( + padding: const EdgeInsets.all(10), + child: Text("Versión: ${snapshot.data?.version}", + style: TextStyle( + color: Colors.white.withOpacity(0.5), + ), + textAlign: TextAlign.center, + ), + ) : Container(), + ), + ], + ), + ], + ), + ); } diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index d8c2589..72e293c 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -3,12 +3,11 @@ import 'dart:core'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/docentes_service.dart'; -import 'package:mi_utem/services/perfil_service.dart'; import 'package:mi_utem/services/review_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/image_view_screen.dart'; @@ -16,55 +15,66 @@ import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:watch_it/watch_it.dart'; class UsuarioScreen extends StatefulWidget { final int tipo; final Map? query; final Asignatura? asignatura; - UsuarioScreen({Key? key, this.tipo = 0, this.query, this.asignatura}) - : super(key: key); + + UsuarioScreen({ + super.key, + this.tipo = 0, + this.query, + this.asignatura, + }); @override _UsuarioScreenState createState() => _UsuarioScreenState(); } class _UsuarioScreenState extends State { - Future? _usuarioFuture; - Usuario? _usuario; + final _authService = di.get(); + + Future? _userFuture; + User? _user; @override void initState() { ReviewService.addScreen("UsuarioScreen"); super.initState(); - _usuarioFuture = _getUsuario(); + _userFuture = _getUsuario(); } - Future _getUsuario() async { + Future _getUsuario() async { try { - Usuario usuario; + User? user; if (widget.tipo == 2) { print(widget.query); if (widget.asignatura == null) { - usuario = - await DocentesService.traerUnDocente(widget.query!["nombre"]); + user = await DocentesService.traerUnDocente(widget.query!["nombre"]); } else { - usuario = await DocentesService.asignarUnDocente( + user = await DocentesService.asignarUnDocente( widget.query!["nombre"], widget.asignatura!.codigo, widget.asignatura!.nombre); } setState(() { - _usuario = usuario; + _user = user; }); } else { - usuario = PerfilService.getLocalUsuario(); + user = await _authService.getUser(); setState(() { - _usuario = usuario; + _user = user; }); } - return usuario; + if (user == null) { + throw "No se pudo obtener el usuario"; + } + + return user; } catch (e) { throw e; } @@ -77,11 +87,11 @@ class _UsuarioScreenState extends State { ); try { - Usuario usuario = await PerfilService.changeFoto(imagen); + User? user = await _authService.updateProfilePicture(imagen); Get.back(); setState(() { - _usuario = usuario; + _user = user; }); return; @@ -101,16 +111,16 @@ class _UsuarioScreenState extends State { List get _datosPersonales { List lista = []; - if(_usuario == null) { + if(_user == null) { return lista; } - if (_usuario?.nombre?.isEmpty == false) { + if (_user?.nombreDisplayCapitalizado?.isEmpty == false) { lista.add(ListTile( title: Text("Nombre", style: TextStyle(color: Colors.grey), ), - subtitle: Text(_usuario!.nombreCompleto!, + subtitle: Text(_user!.nombreCompleto!, style: TextStyle( color: Colors.grey[900], fontSize: 18, @@ -118,13 +128,13 @@ class _UsuarioScreenState extends State { ), )); } else { - if (_usuario?.nombres?.isEmpty == false) { + if (_user?.nombres?.isEmpty == false) { lista.add( ListTile( title: Text("Nombres", style: TextStyle(color: Colors.grey), ), - subtitle: Text(_usuario!.nombres!, + subtitle: Text(_user!.nombres!, style: TextStyle( color: Colors.grey[900], fontSize: 18, @@ -134,14 +144,14 @@ class _UsuarioScreenState extends State { ); } - if (_usuario?.apellidos?.isEmpty == false) { + if (_user?.apellidos?.isEmpty == false) { lista.add(Divider(height: 1)); lista.add( ListTile( title: Text("Apellidos", style: TextStyle(color: Colors.grey), ), - subtitle: Text(_usuario!.apellidos!, + subtitle: Text(_user!.apellidos!, style: TextStyle( color: Colors.grey[900], fontSize: 18, @@ -152,14 +162,14 @@ class _UsuarioScreenState extends State { } } - if (_usuario?.correoUtem?.isEmpty == false) { + if (_user?.correoUtem?.isEmpty == false) { lista.add(Divider(height: 1)); lista.add(ListTile( title: Text("Correo Institucional", style: TextStyle(color: Colors.grey), ), onLongPress: widget.tipo != 0 ? () async { - await FlutterClipboard.copy(_usuario!.correoUtem!); + await FlutterClipboard.copy(_user!.correoUtem!); Get.snackbar( "¡Copiado!", "Correo copiado al portapapeles", @@ -170,9 +180,9 @@ class _UsuarioScreenState extends State { ); } : null, onTap: widget.tipo != 0 ? () async { - await launchUrl(Uri.parse("mailto:${_usuario?.correoUtem ?? ""}")); + await launchUrl(Uri.parse("mailto:${_user?.correoUtem ?? ""}")); } : null, - subtitle: Text(_usuario!.correoUtem ?? "", + subtitle: Text(_user!.correoUtem ?? "", style: TextStyle( color: Colors.grey[900], fontSize: 18, @@ -180,7 +190,7 @@ class _UsuarioScreenState extends State { ), )); } - if (_usuario?.correoPersonal?.isEmpty == false) { + if (_user?.correoPersonal?.isEmpty == false) { lista.add(Divider(height: 1)); lista.add( ListTile( @@ -188,7 +198,7 @@ class _UsuarioScreenState extends State { style: TextStyle(color: Colors.grey), ), onLongPress: widget.tipo != 0 ? () async { - await FlutterClipboard.copy(_usuario!.correoPersonal!); + await FlutterClipboard.copy(_user!.correoPersonal!); Get.snackbar( "¡Copiado!", "Correo copiado al portapapeles", @@ -199,9 +209,9 @@ class _UsuarioScreenState extends State { ); } : null, onTap: widget.tipo != 0 ? () async { - await launchUrl(Uri.parse("mailto:${_usuario?.correoPersonal ?? ""}")); + await launchUrl(Uri.parse("mailto:${_user?.correoPersonal ?? ""}")); } : null, - subtitle: Text(_usuario!.correoPersonal ?? "", + subtitle: Text(_user!.correoPersonal ?? "", style: TextStyle( color: Colors.grey[900], fontSize: 18, @@ -211,13 +221,13 @@ class _UsuarioScreenState extends State { ); } - if (widget.tipo == 0 && _usuario!.rut != null) { + if (widget.tipo == 0 && _user!.rut != null) { lista.add(Divider(height: 1)); lista.add(ListTile( title: Text("RUT", style: TextStyle(color: Colors.grey), ), - subtitle: Text(_usuario!.rut!.formateado(true), + subtitle: Text("${_user!.rut}", style: TextStyle( color: Colors.grey[900], fontSize: 18, @@ -236,69 +246,64 @@ class _UsuarioScreenState extends State { title: Text(widget.tipo == 0 ? "Perfil" : widget.query!["nombre"]), ), body: FutureBuilder( - future: _usuarioFuture, + future: _userFuture, builder: (context, snapshot) { if (snapshot.hasError) { return CustomErrorWidget( - title: "Ocurrió un error al obtener el perfil", - error: snapshot.error); - } else { - if (snapshot.hasData) { - return SingleChildScrollView( - padding: EdgeInsets.symmetric(vertical: 20), - child: Stack( - children: [ - Container( - alignment: Alignment.topCenter, - margin: EdgeInsets.only(top: 80), - child: Card( - margin: EdgeInsets.all(20), - child: ListView( - padding: EdgeInsets.only(bottom: 10, top: 20), - shrinkWrap: true, - physics: ClampingScrollPhysics(), - children: _datosPersonales, - ), + title: "Ocurrió un error al obtener el perfil", + error: snapshot.error, + ); + } + + if (snapshot.hasData) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Stack( + children: [ + Container( + alignment: Alignment.topCenter, + margin: const EdgeInsets.only(top: 80), + child: Card( + margin: const EdgeInsets.all(20), + child: ListView( + padding: const EdgeInsets.only(bottom: 10, top: 20), + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: _datosPersonales, ), ), - Center( - child: ProfilePhoto( - usuario: _usuario, - radius: 60, - editable: widget.tipo == 0, - onImage: widget.tipo == 0 - ? (image) { - _changeFoto(image); - } - : null, - onImageTap: (context, imageProvider) { - Get.to( - () => ImageViewScreen( - imageProvider: imageProvider, - ), - routeName: Routes.imageView, - ); - }), + ), + Center( + child: ProfilePhoto( + user: _user, + radius: 60, + editable: widget.tipo == 0, + onImage: widget.tipo == 0 ? _changeFoto : null, + onImageTap: (context, imageProvider) { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen( + imageProvider: imageProvider, + ))); + }, ), - ], - ), - ); - } else { - return Container( - padding: EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Center( - child: LoadingIndicator(), - ), - ), - ], - ), - ); - } + ), + ], + ), + ); } + + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Expanded( + child: Center( + child: LoadingIndicator(), + ), + ), + ], + ), + ); }, ), ); diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart index 807356b..fe82d29 100644 --- a/lib/services/analytics_service.dart +++ b/lib/services/analytics_service.dart @@ -1,117 +1,69 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/models/usuario.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class AnalyticsService { - static Future setUser( - Usuario user, - ) async { - await FirebaseAnalytics.instance.setUserId( - id: user.correoUtem, - ); + static Future setUser(User user) async { + await FirebaseAnalytics.instance.setUserId(id: user.correoUtem); await FlutterUxcam.setUserIdentity(user.correoUtem); if (user.rut != null) { - await FirebaseAnalytics.instance.setUserProperty( - name: "rut", - value: user.rut?.formateado(false), - ); - - await FlutterUxcam.setUserProperty( - "rut", - user.rut!.formateado(false), - ); + await FirebaseAnalytics.instance.setUserProperty(name: "rut", value: user.rut?.toString()); + await FlutterUxcam.setUserProperty("rut", user.rut!.toString()); } - if (user.nombre != null) { - await FirebaseAnalytics.instance.setUserProperty( - name: "name", - value: user.nombre, - ); - - await FlutterUxcam.setUserProperty( - "name", - user.nombre!, - ); + if (user.primerNombre != null) { + await FirebaseAnalytics.instance.setUserProperty(name: "name", value: user.primerNombre); + await FlutterUxcam.setUserProperty("name", user.primerNombre); } if (user.apellidos != null) { - await FirebaseAnalytics.instance.setUserProperty( - name: "last_name", - value: user.apellidos, - ); - - await FlutterUxcam.setUserProperty( - "last_name", - user.apellidos!, - ); + await FirebaseAnalytics.instance.setUserProperty(name: "last_name", value: user.apellidos); + await FlutterUxcam.setUserProperty("last_name", user.apellidos!); } - await Sentry.configureScope( - (scope) => scope.setUser( - SentryUser( - id: user.correoUtem, - email: user.correoPersonal, - name: user.nombreCompleto, - data: { - "rut": user.rut?.formateado(false), - }, - ipAddress: "{{auto}}", - ), + await Sentry.configureScope((scope) => scope.setUser( + SentryUser( + id: user.correoUtem, + email: user.correoPersonal, + name: user.nombreCompleto, + data: { + "rut": user.rut?.toString(), + }, + ipAddress: "{{auto}}", ), - ); + )); } static Future setCarreraToUser(Carrera carrera) async { if (carrera.nombre != null) { - await FirebaseAnalytics.instance.setUserProperty( - name: "carreraActiva", - value: carrera.nombre!, - ); - - await FlutterUxcam.setUserProperty( - "carreraActiva", - carrera.nombre!, - ); + await FirebaseAnalytics.instance.setUserProperty(name: "carreraActiva", value: carrera.nombre!); + await FlutterUxcam.setUserProperty("carreraActiva", carrera.nombre!); } if (carrera.estado != null) { - await FirebaseAnalytics.instance.setUserProperty( - name: "estadoCarreraActiva", - value: carrera.estado!, - ); - - await FlutterUxcam.setUserProperty( - "estadoCarreraActiva", - carrera.estado!, - ); + await FirebaseAnalytics.instance.setUserProperty(name: "estadoCarreraActiva", value: carrera.estado!); + await FlutterUxcam.setUserProperty("estadoCarreraActiva", carrera.estado!); } } static Future removeUser() async { await FirebaseAnalytics.instance.setUserId(id: null); await FlutterUxcam.setUserIdentity(null); - await Sentry.configureScope( - (scope) => scope.setUser(null), - ); + await Sentry.configureScope((scope) => scope.setUser(null)); } - static Future logEvent( - String name, { - Map? parameters, - }) async { + static Future logEvent(String name, {Map? parameters}) async { await FirebaseAnalytics.instance.logEvent( name: name, parameters: parameters, ); if (parameters != null) { - await FlutterUxcam.logEventWithProperties( - name, - parameters, - ); + await FlutterUxcam.logEventWithProperties(name, parameters); } else { await FlutterUxcam.logEvent(name); } diff --git a/lib/services/asignaturas_service.dart b/lib/services/asignaturas_service.dart deleted file mode 100644 index 9bb7352..0000000 --- a/lib/services/asignaturas_service.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/services/perfil_service.dart'; -import 'package:mi_utem/utils/dio_miutem_client.dart'; - -class AsignaturasService { - static final Dio _dio = DioMiUtemClient.authDio; - static final GetStorage box = GetStorage(); - - static Future> getAsignaturas( - String carreraId, { - bool forceRefresh = false, - }) async { - final uri = "/v1/carreras/$carreraId/asignaturas"; - final user = PerfilService.getLocalUsuario(); - - try { - Response response = await _dio.get( - uri, - options: buildCacheOptions( - Duration(days: 7), - forceRefresh: forceRefresh, - subKey: user.rut?.numero.toString(), - ), - ); - - List asignaturas = Asignatura.fromJsonList(response.data); - - return asignaturas; - } on DioError catch (e) { - print(e.message); - throw e; - } - } - - static Future getDetalleAsignatura( - String? codigo, { - bool forceRefresh = false, - }) async { - final uri = "/v1/asignaturas/$codigo"; - final user = PerfilService.getLocalUsuario(); - - try { - Response response = await _dio.get( - uri, - options: buildCacheOptions( - Duration(days: 7), - forceRefresh: forceRefresh, - subKey: user.rut?.numero.toString(), - ), - ); - - Asignatura asignatura = Asignatura.fromJson(response.data); - - return asignatura; - } on DioError catch (e) { - print(e.message); - throw e; - } - } -} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart deleted file mode 100644 index c871591..0000000 --- a/lib/services/auth_service.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:get/get.dart' as GetX; -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/helpers/snackbars.dart'; -import 'package:mi_utem/models/usuario.dart'; -import 'package:mi_utem/services/perfil_service.dart'; -import 'package:mi_utem/utils/dio_miutem_client.dart'; - -class AuthService { - static final Dio _dio = DioMiUtemClient.initDio; - static final GetStorage box = GetStorage(); - - static Future login(String? correo, String? contrasenia, - [bool guardar = false]) async { - String uri = "/v1/auth"; - - try { - final FlutterSecureStorage storage = new FlutterSecureStorage(); - - dynamic data = {'correo': correo, 'contrasenia': contrasenia}; - - Response response = await _dio.post(uri, data: data); - - Usuario usuario = Usuario(); - if (response.statusCode == 200) { - usuario = Usuario.fromJson(response.data); - box.write('token', usuario.token!); - if (usuario.nombres != null) { - box.write('nombres', usuario.nombres!); - } - if (usuario.apellidos != null) { - box.write('apellidos', usuario.apellidos!); - } - if (usuario.nombre != null) { - box.write('nombre', usuario.nombre!); - } - if (usuario.fotoUrl != null) { - box.write('fotoUrl', usuario.fotoUrl!); - } - if (usuario.correoUtem != null) { - box.write('correoUtem', usuario.correoUtem!); - } - if (usuario.correoPersonal != null) { - box.write('correoPersonal', usuario.correoPersonal!); - } - if (usuario.rut?.numero != null) { - box.write('rut', usuario.rut!.numero!); - } - box.write('esAntiguo', true); - - if (guardar) { - await storage.write(key: "contrasenia", value: contrasenia); - } - } - return usuario; - } on DioError catch (e) { - print(e.message); - throw e; - } catch (e) { - throw e; - } - } - - static Future esPrimeraVez() async { - try { - bool? esAntiguo = box.read("esAntiguo"); - if (esAntiguo == null) { - return true; - } else { - return !esAntiguo; - } - } catch (e) { - print(e); - return false; - } - } - - static bool isLoggedIn() { - try { - String? token = box.read("token"); - bool isLoggedIn = token != null && token.isNotEmpty; - - return isLoggedIn; - } catch (e) { - print(e); - return false; - } - } - - static Future logOut() async { - try { - final FlutterSecureStorage storage = new FlutterSecureStorage(); - - box.remove("token"); - box.remove("correo"); - box.remove("nombres"); - box.remove("nombre"); - box.remove("apellidos"); - box.remove("fotoUrl"); - box.remove("rut"); - await storage.deleteAll(); - try { - await PerfilService.deleteFcmToken(); - } catch (e) {} - } catch (e) { - print(e.toString()); - throw e; - } - } - - static Future refreshToken() async { - String uri = "/v1/auth/refresh"; - - try { - final FlutterSecureStorage secureStorage = new FlutterSecureStorage(); - - String? correo = box.read("correoUtem"); - String? contrasenia = await secureStorage.read(key: "contrasenia"); - - if(correo == null || contrasenia == null) { - await logOut(); - GetX.Get.toNamed(Routes.login); - showDefaultSnackbar("Error", "Lo sentimos, pero parece que tus credenciales no son válidas. Por favor, vuelve a iniciar sesión."); - throw Exception("No se pudo refrescar el token (no hay credenciales)"); - } - - dynamic data = {'correo': correo, 'contrasenia': contrasenia}; - - Response response = await DioMiUtemClient.authDio.post(uri, data: data); - - String token = response.data['token']; - _storeToken(token); - - return token; - } catch (e) { - print(e.toString()); - rethrow; - } - } - - static void _storeToken(String token) async { - box.write('token', token); - } - - static String getToken() { - final token = box.read('token'); - if (token == null) throw Exception("No se ha encontrado el token"); - - return token; - } - - static void invalidateToken() async { - box.remove('token'); - } -} diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 207e5c0..cb6a1e6 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'package:background_fetch/background_fetch.dart'; -import 'package:mi_utem/controllers/grades_changes_controller.dart'; +import 'package:mi_utem/services_new/interfaces/grades_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:watch_it/watch_it.dart'; class BackgroundController { // [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true` @@ -38,7 +39,7 @@ class BackgroundService { requiredNetworkType: NetworkType.NONE, ), (String taskId) async { - await GradesChangesController.checkIfGradesHasChange(); + await di.get().lookForGradeUpdates(); BackgroundFetch.finish(taskId); }, diff --git a/lib/services/carreras_service.dart b/lib/services/carreras_service.dart deleted file mode 100644 index 3326442..0000000 --- a/lib/services/carreras_service.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/services/perfil_service.dart'; -import 'package:mi_utem/utils/dio_miutem_client.dart'; - -class CarreraService { - static final Dio _dio = DioMiUtemClient.authDio; - - static Future> getCarreras({bool forceRefresh = false}) async { - const uri = "/v1/carreras"; - final user = PerfilService.getLocalUsuario(); - - Response response = await _dio.get( - uri, - options: buildCacheOptions( - Duration(days: 7), - forceRefresh: forceRefresh, - subKey: user.rut?.numero.toString(), - ), - ); - - List carreras = Carrera.fromJsonList(response.data); - - return carreras; - } -} diff --git a/lib/services/docentes_service.dart b/lib/services/docentes_service.dart index 888ff4b..2a705a9 100644 --- a/lib/services/docentes_service.dart +++ b/lib/services/docentes_service.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:http/http.dart' as http; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/utils/dio_docente_client.dart'; @@ -7,14 +8,14 @@ import 'package:mi_utem/utils/dio_docente_client.dart'; class DocentesService { static final Dio _dio = DioDocenteClient.initDio; - static Future generarImagenPerfil(Usuario usuario) async { + static Future generarImagenPerfil(User user) async { String baseUrl = "https://mi.utem.cl/static/interdocs/fotos/"; List formatos = [".jpg", ".jpeg", ".png", ".gif"]; - String imageUrl = "$baseUrl${usuario.rut!.numero}${formatos[0]}"; + String imageUrl = "$baseUrl${user.rut?.rut}${formatos[0]}"; - for (var formato in formatos) { - String actualImageUrl = "$baseUrl${usuario.rut!.numero}$formato"; + for (final formato in formatos) { + String actualImageUrl = "$baseUrl${user.rut?.rut}$formato"; final imageResponse = await http.head(Uri.parse(actualImageUrl)); if (imageResponse.statusCode == 200) { @@ -26,7 +27,7 @@ class DocentesService { return imageUrl; } - static Future> buscarDocentes(String nombre) async { + static Future> buscarDocentes(String nombre) async { String uri = "/docentes/buscar"; try { @@ -34,10 +35,10 @@ class DocentesService { Response response = await _dio.get(uri, queryParameters: data); - List usuarios = Usuario.fromJsonList(response.data["docentes"]); - List usuariosConFoto = []; + List usuarios = User.fromJsonList(response.data["docentes"]); + List usuariosConFoto = []; for (var usuario in usuarios) { - Usuario usuarioConFoto = usuario; + User usuarioConFoto = usuario; usuarioConFoto.fotoUrl = await generarImagenPerfil(usuario); usuariosConFoto.add(usuarioConFoto); @@ -49,7 +50,7 @@ class DocentesService { } } - static Future traerUnDocente(String? nombre) async { + static Future traerUnDocente(String? nombre) async { String uri = "/docentes/buscar"; try { @@ -57,20 +58,16 @@ class DocentesService { Response response = await _dio.get(uri, queryParameters: data); - Usuario usuario = Usuario.fromJson(response.data); - - Usuario usuarioConFoto = usuario; - usuarioConFoto.fotoUrl = await generarImagenPerfil(usuario); - - return usuarioConFoto; + User user = User.fromJson(response.data); + user.fotoUrl = await generarImagenPerfil(user); + return user; } on DioError catch (e) { print(e.message); throw e; } } - static Future asignarUnDocente(String? nombreDocente, - String? codigoAsignatura, String? nombreAsignatura) async { + static Future asignarUnDocente(String? nombreDocente, String? codigoAsignatura, String? nombreAsignatura) async { String uri = "/docentes/asignar"; try { @@ -82,12 +79,9 @@ class DocentesService { Response response = await _dio.post(uri, data: data); - Usuario usuario = Usuario.fromJson(response.data); - - Usuario usuarioConFoto = usuario; - usuarioConFoto.fotoUrl = await generarImagenPerfil(usuario); - - return usuarioConFoto; + User user = User.fromJson(response.data); + user.fotoUrl = await generarImagenPerfil(user); + return user; } on DioError catch (e) { print(e.message); throw e; diff --git a/lib/services/grades_service.dart b/lib/services/grades_service.dart deleted file mode 100644 index 4d13597..0000000 --- a/lib/services/grades_service.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/controllers/grades_changes_controller.dart'; -import 'package:mi_utem/models/grades.dart'; -import 'package:mi_utem/services/perfil_service.dart'; -import 'package:mi_utem/utils/dio_miutem_client.dart'; - -class GradesService { - static final Dio _dio = DioMiUtemClient.authDio; - static final GetStorage box = GetStorage(); - - static Future getGrades( - String carreraId, - String asignaturaId, { - bool forceRefresh = false, - bool saveGrades = true, - }) async { - final uri = "/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas"; - final user = PerfilService.getLocalUsuario(); - - Response response = await _dio.get( - uri, - options: buildCacheOptions( - Duration(days: 1), - maxStale: Duration(days: 7), - forceRefresh: forceRefresh, - subKey: user.rut?.numero.toString(), - ), - ); - - Grades grades = Grades.fromJson(response.data); - - if (saveGrades) { - GradesChangesController.saveGrades(asignaturaId, grades); - } - - return grades; - } -} diff --git a/lib/services/horarios_service.dart b/lib/services/horarios_service.dart deleted file mode 100644 index b185f15..0000000 --- a/lib/services/horarios_service.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/services/perfil_service.dart'; -import 'package:mi_utem/utils/dio_miutem_client.dart'; - -class HorarioService { - static final Dio _dio = DioMiUtemClient.authDio; - static final GetStorage box = GetStorage(); - - static Future getHorario( - String carreraId, { - bool forceRefresh = false, - }) async { - final uri = "/v1/carreras/$carreraId/horarios"; - final user = PerfilService.getLocalUsuario(); - - try { - Response response = await _dio.get( - uri, - options: buildCacheOptions( - Duration(days: 30), - forceRefresh: true, - subKey: user.rut?.numero.toString(), - ), - ); - - Horario horario = Horario.fromJson(response.data); - - return horario; - } on DioError catch (e) { - print(e.message); - throw e; - } - } -} diff --git a/lib/services/noticias_service.dart b/lib/services/noticias_service.dart deleted file mode 100644 index aaee8e4..0000000 --- a/lib/services/noticias_service.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:get/get.dart'; -import 'package:mi_utem/config/constants.dart'; - -import 'package:mi_utem/models/noticia.dart'; - -class NoticiasService extends GetConnect { - - Future> getNoticias() async => - get("${Constants.apiUrl}/v1/noticias").then((response) => response.statusCode == 200 ? Noticia.fromApiJsonList(response.body) : []); - -} diff --git a/lib/services/perfil_service.dart b/lib/services/perfil_service.dart deleted file mode 100644 index 7adfa25..0000000 --- a/lib/services/perfil_service.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:dio/dio.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/models/rut.dart'; -import 'package:mi_utem/models/usuario.dart'; -import 'package:mi_utem/services/notification_service.dart'; -import 'package:mi_utem/utils/dio_miutem_client.dart'; - -class PerfilService { - static final Dio _dio = DioMiUtemClient.authDio; - static final GetStorage box = GetStorage(); - - static Usuario getLocalUsuario() { - try { - String? token = box.read("token"); - String? nombres = box.read("nombres"); - String? apellidos = box.read("apellidos"); - String? nombre = box.read("nombre"); - String? fotoUrl = box.read("fotoUrl"); - Rut? rut = box.read("rut") != null - ? (box.read("rut") is int - ? Rut.deEntero(box.read("rut")) - : Rut.deString(box.read("rut"))) - : null; - String? correoUtem = box.read("correoUtem"); - String? correoPersonal = box.read("correoPersonal"); - - return Usuario( - token: token, - nombres: nombres, - fotoUrl: fotoUrl, - nombre: nombre, - apellidos: apellidos, - rut: rut, - correoUtem: correoUtem, - correoPersonal: correoPersonal, - ); - } catch (e) { - print(e); - throw e; - } - } - - static Future changeFoto(String imagen) async { - String uri = "/v1/usuarios/foto"; - - try { - dynamic data = {'imagen': imagen}; - - Response response = await _dio.put(uri, data: data); - - String fotoUrl = response.data["fotoUrl"]; - - box.write('fotoUrl', fotoUrl); - - Usuario usuario = getLocalUsuario(); - - return usuario; - } on DioError catch (e) { - print(e.message); - throw e; - } - } - - static Future deleteFcmToken() async { - String? fcmToken = await NotificationService.fcm.requestFirebaseAppToken(); - CollectionReference usuariosCollection = - FirebaseFirestore.instance.collection('usuarios'); - - QuerySnapshot snapshotRepetidas = await usuariosCollection - .where("fcmTokens", arrayContains: fcmToken) - .get(); - - for (var doc in snapshotRepetidas.docs) { - print("uno repetido ${doc.id}"); - doc.reference.set( - { - "fcmTokens": FieldValue.arrayRemove([fcmToken]), - }, - SetOptions( - merge: true, - ), - ); - } - } - - static Future saveFcmToken() async { - try { - String? fcmToken = - await NotificationService.fcm.requestFirebaseAppToken(); - Usuario usuario = PerfilService.getLocalUsuario(); - CollectionReference usuariosCollection = - FirebaseFirestore.instance.collection('usuarios'); - - await PerfilService.deleteFcmToken(); - - usuariosCollection.doc(usuario.rut!.numero.toString()).set( - { - "fcmTokens": FieldValue.arrayUnion([fcmToken]), - }, - SetOptions( - merge: true, - ), - ); - } catch (e) { - print(e); - } - } -} diff --git a/lib/services/permisos_covid_service.dart b/lib/services/permisos_covid_service.dart deleted file mode 100644 index 7d6e89f..0000000 --- a/lib/services/permisos_covid_service.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/models/permiso_covid.dart'; -import 'package:mi_utem/services/perfil_service.dart'; -import 'package:mi_utem/utils/dio_miutem_client.dart'; - -class PermisosCovidService { - static final Dio _dio = DioMiUtemClient.authDio; - static final GetStorage box = GetStorage(); - - static Future> getPermisos( - {bool forceRefresh = false}) async { - const uri = "/v1/permisos"; - - final user = PerfilService.getLocalUsuario(); - - logger.d("Obteniendo permisos de ${user.rut?.numero}"); - - try { - Response response = await _dio.post( - uri, - options: buildCacheOptions( - Duration(days: 300), - maxStale: Duration(days: 300), - forceRefresh: forceRefresh, - subKey: user.rut?.numero.toString(), - ), - ); - - return PermisoCovid.fromJsonList(response.data); - } on DioError catch (e) { - print(e.message); - throw e; - } - } - - static Future getDetallesPermiso( - String id, { - bool forceRefresh = false, - }) async { - final uri = "/v1/permisos/$id"; - - final user = PerfilService.getLocalUsuario(); - - try { - Response response = await _dio.post( - uri, - options: buildCacheOptions( - Duration(days: 180), - maxStale: Duration(days: 365), - forceRefresh: forceRefresh, - subKey: user.rut?.numero.toString(), - ), - ); - - return PermisoCovid.fromJson(response.data); - } on DioError catch (e) { - print(e.message); - throw e; - } - } -} diff --git a/lib/services_new/implementations/asignaturas_service.dart b/lib/services_new/implementations/asignaturas_service.dart new file mode 100644 index 0000000..617401e --- /dev/null +++ b/lib/services_new/implementations/asignaturas_service.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:http/http.dart' as http; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:watch_it/watch_it.dart'; + +class AsignaturasServiceImplementation implements AsignaturasService { + + @override + Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}) async { + logger.d("Carrera ID: $carreraId"); + if(carreraId == null) { + return null; + } + + final user = await di.get().getUser(); + logger.d("User: ${user?.username}"); + if(user == null) { + return null; + } + + final response = await http.get(Uri.parse('$apiUrl/v1/carreras/$carreraId/asignaturas'), headers: { + 'Authorization': 'Bearer ${user.token}', + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + }); + + final json = jsonDecode(response.body); + logger.d("Response: ${jsonEncode(json)}"); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return Asignatura.fromJsonList(json); + } + + @override + Future getDetalleAsignatura(String? asignaturaId, {bool forceRefresh = false}) async { + final user = await di.get().getUser(); + if(user == null) { + return null; + } + + final response = await http.get(Uri.parse('$apiUrl/v1/asignaturas/$asignaturaId'), headers: { + 'Authorization': 'Bearer ${user.token}', + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + }); + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return Asignatura.fromJson(json as Map); + } + +} \ No newline at end of file diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services_new/implementations/auth_service.dart new file mode 100644 index 0000000..3fa2b49 --- /dev/null +++ b/lib/services_new/implementations/auth_service.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/config/secure_storage.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/user/credential.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/screens/login_screen/login_screen.dart'; +import 'package:mi_utem/services/notification_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:mi_utem/services_new/interfaces/credential_service.dart'; +import 'package:watch_it/watch_it.dart'; + +class AuthServiceImplementation implements AuthService { + + final _credentialsService = di.get(); + + @override + Future isFirstTime() async => !(await secureStorage.containsKey(key: "last_login")); + + @override + Future isLoggedIn() async { + final credential = await _getCredential(); + if(credential == null) { + return false; + } + + // Get stored user + final user = await getUser(); + if(user == null) { + return false; + } + + try { + final response = await http.post(Uri.parse("$apiUrl/v1/auth/refresh"), + body: credential.toString(), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + 'Authorization': 'Bearer ${user.token}', + }, + ); + + final token = jsonDecode(response.body)["token"] as String; + final userJson = user.toJson(); + userJson["token"] = token; + await setUser(User.fromJson(userJson)); + await secureStorage.write(key: "last_login", value: "${DateTime.now().millisecondsSinceEpoch}"); + return true; + } catch (e) { + logger.e("[AuthService#isLoggedIn]: $e"); + } + + return false; + } + + @override + Future login() async { + final credential = await _getCredential(); + if(credential == null) { + return; + } + + final response = await http.post(Uri.parse("$apiUrl/v1/auth"), + body: credential.toString(), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + }, + ); + + final json = jsonDecode(response.body) as Map; + + if(response.statusCode != 200) { + if(json.containsKey("error")) { + throw CustomException.fromJson(json); + } + + throw CustomException.custom(response.reasonPhrase); + } + + final user = User.fromJson(json); + await setUser(user); + } + + @override + Future logout(BuildContext? context) async { + setUser(null); + _credentialsService.setCredentials(null); + + if(context != null) { + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (ctx) => LoginScreen())); + } + } + + @override + Future getUser() async { + final data = await secureStorage.read(key: "user"); + if(data == null) { + return null; + } + + return User.fromJson(jsonDecode(data) as Map); + } + + @override + Future setUser(User? user) async => await secureStorage.write(key: "user", value: user.toString()); + + Future _getCredential() async { + final hasCredential = await _credentialsService.hasCredentials(); + final credential = await _credentialsService.getCredentials(); + if(!hasCredential || credential == null) { + return null; + } + + return credential; + } + + @override + Future updateProfilePicture(String image) async { + final user = await getUser(); + if(user == null) { + return null; + } + + final response = await http.put(Uri.parse("$apiUrl/v1/usuarios/foto"), + body: jsonEncode({"imagen": image}), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + 'Authorization': 'Bearer ${user.token}', + }, + ); + + final json = jsonDecode(response.body) as Map; + + if(response.statusCode != 200) { + if(json.containsKey("error")) { + throw CustomException.fromJson(json); + } + + throw CustomException.custom(response.reasonPhrase); + } + + user.fotoUrl = json["fotoUrl"] as String; + await setUser(user); + + return user; + } + + @override + Future saveFCMToken() async { + try { + String? fcmToken = await NotificationService.fcm.requestFirebaseAppToken(); + final user = await this.getUser(); + if(user == null) { + return; + } + + final usersCollection = FirebaseFirestore.instance.collection('usuarios'); + await this.deleteFCMToken(); + + usersCollection.doc(user.rut?.rut.toString()).set({ + 'fcmTokens': FieldValue.arrayUnion([fcmToken]), + }, SetOptions(merge: true)); + } catch (e) { + logger.e(e); + } + } + + @override + Future deleteFCMToken() async { + String? token = await NotificationService.fcm.requestFirebaseAppToken(); + final usersCollection = FirebaseFirestore.instance.collection('usuarios'); + + final snapshotRepeated = await usersCollection.where("fcmTokens", arrayContains: token).get(); + + for(final doc in snapshotRepeated.docs) { + doc.reference.set({ + "fcmTokens": FieldValue.arrayRemove([token]), + }, SetOptions(merge: true)); + } + } + +} \ No newline at end of file diff --git a/lib/services_new/implementations/calculator_service.dart b/lib/services_new/implementations/calculator_service.dart new file mode 100644 index 0000000..ff11cad --- /dev/null +++ b/lib/services_new/implementations/calculator_service.dart @@ -0,0 +1,254 @@ +import 'package:extended_masked_text/extended_masked_text.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/grades.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; + +class CalculatorServiceImplementation with ChangeNotifier implements CalculatorService { + + /* Porcentaje máximo de todas las notas */ + static const maxPercentage = 100; + + /* Nota máxima */ + static const maxGrade = 7; + + /* Nota mínima para presentarse al examen */ + static const minimumGradeForExam = 2.95; + + /* Nota para pasar el ramo */ + static const passingGrade = 3.95; + + /* Porcentaje de la nota del examen */ + static const examFinalWeight = 0.4; + + /* Porcentaje de la nota de presentación */ + static const presentationFinalWeight = 1 - examFinalWeight; + + /* Notas parciales */ + @override + ValueNotifier> get partialGrades => ValueNotifier>([]); + + /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ + @override + ValueNotifier> get percentageTextFieldControllers => ValueNotifier>([]); + + /* Controlador de texto para las notas con máscara (para autocompletar formato) */ + @override + ValueNotifier> get gradeTextFieldControllers => ValueNotifier>([]); + + /* Nota del examen */ + @override + ValueNotifier get examGrade => ValueNotifier(null); + + /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ + @override + MaskedTextController get examGradeTextFieldController => MaskedTextController(mask: "0.0"); + + @override + ValueNotifier get freeEditable => ValueNotifier(false); + + @override + double? get getCalculatedFinalGrade { + final calculatedPresentationGrade = getCalculatedPresentationGrade; + if (calculatedPresentationGrade == null) { + return null; + } + + final examGradeValue = examGrade.value; + if(examGradeValue == null) { + return calculatedPresentationGrade; + } + + final weightedFinalGrade = calculatedPresentationGrade * presentationFinalWeight; + final weightedExamGrade = examGradeValue * examFinalWeight; + + return weightedFinalGrade + weightedExamGrade; + } + + @override + double? get getCalculatedPresentationGrade { + double presentationGrade = 0; + for (final partialGrade in partialGrades.value) { + final weight = (partialGrade.porcentaje ?? 0) / maxPercentage; + presentationGrade += (partialGrade.nota ?? 0) * weight; + } + + return presentationGrade != 0 ? presentationGrade : null; + } + + @override + int get getAmountOfPartialGradesWithoutGrade => partialGrades.value + .where((partialGrade) => partialGrade.nota == null) + .length; + + @override + int get getAmountOfPartialGradesWithoutPercentage => partialGrades.value + .where((partialGrade) => partialGrade.porcentaje == null) + .length; + + @override + bool get hasMissingPartialGrade => getAmountOfPartialGradesWithoutGrade > 0; + + @override + bool get canTakeExam { + if(hasMissingPartialGrade) { + return false; + } + + final calculatedPresentationGrade = getCalculatedPresentationGrade; + if(calculatedPresentationGrade == null) { + return false; + } + + return calculatedPresentationGrade >= minimumGradeForExam && calculatedPresentationGrade < passingGrade; + } + + @override + double? get getMinimumRequiredExamGrade { + if(!canTakeExam) { + return null; + } + + final calculatedPresentationGrade = getCalculatedPresentationGrade; + if(calculatedPresentationGrade == null) { + return null; + } + + final weightedPresentationGrade = calculatedPresentationGrade * presentationFinalWeight; + return (passingGrade - weightedPresentationGrade) / examFinalWeight; + } + + @override + double get getPercentageOfPartialGrades { + double percentage = 0; + for (final partialGrade in partialGrades.value) { + percentage += (partialGrade.porcentaje ?? 0); + } + return percentage; + } + + @override + double get getMissingPercentage => maxPercentage - getPercentageOfPartialGrades; + + @override + bool get hasMissingPercentage => getAmountOfPartialGradesWithoutPercentage > 0; + + @override + double? get getSuggestedPercentage { + final percentage = getMissingPercentage / getAmountOfPartialGradesWithoutPercentage; + return 0 <= percentage && percentage <= maxPercentage ? percentage : null; + } + + @override + double? get getSuggestedPresentationGrade { + double presentationGrade = 0; + for(final partialGrade in partialGrades.value) { + final weight = (partialGrade.porcentaje ?? (getSuggestedPercentage ?? 0)) / maxPercentage; + presentationGrade += (partialGrade.nota ?? 0) * weight; + } + + return 0 <= presentationGrade && presentationGrade <= maxGrade ? presentationGrade : null; + } + + @override + double get getPercentageWithoutGrade { + double percentage = 0; + for(final partialGrade in partialGrades.value) { + if(partialGrade.nota == null) { + percentage += (partialGrade.porcentaje ?? (getSuggestedPercentage ?? 0)); + } + } + + return percentage; + } + + @override + bool get hasCorrectPercentage => getPercentageOfPartialGrades == maxPercentage; + + @override + double? get getSuggestedGrade { + final percentageWithoutGrade = getPercentageWithoutGrade; + if(!(hasMissingPartialGrade && percentageWithoutGrade > 0)) { + return null; + } + + final weightOfMissingGrades = percentageWithoutGrade / maxPercentage; + final requiredGradeValue = passingGrade - (getSuggestedPresentationGrade ?? 0); + return requiredGradeValue / weightOfMissingGrades; + } + + @override + void updateWithGrades(Grades grades) { + partialGrades.value.clear(); + percentageTextFieldControllers.value.clear(); + gradeTextFieldControllers.value.clear(); + + for(final grade in grades.notasParciales) { + addGrade(IEvaluacion.fromRemote(grade)); + } + + setExamGrade(grades.notaExamen); + notifyListeners(); + } + + @override + void updateGradeAt(int index, IEvaluacion updatedGrade) { + final grade = partialGrades.value[index]; + if(grade.editable || freeEditable.value) { + partialGrades.value[index] = updatedGrade; + + if(hasMissingPartialGrade) { + clearExamGrade(); + } + notifyListeners(); + } else { + throw CustomException.custom("No se puede editar una nota que está asignada"); + } + } + + @override + void addGrade(IEvaluacion grade) { + partialGrades.value.add(grade); + percentageTextFieldControllers.value.add(MaskedTextController( + mask: "000", + text: grade.porcentaje?.toStringAsFixed(0) ?? "", + )); + gradeTextFieldControllers.value.add(MaskedTextController( + mask: "0.0", + text: grade.nota?.toStringAsFixed(1) ?? "", + )); + notifyListeners(); + } + + @override + void removeGradeAt(int index) { + final grade = partialGrades.value[index]; + if(grade.editable || freeEditable.value) { + partialGrades.value.removeRange(index, index+1); + percentageTextFieldControllers.value.removeRange(index, index+1); + gradeTextFieldControllers.value.removeRange(index, index+1); + notifyListeners(); + } else { + throw CustomException.custom("No se puede eliminar una nota que está asignada"); + } + } + + @override + void makeEditable() => freeEditable.value = true; + + @override + void makeNonEditable() => freeEditable.value = false; + + @override + void clearExamGrade() { + examGrade.value = null; + examGradeTextFieldController.updateText(""); + } + + @override + void setExamGrade(num? grade) { + examGrade.value = grade?.toDouble(); + examGradeTextFieldController.updateText(grade?.toDouble().toStringAsFixed(1) ?? ""); + } +} \ No newline at end of file diff --git a/lib/services_new/implementations/carreras_service.dart b/lib/services_new/implementations/carreras_service.dart new file mode 100644 index 0000000..74140b1 --- /dev/null +++ b/lib/services_new/implementations/carreras_service.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_client.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; +import 'package:watch_it/watch_it.dart'; + +class CarrerasServiceImplementation with ChangeNotifier implements CarrerasService { + + @override + ValueNotifier> get carreras => ValueNotifier([]); + + ValueNotifier get selectedCarrera => ValueNotifier(null); + + @override + Future getCarreras({bool forceRefresh = false}) async { + logger.d("[CarrerasService#getCarreras]: Obteniendo carreras..."); + final user = await di.get().getUser(); + if(user == null) { + logger.d("[CarrerasService#getCarreras]: No hay usuario logueado, no se pueden obtener las carreras"); + return; + } + + final response = await AuthClient().get(Uri.parse("$apiUrl/v1/carreras")); + + final json = jsonDecode(response.body); + logger.d("[CarrerasService#getCarreras]: Response: $json"); + if(response.statusCode != 200) { + logger.e("Error al obtener carreras: $json}"); + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(response.reasonPhrase); + } + + carreras.value = Carrera.fromJsonList(json); + autoSelectCarreraActiva(carreras.value); + notifyListeners(); + logger.d("[CarrerasService#getCarreras]: Carreras obtenidas: $json"); + } + + @override + void changeSelectedCarrera(Carrera carrera) => selectedCarrera.value = carrera; + + @override + void autoSelectCarreraActiva(List carreras) { + final estados = ["Regular", "Causal de Eliminacion"] + .reversed + .map((e) => e.toLowerCase()) + .toList(); + + carreras.sort((a,b) => estados.indexOf(b.estado!.toLowerCase()).compareTo(estados.indexOf(a.estado!.toLowerCase()))); + final carreraActiva = carreras.first; + + AnalyticsService.setCarreraToUser(carreraActiva); + changeSelectedCarrera(carreraActiva); + } + +} \ No newline at end of file diff --git a/lib/services_new/implementations/credential_service.dart b/lib/services_new/implementations/credential_service.dart new file mode 100644 index 0000000..4ed0196 --- /dev/null +++ b/lib/services_new/implementations/credential_service.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; + +import 'package:mi_utem/config/secure_storage.dart'; +import 'package:mi_utem/models/user/credential.dart'; +import 'package:mi_utem/services_new/interfaces/credential_service.dart'; + +class CredentialsServiceImplementation implements CredentialsService { + + @override + Future getCredentials() async { + final data = await secureStorage.read(key: "credentials"); + if(data == null) { + return null; + } + + return Credentials.fromJson(jsonDecode(data) as Map); + } + + @override + Future hasCredentials() async => await secureStorage.containsKey(key: "credentials"); + + @override + Future setCredentials(Credentials? credential) async => await secureStorage.write(key: "credentials", value: credential.toString()); + +} \ No newline at end of file diff --git a/lib/services_new/implementations/grades_service.dart b/lib/services_new/implementations/grades_service.dart new file mode 100644 index 0000000..bfb47cb --- /dev/null +++ b/lib/services_new/implementations/grades_service.dart @@ -0,0 +1,246 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/config/secure_storage.dart'; +import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/grades.dart'; +import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; +import 'package:mi_utem/services_new/interfaces/grades_service.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:watch_it/watch_it.dart'; + +class GradesServiceImplementation implements GradesService { + static const savedGradesPrefix = 'savedGrades_'; + static const subscribedAsignaturasPrefix = 'subscribedAsignaturas_'; + + @override + Future getGrades(String carreraId, String asignaturaId, {bool forceRefresh = false, bool saveGrades = true}) async { + final user = await di.get().getUser(); + if(user == null) { + return null; + } + + final response = await http.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas"), + headers: { + 'Authorization': 'Bearer ${user.token}', + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + }, + ); + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(response.reasonPhrase); + } + + final grades = Grades.fromJson(json as Map); + + if(saveGrades) { + await this.saveGrades(asignaturaId, grades); + } + + return grades; + } + + @override + Future saveGrades(String asignaturaId, Grades grades) { + final jsonGrades = grades.toJson(); + jsonGrades['lastUpdate'] = DateTime.now().toIso8601String(); + return secureStorage.write(key: '$savedGradesPrefix$asignaturaId', value: jsonEncode(jsonGrades)); + } + + @override + Future> lookForGradeUpdates() async { + final isLoggedIn = await di.get().isLoggedIn(); + + if(!isLoggedIn) { + return {}; + } + + final carrera = di.get().selectedCarrera.value; + final carreraId = carrera?.id; + + if(carreraId == null) { + return {}; + } + + final subscribedAsignaturasJson = await secureStorage.read(key: '$subscribedAsignaturasPrefix$carreraId'); + List subscribedAsignaturas; + if(subscribedAsignaturasJson == null) { + subscribedAsignaturas = (await di.get().getAsignaturas(carreraId)) ?? []; + await secureStorage.write(key: '$subscribedAsignaturasPrefix$carreraId', value: jsonEncode(subscribedAsignaturas.map((it) => it.toJson()).toList())); + } else { + subscribedAsignaturas = Asignatura.fromJsonList(jsonDecode(subscribedAsignaturasJson) as List); + } + + final response = Map(); + + for(Asignatura? asignatura in subscribedAsignaturas) { + final asignaturaId = asignatura?.id; + if(asignatura == null || asignaturaId == null) { + continue; + } + + final updatedGrades = await this.getGrades(carreraId, asignaturaId, forceRefresh: true, saveGrades: false); + if(updatedGrades == null) { + continue; + } + + final changeType = await this.compareGrades(asignaturaId, updatedGrades); + await this.saveGrades(asignaturaId, updatedGrades); + + this._notifyGradeUpdate(asignatura, changeType); + + response[asignaturaId] = changeType; + } + + return response; + } + + @override + Future compareGrades(String asignaturaId, Grades grades) async { + final prevGradesJson = await secureStorage.read(key: '$savedGradesPrefix$asignaturaId'); + if(prevGradesJson == null) { + return GradeChangeType.noChange; + } + + final prevGrades = Grades.fromJson(jsonDecode(prevGradesJson) as Map); + final prevGradesLength = prevGrades.notasParciales.length; + final currentGradesLength = grades.notasParciales.length; + + if(prevGradesLength == 0) { + if(currentGradesLength == 0) { + return GradeChangeType.noChange; + } + + if(_hasAGradeWithValue(grades)) { + return GradeChangeType.gradeSet; + } + + return GradeChangeType.weightingsSet; + } + + if(currentGradesLength == 0) { + return GradeChangeType.weightingsDeleted; + } else if(prevGradesLength != currentGradesLength) { + return GradeChangeType.weightingsUpdated; + } else if(_hasAWeightingDifference(prevGrades, grades)) { + return GradeChangeType.weightingsUpdated; + } + + return _getGradeValueChangeType(prevGrades, grades); + } + + GradeChangeType _getGradeValueChangeType(Grades prev, Grades current) { + final prevLength = prev.notasParciales.length; + final currentLength = current.notasParciales.length; + if(prevLength != currentLength) { + Sentry.captureMessage("Asignatura $prev.id tiene un número distinto de ponderadores en la función _getGradeValueChangeType", + level: SentryLevel.warning + ); + return GradeChangeType.noChange; + } + + GradeChangeType? changeType; + + for(int i = 0; i < prevLength; i++) { + final prevValue = prev.notasParciales[i]; + final currentValue = current.notasParciales[i]; + if(prevValue.nota == currentValue.nota) { + continue; + } + + if(prevValue.nota == null && currentValue.nota != null) { + Sentry.configureScope((scope) => scope.setExtra('newGrade', currentValue.nota)); + changeType = GradeChangeType.gradeSet; + } else if(prevValue.nota != null && currentValue.nota == null) { + changeType = changeType ?? GradeChangeType.gradeDeleted; + } else { + changeType = changeType ?? GradeChangeType.gradeUpdated; + } + } + + return changeType ?? GradeChangeType.noChange; + } + + bool _hasAWeightingDifference(Grades pev, Grades current) { + final prevLength = pev.notasParciales.length; + final currentLength = current.notasParciales.length; + if(prevLength != currentLength) { + Sentry.captureMessage("Asignatura $pev.id tiene un número distinto de ponderadores en la función _hasAWeightingDifference", + level: SentryLevel.warning + ); + return false; + } + + for(int i = 0; i < prevLength; i++) { + final prevValue = pev.notasParciales[i]; + final currentValue = current.notasParciales[i]; + if(prevValue.porcentaje != currentValue.porcentaje) { + return true; + } + } + + return false; + } + + bool _hasAGradeWithValue(Grades asignatura) => + asignatura.notasParciales.any((it) => it.nota != null); + + void _notifyGradeUpdate(Asignatura asignatura, GradeChangeType changeType) { + final name = asignatura.nombre ?? asignatura.codigo; + + String? title; + String? body; + + switch(changeType) { + case GradeChangeType.gradeSet: + title = "Tienes una nueva nota"; + body = "$name: se ha agregado una nota."; + break; + case GradeChangeType.gradeUpdated: + title = "Una nota ha cambiado"; + body = "$name: se ha actualizado una nota."; + break; + case GradeChangeType.gradeDeleted: + title = "Una nota se ha borrado"; + body = "$name: se ha eliminado una nota."; + break; + default: + break; + } + + if(title != null && body != null) { + Sentry.captureMessage("Asignatura ha cambiado y notificado", + level: SentryLevel.debug, + withScope: (scope) { + scope.setTag("asignaturaId", asignatura.id.toString()); + scope.setTag("asignaturaCodigo", asignatura.codigo.toString()); + scope.setTag("change", changeType.toString()); + } + ); + + // NotificationService.showGradeChangeNotification(title, body, asignatura); + } else if(changeType != GradeChangeType.noChange) { + Sentry.captureMessage("Asignatura ha cambiado pero no notificado", + level: SentryLevel.debug, + withScope: (scope) { + scope.setTag("asignaturaId", asignatura.id.toString()); + scope.setTag("asignaturaCodigo", asignatura.codigo.toString()); + scope.setTag("change", changeType.toString()); + } + ); + } + } + +} \ No newline at end of file diff --git a/lib/services_new/implementations/horario_service.dart b/lib/services_new/implementations/horario_service.dart new file mode 100644 index 0000000..4d8ed5d --- /dev/null +++ b/lib/services_new/implementations/horario_service.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/horario.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:mi_utem/services_new/interfaces/horario_service.dart'; +import 'package:http/http.dart' as http; +import 'package:watch_it/watch_it.dart'; + +class HorarioServiceImplementation implements HorarioService { + @override + Future getHorario(String carreraId, {bool forceRefresh = false}) async { + final user = await di.get().getUser(); + if (user == null) { + return null; + } + + final response = await http.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/horarios"), + headers: { + 'Authorization': 'Bearer ${user.token}', + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + }, + ); + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return Horario.fromJson(json); + } +} diff --git a/lib/services_new/implementations/noticias_service.dart b/lib/services_new/implementations/noticias_service.dart new file mode 100644 index 0000000..7e4dced --- /dev/null +++ b/lib/services_new/implementations/noticias_service.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; + +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/noticia.dart'; +import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; +import 'package:http/http.dart' as http; + +class NoticiasServiceImplementation implements NoticiasService { + + @override + Future?> getNoticias() async { + final response = await http.get(Uri.parse("$apiUrl/v1/noticias")); + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return Noticia.fromApiJsonList(json as List); + } + +} \ No newline at end of file diff --git a/lib/services_new/implementations/qr_pass_service.dart b/lib/services_new/implementations/qr_pass_service.dart new file mode 100644 index 0000000..daaea14 --- /dev/null +++ b/lib/services_new/implementations/qr_pass_service.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; + +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/permiso_covid.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; +import 'package:http/http.dart' as http; +import 'package:watch_it/watch_it.dart'; + +class QRPassServiceImplementation extends QRPassService { + + final _authService = di.get(); + + @override + Future getDetallesPermiso(String id, {bool forceRefresh = false}) async { + final user = await _authService.getUser(); + if(user == null) { + return null; + } + + final response = await http.post(Uri.parse("$apiUrl/v1/permisos/$id"), + headers: { + 'Authorization': 'Bearer ${user.token}', + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM' + }, + ); + + final json = jsonDecode(response.body) as Map; + + if(response.statusCode != 200) { + if(json.containsKey("error")) { + throw CustomException.fromJson(json); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return PermisoCovid.fromJson(json); + } + + @override + Future?> getPermisos({bool forceRefresh = false}) async { + final user = await _authService.getUser(); + if(user == null) { + return null; + } + + final response = await http.post(Uri.parse("$apiUrl/v1/permisos"), + headers: { + 'Authorization': 'Bearer ${user.token}', + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM' + }, + ); + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return PermisoCovid.fromJsonList(json as List); + } + + +} \ No newline at end of file diff --git a/lib/services_new/interfaces/asignaturas_service.dart b/lib/services_new/interfaces/asignaturas_service.dart new file mode 100644 index 0000000..543e14d --- /dev/null +++ b/lib/services_new/interfaces/asignaturas_service.dart @@ -0,0 +1,8 @@ +import 'package:mi_utem/models/asignatura.dart'; + +abstract class AsignaturasService { + + Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}); + + Future getDetalleAsignatura(String? asignaturaId, {bool forceRefresh = false}); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/auth_service.dart b/lib/services_new/interfaces/auth_service.dart new file mode 100644 index 0000000..ba0bff5 --- /dev/null +++ b/lib/services_new/interfaces/auth_service.dart @@ -0,0 +1,23 @@ +import 'package:flutter/widgets.dart'; +import 'package:mi_utem/models/user/user.dart'; + +abstract class AuthService { + + Future isFirstTime(); + + Future isLoggedIn(); + + Future login(); + + Future logout(BuildContext? context); + + Future getUser(); + + Future setUser(User? user); + + Future updateProfilePicture(String image); + + Future saveFCMToken(); + + Future deleteFCMToken(); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/calculator_service.dart b/lib/services_new/interfaces/calculator_service.dart new file mode 100644 index 0000000..a0576bb --- /dev/null +++ b/lib/services_new/interfaces/calculator_service.dart @@ -0,0 +1,84 @@ +import 'package:extended_masked_text/extended_masked_text.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/grades.dart'; + +abstract class CalculatorService { + + /* Notas parciales */ + ValueNotifier> get partialGrades; + + /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ + ValueNotifier> get percentageTextFieldControllers; + + /* Controlador de texto para las notas con máscara (para autocompletar formato) */ + ValueNotifier> get gradeTextFieldControllers; + + /* Nota del examen */ + ValueNotifier get examGrade; + + /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ + MaskedTextController get examGradeTextFieldController; + + ValueNotifier get freeEditable; + + /* Nota final calculada */ + double? get getCalculatedFinalGrade; + + /* Nota de presentación calculada */ + double? get getCalculatedPresentationGrade; + + /* Cantidad de notas parciales sin nota */ + int get getAmountOfPartialGradesWithoutGrade; + + /* Cantidad de notas parciales sin porcentaje */ + int get getAmountOfPartialGradesWithoutPercentage; + + /* Si hay notas parciales sin nota */ + bool get hasMissingPartialGrade; + + /* Si puede tomar examen */ + bool get canTakeExam; + + /* Nota mínima requerida para el examen */ + double? get getMinimumRequiredExamGrade; + + /* Porcentaje de las notas parciales */ + double get getPercentageOfPartialGrades; + + /* Porcentaje faltante */ + double get getMissingPercentage; + + /* Si hay porcentaje faltante */ + bool get hasMissingPercentage; + + /* Porcentaje sugerido */ + double? get getSuggestedPercentage; + + /* Nota de presentación sugerida */ + double? get getSuggestedPresentationGrade; + + /* Porcentaje sin nota */ + double get getPercentageWithoutGrade; + + /* Si hay porcentaje sin nota */ + bool get hasCorrectPercentage; + + double? get getSuggestedGrade; + + void updateWithGrades(Grades grades); + + void updateGradeAt(int index, IEvaluacion updatedGrade); + + void clearExamGrade(); + + void setExamGrade(num? grade); + + void addGrade(IEvaluacion grade); + + void removeGradeAt(int index); + + void makeEditable(); + + void makeNonEditable(); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/carreras_service.dart b/lib/services_new/interfaces/carreras_service.dart new file mode 100644 index 0000000..64ae4a9 --- /dev/null +++ b/lib/services_new/interfaces/carreras_service.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/carrera.dart'; + +abstract class CarrerasService { + + ValueNotifier> get carreras; + ValueNotifier get selectedCarrera; + + Future getCarreras({bool forceRefresh = false}); + + void changeSelectedCarrera(Carrera carrera); + + void autoSelectCarreraActiva(List carreras); + +} \ No newline at end of file diff --git a/lib/services_new/interfaces/credential_service.dart b/lib/services_new/interfaces/credential_service.dart new file mode 100644 index 0000000..4c7513f --- /dev/null +++ b/lib/services_new/interfaces/credential_service.dart @@ -0,0 +1,10 @@ +import 'package:mi_utem/models/user/credential.dart'; + +abstract class CredentialsService { + + Future hasCredentials(); + + Future getCredentials(); + + Future setCredentials(Credentials? credential); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/grades_service.dart b/lib/services_new/interfaces/grades_service.dart new file mode 100644 index 0000000..aae7b74 --- /dev/null +++ b/lib/services_new/interfaces/grades_service.dart @@ -0,0 +1,22 @@ +import 'package:mi_utem/models/grades.dart'; + +abstract class GradesService { + + Future getGrades(String carreraId, String asignaturaId, {bool forceRefresh = false, bool saveGrades = true}); + + Future saveGrades(String asignaturaId, Grades grades); + + Future compareGrades(String asignaturaId, Grades grades); + + Future> lookForGradeUpdates(); +} + +enum GradeChangeType { + weightingsSet, + weightingsUpdated, + weightingsDeleted, + gradeSet, + gradeUpdated, + gradeDeleted, + noChange +} \ No newline at end of file diff --git a/lib/services_new/interfaces/horario_service.dart b/lib/services_new/interfaces/horario_service.dart new file mode 100644 index 0000000..d8db2d2 --- /dev/null +++ b/lib/services_new/interfaces/horario_service.dart @@ -0,0 +1,6 @@ +import 'package:mi_utem/models/horario.dart'; + +abstract class HorarioService { + + Future getHorario(String carreraId, {bool forceRefresh = false}); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/noticias_service.dart b/lib/services_new/interfaces/noticias_service.dart new file mode 100644 index 0000000..e6ccc78 --- /dev/null +++ b/lib/services_new/interfaces/noticias_service.dart @@ -0,0 +1,6 @@ +import 'package:mi_utem/models/noticia.dart'; + +abstract class NoticiasService { + + Future?> getNoticias(); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/qr_pass_service.dart b/lib/services_new/interfaces/qr_pass_service.dart new file mode 100644 index 0000000..517e815 --- /dev/null +++ b/lib/services_new/interfaces/qr_pass_service.dart @@ -0,0 +1,8 @@ +import 'package:mi_utem/models/permiso_covid.dart'; + +abstract class QRPassService { + + Future?> getPermisos({bool forceRefresh = false}); + + Future getDetallesPermiso(String id, {bool forceRefresh = false}); +} \ No newline at end of file diff --git a/lib/services_new/service_manager.dart b/lib/services_new/service_manager.dart new file mode 100644 index 0000000..8573a6b --- /dev/null +++ b/lib/services_new/service_manager.dart @@ -0,0 +1,31 @@ +import 'package:mi_utem/services_new/implementations/asignaturas_service.dart'; +import 'package:mi_utem/services_new/implementations/auth_service.dart'; +import 'package:mi_utem/services_new/implementations/calculator_service.dart'; +import 'package:mi_utem/services_new/implementations/carreras_service.dart'; +import 'package:mi_utem/services_new/implementations/credential_service.dart'; +import 'package:mi_utem/services_new/implementations/grades_service.dart'; +import 'package:mi_utem/services_new/implementations/horario_service.dart'; +import 'package:mi_utem/services_new/implementations/noticias_service.dart'; +import 'package:mi_utem/services_new/implementations/qr_pass_service.dart'; +import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; +import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; +import 'package:mi_utem/services_new/interfaces/credential_service.dart'; +import 'package:mi_utem/services_new/interfaces/grades_service.dart'; +import 'package:mi_utem/services_new/interfaces/horario_service.dart'; +import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; +import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; +import 'package:watch_it/watch_it.dart'; + +Future registerServices() async { + di.registerLazySingleton(() => AsignaturasServiceImplementation()); + di.registerLazySingleton(() => AuthServiceImplementation()); + di.registerLazySingleton(() => CalculatorServiceImplementation()); + di.registerLazySingleton(() => CarrerasServiceImplementation()); + di.registerLazySingleton(() => CredentialsServiceImplementation()); + di.registerLazySingleton(() => GradesServiceImplementation()); + di.registerLazySingleton(() => HorarioServiceImplementation()); + di.registerLazySingleton(() => NoticiasServiceImplementation()); + di.registerLazySingleton(() => QRPassServiceImplementation()); +} \ No newline at end of file diff --git a/lib/utils/dio_miutem_client.dart b/lib/utils/dio_miutem_client.dart index 0db10d8..12af595 100644 --- a/lib/utils/dio_miutem_client.dart +++ b/lib/utils/dio_miutem_client.dart @@ -3,7 +3,6 @@ import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:mi_utem/services/auth_service.dart'; class DioMiUtemClient { static const bool isProduction = bool.fromEnvironment('dart.vm.product'); @@ -45,7 +44,7 @@ class AuthInterceptor extends QueuedInterceptor { final RequestInterceptorHandler handler, ) async { try { - final token = AuthService.getToken(); + final token = ""; options._setAuthenticationHeader(token); @@ -75,7 +74,7 @@ class AuthInterceptor extends QueuedInterceptor { // Force refresh auth token try { - final token = await AuthService.refreshToken(); + final token = ""; log("Refreshing token, attempt $attempt..."); @@ -93,7 +92,7 @@ class AuthInterceptor extends QueuedInterceptor { } Future _onErrorRefreshingToken() async { - AuthService.invalidateToken(); + } } diff --git a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart index 1e9dd22..6babaf7 100644 --- a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart +++ b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart @@ -2,9 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/widgets/image_view_screen.dart'; @@ -20,15 +18,14 @@ class AcercaClubDesarrolladores extends StatelessWidget { borderRadius: BorderRadius.circular(15.0), ), child: Container( - padding: EdgeInsets.all(20), + padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Text( - "Desarrolladores", + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text("Desarrolladores", textAlign: TextAlign.center, style: TextStyle( fontSize: 20, @@ -37,93 +34,77 @@ class AcercaClubDesarrolladores extends StatelessWidget { ), ), ), - Container(height: 10), - ...jsonDecode(RemoteConfigService.miutemDesarrolladores).map((creador) => Container ( + const SizedBox(height: 10), + ...jsonDecode(RemoteConfigService.miutemDesarrolladores).map((developer) => Container( padding: EdgeInsets.symmetric(vertical: 10), child: Row( - children: [ + children: [ ProfilePhoto( - usuario: Usuario(nombres: creador['nombre'], fotoUrl: creador['fotoUrl']), - onImageTap: (context, imageProvider) { - AnalyticsService.logEvent("acerca_person_image_tap", parameters: { - "persona": creador['nombre'], - }); - Get.to(() => ImageViewScreen(imageProvider: imageProvider), - routeName: Routes.imageView, - ); - }), - Container(width: 20), + user: User(nombres: developer['nombre'], fotoUrl: developer['fotoUrl']), + onImageTap: (context, imageProvider) { + AnalyticsService.logEvent("acerca_person_image_tap", parameters: { + "persona": developer['nombre'], + }); + Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen(imageProvider: imageProvider), fullscreenDialog: true)); + }, + ), + const SizedBox(width: 20), Expanded( child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - creador["nombre"], + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(developer["nombre"], style: TextStyle( fontWeight: FontWeight.bold, color: Colors.grey[800], fontSize: 16, ), ), - Text( - creador["rol"], + Text(developer["rol"], style: TextStyle( fontSize: 16, color: Colors.grey[700], ), ), - Container(height: 5), + const SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.start, - children: creador['redes'].map((red) => Container( - margin: EdgeInsets.only(right: 8), - decoration: new BoxDecoration( + children: developer['redes'].map((socialNetwork) => Container( + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( shape: BoxShape.circle, - color: Color(red["color"]), + color: Color(socialNetwork["color"]), ), child: InkWell( customBorder: - CircleBorder(), + const CircleBorder(), onTap: () async { AnalyticsService.logEvent("acerca_person_social_tap", parameters: { - "persona": - creador['nombre'], - "red": red['nombre'], + "persona": developer['nombre'], + "red": socialNetwork['nombre'], }, ); - await launchUrl( - Uri.parse(red["url"]), - ); + await launchUrl(Uri.parse(socialNetwork["url"])); }, child: Container( - padding: - const EdgeInsets.all( - 8), - decoration: - new BoxDecoration( - shape: BoxShape.circle, - ), + padding: const EdgeInsets.all(8), + decoration: const BoxDecoration(shape: BoxShape.circle), child: Icon( - IconDataBrands(red["icono"]), + IconDataBrands(socialNetwork["icono"]), size: 15, color: Colors.white, ), ), ), - ), - ) - .toList(), + )).toList(), ), ], ), ), ], ), - ), - ) - .toList() + )).toList() ], ), ), diff --git a/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart b/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart index ac961dd..41ea096 100644 --- a/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart +++ b/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; class AcercaDialogActionButton extends StatefulWidget { @@ -8,10 +7,10 @@ class AcercaDialogActionButton extends StatefulWidget { final int timeLeft; const AcercaDialogActionButton({ - Key? key, + super.key, required this.isActive, required this.timeLeft, - }) : super(key: key); + }); @override State createState() => _AcercaDialogActionButtonState(); @@ -28,15 +27,15 @@ class _AcercaDialogActionButtonState extends State { runAlignment: WrapAlignment.center, children: [ TextButton( - child: Text(widget.isActive ? "Podrás cerrar en ${widget.timeLeft}" : "Saber más", - style: TextStyle(color: Colors.white), - ), - onPressed: () { - if (!widget.isActive) { - Get.back(); - Get.toNamed(Routes.about); - } + child: Text(widget.isActive ? "Podrás cerrar en ${widget.timeLeft}" : "Saber más", + style: const TextStyle(color: Colors.white), + ), + onPressed: () { + if (!widget.isActive) { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute(builder: (context) => AcercaScreen())); } + }, ), ], ); diff --git a/lib/widgets/asignatura/lista/asignatura_list_tile.dart b/lib/widgets/asignatura/lista/asignatura_list_tile.dart index 2cbda6e..1a77b39 100644 --- a/lib/widgets/asignatura/lista/asignatura_list_tile.dart +++ b/lib/widgets/asignatura/lista/asignatura_list_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/themes/theme.dart'; class AsignaturaListTile extends StatelessWidget { @@ -15,33 +14,30 @@ class AsignaturaListTile extends StatelessWidget { @override Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - ), + padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Card( child: InkWell( - onTap: () => Get.toNamed('${Routes.asignatura}/${asignatura.id}'), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignaturaId: asignatura.id))), child: Container( - padding: EdgeInsets.all(20), + padding: const EdgeInsets.all(20), width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - asignatura.nombre!, + Text("${asignatura.nombre}", maxLines: 2, overflow: TextOverflow.ellipsis, style: MainTheme.theme.textTheme.titleMedium, textAlign: TextAlign.start, ), - Container(height: 10), + const SizedBox(height: 10), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(asignatura.codigo!), - Text(asignatura.tipoHora!), + Text("${asignatura.codigo}"), + Text("${asignatura.tipoHora}"), ], ) ], diff --git a/lib/widgets/calculadora_notas/DisplayNotasWidget.dart b/lib/widgets/calculadora_notas/DisplayNotasWidget.dart index f001304..b2ca66c 100644 --- a/lib/widgets/calculadora_notas/DisplayNotasWidget.dart +++ b/lib/widgets/calculadora_notas/DisplayNotasWidget.dart @@ -1,45 +1,41 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/widgets/calculadora_notas/ModoSimulacionWidget.dart'; import 'package:mi_utem/widgets/calculadora_notas/NotaExamenDisplayWidget.dart'; import 'package:mi_utem/widgets/calculadora_notas/NotaFinalDisplayWidget.dart'; import 'package:mi_utem/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart'; class DisplayNotasWidget extends StatelessWidget { - final CalculatorController _calculatorController; const DisplayNotasWidget({ - Key? key, - required CalculatorController calculatorController, - }) : _calculatorController = calculatorController, - super(key: key); + super.key, + }); @override Widget build(BuildContext context) => Card( child: Stack( alignment: Alignment.center, children: [ - ModoSimulacionWidget(), + const ModoSimulacionWidget(), Container( - padding: EdgeInsets.symmetric(vertical: 20, horizontal: 30), + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 30), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - NotaFinalDisplayWidget(calculatorController: _calculatorController), - Container(width: 10), + children: [ + const NotaFinalDisplayWidget(), + const SizedBox(width: 10), Container( height: 80, width: 0.5, color: Colors.grey, ), - Container(width: 10), + const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - NotaExamenDisplayWidget(calculatorController: _calculatorController), - Container(height: 10), - NotaPresentacionDisplayWidget(calculatorController: _calculatorController), + const NotaExamenDisplayWidget(), + const SizedBox(height: 10), + const NotaPresentacionDisplayWidget(), ], ), ], diff --git a/lib/widgets/calculadora_notas/EditarNotasWidget.dart b/lib/widgets/calculadora_notas/EditarNotasWidget.dart index 9a008bd..f964cfb 100644 --- a/lib/widgets/calculadora_notas/EditarNotasWidget.dart +++ b/lib/widgets/calculadora_notas/EditarNotasWidget.dart @@ -1,18 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; import 'package:mi_utem/widgets/calculadora_notas/ModoSimulacionWidget.dart'; import 'package:mi_utem/widgets/calculadora_notas/NotasCalculadoraWidget.dart'; +import 'package:watch_it/watch_it.dart'; class EditarNotasWidget extends StatelessWidget { - final CalculatorController _calculatorController; - const EditarNotasWidget({ - Key? key, - required CalculatorController calculatorController, - }) : _calculatorController = calculatorController, - super(key: key); + super.key, + }); @override Widget build(BuildContext context) => Card( @@ -24,11 +21,11 @@ class EditarNotasWidget extends StatelessWidget { padding: EdgeInsets.all(20), child: Column( children: [ - NotasCalculadoraWidget(calculatorController: _calculatorController, onDelete: (controller, index) => _deleteGrade(controller, index)), - SizedBox(height: 16), + const NotasCalculadoraWidget(), + const SizedBox(height: 16), TextButton( - onPressed: () => _addGrade(_calculatorController), - child: Text("Agregar nota"), + onPressed: _addGrade, + child: const Text("Agregar nota"), ), ], ), @@ -37,14 +34,9 @@ class EditarNotasWidget extends StatelessWidget { ), ); - void _deleteGrade(CalculatorController controller, int index) { - AnalyticsService.logEvent("calculator_delete_grade"); - controller.removeGradeAt(index); - } - - void _addGrade(CalculatorController controller) { + void _addGrade() { AnalyticsService.logEvent("calculator_add_grade"); - controller.addGrade(IEvaluacion( + di.get().addGrade(IEvaluacion( nota: null, porcentaje: null, )); diff --git a/lib/widgets/calculadora_notas/ModoSimulacionWidget.dart b/lib/widgets/calculadora_notas/ModoSimulacionWidget.dart index 9bd03a6..239bde8 100644 --- a/lib/widgets/calculadora_notas/ModoSimulacionWidget.dart +++ b/lib/widgets/calculadora_notas/ModoSimulacionWidget.dart @@ -2,14 +2,17 @@ import 'package:flutter/material.dart'; class ModoSimulacionWidget extends StatelessWidget { + const ModoSimulacionWidget({ + super.key, + }); + @override Widget build(BuildContext context) => Container( - padding: EdgeInsets.all(20), + padding: const EdgeInsets.all(20), width: double.infinity, child: RotationTransition( - turns: AlwaysStoppedAnimation(-20 / 360), - child: Text( - "Modo simulación".toUpperCase(), + turns: const AlwaysStoppedAnimation(-20/360), + child: Text("Modo simulación".toUpperCase(), style: TextStyle( color: Colors.grey[200], fontWeight: FontWeight.bold, diff --git a/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart b/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart index 766cd4c..c339f19 100644 --- a/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart +++ b/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart @@ -1,38 +1,37 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:watch_it/watch_it.dart'; class NotaExamenDisplayWidget extends StatelessWidget { - final CalculatorController _calculatorController; const NotaExamenDisplayWidget({ - Key? key, - required CalculatorController calculatorController, - }) : _calculatorController = calculatorController, - super(key: key); + super.key, + }); @override - Widget build(BuildContext context) => Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "Examen", - style: TextStyle(fontSize: 16), - ), - Container( - width: 80, - margin: EdgeInsets.only(left: 15), - child: Obx(() => TextField( - controller: _calculatorController.examGradeTextFieldController, + Widget build(BuildContext context) { + final _calculatorService = di.get(); + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Examen", + style: TextStyle(fontSize: 16), + ), + Container( + width: 80, + margin: const EdgeInsets.only(left: 15), + child: TextField( + controller: _calculatorService.examGradeTextFieldController, textAlign: TextAlign.center, onChanged: (String value) { - _calculatorController.examGrade.value = double.tryParse(value.replaceAll(",", ".")); + _calculatorService.examGrade.value = double.tryParse(value.replaceAll(",", ".")); }, - enabled: _calculatorController.canTakeExam, + enabled: _calculatorService.canTakeExam, decoration: InputDecoration( - hintText: _calculatorController.minimumRequiredExamGrade?.toStringAsFixed(1) ?? "", - filled: !_calculatorController.canTakeExam, + hintText: _calculatorService.getMinimumRequiredExamGrade?.toStringAsFixed(1) ?? "", + filled: !_calculatorService.canTakeExam, fillColor: Colors.grey.withOpacity(0.2), disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( @@ -40,13 +39,12 @@ class NotaExamenDisplayWidget extends StatelessWidget { ), ), ), - keyboardType: - TextInputType.numberWithOptions( + keyboardType: const TextInputType.numberWithOptions( decimal: true, ), ), ), - ), - ], - ); + ], + ); + } } diff --git a/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart b/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart index 3ded3ff..c0f09a7 100644 --- a/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart +++ b/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart @@ -1,24 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; +import 'package:watch_it/watch_it.dart'; class NotaFinalDisplayWidget extends StatelessWidget { - final CalculatorController _calculatorController; const NotaFinalDisplayWidget({ - Key? key, - required CalculatorController calculatorController, - }) : _calculatorController = calculatorController, - super(key: key); + super.key, + }); @override Widget build(BuildContext context) => Column( - children: [ - Obx(() => - Text(_calculatorController.calculatedFinalGrade?.toStringAsFixed(1) ?? "--", - style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold), - ), - ), - ], - ); + children: [ + Text(di.get().getCalculatedFinalGrade?.toStringAsFixed(1) ?? "--", + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + ), + ) + ], + ); } diff --git a/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart b/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart index 10e41c2..5c488cf 100644 --- a/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart +++ b/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart @@ -1,45 +1,40 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:watch_it/watch_it.dart'; class NotaPresentacionDisplayWidget extends StatelessWidget { - final CalculatorController _calculatorController; - const NotaPresentacionDisplayWidget({ - Key? key, - required CalculatorController calculatorController, - }) : _calculatorController = calculatorController, - super(key: key); + super.key, + }); @override - Widget build(BuildContext context) => Obx( - () => Row( + Widget build(BuildContext context) { + final _calculatorService = di.get(); + return Row( children: [ - Text( - "Pres.", + const Text("Pres.", style: TextStyle(fontSize: 16), ), Container( width: 80, - margin: EdgeInsets.only(left: 15), + margin: const EdgeInsets.only(left: 15), child: TextField( - controller: TextEditingController(text: _calculatorController.calculatedPresentationGrade?.toStringAsFixed(1) ?? ""), + controller: TextEditingController(text: _calculatorService.getCalculatedPresentationGrade?.toStringAsFixed(1) ?? ""), textAlign: TextAlign.center, enabled: false, decoration: InputDecoration( hintText: "Nota", disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( - borderSide: BorderSide( + borderSide: const BorderSide( color: Colors.transparent, ), ), ), - keyboardType: - TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions(decimal: true), ), ), ], - ), - ); + ); + } } diff --git a/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart b/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart index dfc14b0..5193264 100644 --- a/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart +++ b/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart @@ -1,40 +1,40 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; +import 'package:watch_it/watch_it.dart'; class NotasCalculadoraWidget extends StatelessWidget { - final CalculatorController _calculatorController; - final Function onDelete; const NotasCalculadoraWidget({ - Key? key, - required CalculatorController calculatorController, - required this.onDelete, - }) : _calculatorController = calculatorController, - super(key: key); + super.key, + }); @override - Widget build(BuildContext context) => Obx(() => ListView.separated( + Widget build(BuildContext context) { + final _calculatorService = di.get(); + final _partialGrades = watchValue((CalculatorService _service) => _service.partialGrades); + final _gradeTextFieldControllers = watchValue((CalculatorService _service) => _service.gradeTextFieldControllers); + final _percentageTextFieldControllers = watchValue((CalculatorService _service) => _service.percentageTextFieldControllers); + + return ListView.separated( shrinkWrap: true, - physics: ClampingScrollPhysics(), - separatorBuilder: (context, index) => SizedBox(height: 10), - itemBuilder: (context, i) { - REvaluacion evaluacion = _calculatorController.partialGrades[i]; - return NotaListItem( - evaluacion: IEvaluacion.fromRemote(evaluacion), - editable: true, - gradeController: _calculatorController.gradeTextFieldControllers[i], - percentageController: _calculatorController.percentageTextFieldControllers[i], - onChanged: (evaluacion) { - _calculatorController.changeGradeAt(i, evaluacion); - }, - onDelete: () => Function.apply(onDelete, [_calculatorController, i]), - ); - }, - itemCount: _calculatorController.partialGrades.length, - ), - ); + physics: const ClampingScrollPhysics(), + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, idx) => NotaListItem( + evaluacion: IEvaluacion.fromRemote(_partialGrades[idx]), + editable: true, + gradeController: _gradeTextFieldControllers[idx], + percentageController: _percentageTextFieldControllers[idx], + onChanged: (evaluacion) => _calculatorService.updateGradeAt(idx, evaluacion), + onDelete: () { + AnalyticsService.logEvent("calculator_delete_grade"); + _calculatorService.removeGradeAt(idx); + }, + ), + itemCount: _partialGrades.length, + ); + } } diff --git a/lib/widgets/credencial_card.dart b/lib/widgets/credencial_card.dart index 4674760..83b9bb9 100644 --- a/lib/widgets/credencial_card.dart +++ b/lib/widgets/credencial_card.dart @@ -5,7 +5,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/flip_widget.dart'; @@ -14,14 +14,14 @@ import 'package:simple_gesture_detector/simple_gesture_detector.dart'; import 'package:url_launcher/url_launcher.dart'; class CredencialCard extends StatelessWidget { - final Usuario? usuario; + final User? user; final Carrera? carrera; final FlipController? controller; final Function(SwipeDirection?)? onFlip; CredencialCard( {Key? key, - required this.usuario, + required this.user, required this.carrera, this.controller, this.onFlip}) @@ -40,7 +40,7 @@ class CredencialCard extends StatelessWidget { Container( margin: EdgeInsets.only(top: altoBanner - 40), child: ProfilePhoto( - usuario: usuario, + user: user, radius: 50, borderWidth: 5, ), @@ -74,8 +74,7 @@ class CredencialCard extends StatelessWidget { color: Colors.white, child: Column( children: [ - Text( - usuario!.nombreCompleto!, + Text(user?.nombreCompleto ?? "N/N", maxLines: 2, style: TextStyle( fontSize: 18, @@ -85,8 +84,7 @@ class CredencialCard extends StatelessWidget { textAlign: TextAlign.center, ), OccludeWrapper( - child: Text( - usuario!.rut?.formateado(true) ?? "Sin RUT", + child: Text(user?.rut?.toString() ?? "Sin RUT", style: TextStyle(fontSize: 18), textAlign: TextAlign.center, ), @@ -94,10 +92,7 @@ class CredencialCard extends StatelessWidget { Spacer(), Divider(height: 1), Spacer(), - Text( - (carrera?.nombre == null || carrera!.nombre!.isEmpty - ? "Sin carrera" - : carrera?.nombre!)!, + Text(("${carrera?.nombre}".isEmpty ? "Sin carrera" : "${carrera?.nombre}"), maxLines: 3, style: TextStyle( color: MainTheme.primaryDarkColor, @@ -105,47 +100,46 @@ class CredencialCard extends StatelessWidget { ), textAlign: TextAlign.center, ), - if (usuario!.rut != null || true) Spacer(), - if (usuario!.rut != null || true) - Column( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide(color: Colors.grey), - left: BorderSide(color: Colors.grey), - bottom: BorderSide(color: Colors.grey), - right: BorderSide(color: Colors.grey), - ), - ), - padding: EdgeInsets.symmetric( - vertical: 5, - horizontal: 10, + const Spacer(), + Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide(color: Colors.grey), + left: BorderSide(color: Colors.grey), + bottom: BorderSide(color: Colors.grey), + right: BorderSide(color: Colors.grey), ), - child: OccludeWrapper( - child: BarcodeWidget( - barcode: Barcode.code39(), - data: "${usuario!.rut!.numero}", - width: 200, - height: 50, - drawText: false, - ), + ), + padding: EdgeInsets.symmetric( + vertical: 5, + horizontal: 10, + ), + child: OccludeWrapper( + child: BarcodeWidget( + barcode: Barcode.code39(), + data: "${user?.rut}", + width: 200, + height: 50, + drawText: false, ), ), - Container(height: 10), - MarkdownBody( - selectable: false, - styleSheet: MarkdownStyleSheet( - textAlign: WrapAlignment.center, - p: TextStyle( - fontSize: 12, - ), + ), + Container(height: 10), + MarkdownBody( + selectable: false, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: TextStyle( + fontSize: 12, ), - data: RemoteConfigService.credencialBarras, ), - ], - ) + data: RemoteConfigService.credencialBarras, + ), + ], + ), ], ), ), diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 1880c31..f946696 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -4,33 +4,41 @@ import 'package:badges/badges.dart' as badge; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/config/routes.dart'; -import 'package:mi_utem/models/usuario.dart'; -import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; +import 'package:mi_utem/screens/credencial_screen.dart'; +import 'package:mi_utem/screens/horario/horario_screen.dart'; +import 'package:mi_utem/screens/main_screen.dart'; +import 'package:mi_utem/screens/usuario_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; +import 'package:watch_it/watch_it.dart'; class CustomDrawer extends StatelessWidget { - final Usuario usuario; - CustomDrawer({Key? key, required this.usuario}) : super(key: key); - String? _getRoute(String? name) { + const CustomDrawer({ + super.key, + }); + + Widget? _getRoute(String? name) { switch (name) { case "Perfil": - return Routes.perfil; + return UsuarioScreen(); case "Asignaturas": - return Routes.asignaturas; + return AsignaturasListaScreen(); case "Horario": - return Routes.horario; + return HorarioScreen(); case "Credencial": - return Routes.credencial; + return CredencialScreen(); // case "Docentes": // return DocentesScreen(); // break; default: - return Routes.home; + return MainScreen(); } } @@ -42,108 +50,112 @@ class CustomDrawer extends StatelessWidget { @override Widget build(BuildContext context) { + final _authService = di.get(); + return Drawer( semanticLabel: "Abrir menú", child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return SingleChildScrollView( + builder: (context, BoxConstraints constraints) => SingleChildScrollView( child: ConstrainedBox( constraints: constraints.copyWith( minHeight: constraints.maxHeight, maxHeight: double.infinity, ), - child: IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - UserAccountsDrawerHeader( - accountEmail: Text( - usuario.correoUtem ?? usuario.correoPersonal ?? ""), - accountName: Text( - usuario.nombreCompleto ?? "", - style: - TextStyle(fontSize: 16, fontWeight: FontWeight.w500), - ), - currentAccountPicture: ProfilePhoto( - usuario: usuario, - radius: 30, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomLeft, - end: Alignment.topRight, - colors: [ - MainTheme.utemAzul, - MainTheme.utemVerde, - ], + child: FutureBuilder( + future: _authService.getUser(), + builder: (context, AsyncSnapshot snapshot) { + User? user = snapshot.data; + if(user == null) { + return Container(); + } + + return IntrinsicHeight( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + UserAccountsDrawerHeader( + accountEmail: Text(user.correoUtem ?? user.correoPersonal), + accountName: Text(user.nombreCompleto, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + currentAccountPicture: ProfilePhoto( + user: user, + radius: 30, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomLeft, + end: Alignment.topRight, + colors: [MainTheme.utemAzul, MainTheme.utemVerde], + ), + ), ), - ), - ), - for (var e in _menu!) - ListTile( - leading: Icon(IconData(e["icono"]["codePoint"], - fontFamily: e["icono"]["fontFamily"], - fontPackage: e["icono"]["fontPackage"])), - title: Text(e["nombre"]), - trailing: e["esNuevo"] - ? badge.Badge( - shape: badge.BadgeShape.square, - borderRadius: BorderRadius.circular(10), - padding: EdgeInsets.symmetric( - horizontal: 6, vertical: 3), - elevation: 0, - badgeContent: Text( - 'Nuevo', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), + for (final e in _menu!) + ListTile( + leading: Icon(IconData(e["icono"]["codePoint"], + fontFamily: e["icono"]["fontFamily"], + fontPackage: e["icono"]["fontPackage"], + )), + title: Text(e["nombre"]), + trailing: e["esNuevo"] ? badge.Badge( + shape: badge.BadgeShape.square, + borderRadius: BorderRadius.circular(10), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + elevation: 0, + badgeContent: const Text("Nuevo", + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, ), - ) - : null, - onTap: () async { - String? route = _getRoute(e["nombre"]); - if (route != null) { - Get.toNamed(route); - ReviewService.checkAndRequestReview(); - } - }, - ), - Expanded( - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Divider(height: 5), - ListTile( - leading: Icon(Mdi.heart), - title: Text('Acerca de Mi UTEM'), - onTap: () async { - await Get.toNamed(Routes.about); + ), + ) + : null, + onTap: () { + Widget? route = _getRoute(e["nombre"]); + if (route != null) { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => route)); ReviewService.checkAndRequestReview(); - }, - ), - ListTile( - leading: Icon(Mdi.closeCircle), - title: Text('Cerrar sesión'), - onTap: () async { - await AuthService.logOut(); - - await Get.offAllNamed(Routes.home); - }, + } + }, + ), + Expanded( + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Divider(height: 5), + ListTile( + leading: const Icon(Mdi.heart), + title: const Text("Acerca de Mi UTEM"), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => AcercaScreen())); + ReviewService.checkAndRequestReview(); + }, + ), + ListTile( + leading: const Icon(Mdi.closeCircle), + title: const Text('Cerrar sesión'), + onTap: () async { + await _authService.logout(context); + }, + ), + ], ), - ], + ), ), - ), + ], ), - ], - ), + ); + }, ), ), - ); - }), + ), + ), ); } } diff --git a/lib/widgets/custom_error_widget.dart b/lib/widgets/custom_error_widget.dart index f3b332e..483f843 100644 --- a/lib/widgets/custom_error_widget.dart +++ b/lib/widgets/custom_error_widget.dart @@ -6,40 +6,37 @@ class CustomErrorWidget extends StatelessWidget { final String title; final Object? error; - CustomErrorWidget({ - Key? key, + const CustomErrorWidget({ + super.key, this.emoji = "😕", this.title = "Ocurrió un error inesperado", this.error, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(20), + return Padding( + padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - emoji, + Text(emoji, textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( fontSize: 50, ), ), - Container(height: 15), - Text( - title, + const SizedBox(height: 15), + Text(title, textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18, ), ), if (error != null) ...[ - Container(height: 15), - Text( - error.toString(), + const SizedBox(height: 15), + Text("$error", textAlign: TextAlign.center, ), ], diff --git a/lib/widgets/default_network_image.dart b/lib/widgets/default_network_image.dart index b73470a..598199b 100644 --- a/lib/widgets/default_network_image.dart +++ b/lib/widgets/default_network_image.dart @@ -1,7 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/widgets/image_view_screen.dart'; class DefaultNetworkImage extends StatelessWidget { @@ -17,14 +15,9 @@ class DefaultNetworkImage extends StatelessWidget { height: double.infinity, width: double.infinity, child: CachedNetworkImage( - imageUrl: url ?? "", + imageUrl: "$url", imageBuilder: (context, imageProvider) => GestureDetector( - onTap: () async { - Get.to( - () => ImageViewScreen(imageProvider: imageProvider), - routeName: Routes.imageView, - ); - }, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen(imageProvider: imageProvider))), child: Container( height: double.infinity, width: double.infinity, diff --git a/lib/widgets/dialogs/not_ready_dialog.dart b/lib/widgets/dialogs/not_ready_dialog.dart index f0818e5..1d1dabd 100644 --- a/lib/widgets/dialogs/not_ready_dialog.dart +++ b/lib/widgets/dialogs/not_ready_dialog.dart @@ -1,89 +1,65 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; import 'package:mi_utem/widgets/error_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; class NotReadyDialog extends StatelessWidget { const NotReadyDialog({ - Key? key, - }) : super(key: key); + super.key, + }); @override - Widget build(BuildContext context) { - return ErrorDialog( - mensaje: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: TextStyle( - color: Colors.grey[800], - fontSize: 16, - ), - children: [ - TextSpan( - text: "Aún no estás listo para esto", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: MainTheme.primaryDarkColor, - ), + Widget build(BuildContext context) => ErrorDialog( + mensaje: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + color: Colors.grey[800], + fontSize: 16, + ), + children: [ + TextSpan( + text: "Aún no estás listo para esto", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: MainTheme.primaryDarkColor, ), - TextSpan( - text: "\n\n👀\n\n", - style: TextStyle( - fontSize: 80, - ), + ), + const TextSpan( + text: "\n\n\u{1F440}\n\n", + style: TextStyle( + fontSize: 80, ), - TextSpan( - text: - "• Si eres estudiante nuevo, debes esperar a que se habilite la plataforma para ti. \n• Si eres estudiante antiguo y tampoco puedes acceder a la plataforma web "), - TextSpan( - text: "mi.utem.cl", - style: TextStyle( - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://mi.utem.cl/?ref=appMiUtemInndev", - ), - ); - }, + ), + const TextSpan(text: "• Si eres estudiante nuevo, debes esperar a que se habilite la plataforma para ti. \n• Si eres estudiante antiguo y tampoco puedes acceder a la plataforma web "), + TextSpan( + text: "mi.utem.cl", + style: const TextStyle( + decoration: TextDecoration.underline, ), - TextSpan(text: ", contáctate con "), - TextSpan( - text: "soporte.sisei@utem.cl", - style: TextStyle( - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "mailto:soporte.sisei@utem.cl", - ), - ); - }, + recognizer: TapGestureRecognizer()..onTap = () => launchUrl(Uri.parse("https://mi.utem.cl/?ref=appMiUtemInndev")), + ), + const TextSpan(text: ", contáctate con "), + TextSpan( + text: "soporte.sisei@utem.cl", + style: const TextStyle( + decoration: TextDecoration.underline, ), - TextSpan( - text: - ", ellos podrán ayudarte. \n• Si el problema solo es la app, contáctanos a "), - TextSpan( - text: "nuestras redes sociales", - style: TextStyle( - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - Get.toNamed(Routes.about); - }, + recognizer: TapGestureRecognizer()..onTap = () => launchUrl(Uri.parse("mailto:soporte.sisei@utem.cl")), + ), + TextSpan(text: ", ellos podrán ayudarte. \n• Si el problema solo es la app, contáctanos a "), + TextSpan( + text: "nuestras redes sociales", + style: TextStyle( + decoration: TextDecoration.underline, ), - ], - ), + recognizer: TapGestureRecognizer()..onTap = () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => AcercaScreen())), + ), + ], ), - ); - } + ), + ); } diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart index 65cefb4..686e790 100644 --- a/lib/widgets/loading_indicator.dart +++ b/lib/widgets/loading_indicator.dart @@ -8,7 +8,7 @@ class LoadingIndicator extends StatelessWidget { final EdgeInsetsGeometry padding; final String? message; - LoadingIndicator({ + const LoadingIndicator({ this.color = const Color(0xFF009d9b), this.controller, this.padding = const EdgeInsets.all(20), @@ -16,26 +16,24 @@ class LoadingIndicator extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 30), - child: Center( - child: Container( - padding: padding, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SpinKitDoubleBounce( - controller: controller, - color: color, - size: 40.0, - ), - if (message != null) Container(height: 10), - if (message != null) Text(message!), - ], - ), + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Center( + child: Padding( + padding: padding, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SpinKitDoubleBounce( + controller: controller, + color: color, + size: 40.0, + ), + if (message != null) const SizedBox(height: 10), + if (message != null) Text("$message"), + ], ), ), - ); - } + ), + ); } diff --git a/lib/screens/login_screen/_background.dart b/lib/widgets/login_screen/background.dart similarity index 81% rename from lib/screens/login_screen/_background.dart rename to lib/widgets/login_screen/background.dart index 26c0b5f..456436f 100644 --- a/lib/screens/login_screen/_background.dart +++ b/lib/widgets/login_screen/background.dart @@ -1,18 +1,19 @@ -part of 'login_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; -class _Background extends StatefulWidget { +class LoginBackground extends StatefulWidget { final Widget child; - const _Background({ + const LoginBackground({ Key? key, required this.child, }) : super(key: key); @override - _BackgroundState createState() => _BackgroundState(); + _LoginBackgroundState createState() => _LoginBackgroundState(); } -class _BackgroundState extends State<_Background> { +class _LoginBackgroundState extends State { late final VideoPlayerController _controller; @override @@ -22,8 +23,7 @@ class _BackgroundState extends State<_Background> { videoPlayerOptions: VideoPlayerOptions( mixWithOthers: true, ), - ) - ..setVolume(0) + )..setVolume(0) ..play() ..setLooping(true) ..initialize(); @@ -70,7 +70,7 @@ class _BackgroundState extends State<_Background> { color: Color(0x80000000), ), Container( - height: Get.height, + height: double.infinity, child: widget.child, ), ], diff --git a/lib/widgets/login_screen/creditos_app.dart b/lib/widgets/login_screen/creditos_app.dart index 1b2fd64..787a578 100644 --- a/lib/widgets/login_screen/creditos_app.dart +++ b/lib/widgets/login_screen/creditos_app.dart @@ -3,48 +3,47 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; class CreditosApp extends StatelessWidget { - String get _creditText { - List texts = jsonDecode(RemoteConfigService.creditos); + const CreditosApp({ + super.key + }); - Random random = new Random(); - - return texts[random.nextInt(texts.length)]; + get _creditText { + final texts = jsonDecode(RemoteConfigService.creditos) as List; + return texts[Random().nextInt(texts.length)]; } @override - Widget build(BuildContext context) { - return Expanded( - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: EdgeInsets.all(10), - child: SafeArea( - child: GestureDetector( - child: MarkdownBody( - selectable: false, - styleSheet: MarkdownStyleSheet( - textAlign: WrapAlignment.center, - p: TextStyle(color: Colors.white), - ), - data: _creditText, + Widget build(BuildContext context) => Expanded( + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: SafeArea( + child: GestureDetector( + child: MarkdownBody( + selectable: false, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: TextStyle(color: Colors.white), ), - onTap: () { - Get.toNamed(Routes.about); - }, + data: _creditText, ), + onTap: () => Navigator.push(context, MaterialPageRoute( + builder: (context) => AcercaScreen(), + fullscreenDialog: true, + )), ), ), - ], - ), + ), + ], ), - ); - } + ), + ); } \ No newline at end of file diff --git a/lib/widgets/login_screen/formulario_credenciales.dart b/lib/widgets/login_screen/formulario_credenciales.dart index 19b3fbf..71488bb 100644 --- a/lib/widgets/login_screen/formulario_credenciales.dart +++ b/lib/widgets/login_screen/formulario_credenciales.dart @@ -7,7 +7,6 @@ class FormularioCredenciales extends StatefulWidget { final TextEditingController _correoController; final TextEditingController _contraseniaController; - FormularioCredenciales({ required TextEditingController correoController, required TextEditingController contraseniaController }) : _contraseniaController = contraseniaController, _correoController = correoController; @override @@ -17,46 +16,43 @@ class FormularioCredenciales extends StatefulWidget { class _FormularioCredencialesState extends State { @override - Widget build(BuildContext context) { - return AutofillGroup( - child: Column( - children: [ - LoginTextFormField( - controller: widget._correoController, - hintText: 'nombre@utem.cl', - labelText: 'Correo UTEM', - textCapitalization: TextCapitalization.none, - keyboardType: TextInputType.emailAddress, - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp(" ")), - ], - icon: Icons.person, - autofillHints: [AutofillHints.username], - validator: (String value) { - if (value.isEmpty) { - return 'Debe ingresar un correo UTEM'; - } else if (value.contains("@") && - !value.endsWith("@utem.cl")) { - return 'Debe ingresar un correo UTEM'; - } - }, - ), - LoginTextFormField( - controller: widget._contraseniaController, - hintText: '• • • • • • • • •', - labelText: 'Contraseña', - textCapitalization: TextCapitalization.none, - icon: Icons.lock, - obscureText: true, - autofillHints: [AutofillHints.password], - validator: (String value) { - if (value.isEmpty) { - return 'Debe ingresar una contraseña'; - } - }, - ) - ], - ), - ); - } + Widget build(BuildContext context) => AutofillGroup( + child: Column( + children: [ + LoginTextFormField( + controller: widget._correoController, + hintText: 'nombre@utem.cl', + labelText: 'Correo UTEM', + textCapitalization: TextCapitalization.none, + keyboardType: TextInputType.emailAddress, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(" ")), + ], + icon: Icons.person, + autofillHints: [AutofillHints.email, AutofillHints.username], + validator: (String value) { + if (value.isEmpty) { + return 'Debe ingresar un correo UTEM'; + } else if (value.contains("@") && !value.endsWith("@utem.cl")) { + return 'Debe ingresar un correo UTEM'; + } + }, + ), + LoginTextFormField( + controller: widget._contraseniaController, + hintText: '• • • • • • • • •', + labelText: 'Contraseña', + textCapitalization: TextCapitalization.none, + icon: Icons.lock, + obscureText: true, + autofillHints: [AutofillHints.password], + validator: (String value) { + if (value.isEmpty) { + return 'Debe ingresar una contraseña'; + } + }, + ) + ], + ), + ); } \ No newline at end of file diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index 849e78b..531377b 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -1,15 +1,17 @@ -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/helpers/snackbars.dart'; -import 'package:mi_utem/models/usuario.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/user/credential.dart'; +import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/services_new/interfaces/credential_service.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; -import 'package:mi_utem/widgets/dialogs/not_ready_dialog.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart' as NewAuthService; +import 'package:watch_it/watch_it.dart'; class LoginButton extends StatefulWidget { @@ -35,13 +37,14 @@ class LoginButton extends StatefulWidget { class _LoginButtonState extends State { + final _authService = di.get(); + final _credentialsService = di.get(); + @override - Widget build(BuildContext context) { - return TextButton( - onPressed: () => _login(), - child: Text("Iniciar"), - ); - } + Widget build(BuildContext context) => TextButton( + onPressed: () => _login(), + child: Text("Iniciar"), + ); Future _login() async { final correo = widget._correoController.text; @@ -62,37 +65,49 @@ class _LoginButtonState extends State { Get.dialog(LoadingDialog(), barrierDismissible: false); try { - bool esPrimeraVez = await AuthService.esPrimeraVez(); - Usuario usuario = await AuthService.login(correo, contrasenia, true); + await _credentialsService.setCredentials(Credentials( + email: correo, + password: contrasenia, + )); + + if(!(await _credentialsService.hasCredentials())) { + showDefaultSnackbar("Error", "Ha ocurrido un error al guardar tus claves. Intenta más tarde."); + return; + } - AnalyticsService.logEvent('login'); - AnalyticsService.setUser(usuario); + await _authService.login(); - Get.toNamed(Routes.home); + try { + final isFirstTime = await _authService.isFirstTime(); + final user = await _authService.getUser(); + if(user == null) { + Get.back(); + showDefaultSnackbar("Error", "Ha ocurrido un error desconocido. Por favor intenta más tarde."); + return; + } - if (esPrimeraVez) { - Get.dialog(AcercaDialog()); - } - } on DioError catch (e) { - print(e.message); - Get.back(); - if (e.response?.statusCode == 403) { - if (e.response?.data["codigoInterno"]?.toString() == "4") { - Get.dialog(NotReadyDialog()); - } else { - showDefaultSnackbar("Error", "Usuario o contraseña incorrecta"); + AnalyticsService.logEvent('login'); + AnalyticsService.setUser(user); + + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); + + if(isFirstTime) { + Get.dialog(AcercaDialog()); } - } else if (e.response?.statusCode != null && e.response!.statusCode.toString().startsWith("5")) { - print(e.response?.data); - Get.dialog(MonkeyErrorDialog()); - } else { - print(e.response?.data); - showDefaultSnackbar("Error", "Ocurrió un error inesperado 😢"); + } catch (e) { + logger.e(e); + Get.back(); + showDefaultSnackbar("Error", "Ha ocurrido un error desconocido. Por favor intenta más tarde."); } + return; + } on CustomException catch (e) { + logger.e(e); + Get.back(); + showDefaultSnackbar("Error", e.message); } catch (e) { - print(e.toString()); + logger.e(e); Get.back(); - showDefaultSnackbar("Error", "Ocurrió un error inesperado 😢"); + showDefaultSnackbar("Error", "Ha ocurrido un error desconocido. Por favor intenta más tarde."); } } } \ No newline at end of file diff --git a/lib/widgets/login_screen/login_form.dart b/lib/widgets/login_screen/login_form.dart new file mode 100644 index 0000000..df2f7dd --- /dev/null +++ b/lib/widgets/login_screen/login_form.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mi_utem/services/update_service.dart'; +import 'package:mi_utem/services_new/interfaces/credential_service.dart'; +import 'package:mi_utem/widgets/login_screen/creditos_app.dart'; +import 'package:mi_utem/widgets/login_screen/formulario_credenciales.dart'; +import 'package:mi_utem/widgets/login_screen/login_button.dart'; +import 'package:watch_it/watch_it.dart'; + +class LoginForm extends StatefulWidget { + final BoxConstraints constraints; + + const LoginForm({ + super.key, + required this.constraints, + }); + + @override + State createState() => _LoginFormState(); +} + +class _LoginFormState extends State { + + final GlobalKey _formKey = GlobalKey(); + final TextEditingController _correoController = TextEditingController(); + final TextEditingController _contraseniaController = TextEditingController(); + + final _credentialService = di.get(); + + @override + void initState() { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Brightness.light, + statusBarIconBrightness: Brightness.light, + systemNavigationBarColor: Colors.black, + systemNavigationBarIconBrightness: Brightness.light, + )); + + UpdateService(); + + _credentialService.getCredentials().then((credential){ + if(credential == null) { + return; + } + + _correoController.text = credential.email; + _contraseniaController.text = credential.password; + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) => SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: ConstrainedBox( + constraints: widget.constraints.copyWith( + minHeight: widget.constraints.maxHeight, + maxHeight: double.infinity, + ), + child: IntrinsicHeight( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container(height: widget.constraints.maxHeight * 0.1), + Expanded( + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Hero( + tag: 'utemLogo', + child: Image.asset('assets/images/utem_logo_color_blanco.png', width: 250), + ), + ], + ), + ), + ), + Container(height: widget.constraints.maxHeight * 0.1), + FormularioCredenciales( + correoController: _correoController, + contraseniaController: _contraseniaController, + ), + LoginButton( + correoController: _correoController, + contraseniaController: _contraseniaController, + formKey: _formKey, + ), + Container(height: widget.constraints.maxHeight * 0.1), + const CreditosApp(), + ], + ), + ), + ), + ), + ); +} diff --git a/lib/widgets/login_text_form_field.dart b/lib/widgets/login_text_form_field.dart index 26e3230..80d2580 100644 --- a/lib/widgets/login_text_form_field.dart +++ b/lib/widgets/login_text_form_field.dart @@ -97,6 +97,9 @@ class _LoginTextFormFieldState extends State { } return errorMsg; }, + onTapOutside: (event){ + FocusScope.of(context).unfocus(); + }, ), ); } diff --git a/lib/widgets/nota_list_item.dart b/lib/widgets/nota_list_item.dart index dbfd375..8d0576f 100644 --- a/lib/widgets/nota_list_item.dart +++ b/lib/widgets/nota_list_item.dart @@ -1,9 +1,10 @@ +import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_masked_text/flutter_masked_text.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:watch_it/watch_it.dart'; class NotaListItem extends StatelessWidget { final IEvaluacion evaluacion; @@ -23,15 +24,9 @@ class NotaListItem extends StatelessWidget { this.onDelete, }) : super(key: key); - final _controller = CalculatorController.to; + String get _suggestedGrade => di.get().getSuggestedGrade?.toStringAsFixed(1) ?? "0.0"; - String get _suggestedGrade { - return _controller.suggestedGrade?.toStringAsFixed(1) ?? "0.0"; - } - - String? get _suggestedPercentage { - return _controller.suggestedPercentage?.toStringAsFixed(0); - } + String? get _suggestedPercentage => di.get().getSuggestedPercentage?.toStringAsFixed(0); @override Widget build(BuildContext context) { @@ -51,11 +46,10 @@ class NotaListItem extends StatelessWidget { return Flex( direction: Axis.horizontal, mainAxisSize: MainAxisSize.max, - children: [ + children: [ Container( width: 90, - child: Text( - evaluacion.descripcion ?? "Nota", + child: Text(evaluacion.descripcion ?? "Nota", overflow: TextOverflow.ellipsis, ), ), @@ -101,14 +95,9 @@ class NotaListItem extends StatelessWidget { textAlign: TextAlign.center, onChanged: (String value) { final percentage = int.tryParse(value); - - final changedGrade = - evaluacion.copyWith(porcentaje: percentage); + final changedGrade = evaluacion.copyWith(porcentaje: percentage); changedGrade.porcentaje = percentage; - onChanged?.call(changedGrade); - - //_controller.changeGradeAt(widget.index, changedGrade); }, enabled: editable, decoration: InputDecoration( @@ -130,7 +119,6 @@ class NotaListItem extends StatelessWidget { GestureDetector( onTap: () { onDelete?.call(); - //_controller.removeGradeAt(widget.index); }, child: Icon( Icons.delete, diff --git a/lib/widgets/noticias/NoticiasCarruselWidget.dart b/lib/widgets/noticias/NoticiasCarruselWidget.dart index 56eb8d0..8d0e89c 100644 --- a/lib/widgets/noticias/NoticiasCarruselWidget.dart +++ b/lib/widgets/noticias/NoticiasCarruselWidget.dart @@ -1,11 +1,13 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/services/noticias_service.dart'; +import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/noticias/NoticiaCardWidget.dart'; +import 'package:watch_it/watch_it.dart'; class NoticiasCarruselWidget extends StatefulWidget { NoticiasCarruselWidget({Key? key}) : super(key: key); @@ -15,56 +17,51 @@ class NoticiasCarruselWidget extends StatefulWidget { } class _NoticiasCarruselWidgetState extends State { - Future>? _noticiasFuture; @override - void initState() { - super.initState(); - _noticiasFuture = _getNoticias(); - } + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text("Noticias".toUpperCase(), + style: Get.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 10), + FutureBuilder( + future: di.get().getNoticias(), + builder: (context, AsyncSnapshot?> snapshot) { + if (snapshot.hasError) { + final error = snapshot.error is CustomException ? (snapshot.error as CustomException) : CustomException.custom("No pudimos obtener las noticias."); - Future> _getNoticias() async => await NoticiasService().getNoticias(); + return CustomErrorWidget( + title: "Ocurrió un error al obtener las noticias", + error: error.message, + ); + } - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 20), - child: Text("Noticias".toUpperCase(), - style: Get.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.bold, + List? noticias = snapshot.data; + + if(!snapshot.hasData || noticias == null || noticias.isEmpty) { + return Center(child: LoadingIndicator()); + } + + return CarouselSlider.builder( + options: CarouselOptions( + autoPlay: true, + height: 200, + viewportFraction: 0.5, + initialPage: 0, ), - ), - ), - Container(height: 10), - FutureBuilder>( - future: _noticiasFuture, - builder: (context, snapshot) { - if (snapshot.hasError) { - return CustomErrorWidget(title: "Ocurrió un error al obtener las noticias", error: snapshot.error); - } else { - if (snapshot.hasData && snapshot.data!.length > 0) { - List noticias = snapshot.data!; - return CarouselSlider.builder( - options: CarouselOptions( - autoPlay: true, - height: 200, - viewportFraction: 0.5, - initialPage: 0, - ), - itemBuilder: (BuildContext context, int i, int rI) => NoticiaCardWidget(noticias[i]), - itemCount: noticias.length, - ); - } else { - return Center(child: LoadingIndicator()); - } - } - }, - ), - ], - ); - } + itemBuilder: (BuildContext context, int i, int rI) => NoticiaCardWidget(noticias[i]), + itemCount: noticias.length, + ); + }, + ), + ], + ); } diff --git a/lib/widgets/permiso_card.dart b/lib/widgets/permiso_card.dart index 876207f..3c5fbe1 100644 --- a/lib/widgets/permiso_card.dart +++ b/lib/widgets/permiso_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; import 'package:mi_utem/models/permiso_covid.dart'; +import 'package:mi_utem/screens/permiso_covid_screen.dart'; import 'package:mi_utem/themes/theme.dart'; class PermisoCard extends StatelessWidget { @@ -19,9 +19,7 @@ class PermisoCard extends StatelessWidget { child: Material( color: Colors.transparent, child: InkWell( - onTap: () => Get.toNamed( - '${Routes.passBase}/${permiso.id}', - ), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => PermisoCovidScreen(passId: "${permiso.id}"))), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -66,9 +64,7 @@ class PermisoCard extends StatelessWidget { Container( height: 40, child: InkWell( - onTap: () => Get.toNamed( - '${Routes.passBase}/${permiso.id}', - ), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => PermisoCovidScreen(passId: "${permiso.id}"))), child: Center( child: Text( "Ver QR", diff --git a/lib/widgets/permisos_section.dart b/lib/widgets/permisos_section.dart index 84a11d8..ec511d6 100644 --- a/lib/widgets/permisos_section.dart +++ b/lib/widgets/permisos_section.dart @@ -1,13 +1,18 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/controllers/qr_passes_controller.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/permiso_covid.dart'; +import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/permiso_card.dart'; +import 'package:watch_it/watch_it.dart'; + +class PermisosCovidSection extends StatelessWidget { -class PermisosCovidSection extends GetView { const PermisosCovidSection({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -27,28 +32,34 @@ class PermisosCovidSection extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, ), ), - Container(height: 10), + SizedBox(height: 10), SizedBox( height: 155, - child: Obx( - () { - if (controller.isLoading.value) { + child: FutureBuilder?>( + future: di.get().getPermisos(), + builder: (context, snapshot) { + if(snapshot.connectionState == ConnectionState.waiting) { return LoadingIndicator( message: "Esto tardará un poco, paciencia...", ); } - if (controller.passes.length == 0) { + if(snapshot.hasError) { + final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al cargar los permisos"; + return Text(error); + } + + if(snapshot.data == null || snapshot.data?.isNotEmpty != true) { return Text('No hay permisos de ingresos'); } return ListView.separated( - itemCount: controller.passes.length, + itemCount: snapshot.data!.length, padding: EdgeInsets.symmetric(horizontal: 20), scrollDirection: Axis.horizontal, separatorBuilder: (context, index) => Container(width: 10), itemBuilder: (context, index) => PermisoCard( - permiso: controller.passes[index], + permiso: snapshot.data![index], ), ); }, diff --git a/lib/widgets/profile_photo.dart b/lib/widgets/profile_photo.dart index dc45155..a19fb8f 100644 --- a/lib/widgets/profile_photo.dart +++ b/lib/widgets/profile_photo.dart @@ -6,144 +6,139 @@ import 'package:circular_profile_avatar/circular_profile_avatar.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/imagen_editor_modal.dart'; class ProfilePhoto extends StatefulWidget { final double radius; - final Usuario? usuario; + final User? user; final Function(BuildContext, ImageProvider)? onImageTap; final Function()? onTap; final double borderWidth; final Color borderColor; final bool editable; final Function(String)? onImage; - ProfilePhoto( - {Key? key, - this.onTap, - this.onImageTap, - this.radius = 25, - this.borderColor = Colors.white, - this.borderWidth = 0.0, - required this.usuario, - this.onImage, - this.editable = false}) - : super(key: key); + + ProfilePhoto({ + super.key, + required this.user, + this.onTap, + this.onImageTap, + this.radius = 25, + this.borderColor = Colors.white, + this.borderWidth = 0.0, + this.onImage, + this.editable = false + }); @override _ProfilePhotoState createState() => _ProfilePhotoState(); } class _ProfilePhotoState extends State { - ImagePicker picker = ImagePicker(); - - @override - void initState() { - super.initState(); - } + final _picker = ImagePicker(); @override - Widget build(BuildContext context) { - return Container( - width: widget.radius * 2, - height: widget.radius * 2, - child: Stack( - children: [ - CircularProfileAvatar( - widget.usuario!.fotoUrl ?? "", - onTap: () => widget.onTap != null && widget.onImageTap == null - ? widget.onTap - : null, - borderColor: widget.borderColor, - borderWidth: widget.borderWidth, - radius: widget.radius, - backgroundColor: MainTheme.primaryColor, - imageBuilder: (context, imageProvider) => GestureDetector( - onTap: () { - if (widget.onImageTap != null) { - widget.onImageTap!(context, imageProvider); - } - }, - child: Container( - height: double.infinity, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - image: DecorationImage( - image: imageProvider, - fit: BoxFit.cover, - ), + Widget build(BuildContext context) => Container( + width: widget.radius * 2, + height: widget.radius * 2, + child: Stack( + children: [ + CircularProfileAvatar( + widget.user!.fotoUrl ?? "", + onTap: () => widget.onTap != null && widget.onImageTap == null + ? widget.onTap + : null, + borderColor: widget.borderColor, + borderWidth: widget.borderWidth, + radius: widget.radius, + backgroundColor: MainTheme.primaryColor, + imageBuilder: (context, imageProvider) => GestureDetector( + onTap: () { + if (widget.onImageTap != null) { + widget.onImageTap!(context, imageProvider); + } + }, + child: Container( + height: double.infinity, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, ), ), ), - initialsText: Text( - widget.usuario!.iniciales, - style: TextStyle( - fontSize: widget.radius * 0.5, - color: Colors.white, - ), + ), + initialsText: Text( + widget.user!.iniciales, + style: TextStyle( + fontSize: widget.radius * 0.5, + color: Colors.white, ), ), - if (widget.editable) - InkWell( - onTap: () async { - try { - final imagen = await picker.pickImage( - source: ImageSource.gallery, - ); - if (imagen != null) { - Uint8List imagenOriginalBytes = - File(imagen.path).readAsBytesSync(); + ), + if (widget.editable) + InkWell( + onTap: () async { + try { + final imagen = await _picker.pickImage( + source: ImageSource.gallery, + ); + if (imagen != null) { + Uint8List imagenOriginalBytes = + File(imagen.path).readAsBytesSync(); - Uint8List imagenEditadaBytes = await Get.to( - () => ImagenEditorModal( - imagenInicial: imagenOriginalBytes, - aspectRatio: 1, - ), - ); + Uint8List imagenEditadaBytes = await Get.to( + () => ImagenEditorModal( + imagenInicial: imagenOriginalBytes, + aspectRatio: 1, + ), + ); - String imagenEditadaBase64 = - base64Encode(imagenEditadaBytes); + String imagenEditadaBase64 = + base64Encode(imagenEditadaBytes); - widget.onImage!(imagenEditadaBase64); - } else { - Get.snackbar( - "Error", - "No se pudo obtener la foto", - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); - } - } catch (e) { - print(e); + widget.onImage!(imagenEditadaBase64); + } else { Get.snackbar( "Error", - "No se pudo cambiar la foto", + "No se pudo obtener la foto", colorText: Colors.white, backgroundColor: Get.theme.primaryColor, snackPosition: SnackPosition.BOTTOM, margin: EdgeInsets.all(20), ); } - }, - borderRadius: BorderRadius.circular(25), - child: Container( - padding: EdgeInsets.all(5), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(30), - ), - child: Icon( - Icons.camera_alt, - color: Get.theme.primaryColor, - ), + } catch (e) { + print(e); + Get.snackbar( + "Error", + "No se pudo cambiar la foto", + colorText: Colors.white, + backgroundColor: Get.theme.primaryColor, + snackPosition: SnackPosition.BOTTOM, + margin: EdgeInsets.all(20), + ); + } + }, + borderRadius: BorderRadius.circular(25), + child: Container( + padding: EdgeInsets.all(5), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + ), + child: Icon( + Icons.camera_alt, + color: Get.theme.primaryColor, ), ), - ], - ), - ); - } + ), + ], + ), + ); } diff --git a/lib/widgets/pull_to_refresh.dart b/lib/widgets/pull_to_refresh.dart index 4dc6ff9..aa4c7f1 100644 --- a/lib/widgets/pull_to_refresh.dart +++ b/lib/widgets/pull_to_refresh.dart @@ -11,63 +11,56 @@ class PullToRefresh extends StatelessWidget { final Future Function() onRefresh; final bool opacityEffect; - const PullToRefresh( - {Key? key, - this.child, - required this.onRefresh, - this.opacityEffect = false}) - : super(key: key); + const PullToRefresh({ + super.key, + this.child, + required this.onRefresh, + this.opacityEffect = false + }); static double _offsetToArmed = 60; @override - Widget build(BuildContext context) { - return CustomRefreshIndicator( - offsetToArmed: _offsetToArmed, - onRefresh: onRefresh, - builder: (context, child, controller) => Stack( - children: [ - AnimatedBuilder( - child: child, - animation: controller, - builder: (context, child) { - return Opacity( - opacity: 1.0 - - (opacityEffect ? controller.value.clamp(0.0, 1.0) : 0), - child: Transform.translate( - offset: Offset(0.0, (_offsetToArmed) * controller.value), - child: child, - ), - ); - }, + Widget build(BuildContext context) => CustomRefreshIndicator( + offsetToArmed: _offsetToArmed, + onRefresh: onRefresh, + builder: (context, child, controller) => Stack( + children: [ + AnimatedBuilder( + child: child, + animation: controller, + builder: (context, child) => Opacity( + opacity: 1.0 - (opacityEffect ? controller.value.clamp(0.0, 1.0) : 0), + child: Transform.translate( + offset: Offset(0.0, (_offsetToArmed) * controller.value), + child: child, + ), ), - AnimatedBuilder( - child: LoadingIndicator(padding: EdgeInsets.zero), - animation: controller, - builder: (context, child) { - return SafeArea( - child: Stack( - children: [ - Container( - height: (_offsetToArmed) * controller.value, - width: double.infinity, - child: Container( - height: 30, - width: 30, - child: SpinKitDoubleBounce( - color: MainTheme.primaryColor, - size: 20.0, - ), - ), + ), + AnimatedBuilder( + child: const LoadingIndicator(padding: EdgeInsets.zero), + animation: controller, + builder: (context, child) => SafeArea( + child: Stack( + children: [ + Container( + height: (_offsetToArmed) * controller.value, + width: double.infinity, + child: Container( + height: 30, + width: 30, + child: SpinKitDoubleBounce( + color: MainTheme.primaryColor, + size: 20.0, ), - ], + ), ), - ); - }, + ], + ), ), - ], - ), - child: child!, - ); - } + ), + ], + ), + child: child!, + ); } diff --git a/lib/widgets/quick_menu_card.dart b/lib/widgets/quick_menu_card.dart index a333747..23413d9 100644 --- a/lib/widgets/quick_menu_card.dart +++ b/lib/widgets/quick_menu_card.dart @@ -2,23 +2,26 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:gradient_widgets/gradient_widgets.dart'; import 'package:hexcolor/hexcolor.dart'; -import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; +import 'package:mi_utem/screens/calculadora_notas_screen.dart'; +import 'package:mi_utem/screens/credencial_screen.dart'; +import 'package:mi_utem/screens/horario/horario_screen.dart'; class QuickMenuCard extends StatelessWidget { const QuickMenuCard({Key? key, required this.card}) : super(key: key); final Map card; - String? get _route { + Widget? get _route { switch (card["route"]) { case "/AsignaturasScreen": - return Routes.asignaturas; + return AsignaturasListaScreen(); case "/HorarioScreen": - return Routes.horario; + return HorarioScreen(); case "/CalculadoraNotasScreen": - return Routes.calculadoraNotas; + return CalculadoraNotasScreen(); case "/CredencialScreen": - return Routes.credencial; + return CredencialScreen(); default: return null; } @@ -57,13 +60,12 @@ class QuickMenuCard extends StatelessWidget { child: Material( color: Colors.transparent, child: InkWell( - onTap: _route != null - ? () { - Get.toNamed( - _route!, - ); - } - : null, + onTap: () { + final route = _route; + if(route != null) { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => route)); + } + }, borderRadius: BorderRadius.all(Radius.circular(15)), child: Container( padding: EdgeInsets.all(20), diff --git a/lib/widgets/sad_dialog.dart b/lib/widgets/sad_dialog.dart index cf22aee..a1ecca7 100644 --- a/lib/widgets/sad_dialog.dart +++ b/lib/widgets/sad_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/routes.dart'; +import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; final _formKey = GlobalKey(); @@ -97,11 +97,7 @@ class SadDialog extends StatelessWidget { Padding( padding: EdgeInsets.only(top: 20), child: TextButton( - onPressed: () async { - Get.toNamed( - Routes.about, - ); - }, + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => AcercaScreen())), child: Text("Quiero saber más"), ), ) diff --git a/pubspec.lock b/pubspec.lock index 2347809..63a68cd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -369,6 +369,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.2" + extended_masked_text: + dependency: "direct main" + description: + name: extended_masked_text + sha256: dba132fffa2b931e8cdd005e0509dfac359d3f98a175eca18c0ac71605247b6b + url: "https://pub.dev" + source: hosted + version: "2.3.1" fake_async: dependency: transitive description: @@ -737,6 +745,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.4.0" + functional_listener: + dependency: transitive + description: + name: functional_listener + sha256: "026d1bd4f66367f11d9ec9f1f1ddb42b89e4484b356972c76d983266cf82f33f" + url: "https://pub.dev" + source: hosted + version: "2.3.1" get: dependency: "direct main" description: @@ -745,6 +761,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.6.5" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" + url: "https://pub.dev" + source: hosted + version: "7.6.0" get_storage: dependency: "direct main" description: @@ -1646,6 +1670,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0+2" + watch_it: + dependency: "direct main" + description: + name: watch_it + sha256: d2f69c17c927d947d7faa90d6fa064fe63b73f7c49eac2d3dd6de3255eb18081 + url: "https://pub.dev" + source: hosted + version: "1.0.2" watcher: dependency: transitive description: @@ -1687,5 +1719,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=2.19.0 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 21ebfa4..43559ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,6 +76,9 @@ dependencies: screenshot: ^1.3.0 extended_image: ^7.0.2 cached_network_image: ^3.2.3 + get_it: ^7.6.0 + watch_it: ^1.0.2 + extended_masked_text: ^2.3.1 dependency_overrides: qr: ^3.0.0 From ac6a8398c2fd7d92b32b89b4553218cc2ceb4dbc Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:56:06 -0300 Subject: [PATCH 019/194] patch: se renombra http_client y reparado error en AuthClient * Se renombra http_client a auth_client * Se repara error en AuthClient --- .../{http_client.dart => http_clients/auth_client.dart} | 9 +++++---- lib/services_new/implementations/carreras_service.dart | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) rename lib/config/{http_client.dart => http_clients/auth_client.dart} (84%) diff --git a/lib/config/http_client.dart b/lib/config/http_clients/auth_client.dart similarity index 84% rename from lib/config/http_client.dart rename to lib/config/http_clients/auth_client.dart index 475e061..613699d 100644 --- a/lib/config/http_client.dart +++ b/lib/config/http_clients/auth_client.dart @@ -26,8 +26,9 @@ class AuthClient extends http.BaseClient { } } - var response = await _client.send(request); - final json = jsonDecode(await response.stream.bytesToString()); + var responseStream = await _client.send(request); + var response = await http.Response.fromStream(responseStream); + final json = jsonDecode(response.body); if(response.statusCode == 401 && json is Map && json.containsKey("codigoInterno") && json["codigoInterno"] == 12) { // Refrescar el token final _authService = di.get(); @@ -36,11 +37,11 @@ class AuthClient extends http.BaseClient { final token = user?.token; if (token != null) { request.headers['authorization'] = 'Bearer $token'; - response = await _client.send(request); + responseStream = await _client.send(request); } } - return response; + return responseStream; } @override diff --git a/lib/services_new/implementations/carreras_service.dart b/lib/services_new/implementations/carreras_service.dart index 74140b1..bc51c76 100644 --- a/lib/services_new/implementations/carreras_service.dart +++ b/lib/services_new/implementations/carreras_service.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_client.dart'; +import 'package:mi_utem/config/http_clients/auth_client.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; From 2577b1ea2380d12591948f1afe4b88f6545049df Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+im-fran@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:49:18 -0300 Subject: [PATCH 020/194] =?UTF-8?q?patch:=20se=20arregla=20sentry=20y=20li?= =?UTF-8?q?sta=20de=20notas=20*=20Ahora=20la=20lista=20de=20notas=20funcio?= =?UTF-8?q?na=20perfectamente=20*=20Se=20cambia=20el=20dsn=20de=20sentry?= =?UTF-8?q?=20para=20la=20cuenta=20del=20club=20*=20Se=20actualizan=20algu?= =?UTF-8?q?nas=20dependencias=20y=20se=20agrega=20crypto=20*=20Se=20agrega?= =?UTF-8?q?=20cach=C3=A9=20a=20las=20solicitudes=20web=20(en=20proceso)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Podfile.lock | 22 ++++---- ios/Runner.xcodeproj/project.pbxproj | 2 +- lib/config/constants.dart | 2 +- lib/config/http_clients/auth_client.dart | 52 ++++++++++++++---- lib/main.dart | 3 - lib/models/carrera.dart | 13 +++++ lib/models/pair.dart | 9 +++ .../asignatura/asignaturas_lista_screen.dart | 27 ++++++--- .../implementations/asignaturas_service.dart | 10 +--- .../implementations/auth_service.dart | 55 ++++++++++++++----- .../implementations/carreras_service.dart | 24 ++++---- .../implementations/grades_service.dart | 3 +- .../interfaces/carreras_service.dart | 6 +- .../club/acerca_club_desarrolladores.dart | 4 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 16 files changed, 161 insertions(+), 74 deletions(-) create mode 100644 lib/models/pair.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e00ae90..a89a8b4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -883,13 +883,13 @@ PODS: - PromisesObjC (2.3.1) - PromisesSwift (2.3.1): - PromisesObjC (= 2.3.1) - - Sentry/HybridSDK (8.14.2): - - SentryPrivate (= 8.14.2) + - Sentry/HybridSDK (8.20.0): + - SentryPrivate (= 8.20.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.14.2) - - SentryPrivate (8.14.2) + - Sentry/HybridSDK (= 8.20.0) + - SentryPrivate (8.20.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -923,11 +923,11 @@ DEPENDENCIES: - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) @@ -1004,7 +1004,7 @@ EXTERNAL SOURCES: package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" + :path: ".symlinks/plugins/path_provider_foundation/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" sentry_flutter: @@ -1012,7 +1012,7 @@ EXTERNAL SOURCES: share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + :path: ".symlinks/plugins/shared_preferences_foundation/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: @@ -1069,9 +1069,9 @@ SPEC CHECKSUMS: permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 - Sentry: e0ea366f95ebb68f26d6030d8c22d6b2e6d23dd0 - sentry_flutter: 9a04c51c373d76ee22167bf1e65bc468c0a91fed - SentryPrivate: 949a21fa59872427edc73b524c3ec8456761d97f + Sentry: a8d7b373b9f9868442b02a0c425192f693103cbf + sentry_flutter: 03e7660857a8cdb236e71456a7e8447b65c8a788 + SentryPrivate: 006b24af16828441f70e2ab6adf241bd0a8ad130 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fc3827b..342376c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -256,7 +256,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/lib/config/constants.dart b/lib/config/constants.dart index 11a0e8a..05a37b5 100644 --- a/lib/config/constants.dart +++ b/lib/config/constants.dart @@ -3,7 +3,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; final apiUrl = bool.fromEnvironment('dart.vm.product') ? 'https://api.exdev.cl' : (dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl'); class Constants { - static const String sentryDsn = 'https://0af59b2ad2b44f4e8c9cad4ea8d5f32e@o507661.ingest.sentry.io/5599080'; + static const String sentryDsn = 'https://c03edae5839c62f95de91c1cbabb65d7@o4506938204553216.ingest.us.sentry.io/4506938205470720'; static const String uxCamDevKey = '0y6p88obpgiug1g'; static const String uxCamProdKey = 'fxkjj5ulr7vb4yf'; static String apiUrl = bool.fromEnvironment('dart.vm.product') ? 'https://api.exdev.cl' : (dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl'); diff --git a/lib/config/http_clients/auth_client.dart b/lib/config/http_clients/auth_client.dart index 613699d..6f2e5bf 100644 --- a/lib/config/http_clients/auth_client.dart +++ b/lib/config/http_clients/auth_client.dart @@ -1,15 +1,22 @@ import 'dart:convert'; +import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:watch_it/watch_it.dart'; +final authClient = AuthClient(); + class AuthClient extends http.BaseClient { final _client = http.Client(); + final _cache = >{}; @override Future send(http.BaseRequest request) async { + logger.d("[AuthClient]: ${request.method.toUpperCase()} ${request.url}"); if(!request.headers.containsKey('user-agent')) { request.headers['user-agent'] = "App/MiUTEM"; } @@ -26,19 +33,42 @@ class AuthClient extends http.BaseClient { } } + final useCache = request.headers.containsKey('X-MiUTEM-Use-Cache'); + final cacheKey = sha1.convert(utf8.encode("${request.method}:${request.url}:${request.headers}:${request.contentLength}:${request.followRedirects}:${request.maxRedirects}:${request.persistentConnection}")).toString(); + if (useCache) { + request.headers.remove('X-MiUTEM-Use-Cache'); + final ttl = request.headers.containsKey('X-MiUTEM-Cache-TTL') ? int.parse(request.headers['X-MiUTEM-Cache-TTL']!) : 300; + request.headers.remove('X-MiUTEM-Cache-TTL'); + if (_cache.containsKey(cacheKey)) { + final pair = _cache[cacheKey]!; + if (DateTime.now().millisecondsSinceEpoch - pair.a < ttl * 1000) { // Si no ha expirado + return pair.b; + } + + _cache.remove(cacheKey); // Borrar cache si ya expiró + } + } + var responseStream = await _client.send(request); - var response = await http.Response.fromStream(responseStream); - final json = jsonDecode(response.body); - if(response.statusCode == 401 && json is Map && json.containsKey("codigoInterno") && json["codigoInterno"] == 12) { - // Refrescar el token - final _authService = di.get(); - await _authService.isLoggedIn(); - final user = await _authService.getUser(); - final token = user?.token; - if (token != null) { - request.headers['authorization'] = 'Bearer $token'; - responseStream = await _client.send(request); + if (responseStream.statusCode == 401) { + var response = await http.Response.fromStream(responseStream); + final json = jsonDecode(response.body); + if(json is Map && json.containsKey("codigoInterno") && json["codigoInterno"] == 12) { + // Refrescar el token + final _authService = di.get(); + await _authService.isLoggedIn(); + final user = await _authService.getUser(); + final token = user?.token; + if (token != null) { + return await _client.send(request); + } } + + throw Exception("Error al refrescar el token"); + } + + if (useCache) { + _cache[sha1.convert(utf8.encode(cacheKey)).toString()] = Pair(DateTime.now().millisecondsSinceEpoch, responseStream); } return responseStream; diff --git a/lib/main.dart b/lib/main.dart index 91cf003..2ed475c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,15 +10,12 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/screens/splash_screen.dart'; -import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/background_service.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/services_new/service_manager.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart' as AuthServiceNew; import 'package:mi_utem/themes/theme.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:watch_it/watch_it.dart'; import 'services/remote_config/remote_config.dart'; diff --git a/lib/models/carrera.dart b/lib/models/carrera.dart index e42b32b..9b40563 100644 --- a/lib/models/carrera.dart +++ b/lib/models/carrera.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:recase/recase.dart'; class Carrera { @@ -32,4 +34,15 @@ class Carrera { } return list; } + + toJson() => { + 'id': id, + 'nombre': nombre, + 'estado': estado, + 'codigo': codigo, + 'plan': plan, + }; + + @override + String toString() => jsonEncode(toJson()); } diff --git a/lib/models/pair.dart b/lib/models/pair.dart new file mode 100644 index 0000000..6131b49 --- /dev/null +++ b/lib/models/pair.dart @@ -0,0 +1,9 @@ +class Pair { + final A a; + final B b; + + Pair(this.a, this.b); + + @override + String toString() => "Pair($a, $b)"; +} \ No newline at end of file diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index 2992a5d..bb01396 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; @@ -27,14 +25,19 @@ class _AsignaturasListaScreenState extends State { bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; + @override - Widget build(BuildContext context) { - final _carreraSeleccionada = watchValue((CarrerasService service) => service.selectedCarrera); - if(_carreraSeleccionada?.id == null) { - logger.d("Carrera seleccionada es nula! Refrescando..."); - di.get().getCarreras(forceRefresh: true); + void initState() { + final _carrerasService = di.get(); + if (_carrerasService.selectedCarrera.value == null) { + _carrerasService.getCarreras(forceRefresh: true).then((_) => setState(() => {})); } + super.initState(); + } + + @override + Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar( title: Text("Asignaturas"), @@ -51,7 +54,15 @@ class _AsignaturasListaScreenState extends State { setState(() => {}); }, child: FutureBuilder?>( - future: _asignaturasService.getAsignaturas(_carreraSeleccionada?.id), + future: () async { + final carrerasService = di.get(); + final selectedCarrera = watchValue((CarrerasService service) => service.selectedCarrera); + if (selectedCarrera == null) { + await carrerasService.getCarreras(forceRefresh: true); + } + + return await _asignaturasService.getAsignaturas(selectedCarrera?.id); + }(), builder: (context, snapshot) { if(snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al obtener las asignaturas"; diff --git a/lib/services_new/implementations/asignaturas_service.dart b/lib/services_new/implementations/asignaturas_service.dart index 617401e..ba6dfdb 100644 --- a/lib/services_new/implementations/asignaturas_service.dart +++ b/lib/services_new/implementations/asignaturas_service.dart @@ -1,11 +1,10 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/config/http_clients/auth_client.dart'; import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; -import 'package:http/http.dart' as http; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:watch_it/watch_it.dart'; @@ -13,25 +12,22 @@ class AsignaturasServiceImplementation implements AsignaturasService { @override Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}) async { - logger.d("Carrera ID: $carreraId"); if(carreraId == null) { return null; } final user = await di.get().getUser(); - logger.d("User: ${user?.username}"); if(user == null) { return null; } - final response = await http.get(Uri.parse('$apiUrl/v1/carreras/$carreraId/asignaturas'), headers: { + final response = await authClient.get(Uri.parse('$apiUrl/v1/carreras/$carreraId/asignaturas'), headers: { 'Authorization': 'Bearer ${user.token}', 'Content-Type': 'application/json', 'User-Agent': 'App/MiUTEM', }); final json = jsonDecode(response.body); - logger.d("Response: ${jsonEncode(json)}"); if(response.statusCode != 200) { if(json is Map && json.containsKey("error")) { @@ -51,7 +47,7 @@ class AsignaturasServiceImplementation implements AsignaturasService { return null; } - final response = await http.get(Uri.parse('$apiUrl/v1/asignaturas/$asignaturaId'), headers: { + final response = await authClient.get(Uri.parse('$apiUrl/v1/asignaturas/$asignaturaId'), headers: { 'Authorization': 'Bearer ${user.token}', 'Content-Type': 'application/json', 'User-Agent': 'App/MiUTEM', diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services_new/implementations/auth_service.dart index 3fa2b49..18028fb 100644 --- a/lib/services_new/implementations/auth_service.dart +++ b/lib/services_new/implementations/auth_service.dart @@ -154,35 +154,64 @@ class AuthServiceImplementation implements AuthService { @override Future saveFCMToken() async { + final user = await this.getUser(); + if(user == null) { + return; + } + + String? fcmToken; try { - String? fcmToken = await NotificationService.fcm.requestFirebaseAppToken(); - final user = await this.getUser(); - if(user == null) { - return; - } + fcmToken = await NotificationService.fcm.requestFirebaseAppToken(); + } catch (e) { + logger.e("[AuthService#saveFCMToken]: Error al obtener FCM Token", e); + return; + } + + final usersCollection = FirebaseFirestore.instance.collection('usuarios'); - final usersCollection = FirebaseFirestore.instance.collection('usuarios'); + try { await this.deleteFCMToken(); + } catch (e) { + logger.e("[AuthService#saveFCMToken]: Error al eliminar FCM Token", e); + } + try { usersCollection.doc(user.rut?.rut.toString()).set({ 'fcmTokens': FieldValue.arrayUnion([fcmToken]), }, SetOptions(merge: true)); } catch (e) { - logger.e(e); + logger.e("[AuthService#saveFCMToken]: Error al guardar FCM Token", e); } } @override Future deleteFCMToken() async { - String? token = await NotificationService.fcm.requestFirebaseAppToken(); + String? fcmToken; + try { + fcmToken = await NotificationService.fcm.requestFirebaseAppToken(); + } catch (e) { + logger.e("[AuthService#deleteFCMToken]: Error al obtener FCM Token", e); + return; + } + final usersCollection = FirebaseFirestore.instance.collection('usuarios'); - final snapshotRepeated = await usersCollection.where("fcmTokens", arrayContains: token).get(); + QuerySnapshot> snapshotRepeated; + try { + snapshotRepeated = await usersCollection.where('fcmTokens', arrayContains: fcmToken).get(); + } catch (e) { + logger.e("[AuthService#deleteFCMToken]: Error al obtener FCM Token", e); + return; + } - for(final doc in snapshotRepeated.docs) { - doc.reference.set({ - "fcmTokens": FieldValue.arrayRemove([token]), - }, SetOptions(merge: true)); + try { + for(final doc in snapshotRepeated.docs) { + doc.reference.set({ + "fcmTokens": FieldValue.arrayRemove([fcmToken]), + }, SetOptions(merge: true)); + } + } catch (e) { + logger.e("[AuthService#deleteFCMToken]: Error al eliminar FCM Token", e); } } diff --git a/lib/services_new/implementations/carreras_service.dart b/lib/services_new/implementations/carreras_service.dart index bc51c76..9a7563c 100644 --- a/lib/services_new/implementations/carreras_service.dart +++ b/lib/services_new/implementations/carreras_service.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients/auth_client.dart'; import 'package:mi_utem/config/logger.dart'; @@ -11,12 +11,13 @@ import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; import 'package:watch_it/watch_it.dart'; -class CarrerasServiceImplementation with ChangeNotifier implements CarrerasService { +class CarrerasServiceImplementation extends ChangeNotifier implements CarrerasService { @override - ValueNotifier> get carreras => ValueNotifier([]); + ValueNotifier> carreras = ValueNotifier([]); - ValueNotifier get selectedCarrera => ValueNotifier(null); + @override + ValueNotifier selectedCarrera = ValueNotifier(null); @override Future getCarreras({bool forceRefresh = false}) async { @@ -27,10 +28,9 @@ class CarrerasServiceImplementation with ChangeNotifier implements CarrerasServi return; } - final response = await AuthClient().get(Uri.parse("$apiUrl/v1/carreras")); + final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras")); final json = jsonDecode(response.body); - logger.d("[CarrerasService#getCarreras]: Response: $json"); if(response.statusCode != 200) { logger.e("Error al obtener carreras: $json}"); if(json is Map && json.containsKey("error")) { @@ -41,23 +41,25 @@ class CarrerasServiceImplementation with ChangeNotifier implements CarrerasServi } carreras.value = Carrera.fromJsonList(json); - autoSelectCarreraActiva(carreras.value); + autoSelectCarreraActiva(); notifyListeners(); - logger.d("[CarrerasService#getCarreras]: Carreras obtenidas: $json"); } @override void changeSelectedCarrera(Carrera carrera) => selectedCarrera.value = carrera; @override - void autoSelectCarreraActiva(List carreras) { + void autoSelectCarreraActiva() { + logger.d("[CarrerasService#autoSelectCarreraActiva]: Seleccionando carrera activa... ${carreras.value.map((e) => e.toJson()).toList()}"); final estados = ["Regular", "Causal de Eliminacion"] .reversed .map((e) => e.toLowerCase()) .toList(); - carreras.sort((a,b) => estados.indexOf(b.estado!.toLowerCase()).compareTo(estados.indexOf(a.estado!.toLowerCase()))); - final carreraActiva = carreras.first; + logger.d("[CarrerasService#autoSelectCarreraActiva]: Estados: $estados"); + carreras.value.sort((a,b) => estados.indexOf(b.estado!.toLowerCase()).compareTo(estados.indexOf(a.estado!.toLowerCase()))); + logger.d("[CarrerasService#autoSelectCarreraActiva]: Carreras ordenadas: ${carreras.value.map((e) => e.toJson()).toList()}"); + final carreraActiva = carreras.value.first; AnalyticsService.setCarreraToUser(carreraActiva); changeSelectedCarrera(carreraActiva); diff --git a/lib/services_new/implementations/grades_service.dart b/lib/services_new/implementations/grades_service.dart index bfb47cb..6cf4cfb 100644 --- a/lib/services_new/implementations/grades_service.dart +++ b/lib/services_new/implementations/grades_service.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; @@ -67,7 +66,7 @@ class GradesServiceImplementation implements GradesService { return {}; } - final carrera = di.get().selectedCarrera.value; + final carrera = watchValue((CarrerasService service) => service.selectedCarrera); final carreraId = carrera?.id; if(carreraId == null) { diff --git a/lib/services_new/interfaces/carreras_service.dart b/lib/services_new/interfaces/carreras_service.dart index 64ae4a9..501225e 100644 --- a/lib/services_new/interfaces/carreras_service.dart +++ b/lib/services_new/interfaces/carreras_service.dart @@ -3,13 +3,13 @@ import 'package:mi_utem/models/carrera.dart'; abstract class CarrerasService { - ValueNotifier> get carreras; - ValueNotifier get selectedCarrera; + abstract ValueNotifier> carreras; + abstract ValueNotifier selectedCarrera; Future getCarreras({bool forceRefresh = false}); void changeSelectedCarrera(Carrera carrera); - void autoSelectCarreraActiva(List carreras); + void autoSelectCarreraActiva(); } \ No newline at end of file diff --git a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart index 6babaf7..6b21328 100644 --- a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart +++ b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart @@ -69,7 +69,7 @@ class AcercaClubDesarrolladores extends StatelessWidget { const SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.start, - children: developer['redes'].map((socialNetwork) => Container( + children: (developer['redes'] as List).map((socialNetwork) => Container( margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( shape: BoxShape.circle, @@ -81,7 +81,7 @@ class AcercaClubDesarrolladores extends StatelessWidget { onTap: () async { AnalyticsService.logEvent("acerca_person_social_tap", parameters: { - "persona": developer['nombre'], + "persona": developer['nombre'], "red": socialNetwork['nombre'], }, ); diff --git a/pubspec.lock b/pubspec.lock index 63a68cd..0b6cc01 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -282,7 +282,7 @@ packages: source: hosted version: "0.3.3+5" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab diff --git a/pubspec.yaml b/pubspec.yaml index 43559ee..196e9a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dependencies: get_it: ^7.6.0 watch_it: ^1.0.2 extended_masked_text: ^2.3.1 + crypto: ^3.0.3 dependency_overrides: qr: ^3.0.0 From 78185fbe34877a26b72e0f732d67b5a3d2bff61c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:38:41 -0300 Subject: [PATCH 021/194] =?UTF-8?q?patch:=20algunos=20arreglos=20*=20Se=20?= =?UTF-8?q?renombra=202=20archivos=20*=20se=20agrega=20configuraci=C3=B3n?= =?UTF-8?q?=20de=20printer=20a=20logger=20*=20se=20arregla=20log=20de=20er?= =?UTF-8?q?ror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/config/logger.dart | 4 +++- lib/screens/main_screen.dart | 2 +- lib/services_new/implementations/auth_service.dart | 2 +- .../{NoticiaCardWidget.dart => noticia_card_widget.dart} | 0 ...iciasCarruselWidget.dart => noticias_carrusel_widget.dart} | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) rename lib/widgets/noticias/{NoticiaCardWidget.dart => noticia_card_widget.dart} (100%) rename lib/widgets/noticias/{NoticiasCarruselWidget.dart => noticias_carrusel_widget.dart} (97%) diff --git a/lib/config/logger.dart b/lib/config/logger.dart index 55058ea..13ad4a3 100644 --- a/lib/config/logger.dart +++ b/lib/config/logger.dart @@ -1,3 +1,5 @@ import 'package:logger/logger.dart'; -final logger = Logger(); +final logger = Logger( + printer: PrettyPrinter(), +); diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 915446b..5526f6e 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -14,7 +14,7 @@ import "package:mi_utem/services_new/interfaces/grades_service.dart"; import 'package:mi_utem/widgets/banners_section.dart'; import "package:mi_utem/widgets/custom_app_bar.dart"; import "package:mi_utem/widgets/custom_drawer.dart"; -import "package:mi_utem/widgets/noticias/NoticiasCarruselWidget.dart"; +import "package:mi_utem/widgets/noticias/noticias_carrusel_widget.dart"; import "package:mi_utem/widgets/permisos_section.dart"; import "package:mi_utem/widgets/quick_menu_section.dart"; import "package:watch_it/watch_it.dart"; diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services_new/implementations/auth_service.dart index 18028fb..4bf707c 100644 --- a/lib/services_new/implementations/auth_service.dart +++ b/lib/services_new/implementations/auth_service.dart @@ -200,7 +200,7 @@ class AuthServiceImplementation implements AuthService { try { snapshotRepeated = await usersCollection.where('fcmTokens', arrayContains: fcmToken).get(); } catch (e) { - logger.e("[AuthService#deleteFCMToken]: Error al obtener FCM Token", e); + logger.e("[AuthService#deleteFCMToken]: Error al obtener usuarios con FCM Token", e); return; } diff --git a/lib/widgets/noticias/NoticiaCardWidget.dart b/lib/widgets/noticias/noticia_card_widget.dart similarity index 100% rename from lib/widgets/noticias/NoticiaCardWidget.dart rename to lib/widgets/noticias/noticia_card_widget.dart diff --git a/lib/widgets/noticias/NoticiasCarruselWidget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart similarity index 97% rename from lib/widgets/noticias/NoticiasCarruselWidget.dart rename to lib/widgets/noticias/noticias_carrusel_widget.dart index 8d0e89c..bb067c9 100644 --- a/lib/widgets/noticias/NoticiasCarruselWidget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -6,7 +6,7 @@ import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; -import 'package:mi_utem/widgets/noticias/NoticiaCardWidget.dart'; +import 'package:mi_utem/widgets/noticias/noticia_card_widget.dart'; import 'package:watch_it/watch_it.dart'; class NoticiasCarruselWidget extends StatefulWidget { From 4280b014e175c57b4575373695ffbfee0bdaccca Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:39:06 -0300 Subject: [PATCH 022/194] =?UTF-8?q?patch:=20mejora=20a=20los=20clientes=20?= =?UTF-8?q?web=20*=20Se=20ha=20movido=20a=20http=5Fclients=20*=20Se=20ha?= =?UTF-8?q?=20creado=20interceptores=20en=20lugar=20de=20un=20cliente=20nu?= =?UTF-8?q?evo=20*=20Trabajando=20en=20CachedClient=20*=20Se=20ha=20agrega?= =?UTF-8?q?do=20revisi=C3=B3n=20de=20conexi=C3=B3n=20a=20la=20red=20en=20s?= =?UTF-8?q?plash=20screen.=20*=20Se=20agreg=C3=B3=20icono=20de=20carga=20e?= =?UTF-8?q?n=20splash=20*=20Se=20movi=C3=B3=20banners=20a=20un=20field=20c?= =?UTF-8?q?on=20estado=20en=20main=5Fscreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/config/http_clients.dart | 107 ++++++++++++++++++ lib/config/http_clients/auth_client.dart | 83 -------------- .../asignatura/asignaturas_lista_screen.dart | 82 +++++++------- lib/screens/main_screen.dart | 48 +++++--- lib/screens/splash_screen.dart | 28 ++++- lib/services/docentes_service.dart | 6 +- .../implementations/asignaturas_service.dart | 2 +- .../implementations/auth_service.dart | 20 ++-- .../implementations/carreras_service.dart | 2 +- .../implementations/grades_service.dart | 4 +- .../implementations/horario_service.dart | 4 +- .../implementations/noticias_service.dart | 6 +- .../implementations/qr_pass_service.dart | 6 +- 13 files changed, 228 insertions(+), 170 deletions(-) create mode 100644 lib/config/http_clients.dart delete mode 100644 lib/config/http_clients/auth_client.dart diff --git a/lib/config/http_clients.dart b/lib/config/http_clients.dart new file mode 100644 index 0000000..afc8088 --- /dev/null +++ b/lib/config/http_clients.dart @@ -0,0 +1,107 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:http/http.dart' as http; +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/models/pair.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:watch_it/watch_it.dart'; + +final httpClient = http.Client(); +final authClient = InterceptedClient.build( + interceptors: [ + AuthInterceptor(), + ], + retryPolicy: ExpiredTokenRetryPolicy(), +); + +class AuthInterceptor implements InterceptorContract { + + @override + Future interceptRequest({required RequestData data}) async { + logger.d("[AuthInterceptor]: ${data.method.name.toUpperCase()} ${data.url}"); + if(!data.headers.containsKey('user-agent')) { + data.headers['user-agent'] = "App/MiUTEM"; + } + + if(!data.headers.containsKey('content-type')) { + data.headers['content-type'] = "application/json"; + } + + if (!data.headers.containsKey('authorization') && !data.url.contains("/v1/auth/login") && !data.url.contains("/v1/auth/refresh")) { // No enviar token si es login o refresh + final user = await di.get().getUser(); + final token = user?.token; + if (token != null) { + logger.d("[AuthInterceptor]: Agregado token de autorización al request"); + data.headers['authorization'] = 'Bearer $token'; + } + } + + return data; + } + + @override + Future interceptResponse({required ResponseData data}) async => data; +} + +class ExpiredTokenRetryPolicy extends RetryPolicy { + + @override + Future shouldAttemptRetryOnResponse(ResponseData response) async { + if(response.statusCode == 401) { + logger.d("[ExpiredTokenRetryPolicy]: Se recibió un 401, refrescando token..."); + final _authService = di.get(); + final currentUser = await _authService.getUser(); + await _authService.isLoggedIn(); + final user = await _authService.getUser(); + final token = user?.token; + if (token != null && currentUser?.token != token) { + return true; + } + } + + return false; + } +} + +class CachedClient extends http.BaseClient { + + final _client = http.Client(); + final _cache = >{}; + + @override + Future send(http.BaseRequest request) async { + logger.d("[HttpClient]: ${request.method.toUpperCase()} ${request.url}"); + final useCache = request.headers.containsKey('X-MiUTEM-Use-Cache'); + final cacheKey = sha1.convert(utf8.encode("${request.method}:${request.url}:${request.headers}:${request.contentLength}:${request.followRedirects}:${request.maxRedirects}:${request.persistentConnection}")).toString(); + if (useCache) { + logger.d("[HttpClient]: Usando cache para ${request.method.toUpperCase()} ${request.url}"); + request.headers.remove('X-MiUTEM-Use-Cache'); + final ttl = request.headers.containsKey('X-MiUTEM-Cache-TTL') ? int.parse(request.headers['X-MiUTEM-Cache-TTL']!) : 300; // 5 minutos por defecto (en segundos) + request.headers.remove('X-MiUTEM-Cache-TTL'); + if (_cache.containsKey(cacheKey)) { + final pair = _cache[cacheKey]!; + if ((DateTime.now().millisecondsSinceEpoch - pair.a) < (ttl * 1000)) { // Si no ha expirado + return pair.b; + } + + _cache.remove(cacheKey); // Borrar cache si ya expiró + } + } + + final responseStream = await _client.send(request); + if (useCache && responseStream.statusCode == 200) { + _cache[cacheKey] = Pair(DateTime.now().millisecondsSinceEpoch, responseStream); + } + + return responseStream; + } + + @override + void close() { + _client.close(); + super.close(); + } + +} diff --git a/lib/config/http_clients/auth_client.dart b/lib/config/http_clients/auth_client.dart deleted file mode 100644 index 6f2e5bf..0000000 --- a/lib/config/http_clients/auth_client.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; -import 'package:http/http.dart' as http; -import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/models/pair.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:watch_it/watch_it.dart'; - -final authClient = AuthClient(); - -class AuthClient extends http.BaseClient { - - final _client = http.Client(); - final _cache = >{}; - - @override - Future send(http.BaseRequest request) async { - logger.d("[AuthClient]: ${request.method.toUpperCase()} ${request.url}"); - if(!request.headers.containsKey('user-agent')) { - request.headers['user-agent'] = "App/MiUTEM"; - } - - if(!request.headers.containsKey('content-type')) { - request.headers['content-type'] = "application/json"; - } - - if (!request.headers.containsKey('authorization')) { - final user = await di.get().getUser(); - final token = user?.token; - if (token != null) { - request.headers['authorization'] = 'Bearer $token'; - } - } - - final useCache = request.headers.containsKey('X-MiUTEM-Use-Cache'); - final cacheKey = sha1.convert(utf8.encode("${request.method}:${request.url}:${request.headers}:${request.contentLength}:${request.followRedirects}:${request.maxRedirects}:${request.persistentConnection}")).toString(); - if (useCache) { - request.headers.remove('X-MiUTEM-Use-Cache'); - final ttl = request.headers.containsKey('X-MiUTEM-Cache-TTL') ? int.parse(request.headers['X-MiUTEM-Cache-TTL']!) : 300; - request.headers.remove('X-MiUTEM-Cache-TTL'); - if (_cache.containsKey(cacheKey)) { - final pair = _cache[cacheKey]!; - if (DateTime.now().millisecondsSinceEpoch - pair.a < ttl * 1000) { // Si no ha expirado - return pair.b; - } - - _cache.remove(cacheKey); // Borrar cache si ya expiró - } - } - - var responseStream = await _client.send(request); - if (responseStream.statusCode == 401) { - var response = await http.Response.fromStream(responseStream); - final json = jsonDecode(response.body); - if(json is Map && json.containsKey("codigoInterno") && json["codigoInterno"] == 12) { - // Refrescar el token - final _authService = di.get(); - await _authService.isLoggedIn(); - final user = await _authService.getUser(); - final token = user?.token; - if (token != null) { - return await _client.send(request); - } - } - - throw Exception("Error al refrescar el token"); - } - - if (useCache) { - _cache[sha1.convert(utf8.encode(cacheKey)).toString()] = Pair(DateTime.now().millisecondsSinceEpoch, responseStream); - } - - return responseStream; - } - - @override - void close() { - _client.close(); - super.close(); - } - -} \ No newline at end of file diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index bb01396..f2fc66b 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -37,53 +37,51 @@ class _AsignaturasListaScreenState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: Text("Asignaturas"), - actions: _mostrarCalculadora ? [ - IconButton( - icon: Icon(Mdi.calculator), - tooltip: "Calculadora", - onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())), - ), - ] : [], - ), - body: PullToRefresh( - onRefresh: () async { - setState(() => {}); - }, - child: FutureBuilder?>( - future: () async { - final carrerasService = di.get(); - final selectedCarrera = watchValue((CarrerasService service) => service.selectedCarrera); - if (selectedCarrera == null) { - await carrerasService.getCarreras(forceRefresh: true); - } + Widget build(BuildContext context) => Scaffold( + appBar: CustomAppBar( + title: Text("Asignaturas"), + actions: _mostrarCalculadora ? [ + IconButton( + icon: Icon(Mdi.calculator), + tooltip: "Calculadora", + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())), + ), + ] : [], + ), + body: PullToRefresh( + onRefresh: () async { + setState(() => {}); + }, + child: FutureBuilder?>( + future: () async { + final carrerasService = di.get(); + final selectedCarrera = watchValue((CarrerasService service) => service.selectedCarrera); + if (selectedCarrera == null) { + await carrerasService.getCarreras(forceRefresh: true); + } - return await _asignaturasService.getAsignaturas(selectedCarrera?.id); - }(), - builder: (context, snapshot) { - if(snapshot.hasError) { - final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al obtener las asignaturas"; - return _errorWidget(error); - } + return await _asignaturasService.getAsignaturas(selectedCarrera?.id); + }(), + builder: (context, snapshot) { + if(snapshot.hasError) { + final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al obtener las asignaturas"; + return _errorWidget(error); + } - if(snapshot.connectionState == ConnectionState.waiting) { - return _loadingWidget(); - } + if(snapshot.connectionState == ConnectionState.waiting) { + return _loadingWidget(); + } - final asignaturas = snapshot.data ?? []; - if(asignaturas.isEmpty) { - return _errorWidget("No encontramos asignaturas. Por favor intenta más tarde."); - } + final asignaturas = snapshot.data ?? []; + if(asignaturas.isEmpty) { + return _errorWidget("No encontramos asignaturas. Por favor intenta más tarde."); + } - return ListaAsignaturas(asignaturas: asignaturas); - }, - ), + return ListaAsignaturas(asignaturas: asignaturas); + }, ), - ); - } + ), + ); Widget _loadingWidget() => Padding( padding: const EdgeInsets.all(20), diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 5526f6e..738c865 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -6,16 +6,19 @@ import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_markdown/flutter_markdown.dart"; import "package:get/get.dart"; +import "package:mi_utem/config/logger.dart"; import "package:mi_utem/models/user/user.dart"; import "package:mi_utem/services/remote_config/remote_config.dart"; import "package:mi_utem/services/review_service.dart"; import "package:mi_utem/services_new/interfaces/auth_service.dart"; import "package:mi_utem/services_new/interfaces/grades_service.dart"; +import "package:mi_utem/widgets/banner.dart"; import 'package:mi_utem/widgets/banners_section.dart'; import "package:mi_utem/widgets/custom_app_bar.dart"; import "package:mi_utem/widgets/custom_drawer.dart"; import "package:mi_utem/widgets/noticias/noticias_carrusel_widget.dart"; import "package:mi_utem/widgets/permisos_section.dart"; +import "package:mi_utem/widgets/pull_to_refresh.dart"; import "package:mi_utem/widgets/quick_menu_section.dart"; import "package:watch_it/watch_it.dart"; @@ -30,6 +33,7 @@ class MainScreen extends StatefulWidget { class _MainScreenState extends State { + List _banners = const []; User? _user; final _authService = di.get(); @@ -50,29 +54,37 @@ class _MainScreenState extends State { ReviewService.addScreen("MainScreen"); ReviewService.checkAndRequestReview(); + loadData(); + _authService.getUser().then((user) => setState(() => _user = user)); } + Future loadData() async { + setState(() { + _banners = RemoteConfigService.banners; + logger.d("Banners: $_banners"); + }); + } + String get _greetingText { List texts = jsonDecode(RemoteConfigService.greetings); return texts[Random().nextInt(texts.length)].replaceAll("%name", _user?.primerNombre ?? "N/N"); } @override - Widget build(BuildContext context) { - final banners = RemoteConfigService.banners; - - return Scaffold( - appBar: CustomAppBar(title: Text("Inicio")), - drawer: CustomDrawer(), - floatingActionButton: kDebugMode ? FloatingActionButton( - onPressed: () => di.get().lookForGradeUpdates(), - tooltip: "Probar notificaciones de notas", - child: Icon(Icons.notifications, - color: Colors.white, - ), - ) : null, - body: SingleChildScrollView( + Widget build(BuildContext context) => Scaffold( + appBar: CustomAppBar(title: Text("Inicio")), + drawer: CustomDrawer(), + floatingActionButton: kDebugMode ? FloatingActionButton( + onPressed: () => di.get().lookForGradeUpdates(), + tooltip: "Probar notificaciones de notas", + child: Icon(Icons.notifications, + color: Colors.white, + ), + ) : null, + body: PullToRefresh( + onRefresh: loadData, + child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -93,14 +105,14 @@ class _MainScreenState extends State { const SizedBox(height: 20), QuickMenuSection(), const SizedBox(height: 20), - if (banners.isNotEmpty) ...[ - BannersSection(banners: banners), + if (_banners.isNotEmpty) ...[ + BannersSection(banners: _banners), const SizedBox(height: 20), ], NoticiasCarruselWidget(), ], ), ), - ); - } + ), + ); } diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 4e042c3..f032c38 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -1,13 +1,17 @@ +import 'dart:convert'; + import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; +import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:watch_it/watch_it.dart'; @@ -63,6 +67,27 @@ class _SplashScreenState extends State { fit: BoxFit.contain, animation: "default", callback: (String val) async { + Get.dialog(LoadingDialog(), barrierDismissible: false); + // Revisar si tenemos conexión a internet + try { + final response = await httpClient.get(Uri.parse(apiUrl)); + final json = jsonDecode(response.body); + if(!(json is Map && json["funcionando"] == true)) { + Get.back(); + Get.snackbar("Error", "No se pudo conectar al servidor. Revisa tu conexión a internet.", + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + } catch (e) { + Get.back(); + Get.snackbar("Error", "No se pudo conectar al servidor. Revisa tu conexión a internet.", + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } await NotificationService.requestUserPermissionIfNecessary(); final isLoggedIn = await _authService.isLoggedIn(); if(!isLoggedIn) { @@ -73,6 +98,7 @@ class _SplashScreenState extends State { AnalyticsService.setUser(user); } } + Get.back(); Navigator.pushReplacement(context, MaterialPageRoute(builder: (ctx) => isLoggedIn ? MainScreen() : LoginScreen())); }, ), diff --git a/lib/services/docentes_service.dart b/lib/services/docentes_service.dart index 2a705a9..7efbd74 100644 --- a/lib/services/docentes_service.dart +++ b/lib/services/docentes_service.dart @@ -1,8 +1,6 @@ import 'package:dio/dio.dart'; -import 'package:http/http.dart' as http; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/user/user.dart'; - -import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/utils/dio_docente_client.dart'; class DocentesService { @@ -16,7 +14,7 @@ class DocentesService { for (final formato in formatos) { String actualImageUrl = "$baseUrl${user.rut?.rut}$formato"; - final imageResponse = await http.head(Uri.parse(actualImageUrl)); + final imageResponse = await authClient.head(Uri.parse(actualImageUrl)); if (imageResponse.statusCode == 200) { imageUrl = actualImageUrl; diff --git a/lib/services_new/implementations/asignaturas_service.dart b/lib/services_new/implementations/asignaturas_service.dart index ba6dfdb..8fe77df 100644 --- a/lib/services_new/implementations/asignaturas_service.dart +++ b/lib/services_new/implementations/asignaturas_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients/auth_client.dart'; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services_new/implementations/auth_service.dart index 4bf707c..90230d1 100644 --- a/lib/services_new/implementations/auth_service.dart +++ b/lib/services_new/implementations/auth_service.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; @@ -29,14 +29,13 @@ class AuthServiceImplementation implements AuthService { return false; } - // Get stored user final user = await getUser(); if(user == null) { return false; } try { - final response = await http.post(Uri.parse("$apiUrl/v1/auth/refresh"), + final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth/refresh"), body: credential.toString(), headers: { 'Content-Type': 'application/json', @@ -52,7 +51,7 @@ class AuthServiceImplementation implements AuthService { await secureStorage.write(key: "last_login", value: "${DateTime.now().millisecondsSinceEpoch}"); return true; } catch (e) { - logger.e("[AuthService#isLoggedIn]: $e"); + logger.e("[AuthService#isLoggedIn]: Error al refrescar token", e); } return false; @@ -65,7 +64,7 @@ class AuthServiceImplementation implements AuthService { return; } - final response = await http.post(Uri.parse("$apiUrl/v1/auth"), + final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth"), body: credential.toString(), headers: { 'Content-Type': 'application/json', @@ -73,17 +72,16 @@ class AuthServiceImplementation implements AuthService { }, ); - final json = jsonDecode(response.body) as Map; - + final json = jsonDecode(response.body); if(response.statusCode != 200) { - if(json.containsKey("error")) { + if(json is Map && json.containsKey("error")) { throw CustomException.fromJson(json); } throw CustomException.custom(response.reasonPhrase); } - final user = User.fromJson(json); + final user = User.fromJson(json as Map); await setUser(user); } @@ -100,7 +98,7 @@ class AuthServiceImplementation implements AuthService { @override Future getUser() async { final data = await secureStorage.read(key: "user"); - if(data == null) { + if(data == null || data == "null") { return null; } @@ -127,7 +125,7 @@ class AuthServiceImplementation implements AuthService { return null; } - final response = await http.put(Uri.parse("$apiUrl/v1/usuarios/foto"), + final response = await httpClient.put(Uri.parse("$apiUrl/v1/usuarios/foto"), body: jsonEncode({"imagen": image}), headers: { 'Content-Type': 'application/json', diff --git a/lib/services_new/implementations/carreras_service.dart b/lib/services_new/implementations/carreras_service.dart index 9a7563c..344a4f9 100644 --- a/lib/services_new/implementations/carreras_service.dart +++ b/lib/services_new/implementations/carreras_service.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients/auth_client.dart'; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; diff --git a/lib/services_new/implementations/grades_service.dart b/lib/services_new/implementations/grades_service.dart index 6cf4cfb..9724b8d 100644 --- a/lib/services_new/implementations/grades_service.dart +++ b/lib/services_new/implementations/grades_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; @@ -24,7 +24,7 @@ class GradesServiceImplementation implements GradesService { return null; } - final response = await http.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas"), + final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas"), headers: { 'Authorization': 'Bearer ${user.token}', 'Content-Type': 'application/json', diff --git a/lib/services_new/implementations/horario_service.dart b/lib/services_new/implementations/horario_service.dart index 4d8ed5d..0f3d468 100644 --- a/lib/services_new/implementations/horario_service.dart +++ b/lib/services_new/implementations/horario_service.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/horario_service.dart'; -import 'package:http/http.dart' as http; import 'package:watch_it/watch_it.dart'; class HorarioServiceImplementation implements HorarioService { @@ -16,7 +16,7 @@ class HorarioServiceImplementation implements HorarioService { return null; } - final response = await http.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/horarios"), + final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/horarios"), headers: { 'Authorization': 'Bearer ${user.token}', 'Content-Type': 'application/json', diff --git a/lib/services_new/implementations/noticias_service.dart b/lib/services_new/implementations/noticias_service.dart index 7e4dced..4b1c550 100644 --- a/lib/services_new/implementations/noticias_service.dart +++ b/lib/services_new/implementations/noticias_service.dart @@ -1,16 +1,18 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; -import 'package:http/http.dart' as http; class NoticiasServiceImplementation implements NoticiasService { @override Future?> getNoticias() async { - final response = await http.get(Uri.parse("$apiUrl/v1/noticias")); + final response = await httpClient.get(Uri.parse("$apiUrl/v1/noticias"), headers: { + 'X-MiUTEM-Use-Cache': 'true', + }); final json = jsonDecode(response.body); diff --git a/lib/services_new/implementations/qr_pass_service.dart b/lib/services_new/implementations/qr_pass_service.dart index daaea14..c76b608 100644 --- a/lib/services_new/implementations/qr_pass_service.dart +++ b/lib/services_new/implementations/qr_pass_service.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_covid.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; -import 'package:http/http.dart' as http; import 'package:watch_it/watch_it.dart'; class QRPassServiceImplementation extends QRPassService { @@ -19,7 +19,7 @@ class QRPassServiceImplementation extends QRPassService { return null; } - final response = await http.post(Uri.parse("$apiUrl/v1/permisos/$id"), + final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos/$id"), headers: { 'Authorization': 'Bearer ${user.token}', 'Content-Type': 'application/json', @@ -47,7 +47,7 @@ class QRPassServiceImplementation extends QRPassService { return null; } - final response = await http.post(Uri.parse("$apiUrl/v1/permisos"), + final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos"), headers: { 'Authorization': 'Bearer ${user.token}', 'Content-Type': 'application/json', From 2ce3543d10393abbc719481af6099b09f259e888 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+im-fran@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:17:52 -0300 Subject: [PATCH 023/194] =?UTF-8?q?feat:=20se=20migra=20a=20snackbars=20na?= =?UTF-8?q?tivos=20y=20otras=20cosas=20*=20Se=20ha=20migrado=20a=20los=20s?= =?UTF-8?q?nackbars=20nativos=20(en=20progreso)=20*=20Se=20finaliz=C3=B3?= =?UTF-8?q?=20el=20horario=20"nuevo",=20ahora=20carga=20m=C3=A1s=20r=C3=A1?= =?UTF-8?q?pido,=20tiene=20un=20bot=C3=B3n=20para=20forzar=20la=20actualiz?= =?UTF-8?q?aci=C3=B3n=20del=20horario=20y=20se=20muestra=20como=20carga=20?= =?UTF-8?q?cuando=20se=20comparte.=20*=20Se=20implementa=20un=20cach=C3=A9?= =?UTF-8?q?=20para=20agilizar=20la=20carga=20del=20horario.=20*=20Se=20fin?= =?UTF-8?q?aliz=C3=B3=20la=20lista=20de=20asignaturas,=20ahora=20tarda=20m?= =?UTF-8?q?enos=20en=20cargar.=20*=20Se=20mejora=20el=20tiempo=20de=20carg?= =?UTF-8?q?a=20en=20splash=20screen.=20*=20Trabajando=20en=20arreglar=20la?= =?UTF-8?q?=20calculadora=CF=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/config/http_clients.dart | 59 +++- lib/controllers/horario_controller.dart | 301 ------------------ lib/controllers/qr_passes_controller.dart | 42 --- lib/helpers/snackbars.dart | 13 - lib/models/evaluacion.dart | 51 +-- lib/models/horario.dart | 7 +- .../asignatura/asignatura_detalle_screen.dart | 4 +- .../asignatura/asignaturas_lista_screen.dart | 11 - lib/screens/calculadora_notas_screen.dart | 46 ++- lib/screens/horario/horario_screen.dart | 133 ++++---- .../widgets/horario_blocks_content.dart | 8 +- .../horario/widgets/horario_days_header.dart | 8 +- .../horario/widgets/horario_indicator.dart | 67 ++-- .../widgets/horario_main_scroller.dart | 294 ++++++++--------- .../widgets/horario_periods_header.dart | 8 +- lib/screens/splash_screen.dart | 19 +- lib/services/review_service.dart | 4 +- .../implementations/auth_service.dart | 18 +- .../calculator_controller.dart} | 87 ++--- .../controllers/horario_controller.dart | 293 +++++++++++++++++ .../implementations/horario_service.dart | 2 + .../calculator_controller.dart} | 14 +- .../controllers/horario_controller.dart | 52 +++ lib/services_new/service_manager.dart | 11 +- lib/themes/theme.dart | 194 +++++------ lib/widgets/acerca/dialog/acerca_dialog.dart | 6 +- lib/widgets/bloque_ramo_card.dart | 33 +- .../calculadora_notas/EditarNotasWidget.dart | 44 --- .../NotasCalculadoraWidget.dart | 40 --- ...sWidget.dart => display_notas_widget.dart} | 8 +- .../editar_notas_widget.dart | 56 ++++ ...idget.dart => modo_simulacion_widget.dart} | 0 ...t.dart => nota_examen_display_widget.dart} | 19 +- ...et.dart => nota_final_display_widget.dart} | 4 +- ... => nota_presentacion_display_widget.dart} | 4 +- .../notas_calculadora_widget.dart | 42 +++ lib/widgets/dialogs/monkey_error_dialog.dart | 36 +-- lib/widgets/error_dialog.dart | 72 +++-- lib/widgets/loading_dialog.dart | 9 +- lib/widgets/login_screen/login_button.dart | 56 ++-- lib/widgets/nota_list_item.dart | 6 +- lib/widgets/permisos_section.dart | 4 +- lib/widgets/snackbar.dart | 36 +++ 43 files changed, 1148 insertions(+), 1073 deletions(-) delete mode 100644 lib/controllers/horario_controller.dart delete mode 100644 lib/controllers/qr_passes_controller.dart delete mode 100644 lib/helpers/snackbars.dart rename lib/services_new/implementations/{calculator_service.dart => controllers/calculator_controller.dart} (73%) create mode 100644 lib/services_new/implementations/controllers/horario_controller.dart rename lib/services_new/interfaces/{calculator_service.dart => controllers/calculator_controller.dart} (82%) create mode 100644 lib/services_new/interfaces/controllers/horario_controller.dart delete mode 100644 lib/widgets/calculadora_notas/EditarNotasWidget.dart delete mode 100644 lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart rename lib/widgets/calculadora_notas/{DisplayNotasWidget.dart => display_notas_widget.dart} (78%) create mode 100644 lib/widgets/calculadora_notas/editar_notas_widget.dart rename lib/widgets/calculadora_notas/{ModoSimulacionWidget.dart => modo_simulacion_widget.dart} (100%) rename lib/widgets/calculadora_notas/{NotaExamenDisplayWidget.dart => nota_examen_display_widget.dart} (57%) rename lib/widgets/calculadora_notas/{NotaFinalDisplayWidget.dart => nota_final_display_widget.dart} (68%) rename lib/widgets/calculadora_notas/{NotaPresentacionDisplayWidget.dart => nota_presentacion_display_widget.dart} (88%) create mode 100644 lib/widgets/calculadora_notas/notas_calculadora_widget.dart create mode 100644 lib/widgets/snackbar.dart diff --git a/lib/config/http_clients.dart b/lib/config/http_clients.dart index afc8088..8b5f52f 100644 --- a/lib/config/http_clients.dart +++ b/lib/config/http_clients.dart @@ -8,9 +8,14 @@ import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:watch_it/watch_it.dart'; -final httpClient = http.Client(); +final httpClient = InterceptedClient.build( + interceptors: [ + LoggerInterceptor(), + ], +); final authClient = InterceptedClient.build( interceptors: [ + LoggerInterceptor(), AuthInterceptor(), ], retryPolicy: ExpiredTokenRetryPolicy(), @@ -20,7 +25,6 @@ class AuthInterceptor implements InterceptorContract { @override Future interceptRequest({required RequestData data}) async { - logger.d("[AuthInterceptor]: ${data.method.name.toUpperCase()} ${data.url}"); if(!data.headers.containsKey('user-agent')) { data.headers['user-agent'] = "App/MiUTEM"; } @@ -47,21 +51,50 @@ class AuthInterceptor implements InterceptorContract { class ExpiredTokenRetryPolicy extends RetryPolicy { + @override + int get maxRetryAttempts => 3; + @override Future shouldAttemptRetryOnResponse(ResponseData response) async { - if(response.statusCode == 401) { - logger.d("[ExpiredTokenRetryPolicy]: Se recibió un 401, refrescando token..."); - final _authService = di.get(); - final currentUser = await _authService.getUser(); - await _authService.isLoggedIn(); - final user = await _authService.getUser(); - final token = user?.token; - if (token != null && currentUser?.token != token) { - return true; - } + if(response.statusCode != 401) { + return false; + } + + logger.d("[ExpiredTokenRetryPolicy]: ${response.request?.method.name.toUpperCase()} ${response.request?.url} Recibió un 401, refrescando token..."); + final _authService = di.get(); + final currentUser = await _authService.getUser(); + await _authService.isLoggedIn(); + final user = await _authService.getUser(); + final token = user?.token; + if(token == null) { + logger.d("[ExpiredTokenRetryPolicy]: No se pudo refrescar el token, redirigiendo a login"); + return false; } - return false; + return currentUser?.token == token; + } +} + +class LoggerInterceptor implements InterceptorContract { + + final _times = {}; + + @override + Future interceptRequest({required RequestData data}) async { + logger.d("[LoggerInterceptor#request]: ${data.method.name.toUpperCase()} ${data.url}"); + _times["${data.method}:${data.url}"] = DateTime.now(); + return data; + } + + @override + Future interceptResponse({required ResponseData data}) async { + String? diff; + final time = _times["${data.method}:${data.url}"]; + if(time != null) { + diff = " (${DateTime.now().difference(time).inMilliseconds}ms)"; + } + logger.d("[LoggerInterceptor#response]: ${data.statusCode} ${data.url}$diff"); + return data; } } diff --git a/lib/controllers/horario_controller.dart b/lib/controllers/horario_controller.dart deleted file mode 100644 index ad66efb..0000000 --- a/lib/controllers/horario_controller.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; -import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; -import 'package:mi_utem/services_new/interfaces/horario_service.dart'; -import 'package:vector_math/vector_math_64.dart' as vector; -import 'package:watch_it/watch_it.dart'; - -class HorarioController extends GetxController { - static const daysCount = 6; - static const periodsCount = 9; - - static const startTime = "7:55"; - static const periodDuration = Duration(minutes: 90); - static const periodGap = Duration(minutes: 5); - - static final GetStorage _box = GetStorage(); - static final List _randomColors = Colors.primaries.toList()..shuffle(); - - final horario = Rxn(null); - final loadingHorario = false.obs; - final usedColors = []; - final zoom = 0.5.obs; - final indicatorIsOpen = false.obs; - final isCenteredInCurrentPeriodAndDay = false.obs; - - final blockContentController = TransformationController(); - final daysHeaderController = TransformationController(); - final periodHeaderController = TransformationController(); - final cornerController = TransformationController(); - - static HorarioController get to => Get.find(); - - List get unusedColors { - List availableColors = [..._randomColors]; - availableColors.retainWhere((Color color) => !usedColors.contains(color)); - if (availableColors.length == 0) { - return [..._randomColors]; - } - return availableColors; - } - - DateTime _now = DateTime.now(); - - double get minutesFromStart { - final now = _now; - final startTimeParts = startTime.split(":"); - final startDateTime = DateTime( - now.year, - now.month, - now.day, - int.parse(startTimeParts[0]), - int.parse(startTimeParts[1]), - ); - return now.difference(startDateTime).inMinutes.toDouble(); - } - - int? get indexOfCurrentDayStartingAtMonday { - final now = _now; - final day = now.weekday; - return day > daysCount ? null : day - 1; - } - - int? get indexOfCurrentPeriod { - final minutes = minutesFromStart; - final periodBlockDuration = - periodDuration.inMinutes + (periodGap.inMinutes * 2); - - final period = minutes ~/ periodBlockDuration; - final minutesModule = minutes % periodBlockDuration; - - if (minutesModule >= periodGap.inMinutes && - minutesModule <= (periodBlockDuration - periodGap.inMinutes)) { - return period; - } - - return null; - } - - @override - onInit() { - final selectedCarrera = watchValue((CarrerasService service) => service.selectedCarrera); - if (selectedCarrera != null) { - getHorarioData(selectedCarrera); - } - - _init(); - super.onInit(); - } - - void _init() { - zoom.value = RemoteConfigService.horarioZoom; - - _initControllers(); - - _setScrollControllerListeners(); - } - - Future getHorarioData(Carrera? carrera) async { - log("getHorarioData"); - final carreraId = carrera?.id; - if (carreraId == null) { - return; - } - loadingHorario.value = true; - horario.value = await di.get().getHorario(carreraId); - _setRandomColorsByHorario(); - loadingHorario.value = false; - } - - void _setRandomColorsByHorario() { - if (horario.value?.horario != null) { - for (var dia in horario.value!.horario!) { - for (var bloque in dia) { - if (bloque.asignatura != null) { - addAsignaturaAndSetColor(bloque.asignatura!); - } - } - } - } - } - - void _onChangeAnyController() { - indicatorIsOpen.value = false; - isCenteredInCurrentPeriodAndDay.value = false; - update(); - } - - void _setScrollControllerListeners() { - blockContentController.addListener(() { - final xPosition = blockContentController.value.getTranslation().x; - final yPosition = blockContentController.value.getTranslation().y; - final currentZoom = blockContentController.value.getMaxScaleOnAxis(); - - daysHeaderController.value.setTranslationRaw(xPosition, 0, 0); - periodHeaderController.value.setTranslationRaw(0, yPosition, 0); - - daysHeaderController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - periodHeaderController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - cornerController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - - zoom.value = currentZoom; - _onChangeAnyController(); - }); - - daysHeaderController.addListener(() { - final xPosition = daysHeaderController.value.getTranslation().x; - final currentZoom = daysHeaderController.value.getMaxScaleOnAxis(); - - final contentYPosition = blockContentController.value.getTranslation().y; - - blockContentController.value - .setTranslationRaw(xPosition, contentYPosition, 0); - - blockContentController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - periodHeaderController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - cornerController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - - zoom.value = currentZoom; - _onChangeAnyController(); - }); - - periodHeaderController.addListener(() { - final yPosition = periodHeaderController.value.getTranslation().y; - final currentZoom = periodHeaderController.value.getMaxScaleOnAxis(); - - final contentXPosition = blockContentController.value.getTranslation().x; - - periodHeaderController.value.setTranslationRaw(0, yPosition, 0); - - blockContentController.value - .setTranslationRaw(contentXPosition, yPosition, 0); - - blockContentController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - daysHeaderController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - cornerController.value.setDiagonal( - vector.Vector4(currentZoom, currentZoom, currentZoom, 1), - ); - - zoom.value = currentZoom; - _onChangeAnyController(); - }); - } - - void _initControllers() { - moveViewportToCurrentPeriodAndDay(); - - setZoom(zoom.value); - } - - void moveViewportToCurrentPeriodAndDay() { - final periodIndex = indexOfCurrentPeriod ?? 0; - final dayIndex = indexOfCurrentDayStartingAtMonday ?? 0; - - moveViewportToPeriodIndexAndDayIndex(periodIndex, dayIndex); - - isCenteredInCurrentPeriodAndDay.value = true; - } - - void moveViewportToPeriodIndexAndDayIndex(int periodIndex, int dayIndex) { - final blockWidth = HorarioMainScroller.blockWidth; - final x = (dayIndex * blockWidth) + (blockWidth / 2); - - final blockHeight = HorarioMainScroller.blockHeight; - final y = (periodIndex * blockHeight) + (blockHeight / 2); - - moveViewportTo(x, y); - } - - void moveViewportTo(double x, double y) { - final viewportWidth = Get.width - Get.mediaQuery.padding.horizontal; - final viewportHeight = Get.height - Get.mediaQuery.padding.vertical; - - x = (x + (HorarioMainScroller.periodWidth / 2)) * zoom.value - - (viewportWidth / 2); - y = (y + (HorarioMainScroller.dayHeight / 2)) * zoom.value - - (viewportHeight / 2); - - x = x < 0 ? 0 : x; - y = y < 0 ? 0 : y; - - final maxXPosition = - (HorarioMainScroller.daysWidth + HorarioMainScroller.periodWidth) * - zoom.value - - viewportWidth; - final maxYPosition = - (HorarioMainScroller.periodsHeight + HorarioMainScroller.dayHeight) * - zoom.value - - viewportHeight + - kToolbarHeight; - - x = x > maxXPosition ? maxXPosition : x; - y = y > maxYPosition ? maxYPosition : y; - - blockContentController.value.setTranslationRaw(-x, -y, 0); - periodHeaderController.value.setTranslationRaw(0, -y, 0); - daysHeaderController.value.setTranslationRaw(-x, 0, 0); - - _onChangeAnyController(); - } - - void setZoom(double zoom) { - blockContentController.value.setDiagonal( - vector.Vector4(zoom, zoom, zoom, 1), - ); - periodHeaderController.value.setDiagonal( - vector.Vector4(zoom, zoom, zoom, 1), - ); - daysHeaderController.value.setDiagonal( - vector.Vector4(zoom, zoom, zoom, 1), - ); - cornerController.value.setDiagonal( - vector.Vector4(zoom, zoom, zoom, 1), - ); - - _onChangeAnyController(); - } - - void addAsignaturaAndSetColor(Asignatura asignatura, {Color? color}) { - bool hasColor = getColor(asignatura) != null; - if (!hasColor) { - Color? newColor = color ?? unusedColors[0]; - _setColor(asignatura, newColor); - } - } - - Color? getColor(Asignatura asignatura) { - String key = '${asignatura.codigo}_${asignatura.tipoHora}'; - int? colorValue = _box.read(key); - return colorValue != null ? Color(colorValue) : null; - } - - void _setColor(Asignatura asignatura, Color color) { - String key = '${asignatura.codigo}_${asignatura.tipoHora}'; - usedColors.add(color); - _box.write(key, color.value); - } -} diff --git a/lib/controllers/qr_passes_controller.dart b/lib/controllers/qr_passes_controller.dart deleted file mode 100644 index ac2fe86..0000000 --- a/lib/controllers/qr_passes_controller.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:get/get.dart'; -import 'package:mi_utem/models/permiso_covid.dart'; -import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; -import 'package:watch_it/watch_it.dart'; - -class QrPassesController extends GetxController { - final passes = [].obs; - final isLoading = false.obs; - - @override - void onInit() { - getPasses(); - super.onInit(); - } - - void getPasses({bool refresh = false}) async { - isLoading.value = true; - List? response = await di.get().getPermisos(forceRefresh: refresh); - if(response == null) { - return; - } - passes.value = response; - isLoading.value = false; - - for (var pass in passes) { - if (pass.id != null) { - // Get.put( - // QrPassController(pass.id!), - // tag: pass.id, - // permanent: true, - // ); - } - } - } -} - -class QrPassesBinding extends Bindings { - @override - void dependencies() { - Get.put(QrPassesController()); - } -} diff --git a/lib/helpers/snackbars.dart b/lib/helpers/snackbars.dart deleted file mode 100644 index 4a0d038..0000000 --- a/lib/helpers/snackbars.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -void showDefaultSnackbar(String title, String message) { - Get.snackbar( - title, - message, - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); -} diff --git a/lib/models/evaluacion.dart b/lib/models/evaluacion.dart index ce66c14..a3326f0 100644 --- a/lib/models/evaluacion.dart +++ b/lib/models/evaluacion.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + class REvaluacion { String? descripcion; num? porcentaje; @@ -28,6 +30,9 @@ class REvaluacion { descripcionKey: descripcion, notaKey: nota, }; + + @override + String toString() => jsonEncode(toJson()); } class IEvaluacion extends REvaluacion { @@ -39,24 +44,30 @@ class IEvaluacion extends REvaluacion { num? porcentaje, num? nota, }) : super( - descripcion: descripcion, - porcentaje: porcentaje, - nota: nota, - ); - - factory IEvaluacion.fromRemote(REvaluacion evaluacion) { - return IEvaluacion( - descripcion: evaluacion.descripcion, - porcentaje: evaluacion.porcentaje, - nota: evaluacion.nota, - ); - } - - IEvaluacion copyWith({bool? editable, String? descripcion, num? porcentaje, num? nota}) => - IEvaluacion( - editable: editable ?? this.editable, - descripcion: descripcion ?? this.descripcion, - porcentaje: porcentaje ?? this.porcentaje, - nota: nota ?? this.nota, - ); + descripcion: descripcion, + porcentaje: porcentaje, + nota: nota, + ); + + factory IEvaluacion.fromRemote(REvaluacion evaluacion) => IEvaluacion( + descripcion: evaluacion.descripcion, + porcentaje: evaluacion.porcentaje, + nota: evaluacion.nota, + ); + + IEvaluacion copyWith({bool? editable, String? descripcion, num? porcentaje, num? nota}) => IEvaluacion( + editable: editable ?? this.editable, + descripcion: descripcion ?? this.descripcion, + porcentaje: porcentaje ?? this.porcentaje, + nota: nota ?? this.nota, + ); + + @override + Map toJson() => { + ...super.toJson(), + "editable": editable, + }; + + @override + String toString() => jsonEncode(toJson()); } diff --git a/lib/models/horario.dart b/lib/models/horario.dart index 5f56230..f7d9869 100644 --- a/lib/models/horario.dart +++ b/lib/models/horario.dart @@ -137,9 +137,13 @@ class Horario { } List> get horarioEnlazado { + final _horario = horario; List> horarioNuevo = []; + if(_horario == null) { + return []; + } - for (List fila in horario!) { + for (List fila in _horario) { List filaNueva = []; for (BloqueHorario bloque in fila) { filaNueva.add(bloque); @@ -215,7 +219,6 @@ class BloqueHorario { for (var bloque in json) { List list = []; for (var dia in bloque) { - log('dia: $dia'); list.add(BloqueHorario.fromJson(dia)); } matrix.add(list); diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 0d98f25..2db55f5 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -9,7 +9,7 @@ import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; @@ -95,7 +95,7 @@ class AsignaturaDetalleScreen extends StatelessWidget { onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())); if (asignatura?.grades != null) { - di.get().updateWithGrades(asignatura!.grades!); + di.get().updateWithGrades(asignatura!.grades!); } }, ), diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index f2fc66b..487112a 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -25,17 +25,6 @@ class _AsignaturasListaScreenState extends State { bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; - - @override - void initState() { - final _carrerasService = di.get(); - if (_carrerasService.selectedCarrera.value == null) { - _carrerasService.getCarreras(forceRefresh: true).then((_) => setState(() => {})); - } - - super.initState(); - } - @override Widget build(BuildContext context) => Scaffold( appBar: CustomAppBar( diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index 9fb822e..d6ea614 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -1,21 +1,33 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; -import 'package:mi_utem/widgets/calculadora_notas/DisplayNotasWidget.dart'; -import 'package:mi_utem/widgets/calculadora_notas/EditarNotasWidget.dart'; +import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:mi_utem/widgets/calculadora_notas/display_notas_widget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/editar_notas_widget.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:watch_it/watch_it.dart'; -class CalculadoraNotasScreen extends StatelessWidget { +class CalculadoraNotasScreen extends StatefulWidget with WatchItStatefulWidgetMixin { + const CalculadoraNotasScreen({super.key}); - const CalculadoraNotasScreen({ - super.key, - }); + @override + State createState() => _CalculadoraNotasScreenState(); +} + +class _CalculadoraNotasScreenState extends State { + + final _controller = di.get(); @override - Widget build(BuildContext context) { - final _calculatorService = di.get(); + void initState() { + _controller.makeEditable(); + super.initState(); + } - _calculatorService.makeEditable(); + @override + Widget build(BuildContext context) { + final partialGrades = watchValue((CalculatorController controller) => controller.partialGrades); + final gradeTextFieldControllers = watchValue((CalculatorController controller) => controller.gradeTextFieldControllers); + final percentageTextFieldControllers = watchValue((CalculatorController controller) => controller.percentageTextFieldControllers); return Scaffold( appBar: CustomAppBar( @@ -23,11 +35,21 @@ class CalculadoraNotasScreen extends StatelessWidget { ), body: ListView( padding: const EdgeInsets.all(10), - children: const [ + children: [ DisplayNotasWidget(), - EditarNotasWidget() + EditarNotasWidget( + partialGrades: partialGrades, + gradeTextFieldControllers: gradeTextFieldControllers, + percentageTextFieldControllers: percentageTextFieldControllers, + onAddGrade: () => _controller.addGrade(IEvaluacion( + nota: null, + porcentaje: null, + )), + onRemoveGrade: (idx) => _controller.removeGradeAt(idx), + ) ], ), ); } } + diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index 64a7a64..e2dbad4 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -2,50 +2,31 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/review_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; +import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:path_provider/path_provider.dart'; import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:watch_it/watch_it.dart'; -class HorarioScreen extends StatelessWidget { - HorarioScreen({ - Key? key, - }) : super(key: key); +class HorarioScreen extends StatefulWidget with WatchItStatefulWidgetMixin { + const HorarioScreen({super.key}); - final ScreenshotController _screenshotController = ScreenshotController(); + @override + State createState() => _HorarioScreenState(); +} - final HorarioController controller = Get.put(HorarioController()); +class _HorarioScreenState extends State { - CustomAppBar get _appBar => CustomAppBar( - title: Text("Horario"), - actions: [ - Obx( - () => controller.horario.value != null && - !controller.isCenteredInCurrentPeriodAndDay.value - ? IconButton( - onPressed: () => _moveViewportToCurrentTime(), - icon: Icon(Icons.center_focus_strong), - ) - : Container(), - ), - Obx( - () => controller.horario.value != null - ? IconButton( - onPressed: () => - _captureAndShareScreenshot(controller.horario.value!), - icon: Icon(Icons.share), - ) - : Container(), - ) - ], - ); + final ScreenshotController _screenshotController = ScreenshotController(); + + final controller = di.get(); void _moveViewportToCurrentTime() { AnalyticsService.logEvent("horario_move_viewport_to_current_time"); @@ -53,26 +34,33 @@ class HorarioScreen extends StatelessWidget { } void _captureAndShareScreenshot(Horario horario) async { + showLoadingDialog(context); AnalyticsService.logEvent("horario_capture_and_share_screenshot"); final horarioScroller = HorarioMainScroller( - controller: controller, horario: horario, showActive: false, ); final image = await _screenshotController.captureFromWidget( horarioScroller.basicHorario, targetSize: - Size(HorarioMainScroller.totalWidth, HorarioMainScroller.totalHeight), + Size(HorarioMainScroller.totalWidth, HorarioMainScroller.totalHeight), ); final directory = await getApplicationDocumentsDirectory(); final imagePath = await File('${directory.path}/horario.png').create(); await imagePath.writeAsBytes(image); + Navigator.pop(context); /// Share Plugin await Share.shareXFiles([XFile(imagePath.path)]); } + @override + void initState() { + controller.getHorarioData(); + super.initState(); + } + @override Widget build(BuildContext context) { ReviewService.addScreen("HorarioScreen"); @@ -82,45 +70,56 @@ class HorarioScreen extends StatelessWidget { DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); + controller.init(); + + final horario = watchValue((HorarioController controller) => controller.horario); + final isLoadingHorario = watchValue((HorarioController controller) => controller.loadingHorario); + final isCenteredInCurrentPeriodAndDay = watchValue((HorarioController controller) => controller.isCenteredInCurrentPeriodAndDay); + return Scaffold( - appBar: _appBar, - body: Obx( - () { - if ((controller.loadingHorario.value && - controller.horario.value == null) || - controller.horario.value == null) { - return Container( - padding: EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Center( - child: LoadingIndicator(), - ), - ), - ], + appBar: CustomAppBar( + title: Text("Horario"), + actions: [ + horario != null ? IconButton( + onPressed: () { + controller.getHorarioData(forceRefresh: true).then((value) { + setState(() {}); + }); + }, + icon: Icon(Icons.refresh_sharp), + tooltip: "Forzar actualización del horario", + ) : Container(), + horario != null && !isCenteredInCurrentPeriodAndDay ? IconButton( + onPressed: () => _moveViewportToCurrentTime(), + icon: Icon(Icons.center_focus_strong), + tooltip: "Centrar Horario En Hora Actual", + ) : Container(), + horario != null ? IconButton( + onPressed: () => _captureAndShareScreenshot(horario), + icon: Icon(Icons.share), + tooltip: "Compartir Horario", + ) : Container() + ], + ), + body: ((isLoadingHorario && horario == null) || horario == null) ? Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Expanded( + child: Center( + child: LoadingIndicator(), ), - ); - } - - return Screenshot( - controller: _screenshotController, - child: HorarioMainScroller( - controller: controller, - horario: controller.horario.value!, ), - ); - }, + ], + ), + ) : Screenshot( + controller: _screenshotController, + child: HorarioMainScroller( + horario: horario, + ), ), ); } } - -class HorarioBinding extends Bindings { - @override - void dependencies() { - Get.put(HorarioController(), permanent: true); - } -} diff --git a/lib/screens/horario/widgets/horario_blocks_content.dart b/lib/screens/horario/widgets/horario_blocks_content.dart index ae194e1..04df9db 100644 --- a/lib/screens/horario/widgets/horario_blocks_content.dart +++ b/lib/screens/horario/widgets/horario_blocks_content.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/themes/theme.dart'; @@ -25,17 +23,13 @@ class HorarioBlocksContent extends StatelessWidget { List get _children { final rows = []; - for (int blockIndex = 0; - blockIndex < horario.horarioEnlazado.length; - blockIndex++) { + for (int blockIndex = 0; blockIndex < horario.horarioEnlazado.length; blockIndex++) { final currentRow = []; if ((blockIndex % 2) == 0) { List bloquePorDias = horario.horarioEnlazado[blockIndex]; for (num dia = 0; dia < bloquePorDias.length; dia++) { BloqueHorario block = horario.horarioEnlazado[blockIndex][dia as int]; - log(block.asignatura?.nombre.toString() ?? "aaa"); - currentRow.add( ClassBlockCard( block: block, diff --git a/lib/screens/horario/widgets/horario_days_header.dart b/lib/screens/horario/widgets/horario_days_header.dart index f9ce555..08fe6c1 100644 --- a/lib/screens/horario/widgets/horario_days_header.dart +++ b/lib/screens/horario/widgets/horario_days_header.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/bloque_dias_card.dart'; +import 'package:watch_it/watch_it.dart'; class HorarioDaysHeader extends StatelessWidget { - final HorarioController? controller; final Horario horario; final double height; final double dayWidth; @@ -16,7 +16,6 @@ class HorarioDaysHeader extends StatelessWidget { const HorarioDaysHeader({ Key? key, - this.controller, required this.horario, required this.height, required this.dayWidth, @@ -37,8 +36,7 @@ class HorarioDaysHeader extends StatelessWidget { day: entry.value!, height: height, width: dayWidth, - active: showActiveDay && - entry.key == controller?.indexOfCurrentDayStartingAtMonday, + active: showActiveDay && entry.key == di.get().indexOfCurrentDayStartingAtMonday, backgroundColor: backgroundColor, ), ) diff --git a/lib/screens/horario/widgets/horario_indicator.dart b/lib/screens/horario/widgets/horario_indicator.dart index 42f4dab..028db3f 100644 --- a/lib/screens/horario/widgets/horario_indicator.dart +++ b/lib/screens/horario/widgets/horario_indicator.dart @@ -2,28 +2,27 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; +import 'package:watch_it/watch_it.dart'; -class HorarioIndicator extends StatefulWidget { +class HorarioIndicator extends StatefulWidget with WatchItStatefulWidgetMixin { static const _height = 2.0; static const _circleRadius = 10.0; static const _tapAreaRadius = 15.0; - final HorarioController controller; final EdgeInsets initialMargin; final double heightByMinute; final double maxWidth; final Color color; const HorarioIndicator({ - Key? key, - required this.controller, + super.key, required this.initialMargin, required this.heightByMinute, required this.maxWidth, this.color = Colors.red, - }) : super(key: key); + }); @override State createState() => _HorarioIndicatorState(); @@ -47,23 +46,20 @@ class _HorarioIndicatorState extends State { super.dispose(); } - double get _centerLineYPosition => - (widget.controller.minutesFromStart * widget.heightByMinute); + double get _centerLineYPosition => (di.get().minutesFromStart * widget.heightByMinute); double get _startLineXPosition => widget.initialMargin.left; @override Widget build(BuildContext context) { - final circleTapAreaTop = _centerLineYPosition - - HorarioIndicator._circleRadius - - HorarioIndicator._tapAreaRadius; - final circleTapAreaLeft = _startLineXPosition - - HorarioIndicator._circleRadius - - HorarioIndicator._tapAreaRadius; + final circleTapAreaTop = _centerLineYPosition - HorarioIndicator._circleRadius - HorarioIndicator._tapAreaRadius; + final circleTapAreaLeft = _startLineXPosition - HorarioIndicator._circleRadius - HorarioIndicator._tapAreaRadius; final lineTop = _centerLineYPosition - HorarioIndicator._height / 2; final lineLeft = _startLineXPosition; + final indicatorIsOpen = watchValue((HorarioController controller) => controller.indicatorIsOpen); + return Stack( children: [ if (lineTop > 0 && lineLeft > 0) @@ -83,32 +79,27 @@ class _HorarioIndicatorState extends State { left: circleTapAreaLeft, ), child: GestureDetector( - onTap: () => _onIndicatorDotTap(), + onTap: () { + AnalyticsService.logEvent('horario_indicator_dot_tap'); + di.get().setIndicatorIsOpen(!indicatorIsOpen); + }, child: Container( padding: EdgeInsets.all(HorarioIndicator._tapAreaRadius), decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - HorarioIndicator._tapAreaRadius * 2, - ), + borderRadius: BorderRadius.circular(HorarioIndicator._tapAreaRadius * 2), ), - child: Obx( - () => AnimatedContainer( - duration: const Duration(milliseconds: 300), - height: HorarioIndicator._circleRadius * 2, - width: widget.controller.indicatorIsOpen.value - ? 50 - : HorarioIndicator._circleRadius * 2, - decoration: BoxDecoration( - color: widget.color, - borderRadius: - BorderRadius.circular(HorarioIndicator._circleRadius), - ), - child: widget.controller.indicatorIsOpen.value - ? Center( - child: _TickerTimeText(time: DateTime.now()), - ) - : Container(), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: HorarioIndicator._circleRadius * 2, + width: indicatorIsOpen ? 50 : HorarioIndicator._circleRadius * 2, + decoration: BoxDecoration( + color: widget.color, + borderRadius: + BorderRadius.circular(HorarioIndicator._circleRadius), ), + child: indicatorIsOpen ? Center( + child: _TickerTimeText(time: DateTime.now()), + ) : Container(), ), ), ), @@ -116,12 +107,6 @@ class _HorarioIndicatorState extends State { ], ); } - - _onIndicatorDotTap() { - AnalyticsService.logEvent('horario_indicator_dot_tap'); - widget.controller.indicatorIsOpen.value = - !widget.controller.indicatorIsOpen.value; - } } class _TickerTimeText extends StatefulWidget { diff --git a/lib/screens/horario/widgets/horario_main_scroller.dart b/lib/screens/horario/widgets/horario_main_scroller.dart index bee02e2..6fe4330 100644 --- a/lib/screens/horario/widgets/horario_main_scroller.dart +++ b/lib/screens/horario/widgets/horario_main_scroller.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:get/get_state_manager/get_state_manager.dart'; -import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_blocks_content.dart'; import 'package:mi_utem/screens/horario/widgets/horario_corner.dart'; import 'package:mi_utem/screens/horario/widgets/horario_days_header.dart'; import 'package:mi_utem/screens/horario/widgets/horario_indicator.dart'; import 'package:mi_utem/screens/horario/widgets/horario_periods_header.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; +import 'package:watch_it/watch_it.dart'; class HorarioMainScroller extends StatefulWidget { static const double blockWidth = 320.0; @@ -23,193 +23,173 @@ class HorarioMainScroller extends StatefulWidget { final Horario horario; final bool showActive; - final HorarioController controller; + final controller = di.get(); - const HorarioMainScroller({ - Key? key, + HorarioMainScroller({ + super.key, required this.horario, - required this.controller, this.showActive = true, - }) : super(key: key); + }); @override _HorarioMainScrollerState createState() => _HorarioMainScrollerState(); - static double get daysWidth => dayWidth * HorarioController.daysCount; - static double get periodsHeight => - periodHeight * HorarioController.periodsCount; + static double get daysWidth => dayWidth * di.get().daysCount; + static double get periodsHeight => periodHeight * di.get().periodsCount; static double get totalWidth => daysWidth + periodWidth; static double get totalHeight => periodsHeight + dayHeight; Widget get _horarioBlocksContent => HorarioBlocksContent( - horario: horario, - blockHeight: blockHeight, - blockWidth: blockWidth, - blockInternalMargin: blockInternalMargin, - ); + horario: horario, + blockHeight: blockHeight, + blockWidth: blockWidth, + blockInternalMargin: blockInternalMargin, + ); Widget get _horarioDaysHeader => HorarioDaysHeader( - controller: controller, - horario: horario, - height: dayHeight, - dayWidth: dayWidth, - showActiveDay: showActive, - ); + horario: horario, + height: dayHeight, + dayWidth: dayWidth, + showActiveDay: showActive, + ); Widget get _horarioPeriodsHeader => HorarioPeriodsHeader( - controller: controller, - horario: horario, - width: periodWidth, - periodHeight: periodHeight, - showActivePeriod: showActive, - ); + horario: horario, + width: periodWidth, + periodHeight: periodHeight, + showActivePeriod: showActive, + ); Widget get _horarioCorner => HorarioCorner( - height: dayHeight, - width: periodWidth, - ); + height: dayHeight, + width: periodWidth, + ); Widget get basicHorario => Container( - color: Colors.white, - child: Column( + color: Colors.white, + child: Column( + children: [ + Row( children: [ - Row( - children: [ - _horarioCorner, - _horarioDaysHeader, - ], - ), - Row( - children: [ - _horarioPeriodsHeader, - _horarioBlocksContent, - ], - ) + _horarioCorner, + _horarioDaysHeader, ], ), - ); + Row( + children: [ + _horarioPeriodsHeader, + _horarioBlocksContent, + ], + ) + ], + ), + ); } class _HorarioMainScrollerState extends State { - @override - void initState() { - super.initState(); - } @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.white, - child: GetBuilder(builder: (controller) { - return Stack( - children: [ - Container( - height: HorarioMainScroller.periodsHeight, - width: HorarioMainScroller.daysWidth, - margin: EdgeInsets.only( - top: HorarioMainScroller.dayHeight * - widget.controller.zoom.value, - left: HorarioMainScroller.periodWidth * - widget.controller.zoom.value, - ), - child: InteractiveViewer( - transformationController: - widget.controller.blockContentController, - maxScale: HorarioMainScroller.defaultMaxScale, - minScale: HorarioMainScroller.defaultMinScale, - panAxis: PanAxis.free, - clipBehavior: Clip.none, - constrained: false, - onInteractionUpdate: (interaction) {}, - child: SafeArea( - child: widget._horarioBlocksContent, - ), - ), + Widget build(BuildContext context) => Container( + color: Colors.white, + child: Stack( + children: [ + Container( + height: HorarioMainScroller.periodsHeight, + width: HorarioMainScroller.daysWidth, + margin: EdgeInsets.only( + top: HorarioMainScroller.dayHeight * widget.controller.zoom.value, + left: HorarioMainScroller.periodWidth * widget.controller.zoom.value, + ), + child: InteractiveViewer( + transformationController: + widget.controller.blockContentController, + maxScale: HorarioMainScroller.defaultMaxScale, + minScale: HorarioMainScroller.defaultMinScale, + panAxis: PanAxis.free, + clipBehavior: Clip.none, + constrained: false, + onInteractionUpdate: (interaction) {}, + child: SafeArea( + child: widget._horarioBlocksContent, ), - Container( - width: HorarioMainScroller.daysWidth, - height: HorarioMainScroller.dayHeight, - margin: EdgeInsets.only( - left: HorarioMainScroller.periodWidth * - widget.controller.zoom.value, - ), - child: InteractiveViewer( - transformationController: - widget.controller.daysHeaderController, - maxScale: HorarioMainScroller.defaultMaxScale, - minScale: HorarioMainScroller.defaultMinScale, - panAxis: PanAxis.free, - scaleEnabled: false, - clipBehavior: Clip.none, - constrained: false, - onInteractionUpdate: (interaction) {}, - child: SafeArea( - child: widget._horarioDaysHeader, - ), - ), + ), + ), + Container( + width: HorarioMainScroller.daysWidth, + height: HorarioMainScroller.dayHeight, + margin: EdgeInsets.only( + left: HorarioMainScroller.periodWidth * + widget.controller.zoom.value, + ), + child: InteractiveViewer( + transformationController: + widget.controller.daysHeaderController, + maxScale: HorarioMainScroller.defaultMaxScale, + minScale: HorarioMainScroller.defaultMinScale, + panAxis: PanAxis.free, + scaleEnabled: false, + clipBehavior: Clip.none, + constrained: false, + onInteractionUpdate: (interaction) {}, + child: SafeArea( + child: widget._horarioDaysHeader, ), - Container( - width: HorarioMainScroller.periodWidth, - height: HorarioMainScroller.dayHeight, - child: InteractiveViewer( - transformationController: widget.controller.cornerController, - maxScale: HorarioMainScroller.defaultMaxScale, - minScale: HorarioMainScroller.defaultMinScale, - panAxis: PanAxis.free, - scaleEnabled: false, - panEnabled: false, - clipBehavior: Clip.none, - constrained: false, - onInteractionUpdate: (interaction) {}, - child: SafeArea( - child: widget._horarioCorner, - ), - ), + ), + ), + Container( + width: HorarioMainScroller.periodWidth, + height: HorarioMainScroller.dayHeight, + child: InteractiveViewer( + transformationController: widget.controller.cornerController, + maxScale: HorarioMainScroller.defaultMaxScale, + minScale: HorarioMainScroller.defaultMinScale, + panAxis: PanAxis.free, + scaleEnabled: false, + panEnabled: false, + clipBehavior: Clip.none, + constrained: false, + onInteractionUpdate: (interaction) {}, + child: SafeArea( + child: widget._horarioCorner, ), - Container( - width: HorarioMainScroller.periodWidth, - height: HorarioMainScroller.periodsHeight, - margin: EdgeInsets.only( - top: HorarioMainScroller.dayHeight * - widget.controller.zoom.value, - ), - child: InteractiveViewer( - transformationController: - widget.controller.periodHeaderController, - maxScale: HorarioMainScroller.defaultMaxScale, - minScale: HorarioMainScroller.defaultMinScale, - panAxis: PanAxis.free, - scaleEnabled: false, - clipBehavior: Clip.none, - constrained: false, - onInteractionUpdate: (interaction) {}, - child: SafeArea( - child: Stack( - children: [ - widget._horarioPeriodsHeader, - HorarioIndicator( - controller: widget.controller, - maxWidth: HorarioMainScroller.daysWidth, - initialMargin: EdgeInsets.only( - top: HorarioMainScroller.dayHeight, - left: HorarioMainScroller.periodWidth, - ), - heightByMinute: HorarioMainScroller.blockHeight / 100, - ), - ], + ), + ), + Container( + width: HorarioMainScroller.periodWidth, + height: HorarioMainScroller.periodsHeight, + margin: EdgeInsets.only( + top: HorarioMainScroller.dayHeight * + widget.controller.zoom.value, + ), + child: InteractiveViewer( + transformationController: + widget.controller.periodHeaderController, + maxScale: HorarioMainScroller.defaultMaxScale, + minScale: HorarioMainScroller.defaultMinScale, + panAxis: PanAxis.free, + scaleEnabled: false, + clipBehavior: Clip.none, + constrained: false, + onInteractionUpdate: (interaction) {}, + child: SafeArea( + child: Stack( + children: [ + widget._horarioPeriodsHeader, + HorarioIndicator( + maxWidth: HorarioMainScroller.daysWidth, + initialMargin: EdgeInsets.only( + top: HorarioMainScroller.dayHeight, + left: HorarioMainScroller.periodWidth, + ), + heightByMinute: HorarioMainScroller.blockHeight / 100, ), - ), + ], ), ), - ], - ); - }), - ); - } + ), + ), + ], + ), + ); } diff --git a/lib/screens/horario/widgets/horario_periods_header.dart b/lib/screens/horario/widgets/horario_periods_header.dart index 772ac0c..12b22c0 100644 --- a/lib/screens/horario/widgets/horario_periods_header.dart +++ b/lib/screens/horario/widgets/horario_periods_header.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/bloque_periodo_card.dart'; +import 'package:watch_it/watch_it.dart'; class HorarioPeriodsHeader extends StatelessWidget { - final HorarioController? controller; final Horario horario; final double periodHeight; final double width; @@ -16,7 +16,6 @@ class HorarioPeriodsHeader extends StatelessWidget { const HorarioPeriodsHeader({ Key? key, - this.controller, required this.horario, required this.periodHeight, required this.width, @@ -37,8 +36,7 @@ class HorarioPeriodsHeader extends StatelessWidget { inicio: horario.horasInicio[e.key], intermedio: horario.horasIntermedio[e.key], fin: horario.horasTermino[e.key], - active: showActivePeriod && - controller?.indexOfCurrentPeriod == e.key, + active: showActivePeriod && di.get().indexOfCurrentPeriod == e.key, height: periodHeight, width: width, backgroundColor: backgroundColor, diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index f032c38..f144e72 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -12,6 +12,7 @@ import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:watch_it/watch_it.dart'; @@ -67,24 +68,26 @@ class _SplashScreenState extends State { fit: BoxFit.contain, animation: "default", callback: (String val) async { - Get.dialog(LoadingDialog(), barrierDismissible: false); + showLoadingDialog(context); // Revisar si tenemos conexión a internet try { final response = await httpClient.get(Uri.parse(apiUrl)); final json = jsonDecode(response.body); if(!(json is Map && json["funcionando"] == true)) { - Get.back(); - Get.snackbar("Error", "No se pudo conectar al servidor. Revisa tu conexión a internet.", + Navigator.pop(context); + showTextSnackbar(context, + title: "Error", + message: "No se pudo conectar al servidor. Revisa tu conexión a internet.", backgroundColor: Colors.red, - colorText: Colors.white, ); return; } } catch (e) { - Get.back(); - Get.snackbar("Error", "No se pudo conectar al servidor. Revisa tu conexión a internet.", + Navigator.pop(context); + showTextSnackbar(context, + title: "Error", + message: "No se pudo conectar al servidor. Revisa tu conexión a internet.", backgroundColor: Colors.red, - colorText: Colors.white, ); return; } @@ -98,7 +101,7 @@ class _SplashScreenState extends State { AnalyticsService.setUser(user); } } - Get.back(); + Navigator.pop(context); Navigator.pushReplacement(context, MaterialPageRoute(builder: (ctx) => isLoggedIn ? MainScreen() : LoginScreen())); }, ), diff --git a/lib/services/review_service.dart b/lib/services/review_service.dart index a2cf404..7e46b15 100644 --- a/lib/services/review_service.dart +++ b/lib/services/review_service.dart @@ -1,7 +1,6 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:in_app_review/in_app_review.dart'; - import 'package:mi_utem/widgets/custom_alert_dialog.dart'; class ReviewService { @@ -47,8 +46,7 @@ class ReviewService { DateTime maxDate = DateTime.now().subtract(maxScreen); DateTime minDate = DateTime.now().subtract(minScreen); if (date.isBefore(maxDate) || date.isAfter(minDate)) { - print( - "ReviewService mustRequest: $screen no cumple con una fecha $isoDate"); + print("ReviewService mustRequest: $screen no cumple con una fecha $isoDate"); return false; } } else { diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services_new/implementations/auth_service.dart index 90230d1..ed70189 100644 --- a/lib/services_new/implementations/auth_service.dart +++ b/lib/services_new/implementations/auth_service.dart @@ -34,17 +34,31 @@ class AuthServiceImplementation implements AuthService { return false; } + final lastLogin = await secureStorage.read(key: "last_login"); + if(lastLogin == null) { + return false; + } + + final lastLoginDate = DateTime.fromMillisecondsSinceEpoch(int.parse(lastLogin)); + final now = DateTime.now(); + final difference = now.difference(lastLoginDate); + if(difference.inMinutes < 5) { + return true; + } + try { - final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth/refresh"), + final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth"), body: credential.toString(), headers: { 'Content-Type': 'application/json', 'User-Agent': 'App/MiUTEM', - 'Authorization': 'Bearer ${user.token}', }, ); final token = jsonDecode(response.body)["token"] as String; + if(token == user.token) { + return true; + } final userJson = user.toJson(); userJson["token"] = token; await setUser(User.fromJson(userJson)); diff --git a/lib/services_new/implementations/calculator_service.dart b/lib/services_new/implementations/controllers/calculator_controller.dart similarity index 73% rename from lib/services_new/implementations/calculator_service.dart rename to lib/services_new/implementations/controllers/calculator_controller.dart index ff11cad..ce095ab 100644 --- a/lib/services_new/implementations/calculator_service.dart +++ b/lib/services_new/implementations/controllers/calculator_controller.dart @@ -1,11 +1,11 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/grades.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; -class CalculatorServiceImplementation with ChangeNotifier implements CalculatorService { +class CalculatorControllerImplementation implements CalculatorController { /* Porcentaje máximo de todas las notas */ static const maxPercentage = 100; @@ -27,26 +27,26 @@ class CalculatorServiceImplementation with ChangeNotifier implements CalculatorS /* Notas parciales */ @override - ValueNotifier> get partialGrades => ValueNotifier>([]); + ValueNotifier> partialGrades = ValueNotifier([]); /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ @override - ValueNotifier> get percentageTextFieldControllers => ValueNotifier>([]); + ValueNotifier> percentageTextFieldControllers = ValueNotifier([]); /* Controlador de texto para las notas con máscara (para autocompletar formato) */ @override - ValueNotifier> get gradeTextFieldControllers => ValueNotifier>([]); + ValueNotifier> gradeTextFieldControllers = ValueNotifier([]); /* Nota del examen */ @override - ValueNotifier get examGrade => ValueNotifier(null); + ValueNotifier examGrade = ValueNotifier(null); /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ @override - MaskedTextController get examGradeTextFieldController => MaskedTextController(mask: "0.0"); + ValueNotifier examGradeTextFieldController = ValueNotifier(MaskedTextController(mask: "0.0")); @override - ValueNotifier get freeEditable => ValueNotifier(false); + ValueNotifier freeEditable = ValueNotifier(false); @override double? get getCalculatedFinalGrade { @@ -189,66 +189,75 @@ class CalculatorServiceImplementation with ChangeNotifier implements CalculatorS } setExamGrade(grades.notaExamen); - notifyListeners(); } @override void updateGradeAt(int index, IEvaluacion updatedGrade) { final grade = partialGrades.value[index]; - if(grade.editable || freeEditable.value) { - partialGrades.value[index] = updatedGrade; + if(!(grade.editable || freeEditable.value)) { + return; + } - if(hasMissingPartialGrade) { - clearExamGrade(); - } - notifyListeners(); - } else { - throw CustomException.custom("No se puede editar una nota que está asignada"); + final copy = partialGrades.value; + copy[index] = updatedGrade; + partialGrades.value = copy; + + if(hasMissingPartialGrade) { + clearExamGrade(); } } @override void addGrade(IEvaluacion grade) { - partialGrades.value.add(grade); - percentageTextFieldControllers.value.add(MaskedTextController( - mask: "000", - text: grade.porcentaje?.toStringAsFixed(0) ?? "", - )); - gradeTextFieldControllers.value.add(MaskedTextController( - mask: "0.0", - text: grade.nota?.toStringAsFixed(1) ?? "", - )); - notifyListeners(); + partialGrades.value = [...partialGrades.value, grade]; + percentageTextFieldControllers.value = [ + ...percentageTextFieldControllers.value, + MaskedTextController( + mask: "000", + text: grade.porcentaje?.toStringAsFixed(0) ?? "", + ), + ]; + gradeTextFieldControllers.value = [ + ...gradeTextFieldControllers.value, + MaskedTextController( + mask: "0.0", + text: grade.nota?.toStringAsFixed(1) ?? "", + ), + ]; } @override void removeGradeAt(int index) { final grade = partialGrades.value[index]; - if(grade.editable || freeEditable.value) { - partialGrades.value.removeRange(index, index+1); - percentageTextFieldControllers.value.removeRange(index, index+1); - gradeTextFieldControllers.value.removeRange(index, index+1); - notifyListeners(); - } else { - throw CustomException.custom("No se puede eliminar una nota que está asignada"); + if(!(grade.editable || freeEditable.value)) { + return; } + + partialGrades.value = partialGrades.value..removeAt(index); + percentageTextFieldControllers.value = percentageTextFieldControllers.value..removeAt(index); + gradeTextFieldControllers.value = gradeTextFieldControllers.value..removeAt(index); + logger.d("Removed grade at index $index"); } @override - void makeEditable() => freeEditable.value = true; + void makeEditable() { + freeEditable.value = true; + } @override - void makeNonEditable() => freeEditable.value = false; + void makeNonEditable() { + freeEditable.value = false; + } @override void clearExamGrade() { examGrade.value = null; - examGradeTextFieldController.updateText(""); + examGradeTextFieldController.value = examGradeTextFieldController.value..updateText(""); } @override void setExamGrade(num? grade) { examGrade.value = grade?.toDouble(); - examGradeTextFieldController.updateText(grade?.toDouble().toStringAsFixed(1) ?? ""); + examGradeTextFieldController.value = examGradeTextFieldController.value..updateText(grade?.toDouble().toStringAsFixed(1) ?? ""); } } \ No newline at end of file diff --git a/lib/services_new/implementations/controllers/horario_controller.dart b/lib/services_new/implementations/controllers/horario_controller.dart new file mode 100644 index 0000000..8cef997 --- /dev/null +++ b/lib/services_new/implementations/controllers/horario_controller.dart @@ -0,0 +1,293 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/models/horario.dart'; +import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; +import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; +import 'package:mi_utem/services_new/interfaces/horario_service.dart'; +import 'package:vector_math/vector_math_64.dart' as vector; +import 'package:watch_it/watch_it.dart'; + +class HorarioControllerImplementation extends ChangeNotifier implements HorarioController { + final _storage = GetStorage(); + final _randomColors = Colors.primaries.toList()..shuffle(); + final _now = DateTime.now(); + + @override + num daysCount = 6; + + @override + num periodsCount = 9; + + @override + String startTime = "7:55"; + + @override + Duration periodDuration = Duration(minutes: 90); + + @override + Duration periodGap = Duration(minutes: 5); + + @override + ValueNotifier horario = ValueNotifier(Horario()); + + @override + ValueNotifier loadingHorario = ValueNotifier(false); + + @override + List usedColors = []; + + @override + ValueNotifier zoom = ValueNotifier(0.5); + + @override + ValueNotifier indicatorIsOpen = ValueNotifier(false); + + @override + ValueNotifier isCenteredInCurrentPeriodAndDay = ValueNotifier(false); + + @override + TransformationController blockContentController = TransformationController(); + + @override + TransformationController daysHeaderController = TransformationController(); + + @override + TransformationController periodHeaderController = TransformationController(); + + @override + TransformationController cornerController = TransformationController(); + + @override + List get unusedColors { + List availableColors = [..._randomColors].where((Color color) => !usedColors.contains(color)).toList(); + return availableColors.isEmpty ? [..._randomColors] : availableColors; + } + + @override + double get minutesFromStart => _now.difference(DateTime(_now.year, _now.month, _now.day, int.parse(startTime.split(":")[0]), int.parse(startTime.split(":")[1]))).inMinutes.toDouble(); + + @override + int? get indexOfCurrentDayStartingAtMonday => _now.weekday > daysCount ? null : _now.weekday - 1; + + @override + int? get indexOfCurrentPeriod { + final periodBlockDuration = periodDuration.inMinutes + (periodGap.inMinutes * 2); + + final minutesModule = minutesFromStart % periodBlockDuration; + + if (minutesModule >= periodGap.inMinutes && minutesModule <= (periodBlockDuration - periodGap.inMinutes)) { + return (minutesFromStart ~/ periodBlockDuration); // Periodo actual + } + + return null; + } + + @override + void init() { + zoom.value = RemoteConfigService.horarioZoom; + moveViewportToCurrentPeriodAndDay(); + setZoom(zoom.value); + + _setScrollControllerListeners(); + } + + @override + Future getHorarioData({ bool forceRefresh = false }) async { + loadingHorario.value = true; + notifyListeners(); + final lastUpdate = _storage.read("last_horario_update") ?? "${DateTime.now().toIso8601String()}"; + final lastUpdateDate = DateTime.parse(lastUpdate); + final now = DateTime.now(); + final difference = now.difference(lastUpdateDate).inMinutes; + if(difference < 15 && forceRefresh == false && horario.value != null) { + loadingHorario.value = false; + notifyListeners(); + return; + } + + + horario.value = null; + notifyListeners(); + + final carrerasService = di.get(); + Carrera? carrera = carrerasService.selectedCarrera.value; + if (carrera == null) { + await carrerasService.getCarreras(forceRefresh: true); + } + carrera = carrerasService.selectedCarrera.value; + + final carreraId = carrera?.id; + if(carreraId == null) { + loadingHorario.value = false; + notifyListeners(); + return; + } + horario.value = await di.get().getHorario(carreraId); + _setRandomColorsByHorario(); + + loadingHorario.value = false; + notifyListeners(); + _storage.write("last_horario_update", DateTime.now().toIso8601String()); + } + + @override + void moveViewportTo(double x, double y) { + final viewportWidth = Get.width - Get.mediaQuery.padding.horizontal; + final viewportHeight = Get.height - Get.mediaQuery.padding.vertical; + + x = (x + (HorarioMainScroller.periodWidth / 2)) * zoom.value - (viewportWidth / 2); + y = (y + (HorarioMainScroller.dayHeight / 2)) * zoom.value - (viewportHeight / 2); + + x = x < 0 ? 0 : x; + y = y < 0 ? 0 : y; + + final maxXPosition = (HorarioMainScroller.daysWidth + HorarioMainScroller.periodWidth) * zoom.value - viewportWidth; + final maxYPosition = (HorarioMainScroller.periodsHeight + HorarioMainScroller.dayHeight) * zoom.value - viewportHeight + kToolbarHeight; + + x = x > maxXPosition ? maxXPosition : x; + y = y > maxYPosition ? maxYPosition : y; + + blockContentController.value.setTranslationRaw(-x, -y, 0); + periodHeaderController.value.setTranslationRaw(0, -y, 0); + daysHeaderController.value.setTranslationRaw(-x, 0, 0); + + _onChangeAnyController(); + } + + @override + void moveViewportToPeriodIndexAndDayIndex(int periodIndex, int dayIndex) { + final blockWidth = HorarioMainScroller.blockWidth; + final x = (dayIndex * blockWidth) + (blockWidth / 2); + + final blockHeight = HorarioMainScroller.blockHeight; + final y = (periodIndex * blockHeight) + (blockHeight / 2); + + moveViewportTo(x, y); + } + + @override + void moveViewportToCurrentPeriodAndDay() { + final periodIndex = indexOfCurrentPeriod ?? 0; + final dayIndex = indexOfCurrentDayStartingAtMonday ?? 0; + + moveViewportToPeriodIndexAndDayIndex(periodIndex, dayIndex); + + isCenteredInCurrentPeriodAndDay.value = true; + notifyListeners(); + } + + @override + void setZoom(double zoom) { + blockContentController.value.setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); + periodHeaderController.value.setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); + daysHeaderController.value.setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); + cornerController.value.setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); + + _onChangeAnyController(); + } + + @override + void addAsignaturaAndSetColor(Asignatura asignatura, {Color? color}) { + bool hasColor = getColor(asignatura) != null; + if (hasColor) { + return; + } + + final _newColor = color ?? unusedColors[0]; + final _key = '${asignatura.codigo}_${asignatura.tipoHora}'; + usedColors.add(_newColor); + _storage.write(_key, _newColor.value); + } + + @override + Color? getColor(Asignatura asignatura) { + final _key = '${asignatura.codigo}_${asignatura.tipoHora}'; + final _colorValue = _storage.read(_key); + return _colorValue != null ? Color(_colorValue) : null; + } + + @override + void setIndicatorIsOpen(bool isOpen) { + indicatorIsOpen.value = isOpen; + notifyListeners(); + } + + void _setRandomColorsByHorario() { + final _horario = horario.value?.horario; + if (_horario == null) { + return; + } + + _horario.forEach((dia) => dia.forEach((bloque) { + final _asignatura = bloque.asignatura; + if (_asignatura == null) { + return; + } + + addAsignaturaAndSetColor(bloque.asignatura!); + })); + } + + void _onChangeAnyController() { + setIndicatorIsOpen(true); + isCenteredInCurrentPeriodAndDay.value = false; + notifyListeners(); + } + + void _setScrollControllerListeners() { + blockContentController.addListener(() { + final xPosition = blockContentController.value.getTranslation().x; + final yPosition = blockContentController.value.getTranslation().y; + final currentZoom = blockContentController.value.getMaxScaleOnAxis(); + + daysHeaderController.value.setTranslationRaw(xPosition, 0, 0); + periodHeaderController.value.setTranslationRaw(0, yPosition, 0); + + daysHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1),); + periodHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + + zoom.value = currentZoom; + _onChangeAnyController(); + }); + + daysHeaderController.addListener(() { + final currentZoom = daysHeaderController.value.getMaxScaleOnAxis(); + final xPosition = daysHeaderController.value.getTranslation().x; + final contentYPosition = blockContentController.value.getTranslation().y; + + blockContentController.value.setTranslationRaw(xPosition, contentYPosition, 0); + + blockContentController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + periodHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + + zoom.value = currentZoom; + _onChangeAnyController(); + }); + + periodHeaderController.addListener(() { + final yPosition = periodHeaderController.value.getTranslation().y; + final currentZoom = periodHeaderController.value.getMaxScaleOnAxis(); + + final contentXPosition = blockContentController.value.getTranslation().x; + + periodHeaderController.value.setTranslationRaw(0, yPosition, 0); + + blockContentController.value.setTranslationRaw(contentXPosition, yPosition, 0); + + blockContentController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + daysHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + + zoom.value = currentZoom; + _onChangeAnyController(); + }); + } + +} \ No newline at end of file diff --git a/lib/services_new/implementations/horario_service.dart b/lib/services_new/implementations/horario_service.dart index 0f3d468..8556f44 100644 --- a/lib/services_new/implementations/horario_service.dart +++ b/lib/services_new/implementations/horario_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; @@ -27,6 +28,7 @@ class HorarioServiceImplementation implements HorarioService { final json = jsonDecode(response.body); if(response.statusCode != 200) { + logger.e("[HorarioService] Error al obtener horario: ${response.reasonPhrase}", json); if(json is Map && json.containsKey("error")) { throw CustomException.fromJson(json as Map); } diff --git a/lib/services_new/interfaces/calculator_service.dart b/lib/services_new/interfaces/controllers/calculator_controller.dart similarity index 82% rename from lib/services_new/interfaces/calculator_service.dart rename to lib/services_new/interfaces/controllers/calculator_controller.dart index a0576bb..9c84f79 100644 --- a/lib/services_new/interfaces/calculator_service.dart +++ b/lib/services_new/interfaces/controllers/calculator_controller.dart @@ -3,24 +3,24 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/evaluacion.dart'; import 'package:mi_utem/models/grades.dart'; -abstract class CalculatorService { +abstract class CalculatorController { /* Notas parciales */ - ValueNotifier> get partialGrades; + abstract ValueNotifier> partialGrades; /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ - ValueNotifier> get percentageTextFieldControllers; + abstract ValueNotifier> percentageTextFieldControllers; /* Controlador de texto para las notas con máscara (para autocompletar formato) */ - ValueNotifier> get gradeTextFieldControllers; + abstract ValueNotifier> gradeTextFieldControllers; /* Nota del examen */ - ValueNotifier get examGrade; + abstract ValueNotifier examGrade; /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ - MaskedTextController get examGradeTextFieldController; + abstract ValueNotifier examGradeTextFieldController; - ValueNotifier get freeEditable; + abstract ValueNotifier freeEditable; /* Nota final calculada */ double? get getCalculatedFinalGrade; diff --git a/lib/services_new/interfaces/controllers/horario_controller.dart b/lib/services_new/interfaces/controllers/horario_controller.dart new file mode 100644 index 0000000..11d4b7d --- /dev/null +++ b/lib/services_new/interfaces/controllers/horario_controller.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/horario.dart'; + +abstract class HorarioController { + abstract num daysCount; + abstract num periodsCount; + + abstract String startTime; + abstract Duration periodDuration; + abstract Duration periodGap; + + abstract ValueNotifier horario; + abstract ValueNotifier loadingHorario; + + abstract List usedColors; + abstract ValueNotifier zoom; + abstract ValueNotifier indicatorIsOpen; + abstract ValueNotifier isCenteredInCurrentPeriodAndDay; + + abstract TransformationController blockContentController; + abstract TransformationController daysHeaderController; + abstract TransformationController periodHeaderController; + abstract TransformationController cornerController; + + List get unusedColors; + + double get minutesFromStart; + + int? get indexOfCurrentDayStartingAtMonday; + + int? get indexOfCurrentPeriod; + + void init(); + + Future getHorarioData({ bool forceRefresh = false }); + + void moveViewportToCurrentPeriodAndDay(); + + void moveViewportToPeriodIndexAndDayIndex(int periodIndex, int dayIndex); + + void moveViewportTo(double x, double y); + + void setZoom(double zoom); + + void addAsignaturaAndSetColor(Asignatura asignatura, {Color? color}); + + Color? getColor(Asignatura asignatura); + + void setIndicatorIsOpen(bool isOpen); + +} \ No newline at end of file diff --git a/lib/services_new/service_manager.dart b/lib/services_new/service_manager.dart index 8573a6b..232eda1 100644 --- a/lib/services_new/service_manager.dart +++ b/lib/services_new/service_manager.dart @@ -1,7 +1,8 @@ import 'package:mi_utem/services_new/implementations/asignaturas_service.dart'; import 'package:mi_utem/services_new/implementations/auth_service.dart'; -import 'package:mi_utem/services_new/implementations/calculator_service.dart'; import 'package:mi_utem/services_new/implementations/carreras_service.dart'; +import 'package:mi_utem/services_new/implementations/controllers/calculator_controller.dart'; +import 'package:mi_utem/services_new/implementations/controllers/horario_controller.dart'; import 'package:mi_utem/services_new/implementations/credential_service.dart'; import 'package:mi_utem/services_new/implementations/grades_service.dart'; import 'package:mi_utem/services_new/implementations/horario_service.dart'; @@ -9,8 +10,9 @@ import 'package:mi_utem/services_new/implementations/noticias_service.dart'; import 'package:mi_utem/services_new/implementations/qr_pass_service.dart'; import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/services_new/interfaces/credential_service.dart'; import 'package:mi_utem/services_new/interfaces/grades_service.dart'; import 'package:mi_utem/services_new/interfaces/horario_service.dart'; @@ -21,11 +23,14 @@ import 'package:watch_it/watch_it.dart'; Future registerServices() async { di.registerLazySingleton(() => AsignaturasServiceImplementation()); di.registerLazySingleton(() => AuthServiceImplementation()); - di.registerLazySingleton(() => CalculatorServiceImplementation()); di.registerLazySingleton(() => CarrerasServiceImplementation()); di.registerLazySingleton(() => CredentialsServiceImplementation()); di.registerLazySingleton(() => GradesServiceImplementation()); di.registerLazySingleton(() => HorarioServiceImplementation()); di.registerLazySingleton(() => NoticiasServiceImplementation()); di.registerLazySingleton(() => QRPassServiceImplementation()); + + /* Controladores */ + di.registerLazySingleton(() => HorarioControllerImplementation()); + di.registerLazySingleton(() => CalculatorControllerImplementation()); } \ No newline at end of file diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index a44d01d..45a1f8c 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -24,118 +24,92 @@ class MainTheme { static double elevation = 0; static ThemeData get theme => ThemeData( - floatingActionButtonTheme: FloatingActionButtonThemeData( - backgroundColor: primaryColor, - disabledElevation: 0, - elevation: 0, - ), - tabBarTheme: TabBarTheme(indicatorSize: TabBarIndicatorSize.tab), - disabledColor: disabledColor, - textButtonTheme: TextButtonThemeData( - style: ButtonStyle( - elevation: MaterialStateProperty.all(0), - backgroundColor: MaterialStateProperty.resolveWith( - (states) { - const Set interactiveStates = { - MaterialState.disabled, - }; - return states.any(interactiveStates.contains) - ? disabledColor - : primaryColor; - }, - ), - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(vertical: 5, horizontal: 20), - ), - foregroundColor: MaterialStateProperty.all(Colors.white), - shape: MaterialStateProperty.all( - StadiumBorder(), - ), - ), - ), - outlinedButtonTheme: OutlinedButtonThemeData( - style: ButtonStyle( - elevation: MaterialStateProperty.all(0), - side: MaterialStateProperty.resolveWith( - (states) { - const Set interactiveStates = { - MaterialState.disabled, - }; - return states.any(interactiveStates.contains) - ? BorderSide(color: disabledColor!) - : BorderSide(color: primaryColor); - }, - ), - foregroundColor: MaterialStateProperty.resolveWith( - (states) { - const Set interactiveStates = { - MaterialState.disabled, - }; - return states.any(interactiveStates.contains) - ? disabledColor - : primaryColor; - }, - ), - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(vertical: 5, horizontal: 20)), - shape: MaterialStateProperty.all( - StadiumBorder( - side: BorderSide(width: 3), - ), - ), + useMaterial3: false, // Cuando podamos hay que migrar a material 3. + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: primaryColor, + disabledElevation: 0, + elevation: 0, + ), + tabBarTheme: TabBarTheme(indicatorSize: TabBarIndicatorSize.tab), + disabledColor: disabledColor, + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + elevation: MaterialStateProperty.all(0), + backgroundColor: MaterialStateProperty.resolveWith((states) => states.any({MaterialState.disabled}.contains) ? disabledColor : primaryColor), + padding: MaterialStateProperty.all( + EdgeInsets.symmetric(vertical: 5, horizontal: 20), + ), + foregroundColor: MaterialStateProperty.all(Colors.white), + shape: MaterialStateProperty.all( + StadiumBorder(), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: ButtonStyle( + elevation: MaterialStateProperty.all(0), + side: MaterialStateProperty.resolveWith((states) => states.any({MaterialState.disabled}.contains) ? BorderSide(color: disabledColor!) : BorderSide(color: primaryColor)), + foregroundColor: MaterialStateProperty.resolveWith((states) => states.any({MaterialState.disabled}.contains) ? disabledColor : primaryColor), + padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 5, horizontal: 20)), + shape: MaterialStateProperty.all( + StadiumBorder( + side: BorderSide(width: 3), ), ), - buttonTheme: ButtonThemeData( - shape: const StadiumBorder(), - padding: EdgeInsets.symmetric(vertical: 5, horizontal: 20), - textTheme: ButtonTextTheme.normal, - disabledColor: disabledColor, - ), - cardTheme: CardTheme( - elevation: 0, - clipBehavior: Clip.antiAlias, - color: Colors.white, - margin: EdgeInsets.all(10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - side: BorderSide( - color: Colors.grey, - width: 0.3, - style: BorderStyle.solid, - )), - ), - textTheme: textTheme, - appBarTheme: AppBarTheme( - color: primaryColor, - elevation: 0, - iconTheme: IconThemeData(color: Colors.white), - systemOverlayStyle: SystemUiOverlayStyle.light, - ), - inputDecorationTheme: InputDecorationTheme( - contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), - isDense: true, - hintStyle: TextStyle(color: Colors.grey[400]), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(15), - ), - ), - ), - primaryColor: primaryColor, - colorScheme: ColorScheme( - brightness: Brightness.light, - primary: primaryColor, - onPrimary: Colors.white, - secondary: primaryLightColor, - onSecondary: primaryDarkColor, - error: reprobadoColor, - onError: Colors.white, - background: lightGrey, - onBackground: darkGrey, - surface: lightGrey, - onSurface: darkGrey, - ), - ); + ), + ), + buttonTheme: ButtonThemeData( + shape: const StadiumBorder(), + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 20), + textTheme: ButtonTextTheme.normal, + disabledColor: disabledColor, + ), + cardTheme: CardTheme( + elevation: 0, + clipBehavior: Clip.antiAlias, + color: Colors.white, + margin: EdgeInsets.all(10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + side: BorderSide( + color: Colors.grey, + width: 0.3, + style: BorderStyle.solid, + ), + ), + ), + textTheme: textTheme, + appBarTheme: AppBarTheme( + color: primaryColor, + elevation: 0, + iconTheme: IconThemeData(color: Colors.white), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + inputDecorationTheme: InputDecorationTheme( + contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), + isDense: true, + hintStyle: TextStyle(color: Colors.grey[400]), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(15), + ), + ), + ), + primaryColor: primaryColor, + colorScheme: ColorScheme( + brightness: Brightness.light, + primary: primaryColor, + onPrimary: Colors.white, + secondary: primaryLightColor, + onSecondary: primaryDarkColor, + error: reprobadoColor, + onError: Colors.white, + background: lightGrey, + onBackground: darkGrey, + surface: lightGrey, + onSurface: darkGrey, + ), + ); static TextTheme get textTheme => TextTheme( displayLarge: TextStyle( diff --git a/lib/widgets/acerca/dialog/acerca_dialog.dart b/lib/widgets/acerca/dialog/acerca_dialog.dart index fab64e0..7133f66 100644 --- a/lib/widgets/acerca/dialog/acerca_dialog.dart +++ b/lib/widgets/acerca/dialog/acerca_dialog.dart @@ -6,9 +6,9 @@ import 'package:mi_utem/widgets/acerca/dialog/acerca_aplicacion_content.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog_action_button.dart'; class AcercaDialog extends StatefulWidget { - AcercaDialog({ - Key? key, - }) : super(key: key); + const AcercaDialog({ + super.key, + }); @override State createState() => _AcercaDialogState(); diff --git a/lib/widgets/bloque_ramo_card.dart b/lib/widgets/bloque_ramo_card.dart index c69b0b6..fd7d1ee 100644 --- a/lib/widgets/bloque_ramo_card.dart +++ b/lib/widgets/bloque_ramo_card.dart @@ -1,9 +1,10 @@ import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:watch_it/watch_it.dart'; class ClassBlockCard extends StatelessWidget { final BloqueHorario? block; @@ -28,16 +29,14 @@ class ClassBlockCard extends StatelessWidget { width: width, child: Padding( padding: EdgeInsets.all(internalMargin), - child: block?.asignatura == null - ? _EmptyBlock() - : _ClassBlock( - block: block!, - width: width, - height: height, - textColor: textColor, - onTap: _onTap, - onLongPress: _onLongPress, - ), + child: block?.asignatura == null ? _EmptyBlock() : _ClassBlock( + block: block!, + width: width, + height: height, + textColor: textColor, + onTap: _onTap, + onLongPress: _onLongPress, + ), ), ); } @@ -50,6 +49,8 @@ class ClassBlockCard extends StatelessWidget { "codigo": block.asignatura?.codigo, }, ); + + // TODO: Navegar a la asignatura } _onLongPress(BloqueHorario block) { @@ -60,6 +61,8 @@ class ClassBlockCard extends StatelessWidget { "codigo": block.asignatura?.codigo, }, ); + + // TODO: Acá podríamos agregar una vista "rápida" como: Hora, Sala y Profesor. } } @@ -94,7 +97,7 @@ class _ClassBlock extends StatelessWidget { final void Function(BloqueHorario)? onLongPress; const _ClassBlock({ - Key? key, + super.key, required this.block, required this.width, required this.height, @@ -102,13 +105,15 @@ class _ClassBlock extends StatelessWidget { this.color = Colors.teal, this.onTap, this.onLongPress, - }) : super(key: key); + }); @override Widget build(BuildContext context) { + final _controller = di.get(); + return Container( decoration: BoxDecoration( - color: HorarioController.to.getColor(block.asignatura!) ?? color, + color: _controller.getColor(block.asignatura!) ?? this.color, borderRadius: BorderRadius.circular(15), ), child: Material( diff --git a/lib/widgets/calculadora_notas/EditarNotasWidget.dart b/lib/widgets/calculadora_notas/EditarNotasWidget.dart deleted file mode 100644 index f964cfb..0000000 --- a/lib/widgets/calculadora_notas/EditarNotasWidget.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; -import 'package:mi_utem/widgets/calculadora_notas/ModoSimulacionWidget.dart'; -import 'package:mi_utem/widgets/calculadora_notas/NotasCalculadoraWidget.dart'; -import 'package:watch_it/watch_it.dart'; - -class EditarNotasWidget extends StatelessWidget { - const EditarNotasWidget({ - super.key, - }); - - @override - Widget build(BuildContext context) => Card( - child: Stack( - alignment: Alignment.center, - children: [ - ModoSimulacionWidget(), - Container( - padding: EdgeInsets.all(20), - child: Column( - children: [ - const NotasCalculadoraWidget(), - const SizedBox(height: 16), - TextButton( - onPressed: _addGrade, - child: const Text("Agregar nota"), - ), - ], - ), - ), - ], - ), - ); - - void _addGrade() { - AnalyticsService.logEvent("calculator_add_grade"); - di.get().addGrade(IEvaluacion( - nota: null, - porcentaje: null, - )); - } -} diff --git a/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart b/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart deleted file mode 100644 index 5193264..0000000 --- a/lib/widgets/calculadora_notas/NotasCalculadoraWidget.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; -import 'package:mi_utem/widgets/nota_list_item.dart'; -import 'package:watch_it/watch_it.dart'; - -class NotasCalculadoraWidget extends StatelessWidget { - - const NotasCalculadoraWidget({ - super.key, - }); - - - @override - Widget build(BuildContext context) { - final _calculatorService = di.get(); - final _partialGrades = watchValue((CalculatorService _service) => _service.partialGrades); - final _gradeTextFieldControllers = watchValue((CalculatorService _service) => _service.gradeTextFieldControllers); - final _percentageTextFieldControllers = watchValue((CalculatorService _service) => _service.percentageTextFieldControllers); - - return ListView.separated( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - separatorBuilder: (context, index) => const SizedBox(height: 10), - itemBuilder: (context, idx) => NotaListItem( - evaluacion: IEvaluacion.fromRemote(_partialGrades[idx]), - editable: true, - gradeController: _gradeTextFieldControllers[idx], - percentageController: _percentageTextFieldControllers[idx], - onChanged: (evaluacion) => _calculatorService.updateGradeAt(idx, evaluacion), - onDelete: () { - AnalyticsService.logEvent("calculator_delete_grade"); - _calculatorService.removeGradeAt(idx); - }, - ), - itemCount: _partialGrades.length, - ); - } -} diff --git a/lib/widgets/calculadora_notas/DisplayNotasWidget.dart b/lib/widgets/calculadora_notas/display_notas_widget.dart similarity index 78% rename from lib/widgets/calculadora_notas/DisplayNotasWidget.dart rename to lib/widgets/calculadora_notas/display_notas_widget.dart index b2ca66c..5099f1b 100644 --- a/lib/widgets/calculadora_notas/DisplayNotasWidget.dart +++ b/lib/widgets/calculadora_notas/display_notas_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/calculadora_notas/ModoSimulacionWidget.dart'; -import 'package:mi_utem/widgets/calculadora_notas/NotaExamenDisplayWidget.dart'; -import 'package:mi_utem/widgets/calculadora_notas/NotaFinalDisplayWidget.dart'; -import 'package:mi_utem/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/modo_simulacion_widget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/nota_examen_display_widget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/nota_final_display_widget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/nota_presentacion_display_widget.dart'; class DisplayNotasWidget extends StatelessWidget { diff --git a/lib/widgets/calculadora_notas/editar_notas_widget.dart b/lib/widgets/calculadora_notas/editar_notas_widget.dart new file mode 100644 index 0000000..9c7d132 --- /dev/null +++ b/lib/widgets/calculadora_notas/editar_notas_widget.dart @@ -0,0 +1,56 @@ +import 'package:extended_masked_text/extended_masked_text.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/widgets/calculadora_notas/modo_simulacion_widget.dart'; +import 'package:mi_utem/widgets/calculadora_notas/notas_calculadora_widget.dart'; +import 'package:watch_it/watch_it.dart'; + +class EditarNotasWidget extends StatelessWidget with WatchItMixin { + final List partialGrades; + final List gradeTextFieldControllers; + final List percentageTextFieldControllers; + final Function() onAddGrade; + final Function(int) onRemoveGrade; + + const EditarNotasWidget({ + super.key, + required this.partialGrades, + required this.gradeTextFieldControllers, + required this.percentageTextFieldControllers, + required this.onAddGrade, + required this.onRemoveGrade, + }); + + @override + Widget build(BuildContext context) => Card( + child: Stack( + alignment: Alignment.center, + children: [ + ModoSimulacionWidget(), + Container( + padding: EdgeInsets.all(20), + child: Column( + children: [ + NotasCalculadoraWidget( + partialGrades: partialGrades, + gradeTextFieldControllers: gradeTextFieldControllers, + percentageTextFieldControllers: percentageTextFieldControllers, + onRemoveGrade: onRemoveGrade, + ), + const SizedBox(height: 16), + TextButton( + onPressed: () { + AnalyticsService.logEvent("calculator_add_grade"); + onAddGrade(); + }, + child: const Text("Agregar nota"), + ), + ], + ), + ), + ], + ), + ); + +} diff --git a/lib/widgets/calculadora_notas/ModoSimulacionWidget.dart b/lib/widgets/calculadora_notas/modo_simulacion_widget.dart similarity index 100% rename from lib/widgets/calculadora_notas/ModoSimulacionWidget.dart rename to lib/widgets/calculadora_notas/modo_simulacion_widget.dart diff --git a/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart similarity index 57% rename from lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart rename to lib/widgets/calculadora_notas/nota_examen_display_widget.dart index c339f19..55958c7 100644 --- a/lib/widgets/calculadora_notas/NotaExamenDisplayWidget.dart +++ b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:watch_it/watch_it.dart'; -class NotaExamenDisplayWidget extends StatelessWidget { +class NotaExamenDisplayWidget extends StatelessWidget with WatchItMixin{ const NotaExamenDisplayWidget({ super.key, @@ -11,7 +11,8 @@ class NotaExamenDisplayWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final _calculatorService = di.get(); + final _calculatorController = di.get(); + final examGradeTextFieldController = watchValue((CalculatorController controller) => controller.examGradeTextFieldController); return Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -23,15 +24,13 @@ class NotaExamenDisplayWidget extends StatelessWidget { width: 80, margin: const EdgeInsets.only(left: 15), child: TextField( - controller: _calculatorService.examGradeTextFieldController, + controller: examGradeTextFieldController, textAlign: TextAlign.center, - onChanged: (String value) { - _calculatorService.examGrade.value = double.tryParse(value.replaceAll(",", ".")); - }, - enabled: _calculatorService.canTakeExam, + onChanged: (String value) => _calculatorController.setExamGrade(double.tryParse(value.replaceAll(",", "."))), + enabled: _calculatorController.canTakeExam, decoration: InputDecoration( - hintText: _calculatorService.getMinimumRequiredExamGrade?.toStringAsFixed(1) ?? "", - filled: !_calculatorService.canTakeExam, + hintText: _calculatorController.getMinimumRequiredExamGrade?.toStringAsFixed(1) ?? "", + filled: !_calculatorController.canTakeExam, fillColor: Colors.grey.withOpacity(0.2), disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( diff --git a/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart b/lib/widgets/calculadora_notas/nota_final_display_widget.dart similarity index 68% rename from lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart rename to lib/widgets/calculadora_notas/nota_final_display_widget.dart index c0f09a7..6b70720 100644 --- a/lib/widgets/calculadora_notas/NotaFinalDisplayWidget.dart +++ b/lib/widgets/calculadora_notas/nota_final_display_widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:watch_it/watch_it.dart'; class NotaFinalDisplayWidget extends StatelessWidget { @@ -11,7 +11,7 @@ class NotaFinalDisplayWidget extends StatelessWidget { @override Widget build(BuildContext context) => Column( children: [ - Text(di.get().getCalculatedFinalGrade?.toStringAsFixed(1) ?? "--", + Text(di.get().getCalculatedFinalGrade?.toStringAsFixed(1) ?? "--", style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, diff --git a/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart similarity index 88% rename from lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart rename to lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart index 5c488cf..8afbdfe 100644 --- a/lib/widgets/calculadora_notas/NotaPresentacionDisplayWidget.dart +++ b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:watch_it/watch_it.dart'; @@ -10,7 +10,7 @@ class NotaPresentacionDisplayWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final _calculatorService = di.get(); + final _calculatorService = di.get(); return Row( children: [ const Text("Pres.", diff --git a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart new file mode 100644 index 0000000..dce2bc3 --- /dev/null +++ b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart @@ -0,0 +1,42 @@ +import 'package:extended_masked_text/extended_masked_text.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:mi_utem/widgets/nota_list_item.dart'; +import 'package:watch_it/watch_it.dart'; + +class NotasCalculadoraWidget extends StatelessWidget with WatchItMixin { + + final List partialGrades; + final List gradeTextFieldControllers; + final List percentageTextFieldControllers; + final Function(int) onRemoveGrade; + + const NotasCalculadoraWidget({ + super.key, + required this.partialGrades, + required this.gradeTextFieldControllers, + required this.percentageTextFieldControllers, + required this.onRemoveGrade, + }); + + @override + Widget build(BuildContext context) => ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, idx) => NotaListItem( + evaluacion: IEvaluacion.fromRemote(partialGrades[idx]), + editable: true, + gradeController: gradeTextFieldControllers[idx], + percentageController: percentageTextFieldControllers[idx], + onChanged: (evaluacion) => di.get().updateGradeAt(idx, evaluacion), + onDelete: () { + AnalyticsService.logEvent("calculator_delete_grade"); + onRemoveGrade(idx); + }, + ), + itemCount: partialGrades.length, + ); +} diff --git a/lib/widgets/dialogs/monkey_error_dialog.dart b/lib/widgets/dialogs/monkey_error_dialog.dart index 0108194..7c05f38 100644 --- a/lib/widgets/dialogs/monkey_error_dialog.dart +++ b/lib/widgets/dialogs/monkey_error_dialog.dart @@ -4,26 +4,24 @@ import 'package:mi_utem/widgets/error_dialog.dart'; class MonkeyErrorDialog extends StatelessWidget { const MonkeyErrorDialog({ - Key? key, - }) : super(key: key); + super.key, + }); @override - Widget build(BuildContext context) { - return ErrorDialog( - contenido: Container( - height: 100, - child: FlareActor( - "assets/animations/monito.flr", - alignment: Alignment.center, - fit: BoxFit.contain, - animation: "rascarse", - ), + Widget build(BuildContext context) => ErrorDialog( + contenido: Container( + height: 100, + child: FlareActor( + "assets/animations/monito.flr", + alignment: Alignment.center, + fit: BoxFit.contain, + animation: "rascarse", ), - mensaje: Text( - "Ops, parece que metimos la pata. Sólo queda esperar e intentarlo más tarde.", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 20), - ), - ); - } + ), + mensaje: Text( + "Ops, parece que metimos la pata. Sólo queda esperar e intentarlo más tarde.", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20), + ), + ); } diff --git a/lib/widgets/error_dialog.dart b/lib/widgets/error_dialog.dart index 7c7331d..c3f378b 100644 --- a/lib/widgets/error_dialog.dart +++ b/lib/widgets/error_dialog.dart @@ -1,49 +1,51 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - final _formKey = GlobalKey(); class ErrorDialog extends StatelessWidget { final Widget? contenido; final Widget mensaje; - ErrorDialog({ + + const ErrorDialog({ this.contenido, required this.mensaje, }); @override - Widget build(BuildContext context) { - return Dialog( - elevation: 0, - backgroundColor: Colors.transparent, - child: Container( - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), + Widget build(BuildContext context) => Dialog( + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + child: Padding( + padding: EdgeInsets.all(30), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container(child: this.mensaje), + if (this.contenido != null) Padding( + padding: EdgeInsets.all(20), + child: this.contenido, + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: TextButton( + onPressed: () => Navigator.pop(context), + child: Text("Entendido 😥"), + ), ), - child: Padding( - padding: EdgeInsets.all(30), - child: Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Container(child: this.mensaje), - if (this.contenido != null) - Padding( - padding: EdgeInsets.all(20), - child: this.contenido), - Padding( - padding: EdgeInsets.only(top: 20), - child: TextButton( - onPressed: () async { - Get.back(); - }, - child: Text("Entendido 😥"), - )) - ])))))); - } + ], + ), + ), + ), + ), + ), + ); } diff --git a/lib/widgets/loading_dialog.dart b/lib/widgets/loading_dialog.dart index 85231cc..f8e13fa 100644 --- a/lib/widgets/loading_dialog.dart +++ b/lib/widgets/loading_dialog.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; - import 'package:mi_utem/widgets/loading_indicator.dart'; class LoadingDialog extends StatelessWidget { @override - Widget build(BuildContext context) { - return WillPopScope( + Widget build(BuildContext context) => WillPopScope( onWillPop: () async => false, child: LoadingIndicator( color: Colors.white, ) - ); - } + ); } + +void showLoadingDialog(BuildContext context) => showDialog(context: context, builder: (ctx) => LoadingDialog(), barrierDismissible: false); \ No newline at end of file diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index 531377b..8670327 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/helpers/snackbars.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services_new/interfaces/auth_service.dart' as NewAuthService; import 'package:mi_utem/services_new/interfaces/credential_service.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart' as NewAuthService; +import 'package:mi_utem/widgets/snackbar.dart'; import 'package:watch_it/watch_it.dart'; @@ -42,19 +41,22 @@ class _LoginButtonState extends State { @override Widget build(BuildContext context) => TextButton( - onPressed: () => _login(), + onPressed: () => _login(context), child: Text("Iniciar"), ); - Future _login() async { + Future _login(BuildContext context) async { final correo = widget._correoController.text; final contrasenia = widget._contraseniaController.text; if (correo == "error@utem.cl") { - Get.dialog(MonkeyErrorDialog()); + showDialog(context: context, builder: (ctx) => MonkeyErrorDialog()); return; } else if (correo == "test@utem.cl" && contrasenia == "test") { - showDefaultSnackbar("Error", "Usuario o contraseña incorrecta"); + showTextSnackbar(context, + title: "Error", + message: "Usuario o contraseña incorrecta", + ); return; } @@ -62,7 +64,7 @@ class _LoginButtonState extends State { return; } - Get.dialog(LoadingDialog(), barrierDismissible: false); + showLoadingDialog(context); try { await _credentialsService.setCredentials(Credentials( @@ -71,7 +73,10 @@ class _LoginButtonState extends State { )); if(!(await _credentialsService.hasCredentials())) { - showDefaultSnackbar("Error", "Ha ocurrido un error al guardar tus claves. Intenta más tarde."); + showTextSnackbar(context, + title: "Error", + message: "Ha ocurrido un error al guardar tus claves. Intenta más tarde.", + ); return; } @@ -81,33 +86,46 @@ class _LoginButtonState extends State { final isFirstTime = await _authService.isFirstTime(); final user = await _authService.getUser(); if(user == null) { - Get.back(); - showDefaultSnackbar("Error", "Ha ocurrido un error desconocido. Por favor intenta más tarde."); + Navigator.pop(context); + showTextSnackbar(context, + title: "Error", + message: "Ha ocurrido un error desconocido. Por favor intenta más tarde.", + ); return; } AnalyticsService.logEvent('login'); AnalyticsService.setUser(user); - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); + Navigator.of(context).popUntil((route) => route.isFirst); // Esto elimina todas las pantallas anteriores + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); // Y esto reemplaza la pantalla actual por la nueva, cosa de que no pueda "volver" al login a menos que cierre la sesión. if(isFirstTime) { - Get.dialog(AcercaDialog()); + showDialog(context: context, builder: (ctx) => AcercaDialog()); } } catch (e) { logger.e(e); - Get.back(); - showDefaultSnackbar("Error", "Ha ocurrido un error desconocido. Por favor intenta más tarde."); + Navigator.pop(context); + showTextSnackbar(context, + title: "Error", + message: "Ha ocurrido un error desconocido. Por favor intenta más tarde.", + ); } return; } on CustomException catch (e) { logger.e(e); - Get.back(); - showDefaultSnackbar("Error", e.message); + Navigator.pop(context); + showTextSnackbar(context, + title: "Error", + message: e.message, + ); } catch (e) { logger.e(e); - Get.back(); - showDefaultSnackbar("Error", "Ha ocurrido un error desconocido. Por favor intenta más tarde."); + Navigator.pop(context); + showTextSnackbar(context, + title: "Error", + message: "Ha ocurrido un error desconocido. Por favor intenta más tarde.", + ); } } } \ No newline at end of file diff --git a/lib/widgets/nota_list_item.dart b/lib/widgets/nota_list_item.dart index 8d0576f..5b8a4a3 100644 --- a/lib/widgets/nota_list_item.dart +++ b/lib/widgets/nota_list_item.dart @@ -2,7 +2,7 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/services_new/interfaces/calculator_service.dart'; +import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:watch_it/watch_it.dart'; @@ -24,9 +24,9 @@ class NotaListItem extends StatelessWidget { this.onDelete, }) : super(key: key); - String get _suggestedGrade => di.get().getSuggestedGrade?.toStringAsFixed(1) ?? "0.0"; + String get _suggestedGrade => di.get().getSuggestedGrade?.toStringAsFixed(1) ?? "0.0"; - String? get _suggestedPercentage => di.get().getSuggestedPercentage?.toStringAsFixed(0); + String? get _suggestedPercentage => di.get().getSuggestedPercentage?.toStringAsFixed(0); @override Widget build(BuildContext context) { diff --git a/lib/widgets/permisos_section.dart b/lib/widgets/permisos_section.dart index ec511d6..c881167 100644 --- a/lib/widgets/permisos_section.dart +++ b/lib/widgets/permisos_section.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/qr_passes_controller.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_covid.dart'; import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; @@ -22,8 +21,7 @@ class PermisosCovidSection extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ - Text( - "Permisos activos".toUpperCase(), + Text("Permisos activos".toUpperCase(), style: Get.textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, ), diff --git a/lib/widgets/snackbar.dart b/lib/widgets/snackbar.dart new file mode 100644 index 0000000..97aa337 --- /dev/null +++ b/lib/widgets/snackbar.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/themes/theme.dart'; + +void showErrorSnackbar(BuildContext context, String message) => showTextSnackbar(context, title: "Error", message: message, backgroundColor: Colors.red); + +void showTextSnackbar(BuildContext context, { + required String title, + required String message, + Color? backgroundColor, + Color? textColor, + Duration? duration, +}) => showSnackbar(context, + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: textColor ?? Colors.white)), + Text(message, style: TextStyle(color: textColor ?? Colors.white)), + ], + ), + backgroundColor: backgroundColor, + duration: duration, +); + +void showSnackbar(BuildContext context, { + required Widget content, + Color? backgroundColor, + Duration? duration, +}) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: content, + backgroundColor: backgroundColor ?? MainTheme.primaryColor, + behavior: SnackBarBehavior.floating, + duration: duration ?? const Duration(seconds: 5), + )); +} \ No newline at end of file From 2ed711c30c1492ccdb0b2af90d205968dbe7b29b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 20 Mar 2024 17:38:16 -0300 Subject: [PATCH 024/194] patch: arreglos a los widgets de asignaturas, notas, etc. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/controllers/notification_controller.dart | 2 +- .../asignatura/asignatura_detalle_screen.dart | 128 ++++++------------ .../asignatura/asignatura_notas_tab.dart | 101 +++++--------- .../implementations/asignaturas_service.dart | 27 +--- .../controllers/calculator_controller.dart | 1 + .../lista/asignatura_list_tile.dart | 2 +- 6 files changed, 84 insertions(+), 177 deletions(-) diff --git a/lib/controllers/notification_controller.dart b/lib/controllers/notification_controller.dart index 4f50951..2c6eaeb 100644 --- a/lib/controllers/notification_controller.dart +++ b/lib/controllers/notification_controller.dart @@ -50,7 +50,7 @@ class NotificationController { if (asignaturaJsonString != null) { AnalyticsService.logEvent('notification_tap_grade_change'); final asignatura = Asignatura.fromJson(jsonDecode(asignaturaJsonString)); - navigatorKey.currentState?.push(MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignaturaId: asignatura.id))); + navigatorKey.currentState?.push(MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignatura: asignatura))); } } } diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 2db55f5..69991a7 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -1,18 +1,14 @@ import 'package:flutter/material.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/screens/asignatura/asignatura_estudiantes_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:watch_it/watch_it.dart'; class _ITabs { @@ -28,97 +24,63 @@ class _ITabs { } class AsignaturaDetalleScreen extends StatelessWidget { - final String? asignaturaId; + final Asignatura? asignatura; AsignaturaDetalleScreen({ super.key, - required this.asignaturaId, + required this.asignatura, }); - List<_ITabs> _getTabs(Asignatura asignatura) => [ - _ITabs( - label: "Resumen", - child: AsignaturaResumenTab(asignatura: asignatura), - ), - _ITabs( - label: "Notas", - child: AsignaturaNotasTab(asignaturaId: asignatura.id!), - initial: true, - ), - if (asignatura.estudiantes != null && - asignatura.estudiantes!.length > 0) - _ITabs( - label: "Estudiantes", - child: AsignaturaEstudiantesTab(asignatura: asignatura), - ), - ]; - bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; - int _getInitialIndex(Asignatura asignatura) { - final index = _getTabs(asignatura).indexWhere((tab) => tab.initial); - return index == -1 ? 0 : index; - } - @override Widget build(BuildContext context) { ReviewService.addScreen("AsignaturaScreen"); - return FutureBuilder( - future: di.get().getDetalleAsignatura(asignaturaId), - builder: (context, snapshot) { - if(snapshot.connectionState == ConnectionState.waiting) { - return Scaffold( - appBar: CustomAppBar(), - body: Center( - child: LoadingIndicator(), - ), - ); - } else if (snapshot.hasError) { - final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "No sabemos que pasó, pero no pudimos cargar la asignatura."; - return CustomErrorWidget( - title: "Ocurrió un error", - error: error, - ); - } else if (snapshot.hasData) { - final asignatura = snapshot.data; - return DefaultTabController( - initialIndex: asignatura != null ? _getInitialIndex(asignatura) : 0, - length: asignatura != null ? _getTabs(asignatura).length : 1, - child: Scaffold( - appBar: CustomAppBar( - title: Text(asignatura?.nombre ?? "Asigntura sin nombre"), - actions: _mostrarCalculadora ? [ - IconButton( - icon: Icon(Mdi.calculator), - tooltip: "Calculadora", - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())); - if (asignatura?.grades != null) { - di.get().updateWithGrades(asignatura!.grades!); - } - }, - ), - ] : [], - bottom: asignatura != null ? TabBar( - indicatorColor: Colors.white.withOpacity(0.8), - tabs: _getTabs(asignatura) - .map((tab) => Tab(text: tab.label)) - .toList(), - ) : null, - ), - body: asignatura != null ? TabBarView( - children: _getTabs(asignatura).map((tab) => tab.child).toList(), - ) : Container(), - ), - ); - } + final tabs = [ + _ITabs( + label: "Resumen", + child: AsignaturaResumenTab(asignatura: asignatura), + ), + _ITabs( + label: "Notas", + child: AsignaturaNotasTab(asignatura: asignatura), + initial: true, + ), + if (asignatura?.estudiantes != null && (asignatura?.estudiantes?.length ?? 0) > 0) _ITabs( + label: "Estudiantes", + child: AsignaturaEstudiantesTab(asignatura: asignatura), + ), + ]; + final index = tabs.indexWhere((tab) => tab.initial); - return CustomErrorWidget( - title: "Ocurrió un error", - error: "No sabemos que pasó, pero no pudimos cargar la asignatura.", - ); - } + return DefaultTabController( + initialIndex: asignatura != null ? (index == -1 ? 0 : index) : 0, + length: asignatura != null ? tabs.length : 1, + child: Scaffold( + appBar: CustomAppBar( + title: Text(asignatura?.nombre ?? "Asignatura sin nombre"), + actions: _mostrarCalculadora ? [ + IconButton( + icon: Icon(Mdi.calculator), + tooltip: "Calculadora", + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())); + if (asignatura?.grades != null) { + di.get().updateWithGrades(asignatura!.grades!); + } + }, + ), + ] : [], + bottom: asignatura != null ? TabBar( + indicatorColor: Colors.white.withOpacity(0.8), + tabs: tabs.map((tab) => Tab(text: tab.label)).toList(), + ) : null, + ), + body: asignatura != null ? TabBarView( + children: tabs.map((tab) => tab.child).toList(), + ) : Container(), + ), ); } diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index e6c2496..5312b4e 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,21 +1,17 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; -import 'package:watch_it/watch_it.dart'; class AsignaturaNotasTab extends StatefulWidget { - final String asignaturaId; + final Asignatura? asignatura; const AsignaturaNotasTab({ super.key, - required this.asignaturaId, + required this.asignatura, }); @override @@ -24,71 +20,38 @@ class AsignaturaNotasTab extends StatefulWidget { class _AsignaturaNotasTabState extends State { - Future _future = Future.value(null); - - @override - void initState() { - _future = di.get().getDetalleAsignatura(widget.asignaturaId, forceRefresh: true); - super.initState(); - } - @override Widget build(BuildContext context) => PullToRefresh( - onRefresh: () async { - setState(() { - _future = di.get().getDetalleAsignatura(widget.asignaturaId, forceRefresh: true); - }); - }, - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - final asignatura = snapshot.data; - if (snapshot.connectionState == ConnectionState.waiting) { - return LoadingIndicator(); - } else if (snapshot.hasError) { - final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "No sabemos que pasó, pero no pudimos cargar tus notas."; - return CustomErrorWidget( - title: "Ocurrió un error al cargar las notas", - error: error, - ); - } else if (snapshot.hasData) { - return ListView( - physics: AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.all(10), - children: [ - NotasDisplayWidget( - notaFinal: asignatura?.grades?.notaFinal, - notaExamen: asignatura?.grades?.notaExamen, - notaPresentacion: asignatura?.grades?.notaPresentacion, - estado: asignatura?.estado, - colorPorEstado: asignatura?.colorPorEstado, - ), - Card( - child: Container( - padding: EdgeInsets.all(20), - child: asignatura?.grades?.notasParciales.isNotEmpty == true ? ListView.builder( - shrinkWrap: true, - physics: ClampingScrollPhysics(), - itemBuilder: (context, i) { - REvaluacion evaluacion = asignatura!.grades!.notasParciales[i]; - return NotaListItem(evaluacion: IEvaluacion.fromRemote(evaluacion)); - }, - itemCount: asignatura!.grades!.notasParciales.length, - ) : CustomErrorWidget( - emoji: "🤔", - title: "Parece que aún no hay notas ni ponderadores", - ), - ), - ), - ], - ); - } - - return CustomErrorWidget( - title: "Ocurrió un error al cargar las notas", - error: '', - ); - }, + onRefresh: () async => setState(() {}), + child: ListView( + physics: AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.all(10), + children: [ + NotasDisplayWidget( + notaFinal: widget.asignatura?.grades?.notaFinal, + notaExamen: widget.asignatura?.grades?.notaExamen, + notaPresentacion: widget.asignatura?.grades?.notaPresentacion, + estado: widget.asignatura?.estado, + colorPorEstado: widget.asignatura?.colorPorEstado, + ), + Card( + child: Container( + padding: EdgeInsets.all(20), + child: widget.asignatura?.grades?.notasParciales.isNotEmpty == true ? ListView.builder( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemBuilder: (context, i) { + REvaluacion evaluacion = widget.asignatura!.grades!.notasParciales[i]; + return NotaListItem(evaluacion: IEvaluacion.fromRemote(evaluacion)); + }, + itemCount: widget.asignatura!.grades!.notasParciales.length, + ) : CustomErrorWidget( + emoji: "🤔", + title: "Parece que aún no hay notas ni ponderadores", + ), + ), + ), + ], ), ); } diff --git a/lib/services_new/implementations/asignaturas_service.dart b/lib/services_new/implementations/asignaturas_service.dart index 8fe77df..78976e3 100644 --- a/lib/services_new/implementations/asignaturas_service.dart +++ b/lib/services_new/implementations/asignaturas_service.dart @@ -2,11 +2,10 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:watch_it/watch_it.dart'; class AsignaturasServiceImplementation implements AsignaturasService { @@ -16,16 +15,7 @@ class AsignaturasServiceImplementation implements AsignaturasService { return null; } - final user = await di.get().getUser(); - if(user == null) { - return null; - } - - final response = await authClient.get(Uri.parse('$apiUrl/v1/carreras/$carreraId/asignaturas'), headers: { - 'Authorization': 'Bearer ${user.token}', - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - }); + final response = await authClient.get(Uri.parse('$apiUrl/v1/carreras/$carreraId/asignaturas')); final json = jsonDecode(response.body); @@ -42,20 +32,11 @@ class AsignaturasServiceImplementation implements AsignaturasService { @override Future getDetalleAsignatura(String? asignaturaId, {bool forceRefresh = false}) async { - final user = await di.get().getUser(); - if(user == null) { - return null; - } - - final response = await authClient.get(Uri.parse('$apiUrl/v1/asignaturas/$asignaturaId'), headers: { - 'Authorization': 'Bearer ${user.token}', - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - }); + final response = await authClient.get(Uri.parse('$apiUrl/v1/asignaturas/$asignaturaId')); final json = jsonDecode(response.body); - if(response.statusCode != 200) { + logger.e("Error al obtener detalle de asignatura: ${response.reasonPhrase}", json); if(json is Map && json.containsKey("error")) { throw CustomException.fromJson(json as Map); } diff --git a/lib/services_new/implementations/controllers/calculator_controller.dart b/lib/services_new/implementations/controllers/calculator_controller.dart index ce095ab..6bcf99b 100644 --- a/lib/services_new/implementations/controllers/calculator_controller.dart +++ b/lib/services_new/implementations/controllers/calculator_controller.dart @@ -224,6 +224,7 @@ class CalculatorControllerImplementation implements CalculatorController { text: grade.nota?.toStringAsFixed(1) ?? "", ), ]; + logger.d("A grade has been added"); } @override diff --git a/lib/widgets/asignatura/lista/asignatura_list_tile.dart b/lib/widgets/asignatura/lista/asignatura_list_tile.dart index 1a77b39..ee987a1 100644 --- a/lib/widgets/asignatura/lista/asignatura_list_tile.dart +++ b/lib/widgets/asignatura/lista/asignatura_list_tile.dart @@ -17,7 +17,7 @@ class AsignaturaListTile extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Card( child: InkWell( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignaturaId: asignatura.id))), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignatura: asignatura))), child: Container( padding: const EdgeInsets.all(20), width: double.infinity, From 24f621d82734287f27ed1a1f19c049d0183d09db Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 21 Mar 2024 18:01:51 -0300 Subject: [PATCH 025/194] =?UTF-8?q?patch:=20arreglos=20generales=20*=20Se?= =?UTF-8?q?=20arregla=20calculadora=20de=20notas=20*=20Se=20elimina=20cach?= =?UTF-8?q?ed=20client=20(a=C3=BAn=20falta=20crear=20un=20cliente=20nuevo)?= =?UTF-8?q?=20*=20Se=20agrega=20modelo=20de=20navigation=5Ftab.dart=20*=20?= =?UTF-8?q?Se=20repara=20pantalla=20de=20horario=20*=20Se=20repara=20panta?= =?UTF-8?q?llas=20de=20asignatura=20(y=20separa=20por=20categor=C3=ADa)=20?= =?UTF-8?q?*=20Se=20elimina=20algo=20de=20c=C3=B3digo=20innecesario=20*=20?= =?UTF-8?q?Se=20agrega=20string=5Futils.dart=20*=20Se=20mejora=20el=20sopo?= =?UTF-8?q?rte=20para=20la=20vista=20horizontal=20del=20dispositivo=20*=20?= =?UTF-8?q?Se=20agrega=20un=20paquete=20para=20listas=20din=C3=A1micas=20(?= =?UTF-8?q?que=20actualizan=20vistas=20al=20cambiarse)=20*=20Otros=20arreg?= =?UTF-8?q?los=20de=20calidad=20de=20vida?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/config/http_clients.dart | 65 +--- .../asignaturas/detalles/navigation_tab.dart | 13 + .../asignatura/asignatura_detalle_screen.dart | 40 +- .../asignatura/asignatura_notas_tab.dart | 39 +- .../asignatura/asignatura_resumen_tab.dart | 268 ++++++++------ lib/screens/horario/horario_screen.dart | 5 +- .../widgets/horario_main_scroller.dart | 19 +- .../implementations/auth_service.dart | 25 +- .../implementations/carreras_service.dart | 8 - .../controllers/calculator_controller.dart | 344 +++++++++++------- .../controllers/horario_controller.dart | 50 ++- .../implementations/qr_pass_service.dart | 30 +- lib/services_new/interfaces/auth_service.dart | 2 +- .../controllers/calculator_controller.dart | 38 +- lib/utils/string_utils.dart | 3 + .../lista/asignatura_list_tile.dart | 12 +- .../notas_tab/nota_final_display.dart | 52 ++- .../asignatura/notas_tab/notas_display.dart | 119 +++--- .../nota_examen_display_widget.dart | 40 +- .../nota_final_display_widget.dart | 28 +- .../nota_presentacion_display_widget.dart | 6 +- lib/widgets/field_list_tile.dart | 12 +- .../login_screen/formulario_credenciales.dart | 4 +- lib/widgets/nota_list_item.dart | 78 +++- .../noticias/noticias_carrusel_widget.dart | 13 +- lib/widgets/permisos_section.dart | 90 +++-- pubspec.lock | 8 + pubspec.yaml | 1 + 28 files changed, 771 insertions(+), 641 deletions(-) create mode 100644 lib/models/asignaturas/detalles/navigation_tab.dart create mode 100644 lib/utils/string_utils.dart diff --git a/lib/config/http_clients.dart b/lib/config/http_clients.dart index 8b5f52f..51a0369 100644 --- a/lib/config/http_clients.dart +++ b/lib/config/http_clients.dart @@ -1,10 +1,6 @@ -import 'dart:convert'; -import 'package:crypto/crypto.dart'; -import 'package:http/http.dart' as http; import 'package:http_interceptor/http_interceptor.dart'; import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:watch_it/watch_it.dart'; @@ -52,7 +48,7 @@ class AuthInterceptor implements InterceptorContract { class ExpiredTokenRetryPolicy extends RetryPolicy { @override - int get maxRetryAttempts => 3; + int get maxRetryAttempts => 6; @override Future shouldAttemptRetryOnResponse(ResponseData response) async { @@ -62,16 +58,20 @@ class ExpiredTokenRetryPolicy extends RetryPolicy { logger.d("[ExpiredTokenRetryPolicy]: ${response.request?.method.name.toUpperCase()} ${response.request?.url} Recibió un 401, refrescando token..."); final _authService = di.get(); - final currentUser = await _authService.getUser(); - await _authService.isLoggedIn(); - final user = await _authService.getUser(); - final token = user?.token; + final currentToken = (await _authService.getUser())?.token; + if(currentToken == null) { + await _authService.login(); + } else { + await _authService.isLoggedIn(forceRefresh: true); + } + + final token = (await _authService.getUser())?.token; if(token == null) { - logger.d("[ExpiredTokenRetryPolicy]: No se pudo refrescar el token, redirigiendo a login"); + logger.d("[ExpiredTokenRetryPolicy]: No se pudo refrescar el token!"); return false; } - return currentUser?.token == token; + return true; } } @@ -96,45 +96,4 @@ class LoggerInterceptor implements InterceptorContract { logger.d("[LoggerInterceptor#response]: ${data.statusCode} ${data.url}$diff"); return data; } -} - -class CachedClient extends http.BaseClient { - - final _client = http.Client(); - final _cache = >{}; - - @override - Future send(http.BaseRequest request) async { - logger.d("[HttpClient]: ${request.method.toUpperCase()} ${request.url}"); - final useCache = request.headers.containsKey('X-MiUTEM-Use-Cache'); - final cacheKey = sha1.convert(utf8.encode("${request.method}:${request.url}:${request.headers}:${request.contentLength}:${request.followRedirects}:${request.maxRedirects}:${request.persistentConnection}")).toString(); - if (useCache) { - logger.d("[HttpClient]: Usando cache para ${request.method.toUpperCase()} ${request.url}"); - request.headers.remove('X-MiUTEM-Use-Cache'); - final ttl = request.headers.containsKey('X-MiUTEM-Cache-TTL') ? int.parse(request.headers['X-MiUTEM-Cache-TTL']!) : 300; // 5 minutos por defecto (en segundos) - request.headers.remove('X-MiUTEM-Cache-TTL'); - if (_cache.containsKey(cacheKey)) { - final pair = _cache[cacheKey]!; - if ((DateTime.now().millisecondsSinceEpoch - pair.a) < (ttl * 1000)) { // Si no ha expirado - return pair.b; - } - - _cache.remove(cacheKey); // Borrar cache si ya expiró - } - } - - final responseStream = await _client.send(request); - if (useCache && responseStream.statusCode == 200) { - _cache[cacheKey] = Pair(DateTime.now().millisecondsSinceEpoch, responseStream); - } - - return responseStream; - } - - @override - void close() { - _client.close(); - super.close(); - } - -} +} \ No newline at end of file diff --git a/lib/models/asignaturas/detalles/navigation_tab.dart b/lib/models/asignaturas/detalles/navigation_tab.dart new file mode 100644 index 0000000..7cb92c4 --- /dev/null +++ b/lib/models/asignaturas/detalles/navigation_tab.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +class NavigationTab { + final String label; + final Widget child; + final bool initial; + + NavigationTab({ + required this.label, + required this.child, + this.initial = false, + }); +} \ No newline at end of file diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 69991a7..bc8261d 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_estudiantes_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; @@ -11,20 +12,8 @@ import 'package:mi_utem/services_new/interfaces/controllers/calculator_controlle import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:watch_it/watch_it.dart'; -class _ITabs { - final String label; - final Widget child; - final bool initial; - - _ITabs({ - required this.label, - required this.child, - this.initial = false, - }); -} - class AsignaturaDetalleScreen extends StatelessWidget { - final Asignatura? asignatura; + final Asignatura asignatura; AsignaturaDetalleScreen({ super.key, @@ -38,16 +27,16 @@ class AsignaturaDetalleScreen extends StatelessWidget { ReviewService.addScreen("AsignaturaScreen"); final tabs = [ - _ITabs( + NavigationTab( label: "Resumen", child: AsignaturaResumenTab(asignatura: asignatura), ), - _ITabs( + NavigationTab( label: "Notas", child: AsignaturaNotasTab(asignatura: asignatura), initial: true, ), - if (asignatura?.estudiantes != null && (asignatura?.estudiantes?.length ?? 0) > 0) _ITabs( + if ((asignatura.estudiantes?.length ?? 0) > 0) NavigationTab( label: "Estudiantes", child: AsignaturaEstudiantesTab(asignatura: asignatura), ), @@ -55,31 +44,32 @@ class AsignaturaDetalleScreen extends StatelessWidget { final index = tabs.indexWhere((tab) => tab.initial); return DefaultTabController( - initialIndex: asignatura != null ? (index == -1 ? 0 : index) : 0, - length: asignatura != null ? tabs.length : 1, + initialIndex: index == -1 ? 0 : index, + length: tabs.length, child: Scaffold( appBar: CustomAppBar( - title: Text(asignatura?.nombre ?? "Asignatura sin nombre"), + title: Text(asignatura.nombre ?? "Asignatura sin nombre"), actions: _mostrarCalculadora ? [ IconButton( icon: Icon(Mdi.calculator), tooltip: "Calculadora", onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())); - if (asignatura?.grades != null) { - di.get().updateWithGrades(asignatura!.grades!); + final grades = asignatura.grades; + if (grades != null) { + di.get().updateWithGrades(grades); } }, ), ] : [], - bottom: asignatura != null ? TabBar( + bottom: TabBar( indicatorColor: Colors.white.withOpacity(0.8), tabs: tabs.map((tab) => Tab(text: tab.label)).toList(), - ) : null, + ), ), - body: asignatura != null ? TabBarView( + body: TabBarView( children: tabs.map((tab) => tab.child).toList(), - ) : Container(), + ), ), ); } diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index 5312b4e..c5ba8ab 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignatura.dart'; import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; +import 'package:watch_it/watch_it.dart'; class AsignaturaNotasTab extends StatefulWidget { - final Asignatura? asignatura; + final Asignatura asignatura; const AsignaturaNotasTab({ super.key, @@ -20,31 +23,47 @@ class AsignaturaNotasTab extends StatefulWidget { class _AsignaturaNotasTabState extends State { + late Asignatura asignatura; + + @override + void initState() { + this.asignatura = widget.asignatura; + super.initState(); + } + @override Widget build(BuildContext context) => PullToRefresh( - onRefresh: () async => setState(() {}), + onRefresh: () async { + final asignatura = await di.get().getDetalleAsignatura(this.asignatura.codigo); + if(asignatura == null) { + showErrorSnackbar(context, "Ocurrió un error actualizar la asignatura. Por favor intenta más tarde."); + return; + } + + setState(() => this.asignatura = asignatura); + }, child: ListView( physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.all(10), children: [ NotasDisplayWidget( - notaFinal: widget.asignatura?.grades?.notaFinal, - notaExamen: widget.asignatura?.grades?.notaExamen, - notaPresentacion: widget.asignatura?.grades?.notaPresentacion, - estado: widget.asignatura?.estado, - colorPorEstado: widget.asignatura?.colorPorEstado, + notaFinal: widget.asignatura.grades?.notaFinal, + notaExamen: widget.asignatura.grades?.notaExamen, + notaPresentacion: widget.asignatura.grades?.notaPresentacion, + estado: widget.asignatura.estado, + colorPorEstado: widget.asignatura.colorPorEstado, ), Card( child: Container( padding: EdgeInsets.all(20), - child: widget.asignatura?.grades?.notasParciales.isNotEmpty == true ? ListView.builder( + child: widget.asignatura.grades?.notasParciales.isNotEmpty == true ? ListView.builder( shrinkWrap: true, physics: ClampingScrollPhysics(), itemBuilder: (context, i) { - REvaluacion evaluacion = widget.asignatura!.grades!.notasParciales[i]; + REvaluacion evaluacion = widget.asignatura.grades!.notasParciales[i]; return NotaListItem(evaluacion: IEvaluacion.fromRemote(evaluacion)); }, - itemCount: widget.asignatura!.grades!.notasParciales.length, + itemCount: widget.asignatura.grades!.notasParciales.length, ) : CustomErrorWidget( emoji: "🤔", title: "Parece que aún no hay notas ni ponderadores", diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/asignatura_resumen_tab.dart index c93b79b..67f4952 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/asignatura_resumen_tab.dart @@ -1,128 +1,178 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/widgets/custom_error_widget.dart'; +import 'package:mi_utem/utils/string_utils.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; class AsignaturaResumenTab extends StatelessWidget { - final Asignatura? asignatura; + final Asignatura asignatura; AsignaturaResumenTab({ Key? key, - this.asignatura, + required this.asignatura, }) : super(key: key); + List>? get datosHorario => asignatura.horario?.split("||").map((e) => e.trim()).map((it) => { + 'dia': capitalize(it.split("|")[0]).trim(), + 'horas': it.split("|")[1].split("/").map((it) => it.trim()).map((it) => "- $it").join("\n"), + }).toList(); + @override - Widget build(BuildContext context) { - return asignatura == null - ? CustomErrorWidget( - title: "Ocurrió un error al obtener el resumen de la asignatura", - error: '', - ) - : SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget build(BuildContext context) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + child: Container( + child: ListView( + padding: EdgeInsets.only(top: 20.0, bottom: 5.0), + shrinkWrap: true, + physics: ScrollPhysics(), children: [ - Card( - child: Container( - child: ListView( - shrinkWrap: true, - physics: ScrollPhysics(), - children: [ - Container(height: 20), - Container( - padding: EdgeInsets.symmetric(horizontal: 15), - width: double.infinity, - child: Text( - "Resumen".toUpperCase(), - style: Get.textTheme.bodyLarge, - textAlign: TextAlign.left, - ), - ), - FieldListTile( - title: "Nombre", - value: asignatura?.nombre, - ), - Divider(height: 5, indent: 20, endIndent: 20), - FieldListTile( - title: "Código", - value: asignatura?.codigo, - ), - if (asignatura?.seccion != null && - asignatura!.seccion!.isNotEmpty) ...[ - Divider(height: 5, indent: 20, endIndent: 20), - FieldListTile( - title: "Sección", - value: asignatura!.seccion.toString(), - ), - ], - Divider(height: 5, indent: 20, endIndent: 20), - GestureDetector( - child: FieldListTile( - title: "Docente", - value: asignatura?.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ") ?? "Sin docente", // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. - ), - onTap: () async { - /*await Get.to(() => UsuarioScreen( - tipo: 2, - query: { "nombre": asignatura?.docente }, - asignatura: asignatura, - ));*/ - }, - ), - Divider(height: 5, indent: 20, endIndent: 20), - if (asignatura?.tipoAsignatura != null) ...[ - FieldListTile( - title: "Tipo de asignatura", - value: asignatura!.tipoAsignatura!, - ), - Divider(height: 5, indent: 20, endIndent: 20), - ], - FieldListTile( - title: "Tipo de hora", - value: asignatura?.tipoHora, - ), - Divider(height: 5, indent: 20, endIndent: 20), - if (asignatura?.horario != null) ...[ - FieldListTile( - title: "Horario", - value: asignatura!.horario!, - ), - Divider(height: 5, indent: 20, endIndent: 20), - ], - FieldListTile( - title: "Intentos", - value: asignatura?.intentos.toString(), - ), - Divider(height: 5, indent: 20, endIndent: 20), - FieldListTile( - title: "Sala", - value: asignatura?.sala, - ), - ], + Container( + padding: EdgeInsets.symmetric(horizontal: 15), + width: double.infinity, + child: Text("Asignatura".toUpperCase(), + style: Get.textTheme.bodyLarge, + textAlign: TextAlign.left, + ), + ), + GestureDetector( + child: FieldListTile( + title: "Docente", + value: asignatura.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ") ?? "Sin docente", // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. + ), + onTap: () async { + // TODO: Mostrar perfil del docente. + }, + ), + if (asignatura.seccion?.isNotEmpty == true) ...[ + Divider(height: 5, indent: 20, endIndent: 20), + FieldListTile( + title: "Sección", + value: asignatura.seccion.toString(), + ), + ], + Divider(height: 5, indent: 20, endIndent: 20), + FieldListTile( + title: "Código Asignatura", + value: asignatura.codigo, + ), + if (asignatura.tipoAsignatura != null) ...[ + Divider(height: 5, indent: 20, endIndent: 20), + FieldListTile( + title: "Tipo de Asignatura", + value: asignatura.tipoAsignatura, ), + Divider(height: 5, indent: 20, endIndent: 20), + ], + Divider(height: 5, indent: 20, endIndent: 20), + FieldListTile( + title: "Intentos", + value: "${asignatura.intentos}", + ), + + Divider(height: 5), + + Container( + padding: EdgeInsets.symmetric(horizontal: 15), + margin: EdgeInsets.only(top: 20), + width: double.infinity, + child: Text("Sala".toUpperCase(), + style: Get.textTheme.bodyLarge, + textAlign: TextAlign.left, + ), + ), + FieldListTile( + title: "Tipo de hora", + value: asignatura.tipoHora, + ), + Divider(height: 5, indent: 20, endIndent: 20), + FieldListTile( + title: "Sala", + value: asignatura.sala, + ), + + Divider(height: 5), + Container( + padding: EdgeInsets.symmetric(horizontal: 15), + margin: EdgeInsets.only(top: 20), + width: double.infinity, + child: Text("Horario".toUpperCase(), + style: Get.textTheme.bodyLarge, + textAlign: TextAlign.left, ), ), - // Card( - // child: Container( - // padding: EdgeInsets.all(20), - // child: Column( - // children: [ - // Container( - // width: double.infinity, - // child: Text( - // "Asistencia".toUpperCase(), - // style: Get.textTheme.headline4, - // textAlign: TextAlign.left, - // ), - // ), - // AsistenciaChart(asistencia: _asignatura.asistencia), - // ], - // ), - // ), - // ), + ...datosHorario?.map((it) => Column( + children: [ + FieldListTile( + title: it['dia'] ?? "", + value: it['horas'] + ), + if ("${datosHorario?.last['dia']} ${datosHorario?.last['horas']}" != "${it['dia']} ${it['horas']}") Divider(height: 5, indent: 20, endIndent: 20), + ], + )).toList() ?? [], ], ), - ); - } + ), + ), + // Card( + // child: Container( + // child: ListView( + // padding: EdgeInsets.only(top: 20.0, bottom: 5.0), + // shrinkWrap: true, + // physics: ScrollPhysics(), + // children: [ + // Container( + // padding: EdgeInsets.symmetric(horizontal: 15), + // width: double.infinity, + // child: Text("Sala".toUpperCase(), + // style: Get.textTheme.bodyLarge, + // textAlign: TextAlign.left, + // ), + // ), + // FieldListTile( + // title: "Tipo de hora", + // value: asignatura.tipoHora, + // ), + // Divider(height: 5, indent: 20, endIndent: 20), + // FieldListTile( + // title: "Sala", + // value: asignatura.sala, + // ), + // ], + // ), + // ) + // ), + // Card( + // child: Container( + // child: ListView( + // padding: EdgeInsets.only(top: 20.0, bottom: 5.0), + // shrinkWrap: true, + // physics: ScrollPhysics(), + // children: [ + // Container( + // padding: EdgeInsets.symmetric(horizontal: 15), + // width: double.infinity, + // child: Text("Horario".toUpperCase(), + // style: Get.textTheme.bodyLarge, + // textAlign: TextAlign.left, + // ), + // ), + // ...datosHorario?.map((it) => Column( + // children: [ + // FieldListTile( + // title: it['dia'] ?? "", + // value: it['horas'] + // ), + // if ("${datosHorario?.last['dia']} ${datosHorario?.last['horas']}" != "${it['dia']} ${it['horas']}") Divider(height: 5, indent: 20, endIndent: 20), + // ], + // )).toList() ?? [], + // ], + // ), + // ), + // ), + ], + ), + ); } diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index e2dbad4..f792556 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -10,6 +10,7 @@ import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.d import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; import 'package:path_provider/path_provider.dart'; import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; @@ -57,7 +58,9 @@ class _HorarioScreenState extends State { @override void initState() { - controller.getHorarioData(); + controller.getHorarioData().catchError((err) => { + showErrorSnackbar(context, "Ocurrió un error al cargar el horario! Por favor intenta más tarde.") + }); super.initState(); } diff --git a/lib/screens/horario/widgets/horario_main_scroller.dart b/lib/screens/horario/widgets/horario_main_scroller.dart index 6fe4330..3bb071e 100644 --- a/lib/screens/horario/widgets/horario_main_scroller.dart +++ b/lib/screens/horario/widgets/horario_main_scroller.dart @@ -102,8 +102,7 @@ class _HorarioMainScrollerState extends State { left: HorarioMainScroller.periodWidth * widget.controller.zoom.value, ), child: InteractiveViewer( - transformationController: - widget.controller.blockContentController, + transformationController: widget.controller.blockContentController, maxScale: HorarioMainScroller.defaultMaxScale, minScale: HorarioMainScroller.defaultMinScale, panAxis: PanAxis.free, @@ -118,13 +117,9 @@ class _HorarioMainScrollerState extends State { Container( width: HorarioMainScroller.daysWidth, height: HorarioMainScroller.dayHeight, - margin: EdgeInsets.only( - left: HorarioMainScroller.periodWidth * - widget.controller.zoom.value, - ), + margin: EdgeInsets.only(left: HorarioMainScroller.periodWidth * widget.controller.zoom.value), child: InteractiveViewer( - transformationController: - widget.controller.daysHeaderController, + transformationController: widget.controller.daysHeaderController, maxScale: HorarioMainScroller.defaultMaxScale, minScale: HorarioMainScroller.defaultMinScale, panAxis: PanAxis.free, @@ -158,13 +153,9 @@ class _HorarioMainScrollerState extends State { Container( width: HorarioMainScroller.periodWidth, height: HorarioMainScroller.periodsHeight, - margin: EdgeInsets.only( - top: HorarioMainScroller.dayHeight * - widget.controller.zoom.value, - ), + margin: EdgeInsets.only(top: HorarioMainScroller.dayHeight * widget.controller.zoom.value), child: InteractiveViewer( - transformationController: - widget.controller.periodHeaderController, + transformationController: widget.controller.periodHeaderController, maxScale: HorarioMainScroller.defaultMaxScale, minScale: HorarioMainScroller.defaultMinScale, panAxis: PanAxis.free, diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services_new/implementations/auth_service.dart index ed70189..f309855 100644 --- a/lib/services_new/implementations/auth_service.dart +++ b/lib/services_new/implementations/auth_service.dart @@ -23,7 +23,7 @@ class AuthServiceImplementation implements AuthService { Future isFirstTime() async => !(await secureStorage.containsKey(key: "last_login")); @override - Future isLoggedIn() async { + Future isLoggedIn({ bool forceRefresh = false }) async { final credential = await _getCredential(); if(credential == null) { return false; @@ -42,23 +42,37 @@ class AuthServiceImplementation implements AuthService { final lastLoginDate = DateTime.fromMillisecondsSinceEpoch(int.parse(lastLogin)); final now = DateTime.now(); final difference = now.difference(lastLoginDate); - if(difference.inMinutes < 5) { + if(difference.inMinutes < 5 && !forceRefresh) { return true; } try { - final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth"), + final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth/refresh"), body: credential.toString(), headers: { 'Content-Type': 'application/json', 'User-Agent': 'App/MiUTEM', + 'Authorization': 'Bearer ${user.token}', }, ); - final token = jsonDecode(response.body)["token"] as String; + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json); + } + + throw CustomException.custom(response.reasonPhrase); + } + + + final token = json["token"] as String; if(token == user.token) { return true; } + final userJson = user.toJson(); userJson["token"] = token; await setUser(User.fromJson(userJson)); @@ -95,8 +109,7 @@ class AuthServiceImplementation implements AuthService { throw CustomException.custom(response.reasonPhrase); } - final user = User.fromJson(json as Map); - await setUser(user); + await setUser(User.fromJson(json as Map)); } @override diff --git a/lib/services_new/implementations/carreras_service.dart b/lib/services_new/implementations/carreras_service.dart index 344a4f9..0fb8122 100644 --- a/lib/services_new/implementations/carreras_service.dart +++ b/lib/services_new/implementations/carreras_service.dart @@ -7,9 +7,7 @@ import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; -import 'package:watch_it/watch_it.dart'; class CarrerasServiceImplementation extends ChangeNotifier implements CarrerasService { @@ -22,12 +20,6 @@ class CarrerasServiceImplementation extends ChangeNotifier implements CarrerasSe @override Future getCarreras({bool forceRefresh = false}) async { logger.d("[CarrerasService#getCarreras]: Obteniendo carreras..."); - final user = await di.get().getUser(); - if(user == null) { - logger.d("[CarrerasService#getCarreras]: No hay usuario logueado, no se pueden obtener las carreras"); - return; - } - final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras")); final json = jsonDecode(response.body); diff --git a/lib/services_new/implementations/controllers/calculator_controller.dart b/lib/services_new/implementations/controllers/calculator_controller.dart index 6bcf99b..fd95173 100644 --- a/lib/services_new/implementations/controllers/calculator_controller.dart +++ b/lib/services_new/implementations/controllers/calculator_controller.dart @@ -1,6 +1,6 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/config/logger.dart'; +import 'package:listenable_collections/listenable_collections.dart'; import 'package:mi_utem/models/evaluacion.dart'; import 'package:mi_utem/models/grades.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; @@ -27,15 +27,15 @@ class CalculatorControllerImplementation implements CalculatorController { /* Notas parciales */ @override - ValueNotifier> partialGrades = ValueNotifier([]); + ListNotifier partialGrades = ListNotifier(); /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ @override - ValueNotifier> percentageTextFieldControllers = ValueNotifier([]); + ListNotifier percentageTextFieldControllers = ListNotifier(); /* Controlador de texto para las notas con máscara (para autocompletar formato) */ @override - ValueNotifier> gradeTextFieldControllers = ValueNotifier([]); + ListNotifier gradeTextFieldControllers = ListNotifier(); /* Nota del examen */ @override @@ -49,138 +49,53 @@ class CalculatorControllerImplementation implements CalculatorController { ValueNotifier freeEditable = ValueNotifier(false); @override - double? get getCalculatedFinalGrade { - final calculatedPresentationGrade = getCalculatedPresentationGrade; - if (calculatedPresentationGrade == null) { - return null; - } - - final examGradeValue = examGrade.value; - if(examGradeValue == null) { - return calculatedPresentationGrade; - } - - final weightedFinalGrade = calculatedPresentationGrade * presentationFinalWeight; - final weightedExamGrade = examGradeValue * examFinalWeight; - - return weightedFinalGrade + weightedExamGrade; - } + ValueNotifier calculatedFinalGrade = ValueNotifier(null); @override - double? get getCalculatedPresentationGrade { - double presentationGrade = 0; - for (final partialGrade in partialGrades.value) { - final weight = (partialGrade.porcentaje ?? 0) / maxPercentage; - presentationGrade += (partialGrade.nota ?? 0) * weight; - } - - return presentationGrade != 0 ? presentationGrade : null; - } + ValueNotifier calculatedPresentationGrade = ValueNotifier(null); @override - int get getAmountOfPartialGradesWithoutGrade => partialGrades.value - .where((partialGrade) => partialGrade.nota == null) - .length; + ValueNotifier amountOfPartialGradesWithoutGrade = ValueNotifier(0); @override - int get getAmountOfPartialGradesWithoutPercentage => partialGrades.value - .where((partialGrade) => partialGrade.porcentaje == null) - .length; + ValueNotifier amountOfPartialGradesWithoutPercentage = ValueNotifier(0); @override - bool get hasMissingPartialGrade => getAmountOfPartialGradesWithoutGrade > 0; + ValueNotifier hasMissingPartialGrade = ValueNotifier(false); @override - bool get canTakeExam { - if(hasMissingPartialGrade) { - return false; - } - - final calculatedPresentationGrade = getCalculatedPresentationGrade; - if(calculatedPresentationGrade == null) { - return false; - } - - return calculatedPresentationGrade >= minimumGradeForExam && calculatedPresentationGrade < passingGrade; - } + ValueNotifier canTakeExam = ValueNotifier(false); @override - double? get getMinimumRequiredExamGrade { - if(!canTakeExam) { - return null; - } - - final calculatedPresentationGrade = getCalculatedPresentationGrade; - if(calculatedPresentationGrade == null) { - return null; - } - - final weightedPresentationGrade = calculatedPresentationGrade * presentationFinalWeight; - return (passingGrade - weightedPresentationGrade) / examFinalWeight; - } + ValueNotifier minimumRequiredExamGrade = ValueNotifier(null); @override - double get getPercentageOfPartialGrades { - double percentage = 0; - for (final partialGrade in partialGrades.value) { - percentage += (partialGrade.porcentaje ?? 0); - } - return percentage; - } + ValueNotifier percentageOfPartialGrades = ValueNotifier(0); @override - double get getMissingPercentage => maxPercentage - getPercentageOfPartialGrades; + ValueNotifier missingPercentage = ValueNotifier(0); @override - bool get hasMissingPercentage => getAmountOfPartialGradesWithoutPercentage > 0; + ValueNotifier hasMissingPercentage = ValueNotifier(false); @override - double? get getSuggestedPercentage { - final percentage = getMissingPercentage / getAmountOfPartialGradesWithoutPercentage; - return 0 <= percentage && percentage <= maxPercentage ? percentage : null; - } + ValueNotifier suggestedPercentage = ValueNotifier(null); @override - double? get getSuggestedPresentationGrade { - double presentationGrade = 0; - for(final partialGrade in partialGrades.value) { - final weight = (partialGrade.porcentaje ?? (getSuggestedPercentage ?? 0)) / maxPercentage; - presentationGrade += (partialGrade.nota ?? 0) * weight; - } - - return 0 <= presentationGrade && presentationGrade <= maxGrade ? presentationGrade : null; - } + ValueNotifier suggestedPresentationGrade = ValueNotifier(null); @override - double get getPercentageWithoutGrade { - double percentage = 0; - for(final partialGrade in partialGrades.value) { - if(partialGrade.nota == null) { - percentage += (partialGrade.porcentaje ?? (getSuggestedPercentage ?? 0)); - } - } - - return percentage; - } + ValueNotifier percentageWithoutGrade = ValueNotifier(0); @override - bool get hasCorrectPercentage => getPercentageOfPartialGrades == maxPercentage; + ValueNotifier hasCorrectPercentage = ValueNotifier(false); @override - double? get getSuggestedGrade { - final percentageWithoutGrade = getPercentageWithoutGrade; - if(!(hasMissingPartialGrade && percentageWithoutGrade > 0)) { - return null; - } - - final weightOfMissingGrades = percentageWithoutGrade / maxPercentage; - final requiredGradeValue = passingGrade - (getSuggestedPresentationGrade ?? 0); - return requiredGradeValue / weightOfMissingGrades; - } + ValueNotifier suggestedGrade = ValueNotifier(null); @override void updateWithGrades(Grades grades) { - partialGrades.value.clear(); + partialGrades.clear(); percentageTextFieldControllers.value.clear(); gradeTextFieldControllers.value.clear(); @@ -189,76 +104,239 @@ class CalculatorControllerImplementation implements CalculatorController { } setExamGrade(grades.notaExamen); + _updateCalculations(); } @override void updateGradeAt(int index, IEvaluacion updatedGrade) { - final grade = partialGrades.value[index]; + final grade = partialGrades[index]; if(!(grade.editable || freeEditable.value)) { return; } - final copy = partialGrades.value; - copy[index] = updatedGrade; - partialGrades.value = copy; + partialGrades[index] = updatedGrade; - if(hasMissingPartialGrade) { + _updateCalculations(); + if(hasMissingPartialGrade.value) { clearExamGrade(); } } @override void addGrade(IEvaluacion grade) { - partialGrades.value = [...partialGrades.value, grade]; - percentageTextFieldControllers.value = [ - ...percentageTextFieldControllers.value, - MaskedTextController( - mask: "000", - text: grade.porcentaje?.toStringAsFixed(0) ?? "", - ), - ]; - gradeTextFieldControllers.value = [ - ...gradeTextFieldControllers.value, - MaskedTextController( - mask: "0.0", - text: grade.nota?.toStringAsFixed(1) ?? "", - ), - ]; - logger.d("A grade has been added"); + partialGrades.add(grade); + percentageTextFieldControllers.add(MaskedTextController( + mask: "000", + text: grade.porcentaje?.toStringAsFixed(0) ?? "", + )); + gradeTextFieldControllers.add(MaskedTextController( + mask: "0.0", + text: grade.nota?.toStringAsFixed(1) ?? "", + )); + _updateCalculations(); } @override void removeGradeAt(int index) { - final grade = partialGrades.value[index]; + final grade = partialGrades[index]; if(!(grade.editable || freeEditable.value)) { return; } - partialGrades.value = partialGrades.value..removeAt(index); - percentageTextFieldControllers.value = percentageTextFieldControllers.value..removeAt(index); - gradeTextFieldControllers.value = gradeTextFieldControllers.value..removeAt(index); - logger.d("Removed grade at index $index"); + partialGrades.removeAt(index); + percentageTextFieldControllers.removeAt(index); + gradeTextFieldControllers.removeAt(index); + _updateCalculations(); } @override void makeEditable() { freeEditable.value = true; + _updateCalculations(); } @override void makeNonEditable() { freeEditable.value = false; + _updateCalculations(); } @override void clearExamGrade() { examGrade.value = null; - examGradeTextFieldController.value = examGradeTextFieldController.value..updateText(""); + final examGradeTextFieldController = this.examGradeTextFieldController.value; + examGradeTextFieldController.updateText(""); + this.examGradeTextFieldController.value = examGradeTextFieldController; + _updateCalculations(); } @override void setExamGrade(num? grade) { examGrade.value = grade?.toDouble(); - examGradeTextFieldController.value = examGradeTextFieldController.value..updateText(grade?.toDouble().toStringAsFixed(1) ?? ""); + examGradeTextFieldController.value.updateText(grade?.toDouble().toStringAsFixed(1) ?? ""); + _updateCalculations(); + } + + void _updateCalculations() { + _calculateFinalGrade(); + _calculatePresentationGrade(); + _calculateAmountOfPartialGradesWithoutGrade(); + _calculateAmountOfPartialGradesWithoutPercentage(); + _checkHasMissingPartialGrade(); + _checkCanTakeExam(); + _calculateMinimumRequiredExamGrade(); + _calculatePercentageOfPartialGrades(); + _calculateMissingPercentage(); + _checkMissingPercentage(); + _calculateSuggestedPercentage(); + _calculateSuggestedPresentationGrade(); + _calculatePercentageWithoutGrade(); + _checkCorrectPercentage(); + _calculateSuggestedGrade(); + } + + void _calculateFinalGrade() { + _calculatePresentationGrade(); + final calculatedPresentationGrade = this.calculatedPresentationGrade.value; + if (calculatedPresentationGrade == null) { + calculatedFinalGrade.value = null; + return; + } + + final examGradeValue = examGrade.value; + if(examGradeValue == null) { + calculatedFinalGrade.value = calculatedPresentationGrade; + return; + } + + final weightedFinalGrade = calculatedPresentationGrade * presentationFinalWeight; + final weightedExamGrade = examGradeValue * examFinalWeight; + + calculatedFinalGrade.value = weightedFinalGrade + weightedExamGrade; + } + + void _calculatePresentationGrade() { + double presentationGrade = 0; + for (final partialGrade in partialGrades) { + final weight = (partialGrade.porcentaje ?? 0) / maxPercentage; + presentationGrade += (partialGrade.nota ?? 0) * weight; + } + + calculatedPresentationGrade.value = presentationGrade != 0 ? presentationGrade : null; + } + + void _calculateAmountOfPartialGradesWithoutGrade() => amountOfPartialGradesWithoutGrade.value = partialGrades + .where((partialGrade) => partialGrade.nota == null) + .length; + + void _calculateAmountOfPartialGradesWithoutPercentage() => amountOfPartialGradesWithoutPercentage.value = partialGrades + .where((partialGrade) => partialGrade.porcentaje == null) + .length; + + void _checkHasMissingPartialGrade() { + _calculateAmountOfPartialGradesWithoutGrade(); + hasMissingPartialGrade.value = amountOfPartialGradesWithoutGrade.value > 0; + } + + void _checkCanTakeExam() { + _checkHasMissingPartialGrade(); + if(hasMissingPartialGrade.value) { + canTakeExam.value = false; + return; + } + + _calculatePresentationGrade(); + final calculatedPresentationGrade = this.calculatedPresentationGrade.value; + if(calculatedPresentationGrade == null) { + canTakeExam.value = false; + return; + } + + canTakeExam.value = calculatedPresentationGrade >= minimumGradeForExam && calculatedPresentationGrade < passingGrade; + } + + void _calculateMinimumRequiredExamGrade() { + _checkCanTakeExam(); + if(!canTakeExam.value) { + minimumRequiredExamGrade.value = null; + return; + } + + _calculatePresentationGrade(); + final calculatedPresentationGrade = this.calculatedPresentationGrade.value; + if(calculatedPresentationGrade == null) { + minimumRequiredExamGrade.value = null; + return; + } + + final weightedPresentationGrade = calculatedPresentationGrade * presentationFinalWeight; + minimumRequiredExamGrade.value = (passingGrade - weightedPresentationGrade) / examFinalWeight; + } + + void _calculatePercentageOfPartialGrades() { + double percentage = 0; + for (final partialGrade in partialGrades) { + percentage += (partialGrade.porcentaje ?? 0); + } + percentageOfPartialGrades.value = percentage; + } + + void _calculateMissingPercentage() { + _calculatePercentageOfPartialGrades(); + missingPercentage.value = maxPercentage - percentageOfPartialGrades.value; + } + + void _checkMissingPercentage() { + _calculateAmountOfPartialGradesWithoutPercentage(); + hasMissingPercentage.value = amountOfPartialGradesWithoutPercentage.value > 0; + } + + void _calculateSuggestedPercentage() { + _calculateAmountOfPartialGradesWithoutPercentage(); + _calculateMissingPercentage(); + final percentage = missingPercentage.value / amountOfPartialGradesWithoutPercentage.value; + suggestedPercentage.value = 0 <= percentage && percentage <= maxPercentage ? percentage : null; + } + + void _calculateSuggestedPresentationGrade() { + _calculateSuggestedPercentage(); + double presentationGrade = 0; + for(final partialGrade in partialGrades) { + final weight = (partialGrade.porcentaje ?? (suggestedPercentage.value ?? 0)) / maxPercentage; + presentationGrade += (partialGrade.nota ?? 0) * weight; + } + + suggestedPresentationGrade.value = 0 <= presentationGrade && presentationGrade <= maxGrade ? presentationGrade : null; + } + + void _calculatePercentageWithoutGrade() { + _calculateSuggestedPercentage(); + double percentage = 0; + for(final partialGrade in partialGrades) { + if(partialGrade.nota == null) { + percentage += (partialGrade.porcentaje ?? (suggestedPercentage.value ?? 0)); + } + } + + percentageWithoutGrade.value = percentage; + } + + void _checkCorrectPercentage() { + _calculatePercentageOfPartialGrades(); + hasCorrectPercentage.value = percentageOfPartialGrades.value == maxPercentage; + } + + void _calculateSuggestedGrade() { + _calculatePercentageWithoutGrade(); + final percentageWithoutGrade = this.percentageWithoutGrade.value; + if(!(hasMissingPartialGrade.value && percentageWithoutGrade > 0)) { + suggestedGrade.value = null; + return; + } + + _calculateSuggestedPresentationGrade(); + final weightOfMissingGrades = percentageWithoutGrade / maxPercentage; + final requiredGradeValue = passingGrade - (suggestedPresentationGrade.value ?? 0); + suggestedGrade.value = requiredGradeValue / weightOfMissingGrades; } } \ No newline at end of file diff --git a/lib/services_new/implementations/controllers/horario_controller.dart b/lib/services_new/implementations/controllers/horario_controller.dart index 8cef997..73df0cb 100644 --- a/lib/services_new/implementations/controllers/horario_controller.dart +++ b/lib/services_new/implementations/controllers/horario_controller.dart @@ -99,20 +99,17 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC @override Future getHorarioData({ bool forceRefresh = false }) async { loadingHorario.value = true; - notifyListeners(); final lastUpdate = _storage.read("last_horario_update") ?? "${DateTime.now().toIso8601String()}"; final lastUpdateDate = DateTime.parse(lastUpdate); final now = DateTime.now(); final difference = now.difference(lastUpdateDate).inMinutes; if(difference < 15 && forceRefresh == false && horario.value != null) { loadingHorario.value = false; - notifyListeners(); return; } horario.value = null; - notifyListeners(); final carrerasService = di.get(); Carrera? carrera = carrerasService.selectedCarrera.value; @@ -124,14 +121,12 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC final carreraId = carrera?.id; if(carreraId == null) { loadingHorario.value = false; - notifyListeners(); return; } horario.value = await di.get().getHorario(carreraId); _setRandomColorsByHorario(); loadingHorario.value = false; - notifyListeners(); _storage.write("last_horario_update", DateTime.now().toIso8601String()); } @@ -152,9 +147,9 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC x = x > maxXPosition ? maxXPosition : x; y = y > maxYPosition ? maxYPosition : y; - blockContentController.value.setTranslationRaw(-x, -y, 0); - periodHeaderController.value.setTranslationRaw(0, -y, 0); - daysHeaderController.value.setTranslationRaw(-x, 0, 0); + blockContentController.value = blockContentController.value..setTranslationRaw(-x, -y, 0); + periodHeaderController.value = periodHeaderController.value..setTranslationRaw(0, -y, 0); + daysHeaderController.value = daysHeaderController.value..setTranslationRaw(-x, 0, 0); _onChangeAnyController(); } @@ -178,15 +173,14 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC moveViewportToPeriodIndexAndDayIndex(periodIndex, dayIndex); isCenteredInCurrentPeriodAndDay.value = true; - notifyListeners(); } @override void setZoom(double zoom) { - blockContentController.value.setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); - periodHeaderController.value.setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); - daysHeaderController.value.setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); - cornerController.value.setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); + blockContentController.value = blockContentController.value..setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); + periodHeaderController.value = periodHeaderController.value..setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); + daysHeaderController.value = daysHeaderController.value..setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); + cornerController.value = cornerController.value..setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); _onChangeAnyController(); } @@ -214,7 +208,6 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC @override void setIndicatorIsOpen(bool isOpen) { indicatorIsOpen.value = isOpen; - notifyListeners(); } void _setRandomColorsByHorario() { @@ -236,7 +229,6 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC void _onChangeAnyController() { setIndicatorIsOpen(true); isCenteredInCurrentPeriodAndDay.value = false; - notifyListeners(); } void _setScrollControllerListeners() { @@ -245,12 +237,12 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC final yPosition = blockContentController.value.getTranslation().y; final currentZoom = blockContentController.value.getMaxScaleOnAxis(); - daysHeaderController.value.setTranslationRaw(xPosition, 0, 0); - periodHeaderController.value.setTranslationRaw(0, yPosition, 0); + daysHeaderController.value = daysHeaderController.value..setTranslationRaw(xPosition, 0, 0); + periodHeaderController.value = periodHeaderController.value..setTranslationRaw(0, yPosition, 0); - daysHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1),); - periodHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + daysHeaderController.value = daysHeaderController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1),); + periodHeaderController.value = periodHeaderController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value = cornerController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); zoom.value = currentZoom; _onChangeAnyController(); @@ -261,11 +253,11 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC final xPosition = daysHeaderController.value.getTranslation().x; final contentYPosition = blockContentController.value.getTranslation().y; - blockContentController.value.setTranslationRaw(xPosition, contentYPosition, 0); + blockContentController.value = blockContentController.value..setTranslationRaw(xPosition, contentYPosition, 0); - blockContentController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - periodHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + blockContentController.value = blockContentController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + periodHeaderController.value = periodHeaderController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value = cornerController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); zoom.value = currentZoom; _onChangeAnyController(); @@ -277,13 +269,13 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC final contentXPosition = blockContentController.value.getTranslation().x; - periodHeaderController.value.setTranslationRaw(0, yPosition, 0); + periodHeaderController.value = periodHeaderController.value..setTranslationRaw(0, yPosition, 0); - blockContentController.value.setTranslationRaw(contentXPosition, yPosition, 0); + blockContentController.value = blockContentController.value..setTranslationRaw(contentXPosition, yPosition, 0); - blockContentController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - daysHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + blockContentController.value = blockContentController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + daysHeaderController.value = daysHeaderController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value = cornerController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); zoom.value = currentZoom; _onChangeAnyController(); diff --git a/lib/services_new/implementations/qr_pass_service.dart b/lib/services_new/implementations/qr_pass_service.dart index c76b608..d8a3c13 100644 --- a/lib/services_new/implementations/qr_pass_service.dart +++ b/lib/services_new/implementations/qr_pass_service.dart @@ -4,28 +4,13 @@ import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_covid.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; -import 'package:watch_it/watch_it.dart'; class QRPassServiceImplementation extends QRPassService { - final _authService = di.get(); - @override Future getDetallesPermiso(String id, {bool forceRefresh = false}) async { - final user = await _authService.getUser(); - if(user == null) { - return null; - } - - final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos/$id"), - headers: { - 'Authorization': 'Bearer ${user.token}', - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM' - }, - ); + final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos/$id")); final json = jsonDecode(response.body) as Map; @@ -42,18 +27,7 @@ class QRPassServiceImplementation extends QRPassService { @override Future?> getPermisos({bool forceRefresh = false}) async { - final user = await _authService.getUser(); - if(user == null) { - return null; - } - - final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos"), - headers: { - 'Authorization': 'Bearer ${user.token}', - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM' - }, - ); + final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos")); final json = jsonDecode(response.body); diff --git a/lib/services_new/interfaces/auth_service.dart b/lib/services_new/interfaces/auth_service.dart index ba0bff5..cb32743 100644 --- a/lib/services_new/interfaces/auth_service.dart +++ b/lib/services_new/interfaces/auth_service.dart @@ -5,7 +5,7 @@ abstract class AuthService { Future isFirstTime(); - Future isLoggedIn(); + Future isLoggedIn({ bool forceRefresh = false }); Future login(); diff --git a/lib/services_new/interfaces/controllers/calculator_controller.dart b/lib/services_new/interfaces/controllers/calculator_controller.dart index 9c84f79..b7cf995 100644 --- a/lib/services_new/interfaces/controllers/calculator_controller.dart +++ b/lib/services_new/interfaces/controllers/calculator_controller.dart @@ -1,18 +1,19 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; +import 'package:listenable_collections/listenable_collections.dart'; import 'package:mi_utem/models/evaluacion.dart'; import 'package:mi_utem/models/grades.dart'; abstract class CalculatorController { /* Notas parciales */ - abstract ValueNotifier> partialGrades; + abstract ListNotifier partialGrades; /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ - abstract ValueNotifier> percentageTextFieldControllers; + abstract ListNotifier percentageTextFieldControllers; /* Controlador de texto para las notas con máscara (para autocompletar formato) */ - abstract ValueNotifier> gradeTextFieldControllers; + abstract ListNotifier gradeTextFieldControllers; /* Nota del examen */ abstract ValueNotifier examGrade; @@ -23,48 +24,49 @@ abstract class CalculatorController { abstract ValueNotifier freeEditable; /* Nota final calculada */ - double? get getCalculatedFinalGrade; + abstract ValueNotifier calculatedFinalGrade; /* Nota de presentación calculada */ - double? get getCalculatedPresentationGrade; + abstract ValueNotifier calculatedPresentationGrade; /* Cantidad de notas parciales sin nota */ - int get getAmountOfPartialGradesWithoutGrade; + abstract ValueNotifier amountOfPartialGradesWithoutGrade; /* Cantidad de notas parciales sin porcentaje */ - int get getAmountOfPartialGradesWithoutPercentage; + abstract ValueNotifier amountOfPartialGradesWithoutPercentage; /* Si hay notas parciales sin nota */ - bool get hasMissingPartialGrade; + abstract ValueNotifier hasMissingPartialGrade; /* Si puede tomar examen */ - bool get canTakeExam; + abstract ValueNotifier canTakeExam; /* Nota mínima requerida para el examen */ - double? get getMinimumRequiredExamGrade; + abstract ValueNotifier minimumRequiredExamGrade; /* Porcentaje de las notas parciales */ - double get getPercentageOfPartialGrades; + abstract ValueNotifier percentageOfPartialGrades; /* Porcentaje faltante */ - double get getMissingPercentage; + abstract ValueNotifier missingPercentage; /* Si hay porcentaje faltante */ - bool get hasMissingPercentage; + abstract ValueNotifier hasMissingPercentage; /* Porcentaje sugerido */ - double? get getSuggestedPercentage; + abstract ValueNotifier suggestedPercentage; /* Nota de presentación sugerida */ - double? get getSuggestedPresentationGrade; + abstract ValueNotifier suggestedPresentationGrade; /* Porcentaje sin nota */ - double get getPercentageWithoutGrade; + abstract ValueNotifier percentageWithoutGrade; /* Si hay porcentaje sin nota */ - bool get hasCorrectPercentage; + abstract ValueNotifier hasCorrectPercentage; - double? get getSuggestedGrade; + /* Nota sugerida */ + abstract ValueNotifier suggestedGrade; void updateWithGrades(Grades grades); diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart new file mode 100644 index 0000000..5f758eb --- /dev/null +++ b/lib/utils/string_utils.dart @@ -0,0 +1,3 @@ +// Capitalize the first letter of a string, and if it has more than one word, capitalize the first letter of each word. +// Also does nothing if the string is null or empty. +String capitalize(String s) => s.split(" ").map((e) => e.isNotEmpty ? e[0].toUpperCase() + e.substring(1) : "").join(" "); \ No newline at end of file diff --git a/lib/widgets/asignatura/lista/asignatura_list_tile.dart b/lib/widgets/asignatura/lista/asignatura_list_tile.dart index ee987a1..e96be54 100644 --- a/lib/widgets/asignatura/lista/asignatura_list_tile.dart +++ b/lib/widgets/asignatura/lista/asignatura_list_tile.dart @@ -4,20 +4,21 @@ import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/themes/theme.dart'; class AsignaturaListTile extends StatelessWidget { + final Asignatura asignatura; const AsignaturaListTile({ - Key? key, + super.key, required this.asignatura, - }) : super(key: key); - - final Asignatura asignatura; + }); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Card( child: InkWell( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignatura: asignatura))), + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen( + asignatura: asignatura, + ))), child: Container( padding: const EdgeInsets.all(20), width: double.infinity, @@ -46,4 +47,5 @@ class AsignaturaListTile extends StatelessWidget { ), ), ); + } \ No newline at end of file diff --git a/lib/widgets/asignatura/notas_tab/nota_final_display.dart b/lib/widgets/asignatura/notas_tab/nota_final_display.dart index 527c187..8c5b0b6 100644 --- a/lib/widgets/asignatura/notas_tab/nota_final_display.dart +++ b/lib/widgets/asignatura/notas_tab/nota_final_display.dart @@ -1,38 +1,30 @@ import 'package:flutter/material.dart'; -class NotaFinalDisplayWidget extends StatefulWidget { - final num? _notaFinal; - final String? _estado; +class NotaFinalDisplayWidget extends StatelessWidget { + final num? notaFinal; + final String? estado; - NotaFinalDisplayWidget({Key? key, num? notaFinal, String? estado}) - : _notaFinal = notaFinal, - _estado = estado, - super(key: key); + NotaFinalDisplayWidget({ + super.key, + this.notaFinal, + this.estado, + }); @override - _NotaFinalDisplayWidgetState createState() => _NotaFinalDisplayWidgetState(); -} - -class _NotaFinalDisplayWidgetState extends State { - @override - Widget build(BuildContext context) { - return Column( - children: [ - Text( - widget._notaFinal?.toStringAsFixed(1) ?? "S/N", - style: TextStyle( - fontSize: 40, - fontWeight: FontWeight.bold, - ), + Widget build(BuildContext context) => Column( + children: [ + Text(notaFinal?.toStringAsFixed(1) ?? "S/N", + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, ), - Text( - widget._estado ?? "---", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + ), + Text(estado ?? "---", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), - ], - ); - } + ), + ], + ); } diff --git a/lib/widgets/asignatura/notas_tab/notas_display.dart b/lib/widgets/asignatura/notas_tab/notas_display.dart index c0c16ef..89b0d71 100644 --- a/lib/widgets/asignatura/notas_tab/notas_display.dart +++ b/lib/widgets/asignatura/notas_tab/notas_display.dart @@ -2,78 +2,63 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/labeled_nota_display.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/nota_final_display.dart'; -class NotasDisplayWidget extends StatefulWidget { - final num? _notaFinal; - final num? _notaExamen; - final num? _notaPresentacion; - final String? _estado; - final Color? _colorPorEstado; +class NotasDisplayWidget extends StatelessWidget { + final num? notaFinal; + final num? notaExamen; + final num? notaPresentacion; + final String? estado; + final Color? colorPorEstado; NotasDisplayWidget({ - notaFinal, - notaExamen, - notaPresentacion, - estado, - colorPorEstado, - }) : _notaFinal = notaFinal, - _notaExamen = notaExamen, - _notaPresentacion = notaPresentacion, - _estado = estado, - _colorPorEstado = colorPorEstado; + this.notaFinal, + this.notaExamen, + this.notaPresentacion, + this.estado, + this.colorPorEstado, + }); @override - State createState() => NotasDisplayWidgetState(); -} - -class NotasDisplayWidgetState extends State { @override - Widget build(BuildContext context) { - return Card( - child: Row( - children: [ - Container( - height: 130, - width: 10, - color: widget._colorPorEstado, - ), - Expanded( - child: Container( - padding: EdgeInsets.fromLTRB(15, 20, 20, 20), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - NotaFinalDisplayWidget( - notaFinal: widget._notaFinal, - estado: widget._estado, - ), - Container(width: 10), - Container( - height: 80, - width: 0.5, - color: Colors.grey, - ), - Container(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - LabeledNotaDisplayWidget( - label: "Examen", - nota: widget._notaExamen, - ), - Container(height: 10), - LabeledNotaDisplayWidget( - label: "Presentación", - nota: widget._notaPresentacion, - hint: "--"), - ], - ), - ], - ), + Widget build(BuildContext context) => Card( + child: Row( + children: [ + Container( + height: 130, + width: 10, + color: colorPorEstado, + ), + Expanded( + child: Padding( + padding: EdgeInsets.fromLTRB(15, 20, 20, 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + NotaFinalDisplayWidget(notaFinal: notaFinal, estado: estado), + SizedBox(width: 10), + Container(height: 80, width: 0.5, color: Colors.grey), + SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + LabeledNotaDisplayWidget( + label: "Examen", + nota: notaExamen, + hint: "--", + ), + Container(height: 10), + LabeledNotaDisplayWidget( + label: "Presentación", + nota: notaPresentacion, + hint: "--", + ), + ], + ), + ], ), ), - ], - ), - ); - } + ), + ], + ), + ); } diff --git a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart index 55958c7..39c1b0a 100644 --- a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:watch_it/watch_it.dart'; @@ -11,8 +12,9 @@ class NotaExamenDisplayWidget extends StatelessWidget with WatchItMixin{ @override Widget build(BuildContext context) { - final _calculatorController = di.get(); final examGradeTextFieldController = watchValue((CalculatorController controller) => controller.examGradeTextFieldController); + final canTakeExam = watchValue((CalculatorController controller) => controller.canTakeExam); + final minimumRequiredExamGrade = watchValue((CalculatorController controller) => controller.minimumRequiredExamGrade); return Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -26,11 +28,11 @@ class NotaExamenDisplayWidget extends StatelessWidget with WatchItMixin{ child: TextField( controller: examGradeTextFieldController, textAlign: TextAlign.center, - onChanged: (String value) => _calculatorController.setExamGrade(double.tryParse(value.replaceAll(",", "."))), - enabled: _calculatorController.canTakeExam, + onChanged: (String value) => di.get().setExamGrade(double.tryParse(value.replaceAll(",", "."))), + enabled: canTakeExam, decoration: InputDecoration( - hintText: _calculatorController.getMinimumRequiredExamGrade?.toStringAsFixed(1) ?? "", - filled: !_calculatorController.canTakeExam, + hintText: minimumRequiredExamGrade?.toStringAsFixed(1) ?? "--", + filled: canTakeExam, fillColor: Colors.grey.withOpacity(0.2), disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( @@ -38,9 +40,31 @@ class NotaExamenDisplayWidget extends StatelessWidget with WatchItMixin{ ), ), ), - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + TextInputFormatter.withFunction((prev, input) { + final val = input.text; + if(val.isEmpty) { // Si está vacío, no hacer nada + return input; + } + + final firstDigit = int.tryParse(val[0]); + if(firstDigit != null && (firstDigit < 1 || firstDigit > 7)) { // Si el primer dígito es menor a 1 o mayor a 7, no hacer nada + return prev; + } + + if(val.length == 1) { + return input; + } + + final secondDigit = int.tryParse(val[1]); + if(secondDigit != null && ((secondDigit < 0 || secondDigit > 9) || (firstDigit == 7 && secondDigit > 0)) || val.length > 3) { // Si el segundo dígito es menor a 0 o mayor a 9, o si el primer dígito es 7 y el segundo dígito es mayor a 0, no hacer nada + return prev; + } + + return input; + }), + ], ), ), ], diff --git a/lib/widgets/calculadora_notas/nota_final_display_widget.dart b/lib/widgets/calculadora_notas/nota_final_display_widget.dart index 6b70720..f47201b 100644 --- a/lib/widgets/calculadora_notas/nota_final_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_final_display_widget.dart @@ -1,22 +1,28 @@ import 'package:flutter/material.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:watch_it/watch_it.dart'; -class NotaFinalDisplayWidget extends StatelessWidget { +class NotaFinalDisplayWidget extends StatelessWidget with WatchItMixin { const NotaFinalDisplayWidget({ super.key, }); @override - Widget build(BuildContext context) => Column( - children: [ - Text(di.get().getCalculatedFinalGrade?.toStringAsFixed(1) ?? "--", - style: TextStyle( - fontSize: 40, - fontWeight: FontWeight.bold, - ), - ) - ], - ); + Widget build(BuildContext context) { + final calculatedFinalGrade = watchValue((CalculatorController controller) => controller.calculatedFinalGrade); + logger.d("calculatedFinalGrade: $calculatedFinalGrade"); + + return Column( + children: [ + Text(calculatedFinalGrade?.toStringAsFixed(1) ?? "--", + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + ), + ) + ], + ); + } } diff --git a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart index 8afbdfe..cc8c02e 100644 --- a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart @@ -3,14 +3,14 @@ import 'package:mi_utem/services_new/interfaces/controllers/calculator_controlle import 'package:mi_utem/themes/theme.dart'; import 'package:watch_it/watch_it.dart'; -class NotaPresentacionDisplayWidget extends StatelessWidget { +class NotaPresentacionDisplayWidget extends StatelessWidget with WatchItMixin { const NotaPresentacionDisplayWidget({ super.key, }); @override Widget build(BuildContext context) { - final _calculatorService = di.get(); + final calculatedPresentationGrade = watchValue((CalculatorController controller) => controller.calculatedPresentationGrade); return Row( children: [ const Text("Pres.", @@ -20,7 +20,7 @@ class NotaPresentacionDisplayWidget extends StatelessWidget { width: 80, margin: const EdgeInsets.only(left: 15), child: TextField( - controller: TextEditingController(text: _calculatorService.getCalculatedPresentationGrade?.toStringAsFixed(1) ?? ""), + controller: TextEditingController(text: calculatedPresentationGrade?.toStringAsFixed(1) ?? ""), textAlign: TextAlign.center, enabled: false, decoration: InputDecoration( diff --git a/lib/widgets/field_list_tile.dart b/lib/widgets/field_list_tile.dart index 5f3bfd5..15666e3 100644 --- a/lib/widgets/field_list_tile.dart +++ b/lib/widgets/field_list_tile.dart @@ -2,19 +2,19 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; class FieldListTile extends StatelessWidget { + final String title; + final String? value; + final EdgeInsetsGeometry padding; + const FieldListTile({ - Key? key, + super.key, required this.title, this.value, this.padding = const EdgeInsets.symmetric( vertical: 12, horizontal: 16, ), - }) : super(key: key); - - final String title; - final String? value; - final EdgeInsetsGeometry padding; + }); @override Widget build(BuildContext context) => Padding( diff --git a/lib/widgets/login_screen/formulario_credenciales.dart b/lib/widgets/login_screen/formulario_credenciales.dart index 71488bb..d98ba75 100644 --- a/lib/widgets/login_screen/formulario_credenciales.dart +++ b/lib/widgets/login_screen/formulario_credenciales.dart @@ -21,8 +21,8 @@ class _FormularioCredencialesState extends State { children: [ LoginTextFormField( controller: widget._correoController, - hintText: 'nombre@utem.cl', - labelText: 'Correo UTEM', + hintText: 'usuario@utem.cl', + labelText: 'Usuario/Correo UTEM', textCapitalization: TextCapitalization.none, keyboardType: TextInputType.emailAddress, inputFormatters: [ diff --git a/lib/widgets/nota_list_item.dart b/lib/widgets/nota_list_item.dart index 5b8a4a3..0d82824 100644 --- a/lib/widgets/nota_list_item.dart +++ b/lib/widgets/nota_list_item.dart @@ -1,12 +1,13 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/evaluacion.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:watch_it/watch_it.dart'; -class NotaListItem extends StatelessWidget { +class NotaListItem extends StatelessWidget with WatchItMixin { final IEvaluacion evaluacion; final bool editable; final TextEditingController? gradeController; @@ -14,19 +15,15 @@ class NotaListItem extends StatelessWidget { final Function(IEvaluacion)? onChanged; final Function()? onDelete; - NotaListItem({ - Key? key, + const NotaListItem({ + super.key, required this.evaluacion, this.editable = false, this.gradeController, this.percentageController, this.onChanged, this.onDelete, - }) : super(key: key); - - String get _suggestedGrade => di.get().getSuggestedGrade?.toStringAsFixed(1) ?? "0.0"; - - String? get _suggestedPercentage => di.get().getSuggestedPercentage?.toStringAsFixed(0); + }); @override Widget build(BuildContext context) { @@ -41,7 +38,11 @@ class NotaListItem extends StatelessWidget { final showSuggestedGrade = editable; - final hintText = showSuggestedGrade ? _suggestedGrade : "--"; + final suggestedGrade = watchValue((CalculatorController controller) => controller.suggestedGrade); + final suggestedPercentage = watchValue((CalculatorController controller) => controller.suggestedPercentage); + + + final hintText = showSuggestedGrade ? (suggestedGrade?.toStringAsFixed(0) ?? "--") : "--"; return Flex( direction: Axis.horizontal, @@ -61,28 +62,47 @@ class NotaListItem extends StatelessWidget { controller: gradeController ?? defaultGradeController, enabled: editable, onChanged: (String value) { - final grade = double.tryParse( - value.replaceAll(",", "."), - ); + final grade = double.tryParse(value.replaceAll(",", ".")); final changedGrade = evaluacion.copyWith(nota: grade); changedGrade.nota = grade; onChanged?.call(changedGrade); - - //_controller.changeGradeAt(widget.index, changedGrade); }, textAlign: TextAlign.center, decoration: InputDecoration( hintText: hintText, - disabledBorder: - MainTheme.theme.inputDecorationTheme.border!.copyWith( + disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( color: Colors.transparent, ), ), ), keyboardType: TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + TextInputFormatter.withFunction((prev, input) { + final val = input.text; + if(val.isEmpty) { // Si está vacío, no hacer nada + return input; + } + + final firstDigit = int.tryParse(val[0]); + if(firstDigit != null && (firstDigit < 1 || firstDigit > 7)) { // Si el primer dígito es menor a 1 o mayor a 7, no hacer nada + return prev; + } + + if(val.length == 1) { + return input; + } + + final secondDigit = int.tryParse(val[1]); + if(secondDigit != null && ((secondDigit < 0 || secondDigit > 9) || (firstDigit == 7 && secondDigit > 0)) || val.length > 3) { // Si el segundo dígito es menor a 0 o mayor a 9, o si el primer dígito es 7 y el segundo dígito es mayor a 0, no hacer nada + return prev; + } + + return input; + }), + ], ), ), ), @@ -101,16 +121,34 @@ class NotaListItem extends StatelessWidget { }, enabled: editable, decoration: InputDecoration( - hintText: _suggestedPercentage ?? "Peso", + hintText: suggestedPercentage?.toStringAsFixed(0) ?? "Peso", suffixText: "%", - disabledBorder: - MainTheme.theme.inputDecorationTheme.border!.copyWith( + disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( color: Colors.transparent, ), ), ), - keyboardType: TextInputType.phone, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(3), + // Solo permitir números entre 0 y 100 + TextInputFormatter.withFunction((prev, input) { + final val = input.text; + if(val.isEmpty) { // Si está vacío, no hacer nada + return input; + } + + final number = int.tryParse(val); + if(number == null) { // Si no es un número, no hacer nada + return prev; + } + + return number > 100 ? prev : input; + }), + + ], ), ), ), diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index bb067c9..a13471d 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -9,14 +9,11 @@ import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/noticias/noticia_card_widget.dart'; import 'package:watch_it/watch_it.dart'; -class NoticiasCarruselWidget extends StatefulWidget { - NoticiasCarruselWidget({Key? key}) : super(key: key); +class NoticiasCarruselWidget extends StatelessWidget { - @override - State createState() => _NoticiasCarruselWidgetState(); -} - -class _NoticiasCarruselWidgetState extends State { + const NoticiasCarruselWidget({ + super.key, + }); @override Widget build(BuildContext context) => Column( @@ -54,7 +51,7 @@ class _NoticiasCarruselWidgetState extends State { options: CarouselOptions( autoPlay: true, height: 200, - viewportFraction: 0.5, + viewportFraction: MediaQuery.of(context).orientation == Orientation.landscape ? 0.3 : 0.5, initialPage: 0, ), itemBuilder: (BuildContext context, int i, int rI) => NoticiaCardWidget(noticias[i]), diff --git a/lib/widgets/permisos_section.dart b/lib/widgets/permisos_section.dart index c881167..3d7c003 100644 --- a/lib/widgets/permisos_section.dart +++ b/lib/widgets/permisos_section.dart @@ -14,56 +14,54 @@ class PermisosCovidSection extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - Text("Permisos activos".toUpperCase(), - style: Get.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.bold, - ), + Widget build(BuildContext context) => Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Text("Permisos activos".toUpperCase(), + style: Get.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, ), - ], - mainAxisAlignment: MainAxisAlignment.spaceBetween, - ), + ), + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, ), - SizedBox(height: 10), - SizedBox( - height: 155, - child: FutureBuilder?>( - future: di.get().getPermisos(), - builder: (context, snapshot) { - if(snapshot.connectionState == ConnectionState.waiting) { - return LoadingIndicator( - message: "Esto tardará un poco, paciencia...", - ); - } + ), + SizedBox(height: 10), + SizedBox( + height: 155, + child: FutureBuilder?>( + future: di.get().getPermisos(), + builder: (context, snapshot) { + if(snapshot.connectionState == ConnectionState.waiting) { + return LoadingIndicator( + message: "Esto tardará un poco, paciencia...", + ); + } - if(snapshot.hasError) { - final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al cargar los permisos"; - return Text(error); - } + if(snapshot.hasError) { + final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al cargar los permisos"; + return Text(error); + } - if(snapshot.data == null || snapshot.data?.isNotEmpty != true) { - return Text('No hay permisos de ingresos'); - } + if(snapshot.data == null || snapshot.data?.isNotEmpty != true) { + return Text('No hay permisos de ingresos'); + } - return ListView.separated( - itemCount: snapshot.data!.length, - padding: EdgeInsets.symmetric(horizontal: 20), - scrollDirection: Axis.horizontal, - separatorBuilder: (context, index) => Container(width: 10), - itemBuilder: (context, index) => PermisoCard( - permiso: snapshot.data![index], - ), - ); - }, - ), + return ListView.separated( + itemCount: snapshot.data!.length, + padding: EdgeInsets.symmetric(horizontal: 20), + scrollDirection: Axis.horizontal, + separatorBuilder: (context, index) => Container(width: 10), + itemBuilder: (context, index) => PermisoCard( + permiso: snapshot.data![index], + ), + ); + }, ), - ], - ); - } + ), + ], + ); } diff --git a/pubspec.lock b/pubspec.lock index 0b6cc01..65b0e32 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1017,6 +1017,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.20" + listenable_collections: + dependency: "direct main" + description: + name: listenable_collections + sha256: "49dd3be0955d8c109580184b5f43ee88a269df64baba20a35c16fd9ddd9fb62e" + url: "https://pub.dev" + source: hosted + version: "1.0.0" logger: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 196e9a6..293c939 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,6 +80,7 @@ dependencies: watch_it: ^1.0.2 extended_masked_text: ^2.3.1 crypto: ^3.0.3 + listenable_collections: ^1.0.0 dependency_overrides: qr: ^3.0.0 From eda1ae0a83f8c7d60b6dfb2116c6d64af9634897 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:19:40 -0300 Subject: [PATCH 026/194] =?UTF-8?q?patch:=20arreglos=20y=20orden=20de=20cl?= =?UTF-8?q?ases=20*=20Se=20ordenan=20algunos=20modelos=20*=20Se=20repara?= =?UTF-8?q?=20carga=20de=20notas=20desde=20asignatura=20*=20Se=20repara=20?= =?UTF-8?q?string=5Futils=20*=20Se=20mueven=20evaluaci=C3=B3n=20y=20grades?= =?UTF-8?q?=20a=20su=20propia=20carpeta=20*=20Se=20mueve=20asignatura=20a?= =?UTF-8?q?=20la=20carpeta=20asignaturas=20*=20Se=20finaliza=20la=20migrac?= =?UTF-8?q?i=C3=B3n=20a=20snackbars=20nativos=20*=20Se=20contin=C3=BAa=20l?= =?UTF-8?q?a=20migraci=C3=B3n=20a=20navegaci=C3=B3n=20nativa.=20*=20Arregl?= =?UTF-8?q?os=20generales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/controllers/notification_controller.dart | 2 +- lib/models/asignatura.dart | 155 ------------------ lib/models/asignaturas/asignatura.dart | 121 ++++++++++++++ lib/models/{ => evaluacion}/evaluacion.dart | 0 lib/models/{ => evaluacion}/grades.dart | 2 +- lib/models/horario.dart | 2 +- .../asignatura/asignatura_detalle_screen.dart | 2 +- .../asignatura_estudiantes_tab.dart | 2 +- .../asignatura/asignatura_notas_tab.dart | 4 +- .../asignatura/asignatura_resumen_tab.dart | 2 +- .../asignatura/asignaturas_lista_screen.dart | 2 +- lib/screens/calculadora_notas_screen.dart | 2 +- lib/screens/usuario_screen.dart | 57 ++----- lib/services/notification_service.dart | 2 +- .../implementations/asignaturas_service.dart | 2 +- .../controllers/calculator_controller.dart | 8 +- .../controllers/horario_controller.dart | 2 +- .../implementations/grades_service.dart | 4 +- .../interfaces/asignaturas_service.dart | 2 +- .../controllers/calculator_controller.dart | 4 +- .../controllers/horario_controller.dart | 2 +- .../interfaces/grades_service.dart | 2 +- lib/utils/string_utils.dart | 6 +- .../lista/asignatura_list_tile.dart | 2 +- .../asignatura/lista/lista_asignaturas.dart | 2 +- lib/widgets/asistencia_chart.dart | 2 +- .../editar_notas_widget.dart | 2 +- .../nota_final_display_widget.dart | 2 - .../notas_calculadora_widget.dart | 2 +- lib/widgets/nota_list_item.dart | 2 +- lib/widgets/profile_photo.dart | 60 +++---- 31 files changed, 187 insertions(+), 274 deletions(-) delete mode 100644 lib/models/asignatura.dart create mode 100644 lib/models/asignaturas/asignatura.dart rename lib/models/{ => evaluacion}/evaluacion.dart (100%) rename lib/models/{ => evaluacion}/grades.dart (92%) diff --git a/lib/controllers/notification_controller.dart b/lib/controllers/notification_controller.dart index 2c6eaeb..ac9a159 100644 --- a/lib/controllers/notification_controller.dart +++ b/lib/controllers/notification_controller.dart @@ -5,7 +5,7 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:flutter/material.dart'; import 'package:mi_utem/main.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; diff --git a/lib/models/asignatura.dart b/lib/models/asignatura.dart deleted file mode 100644 index abf2045..0000000 --- a/lib/models/asignatura.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mi_utem/models/grades.dart'; -import 'package:mi_utem/models/usuario.dart'; -import 'package:mi_utem/themes/theme.dart'; -import 'package:recase/recase.dart'; - -class Asignatura { - String? id; - String? nombre; - String? codigo; - String? tipoHora; - String? estado; - String? docente; - String? seccion; - - Asistencia? asistencia; - Grades? grades; - List? estudiantes; - String? tipoAsignatura; - num? intentos; - String? horario; - String? sala; - String? tipoSala; - - Asignatura({ - this.id, - this.nombre, - this.codigo, - this.tipoHora, - this.estado, - this.docente, - this.seccion, - this.asistencia, - this.grades, - this.estudiantes, - this.tipoAsignatura, - this.sala, - this.horario, - this.intentos, - this.tipoSala, - }); - - Color get colorPorEstado { - switch (estado) { - case "Aprobado": - return MainTheme.aprobadoColor; - case "Reprobado": - return MainTheme.reprobadoColor; - default: - return MainTheme.inscritoColor; - } - } - - factory Asignatura.fromJson(Map? json) { - if (json == null) { - return Asignatura(); - } - - return Asignatura( - id: json['id'], - codigo: json['codigo'], - nombre: ReCase(json['nombre'] ?? '').titleCase, - tipoHora: ReCase(json['tipoHora'] ?? '').titleCase, - estado: ReCase(json['estado'] ?? '').titleCase, - docente: ReCase(json['docente'] ?? '').titleCase, - seccion: json['seccion'], - grades: json['notas'] != null ? Grades.fromJson(json['notas']) : null, - // estudiantes: Usuario.fromJsonList(json["estudiantes"]), - asistencia: Asistencia(asistidos: json['asistenciaAlDia']), - // tipoAsignatura: ReCase(json['tipoAsignatura'].toString()).titleCase, - sala: ReCase(json['sala'] ?? '').titleCase, - horario: json['horario'], - intentos: - json['intentos'] != null ? int.parse(json['intentos'].toString()) : 0, - tipoSala: ReCase(json['tipoSala'] ?? '').titleCase, - ); - } - - static List fromJsonList(dynamic json) { - if (json == null) { - return []; - } - List list = []; - for (var item in json) { - list.add(Asignatura.fromJson(item)); - } - return list; - } - - Map toJson() { - return { - 'id': id, - 'codigo': codigo, - 'nombre': nombre, - 'tipoHora': tipoHora, - 'estado': estado, - 'docente': docente, - 'seccion': seccion, - 'estudiantes': estudiantes, - 'notas': grades?.toJson(), - 'asistencia': asistencia?.toJson(), - 'tipoAsignatura': tipoAsignatura, - 'sala': sala, - 'horario': horario, - 'intentos': intentos, - 'tipoSala': tipoSala, - }; - } -} - -class Asistencia { - num? total; - num? asistidos; - num? noAsistidos; - num? sinRegistro; - - Asistencia({ - this.total = 0, - this.asistidos = 0, - this.noAsistidos = 0, - this.sinRegistro = 0, - }); - - factory Asistencia.fromJson(Map? json) { - if (json == null) { - return Asistencia(); - } - return Asistencia( - total: json['total'] ?? 0, - asistidos: json['asistida'] ?? 0, - noAsistidos: json['noAsistidos'] ?? 0, - sinRegistro: json['sinRegistro'] ?? 0, - ); - } - - static List fromJsonList(dynamic json) { - if (json == null) { - return []; - } - List list = []; - for (var item in json) { - list.add(Asistencia.fromJson(item)); - } - return list; - } - - Map toJson() { - return { - 'total': total, - 'asistidos': asistidos, - 'noAsistidos': noAsistidos, - 'sinRegistro': sinRegistro, - }; - } -} diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart new file mode 100644 index 0000000..536d648 --- /dev/null +++ b/lib/models/asignaturas/asignatura.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/evaluacion/grades.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/utils/string_utils.dart'; + +class Asignatura { + String? id; + String? nombre; + String? codigo; + String? tipoHora; + String? estado; + String? docente; + String? seccion; + + Asistencia? asistencia; + Grades? grades; + List? estudiantes; + String? tipoAsignatura; + num? intentos; + String? horario; + String? sala; + String? tipoSala; + + Asignatura({ + this.id, + this.nombre, + this.codigo, + this.tipoHora, + this.estado, + this.docente, + this.seccion, + this.asistencia, + this.grades, + this.estudiantes, + this.tipoAsignatura, + this.sala, + this.horario, + this.intentos, + this.tipoSala, + }); + + Color get colorPorEstado { + switch (estado) { + case "Aprobado": + return MainTheme.aprobadoColor; + case "Reprobado": + return MainTheme.reprobadoColor; + default: + return MainTheme.inscritoColor; + } + } + + factory Asignatura.fromJson(Map? json) => json != null ? Asignatura( + id: json['id'], + codigo: json['codigo'], + nombre: capitalize(json['nombre'] ?? ''), + tipoHora: capitalize(json['tipoHora'] ?? ''), + estado: capitalize(json['estado'] ?? ''), + docente: capitalize(json['docente'] ?? ''), + seccion: json['seccion'], + grades: json.containsKey('notas') ? Grades.fromJson(json['notas']) : null, + estudiantes: User.fromJsonList(json["estudiantes"]), + asistencia: Asistencia(asistidos: json['asistenciaAlDia']), + tipoAsignatura: capitalize(json['tipoAsignatura'] as String), + sala: capitalize(json['sala'] ?? ''), + horario: json['horario'], + intentos: int.tryParse(json['intentos'] ?? '0') ?? 0, + tipoSala: capitalize(json['tipoSala'] ?? ''), + ) : Asignatura(); + + static List fromJsonList(dynamic json) => json != null ? (json as List).map((it) => Asignatura.fromJson(it)).toList() : []; + + Map toJson() => { + 'id': id, + 'codigo': codigo, + 'nombre': nombre, + 'tipoHora': tipoHora, + 'estado': estado, + 'docente': docente, + 'seccion': seccion, + 'estudiantes': estudiantes, + 'notas': grades?.toJson(), + 'asistencia': asistencia?.toJson(), + 'tipoAsignatura': tipoAsignatura, + 'sala': sala, + 'horario': horario, + 'intentos': intentos, + 'tipoSala': tipoSala, + }; +} + +class Asistencia { + num? total; + num? asistidos; + num? noAsistidos; + num? sinRegistro; + + Asistencia({ + this.total = 0, + this.asistidos = 0, + this.noAsistidos = 0, + this.sinRegistro = 0, + }); + + factory Asistencia.fromJson(Map? json) => json != null ? Asistencia( + total: json['total'] ?? 0, + asistidos: json['asistida'] ?? 0, + noAsistidos: json['noAsistidos'] ?? 0, + sinRegistro: json['sinRegistro'] ?? 0, + ) : Asistencia(); + + static List fromJsonList(dynamic json) => json != null ? (json as List).map((it) => Asistencia.fromJson(it)).toList() : []; + + Map toJson() => { + 'total': total, + 'asistidos': asistidos, + 'noAsistidos': noAsistidos, + 'sinRegistro': sinRegistro, + }; +} diff --git a/lib/models/evaluacion.dart b/lib/models/evaluacion/evaluacion.dart similarity index 100% rename from lib/models/evaluacion.dart rename to lib/models/evaluacion/evaluacion.dart diff --git a/lib/models/grades.dart b/lib/models/evaluacion/grades.dart similarity index 92% rename from lib/models/grades.dart rename to lib/models/evaluacion/grades.dart index 761cb2d..79fb0ae 100644 --- a/lib/models/grades.dart +++ b/lib/models/evaluacion/grades.dart @@ -1,4 +1,4 @@ -import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; class Grades { List notasParciales; diff --git a/lib/models/horario.dart b/lib/models/horario.dart index f7d9869..863070e 100644 --- a/lib/models/horario.dart +++ b/lib/models/horario.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; class Horario { List? asignaturas; diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index bc8261d..8040f0f 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_estudiantes_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; diff --git a/lib/screens/asignatura/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/asignatura_estudiantes_tab.dart index 76e4ba1..53cb247 100644 --- a/lib/screens/asignatura/asignatura_estudiantes_tab.dart +++ b/lib/screens/asignatura/asignatura_estudiantes_tab.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index c5ba8ab..b0fc269 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/models/asignatura.dart'; -import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/asignatura_resumen_tab.dart index 67f4952..9d065c3 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/asignatura_resumen_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/utils/string_utils.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index 487112a..9f2ea83 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index d6ea614..f621bf7 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/widgets/calculadora_notas/display_notas_widget.dart'; import 'package:mi_utem/widgets/calculadora_notas/editar_notas_widget.dart'; diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index 72e293c..4856ad0 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -2,8 +2,7 @@ import 'dart:core'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/docentes_service.dart'; import 'package:mi_utem/services/review_service.dart'; @@ -14,6 +13,7 @@ import 'package:mi_utem/widgets/image_view_screen.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:watch_it/watch_it.dart'; @@ -81,31 +81,16 @@ class _UsuarioScreenState extends State { } Future _changeFoto(String imagen) async { - Get.dialog( - LoadingDialog(), - barrierDismissible: false, - ); + showLoadingDialog(context); try { - User? user = await _authService.updateProfilePicture(imagen); - Get.back(); - - setState(() { - _user = user; - }); - - return; + final user = await _authService.updateProfilePicture(imagen); + Navigator.pop(context); + setState(() => _user = user); } catch (e) { - Get.back(); + Navigator.pop(context); print("Error cambiando la imagen ${e.toString()}"); - Get.snackbar( - "Error", - "No se pudo cambiar la foto", - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); + showErrorSnackbar(context, "No se pudo cambiar la foto."); } } @@ -120,7 +105,7 @@ class _UsuarioScreenState extends State { title: Text("Nombre", style: TextStyle(color: Colors.grey), ), - subtitle: Text(_user!.nombreCompleto!, + subtitle: Text(_user!.nombreCompleto, style: TextStyle( color: Colors.grey[900], fontSize: 18, @@ -170,14 +155,7 @@ class _UsuarioScreenState extends State { ), onLongPress: widget.tipo != 0 ? () async { await FlutterClipboard.copy(_user!.correoUtem!); - Get.snackbar( - "¡Copiado!", - "Correo copiado al portapapeles", - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); + showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); } : null, onTap: widget.tipo != 0 ? () async { await launchUrl(Uri.parse("mailto:${_user?.correoUtem ?? ""}")); @@ -190,7 +168,7 @@ class _UsuarioScreenState extends State { ), )); } - if (_user?.correoPersonal?.isEmpty == false) { + if (_user?.correoPersonal.isEmpty == false) { lista.add(Divider(height: 1)); lista.add( ListTile( @@ -198,20 +176,13 @@ class _UsuarioScreenState extends State { style: TextStyle(color: Colors.grey), ), onLongPress: widget.tipo != 0 ? () async { - await FlutterClipboard.copy(_user!.correoPersonal!); - Get.snackbar( - "¡Copiado!", - "Correo copiado al portapapeles", - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); + await FlutterClipboard.copy(_user!.correoPersonal); + showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); } : null, onTap: widget.tipo != 0 ? () async { await launchUrl(Uri.parse("mailto:${_user?.correoPersonal ?? ""}")); } : null, - subtitle: Text(_user!.correoPersonal ?? "", + subtitle: Text(_user!.correoPersonal, style: TextStyle( color: Colors.grey[900], fontSize: 18, diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 77c40d9..ebe3dc5 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -5,7 +5,7 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:get/get.dart'; import 'package:mi_utem/controllers/notification_controller.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/widgets/custom_alert_dialog.dart'; class NotificationService { diff --git a/lib/services_new/implementations/asignaturas_service.dart b/lib/services_new/implementations/asignaturas_service.dart index 78976e3..806016f 100644 --- a/lib/services_new/implementations/asignaturas_service.dart +++ b/lib/services_new/implementations/asignaturas_service.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; diff --git a/lib/services_new/implementations/controllers/calculator_controller.dart b/lib/services_new/implementations/controllers/calculator_controller.dart index fd95173..dca0265 100644 --- a/lib/services_new/implementations/controllers/calculator_controller.dart +++ b/lib/services_new/implementations/controllers/calculator_controller.dart @@ -1,8 +1,8 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:listenable_collections/listenable_collections.dart'; -import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/models/grades.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; +import 'package:mi_utem/models/evaluacion/grades.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; class CalculatorControllerImplementation implements CalculatorController { @@ -96,8 +96,8 @@ class CalculatorControllerImplementation implements CalculatorController { @override void updateWithGrades(Grades grades) { partialGrades.clear(); - percentageTextFieldControllers.value.clear(); - gradeTextFieldControllers.value.clear(); + percentageTextFieldControllers.clear(); + gradeTextFieldControllers.clear(); for(final grade in grades.notasParciales) { addGrade(IEvaluacion.fromRemote(grade)); diff --git a/lib/services_new/implementations/controllers/horario_controller.dart b/lib/services_new/implementations/controllers/horario_controller.dart index 73df0cb..9df883b 100644 --- a/lib/services_new/implementations/controllers/horario_controller.dart +++ b/lib/services_new/implementations/controllers/horario_controller.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; diff --git a/lib/services_new/implementations/grades_service.dart b/lib/services_new/implementations/grades_service.dart index 9724b8d..625f7fa 100644 --- a/lib/services_new/implementations/grades_service.dart +++ b/lib/services_new/implementations/grades_service.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/secure_storage.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/evaluacion/grades.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/models/grades.dart'; import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; diff --git a/lib/services_new/interfaces/asignaturas_service.dart b/lib/services_new/interfaces/asignaturas_service.dart index 543e14d..be1cc54 100644 --- a/lib/services_new/interfaces/asignaturas_service.dart +++ b/lib/services_new/interfaces/asignaturas_service.dart @@ -1,4 +1,4 @@ -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; abstract class AsignaturasService { diff --git a/lib/services_new/interfaces/controllers/calculator_controller.dart b/lib/services_new/interfaces/controllers/calculator_controller.dart index b7cf995..90b178b 100644 --- a/lib/services_new/interfaces/controllers/calculator_controller.dart +++ b/lib/services_new/interfaces/controllers/calculator_controller.dart @@ -1,8 +1,8 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:listenable_collections/listenable_collections.dart'; -import 'package:mi_utem/models/evaluacion.dart'; -import 'package:mi_utem/models/grades.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; +import 'package:mi_utem/models/evaluacion/grades.dart'; abstract class CalculatorController { diff --git a/lib/services_new/interfaces/controllers/horario_controller.dart b/lib/services_new/interfaces/controllers/horario_controller.dart index 11d4b7d..dc75156 100644 --- a/lib/services_new/interfaces/controllers/horario_controller.dart +++ b/lib/services_new/interfaces/controllers/horario_controller.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/horario.dart'; abstract class HorarioController { diff --git a/lib/services_new/interfaces/grades_service.dart b/lib/services_new/interfaces/grades_service.dart index aae7b74..2ccd629 100644 --- a/lib/services_new/interfaces/grades_service.dart +++ b/lib/services_new/interfaces/grades_service.dart @@ -1,4 +1,4 @@ -import 'package:mi_utem/models/grades.dart'; +import 'package:mi_utem/models/evaluacion/grades.dart'; abstract class GradesService { diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index 5f758eb..e6ac5ba 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -1,3 +1,3 @@ -// Capitalize the first letter of a string, and if it has more than one word, capitalize the first letter of each word. -// Also does nothing if the string is null or empty. -String capitalize(String s) => s.split(" ").map((e) => e.isNotEmpty ? e[0].toUpperCase() + e.substring(1) : "").join(" "); \ No newline at end of file +String firstLetterUpperCase(String s) => s.isNotEmpty ? s[0].toUpperCase() + (s.length > 1 ? s.substring(1) : "").toLowerCase() : ""; + +String capitalize(String s) => s.split(" ").map(firstLetterUpperCase).join(" "); \ No newline at end of file diff --git a/lib/widgets/asignatura/lista/asignatura_list_tile.dart b/lib/widgets/asignatura/lista/asignatura_list_tile.dart index e96be54..0130f4c 100644 --- a/lib/widgets/asignatura/lista/asignatura_list_tile.dart +++ b/lib/widgets/asignatura/lista/asignatura_list_tile.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/themes/theme.dart'; diff --git a/lib/widgets/asignatura/lista/lista_asignaturas.dart b/lib/widgets/asignatura/lista/lista_asignaturas.dart index 5679e37..645789d 100644 --- a/lib/widgets/asignatura/lista/lista_asignaturas.dart +++ b/lib/widgets/asignatura/lista/lista_asignaturas.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/widgets/asignatura/lista/asignatura_list_tile.dart'; class ListaAsignaturas extends StatelessWidget { diff --git a/lib/widgets/asistencia_chart.dart b/lib/widgets/asistencia_chart.dart index da2c689..07f61d4 100644 --- a/lib/widgets/asistencia_chart.dart +++ b/lib/widgets/asistencia_chart.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:community_charts_flutter/community_charts_flutter.dart' as charts; import 'package:flutter/material.dart'; -import 'package:mi_utem/models/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/themes/theme.dart'; extension StringExtension on Color { diff --git a/lib/widgets/calculadora_notas/editar_notas_widget.dart b/lib/widgets/calculadora_notas/editar_notas_widget.dart index 9c7d132..1dabda8 100644 --- a/lib/widgets/calculadora_notas/editar_notas_widget.dart +++ b/lib/widgets/calculadora_notas/editar_notas_widget.dart @@ -1,6 +1,6 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/calculadora_notas/modo_simulacion_widget.dart'; import 'package:mi_utem/widgets/calculadora_notas/notas_calculadora_widget.dart'; diff --git a/lib/widgets/calculadora_notas/nota_final_display_widget.dart b/lib/widgets/calculadora_notas/nota_final_display_widget.dart index f47201b..4b4eaad 100644 --- a/lib/widgets/calculadora_notas/nota_final_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_final_display_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:watch_it/watch_it.dart'; @@ -12,7 +11,6 @@ class NotaFinalDisplayWidget extends StatelessWidget with WatchItMixin { @override Widget build(BuildContext context) { final calculatedFinalGrade = watchValue((CalculatorController controller) => controller.calculatedFinalGrade); - logger.d("calculatedFinalGrade: $calculatedFinalGrade"); return Column( children: [ diff --git a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart index dce2bc3..760782d 100644 --- a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart +++ b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart @@ -1,6 +1,6 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; diff --git a/lib/widgets/nota_list_item.dart b/lib/widgets/nota_list_item.dart index 0d82824..61e29da 100644 --- a/lib/widgets/nota_list_item.dart +++ b/lib/widgets/nota_list_item.dart @@ -2,7 +2,7 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/models/evaluacion.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:watch_it/watch_it.dart'; diff --git a/lib/widgets/profile_photo.dart b/lib/widgets/profile_photo.dart index a19fb8f..cfb675b 100644 --- a/lib/widgets/profile_photo.dart +++ b/lib/widgets/profile_photo.dart @@ -6,10 +6,11 @@ import 'package:circular_profile_avatar/circular_profile_avatar.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/imagen_editor_modal.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; class ProfilePhoto extends StatefulWidget { final double radius; @@ -45,12 +46,9 @@ class _ProfilePhotoState extends State { width: widget.radius * 2, height: widget.radius * 2, child: Stack( - children: [ - CircularProfileAvatar( - widget.user!.fotoUrl ?? "", - onTap: () => widget.onTap != null && widget.onImageTap == null - ? widget.onTap - : null, + children: [ + CircularProfileAvatar(widget.user!.fotoUrl ?? "", + onTap: () => widget.onTap != null && widget.onImageTap == null ? widget.onTap : null, borderColor: widget.borderColor, borderWidth: widget.borderWidth, radius: widget.radius, @@ -85,44 +83,24 @@ class _ProfilePhotoState extends State { InkWell( onTap: () async { try { - final imagen = await _picker.pickImage( - source: ImageSource.gallery, - ); - if (imagen != null) { - Uint8List imagenOriginalBytes = - File(imagen.path).readAsBytesSync(); + final imagen = await _picker.pickImage(source: ImageSource.gallery); + if (imagen == null) { + showErrorSnackbar(context, "No se pudo obtener la foto"); + return; + } - Uint8List imagenEditadaBytes = await Get.to( - () => ImagenEditorModal( - imagenInicial: imagenOriginalBytes, - aspectRatio: 1, - ), - ); + Uint8List imagenOriginalBytes = + File(imagen.path).readAsBytesSync(); - String imagenEditadaBase64 = - base64Encode(imagenEditadaBytes); + Uint8List imagenEditadaBytes = await Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImagenEditorModal(imagenInicial: imagenOriginalBytes, aspectRatio: 1))); - widget.onImage!(imagenEditadaBase64); - } else { - Get.snackbar( - "Error", - "No se pudo obtener la foto", - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); - } + String imagenEditadaBase64 = + base64Encode(imagenEditadaBytes); + + widget.onImage!(imagenEditadaBase64); } catch (e) { - print(e); - Get.snackbar( - "Error", - "No se pudo cambiar la foto", - colorText: Colors.white, - backgroundColor: Get.theme.primaryColor, - snackPosition: SnackPosition.BOTTOM, - margin: EdgeInsets.all(20), - ); + logger.e("Error al cambiar la foto", e); + showErrorSnackbar(context, "No se pudo cambiar la foto"); } }, borderRadius: BorderRadius.circular(25), From 08b048b61b2b34ed4eb680b575fa9f7d9ead7c92 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:31:11 -0300 Subject: [PATCH 027/194] patch: se repara credencial del estudiante Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/credencial_screen.dart | 80 +++++++++-------- lib/widgets/quick_menu_card.dart | 136 ++++++++++++++--------------- 2 files changed, 111 insertions(+), 105 deletions(-) diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index a8b347a..afffbb5 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -14,7 +14,7 @@ import 'package:mi_utem/widgets/flip_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:watch_it/watch_it.dart'; -class CredencialScreen extends StatefulWidget { +class CredencialScreen extends StatefulWidget with WatchItStatefulWidgetMixin { CredencialScreen({ Key? key, }) : super(key: key); @@ -24,7 +24,6 @@ class CredencialScreen extends StatefulWidget { } class _CredencialScreenState extends State { - User? _usuario; FlipController _flipController = FlipController(); @override @@ -65,8 +64,15 @@ class _CredencialScreenState extends State { ], ), backgroundColor: Colors.grey[200], - body: FutureBuilder( - future: di.get().getUser(), + body: FutureBuilder( + future: () async { + final user = await di.get().getUser(); + if(carreraActiva == null) { + await di.get().getCarreras(forceRefresh: true); + } + + return user; + }(), builder: (context, snapshot) { if (snapshot.hasError) { return CustomErrorWidget( @@ -75,58 +81,60 @@ class _CredencialScreenState extends State { ); } - if (snapshot.hasData) { - if (_usuario!.rut != null && carreraActiva!.nombre != null && carreraActiva.nombre!.isNotEmpty) { - return Center( - child: SafeArea( - child: CredencialCard( - user: _usuario, - carrera: carreraActiva, - controller: _flipController, - onFlip: (direction) => _onFlip(), - ), - ), - ); - } + final user = snapshot.data; + if (!snapshot.hasData || user == null) { return Container( padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text("😕", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 50, + const Expanded( + child: Center( + child: LoadingIndicator(), ), ), - const SizedBox(height: 15), - const Text("Ocurrió un error al generar tu credencial", - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - const SizedBox(height: 15), - Text(snapshot.error.toString(), - textAlign: TextAlign.center, - ), ], ), ); } + if (user.rut != null && carreraActiva!.nombre != null && carreraActiva.nombre!.isNotEmpty) { + return Center( + child: SafeArea( + child: CredencialCard( + user: user, + carrera: carreraActiva, + controller: _flipController, + onFlip: (direction) => _onFlip(), + ), + ), + ); + } + return Container( padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Expanded( - child: Center( - child: LoadingIndicator(), + const Text("😕", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 50, ), ), + const SizedBox(height: 15), + const Text("Ocurrió un error al generar tu credencial", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 15), + Text(snapshot.error.toString(), + textAlign: TextAlign.center, + ), ], ), ); diff --git a/lib/widgets/quick_menu_card.dart b/lib/widgets/quick_menu_card.dart index 23413d9..d53f2b7 100644 --- a/lib/widgets/quick_menu_card.dart +++ b/lib/widgets/quick_menu_card.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:gradient_widgets/gradient_widgets.dart'; import 'package:hexcolor/hexcolor.dart'; import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; @@ -8,10 +7,13 @@ import 'package:mi_utem/screens/credencial_screen.dart'; import 'package:mi_utem/screens/horario/horario_screen.dart'; class QuickMenuCard extends StatelessWidget { - const QuickMenuCard({Key? key, required this.card}) : super(key: key); - final Map card; + const QuickMenuCard({ + super.key, + required this.card + }); + Widget? get _route { switch (card["route"]) { case "/AsignaturasScreen": @@ -28,76 +30,72 @@ class QuickMenuCard extends StatelessWidget { } @override - Widget build(BuildContext context) { - return Container( - height: 130, - width: 150, - child: GradientCard( - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - gradient: LinearGradient( - colors: card["degradado"]["colors"] - .map((dynamic c) => HexColor(c.toString())) - .toList(), - stops: card["degradado"]["stops"] - ?.map((num s) => s.toDouble()) - .toList(), - begin: card["degradado"]["begin"] != null - ? Alignment( - card["degradado"]["begin"][0].toDouble(), - card["degradado"]["begin"][1].toDouble(), - ) - : Alignment.centerLeft, - end: card["degradado"]["end"] != null - ? Alignment( - card["degradado"]["end"][0].toDouble(), - card["degradado"]["end"][1].toDouble(), - ) - : Alignment.centerRight, - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - final route = _route; - if(route != null) { - Navigator.push(context, MaterialPageRoute(builder: (ctx) => route)); - } - }, - borderRadius: BorderRadius.all(Radius.circular(15)), - child: Container( - padding: EdgeInsets.all(20), - width: double.infinity, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - IconData( - card["icono"]["codePoint"], - fontFamily: card["icono"]["fontFamily"], - fontPackage: card["icono"]["fontPackage"], - ), - color: Colors.white, - size: 30, + Widget build(BuildContext context) => Container( + height: 130, + width: 150, + child: GradientCard( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + gradient: LinearGradient( + colors: card["degradado"]["colors"] + .map((dynamic c) => HexColor(c.toString())) + .toList(), + stops: card["degradado"]["stops"] + ?.map((num s) => s.toDouble()) + .toList(), + begin: card["degradado"]["begin"] != null + ? Alignment( + card["degradado"]["begin"][0].toDouble(), + card["degradado"]["begin"][1].toDouble(), + ) + : Alignment.centerLeft, + end: card["degradado"]["end"] != null + ? Alignment( + card["degradado"]["end"][0].toDouble(), + card["degradado"]["end"][1].toDouble(), + ) + : Alignment.centerRight, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final route = _route; + if(route != null) { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => route)); + } + }, + borderRadius: BorderRadius.all(Radius.circular(15)), + child: Container( + padding: EdgeInsets.all(20), + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + IconData(card["icono"]["codePoint"], + fontFamily: card["icono"]["fontFamily"], + fontPackage: card["icono"]["fontPackage"], ), - Container(height: 10), - Text( - card["nombre"], - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Get.textTheme.titleLarge!.copyWith( - color: Colors.white, - ), - textAlign: TextAlign.center, + color: Colors.white, + size: 30, + ), + Container(height: 10), + Text(card["nombre"], + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Colors.white, ), - ], - ), + textAlign: TextAlign.center, + ), + ], ), ), ), ), - ); - } + ), + ); } From efeba55d8cd7970a913d2ec054c2d5fac4cbcebe Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:35:09 -0300 Subject: [PATCH 028/194] patch: se agrega nuevo error a la credencial Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/credencial_screen.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index afffbb5..31e15ec 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -83,7 +83,7 @@ class _CredencialScreenState extends State { final user = snapshot.data; - if (!snapshot.hasData || user == null) { + if (!snapshot.hasData) { return Container( padding: const EdgeInsets.all(20), child: Column( @@ -99,6 +99,13 @@ class _CredencialScreenState extends State { ); } + if (user == null) { + return CustomErrorWidget( + title: "Ocurrió un error al generar tu crendencial. Por favor, intenta nuevamente.", + error: snapshot.error, + ); + } + if (user.rut != null && carreraActiva!.nombre != null && carreraActiva.nombre!.isNotEmpty) { return Center( child: SafeArea( From 3f920ccb4a00ef428aced087de4c22a30763dd8f Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:56:54 -0300 Subject: [PATCH 029/194] patch: se elimina modelos antiguos y reparado qr * Se repara permisos de ingreso * Se eliminan modelos antiguos de rut y usuario Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/permiso_covid.dart | 8 +- lib/models/rut.dart | 89 --------------------- lib/models/user/rut.dart | 24 +++++- lib/models/user/user.dart | 6 +- lib/models/usuario.dart | 109 -------------------------- lib/screens/avancemalla_screen.dart | 1 - lib/screens/boletin_screen.dart | 1 - lib/screens/permiso_covid_screen.dart | 65 ++++++++------- lib/screens/usuario_screen.dart | 6 +- lib/services/analytics_service.dart | 1 - lib/widgets/custom_drawer.dart | 3 +- 11 files changed, 68 insertions(+), 245 deletions(-) delete mode 100644 lib/models/rut.dart delete mode 100644 lib/models/usuario.dart diff --git a/lib/models/permiso_covid.dart b/lib/models/permiso_covid.dart index a2556d2..34dc0bc 100644 --- a/lib/models/permiso_covid.dart +++ b/lib/models/permiso_covid.dart @@ -38,11 +38,5 @@ class PermisoCovid { fechaSolicitud: DateTime.tryParse(json['fechaSolicitud']), ); - static List fromJsonList(List? json) { - if(json == null) { - return []; - } - - return json.map((it) => PermisoCovid.fromJson(it)).toList(); - } + static List fromJsonList(List? json) => json != null ? json.map((it) => PermisoCovid.fromJson(it)).toList() : []; } diff --git a/lib/models/rut.dart b/lib/models/rut.dart deleted file mode 100644 index c477d7b..0000000 --- a/lib/models/rut.dart +++ /dev/null @@ -1,89 +0,0 @@ -class Rut { - int? numero; - String? dv; - - Rut(this.numero, this.dv); - - Rut.deEntero(int? rut) { - this.numero = rut; - this.dv = calcularDv(rut); - } - - Rut.deString(String? rut) { - List separated = separarRutYDv(rut); - this.numero = separated[0] != null ? int.tryParse(separated[0]!) : null; - this.dv = separated[1]; - } - - static List separarRutYDv(String? rutCompleto) { - if (rutCompleto != null) { - String limpio = limpiar(rutCompleto); - if (limpio.length == 0) { - return [null, null]; - } - if (limpio.length == 1) { - return [limpio, null]; - } - String dv = limpio.substring(limpio.length - 1); - String rut = limpio.substring(0, limpio.length - 1); - return [rut, dv]; - } - return [null, null]; - } - - static String? calcularDv(int? numero) { - if (numero != null) { - int suma = 0; - int multiplicador = 2; - String rut = numero.toString(); - for (var i = rut.length - 1; i >= 0; i--) { - String charDigito = String.fromCharCode(rut.runes.elementAt(i)); - suma = suma + int.parse(charDigito) * multiplicador; - multiplicador = multiplicador >= 7 ? 2 : multiplicador + 1; - } - - int valor = 11 - (suma % 11); - - switch (valor) { - case 10: - return "K"; - case 11: - return "0"; - default: - return valor.toString(); - } - } - return null; - } - - bool esValido() { - return this.dv == calcularDv(this.numero); - } - - String formateado(bool conSeparadorDeMiles) { - if (conSeparadorDeMiles) { - String numeroStr = this.numero.toString(); - String numeroFinal = ""; - int contador = 0; - for (var i = (numeroStr.runes.length - 1); i >= 0; i--) { - var char = new String.fromCharCode(numeroStr.runes.elementAt(i)); - contador++; - numeroFinal += char; - if (contador == 3) { - numeroFinal += "."; - contador = 0; - } - } - - numeroFinal = - new String.fromCharCodes(numeroFinal.runes.toList().reversed); - return "$numeroFinal-${this.dv}".toUpperCase(); - } else { - return "${this.numero}-${this.dv}".toUpperCase(); - } - } - - static String limpiar(String rut) { - return rut.replaceAll(new RegExp(r'^0+|[^0-9kK]+'), '').toUpperCase(); - } -} diff --git a/lib/models/user/rut.dart b/lib/models/user/rut.dart index 06f9c5d..4d4beb3 100644 --- a/lib/models/user/rut.dart +++ b/lib/models/user/rut.dart @@ -1,3 +1,5 @@ + + class Rut { int rut; @@ -26,10 +28,28 @@ class Rut { rut = rut.split("-")[0]; } - return Rut(int.parse(rut)); + return Rut(int.parse(rut.replaceAll(".", ""))); } @override - String toString() => "$rut-${dv.toUpperCase()}"; + String toString() { + final rut = "${this.rut}${dv.toUpperCase()}"; + if (rut.isEmpty) { + return rut; + } + var rutLength = rut.length; + var verificationDigit = '-${rut.substring(rutLength - 1)}'; + + var result = ''; + for (var i = 1; i < rutLength; i += 1) { + var start = rutLength - i - 1; + var end = rutLength - i; + result = '${rut.substring(start, end)}$result'; + if (i % 3 == 0) { + result = '.$result'; + } + } + return '$result$verificationDigit'; + } } \ No newline at end of file diff --git a/lib/models/user/user.dart b/lib/models/user/user.dart index a39cc35..7b515fd 100644 --- a/lib/models/user/user.dart +++ b/lib/models/user/user.dart @@ -7,7 +7,7 @@ class User { String? token; Rut? rut; - String correoPersonal; + String? correoPersonal; String? correoUtem; String? fotoBase64; @@ -49,11 +49,11 @@ class User { factory User.fromJson(Map json) => User( token: json['token'], - rut: json['rut'] is int ? Rut(json['rut'] as int) : Rut.fromString("${json['rut']}"), + rut: Rut.fromString("${json['rut']}"), correoPersonal: json['correoPersonal'], correoUtem: json['correoUtem'], fotoBase64: json['fotoBase64'], - perfiles: ((json['perfiles'] as List)).map((it) => it.toString()).toList(), + perfiles: ((json['perfiles'] as List?) ?? []).map((it) => it.toString()).toList(), nombreCompleto: json['nombreCompleto'], nombres: json['nombres'], apellidos: json['apellidos'], diff --git a/lib/models/usuario.dart b/lib/models/usuario.dart deleted file mode 100644 index 66d0dd3..0000000 --- a/lib/models/usuario.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:mi_utem/models/rut.dart'; -import 'package:recase/recase.dart'; - -class Usuario { - String? nombre; - String? nombres; - String? apellidos; - String? correoUtem; - String? correoPersonal; - String? token; - String? fotoUrl; - Rut? rut; - - Usuario( - {this.correoUtem, - this.correoPersonal, - this.token, - this.nombres, - this.nombre, - this.fotoUrl, - this.apellidos, - this.rut}); - - factory Usuario.fromJson(Map? json) { - if (json == null) { - return Usuario(); - } - return Usuario( - rut: json['rut'] != null - ? (json['rut'] is int - ? Rut.deEntero(json['rut']) - : Rut.deString(json["rut"])) - : null, - correoUtem: json['correoUtem'], - correoPersonal: json['correoPersonal'], - token: json['token'], - fotoUrl: json['fotoUrl'], - nombres: - json['nombres'] != null ? ReCase(json['nombres']).titleCase : null, - nombre: json['nombreCompleto'] != null - ? ReCase(json['nombreCompleto']).titleCase - : null, - apellidos: json['apellidos'] != null - ? ReCase(json['apellidos']).titleCase - : null); - } - - static List fromJsonList(dynamic json) { - if (json == null) { - return []; - } - List list = []; - for (var item in json) { - list.add(Usuario.fromJson(item)); - } - return list; - } - - String? get nombreCompleto { - if (nombre != null && nombre!.isNotEmpty) { - return nombre; - } else { - String completo = ''; - if (nombres != null && nombres!.isNotEmpty) { - completo += nombres!; - } - if (apellidos != null && apellidos!.isNotEmpty) { - if (completo.isNotEmpty) { - completo += ' '; - } - completo += apellidos!; - } - return completo; - } - } - - String get primerNombre { - return nombreCompleto!.split(' ')[0]; - } - - String get iniciales => primeraLetraCadaPalabra(this.nombreCompleto); - - String primeraLetraCadaPalabra(String? sentence) { - if (sentence == null || sentence.isEmpty) { - return "NN"; - } else { - List words = sentence.split(" "); - List letters = []; - - for (var word in words) { - letters.add('${word[0]}'); - } - return letters.join(""); - } - } - - Map toJson() { - return { - 'nombreCompleto': nombreCompleto, - 'correoUtem': correoUtem, - 'correoPersonal': correoPersonal, - 'token': token, - 'fotoUrl': fotoUrl, - 'nombres': nombres, - 'apellidos': apellidos, - 'rut': rut?.toString(), - }; - } -} diff --git a/lib/screens/avancemalla_screen.dart b/lib/screens/avancemalla_screen.dart index a5e59cd..55b6413 100644 --- a/lib/screens/avancemalla_screen.dart +++ b/lib/screens/avancemalla_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter_statusbar/flutter_statusbar.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:mi_utem/models/avance_malla.dart'; -import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/services/malla_service.dart'; import 'package:mi_utem/widgets/avance_ramo_card.dart'; diff --git a/lib/screens/boletin_screen.dart b/lib/screens/boletin_screen.dart index b47c37b..3bcf906 100644 --- a/lib/screens/boletin_screen.dart +++ b/lib/screens/boletin_screen.dart @@ -3,7 +3,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:mi_utem/models/boletin_notas.dart'; -import 'package:mi_utem/models/usuario.dart'; import 'package:mi_utem/services/boletin_service.dart'; import 'package:mi_utem/services/horario_service.dart'; import 'package:mi_utem/widgets/custom_expansion_tile.dart' as custom; diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index adb64e5..ad1427e 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -7,6 +7,8 @@ import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:get/get.dart'; import 'package:image/image.dart' as dartImage; import 'package:intl/intl.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_covid.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; @@ -16,26 +18,34 @@ import 'package:mi_utem/widgets/field_list_tile.dart'; import 'package:mi_utem/widgets/image_view_screen.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; +import 'package:mi_utem/widgets/pull_to_refresh.dart'; import 'package:watch_it/watch_it.dart'; -class PermisoCovidScreen extends StatelessWidget { - +class PermisoCovidScreen extends StatefulWidget { final String passId; - const PermisoCovidScreen({ - super.key, - required this.passId, - }); + const PermisoCovidScreen({super.key, required this.passId}); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar(title: Text("Permiso de ingreso")), - body: FutureBuilder( - future: di.get().getDetallesPermiso(passId), - builder: (context, AsyncSnapshot snapshot) { + State createState() => _PermisoCovidScreenState(); +} + +class _PermisoCovidScreenState extends State { + + @override + Widget build(BuildContext context) => Scaffold( + appBar: CustomAppBar(title: Text("Permiso de ingreso")), + body: PullToRefresh( + onRefresh: () async => setState(() {}), + child: FutureBuilder( + future: di.get().getDetallesPermiso(widget.passId), + builder: (context, snapshot) { if (snapshot.hasError) { - return const CustomErrorWidget(); + final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "No sabemos lo que ocurrió. Por favor intenta más tarde."; + logger.e("Error al cargar permiso", snapshot.error); + return Center( + child: CustomErrorWidget(error: error), + ); } final permiso = snapshot.data; @@ -48,23 +58,24 @@ class PermisoCovidScreen extends StatelessWidget { } if(permiso == null) { - return const CustomErrorWidget( - title: "Permiso no encontrado", - emoji: "\u{1F914}", + return const Center( + child: CustomErrorWidget( + title: "Permiso no encontrado", + emoji: "\u{1F914}", + ), ); } - return SingleChildScrollView( - child: LoadedScreen(permiso: permiso), - ); + return SingleChildScrollView(child: QRCard(permiso: permiso)); }, ), - ); - } + ), + ); } -class LoadedScreen extends StatelessWidget { - const LoadedScreen({ + +class QRCard extends StatelessWidget { + const QRCard({ Key? key, required this.permiso, }) : super(key: key); @@ -165,13 +176,13 @@ class UsuarioDetalle extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(user.nombreDisplayCapitalizado, + Text(user.nombreCompletoCapitalizado, maxLines: 2, - style: Get.textTheme.bodyLarge, + style: Theme.of(context).textTheme.bodyLarge, ), Container(height: 4), - Text(user.rut.toString(), - style: Get.textTheme.bodyMedium, + Text("${user.rut}", + style: Theme.of(context).textTheme.bodyMedium, ), ], ), diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index 4856ad0..bb77746 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -168,7 +168,7 @@ class _UsuarioScreenState extends State { ), )); } - if (_user?.correoPersonal.isEmpty == false) { + if (_user?.correoPersonal?.isEmpty == false) { lista.add(Divider(height: 1)); lista.add( ListTile( @@ -176,13 +176,13 @@ class _UsuarioScreenState extends State { style: TextStyle(color: Colors.grey), ), onLongPress: widget.tipo != 0 ? () async { - await FlutterClipboard.copy(_user!.correoPersonal); + await FlutterClipboard.copy(_user!.correoPersonal!); showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); } : null, onTap: widget.tipo != 0 ? () async { await launchUrl(Uri.parse("mailto:${_user?.correoPersonal ?? ""}")); } : null, - subtitle: Text(_user!.correoPersonal, + subtitle: Text(_user!.correoPersonal!, style: TextStyle( color: Colors.grey[900], fontSize: 18, diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart index fe82d29..c317491 100644 --- a/lib/services/analytics_service.dart +++ b/lib/services/analytics_service.dart @@ -2,7 +2,6 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/models/usuario.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class AnalyticsService { diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index f946696..8483030 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:badges/badges.dart' as badge; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; @@ -75,7 +74,7 @@ class CustomDrawer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ UserAccountsDrawerHeader( - accountEmail: Text(user.correoUtem ?? user.correoPersonal), + accountEmail: Text(user.correoUtem ?? user.correoPersonal ?? ""), accountName: Text(user.nombreCompleto, style: const TextStyle( fontSize: 16, From 5277fd19461b203d42a9066d36adde760056a485 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:56:28 -0300 Subject: [PATCH 030/194] patch: finalizada migracion de getx a nativo Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/main.dart | 4 +- lib/models/asignaturas/asignatura.dart | 2 +- lib/models/horario.dart | 182 ++++------ .../asignatura/asignatura_resumen_tab.dart | 7 +- lib/screens/carreras_screen.dart | 3 +- lib/screens/horario/horario_screen.dart | 6 +- .../widgets/horario_blocks_content.dart | 2 +- .../horario/widgets/horario_days_header.dart | 2 +- .../horario/widgets/horario_indicator.dart | 7 +- .../widgets/horario_periods_header.dart | 2 +- lib/screens/login_screen/login_screen.dart | 9 - lib/screens/main_screen.dart | 6 +- lib/screens/malla_screen.dart | 22 +- lib/screens/permiso_covid_screen.dart | 3 +- lib/screens/splash_screen.dart | 5 +- lib/services/notification_service.dart | 41 +-- lib/services/review_service.dart | 60 ++-- .../controllers/horario_controller.dart | 19 +- .../implementations/horario_service.dart | 15 +- .../controllers/horario_controller.dart | 8 +- lib/widgets/acerca/dialog/acerca_dialog.dart | 5 +- lib/widgets/banners_section.dart | 3 +- lib/widgets/credencial_card.dart | 23 +- lib/widgets/custom_drawer.dart | 4 +- lib/widgets/field_list_tile.dart | 5 +- lib/widgets/footer_layout.dart | 9 +- .../{ => horario}/bloque_dias_card.dart | 5 +- .../{ => horario}/bloque_periodo_card.dart | 0 .../{ => horario}/bloque_ramo_card.dart | 0 lib/widgets/image_view_screen.dart | 17 +- lib/widgets/imagen_editor_modal.dart | 8 +- lib/widgets/login_text_form_field.dart | 13 +- lib/widgets/nota_list_item.dart | 3 +- lib/widgets/noticias/noticia_card_widget.dart | 3 +- .../noticias/noticias_carrusel_widget.dart | 3 +- lib/widgets/permiso_card.dart | 19 +- lib/widgets/permisos_section.dart | 3 +- lib/widgets/profile_photo.dart | 3 +- lib/widgets/progress_button.dart | 52 ++- lib/widgets/quick_menu_section.dart | 3 +- lib/widgets/sad_dialog.dart | 1 - lib/widgets/semestre_boletin_card.dart | 328 +++++++++--------- 42 files changed, 395 insertions(+), 520 deletions(-) rename lib/widgets/{ => horario}/bloque_dias_card.dart (93%) rename lib/widgets/{ => horario}/bloque_periodo_card.dart (100%) rename lib/widgets/{ => horario}/bloque_ramo_card.dart (100%) diff --git a/lib/main.dart b/lib/main.dart index 2ed475c..96dcb71 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,10 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; -import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/screens/splash_screen.dart'; @@ -60,7 +58,7 @@ class _MiUtemState extends State { enableMultiSessionRecord: true, )); - return GetMaterialApp( + return MaterialApp( home: SplashScreen(), debugShowCheckedModeBanner: false, title: 'Mi UTEM', diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart index 536d648..9e1f778 100644 --- a/lib/models/asignaturas/asignatura.dart +++ b/lib/models/asignaturas/asignatura.dart @@ -62,7 +62,7 @@ class Asignatura { grades: json.containsKey('notas') ? Grades.fromJson(json['notas']) : null, estudiantes: User.fromJsonList(json["estudiantes"]), asistencia: Asistencia(asistidos: json['asistenciaAlDia']), - tipoAsignatura: capitalize(json['tipoAsignatura'] as String), + tipoAsignatura: capitalize(json['tipoAsignatura'] as String? ?? ''), sala: capitalize(json['sala'] ?? ''), horario: json['horario'], intentos: int.tryParse(json['intentos'] ?? '0') ?? 0, diff --git a/lib/models/horario.dart b/lib/models/horario.dart index 863070e..0a09d82 100644 --- a/lib/models/horario.dart +++ b/lib/models/horario.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:mi_utem/models/asignaturas/asignatura.dart'; class Horario { @@ -63,78 +61,52 @@ class Horario { Horario({this.asignaturas, this.horario, this.dias, this.periodos}); - factory Horario.fromJson(Map? json) { - if (json == null) { - return Horario(); - } - return Horario( - // asignaturas: Asignatura.fromJsonList(json['asignaturas']), - horario: BloqueHorario.fromJsonMatrix(json['horario']), - // dias: json["dias"], - // periodos: Periodo.fromJsonList(json["periodos"]), - ); - } - - static List fromJsonList(dynamic json) { - if (json == null) { - return []; - } - List list = []; - for (var bloque in json) { - log('bloque: $bloque'); - for (var dia in bloque) { - log('dia: $dia'); - list.add(Horario.fromJson(dia)); - } - } - return list; - } - - List get horasInicio { - return [ - "08:00", - "09:40", - "11:20", - "13:00", - "14:40", - "16:20", - "18:00", - "19:40", - "21:20" - ]; - } + factory Horario.fromJson(Map? json) => json != null ? Horario( + // asignaturas: Asignatura.fromJsonList(json['asignaturas']), + horario: BloqueHorario.fromJsonMatrix(json['horario']), + // dias: json["dias"], + // periodos: Periodo.fromJsonList(json["periodos"]), + ) : Horario(); + + static List fromJsonList(dynamic json) => (json as List? ?? []).expand((bloque) => (bloque as List? ?? []).map((dia) => Horario.fromJson(dia))).toList(); + + List get horasInicio => [ + "08:00", + "09:40", + "11:20", + "13:00", + "14:40", + "16:20", + "18:00", + "19:40", + "21:20" + ]; - List get horasIntermedio { - return [ - "08:45", - "10:25", - "12:05", - "13:45", - "15:25", - "17:05", - "18:45", - "20:25", - "22:05" - ]; - } + List get horasIntermedio => [ + "08:45", + "10:25", + "12:05", + "13:45", + "15:25", + "17:05", + "18:45", + "20:25", + "22:05" + ]; - List get horasTermino { - return [ - "09:30", - "11:10", - "12:50", - "14:30", - "16:10", - "17:50", - "19:30", - "21:10", - "22:50" - ]; - } + List get horasTermino => [ + "09:30", + "11:10", + "12:50", + "14:30", + "16:10", + "17:50", + "19:30", + "21:10", + "22:50" + ]; - List get diasHorario { - return ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"]; - } + List get diasHorario => ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"]; List> get horarioEnlazado { final _horario = horario; @@ -160,30 +132,21 @@ class Periodo { String? horaIntermedio; String? horaTermino; - Periodo( - {this.numero, this.horaInicio, this.horaIntermedio, this.horaTermino}); + Periodo({ + this.numero, + this.horaInicio, + this.horaIntermedio, + this.horaTermino, + }); - factory Periodo.fromJson(Map? json) { - if (json == null) { - return Periodo(); - } - return Periodo( - numero: json["numero"], - horaInicio: json["horaInicio"], - horaIntermedio: json["horaIntermedio"], - horaTermino: json["horaTermino"]); - } + factory Periodo.fromJson(Map? json) => json != null ? Periodo( + numero: json["numero"], + horaInicio: json["horaInicio"], + horaIntermedio: json["horaIntermedio"], + horaTermino: json["horaTermino"], + ) : Periodo(); - static List fromJsonList(dynamic json) { - if (json == null) { - return []; - } - List list = []; - for (var item in json) { - list.add(Periodo.fromJson(item)); - } - return list; - } + static List fromJsonList(dynamic json) => json != null ? (json as List ?? []).map((item) => Periodo.fromJson(item)).toList() : []; } class BloqueHorario { @@ -197,32 +160,11 @@ class BloqueHorario { this.codigo, }); - factory BloqueHorario.fromJson(Map? json) { - if (json == null) { - return BloqueHorario(); - } - - BloqueHorario bloque = BloqueHorario( - asignatura: Asignatura.fromJson(json['asignatura']), - sala: json['asignatura']['sala'], - codigo: - "${json['asignatura']['codigo']}/${json['asignatura']['seccion']}"); - - return bloque; - } + factory BloqueHorario.fromJson(Map? json) => json != null ? BloqueHorario( + asignatura: Asignatura.fromJson(json['asignatura']), + sala: json['asignatura']['sala'], + codigo: "${json['asignatura']['codigo']}/${json['asignatura']['seccion']}", + ) : BloqueHorario(); - static List>? fromJsonMatrix(dynamic json) { - if (json == null) { - return null; - } - List> matrix = []; - for (var bloque in json) { - List list = []; - for (var dia in bloque) { - list.add(BloqueHorario.fromJson(dia)); - } - matrix.add(list); - } - return matrix; - } + static List>? fromJsonMatrix(dynamic json) => json == null ? null : (json as List? ?? []).map((bloque) => (bloque as List? ?? []).map((dia) => BloqueHorario.fromJson(dia)).toList()).toList(); } diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/asignatura_resumen_tab.dart index 9d065c3..a82376a 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/asignatura_resumen_tab.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/utils/string_utils.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; @@ -33,7 +32,7 @@ class AsignaturaResumenTab extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 15), width: double.infinity, child: Text("Asignatura".toUpperCase(), - style: Get.textTheme.bodyLarge, + style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.left, ), ), @@ -79,7 +78,7 @@ class AsignaturaResumenTab extends StatelessWidget { margin: EdgeInsets.only(top: 20), width: double.infinity, child: Text("Sala".toUpperCase(), - style: Get.textTheme.bodyLarge, + style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.left, ), ), @@ -99,7 +98,7 @@ class AsignaturaResumenTab extends StatelessWidget { margin: EdgeInsets.only(top: 20), width: double.infinity, child: Text("Horario".toUpperCase(), - style: Get.textTheme.bodyLarge, + style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.left, ), ), diff --git a/lib/screens/carreras_screen.dart b/lib/screens/carreras_screen.dart index df5c85b..6d7afda 100644 --- a/lib/screens/carreras_screen.dart +++ b/lib/screens/carreras_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:mi_utem/widgets/custom_app_bar.dart'; final dynamic carreras = [ @@ -56,7 +55,7 @@ class _CarrerasScreenState extends State { subtitle: Text('${carreras[i]["codigo"]}/${carreras[i]["plan"]}'), /* leading: CircleAvatar( radius: 25, - backgroundColor: Get.theme.primaryColor, + backgroundColor: Theme.of(context).primaryColor, child: Icon( IconData(carreras[i]["icono"], fontFamily: 'MaterialIcons'), size: 25, diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index f792556..158044c 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; import 'package:mi_utem/services/analytics_service.dart'; @@ -31,7 +32,7 @@ class _HorarioScreenState extends State { void _moveViewportToCurrentTime() { AnalyticsService.logEvent("horario_move_viewport_to_current_time"); - controller.moveViewportToCurrentPeriodAndDay(); + controller.moveViewportToCurrentPeriodAndDay(context); } void _captureAndShareScreenshot(Horario horario) async { @@ -59,6 +60,7 @@ class _HorarioScreenState extends State { @override void initState() { controller.getHorarioData().catchError((err) => { + logger.e("Error al cargar el horario", err), showErrorSnackbar(context, "Ocurrió un error al cargar el horario! Por favor intenta más tarde.") }); super.initState(); @@ -73,7 +75,7 @@ class _HorarioScreenState extends State { DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); - controller.init(); + controller.init(context); final horario = watchValue((HorarioController controller) => controller.horario); final isLoadingHorario = watchValue((HorarioController controller) => controller.loadingHorario); diff --git a/lib/screens/horario/widgets/horario_blocks_content.dart b/lib/screens/horario/widgets/horario_blocks_content.dart index 04df9db..684d39c 100644 --- a/lib/screens/horario/widgets/horario_blocks_content.dart +++ b/lib/screens/horario/widgets/horario_blocks_content.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:mi_utem/widgets/bloque_ramo_card.dart'; +import 'package:mi_utem/widgets/horario/bloque_ramo_card.dart'; class HorarioBlocksContent extends StatelessWidget { final Horario horario; diff --git a/lib/screens/horario/widgets/horario_days_header.dart b/lib/screens/horario/widgets/horario_days_header.dart index 08fe6c1..4f236cf 100644 --- a/lib/screens/horario/widgets/horario_days_header.dart +++ b/lib/screens/horario/widgets/horario_days_header.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:mi_utem/widgets/bloque_dias_card.dart'; +import 'package:mi_utem/widgets/horario/bloque_dias_card.dart'; import 'package:watch_it/watch_it.dart'; class HorarioDaysHeader extends StatelessWidget { diff --git a/lib/screens/horario/widgets/horario_indicator.dart b/lib/screens/horario/widgets/horario_indicator.dart index 028db3f..5d4fde0 100644 --- a/lib/screens/horario/widgets/horario_indicator.dart +++ b/lib/screens/horario/widgets/horario_indicator.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:watch_it/watch_it.dart'; @@ -154,19 +153,19 @@ class __TickerTimeTextState extends State<_TickerTimeText> { children: [ TextSpan( text: _timeHour, - style: Get.textTheme.bodySmall?.copyWith( + style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white, ), ), TextSpan( text: ":", - style: Get.textTheme.bodySmall?.copyWith( + style: Theme.of(context).textTheme.bodySmall?.copyWith( color: _showColon ? Colors.white : Colors.transparent, ), ), TextSpan( text: _timeMinutes, - style: Get.textTheme.bodySmall?.copyWith( + style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white, ), ), diff --git a/lib/screens/horario/widgets/horario_periods_header.dart b/lib/screens/horario/widgets/horario_periods_header.dart index 12b22c0..f50a60f 100644 --- a/lib/screens/horario/widgets/horario_periods_header.dart +++ b/lib/screens/horario/widgets/horario_periods_header.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:mi_utem/widgets/bloque_periodo_card.dart'; +import 'package:mi_utem/widgets/horario/bloque_periodo_card.dart'; import 'package:watch_it/watch_it.dart'; class HorarioPeriodsHeader extends StatelessWidget { diff --git a/lib/screens/login_screen/login_screen.dart b/lib/screens/login_screen/login_screen.dart index e5912c7..199e8d8 100644 --- a/lib/screens/login_screen/login_screen.dart +++ b/lib/screens/login_screen/login_screen.dart @@ -1,15 +1,6 @@ - import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/services/update_service.dart'; import 'package:mi_utem/widgets/login_screen/background.dart'; -import 'package:mi_utem/widgets/login_screen/creditos_app.dart'; -import 'package:mi_utem/widgets/login_screen/formulario_credenciales.dart'; -import 'package:mi_utem/widgets/login_screen/login_button.dart'; import 'package:mi_utem/widgets/login_screen/login_form.dart'; -import 'package:video_player/video_player.dart'; class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 738c865..97125f7 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_markdown/flutter_markdown.dart"; -import "package:get/get.dart"; import "package:mi_utem/config/logger.dart"; import "package:mi_utem/models/user/user.dart"; import "package:mi_utem/services/remote_config/remote_config.dart"; @@ -52,7 +51,7 @@ class _MainScreenState extends State { ); ReviewService.addScreen("MainScreen"); - ReviewService.checkAndRequestReview(); + ReviewService.checkAndRequestReview(context); loadData(); @@ -95,8 +94,7 @@ class _MainScreenState extends State { child: MarkdownBody( data: _greetingText, styleSheet: MarkdownStyleSheet( - p: Get.textTheme.displayMedium! - .copyWith(fontWeight: FontWeight.normal), + p: Theme.of(context).textTheme.displayMedium!.copyWith(fontWeight: FontWeight.normal), ), ), ), diff --git a/lib/screens/malla_screen.dart b/lib/screens/malla_screen.dart index ebf3022..b6556d5 100644 --- a/lib/screens/malla_screen.dart +++ b/lib/screens/malla_screen.dart @@ -1,21 +1,19 @@ import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - import 'package:mi_utem/widgets/custom_app_bar.dart'; class MallaScreen extends StatelessWidget { + @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - title: Text("Malla"), - ), - body: SingleChildScrollView( - padding: EdgeInsets.all(20), - scrollDirection: Axis.horizontal, - child: Image.asset('assets/images/malla.png', - height: Get.mediaQuery.size.height), - )); + appBar: CustomAppBar( + title: Text("Malla"), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(20), + scrollDirection: Axis.horizontal, + child: Image.asset('assets/images/malla.png', height: MediaQuery.of(context).size.height), + ), + ); } } diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index ad1427e..e937e8c 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -4,7 +4,6 @@ import 'package:barcode_image/barcode_image.dart'; import 'package:barcode_widget/barcode_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; -import 'package:get/get.dart'; import 'package:image/image.dart' as dartImage; import 'package:intl/intl.dart'; import 'package:mi_utem/config/logger.dart'; @@ -147,7 +146,7 @@ class QRCard extends StatelessWidget { ), Container(height: 20), Text("Permiso generado el ${DateFormat('dd/MM/yyyy').format(permiso.fechaSolicitud!)}", - style: Get.textTheme.bodySmall, + style: Theme.of(context).textTheme.bodySmall, ), Container(height: 20), ], diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index f144e72..23d0230 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; @@ -44,7 +43,7 @@ class _SplashScreenState extends State { @override Widget build(BuildContext context) => Scaffold( - backgroundColor: Get.theme.primaryColor, + backgroundColor: Theme.of(context).primaryColor, body: Stack( children: [ Container( @@ -91,7 +90,7 @@ class _SplashScreenState extends State { ); return; } - await NotificationService.requestUserPermissionIfNecessary(); + await NotificationService.requestUserPermissionIfNecessary(context); final isLoggedIn = await _authService.isLoggedIn(); if(!isLoggedIn) { AnalyticsService.removeUser(); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index ebe3dc5..2ec8bcb 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; -import 'package:get/get.dart'; +import 'package:flutter/material.dart'; import 'package:mi_utem/controllers/notification_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/widgets/custom_alert_dialog.dart'; @@ -67,30 +67,25 @@ class NotificationService { ); } - static Future requestUserPermissionIfNecessary() async { + static Future requestUserPermissionIfNecessary(BuildContext context) async { bool isAllowed = await notifications.isNotificationAllowed(); if (!isAllowed) { - isAllowed = await Get.dialog( - CustomAlertDialog( - titulo: "Activa las notificaciones", - emoji: "🔔", - descripcion: - "Necesitamos tu permiso para poder enviarte notificaciones. Nada de spam, lo prometemos.", - onCancelar: () async { - bool isAllowed = await notifications.isNotificationAllowed(); - Get.back(result: isAllowed); - Get.back(result: isAllowed); - }, - onConfirmar: () async { - await notifications.requestPermissionToSendNotifications(); - bool isAllowed = await notifications.isNotificationAllowed(); - Get.back(result: isAllowed); - Get.back(result: isAllowed); - }, - cancelarTextoBoton: "No permitir", - confirmarTextoBoton: "Permitir", - ), - ); + isAllowed = await showDialog(context: context, builder: (ctx) => CustomAlertDialog( + titulo: "Activa las notificaciones", + emoji: "🔔", + descripcion: "Necesitamos tu permiso para poder enviarte notificaciones. Nada de spam, lo prometemos.", + onCancelar: () async { + bool isAllowed = await notifications.isNotificationAllowed(); + Navigator.pop(ctx, isAllowed); + }, + onConfirmar: () async { + await notifications.requestPermissionToSendNotifications(); + bool isAllowed = await notifications.isNotificationAllowed(); + Navigator.pop(ctx, isAllowed); + }, + cancelarTextoBoton: "No permitir", + confirmarTextoBoton: "Permitir", + )); } return isAllowed; } diff --git a/lib/services/review_service.dart b/lib/services/review_service.dart index 7e46b15..6e4e1cc 100644 --- a/lib/services/review_service.dart +++ b/lib/services/review_service.dart @@ -1,6 +1,7 @@ -import 'package:get/get.dart'; +import 'package:flutter/material.dart'; import 'package:get_storage/get_storage.dart'; import 'package:in_app_review/in_app_review.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/widgets/custom_alert_dialog.dart'; class ReviewService { @@ -27,12 +28,11 @@ class ReviewService { box.write("review$screenName", DateTime.now().toIso8601String()); return true; } else { - print( - "ERROR ReviewService addScreen: no estaba contemplada esta pantalla"); + logger.e("ERROR ReviewService addScreen: no estaba contemplada esta pantalla"); return false; } } catch (e) { - print(e); + logger.e(e); throw e; } } @@ -46,11 +46,11 @@ class ReviewService { DateTime maxDate = DateTime.now().subtract(maxScreen); DateTime minDate = DateTime.now().subtract(minScreen); if (date.isBefore(maxDate) || date.isAfter(minDate)) { - print("ReviewService mustRequest: $screen no cumple con una fecha $isoDate"); + logger.i("ReviewService mustRequest: $screen no cumple con una fecha $isoDate"); return false; } } else { - print("ReviewService mustRequest: $screen no se ha visitado"); + logger.i("ReviewService mustRequest: $screen no se ha visitado"); return false; } } @@ -59,8 +59,7 @@ class ReviewService { DateTime lastRequestDate = DateTime.parse(lastRequestIsoDate); DateTime minDate = DateTime.now().subtract(minRequest); if (lastRequestDate.isAfter(minDate)) { - print( - "ReviewService mustRequest: no ha pasado el minimo desde la ultima request $lastRequestIsoDate"); + logger.i("ReviewService mustRequest: no ha pasado el minimo desde la ultima request $lastRequestIsoDate"); return false; } else { return true; @@ -74,40 +73,35 @@ class ReviewService { } } - static Future checkAndRequestReview() async { + static Future checkAndRequestReview(BuildContext context) async { try { - print("ReviewService checkAndRequestReview"); + logger.i("ReviewService checkAndRequestReview"); bool mustRequest = await ReviewService.mustRequest(); if (mustRequest) { if (await inAppReview.isAvailable()) { - await Get.dialog( - CustomAlertDialog( - titulo: "¿Te gustaría calificar a Mi UTEM?", - emoji: "⭐", - descripcion: - "Te invitamos a dejarnos tus comentarios y estrellitas", - onCancelar: () async { - Get.back(); - }, - onConfirmar: () async { - await inAppReview.requestReview(); - box.write( - "reviewLastRequest", DateTime.now().toIso8601String()); - Get.back(); - }, - cancelarTextoBoton: "No, gracias 😡", - confirmarTextoBoton: "Dejar estrellitas 😊", - ), - ); + await showDialog(context: context, builder: (ctx) => CustomAlertDialog( + titulo: "¿Te gustaría calificar a Mi UTEM?", + emoji: "⭐", + descripcion: "Te invitamos a dejarnos tus comentarios y estrellitas", + onCancelar: () async { + Navigator.pop(ctx); + }, + onConfirmar: () async { + await inAppReview.requestReview(); + box.write("reviewLastRequest", DateTime.now().toIso8601String()); + Navigator.pop(ctx); + }, + cancelarTextoBoton: "No, gracias 😡", + confirmarTextoBoton: "Dejar estrellitas 😊", + )); } else { - print( - "ReviewService checkAndRequestReview: inAppReview no está disponible"); + logger.i("ReviewService checkAndRequestReview: inAppReview no está disponible"); } } else { - print("ReviewService checkAndRequestReview: mustRequest es false"); + logger.i("ReviewService checkAndRequestReview: mustRequest es false"); } } catch (e) { - print(e); + logger.e(e); throw e; } } diff --git a/lib/services_new/implementations/controllers/horario_controller.dart b/lib/services_new/implementations/controllers/horario_controller.dart index 9df883b..767bf73 100644 --- a/lib/services_new/implementations/controllers/horario_controller.dart +++ b/lib/services_new/implementations/controllers/horario_controller.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/carrera.dart'; @@ -88,9 +87,9 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC } @override - void init() { + void init(BuildContext context) { zoom.value = RemoteConfigService.horarioZoom; - moveViewportToCurrentPeriodAndDay(); + moveViewportToCurrentPeriodAndDay(context); setZoom(zoom.value); _setScrollControllerListeners(); @@ -131,9 +130,9 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC } @override - void moveViewportTo(double x, double y) { - final viewportWidth = Get.width - Get.mediaQuery.padding.horizontal; - final viewportHeight = Get.height - Get.mediaQuery.padding.vertical; + void moveViewportTo(BuildContext context, double x, double y) { + final viewportWidth = MediaQuery.of(context).size.width - MediaQuery.of(context).padding.horizontal; + final viewportHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.vertical; x = (x + (HorarioMainScroller.periodWidth / 2)) * zoom.value - (viewportWidth / 2); y = (y + (HorarioMainScroller.dayHeight / 2)) * zoom.value - (viewportHeight / 2); @@ -155,22 +154,22 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC } @override - void moveViewportToPeriodIndexAndDayIndex(int periodIndex, int dayIndex) { + void moveViewportToPeriodIndexAndDayIndex(BuildContext context, int periodIndex, int dayIndex) { final blockWidth = HorarioMainScroller.blockWidth; final x = (dayIndex * blockWidth) + (blockWidth / 2); final blockHeight = HorarioMainScroller.blockHeight; final y = (periodIndex * blockHeight) + (blockHeight / 2); - moveViewportTo(x, y); + moveViewportTo(context, x, y); } @override - void moveViewportToCurrentPeriodAndDay() { + void moveViewportToCurrentPeriodAndDay(BuildContext context) { final periodIndex = indexOfCurrentPeriod ?? 0; final dayIndex = indexOfCurrentDayStartingAtMonday ?? 0; - moveViewportToPeriodIndexAndDayIndex(periodIndex, dayIndex); + moveViewportToPeriodIndexAndDayIndex(context, periodIndex, dayIndex); isCenteredInCurrentPeriodAndDay.value = true; } diff --git a/lib/services_new/implementations/horario_service.dart b/lib/services_new/implementations/horario_service.dart index 8556f44..07f0a33 100644 --- a/lib/services_new/implementations/horario_service.dart +++ b/lib/services_new/implementations/horario_service.dart @@ -5,25 +5,12 @@ import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/horario_service.dart'; -import 'package:watch_it/watch_it.dart'; class HorarioServiceImplementation implements HorarioService { @override Future getHorario(String carreraId, {bool forceRefresh = false}) async { - final user = await di.get().getUser(); - if (user == null) { - return null; - } - - final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/horarios"), - headers: { - 'Authorization': 'Bearer ${user.token}', - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - }, - ); + final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/horarios")); final json = jsonDecode(response.body); diff --git a/lib/services_new/interfaces/controllers/horario_controller.dart b/lib/services_new/interfaces/controllers/horario_controller.dart index dc75156..261e498 100644 --- a/lib/services_new/interfaces/controllers/horario_controller.dart +++ b/lib/services_new/interfaces/controllers/horario_controller.dart @@ -31,15 +31,15 @@ abstract class HorarioController { int? get indexOfCurrentPeriod; - void init(); + void init(BuildContext context); Future getHorarioData({ bool forceRefresh = false }); - void moveViewportToCurrentPeriodAndDay(); + void moveViewportToCurrentPeriodAndDay(BuildContext context); - void moveViewportToPeriodIndexAndDayIndex(int periodIndex, int dayIndex); + void moveViewportToPeriodIndexAndDayIndex(BuildContext context, int periodIndex, int dayIndex); - void moveViewportTo(double x, double y); + void moveViewportTo(BuildContext context, double x, double y); void setZoom(double zoom); diff --git a/lib/widgets/acerca/dialog/acerca_dialog.dart b/lib/widgets/acerca/dialog/acerca_dialog.dart index 7133f66..eb12c4d 100644 --- a/lib/widgets/acerca/dialog/acerca_dialog.dart +++ b/lib/widgets/acerca/dialog/acerca_dialog.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_aplicacion_content.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog_action_button.dart'; @@ -80,10 +79,10 @@ class _AcercaDialogState extends State { if (!_isActive) OutlinedButton( child: Text("Cerrar", - style: TextStyle(color: Get.theme.primaryColor), + style: TextStyle(color: Theme.of(context).primaryColor), ), onPressed: () { - Get.back(); + Navigator.pop(context); }, ), Container(height: 20), diff --git a/lib/widgets/banners_section.dart b/lib/widgets/banners_section.dart index 5266c86..4e6c3d1 100644 --- a/lib/widgets/banners_section.dart +++ b/lib/widgets/banners_section.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/widgets/banner.dart'; class BannersSection extends StatelessWidget { @@ -25,7 +24,7 @@ class BannersSection extends StatelessWidget { Text( "Novedades".toUpperCase(), textAlign: TextAlign.left, - style: Get.textTheme.titleMedium!.copyWith( + style: Theme.of(context).textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, ), ), diff --git a/lib/widgets/credencial_card.dart b/lib/widgets/credencial_card.dart index 83b9bb9..88032a8 100644 --- a/lib/widgets/credencial_card.dart +++ b/lib/widgets/credencial_card.dart @@ -3,7 +3,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; @@ -19,16 +18,16 @@ class CredencialCard extends StatelessWidget { final FlipController? controller; final Function(SwipeDirection?)? onFlip; - CredencialCard( - {Key? key, - required this.user, - required this.carrera, - this.controller, - this.onFlip}) - : super(key: key); + CredencialCard({ + super.key, + required this.user, + required this.carrera, + this.controller, + this.onFlip + }); - Widget _buildFront() { - double altoBanner = Get.mediaQuery.size.height * 0.2; + Widget _buildFront(BuildContext context) { + double altoBanner = MediaQuery.of(context).size.height * 0.2; return Card( elevation: 1, clipBehavior: Clip.antiAlias, @@ -63,7 +62,7 @@ class CredencialCard extends StatelessWidget { padding: EdgeInsets.only(bottom: 20), child: Image.asset( 'assets/images/utem_logo_negativo.png', - width: Get.mediaQuery.size.width * 0.4, + width: MediaQuery.of(context).size.width * 0.4, ), ), ), @@ -274,7 +273,7 @@ class CredencialCard extends StatelessWidget { return AspectRatio( aspectRatio: 53.98 / 85.60, child: FlipWidget( - front: _buildFront(), + front: _buildFront(context), back: _buildRear(), controller: controller, onFlip: onFlip, diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 8483030..9d8ea54 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -118,7 +118,7 @@ class CustomDrawer extends StatelessWidget { Widget? route = _getRoute(e["nombre"]); if (route != null) { Navigator.push(context, MaterialPageRoute(builder: (ctx) => route)); - ReviewService.checkAndRequestReview(); + ReviewService.checkAndRequestReview(context); } }, ), @@ -133,7 +133,7 @@ class CustomDrawer extends StatelessWidget { title: const Text("Acerca de Mi UTEM"), onTap: () { Navigator.push(context, MaterialPageRoute(builder: (ctx) => AcercaScreen())); - ReviewService.checkAndRequestReview(); + ReviewService.checkAndRequestReview(context); }, ), ListTile( diff --git a/lib/widgets/field_list_tile.dart b/lib/widgets/field_list_tile.dart index 15666e3..58a9b8a 100644 --- a/lib/widgets/field_list_tile.dart +++ b/lib/widgets/field_list_tile.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; class FieldListTile extends StatelessWidget { final String title; @@ -27,10 +26,10 @@ class FieldListTile extends StatelessWidget { children: [ Text(title.toUpperCase(), maxLines: 2, - style: Get.textTheme.bodySmall!.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontWeight: FontWeight.bold), ), Text(value ?? "Sin información", - style: Get.textTheme.bodyMedium, + style: Theme.of(context).textTheme.bodyMedium, ), ], ), diff --git a/lib/widgets/footer_layout.dart b/lib/widgets/footer_layout.dart index 4f178fe..3a47b9c 100644 --- a/lib/widgets/footer_layout.dart +++ b/lib/widgets/footer_layout.dart @@ -2,8 +2,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - class FooterLayout extends StatelessWidget { const FooterLayout({ Key? key, @@ -17,8 +15,8 @@ class FooterLayout extends StatelessWidget { @override Widget build(BuildContext context) { return CustomMultiChildLayout( - delegate: _FooterLayoutDelegate(Get.mediaQuery.viewInsets), - children: [ + delegate: _FooterLayoutDelegate(MediaQuery.of(context).viewInsets), + children: [ LayoutId( id: _FooterLayout.body, child: body, @@ -45,8 +43,7 @@ class _FooterLayoutDelegate extends MultiChildLayoutDelegate { @override void performLayout(Size size) { size = Size(size.width, size.height + viewInsets.bottom); - final footer = - layoutChild(_FooterLayout.footer, BoxConstraints.loose(size)); + final footer = layoutChild(_FooterLayout.footer, BoxConstraints.loose(size)); final bodyConstraints = BoxConstraints.tightFor( height: size.height - max(footer.height, viewInsets.bottom), diff --git a/lib/widgets/bloque_dias_card.dart b/lib/widgets/horario/bloque_dias_card.dart similarity index 93% rename from lib/widgets/bloque_dias_card.dart rename to lib/widgets/horario/bloque_dias_card.dart index 2ab1ded..5ad9d20 100644 --- a/lib/widgets/bloque_dias_card.dart +++ b/lib/widgets/horario/bloque_dias_card.dart @@ -27,9 +27,8 @@ class BloqueDiasCard extends StatelessWidget { height: height, width: width, child: Column( - children: [ - Text( - day.trim(), + children: [ + Text(day.trim(), textAlign: TextAlign.center, style: TextStyle( color: Color(0xFF363636), diff --git a/lib/widgets/bloque_periodo_card.dart b/lib/widgets/horario/bloque_periodo_card.dart similarity index 100% rename from lib/widgets/bloque_periodo_card.dart rename to lib/widgets/horario/bloque_periodo_card.dart diff --git a/lib/widgets/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart similarity index 100% rename from lib/widgets/bloque_ramo_card.dart rename to lib/widgets/horario/bloque_ramo_card.dart diff --git a/lib/widgets/image_view_screen.dart b/lib/widgets/image_view_screen.dart index a3065c7..b479298 100644 --- a/lib/widgets/image_view_screen.dart +++ b/lib/widgets/image_view_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; -import 'package:get/get.dart'; import 'package:photo_view/photo_view.dart'; class ImageViewScreen extends StatefulWidget { @@ -24,20 +23,14 @@ class _ImageViewScreenState extends State { Widget build(BuildContext context) { final photoView = PhotoView( imageProvider: widget.imageProvider, - heroAttributes: widget.heroTag != null - ? PhotoViewHeroAttributes(tag: widget.heroTag!) - : null, + heroAttributes: widget.heroTag != null ? PhotoViewHeroAttributes(tag: widget.heroTag!) : null, ); return Scaffold( body: Stack( - children: [ + children: [ Container( - child: widget.occlude - ? OccludeWrapper( - child: photoView, - ) - : photoView, + child: widget.occlude ? OccludeWrapper(child: photoView) : photoView, ), SafeArea( child: Padding( @@ -52,9 +45,7 @@ class _ImageViewScreenState extends State { Icons.arrow_back, size: 20, ), - onPressed: () { - Get.back(); - }, + onPressed: () => Navigator.pop(context), color: Colors.white, ), ), diff --git a/lib/widgets/imagen_editor_modal.dart b/lib/widgets/imagen_editor_modal.dart index fc0ea49..43d52fe 100644 --- a/lib/widgets/imagen_editor_modal.dart +++ b/lib/widgets/imagen_editor_modal.dart @@ -1,10 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:flutter/material.dart'; - import 'package:extended_image/extended_image.dart'; -import 'package:get/get.dart'; +import 'package:flutter/material.dart'; import 'package:image_editor/image_editor.dart'; class ImagenEditorModal extends StatefulWidget { @@ -51,7 +49,7 @@ class _ImagenEditorModalState extends State { extendBody: true, extendBodyBehindAppBar: true, bottomNavigationBar: new Theme( - data: Get.theme.copyWith( + data: Theme.of(context).copyWith( canvasColor: Colors.black.withOpacity(0.5), ), child: Container( @@ -108,7 +106,7 @@ class _ImagenEditorModalState extends State { break; default: Uint8List? imagen = await _cropImage(); - Get.back(result: imagen); + Navigator.pop(context, imagen); } }, ), diff --git a/lib/widgets/login_text_form_field.dart b/lib/widgets/login_text_form_field.dart index 80d2580..339c86a 100644 --- a/lib/widgets/login_text_form_field.dart +++ b/lib/widgets/login_text_form_field.dart @@ -2,7 +2,6 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; class LoginTextFormField extends StatefulWidget { LoginTextFormField({ @@ -64,24 +63,24 @@ class _LoginTextFormFieldState extends State { borderSide: BorderSide(color: Colors.white, width: 2)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25), - borderSide: BorderSide(color: Get.theme.primaryColor, width: 2)), + borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2)), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25), - borderSide: BorderSide(color: Get.theme.primaryColor, width: 2)), + borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2)), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25), borderSide: BorderSide(color: Colors.red, width: 2)), contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 20), labelStyle: TextStyle( color: _focusNode!.hasFocus - ? Get.theme.primaryColor - : (_error ? Get.theme.colorScheme.error : Color(0x80FFFFFF))), + ? Theme.of(context).primaryColor + : (_error ? Theme.of(context).colorScheme.error : Color(0x80FFFFFF))), errorStyle: TextStyle(color: Colors.red), hintStyle: TextStyle(color: Color(0x80FFFFFF)), prefixIcon: Icon(widget.icon, color: _focusNode!.hasFocus - ? Get.theme.primaryColor - : (_error ? Get.theme.colorScheme.error : Colors.white)), + ? Theme.of(context).primaryColor + : (_error ? Theme.of(context).colorScheme.error : Colors.white)), hintText: widget.hintText, labelText: widget.labelText, ), diff --git a/lib/widgets/nota_list_item.dart b/lib/widgets/nota_list_item.dart index 61e29da..1c1dd9a 100644 --- a/lib/widgets/nota_list_item.dart +++ b/lib/widgets/nota_list_item.dart @@ -1,7 +1,6 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; @@ -160,7 +159,7 @@ class NotaListItem extends StatelessWidget with WatchItMixin { }, child: Icon( Icons.delete, - color: Get.theme.primaryColor, + color: Theme.of(context).primaryColor, ), ) ], diff --git a/lib/widgets/noticias/noticia_card_widget.dart b/lib/widgets/noticias/noticia_card_widget.dart index 0793f0f..c6340ce 100644 --- a/lib/widgets/noticias/noticia_card_widget.dart +++ b/lib/widgets/noticias/noticia_card_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/services/analytics_service.dart'; @@ -54,7 +53,7 @@ class _NoticiaCardWidgetState extends State { widget._noticia.titulo!, maxLines: 3, overflow: TextOverflow.ellipsis, - style: Get.theme.textTheme.bodyLarge, + style: Theme.of(context).textTheme.bodyLarge, ), Spacer(), ], diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index a13471d..8d5d14d 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -1,6 +1,5 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; @@ -23,7 +22,7 @@ class NoticiasCarruselWidget extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Text("Noticias".toUpperCase(), - style: Get.textTheme.titleMedium!.copyWith( + style: Theme.of(context).textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, ), ), diff --git a/lib/widgets/permiso_card.dart b/lib/widgets/permiso_card.dart index 3c5fbe1..e39d323 100644 --- a/lib/widgets/permiso_card.dart +++ b/lib/widgets/permiso_card.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/models/permiso_covid.dart'; import 'package:mi_utem/screens/permiso_covid_screen.dart'; import 'package:mi_utem/themes/theme.dart'; @@ -40,20 +39,17 @@ class PermisoCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(permiso.perfil!), - Text( - permiso.motivo!, - style: Get.textTheme.bodyLarge!.copyWith( + Text(permiso.motivo!, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( color: MainTheme.primaryColor, fontWeight: FontWeight.bold, ), ), - if (permiso.campus != null) - Text( - "${permiso.campus ?? ''} (${permiso.campus!})", - style: Get.textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - ), + if (permiso.campus != null)Text("${permiso.campus ?? ''} (${permiso.campus!})", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, ), + ), ], ), ), @@ -66,8 +62,7 @@ class PermisoCard extends StatelessWidget { child: InkWell( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => PermisoCovidScreen(passId: "${permiso.id}"))), child: Center( - child: Text( - "Ver QR", + child: Text("Ver QR", style: TextStyle( color: Colors.black, fontWeight: FontWeight.w700, diff --git a/lib/widgets/permisos_section.dart b/lib/widgets/permisos_section.dart index 3d7c003..fb4b98b 100644 --- a/lib/widgets/permisos_section.dart +++ b/lib/widgets/permisos_section.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_covid.dart'; import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; @@ -21,7 +20,7 @@ class PermisosCovidSection extends StatelessWidget { child: Row( children: [ Text("Permisos activos".toUpperCase(), - style: Get.textTheme.titleMedium!.copyWith( + style: Theme.of(context).textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, ), ), diff --git a/lib/widgets/profile_photo.dart b/lib/widgets/profile_photo.dart index cfb675b..9a48f74 100644 --- a/lib/widgets/profile_photo.dart +++ b/lib/widgets/profile_photo.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; import 'package:circular_profile_avatar/circular_profile_avatar.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/user/user.dart'; @@ -112,7 +111,7 @@ class _ProfilePhotoState extends State { ), child: Icon( Icons.camera_alt, - color: Get.theme.primaryColor, + color: Theme.of(context).primaryColor, ), ), ), diff --git a/lib/widgets/progress_button.dart b/lib/widgets/progress_button.dart index ca2098c..a21bef3 100644 --- a/lib/widgets/progress_button.dart +++ b/lib/widgets/progress_button.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - class ProgressButton extends StatefulWidget { final Function callback; @@ -34,32 +32,30 @@ class _ProgressButtonState extends State } @override - Widget build(BuildContext context) { - return PhysicalModel( - color: Get.theme.primaryColor, - borderRadius: BorderRadius.circular(30), - child: Container( - key: _globalKey, - height: 48.0, - width: _width, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: - _state == 2 ? Colors.green : Get.theme.primaryColor, - padding: EdgeInsets.zero, - ), - child: buildButtonChild(), - onPressed: () {}, - onFocusChange: (isPressed) { - setState(() { - if (_state == 0) { - animateButton(); - } - }); - }, - ), - )); - } + Widget build(BuildContext context) => PhysicalModel( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(30), + child: Container( + key: _globalKey, + height: 48.0, + width: _width, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: _state == 2 ? Colors.green : Theme.of(context).primaryColor, + padding: EdgeInsets.zero, + ), + child: buildButtonChild(), + onPressed: () {}, + onFocusChange: (isPressed) { + setState(() { + if (_state == 0) { + animateButton(); + } + }); + }, + ), + ), + ); void animateButton() { double initialWidth = _globalKey.currentContext!.size!.width; diff --git a/lib/widgets/quick_menu_section.dart b/lib/widgets/quick_menu_section.dart index e75124c..da59d1e 100644 --- a/lib/widgets/quick_menu_section.dart +++ b/lib/widgets/quick_menu_section.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/widgets/quick_menu_card.dart'; @@ -22,7 +21,7 @@ class QuickMenuSection extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 20), child: Text( "Acceso rápido".toUpperCase(), - style: Get.textTheme.titleMedium!.copyWith( + style: Theme.of(context).textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, ), ), diff --git a/lib/widgets/sad_dialog.dart b/lib/widgets/sad_dialog.dart index a1ecca7..c1296c0 100644 --- a/lib/widgets/sad_dialog.dart +++ b/lib/widgets/sad_dialog.dart @@ -1,6 +1,5 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; final _formKey = GlobalKey(); diff --git a/lib/widgets/semestre_boletin_card.dart b/lib/widgets/semestre_boletin_card.dart index 234ab66..310aaa1 100644 --- a/lib/widgets/semestre_boletin_card.dart +++ b/lib/widgets/semestre_boletin_card.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - class SemestreBoletinCard extends StatelessWidget { final String? semestre; final int? aprobados; @@ -21,170 +19,184 @@ class SemestreBoletinCard extends StatelessWidget { : super(key: key); @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(left: 10.0, right: 10.0), - child: Card( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + Widget build(BuildContext context) => Container( + margin: const EdgeInsets.only(left: 10.0, right: 10.0), + child: Card( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0),), + color: Color(0xFFD8BFD8), + child: Container( + padding: EdgeInsets.symmetric(vertical: 10), + width: MediaQuery.of(context).size.width * 0.4, + child: Column( + children: [ + Text("Promedio", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.6), + ), + ), + Text(promedio.toString(), + style: TextStyle(color: Colors.black.withOpacity(0.6)), + ), + ], + ), + ), + ), + Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), + color: Color(0xFF98FB98), + child: Container( + padding: EdgeInsets.symmetric(vertical: 10), + width: MediaQuery.of(context).size.width * 0.4, + child: Column( + children: [ + Text("Aprobados", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.6), + ), + ), + Text(aprobados.toString(), + style: TextStyle( + color: Colors.black.withOpacity(0.6), + ), + ), + ], + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0),), + color: Color(0xFFFFC0CB), + child: Container( + padding: EdgeInsets.symmetric(vertical: 10), + width: MediaQuery.of(context).size.width * 0.4, + child: Column( + children: [ + Text("Reprobados", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.6), + ), + ), + Text(reprobados.toString(), + style: TextStyle( + color: Colors.black.withOpacity(0.6), + ), + ), + ], + ), + ), + ), + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + color: Color(0xFFFFDEAD), + child: Container( + padding: EdgeInsets.symmetric(vertical: 10), + width: MediaQuery.of(context).size.width * 0.4, + child: Column( + children: [ + Text("Convalidados", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.6), + ), + ), + Text(convalidados.toString(), + style: TextStyle( + color: Colors.black.withOpacity(0.6), + ), + ), + ], + ), + ), + ), + ], + ), + Column( + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: new Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.5, + child: Text("Asignatura", + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold), + ), ), - color: Color(0xFFD8BFD8), - child: Container( - padding: EdgeInsets.symmetric(vertical: 10), - width: Get.mediaQuery.size.width * 0.4, - child: Column( - children: [ - Text("Promedio", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black.withOpacity(0.6))), - Text(promedio.toString(), - style: TextStyle( - color: Colors.black.withOpacity(0.6))) - ], + Container( + width: MediaQuery.of(context).size.width * 0.2, + child: Text("Estado", + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold), ), - )), - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), ), - color: Color(0xFF98FB98), - child: Container( - padding: EdgeInsets.symmetric(vertical: 10), - width: Get.mediaQuery.size.width * 0.4, - child: Column( - children: [ - Text("Aprobados", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black.withOpacity(0.6))), - Text(aprobados.toString(), - style: TextStyle( - color: Colors.black.withOpacity(0.6))) - ], + Container( + width: MediaQuery.of(context).size.width * 0.1, + child: Text("Nota", + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold), ), - )), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), ), - color: Color(0xFFFFC0CB), - child: Container( - padding: EdgeInsets.symmetric(vertical: 10), - width: Get.mediaQuery.size.width * 0.4, - child: Column( - children: [ - Text("Reprobados", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black.withOpacity(0.6))), - Text(reprobados.toString(), - style: TextStyle( - color: Colors.black.withOpacity(0.6))) - ], + ], + ), + ), + new ListView.separated( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemCount: ramos!.length, + separatorBuilder: (BuildContext context, int i) => Divider( + indent: 20, + endIndent: 20, + ), + itemBuilder: (BuildContext context, int i) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.only(top: 5, bottom: 5), + width: MediaQuery.of(context).size.width * 0.5, + child: Text(ramos![i]["nombre"], + textAlign: TextAlign.center, ), - )), - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), ), - color: Color(0xFFFFDEAD), - child: Container( - padding: EdgeInsets.symmetric(vertical: 10), - width: Get.mediaQuery.size.width * 0.4, - child: Column( - children: [ - Text("Convalidados", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black.withOpacity(0.6))), - Text(convalidados.toString(), - style: TextStyle( - color: Colors.black.withOpacity(0.6))) - ], + Container( + padding: EdgeInsets.only(top: 5, bottom: 5), + width: MediaQuery.of(context).size.width * 0.2, + child: Text(ramos![i]["estado"].substring(0, 1), + textAlign: TextAlign.center, ), - )) - ], - ), - Column( - children: [ - Container( - padding: EdgeInsets.symmetric(vertical: 20), - child: new Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: Get.mediaQuery.size.width * 0.5, - child: Text("Asignatura", - textAlign: TextAlign.center, - style: - TextStyle(fontWeight: FontWeight.bold))), - Container( - width: Get.mediaQuery.size.width * 0.2, - child: Text("Estado", - textAlign: TextAlign.center, - style: - TextStyle(fontWeight: FontWeight.bold))), - Container( - width: Get.mediaQuery.size.width * 0.1, - child: Text("Nota", - textAlign: TextAlign.center, - style: - TextStyle(fontWeight: FontWeight.bold))), - ])), - new ListView.separated( - shrinkWrap: true, - physics: ClampingScrollPhysics(), - itemCount: ramos!.length, - separatorBuilder: (BuildContext context, int i) => Divider( - indent: 20, - endIndent: 20, - ), - itemBuilder: (BuildContext context, int i) { - return new Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.only(top: 5, bottom: 5), - width: Get.mediaQuery.size.width * 0.5, - child: Text( - ramos![i]["nombre"], - textAlign: TextAlign.center, - )), - Container( - padding: EdgeInsets.only(top: 5, bottom: 5), - width: Get.mediaQuery.size.width * 0.2, - child: Text( - ramos![i]["estado"].substring(0, 1), - textAlign: TextAlign.center, - )), - Container( - padding: EdgeInsets.only(top: 5, bottom: 5), - width: Get.mediaQuery.size.width * 0.1, - child: Text( - ramos![i]["nota"].toString(), - textAlign: TextAlign.center, - )), - ]); - }, + ), + Container( + padding: EdgeInsets.only(top: 5, bottom: 5), + width: MediaQuery.of(context).size.width * 0.1, + child: Text(ramos![i]["nota"].toString(), + textAlign: TextAlign.center, + ), + ), + ], ), - ], - ), - ], - ), - color: Colors.white, + ), + ], + ), + ], ), - ); - } + color: Colors.white, + ), + ); } From be8e90d4ee17cb4b134c0998c84d2df4a4640f6c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:03:14 -0300 Subject: [PATCH 031/194] patch: arreglado nombre de docente Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/asignatura/asignatura_resumen_tab.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/asignatura_resumen_tab.dart index a82376a..94e3888 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/asignatura_resumen_tab.dart @@ -39,7 +39,7 @@ class AsignaturaResumenTab extends StatelessWidget { GestureDetector( child: FieldListTile( title: "Docente", - value: asignatura.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ") ?? "Sin docente", // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. + value: asignatura.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ").replaceAll("- ", "") ?? "Sin docente", // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. ), onTap: () async { // TODO: Mostrar perfil del docente. From 8303fec1f323b73bb2a4014fc8e1c29835a8833c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:15:23 -0300 Subject: [PATCH 032/194] =?UTF-8?q?patch:=20peque=C3=B1os=20arreglos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/horario.dart | 2 +- lib/screens/horario/widgets/horario_days_header.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/horario.dart b/lib/models/horario.dart index 0a09d82..ae004c2 100644 --- a/lib/models/horario.dart +++ b/lib/models/horario.dart @@ -146,7 +146,7 @@ class Periodo { horaTermino: json["horaTermino"], ) : Periodo(); - static List fromJsonList(dynamic json) => json != null ? (json as List ?? []).map((item) => Periodo.fromJson(item)).toList() : []; + static List fromJsonList(dynamic json) => json != null ? (json as List).map((item) => Periodo.fromJson(item)).toList() : []; } class BloqueHorario { diff --git a/lib/screens/horario/widgets/horario_days_header.dart b/lib/screens/horario/widgets/horario_days_header.dart index 4f236cf..ab35443 100644 --- a/lib/screens/horario/widgets/horario_days_header.dart +++ b/lib/screens/horario/widgets/horario_days_header.dart @@ -33,7 +33,7 @@ class HorarioDaysHeader extends StatelessWidget { .entries .map( (entry) => BloqueDiasCard( - day: entry.value!, + day: entry.value, height: height, width: dayWidth, active: showActiveDay && entry.key == di.get().indexOfCurrentDayStartingAtMonday, From 14b14e448418ee6d989e91c9ddc39c97ace50f97 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:29:27 -0300 Subject: [PATCH 033/194] patch: eliminado getx de las dependencias Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.lock | 2 +- pubspec.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 65b0e32..1dc5ce6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -754,7 +754,7 @@ packages: source: hosted version: "2.3.1" get: - dependency: "direct main" + dependency: transitive description: name: get sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" diff --git a/pubspec.yaml b/pubspec.yaml index 293c939..a748efa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,6 @@ dependencies: flutter_spinkit: ^5.1.0 flutter_windowmanager: ^0.2.0 font_awesome_flutter: ^10.0.0 - get: ^4.1.4 gradient_widgets: ^0.6.0 html: ^0.15.0 image_editor: ^1.0.0 From 01ecfbf78b79a793f8a7851c8ac9583f3c520935 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:34:35 -0300 Subject: [PATCH 034/194] =?UTF-8?q?patch:=20reparada=20descripci=C3=B3n=20?= =?UTF-8?q?de=20la=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index a748efa..abc3c6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,5 @@ name: mi_utem -description: Plataforma académica para estudiantes de la Universidad Tecnológica - Metropolitana (UTEM) +description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none version: 3.0.0 From 6fb913c105c4004f6705509f83c8cea15d5578a3 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 22 Mar 2024 18:17:21 -0300 Subject: [PATCH 035/194] =?UTF-8?q?patch:=20modificado=20changelog,=20podf?= =?UTF-8?q?ile.lock=20y=20Fastfile=20(se=20removi=C3=B3=20un=20valor=20no?= =?UTF-8?q?=20necesario)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- CHANGELOG.md | 9 ++++++++- fastlane/Fastfile | 6 ++---- ios/Podfile.lock | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba86ae6..d316b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,14 @@ Tipos de cambios ### Changed -- Se actualizan dependecias de Flutter +- Se actualizan dependencias de Flutter +- Se actualiza a enrutador nativo de flutter +- Se mejora el rendimiento de la aplicación +- Se ordenan clases y widgets + +### Removed + +- Dependencia GetX ## [2.11.9] - 2023-10-11Z diff --git a/fastlane/Fastfile b/fastlane/Fastfile index fa0e814..aab4522 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -59,8 +59,7 @@ platform :mobile do slack( message: "Este es un mensaje de prueba desde Fastlane", - slack_url: ENV["SLACK_URL"], - channel: "#proy-mi-utem-bots", + channel: "proy-mi-utem-bots", default_payloads: [], attachment_properties: { fields: [ @@ -179,8 +178,7 @@ platform :mobile do unless skip_slack slack( message: "🚀 Se ha publicado la nueva versión #{build_name} (#{build_number})", - slack_url: ENV["SLACK_URL"], - channel: "#proy-mi-utem", + channel: "proy-mi-utem", default_payloads: [], attachment_properties: { fields: [ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a89a8b4..b97f735 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1081,4 +1081,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 816280d49dfc997733647f095bf1e4c52c291937 -COCOAPODS: 1.14.2 +COCOAPODS: 1.15.2 From a160f5fcd66e5755596b91d053f08c1d10ed9353 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 25 Mar 2024 00:51:34 -0300 Subject: [PATCH 036/194] =?UTF-8?q?patch:=20arreglos=20y=20orden=20*=20Se?= =?UTF-8?q?=20ha=20renombrado=20permiso=5Fcovid=20a=20permiso=20de=20ingre?= =?UTF-8?q?so=20*=20Se=20ha=20reparado=20une=20error=20al=20cargar=20el=20?= =?UTF-8?q?horario=20por=20primera=20vez=20*=20Se=20ha=20movido=20todo=20l?= =?UTF-8?q?o=20relacionado=20a=20rest=20a=20"repositorios"=20*=20Se=20ha?= =?UTF-8?q?=20movido=20el=20=C3=BAltimo=20controlador=20(de=20notificacion?= =?UTF-8?q?es)=20a=20lib/services=5Fnew/legacy=5Fcontrollers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ermiso_covid.dart => permiso_ingreso.dart} | 8 +- .../asignatura/asignatura_notas_tab.dart | 4 +- .../asignatura/asignaturas_lista_screen.dart | 6 +- lib/screens/boletin_screen.dart | 2 +- lib/screens/credencial_screen.dart | 2 +- lib/screens/docentes_screen.dart | 2 +- lib/screens/permiso_covid_screen.dart | 10 +- lib/screens/splash_screen.dart | 2 +- lib/services/notification_service.dart | 2 +- .../implementations/auth_service.dart | 95 ++++--------------- .../implementations/carreras_service.dart | 39 +++----- .../controllers/horario_controller.dart | 12 +-- .../implementations/grades_service.dart | 34 +------ .../asignaturas_repository.dart} | 4 +- .../repositories/auth_repository.dart | 85 +++++++++++++++++ .../repositories/carreras_repository.dart | 27 ++++++ .../credentials_repository.dart} | 4 +- .../repositories/grades_repository.dart | 27 ++++++ .../horario_repository.dart} | 6 +- .../noticias_repository.dart} | 4 +- .../permiso_ingreso_repository.dart} | 15 ++- .../interfaces/carreras_service.dart | 5 +- .../interfaces/credential_service.dart | 10 -- .../interfaces/qr_pass_service.dart | 8 -- .../asignaturas_repository.dart} | 4 +- .../repositories/auth_repository.dart | 21 ++++ .../repositories/carreras_repository.dart | 7 ++ .../repositories/credentials_repository.dart | 22 +++++ .../repositories/grades_repository.dart | 10 ++ .../horario_repository.dart} | 2 +- .../noticias_repository.dart} | 2 +- .../permiso_ingreso_repository.dart | 8 ++ .../notification_controller.dart | 0 lib/services_new/service_manager.dart | 45 +++++---- lib/widgets/login_screen/login_button.dart | 4 +- lib/widgets/login_screen/login_form.dart | 4 +- .../noticias/noticias_carrusel_widget.dart | 4 +- lib/widgets/permiso_card.dart | 4 +- lib/widgets/permisos_section.dart | 8 +- 39 files changed, 337 insertions(+), 221 deletions(-) rename lib/models/{permiso_covid.dart => permiso_ingreso.dart} (75%) rename lib/services_new/implementations/{asignaturas_service.dart => repositories/asignaturas_repository.dart} (90%) create mode 100644 lib/services_new/implementations/repositories/auth_repository.dart create mode 100644 lib/services_new/implementations/repositories/carreras_repository.dart rename lib/services_new/implementations/{credential_service.dart => repositories/credentials_repository.dart} (79%) create mode 100644 lib/services_new/implementations/repositories/grades_repository.dart rename lib/services_new/implementations/{horario_service.dart => repositories/horario_repository.dart} (73%) rename lib/services_new/implementations/{noticias_service.dart => repositories/noticias_repository.dart} (82%) rename lib/services_new/implementations/{qr_pass_service.dart => repositories/permiso_ingreso_repository.dart} (67%) delete mode 100644 lib/services_new/interfaces/credential_service.dart delete mode 100644 lib/services_new/interfaces/qr_pass_service.dart rename lib/services_new/interfaces/{asignaturas_service.dart => repositories/asignaturas_repository.dart} (68%) create mode 100644 lib/services_new/interfaces/repositories/auth_repository.dart create mode 100644 lib/services_new/interfaces/repositories/carreras_repository.dart create mode 100644 lib/services_new/interfaces/repositories/credentials_repository.dart create mode 100644 lib/services_new/interfaces/repositories/grades_repository.dart rename lib/services_new/interfaces/{horario_service.dart => repositories/horario_repository.dart} (77%) rename lib/services_new/interfaces/{noticias_service.dart => repositories/noticias_repository.dart} (70%) create mode 100644 lib/services_new/interfaces/repositories/permiso_ingreso_repository.dart rename lib/{controllers => services_new/legacy_controllers}/notification_controller.dart (100%) diff --git a/lib/models/permiso_covid.dart b/lib/models/permiso_ingreso.dart similarity index 75% rename from lib/models/permiso_covid.dart rename to lib/models/permiso_ingreso.dart index 34dc0bc..18a9528 100644 --- a/lib/models/permiso_covid.dart +++ b/lib/models/permiso_ingreso.dart @@ -1,6 +1,6 @@ import 'package:mi_utem/models/user/user.dart'; -class PermisoCovid { +class PermisoIngreso { String? id; User? user; String? codigoQr; @@ -12,7 +12,7 @@ class PermisoCovid { String? vigencia; DateTime? fechaSolicitud; - PermisoCovid({ + PermisoIngreso({ this.id, this.user, this.codigoQr, @@ -25,7 +25,7 @@ class PermisoCovid { this.fechaSolicitud, }); - factory PermisoCovid.fromJson(Map json) => PermisoCovid( + factory PermisoIngreso.fromJson(Map json) => PermisoIngreso( id: json['id'], user: json.containsKey("usuario") ? User.fromJson(json['usuario']) : null, codigoQr: json['codigoQr'], @@ -38,5 +38,5 @@ class PermisoCovid { fechaSolicitud: DateTime.tryParse(json['fechaSolicitud']), ); - static List fromJsonList(List? json) => json != null ? json.map((it) => PermisoCovid.fromJson(it)).toList() : []; + static List fromJsonList(List? json) => json != null ? json.map((it) => PermisoIngreso.fromJson(it)).toList() : []; } diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index b0fc269..cb10170 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; -import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; @@ -34,7 +34,7 @@ class _AsignaturaNotasTabState extends State { @override Widget build(BuildContext context) => PullToRefresh( onRefresh: () async { - final asignatura = await di.get().getDetalleAsignatura(this.asignatura.codigo); + final asignatura = await di.get().getDetalleAsignatura(this.asignatura.codigo); if(asignatura == null) { showErrorSnackbar(context, "Ocurrió un error actualizar la asignatura. Por favor intenta más tarde."); return; diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index 9f2ea83..7f9e0b5 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -4,7 +4,7 @@ import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; import 'package:mi_utem/widgets/asignatura/lista/lista_asignaturas.dart'; import 'package:mi_utem/widgets/asignatura/lista/sin_asignaturas_mensaje.dart'; @@ -21,7 +21,7 @@ class AsignaturasListaScreen extends StatefulWidget with WatchItStatefulWidgetMi } class _AsignaturasListaScreenState extends State { - final _asignaturasService = di.get(); + final _asignaturasService = di.get(); bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; @@ -46,7 +46,7 @@ class _AsignaturasListaScreenState extends State { final carrerasService = di.get(); final selectedCarrera = watchValue((CarrerasService service) => service.selectedCarrera); if (selectedCarrera == null) { - await carrerasService.getCarreras(forceRefresh: true); + await carrerasService.getCarreras(); } return await _asignaturasService.getAsignaturas(selectedCarrera?.id); diff --git a/lib/screens/boletin_screen.dart b/lib/screens/boletin_screen.dart index 3bcf906..1043b61 100644 --- a/lib/screens/boletin_screen.dart +++ b/lib/screens/boletin_screen.dart @@ -4,7 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:mi_utem/models/boletin_notas.dart'; import 'package:mi_utem/services/boletin_service.dart'; -import 'package:mi_utem/services/horario_service.dart'; +import 'package:mi_utem/services/horario_repository.dart'; import 'package:mi_utem/widgets/custom_expansion_tile.dart' as custom; import 'package:mi_utem/widgets/info_boletin_card.dart'; import 'package:mi_utem/widgets/semestre_boletin_card.dart'; diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index 31e15ec..1071609 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -68,7 +68,7 @@ class _CredencialScreenState extends State { future: () async { final user = await di.get().getUser(); if(carreraActiva == null) { - await di.get().getCarreras(forceRefresh: true); + await di.get().getCarreras(); } return user; diff --git a/lib/screens/docentes_screen.dart b/lib/screens/docentes_screen.dart index 2828276..3f457bb 100644 --- a/lib/screens/docentes_screen.dart +++ b/lib/screens/docentes_screen.dart @@ -121,7 +121,7 @@ class _DocentesScreenState extends State { radius: 20, editable: false, ), - title: Text(docente.nombreCompleto ?? "N/N", + title: Text(docente.nombreCompleto, maxLines: 2, overflow: TextOverflow.ellipsis, ), diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index e937e8c..733350d 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -8,9 +8,9 @@ import 'package:image/image.dart' as dartImage; import 'package:intl/intl.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/models/permiso_covid.dart'; +import 'package:mi_utem/models/permiso_ingreso.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; @@ -36,8 +36,8 @@ class _PermisoCovidScreenState extends State { appBar: CustomAppBar(title: Text("Permiso de ingreso")), body: PullToRefresh( onRefresh: () async => setState(() {}), - child: FutureBuilder( - future: di.get().getDetallesPermiso(widget.passId), + child: FutureBuilder( + future: di.get().getDetallesPermiso(widget.passId), builder: (context, snapshot) { if (snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "No sabemos lo que ocurrió. Por favor intenta más tarde."; @@ -79,7 +79,7 @@ class QRCard extends StatelessWidget { required this.permiso, }) : super(key: key); - final PermisoCovid permiso; + final PermisoIngreso permiso; _openQr(BuildContext context, String heroTag) { final image = dartImage.Image(500, 500); diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 23d0230..360015a 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -100,7 +100,7 @@ class _SplashScreenState extends State { AnalyticsService.setUser(user); } } - Navigator.pop(context); + Navigator.popUntil(context, (route) => route.isFirst); Navigator.pushReplacement(context, MaterialPageRoute(builder: (ctx) => isLoggedIn ? MainScreen() : LoginScreen())); }, ), diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 2ec8bcb..df293b6 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/controllers/notification_controller.dart'; +import 'package:mi_utem/services_new/legacy_controllers/notification_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/widgets/custom_alert_dialog.dart'; diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services_new/implementations/auth_service.dart index f309855..9a97215 100644 --- a/lib/services_new/implementations/auth_service.dart +++ b/lib/services_new/implementations/auth_service.dart @@ -2,40 +2,43 @@ import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/config/secure_storage.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:mi_utem/services_new/interfaces/credential_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/auth_repository.dart'; import 'package:watch_it/watch_it.dart'; class AuthServiceImplementation implements AuthService { - final _credentialsService = di.get(); + final _authRepository = di.get(); + final _credentialsService = di.get(); @override Future isFirstTime() async => !(await secureStorage.containsKey(key: "last_login")); @override Future isLoggedIn({ bool forceRefresh = false }) async { - final credential = await _getCredential(); - if(credential == null) { + final credentials = await _getCredential(); + if(credentials == null) { + logger.d("[AuthService#isLoggedIn]: no credential"); return false; } final user = await getUser(); - if(user == null) { + final userToken = user?.token; + if(user == null || userToken == null) { + logger.d("[AuthService#isLoggedIn]: user || token => false => ${user == null} || ${userToken == null}"); return false; } final lastLogin = await secureStorage.read(key: "last_login"); if(lastLogin == null) { + logger.d("[AuthService#isLoggedIn]: no last login"); return false; } @@ -47,31 +50,7 @@ class AuthServiceImplementation implements AuthService { } try { - final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth/refresh"), - body: credential.toString(), - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - 'Authorization': 'Bearer ${user.token}', - }, - ); - - - final json = jsonDecode(response.body); - - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json); - } - - throw CustomException.custom(response.reasonPhrase); - } - - - final token = json["token"] as String; - if(token == user.token) { - return true; - } + final token = _authRepository.refresh(token: userToken, credentials: credentials); final userJson = user.toJson(); userJson["token"] = token; @@ -87,29 +66,15 @@ class AuthServiceImplementation implements AuthService { @override Future login() async { - final credential = await _getCredential(); - if(credential == null) { + final credentials = await _getCredential(); + if(credentials == null) { return; } - final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth"), - body: credential.toString(), - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - }, - ); - - final json = jsonDecode(response.body); - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json); - } - - throw CustomException.custom(response.reasonPhrase); - } + final user = await _authRepository.auth(credentials: credentials); - await setUser(User.fromJson(json as Map)); + await setUser(user); + await secureStorage.write(key: "last_login", value: "${DateTime.now().millisecondsSinceEpoch}"); } @override @@ -152,28 +117,10 @@ class AuthServiceImplementation implements AuthService { return null; } - final response = await httpClient.put(Uri.parse("$apiUrl/v1/usuarios/foto"), - body: jsonEncode({"imagen": image}), - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - 'Authorization': 'Bearer ${user.token}', - }, - ); - - final json = jsonDecode(response.body) as Map; - - if(response.statusCode != 200) { - if(json.containsKey("error")) { - throw CustomException.fromJson(json); - } - - throw CustomException.custom(response.reasonPhrase); - } - - user.fotoUrl = json["fotoUrl"] as String; - await setUser(user); - + final _fotoUrl = _authRepository.updateProfilePicture(image: image); + final jsonUser = user.toJson(); + jsonUser["fotoUrl"] = _fotoUrl; + await setUser(User.fromJson(jsonUser)); return user; } diff --git a/lib/services_new/implementations/carreras_service.dart b/lib/services_new/implementations/carreras_service.dart index 0fb8122..60e9b66 100644 --- a/lib/services_new/implementations/carreras_service.dart +++ b/lib/services_new/implementations/carreras_service.dart @@ -1,40 +1,31 @@ -import 'dart:convert'; import 'package:flutter/widgets.dart'; -import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; +import 'package:listenable_collections/listenable_collections.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/carreras_repository.dart'; +import 'package:watch_it/watch_it.dart'; -class CarrerasServiceImplementation extends ChangeNotifier implements CarrerasService { +class CarrerasServiceImplementation implements CarrerasService { + + final _carrerasRepository = di.get(); @override - ValueNotifier> carreras = ValueNotifier([]); + ListNotifier carreras = ListNotifier(); @override ValueNotifier selectedCarrera = ValueNotifier(null); @override - Future getCarreras({bool forceRefresh = false}) async { + Future getCarreras() async { logger.d("[CarrerasService#getCarreras]: Obteniendo carreras..."); - final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras")); - - final json = jsonDecode(response.body); - if(response.statusCode != 200) { - logger.e("Error al obtener carreras: $json}"); - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); - } - - throw CustomException.custom(response.reasonPhrase); - } + final _carreras = await _carrerasRepository.getCarreras(); - carreras.value = Carrera.fromJsonList(json); + carreras.clear(); + carreras.addAll(_carreras); autoSelectCarreraActiva(); - notifyListeners(); } @override @@ -42,16 +33,16 @@ class CarrerasServiceImplementation extends ChangeNotifier implements CarrerasSe @override void autoSelectCarreraActiva() { - logger.d("[CarrerasService#autoSelectCarreraActiva]: Seleccionando carrera activa... ${carreras.value.map((e) => e.toJson()).toList()}"); + logger.d("[CarrerasService#autoSelectCarreraActiva]: Seleccionando carrera activa... ${carreras.map((e) => e.toJson()).toList()}"); final estados = ["Regular", "Causal de Eliminacion"] .reversed .map((e) => e.toLowerCase()) .toList(); logger.d("[CarrerasService#autoSelectCarreraActiva]: Estados: $estados"); - carreras.value.sort((a,b) => estados.indexOf(b.estado!.toLowerCase()).compareTo(estados.indexOf(a.estado!.toLowerCase()))); - logger.d("[CarrerasService#autoSelectCarreraActiva]: Carreras ordenadas: ${carreras.value.map((e) => e.toJson()).toList()}"); - final carreraActiva = carreras.value.first; + carreras.sort((a,b) => estados.indexOf(b.estado!.toLowerCase()).compareTo(estados.indexOf(a.estado!.toLowerCase()))); + logger.d("[CarrerasService#autoSelectCarreraActiva]: Carreras ordenadas: ${carreras.map((e) => e.toJson()).toList()}"); + final carreraActiva = carreras.first; AnalyticsService.setCarreraToUser(carreraActiva); changeSelectedCarrera(carreraActiva); diff --git a/lib/services_new/implementations/controllers/horario_controller.dart b/lib/services_new/implementations/controllers/horario_controller.dart index 767bf73..4ba5424 100644 --- a/lib/services_new/implementations/controllers/horario_controller.dart +++ b/lib/services_new/implementations/controllers/horario_controller.dart @@ -7,7 +7,7 @@ import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; -import 'package:mi_utem/services_new/interfaces/horario_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/horario_repository.dart'; import 'package:vector_math/vector_math_64.dart' as vector; import 'package:watch_it/watch_it.dart'; @@ -98,11 +98,11 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC @override Future getHorarioData({ bool forceRefresh = false }) async { loadingHorario.value = true; - final lastUpdate = _storage.read("last_horario_update") ?? "${DateTime.now().toIso8601String()}"; - final lastUpdateDate = DateTime.parse(lastUpdate); final now = DateTime.now(); + final lastUpdate = _storage.read("last_update_horario") ?? "${now.toIso8601String()}"; + final lastUpdateDate = DateTime.parse(lastUpdate); final difference = now.difference(lastUpdateDate).inMinutes; - if(difference < 15 && forceRefresh == false && horario.value != null) { + if(difference < 15 && forceRefresh == false && horario.value != null && lastUpdate != now.toIso8601String()) { loadingHorario.value = false; return; } @@ -113,7 +113,7 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC final carrerasService = di.get(); Carrera? carrera = carrerasService.selectedCarrera.value; if (carrera == null) { - await carrerasService.getCarreras(forceRefresh: true); + await carrerasService.getCarreras(); } carrera = carrerasService.selectedCarrera.value; @@ -122,7 +122,7 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC loadingHorario.value = false; return; } - horario.value = await di.get().getHorario(carreraId); + horario.value = await di.get().getHorario(carreraId); _setRandomColorsByHorario(); loadingHorario.value = false; diff --git a/lib/services_new/implementations/grades_service.dart b/lib/services_new/implementations/grades_service.dart index 625f7fa..02b6bdb 100644 --- a/lib/services_new/implementations/grades_service.dart +++ b/lib/services_new/implementations/grades_service.dart @@ -1,48 +1,24 @@ import 'dart:convert'; -import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; import 'package:mi_utem/services_new/interfaces/grades_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/grades_repository.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:watch_it/watch_it.dart'; class GradesServiceImplementation implements GradesService { static const savedGradesPrefix = 'savedGrades_'; static const subscribedAsignaturasPrefix = 'subscribedAsignaturas_'; + final _gradesRepository = di.get(); @override Future getGrades(String carreraId, String asignaturaId, {bool forceRefresh = false, bool saveGrades = true}) async { - final user = await di.get().getUser(); - if(user == null) { - return null; - } - - final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas"), - headers: { - 'Authorization': 'Bearer ${user.token}', - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - }, - ); - - final json = jsonDecode(response.body); - - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); - } - - throw CustomException.custom(response.reasonPhrase); - } - - final grades = Grades.fromJson(json as Map); + final grades = await _gradesRepository.getGrades(carreraId: carreraId, asignaturaId: asignaturaId); if(saveGrades) { await this.saveGrades(asignaturaId, grades); @@ -76,7 +52,7 @@ class GradesServiceImplementation implements GradesService { final subscribedAsignaturasJson = await secureStorage.read(key: '$subscribedAsignaturasPrefix$carreraId'); List subscribedAsignaturas; if(subscribedAsignaturasJson == null) { - subscribedAsignaturas = (await di.get().getAsignaturas(carreraId)) ?? []; + subscribedAsignaturas = (await di.get().getAsignaturas(carreraId)) ?? []; await secureStorage.write(key: '$subscribedAsignaturasPrefix$carreraId', value: jsonEncode(subscribedAsignaturas.map((it) => it.toJson()).toList())); } else { subscribedAsignaturas = Asignatura.fromJsonList(jsonDecode(subscribedAsignaturasJson) as List); diff --git a/lib/services_new/implementations/asignaturas_service.dart b/lib/services_new/implementations/repositories/asignaturas_repository.dart similarity index 90% rename from lib/services_new/implementations/asignaturas_service.dart rename to lib/services_new/implementations/repositories/asignaturas_repository.dart index 806016f..fee1a9c 100644 --- a/lib/services_new/implementations/asignaturas_service.dart +++ b/lib/services_new/implementations/repositories/asignaturas_repository.dart @@ -5,9 +5,9 @@ import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; -class AsignaturasServiceImplementation implements AsignaturasService { +class AsignaturasRepositoryImplementation implements AsignaturasRepository { @override Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}) async { diff --git a/lib/services_new/implementations/repositories/auth_repository.dart b/lib/services_new/implementations/repositories/auth_repository.dart new file mode 100644 index 0000000..656bb73 --- /dev/null +++ b/lib/services_new/implementations/repositories/auth_repository.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; + +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/user/credential.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/auth_repository.dart'; + +class AuthRepositoryImplementation extends AuthRepository { + + @override + Future auth({ + required Credentials credentials, + }) async { + final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth"), + body: credentials.toString(), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + }, + ); + + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return User.fromJson(json as Map); + } + + @override + Future refresh({ + required String token, + required Credentials credentials + }) async { + final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth/refresh"), + body: credentials.toString(), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'App/MiUTEM', + 'Authorization': 'Bearer $token', + }, + ); + + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return json["token"]; + } + + @override + Future updateProfilePicture({required String image}) async { + final response = await authClient.put(Uri.parse("$apiUrl/v1/usuarios/foto"), + body: jsonEncode({"imagen": image}), + ); + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return json["fotoUrl"] as String; + } + +} \ No newline at end of file diff --git a/lib/services_new/implementations/repositories/carreras_repository.dart b/lib/services_new/implementations/repositories/carreras_repository.dart new file mode 100644 index 0000000..2f770c7 --- /dev/null +++ b/lib/services_new/implementations/repositories/carreras_repository.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; + +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; +import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/carreras_repository.dart'; + +class CarrerasRepositoryImplementation extends CarrerasRepository { + + @override + Future> getCarreras() async { + final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras")); + + final json = jsonDecode(response.body); + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return Carrera.fromJsonList(json as List); + } + +} \ No newline at end of file diff --git a/lib/services_new/implementations/credential_service.dart b/lib/services_new/implementations/repositories/credentials_repository.dart similarity index 79% rename from lib/services_new/implementations/credential_service.dart rename to lib/services_new/implementations/repositories/credentials_repository.dart index 4ed0196..54aedc1 100644 --- a/lib/services_new/implementations/credential_service.dart +++ b/lib/services_new/implementations/repositories/credentials_repository.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/user/credential.dart'; -import 'package:mi_utem/services_new/interfaces/credential_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; -class CredentialsServiceImplementation implements CredentialsService { +class CredentialsRepositoryImplementation implements CredentialsRepository { @override Future getCredentials() async { diff --git a/lib/services_new/implementations/repositories/grades_repository.dart b/lib/services_new/implementations/repositories/grades_repository.dart new file mode 100644 index 0000000..69e0b65 --- /dev/null +++ b/lib/services_new/implementations/repositories/grades_repository.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; + +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/http_clients.dart'; +import 'package:mi_utem/models/evaluacion/grades.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/grades_repository.dart'; + +class GradesRepositoryImplementation extends GradesRepository { + + @override + Future getGrades({required String carreraId, required String asignaturaId}) async { + final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas")); + + final json = jsonDecode(response.body); + + if(response.statusCode != 200) { + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(response.reasonPhrase); + } + + return Grades.fromJson(json as Map); + } +} \ No newline at end of file diff --git a/lib/services_new/implementations/horario_service.dart b/lib/services_new/implementations/repositories/horario_repository.dart similarity index 73% rename from lib/services_new/implementations/horario_service.dart rename to lib/services_new/implementations/repositories/horario_repository.dart index 07f0a33..7e79382 100644 --- a/lib/services_new/implementations/horario_service.dart +++ b/lib/services_new/implementations/repositories/horario_repository.dart @@ -2,12 +2,11 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/services_new/interfaces/horario_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/horario_repository.dart'; -class HorarioServiceImplementation implements HorarioService { +class HorarioRepositoryImplementation implements HorarioRepository { @override Future getHorario(String carreraId, {bool forceRefresh = false}) async { final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/horarios")); @@ -15,7 +14,6 @@ class HorarioServiceImplementation implements HorarioService { final json = jsonDecode(response.body); if(response.statusCode != 200) { - logger.e("[HorarioService] Error al obtener horario: ${response.reasonPhrase}", json); if(json is Map && json.containsKey("error")) { throw CustomException.fromJson(json as Map); } diff --git a/lib/services_new/implementations/noticias_service.dart b/lib/services_new/implementations/repositories/noticias_repository.dart similarity index 82% rename from lib/services_new/implementations/noticias_service.dart rename to lib/services_new/implementations/repositories/noticias_repository.dart index 4b1c550..a86246a 100644 --- a/lib/services_new/implementations/noticias_service.dart +++ b/lib/services_new/implementations/repositories/noticias_repository.dart @@ -4,9 +4,9 @@ import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/noticias_repository.dart'; -class NoticiasServiceImplementation implements NoticiasService { +class NoticiasRepositoryImplementation implements NoticiasRepository { @override Future?> getNoticias() async { diff --git a/lib/services_new/implementations/qr_pass_service.dart b/lib/services_new/implementations/repositories/permiso_ingreso_repository.dart similarity index 67% rename from lib/services_new/implementations/qr_pass_service.dart rename to lib/services_new/implementations/repositories/permiso_ingreso_repository.dart index d8a3c13..29dda07 100644 --- a/lib/services_new/implementations/qr_pass_service.dart +++ b/lib/services_new/implementations/repositories/permiso_ingreso_repository.dart @@ -3,13 +3,13 @@ import 'dart:convert'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/models/permiso_covid.dart'; -import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; +import 'package:mi_utem/models/permiso_ingreso.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/permiso_ingreso_repository.dart'; -class QRPassServiceImplementation extends QRPassService { +class PermisoIngresoRepositoryImplementation extends PermisoIngresoRepository { @override - Future getDetallesPermiso(String id, {bool forceRefresh = false}) async { + Future getDetallesPermiso(String id) async { final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos/$id")); final json = jsonDecode(response.body) as Map; @@ -22,11 +22,11 @@ class QRPassServiceImplementation extends QRPassService { throw CustomException.custom(response.reasonPhrase); } - return PermisoCovid.fromJson(json); + return PermisoIngreso.fromJson(json); } @override - Future?> getPermisos({bool forceRefresh = false}) async { + Future> getPermisos() async { final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos")); final json = jsonDecode(response.body); @@ -39,8 +39,7 @@ class QRPassServiceImplementation extends QRPassService { throw CustomException.custom(response.reasonPhrase); } - return PermisoCovid.fromJsonList(json as List); + return PermisoIngreso.fromJsonList(json as List); } - } \ No newline at end of file diff --git a/lib/services_new/interfaces/carreras_service.dart b/lib/services_new/interfaces/carreras_service.dart index 501225e..a301906 100644 --- a/lib/services_new/interfaces/carreras_service.dart +++ b/lib/services_new/interfaces/carreras_service.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:listenable_collections/listenable_collections.dart'; import 'package:mi_utem/models/carrera.dart'; abstract class CarrerasService { - abstract ValueNotifier> carreras; + abstract ListNotifier carreras; abstract ValueNotifier selectedCarrera; - Future getCarreras({bool forceRefresh = false}); + Future getCarreras(); void changeSelectedCarrera(Carrera carrera); diff --git a/lib/services_new/interfaces/credential_service.dart b/lib/services_new/interfaces/credential_service.dart deleted file mode 100644 index 4c7513f..0000000 --- a/lib/services_new/interfaces/credential_service.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:mi_utem/models/user/credential.dart'; - -abstract class CredentialsService { - - Future hasCredentials(); - - Future getCredentials(); - - Future setCredentials(Credentials? credential); -} \ No newline at end of file diff --git a/lib/services_new/interfaces/qr_pass_service.dart b/lib/services_new/interfaces/qr_pass_service.dart deleted file mode 100644 index 517e815..0000000 --- a/lib/services_new/interfaces/qr_pass_service.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:mi_utem/models/permiso_covid.dart'; - -abstract class QRPassService { - - Future?> getPermisos({bool forceRefresh = false}); - - Future getDetallesPermiso(String id, {bool forceRefresh = false}); -} \ No newline at end of file diff --git a/lib/services_new/interfaces/asignaturas_service.dart b/lib/services_new/interfaces/repositories/asignaturas_repository.dart similarity index 68% rename from lib/services_new/interfaces/asignaturas_service.dart rename to lib/services_new/interfaces/repositories/asignaturas_repository.dart index be1cc54..57c4358 100644 --- a/lib/services_new/interfaces/asignaturas_service.dart +++ b/lib/services_new/interfaces/repositories/asignaturas_repository.dart @@ -1,8 +1,10 @@ import 'package:mi_utem/models/asignaturas/asignatura.dart'; -abstract class AsignaturasService { +abstract class AsignaturasRepository { + /* Obtiene las asignaturas */ Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}); + /* Obtiene el detalle de una asignatura */ Future getDetalleAsignatura(String? asignaturaId, {bool forceRefresh = false}); } \ No newline at end of file diff --git a/lib/services_new/interfaces/repositories/auth_repository.dart b/lib/services_new/interfaces/repositories/auth_repository.dart new file mode 100644 index 0000000..3ccb8b9 --- /dev/null +++ b/lib/services_new/interfaces/repositories/auth_repository.dart @@ -0,0 +1,21 @@ +import 'package:mi_utem/models/user/credential.dart'; +import 'package:mi_utem/models/user/user.dart'; + +abstract class AuthRepository { + + /* Envía las credenciales y retorna al usuario */ + Future auth({ + required Credentials credentials, + }); + + /* Refresca y retorna el nuevo token */ + Future refresh({ + required String token, + required Credentials credentials, + }); + + /* Actualiza la foto de perfil y retorna la url */ + Future updateProfilePicture({ + required String image, + }); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/repositories/carreras_repository.dart b/lib/services_new/interfaces/repositories/carreras_repository.dart new file mode 100644 index 0000000..67e5c0b --- /dev/null +++ b/lib/services_new/interfaces/repositories/carreras_repository.dart @@ -0,0 +1,7 @@ +import 'package:mi_utem/models/carrera.dart'; + +abstract class CarrerasRepository { + + /* Obtiene las carreras */ + Future> getCarreras(); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/repositories/credentials_repository.dart b/lib/services_new/interfaces/repositories/credentials_repository.dart new file mode 100644 index 0000000..8221eab --- /dev/null +++ b/lib/services_new/interfaces/repositories/credentials_repository.dart @@ -0,0 +1,22 @@ +import 'package:mi_utem/models/user/credential.dart'; + +abstract class CredentialsRepository { + + /* + Verifica si existen credenciales guardadas + @return true si existen credenciales guardadas, false en caso contrario + */ + Future hasCredentials(); + + /* + Obtiene las credenciales guardadas + @return Credenciales guardadas + */ + Future getCredentials(); + + /* + Guarda las credenciales + @param credential Credenciales a guardar + */ + Future setCredentials(Credentials? credential); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/repositories/grades_repository.dart b/lib/services_new/interfaces/repositories/grades_repository.dart new file mode 100644 index 0000000..7dc61d4 --- /dev/null +++ b/lib/services_new/interfaces/repositories/grades_repository.dart @@ -0,0 +1,10 @@ +import 'package:mi_utem/models/evaluacion/grades.dart'; + +abstract class GradesRepository { + + /* Obtiene las notas de la carrera y asignatura especificada */ + Future getGrades({ + required String carreraId, + required String asignaturaId, + }); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/horario_service.dart b/lib/services_new/interfaces/repositories/horario_repository.dart similarity index 77% rename from lib/services_new/interfaces/horario_service.dart rename to lib/services_new/interfaces/repositories/horario_repository.dart index d8db2d2..34ceef6 100644 --- a/lib/services_new/interfaces/horario_service.dart +++ b/lib/services_new/interfaces/repositories/horario_repository.dart @@ -1,6 +1,6 @@ import 'package:mi_utem/models/horario.dart'; -abstract class HorarioService { +abstract class HorarioRepository { Future getHorario(String carreraId, {bool forceRefresh = false}); } \ No newline at end of file diff --git a/lib/services_new/interfaces/noticias_service.dart b/lib/services_new/interfaces/repositories/noticias_repository.dart similarity index 70% rename from lib/services_new/interfaces/noticias_service.dart rename to lib/services_new/interfaces/repositories/noticias_repository.dart index e6ccc78..18835c8 100644 --- a/lib/services_new/interfaces/noticias_service.dart +++ b/lib/services_new/interfaces/repositories/noticias_repository.dart @@ -1,6 +1,6 @@ import 'package:mi_utem/models/noticia.dart'; -abstract class NoticiasService { +abstract class NoticiasRepository { Future?> getNoticias(); } \ No newline at end of file diff --git a/lib/services_new/interfaces/repositories/permiso_ingreso_repository.dart b/lib/services_new/interfaces/repositories/permiso_ingreso_repository.dart new file mode 100644 index 0000000..ebb07a6 --- /dev/null +++ b/lib/services_new/interfaces/repositories/permiso_ingreso_repository.dart @@ -0,0 +1,8 @@ +import 'package:mi_utem/models/permiso_ingreso.dart'; + +abstract class PermisoIngresoRepository { + + Future getDetallesPermiso(String id); + + Future> getPermisos(); +} \ No newline at end of file diff --git a/lib/controllers/notification_controller.dart b/lib/services_new/legacy_controllers/notification_controller.dart similarity index 100% rename from lib/controllers/notification_controller.dart rename to lib/services_new/legacy_controllers/notification_controller.dart diff --git a/lib/services_new/service_manager.dart b/lib/services_new/service_manager.dart index 232eda1..6cf6f37 100644 --- a/lib/services_new/service_manager.dart +++ b/lib/services_new/service_manager.dart @@ -1,36 +1,49 @@ -import 'package:mi_utem/services_new/implementations/asignaturas_service.dart'; +import 'package:mi_utem/services_new/implementations/repositories/asignaturas_repository.dart'; import 'package:mi_utem/services_new/implementations/auth_service.dart'; import 'package:mi_utem/services_new/implementations/carreras_service.dart'; import 'package:mi_utem/services_new/implementations/controllers/calculator_controller.dart'; import 'package:mi_utem/services_new/implementations/controllers/horario_controller.dart'; -import 'package:mi_utem/services_new/implementations/credential_service.dart'; +import 'package:mi_utem/services_new/implementations/repositories/carreras_repository.dart'; +import 'package:mi_utem/services_new/implementations/repositories/credentials_repository.dart'; import 'package:mi_utem/services_new/implementations/grades_service.dart'; -import 'package:mi_utem/services_new/implementations/horario_service.dart'; -import 'package:mi_utem/services_new/implementations/noticias_service.dart'; -import 'package:mi_utem/services_new/implementations/qr_pass_service.dart'; -import 'package:mi_utem/services_new/interfaces/asignaturas_service.dart'; +import 'package:mi_utem/services_new/implementations/repositories/horario_repository.dart'; +import 'package:mi_utem/services_new/implementations/repositories/noticias_repository.dart'; +import 'package:mi_utem/services_new/implementations/repositories/auth_repository.dart'; +import 'package:mi_utem/services_new/implementations/repositories/grades_repository.dart'; +import 'package:mi_utem/services_new/implementations/repositories/permiso_ingreso_repository.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; -import 'package:mi_utem/services_new/interfaces/credential_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/carreras_repository.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; import 'package:mi_utem/services_new/interfaces/grades_service.dart'; -import 'package:mi_utem/services_new/interfaces/horario_service.dart'; -import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; -import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/horario_repository.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/noticias_repository.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/auth_repository.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/grades_repository.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/permiso_ingreso_repository.dart'; import 'package:watch_it/watch_it.dart'; Future registerServices() async { - di.registerLazySingleton(() => AsignaturasServiceImplementation()); + /* Repositorios (Para conectarse a la REST Api */ + di.registerLazySingleton(() => AuthRepositoryImplementation()); + di.registerLazySingleton(() => AsignaturasRepositoryImplementation()); + di.registerLazySingleton(() => CredentialsRepositoryImplementation()); + di.registerLazySingleton(() => CarrerasRepositoryImplementation()); + di.registerLazySingleton(() => GradesRepositoryImplementation()); + di.registerLazySingleton(() => PermisoIngresoRepositoryImplementation()); + di.registerLazySingleton(() => NoticiasRepositoryImplementation()); + di.registerLazySingleton(() => HorarioRepositoryImplementation()); + + + /* Servicios (Para procesar datos REST) */ di.registerLazySingleton(() => AuthServiceImplementation()); di.registerLazySingleton(() => CarrerasServiceImplementation()); - di.registerLazySingleton(() => CredentialsServiceImplementation()); di.registerLazySingleton(() => GradesServiceImplementation()); - di.registerLazySingleton(() => HorarioServiceImplementation()); - di.registerLazySingleton(() => NoticiasServiceImplementation()); - di.registerLazySingleton(() => QRPassServiceImplementation()); - /* Controladores */ + /* Controladores (Para procesar datos de interfaz) */ di.registerLazySingleton(() => HorarioControllerImplementation()); di.registerLazySingleton(() => CalculatorControllerImplementation()); } \ No newline at end of file diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index 8670327..0f6fef2 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -5,7 +5,7 @@ import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart' as NewAuthService; -import 'package:mi_utem/services_new/interfaces/credential_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; @@ -37,7 +37,7 @@ class LoginButton extends StatefulWidget { class _LoginButtonState extends State { final _authService = di.get(); - final _credentialsService = di.get(); + final _credentialsService = di.get(); @override Widget build(BuildContext context) => TextButton( diff --git a/lib/widgets/login_screen/login_form.dart b/lib/widgets/login_screen/login_form.dart index df2f7dd..3fb341e 100644 --- a/lib/widgets/login_screen/login_form.dart +++ b/lib/widgets/login_screen/login_form.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mi_utem/services/update_service.dart'; -import 'package:mi_utem/services_new/interfaces/credential_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; import 'package:mi_utem/widgets/login_screen/creditos_app.dart'; import 'package:mi_utem/widgets/login_screen/formulario_credenciales.dart'; import 'package:mi_utem/widgets/login_screen/login_button.dart'; @@ -25,7 +25,7 @@ class _LoginFormState extends State { final TextEditingController _correoController = TextEditingController(); final TextEditingController _contraseniaController = TextEditingController(); - final _credentialService = di.get(); + final _credentialService = di.get(); @override void initState() { diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index 8d5d14d..dea38e2 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -2,7 +2,7 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/services_new/interfaces/noticias_service.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/noticias_repository.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/noticias/noticia_card_widget.dart'; @@ -29,7 +29,7 @@ class NoticiasCarruselWidget extends StatelessWidget { ), const SizedBox(height: 10), FutureBuilder( - future: di.get().getNoticias(), + future: di.get().getNoticias(), builder: (context, AsyncSnapshot?> snapshot) { if (snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException) : CustomException.custom("No pudimos obtener las noticias."); diff --git a/lib/widgets/permiso_card.dart b/lib/widgets/permiso_card.dart index e39d323..2c14d06 100644 --- a/lib/widgets/permiso_card.dart +++ b/lib/widgets/permiso_card.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/models/permiso_covid.dart'; +import 'package:mi_utem/models/permiso_ingreso.dart'; import 'package:mi_utem/screens/permiso_covid_screen.dart'; import 'package:mi_utem/themes/theme.dart'; class PermisoCard extends StatelessWidget { const PermisoCard({Key? key, required this.permiso}) : super(key: key); - final PermisoCovid permiso; + final PermisoIngreso permiso; @override Widget build(BuildContext context) { diff --git a/lib/widgets/permisos_section.dart b/lib/widgets/permisos_section.dart index fb4b98b..bc450ff 100644 --- a/lib/widgets/permisos_section.dart +++ b/lib/widgets/permisos_section.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/models/permiso_covid.dart'; -import 'package:mi_utem/services_new/interfaces/qr_pass_service.dart'; +import 'package:mi_utem/models/permiso_ingreso.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/permiso_card.dart'; import 'package:watch_it/watch_it.dart'; @@ -31,8 +31,8 @@ class PermisosCovidSection extends StatelessWidget { SizedBox(height: 10), SizedBox( height: 155, - child: FutureBuilder?>( - future: di.get().getPermisos(), + child: FutureBuilder?>( + future: di.get().getPermisos(), builder: (context, snapshot) { if(snapshot.connectionState == ConnectionState.waiting) { return LoadingIndicator( From 69e2f4df4eb4db1add7f784fe5b1d8c81bfca198 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 25 Mar 2024 09:55:17 -0300 Subject: [PATCH 037/194] =?UTF-8?q?patch:=20arreglado=20error=20al=20inici?= =?UTF-8?q?ar=20sesi=C3=B3n=20y=20actualizado=20Podfile.lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Podfile.lock | 2 +- lib/services_new/implementations/auth_service.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b97f735..a89a8b4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1081,4 +1081,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 816280d49dfc997733647f095bf1e4c52c291937 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.2 diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services_new/implementations/auth_service.dart index 9a97215..b23dc6e 100644 --- a/lib/services_new/implementations/auth_service.dart +++ b/lib/services_new/implementations/auth_service.dart @@ -9,8 +9,8 @@ import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; import 'package:mi_utem/services_new/interfaces/repositories/auth_repository.dart'; +import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; import 'package:watch_it/watch_it.dart'; class AuthServiceImplementation implements AuthService { @@ -50,7 +50,7 @@ class AuthServiceImplementation implements AuthService { } try { - final token = _authRepository.refresh(token: userToken, credentials: credentials); + final token = await _authRepository.refresh(token: userToken, credentials: credentials); final userJson = user.toJson(); userJson["token"] = token; From be2e665a55eda8cf3d8fe38725c645a26e92315d Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 25 Mar 2024 17:13:36 -0300 Subject: [PATCH 038/194] patch: arreglados algunos archivos Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- Gemfile | 1 + Gemfile.lock | 195 +++++++++--------- fastlane/.env.example | 27 ++- fastlane/Fastfile | 57 +++-- ios/Podfile.lock | 67 +++--- ios/Runner.xcodeproj/project.pbxproj | 12 +- ios/fastlane/Fastfile | 1 + .../controllers/horario_controller.dart | 2 +- 8 files changed, 205 insertions(+), 157 deletions(-) diff --git a/Gemfile b/Gemfile index e4b5be5..4812693 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ source "https://rubygems.org" gem 'fastlane' gem 'cocoapods' +gem 'abbrev' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 9b45670..b00e642 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,48 +1,57 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.5) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (6.1.6) + abbrev (0.1.2) + activesupport (7.1.3.2) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.570.0) - aws-sdk-core (3.130.0) - aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.525.0) - aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.55.0) - aws-sdk-core (~> 3, >= 3.127.0) + aws-eventstream (1.3.0) + aws-partitions (1.899.0) + aws-sdk-core (3.191.4) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.78.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.113.0) - aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-s3 (1.146.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) + bigdecimal (3.1.7) claide (1.1.0) - cocoapods (1.11.3) + cocoapods (1.15.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.3) + cocoapods-core (= 1.15.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) @@ -50,10 +59,10 @@ GEM gh_inspector (~> 1.0) molinillo (~> 0.8.0) nap (~> 1.0) - ruby-macho (>= 1.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.3) - activesupport (>= 5.0, < 7) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.15.2) + activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) concurrent-ruby (~> 1.1) @@ -63,7 +72,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -75,19 +84,20 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) declarative (0.0.20) - digest-crc (0.6.4) + digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.6) + domain_name (0.6.20240107) + dotenv (2.8.1) + drb (2.2.1) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.15.0) + ethon (0.16.0) ffi (>= 1.15.0) - excon (0.92.1) - faraday (1.10.0) + excon (0.110.0) + faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -106,8 +116,8 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.3) - multipart-post (>= 1.2, < 3) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -115,8 +125,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.6) - fastlane (2.205.1) + fastimage (2.3.0) + fastlane (2.219.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -135,20 +145,22 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) + multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) @@ -156,13 +168,13 @@ GEM xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) fastlane-plugin-changelog (0.16.0) - ffi (1.15.5) + ffi (1.16.3) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.16.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.2) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -170,98 +182,94 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick - google-apis-iamcredentials_v1 (0.10.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.7.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.11.0) - google-apis-core (>= 0.4, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.2.0) - google-cloud-storage (1.36.1) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.2) + googleauth (1.8.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.10.0) + i18n (1.14.4) concurrent-ruby (~> 1.0) - jmespath (1.6.1) - json (2.6.1) - jwt (2.3.0) - memoist (0.16.2) - mini_magick (4.11.0) - mini_mime (1.1.2) - minitest (5.15.0) + jmespath (1.6.2) + json (2.7.1) + jwt (2.8.1) + base64 + mini_magick (4.12.0) + mini_mime (1.1.5) + minitest (5.22.3) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.0.0) + multipart-post (2.4.0) + mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.4.0) os (1.1.4) - plist (3.6.0) - public_suffix (4.0.6) - rake (13.0.6) - representable (3.1.1) + plist (3.7.1) + public_suffix (4.0.7) + rake (13.1.0) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.6) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.16.1) + signet (0.19.0) addressable (~> 2.8) - faraday (>= 0.17.5, < 3.0) + faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.8) + simctl (1.6.10) CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) - tzinfo (2.0.4) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.1) - unicode-display_width (1.8.0) - webrick (1.7.0) + unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.21.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -272,17 +280,18 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.5.4) PLATFORMS + arm64-darwin-22 x86_64-darwin-19 x86_64-darwin-20 x86_64-darwin-21 DEPENDENCIES + abbrev cocoapods fastlane fastlane-plugin-changelog BUNDLED WITH - 2.3.7 + 2.5.7 diff --git a/fastlane/.env.example b/fastlane/.env.example index f06b1b3..96dddc9 100644 --- a/fastlane/.env.example +++ b/fastlane/.env.example @@ -1,15 +1,20 @@ -SLACK_URL=https://hooks.slack.com/services/ASDFG... +APP_IDENTIFIER_IOS=cl.utem.miutem +APP_IDENTIFIER_ANDROID=cl.inndev.miutem + +SLACK_URL=https://hooks.slack.com/services/... APPLE_ID=exdev@utem.cl + FASTLANE_USER=exdev@utem.cl -FASTLANE_PASSWORD=apple_id_password +FASTLANE_PASSWORD= FASTLANE_ITC_TEAM_ID=122942314 FASTLANE_TEAM_ID=DS265QHA5J -FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=app_specific_password -MATCH_PASSWORD=match_password -GITHUB_TOKEN=github_token -APP_IDENTIFIER_IOS=com.app.identifier_ios -APP_IDENTIFIER_ANDROID=com.app.identifier_android -MATCH_REPO_GIT_URL=git@github.com:certificates-repo - -KEYCHAIN_PASSWORD=tempKeychainPassword -KEYCHAIN_NAME=tempKeychainName \ No newline at end of file +FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD= + +GITHUB_TOKEN= + +MATCH_REPO_GIT_URL=git@github.com:example/examplerepo.git +MATCH_GIT_BASIC_AUTHORIZATION= +MATCH_KEYCHAIN_PASSWORD= + +KEYCHAIN_NAME=login.keychain +KEYCHAIN_PASSWORD= diff --git a/fastlane/Fastfile b/fastlane/Fastfile index aab4522..e9c5e71 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -42,34 +42,57 @@ platform :mobile do changelog end - lane :test do - puts("Testing Fastlane for mobile") + lane :test do |options| + skip_slack = false + skip_android_test = false + skip_ios_test = false + + if options[:skip_slack] + skip_slack = options[:skip_slack] + end + + if options[:skip_android_test] + skip_android_test = options[:skip_android_test] + end + + if options[:skip_ios_test] + skip_ios_test = options[:skip_ios_test] + end + + puts "Testing Fastlane for mobile" changelog = read_changelog emojified_changelog = emojify_changelog - + changelog = translate_changelog(changelog: changelog) emojified_changelog = translate_changelog(changelog: emojified_changelog) puts "Changelog: #{changelog}" puts "Emojified Changelog: #{emojified_changelog}" - android_test - ios_test + unless skip_android_test + android_test + end - slack( - message: "Este es un mensaje de prueba desde Fastlane", - channel: "proy-mi-utem-bots", - default_payloads: [], - attachment_properties: { - fields: [ - { - title: "Campo", - value: "Prueba" + unless skip_ios_test + ios_test + end + + unless skip_slack + slack( + message: "Este es un mensaje de prueba desde Fastlane", + channel: "proy-mi-utem-bots", + default_payloads: [], + attachment_properties: { + fields: [ + { + title: "Campo", + value: "Prueba" + }, + ] }, - ] - }, - ) + ) + end end lane :upload do |options| diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a89a8b4..bc93c24 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -662,7 +662,7 @@ PODS: - Firebase/RemoteConfig (= 10.7.0) - firebase_core - Flutter - - FirebaseABTesting (10.16.0): + - FirebaseABTesting (10.22.0): - FirebaseCore (~> 10.0) - FirebaseAnalytics (10.7.0): - FirebaseAnalytics/AdIdSupport (= 10.7.0) @@ -686,7 +686,7 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreExtension (10.17.0): + - FirebaseCoreExtension (10.22.0): - FirebaseCore (~> 10.0) - FirebaseCoreInternal (10.16.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" @@ -717,7 +717,7 @@ PODS: - FirebaseInstallations (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseInstallations (10.16.0): + - FirebaseInstallations (10.22.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -737,13 +737,13 @@ PODS: - FirebaseInstallations (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseSessions (10.17.0): + - FirebaseSessions (10.22.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleDataTransport (~> 9.2) - GoogleUtilities/Environment (~> 7.10) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesSwift (~> 2.1) - Flutter (1.0.0) - flutter_keyboard_visibility (0.0.1): @@ -776,29 +776,38 @@ PODS: - GoogleUtilities/Network (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleDataTransport (9.2.5): + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.11.5): + - GoogleUtilities/AppDelegateSwizzler (7.13.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Logger (7.13.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (7.13.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.5)" - - GoogleUtilities/Reachability (7.11.5): + - "GoogleUtilities/NSData+zlib (7.13.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.0) + - GoogleUtilities/Reachability (7.13.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.0): - GoogleUtilities/Logger + - GoogleUtilities/Privacy - "gRPC-C++ (1.44.0)": - "gRPC-C++/Implementation (= 1.44.0)" - "gRPC-C++/Interface (= 1.44.0)" @@ -861,7 +870,7 @@ PODS: - FirebaseCore - FirebaseMessaging - IosAwnCore (~> 0.8.0) - - leveldb-library (1.22.2) + - leveldb-library (1.22.4) - Libuv-gRPC (0.0.10): - Libuv-gRPC/Implementation (= 0.0.10) - Libuv-gRPC/Interface (= 0.0.10) @@ -880,9 +889,9 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - PromisesObjC (2.3.1) - - PromisesSwift (2.3.1): - - PromisesObjC (= 2.3.1) + - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) - Sentry/HybridSDK (8.20.0): - SentryPrivate (= 8.20.0) - sentry_flutter (0.0.1): @@ -1034,26 +1043,26 @@ SPEC CHECKSUMS: firebase_crashlytics: 1b3768f2e118b0281ad30f2931369e0d1921789c firebase_in_app_messaging: 2b36a1746f4fefbd6f578a2f6659358a0d3f2c94 firebase_remote_config: e5f1ed5b29191424280b7b249228f0ed64bc90ee - FirebaseABTesting: 03f0a8b88cf618350527f2c6a2234e29b9c65064 + FirebaseABTesting: 66d2594b36d4ff6e7d3c8719802100990de05857 FirebaseAnalytics: f8133442ee6f8512e28ff19e62ce15398bfaeace FirebaseCore: e317665b9d744727a97e623edbbed009320afdd7 - FirebaseCoreExtension: 47720bb330d7041047c0935a34a3a4b92f818074 + FirebaseCoreExtension: 6394c00b887d0bebadbc7049c464aa0cbddc5d41 FirebaseCoreInternal: 26233f705cc4531236818a07ac84d20c333e505a FirebaseCrashlytics: 35fdd1a433b31e28adcf5c8933f4c526691a1e0b FirebaseFirestore: 3963a6edd1c84b4748dab3e2c62624a29d03eca1 FirebaseInAppMessaging: d04732fe9c37c3d026d66435abba60120087a7f5 - FirebaseInstallations: b822f91a61f7d1ba763e5ccc9d4f2e6f2ed3b3ee + FirebaseInstallations: 763814908793c0da14c18b3dcffdec71e29ed55e FirebaseMessaging: ac9062bcc35ed56e15a0241d8fd317022499baf8 FirebaseRemoteConfig: d5de62211e2eaa2152d8ee85a23c301b70887a74 - FirebaseSessions: 49f39e5c10e3f9fdd38d01b748329bae2a2fa8ed + FirebaseSessions: cd97fb07674f3906619c871eefbd260a1546c9d3 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_uxcam: 49ca783e481233911475724eb1366064183fffc1 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleAppMeasurement: fe17c92a32207dd5cdd4e8d742767f2da74857f6 - GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 "gRPC-C++": 9675f953ace2b3de7c506039d77be1f2e77a8db2 gRPC-Core: 943e491cb0d45598b0b0eb9e910c88080369290b image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 @@ -1061,14 +1070,14 @@ SPEC CHECKSUMS: in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d IosAwnCore: ed1b2b6d84962a758354dbacd9ce525c72ce28a9 IosAwnFcmDist: 0ac4da8f9ef8f2db9e5ec5d0c10c8db793ce447d - leveldb-library: f03246171cce0484482ec291f88b6d563699ee06 + leveldb-library: 06a69cc7582d64b29424a63e085e683cc188230a Libuv-gRPC: 55e51798e14ef436ad9bc45d12d43b77b49df378 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 Sentry: a8d7b373b9f9868442b02a0c425192f693103cbf sentry_flutter: 03e7660857a8cdb236e71456a7e8447b65c8a788 SentryPrivate: 006b24af16828441f70e2ab6adf241bd0a8ad130 @@ -1081,4 +1090,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 816280d49dfc997733647f095bf1e4c52c291937 -COCOAPODS: 1.14.2 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 342376c..792c73f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -848,7 +848,7 @@ INFOPLIST_FILE = MiUtemNotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MiUtemNotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -980,7 +980,7 @@ INFOPLIST_FILE = MiUtemNotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MiUtemNotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1104,7 +1104,7 @@ INFOPLIST_FILE = MiUtemNotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MiUtemNotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1142,7 +1142,7 @@ INFOPLIST_FILE = MiUtemNotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MiUtemNotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1185,7 +1185,7 @@ INFOPLIST_FILE = MiUtemNotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MiUtemNotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1225,7 +1225,7 @@ INFOPLIST_FILE = MiUtemNotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MiUtemNotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index add9d1b..bf9b28f 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -62,6 +62,7 @@ lane :ios_build_and_upload do |options| app_identifier: [ENV["APP_IDENTIFIER_IOS"], ENV["APP_IDENTIFIER_IOS"] + ".MiUtemNotificationServiceExtension"], type: "appstore", git_url: ENV["MATCH_REPO_GIT_URL"], + git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"], readonly: is_ci, keychain_name: ENV["KEYCHAIN_NAME"], keychain_password: ENV["KEYCHAIN_PASSWORD"], diff --git a/lib/services_new/implementations/controllers/horario_controller.dart b/lib/services_new/implementations/controllers/horario_controller.dart index 4ba5424..4a13a33 100644 --- a/lib/services_new/implementations/controllers/horario_controller.dart +++ b/lib/services_new/implementations/controllers/horario_controller.dart @@ -126,7 +126,7 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC _setRandomColorsByHorario(); loadingHorario.value = false; - _storage.write("last_horario_update", DateTime.now().toIso8601String()); + _storage.write("last_update_horario", DateTime.now().toIso8601String()); } @override From d0d236c42f871da7a77902c40bb42923780f0db0 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 26 Mar 2024 07:38:16 -0300 Subject: [PATCH 039/194] patch: eliminado flutter_keyboard_visibility ya que no se utiliza. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.lock | 48 ------------------------------------------------ pubspec.yaml | 1 - 2 files changed, 49 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 1dc5ce6..c4b8eb7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -574,54 +574,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" - flutter_keyboard_visibility: - dependency: "direct main" - description: - name: flutter_keyboard_visibility - sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" - url: "https://pub.dev" - source: hosted - version: "5.4.1" - flutter_keyboard_visibility_linux: - dependency: transitive - description: - name: flutter_keyboard_visibility_linux - sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_macos: - dependency: transitive - description: - name: flutter_keyboard_visibility_macos - sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_web: - dependency: transitive - description: - name: flutter_keyboard_visibility_web - sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_windows: - dependency: transitive - description: - name: flutter_keyboard_visibility_windows - sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 - url: "https://pub.dev" - source: hosted - version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index abc3c6c..6eca4ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,6 @@ dependencies: flutter: sdk: flutter flutter_dotenv: ^5.0.2 - flutter_keyboard_visibility: ^5.0.3 flutter_markdown: ^0.6.1 flutter_masked_text: git: From 9f0002636e23c27a4f289887180e22696415d7d8 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 26 Mar 2024 10:39:23 -0300 Subject: [PATCH 040/194] =?UTF-8?q?patch:=20mejorada=20vista=20de=20salas?= =?UTF-8?q?=20en=20resumen=20de=20asignatura=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Podfile.lock | 6 ------ lib/screens/asignatura/asignatura_resumen_tab.dart | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bc93c24..5f0d223 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -746,8 +746,6 @@ PODS: - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesSwift (~> 2.1) - Flutter (1.0.0) - - flutter_keyboard_visibility (0.0.1): - - Flutter - flutter_secure_storage (6.0.0): - Flutter - flutter_uxcam (2.4.5): @@ -925,7 +923,6 @@ DEPENDENCIES: - firebase_in_app_messaging (from `.symlinks/plugins/firebase_in_app_messaging/ios`) - firebase_remote_config (from `.symlinks/plugins/firebase_remote_config/ios`) - Flutter (from `Flutter`) - - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_uxcam (from `.symlinks/plugins/flutter_uxcam/ios`) - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) @@ -998,8 +995,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_remote_config/ios" Flutter: :path: Flutter - flutter_keyboard_visibility: - :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_uxcam: @@ -1056,7 +1051,6 @@ SPEC CHECKSUMS: FirebaseRemoteConfig: d5de62211e2eaa2152d8ee85a23c301b70887a74 FirebaseSessions: cd97fb07674f3906619c871eefbd260a1546c9d3 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_uxcam: 49ca783e481233911475724eb1366064183fffc1 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/asignatura_resumen_tab.dart index 94e3888..6e560d0 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/asignatura_resumen_tab.dart @@ -89,7 +89,7 @@ class AsignaturaResumenTab extends StatelessWidget { Divider(height: 5, indent: 20, endIndent: 20), FieldListTile( title: "Sala", - value: asignatura.sala, + value: asignatura.sala?.split(",").map((it) => "- ${it.trim().replaceAll("-", " - ")}").join("\n"), ), Divider(height: 5), From 5ab415ca0261fb31bacb39f8b1baca25a79eb46b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:31:53 -0300 Subject: [PATCH 041/194] =?UTF-8?q?patch:=20regreso=20de=20getx=20y=20algu?= =?UTF-8?q?nos=20parches=20*=20Ha=20vuelto=20getx!=20*=20Se=20parcharon=20?= =?UTF-8?q?algunos=20archivos=20(reducciones=20a=20una=20l=C3=ADnea)=20*?= =?UTF-8?q?=20Se=20agrega=20a=20README.md=20la=20organizaci=C3=B3n=20de=20?= =?UTF-8?q?carpetas=20*=20Se=20convierten=20algunas=20funciones=20en=20fun?= =?UTF-8?q?ciones=20de=20flecha=20(arrow=20functions)=20*=20Se=20han=20eli?= =?UTF-8?q?minado=20referencias=20a=20watch=5Fit,=20get=5Fit=20y=20listena?= =?UTF-8?q?ble=5Fcollections=20*=20Se=20ha=20cambiado=20a=20getx=20todas?= =?UTF-8?q?=20las=20referencias=20de=20watch=5Fit,=20get=5Fit=20y=20listen?= =?UTF-8?q?able=5Fcollections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +++++- lib/config/http_clients.dart | 8 +-- .../calculator_controller.dart | 47 ++++++------ .../implementations}/horario_controller.dart | 28 ++++---- .../interfaces}/calculator_controller.dart | 45 ++++++------ .../interfaces}/horario_controller.dart | 13 ++-- lib/main.dart | 2 +- lib/models/carrera.dart | 32 +++------ lib/models/noticia.dart | 31 ++++---- lib/models/pair.dart | 9 ++- lib/models/permiso_ingreso.dart | 4 +- lib/models/user/rut.dart | 2 - .../asignaturas_repository.dart | 2 +- .../implementations}/auth_repository.dart | 2 +- .../implementations}/carreras_repository.dart | 2 +- .../credentials_repository.dart | 2 +- .../implementations}/grades_repository.dart | 2 +- .../implementations}/horario_repository.dart | 2 +- .../implementations}/noticias_repository.dart | 4 +- .../permiso_ingreso_repository.dart | 2 +- .../interfaces}/asignaturas_repository.dart | 0 .../interfaces}/auth_repository.dart | 0 .../interfaces}/carreras_repository.dart | 0 .../interfaces}/credentials_repository.dart | 0 .../interfaces}/grades_repository.dart | 0 .../interfaces}/horario_repository.dart | 0 .../interfaces}/noticias_repository.dart | 0 .../permiso_ingreso_repository.dart | 0 .../asignatura/asignatura_detalle_screen.dart | 6 +- .../asignatura_estudiantes_tab.dart | 42 +++++------ .../asignatura/asignatura_notas_tab.dart | 6 +- .../asignatura/asignaturas_lista_screen.dart | 17 ++--- lib/screens/calculadora_notas_screen.dart | 23 +++--- lib/screens/credencial_screen.dart | 61 ++++++++-------- lib/screens/horario/horario_screen.dart | 48 ++++++------- .../horario/widgets/horario_days_header.dart | 66 ++++++++--------- .../horario/widgets/horario_indicator.dart | 22 +++--- .../widgets/horario_main_scroller.dart | 10 +-- .../widgets/horario_periods_header.dart | 72 ++++++++----------- lib/screens/main_screen.dart | 10 +-- lib/screens/permiso_covid_screen.dart | 6 +- lib/screens/splash_screen.dart | 6 +- lib/screens/usuario_screen.dart | 6 +- lib/services/background_service.dart | 6 +- .../implementations/auth_service.dart | 12 ++-- .../implementations/carreras_service.dart | 16 ++--- .../implementations/grades_service.dart | 24 ++++--- .../interfaces/auth_service.dart | 0 lib/services/interfaces/carreras_service.dart | 14 ++++ .../interfaces/grades_service.dart | 0 .../notification_controller.dart | 0 lib/services/notification_service.dart | 2 +- lib/services/service_manager.dart | 49 +++++++++++++ .../interfaces/carreras_service.dart | 16 ----- lib/services_new/service_manager.dart | 49 ------------- lib/utils/dio_wordpress_client.dart | 13 ---- lib/widgets/acerca/acerca_screen.dart | 54 +++++++------- .../asignatura/lista/lista_asignaturas.dart | 4 +- .../lista/sin_asignaturas_mensaje.dart | 4 +- .../notas_tab/labeled_nota_display.dart | 34 +++++---- .../asignatura/notas_tab/notas_display.dart | 3 +- .../editar_notas_widget.dart | 6 +- .../nota_examen_display_widget.dart | 24 +++---- .../nota_final_display_widget.dart | 12 ++-- .../nota_presentacion_display_widget.dart | 14 ++-- .../notas_calculadora_widget.dart | 8 +-- lib/widgets/custom_drawer.dart | 10 ++- lib/widgets/horario/bloque_ramo_card.dart | 9 ++- lib/widgets/login_screen/login_button.dart | 10 +-- lib/widgets/login_screen/login_form.dart | 6 +- lib/widgets/nota_list_item.dart | 41 +++++------ .../noticias/noticias_carrusel_widget.dart | 6 +- lib/widgets/permisos_section.dart | 6 +- pubspec.lock | 36 +--------- pubspec.yaml | 4 +- 75 files changed, 533 insertions(+), 622 deletions(-) rename lib/{services_new/implementations/controllers => controllers/implementations}/calculator_controller.dart (84%) rename lib/{services_new/implementations/controllers => controllers/implementations}/horario_controller.dart (91%) rename lib/{services_new/interfaces/controllers => controllers/interfaces}/calculator_controller.dart (50%) rename lib/{services_new/interfaces/controllers => controllers/interfaces}/horario_controller.dart (81%) rename lib/{services_new/implementations/repositories => repositories/implementations}/asignaturas_repository.dart (94%) rename lib/{services_new/implementations/repositories => repositories/implementations}/auth_repository.dart (96%) rename lib/{services_new/implementations/repositories => repositories/implementations}/carreras_repository.dart (89%) rename lib/{services_new/implementations/repositories => repositories/implementations}/credentials_repository.dart (88%) rename lib/{services_new/implementations/repositories => repositories/implementations}/grades_repository.dart (91%) rename lib/{services_new/implementations/repositories => repositories/implementations}/horario_repository.dart (90%) rename lib/{services_new/implementations/repositories => repositories/implementations}/noticias_repository.dart (84%) rename lib/{services_new/implementations/repositories => repositories/implementations}/permiso_ingreso_repository.dart (93%) rename lib/{services_new/interfaces/repositories => repositories/interfaces}/asignaturas_repository.dart (100%) rename lib/{services_new/interfaces/repositories => repositories/interfaces}/auth_repository.dart (100%) rename lib/{services_new/interfaces/repositories => repositories/interfaces}/carreras_repository.dart (100%) rename lib/{services_new/interfaces/repositories => repositories/interfaces}/credentials_repository.dart (100%) rename lib/{services_new/interfaces/repositories => repositories/interfaces}/grades_repository.dart (100%) rename lib/{services_new/interfaces/repositories => repositories/interfaces}/horario_repository.dart (100%) rename lib/{services_new/interfaces/repositories => repositories/interfaces}/noticias_repository.dart (100%) rename lib/{services_new/interfaces/repositories => repositories/interfaces}/permiso_ingreso_repository.dart (100%) rename lib/{services_new => services}/implementations/auth_service.dart (93%) rename lib/{services_new => services}/implementations/carreras_service.dart (73%) rename lib/{services_new => services}/implementations/grades_service.dart (89%) rename lib/{services_new => services}/interfaces/auth_service.dart (100%) create mode 100644 lib/services/interfaces/carreras_service.dart rename lib/{services_new => services}/interfaces/grades_service.dart (100%) rename lib/{services_new => services}/legacy_controllers/notification_controller.dart (100%) create mode 100644 lib/services/service_manager.dart delete mode 100644 lib/services_new/interfaces/carreras_service.dart delete mode 100644 lib/services_new/service_manager.dart delete mode 100644 lib/utils/dio_wordpress_client.dart diff --git a/README.md b/README.md index 71eeefb..5f755f2 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,30 @@ Aplicación multiplataforma hecha por estudiantes de la [Universidad Tecnológic ## Requisitos técnicos - Flutter 3.13.6 +## Organización de carpetas +``` +|-- lib +| |-- config (Configuración de la aplicación) +| |-- controllers (Controladores de la aplicación, para procesar datos de una vista especifica) +| | |-- implementations (Implementaciones de controladores) +| | |-- interfaces (Interfaces de controladores) +| |-- models (Modelos de datos) +| |-- repositories (Repositorios de datos, para obtener datos desde la API) +| | |-- implementations (Implementaciones de repositorios) +| | |-- interfaces (Interfaces de repositorios) +| |-- screens (Pantallas de la aplicación) +| |-- services (Servicios de la aplicación, maneja y procesa los datos de repositorios) +| | |-- implementations (Implementaciones de servicios) +| | |-- interfaces (Interfaces de servicios) +| |-- themes (Temas de la aplicación) +| |-- utils (Utilidades de la aplicación) +| |-- widgets (Widgets de la aplicación) +|-- main.dart (Punto de entrada de la aplicación) +``` + ## Créditos Este proyecto fue creado por el Club de Desarrollo Experimental (ExDev) de la Universidad Tecnológica Metropolitana y es mantenido por los propios estudiantes con el apoyo del equipo de SISEI. Mira los perfiles que han contribuido a este proyecto: - + Contribuidores \ No newline at end of file diff --git a/lib/config/http_clients.dart b/lib/config/http_clients.dart index 51a0369..edfb362 100644 --- a/lib/config/http_clients.dart +++ b/lib/config/http_clients.dart @@ -1,8 +1,8 @@ +import 'package:get/get.dart'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:watch_it/watch_it.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; final httpClient = InterceptedClient.build( interceptors: [ @@ -30,7 +30,7 @@ class AuthInterceptor implements InterceptorContract { } if (!data.headers.containsKey('authorization') && !data.url.contains("/v1/auth/login") && !data.url.contains("/v1/auth/refresh")) { // No enviar token si es login o refresh - final user = await di.get().getUser(); + final user = await Get.find().getUser(); final token = user?.token; if (token != null) { logger.d("[AuthInterceptor]: Agregado token de autorización al request"); @@ -57,7 +57,7 @@ class ExpiredTokenRetryPolicy extends RetryPolicy { } logger.d("[ExpiredTokenRetryPolicy]: ${response.request?.method.name.toUpperCase()} ${response.request?.url} Recibió un 401, refrescando token..."); - final _authService = di.get(); + final _authService = Get.find(); final currentToken = (await _authService.getUser())?.token; if(currentToken == null) { await _authService.login(); diff --git a/lib/services_new/implementations/controllers/calculator_controller.dart b/lib/controllers/implementations/calculator_controller.dart similarity index 84% rename from lib/services_new/implementations/controllers/calculator_controller.dart rename to lib/controllers/implementations/calculator_controller.dart index dca0265..c63cca5 100644 --- a/lib/services_new/implementations/controllers/calculator_controller.dart +++ b/lib/controllers/implementations/calculator_controller.dart @@ -1,9 +1,8 @@ import 'package:extended_masked_text/extended_masked_text.dart'; -import 'package:flutter/material.dart'; -import 'package:listenable_collections/listenable_collections.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; class CalculatorControllerImplementation implements CalculatorController { @@ -27,71 +26,71 @@ class CalculatorControllerImplementation implements CalculatorController { /* Notas parciales */ @override - ListNotifier partialGrades = ListNotifier(); + RxList partialGrades = [].obs; /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ @override - ListNotifier percentageTextFieldControllers = ListNotifier(); + RxList percentageTextFieldControllers = [].obs; /* Controlador de texto para las notas con máscara (para autocompletar formato) */ @override - ListNotifier gradeTextFieldControllers = ListNotifier(); + RxList gradeTextFieldControllers = [].obs; /* Nota del examen */ @override - ValueNotifier examGrade = ValueNotifier(null); + Rx examGrade = null.obs; /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ @override - ValueNotifier examGradeTextFieldController = ValueNotifier(MaskedTextController(mask: "0.0")); + Rx examGradeTextFieldController = MaskedTextController(mask: "0.0").obs; @override - ValueNotifier freeEditable = ValueNotifier(false); + RxBool freeEditable = false.obs; @override - ValueNotifier calculatedFinalGrade = ValueNotifier(null); + Rx calculatedFinalGrade = null.obs; @override - ValueNotifier calculatedPresentationGrade = ValueNotifier(null); + Rx calculatedPresentationGrade = null.obs; @override - ValueNotifier amountOfPartialGradesWithoutGrade = ValueNotifier(0); + Rx amountOfPartialGradesWithoutGrade = 0.obs; @override - ValueNotifier amountOfPartialGradesWithoutPercentage = ValueNotifier(0); + Rx amountOfPartialGradesWithoutPercentage = 0.obs; @override - ValueNotifier hasMissingPartialGrade = ValueNotifier(false); + RxBool hasMissingPartialGrade = false.obs; @override - ValueNotifier canTakeExam = ValueNotifier(false); + RxBool canTakeExam = false.obs; @override - ValueNotifier minimumRequiredExamGrade = ValueNotifier(null); + Rx minimumRequiredExamGrade = null.obs; @override - ValueNotifier percentageOfPartialGrades = ValueNotifier(0); + RxDouble percentageOfPartialGrades = 0.0.obs; @override - ValueNotifier missingPercentage = ValueNotifier(0); + RxDouble missingPercentage = 0.0.obs; @override - ValueNotifier hasMissingPercentage = ValueNotifier(false); + RxBool hasMissingPercentage = false.obs; @override - ValueNotifier suggestedPercentage = ValueNotifier(null); + Rx suggestedPercentage = null.obs; @override - ValueNotifier suggestedPresentationGrade = ValueNotifier(null); + Rx suggestedPresentationGrade = null.obs; @override - ValueNotifier percentageWithoutGrade = ValueNotifier(0); + RxDouble percentageWithoutGrade = 0.0.obs; @override - ValueNotifier hasCorrectPercentage = ValueNotifier(false); + RxBool hasCorrectPercentage = false.obs; @override - ValueNotifier suggestedGrade = ValueNotifier(null); + Rx suggestedGrade = null.obs; @override void updateWithGrades(Grades grades) { diff --git a/lib/services_new/implementations/controllers/horario_controller.dart b/lib/controllers/implementations/horario_controller.dart similarity index 91% rename from lib/services_new/implementations/controllers/horario_controller.dart rename to lib/controllers/implementations/horario_controller.dart index 4ba5424..fd0232e 100644 --- a/lib/services_new/implementations/controllers/horario_controller.dart +++ b/lib/controllers/implementations/horario_controller.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/horario.dart'; +import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/horario_repository.dart'; import 'package:vector_math/vector_math_64.dart' as vector; -import 'package:watch_it/watch_it.dart'; -class HorarioControllerImplementation extends ChangeNotifier implements HorarioController { +class HorarioControllerImplementation implements HorarioController { final _storage = GetStorage(); final _randomColors = Colors.primaries.toList()..shuffle(); final _now = DateTime.now(); @@ -32,22 +32,22 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC Duration periodGap = Duration(minutes: 5); @override - ValueNotifier horario = ValueNotifier(Horario()); + Rx horario = Rx(Horario()); @override - ValueNotifier loadingHorario = ValueNotifier(false); + RxBool loadingHorario = false.obs; @override List usedColors = []; @override - ValueNotifier zoom = ValueNotifier(0.5); + RxDouble zoom = 0.5.obs; @override - ValueNotifier indicatorIsOpen = ValueNotifier(false); + RxBool indicatorIsOpen = false.obs; @override - ValueNotifier isCenteredInCurrentPeriodAndDay = ValueNotifier(false); + RxBool isCenteredInCurrentPeriodAndDay = false.obs; @override TransformationController blockContentController = TransformationController(); @@ -110,19 +110,19 @@ class HorarioControllerImplementation extends ChangeNotifier implements HorarioC horario.value = null; - final carrerasService = di.get(); - Carrera? carrera = carrerasService.selectedCarrera.value; + final carrerasService = Get.find(); + Carrera? carrera = carrerasService.selectedCarrera; if (carrera == null) { await carrerasService.getCarreras(); } - carrera = carrerasService.selectedCarrera.value; + carrera = carrerasService.selectedCarrera; final carreraId = carrera?.id; if(carreraId == null) { loadingHorario.value = false; return; } - horario.value = await di.get().getHorario(carreraId); + horario.value = await Get.find().getHorario(carreraId); _setRandomColorsByHorario(); loadingHorario.value = false; diff --git a/lib/services_new/interfaces/controllers/calculator_controller.dart b/lib/controllers/interfaces/calculator_controller.dart similarity index 50% rename from lib/services_new/interfaces/controllers/calculator_controller.dart rename to lib/controllers/interfaces/calculator_controller.dart index 90b178b..2969817 100644 --- a/lib/services_new/interfaces/controllers/calculator_controller.dart +++ b/lib/controllers/interfaces/calculator_controller.dart @@ -1,72 +1,71 @@ import 'package:extended_masked_text/extended_masked_text.dart'; -import 'package:flutter/material.dart'; -import 'package:listenable_collections/listenable_collections.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; abstract class CalculatorController { /* Notas parciales */ - abstract ListNotifier partialGrades; + abstract RxList partialGrades; /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ - abstract ListNotifier percentageTextFieldControllers; + abstract RxList percentageTextFieldControllers; /* Controlador de texto para las notas con máscara (para autocompletar formato) */ - abstract ListNotifier gradeTextFieldControllers; + abstract RxList gradeTextFieldControllers; /* Nota del examen */ - abstract ValueNotifier examGrade; + abstract Rx examGrade; /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ - abstract ValueNotifier examGradeTextFieldController; + abstract Rx examGradeTextFieldController; - abstract ValueNotifier freeEditable; + abstract RxBool freeEditable; /* Nota final calculada */ - abstract ValueNotifier calculatedFinalGrade; + abstract Rx calculatedFinalGrade; /* Nota de presentación calculada */ - abstract ValueNotifier calculatedPresentationGrade; + abstract Rx calculatedPresentationGrade; /* Cantidad de notas parciales sin nota */ - abstract ValueNotifier amountOfPartialGradesWithoutGrade; + abstract Rx amountOfPartialGradesWithoutGrade; /* Cantidad de notas parciales sin porcentaje */ - abstract ValueNotifier amountOfPartialGradesWithoutPercentage; + abstract Rx amountOfPartialGradesWithoutPercentage; /* Si hay notas parciales sin nota */ - abstract ValueNotifier hasMissingPartialGrade; + abstract RxBool hasMissingPartialGrade; /* Si puede tomar examen */ - abstract ValueNotifier canTakeExam; + abstract RxBool canTakeExam; /* Nota mínima requerida para el examen */ - abstract ValueNotifier minimumRequiredExamGrade; + abstract Rx minimumRequiredExamGrade; /* Porcentaje de las notas parciales */ - abstract ValueNotifier percentageOfPartialGrades; + abstract RxDouble percentageOfPartialGrades; /* Porcentaje faltante */ - abstract ValueNotifier missingPercentage; + abstract RxDouble missingPercentage; /* Si hay porcentaje faltante */ - abstract ValueNotifier hasMissingPercentage; + abstract RxBool hasMissingPercentage; /* Porcentaje sugerido */ - abstract ValueNotifier suggestedPercentage; + abstract Rx suggestedPercentage; /* Nota de presentación sugerida */ - abstract ValueNotifier suggestedPresentationGrade; + abstract Rx suggestedPresentationGrade; /* Porcentaje sin nota */ - abstract ValueNotifier percentageWithoutGrade; + abstract RxDouble percentageWithoutGrade; /* Si hay porcentaje sin nota */ - abstract ValueNotifier hasCorrectPercentage; + abstract RxBool hasCorrectPercentage; /* Nota sugerida */ - abstract ValueNotifier suggestedGrade; + abstract Rx suggestedGrade; void updateWithGrades(Grades grades); diff --git a/lib/services_new/interfaces/controllers/horario_controller.dart b/lib/controllers/interfaces/horario_controller.dart similarity index 81% rename from lib/services_new/interfaces/controllers/horario_controller.dart rename to lib/controllers/interfaces/horario_controller.dart index 261e498..19c3fc3 100644 --- a/lib/services_new/interfaces/controllers/horario_controller.dart +++ b/lib/controllers/interfaces/horario_controller.dart @@ -1,4 +1,5 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/horario.dart'; @@ -10,13 +11,13 @@ abstract class HorarioController { abstract Duration periodDuration; abstract Duration periodGap; - abstract ValueNotifier horario; - abstract ValueNotifier loadingHorario; + abstract Rx horario; + abstract RxBool loadingHorario; abstract List usedColors; - abstract ValueNotifier zoom; - abstract ValueNotifier indicatorIsOpen; - abstract ValueNotifier isCenteredInCurrentPeriodAndDay; + abstract RxDouble zoom; + abstract RxBool indicatorIsOpen; + abstract RxBool isCenteredInCurrentPeriodAndDay; abstract TransformationController blockContentController; abstract TransformationController daysHeaderController; diff --git a/lib/main.dart b/lib/main.dart index 96dcb71..f3343fd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,7 +10,7 @@ import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/screens/splash_screen.dart'; import 'package:mi_utem/services/background_service.dart'; import 'package:mi_utem/services/notification_service.dart'; -import 'package:mi_utem/services_new/service_manager.dart'; +import 'package:mi_utem/services/service_manager.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/lib/models/carrera.dart b/lib/models/carrera.dart index 9b40563..9f4331b 100644 --- a/lib/models/carrera.dart +++ b/lib/models/carrera.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:recase/recase.dart'; +import 'package:mi_utem/utils/string_utils.dart'; class Carrera { String? id; @@ -11,29 +11,15 @@ class Carrera { Carrera({this.id, this.nombre, this.estado, this.codigo, this.plan}); - factory Carrera.fromJson(Map? json) { - if (json == null) { - return Carrera(); - } - return Carrera( - id: json['id'], - nombre: json['nombre'] != null ? ReCase(json['nombre']).titleCase : null, - estado: json['estado'] != null ? ReCase(json['estado']).titleCase : null, - plan: json['plan'] != null ? ReCase(json['plan']).titleCase : null, - codigo: json['codigo'], - ); - } + factory Carrera.fromJson(Map? json) => json != null ? Carrera( + id: json['id'], + nombre: json['nombre'] != null ? capitalize(json['nombre']) : null, + estado: json['estado'] != null ? capitalize(json['estado']) : null, + plan: json['plan'] != null ? capitalize(json['plan']) : null, + codigo: json['codigo'], + ) : Carrera(); - static List fromJsonList(dynamic json) { - if (json == null) { - return []; - } - List list = []; - for (var item in json) { - list.add(Carrera.fromJson(item)); - } - return list; - } + static List fromJsonList(dynamic json) => json != null ? (json as List).map((it) => Carrera.fromJson(it)).toList() : []; toJson() => { 'id': id, diff --git a/lib/models/noticia.dart b/lib/models/noticia.dart index dfc1fa2..fba9622 100644 --- a/lib/models/noticia.dart +++ b/lib/models/noticia.dart @@ -4,22 +4,21 @@ class Noticia { Noticia({this.id, this.titulo, this.subtitulo, this.link, this.imagen}); - Noticia.empty() - : id = null, - titulo = "", - subtitulo = "", - link = "", - imagen = ""; + Noticia.empty() : this( + id: null, + titulo: '', + subtitulo: '', + link: '', + imagen: '', + ); - factory Noticia.fromApiJson(Map json) => - Noticia( - id: json["id"], - titulo: json["titulo"], - subtitulo: json["subtitulo"], - link: json["link"], - imagen: json["imagen"], - ); + factory Noticia.fromJson(Map? json) => json != null ? Noticia( + id: json["id"], + titulo: json["titulo"], + subtitulo: json["subtitulo"], + link: json["link"], + imagen: json["imagen"], + ) : Noticia.empty(); - static List fromApiJsonList(List json) => - json.map((e) => Noticia.fromApiJson(e)).toList(); + static List fromJsonList(List json) => json.map((e) => Noticia.fromJson(e)).toList(); } diff --git a/lib/models/pair.dart b/lib/models/pair.dart index 6131b49..e288222 100644 --- a/lib/models/pair.dart +++ b/lib/models/pair.dart @@ -1,9 +1,16 @@ +import 'dart:convert'; + class Pair { final A a; final B b; Pair(this.a, this.b); + toJson() => { + 'a': a, + 'b': b, + }; + @override - String toString() => "Pair($a, $b)"; + String toString() => jsonEncode(toJson()); } \ No newline at end of file diff --git a/lib/models/permiso_ingreso.dart b/lib/models/permiso_ingreso.dart index 18a9528..59d6cfc 100644 --- a/lib/models/permiso_ingreso.dart +++ b/lib/models/permiso_ingreso.dart @@ -25,7 +25,7 @@ class PermisoIngreso { this.fechaSolicitud, }); - factory PermisoIngreso.fromJson(Map json) => PermisoIngreso( + factory PermisoIngreso.fromJson(Map? json) => json != null ? PermisoIngreso( id: json['id'], user: json.containsKey("usuario") ? User.fromJson(json['usuario']) : null, codigoQr: json['codigoQr'], @@ -36,7 +36,7 @@ class PermisoIngreso { jornada: json['jornada'], vigencia: json['vigencia'], fechaSolicitud: DateTime.tryParse(json['fechaSolicitud']), - ); + ) : PermisoIngreso(); static List fromJsonList(List? json) => json != null ? json.map((it) => PermisoIngreso.fromJson(it)).toList() : []; } diff --git a/lib/models/user/rut.dart b/lib/models/user/rut.dart index 4d4beb3..685739d 100644 --- a/lib/models/user/rut.dart +++ b/lib/models/user/rut.dart @@ -1,5 +1,3 @@ - - class Rut { int rut; diff --git a/lib/services_new/implementations/repositories/asignaturas_repository.dart b/lib/repositories/implementations/asignaturas_repository.dart similarity index 94% rename from lib/services_new/implementations/repositories/asignaturas_repository.dart rename to lib/repositories/implementations/asignaturas_repository.dart index fee1a9c..ca40146 100644 --- a/lib/services_new/implementations/repositories/asignaturas_repository.dart +++ b/lib/repositories/implementations/asignaturas_repository.dart @@ -5,7 +5,7 @@ import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; class AsignaturasRepositoryImplementation implements AsignaturasRepository { diff --git a/lib/services_new/implementations/repositories/auth_repository.dart b/lib/repositories/implementations/auth_repository.dart similarity index 96% rename from lib/services_new/implementations/repositories/auth_repository.dart rename to lib/repositories/implementations/auth_repository.dart index 656bb73..865b2f0 100644 --- a/lib/services_new/implementations/repositories/auth_repository.dart +++ b/lib/repositories/implementations/auth_repository.dart @@ -5,7 +5,7 @@ import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/auth_repository.dart'; +import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; class AuthRepositoryImplementation extends AuthRepository { diff --git a/lib/services_new/implementations/repositories/carreras_repository.dart b/lib/repositories/implementations/carreras_repository.dart similarity index 89% rename from lib/services_new/implementations/repositories/carreras_repository.dart rename to lib/repositories/implementations/carreras_repository.dart index 2f770c7..35fc657 100644 --- a/lib/services_new/implementations/repositories/carreras_repository.dart +++ b/lib/repositories/implementations/carreras_repository.dart @@ -4,7 +4,7 @@ import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/carreras_repository.dart'; +import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; class CarrerasRepositoryImplementation extends CarrerasRepository { diff --git a/lib/services_new/implementations/repositories/credentials_repository.dart b/lib/repositories/implementations/credentials_repository.dart similarity index 88% rename from lib/services_new/implementations/repositories/credentials_repository.dart rename to lib/repositories/implementations/credentials_repository.dart index 54aedc1..c4dc798 100644 --- a/lib/services_new/implementations/repositories/credentials_repository.dart +++ b/lib/repositories/implementations/credentials_repository.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/user/credential.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; +import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; class CredentialsRepositoryImplementation implements CredentialsRepository { diff --git a/lib/services_new/implementations/repositories/grades_repository.dart b/lib/repositories/implementations/grades_repository.dart similarity index 91% rename from lib/services_new/implementations/repositories/grades_repository.dart rename to lib/repositories/implementations/grades_repository.dart index 69e0b65..acb105e 100644 --- a/lib/services_new/implementations/repositories/grades_repository.dart +++ b/lib/repositories/implementations/grades_repository.dart @@ -4,7 +4,7 @@ import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/grades_repository.dart'; +import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; class GradesRepositoryImplementation extends GradesRepository { diff --git a/lib/services_new/implementations/repositories/horario_repository.dart b/lib/repositories/implementations/horario_repository.dart similarity index 90% rename from lib/services_new/implementations/repositories/horario_repository.dart rename to lib/repositories/implementations/horario_repository.dart index 7e79382..db505c5 100644 --- a/lib/services_new/implementations/repositories/horario_repository.dart +++ b/lib/repositories/implementations/horario_repository.dart @@ -4,7 +4,7 @@ import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/horario_repository.dart'; +import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; class HorarioRepositoryImplementation implements HorarioRepository { @override diff --git a/lib/services_new/implementations/repositories/noticias_repository.dart b/lib/repositories/implementations/noticias_repository.dart similarity index 84% rename from lib/services_new/implementations/repositories/noticias_repository.dart rename to lib/repositories/implementations/noticias_repository.dart index a86246a..d6e8dab 100644 --- a/lib/services_new/implementations/repositories/noticias_repository.dart +++ b/lib/repositories/implementations/noticias_repository.dart @@ -4,7 +4,7 @@ import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/noticias_repository.dart'; +import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; class NoticiasRepositoryImplementation implements NoticiasRepository { @@ -24,7 +24,7 @@ class NoticiasRepositoryImplementation implements NoticiasRepository { throw CustomException.custom(response.reasonPhrase); } - return Noticia.fromApiJsonList(json as List); + return Noticia.fromJsonList(json as List); } } \ No newline at end of file diff --git a/lib/services_new/implementations/repositories/permiso_ingreso_repository.dart b/lib/repositories/implementations/permiso_ingreso_repository.dart similarity index 93% rename from lib/services_new/implementations/repositories/permiso_ingreso_repository.dart rename to lib/repositories/implementations/permiso_ingreso_repository.dart index 29dda07..0504738 100644 --- a/lib/services_new/implementations/repositories/permiso_ingreso_repository.dart +++ b/lib/repositories/implementations/permiso_ingreso_repository.dart @@ -4,7 +4,7 @@ import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; class PermisoIngresoRepositoryImplementation extends PermisoIngresoRepository { diff --git a/lib/services_new/interfaces/repositories/asignaturas_repository.dart b/lib/repositories/interfaces/asignaturas_repository.dart similarity index 100% rename from lib/services_new/interfaces/repositories/asignaturas_repository.dart rename to lib/repositories/interfaces/asignaturas_repository.dart diff --git a/lib/services_new/interfaces/repositories/auth_repository.dart b/lib/repositories/interfaces/auth_repository.dart similarity index 100% rename from lib/services_new/interfaces/repositories/auth_repository.dart rename to lib/repositories/interfaces/auth_repository.dart diff --git a/lib/services_new/interfaces/repositories/carreras_repository.dart b/lib/repositories/interfaces/carreras_repository.dart similarity index 100% rename from lib/services_new/interfaces/repositories/carreras_repository.dart rename to lib/repositories/interfaces/carreras_repository.dart diff --git a/lib/services_new/interfaces/repositories/credentials_repository.dart b/lib/repositories/interfaces/credentials_repository.dart similarity index 100% rename from lib/services_new/interfaces/repositories/credentials_repository.dart rename to lib/repositories/interfaces/credentials_repository.dart diff --git a/lib/services_new/interfaces/repositories/grades_repository.dart b/lib/repositories/interfaces/grades_repository.dart similarity index 100% rename from lib/services_new/interfaces/repositories/grades_repository.dart rename to lib/repositories/interfaces/grades_repository.dart diff --git a/lib/services_new/interfaces/repositories/horario_repository.dart b/lib/repositories/interfaces/horario_repository.dart similarity index 100% rename from lib/services_new/interfaces/repositories/horario_repository.dart rename to lib/repositories/interfaces/horario_repository.dart diff --git a/lib/services_new/interfaces/repositories/noticias_repository.dart b/lib/repositories/interfaces/noticias_repository.dart similarity index 100% rename from lib/services_new/interfaces/repositories/noticias_repository.dart rename to lib/repositories/interfaces/noticias_repository.dart diff --git a/lib/services_new/interfaces/repositories/permiso_ingreso_repository.dart b/lib/repositories/interfaces/permiso_ingreso_repository.dart similarity index 100% rename from lib/services_new/interfaces/repositories/permiso_ingreso_repository.dart rename to lib/repositories/interfaces/permiso_ingreso_repository.dart diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 8040f0f..0b22488 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; @@ -8,9 +9,8 @@ import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:watch_it/watch_it.dart'; class AsignaturaDetalleScreen extends StatelessWidget { final Asignatura asignatura; @@ -57,7 +57,7 @@ class AsignaturaDetalleScreen extends StatelessWidget { Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())); final grades = asignatura.grades; if (grades != null) { - di.get().updateWithGrades(grades); + Get.find().updateWithGrades(grades); } }, ), diff --git a/lib/screens/asignatura/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/asignatura_estudiantes_tab.dart index 53cb247..1d1a31d 100644 --- a/lib/screens/asignatura/asignatura_estudiantes_tab.dart +++ b/lib/screens/asignatura/asignatura_estudiantes_tab.dart @@ -12,28 +12,24 @@ class AsignaturaEstudiantesTab extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) { - return asignatura == null - ? CustomErrorWidget( - title: "Ocurrió un error trayendo a los estudiantes", - error: '', - ) - : ListView.separated( - itemCount: 0, - separatorBuilder: (context, index) => Divider( - height: 5, - indent: 20, - endIndent: 20, - ), - itemBuilder: (context, i) { - return ListTile( - onTap: () { - AnalyticsService.logEvent( - 'asignatura_estudiante_tap', - ); - }, - ); - }, + Widget build(BuildContext context) => asignatura == null ? CustomErrorWidget( + title: "Ocurrió un error trayendo a los estudiantes", + error: '', + ) : ListView.separated( + itemCount: 0, + separatorBuilder: (context, index) => Divider( + height: 5, + indent: 20, + endIndent: 20, + ), + itemBuilder: (context, i) { + return ListTile( + onTap: () { + AnalyticsService.logEvent( + 'asignatura_estudiante_tap', ); - } + }, + ); + }, + ); } diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index cb10170..bb8c71e 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; import 'package:mi_utem/widgets/snackbar.dart'; -import 'package:watch_it/watch_it.dart'; class AsignaturaNotasTab extends StatefulWidget { final Asignatura asignatura; @@ -34,7 +34,7 @@ class _AsignaturaNotasTabState extends State { @override Widget build(BuildContext context) => PullToRefresh( onRefresh: () async { - final asignatura = await di.get().getDetalleAsignatura(this.asignatura.codigo); + final asignatura = await Get.find().getDetalleAsignatura(this.asignatura.codigo); if(asignatura == null) { showErrorSnackbar(context, "Ocurrió un error actualizar la asignatura. Por favor intenta más tarde."); return; diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index 7f9e0b5..e19dfd4 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; -import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/widgets/asignatura/lista/lista_asignaturas.dart'; import 'package:mi_utem/widgets/asignatura/lista/sin_asignaturas_mensaje.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; -import 'package:watch_it/watch_it.dart'; -class AsignaturasListaScreen extends StatefulWidget with WatchItStatefulWidgetMixin { +class AsignaturasListaScreen extends StatefulWidget { const AsignaturasListaScreen({super.key}); @override @@ -21,7 +21,7 @@ class AsignaturasListaScreen extends StatefulWidget with WatchItStatefulWidgetMi } class _AsignaturasListaScreenState extends State { - final _asignaturasService = di.get(); + final _asignaturasService = Get.find(); bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; @@ -43,12 +43,13 @@ class _AsignaturasListaScreenState extends State { }, child: FutureBuilder?>( future: () async { - final carrerasService = di.get(); - final selectedCarrera = watchValue((CarrerasService service) => service.selectedCarrera); - if (selectedCarrera == null) { + final carrerasService = Get.find(); + if (carrerasService.selectedCarrera == null) { await carrerasService.getCarreras(); } + final selectedCarrera = carrerasService.selectedCarrera; + return await _asignaturasService.getAsignaturas(selectedCarrera?.id); }(), builder: (context, snapshot) { diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index f621bf7..12d27b0 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/widgets/calculadora_notas/display_notas_widget.dart'; import 'package:mi_utem/widgets/calculadora_notas/editar_notas_widget.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:watch_it/watch_it.dart'; -class CalculadoraNotasScreen extends StatefulWidget with WatchItStatefulWidgetMixin { +class CalculadoraNotasScreen extends StatefulWidget { const CalculadoraNotasScreen({super.key}); @override @@ -15,7 +15,7 @@ class CalculadoraNotasScreen extends StatefulWidget with WatchItStatefulWidgetMi class _CalculadoraNotasScreenState extends State { - final _controller = di.get(); + CalculatorController _controller = Get.find(); @override void initState() { @@ -25,10 +25,6 @@ class _CalculadoraNotasScreenState extends State { @override Widget build(BuildContext context) { - final partialGrades = watchValue((CalculatorController controller) => controller.partialGrades); - final gradeTextFieldControllers = watchValue((CalculatorController controller) => controller.gradeTextFieldControllers); - final percentageTextFieldControllers = watchValue((CalculatorController controller) => controller.percentageTextFieldControllers); - return Scaffold( appBar: CustomAppBar( title: const Text("Calculadora de notas"), @@ -37,16 +33,17 @@ class _CalculadoraNotasScreenState extends State { padding: const EdgeInsets.all(10), children: [ DisplayNotasWidget(), - EditarNotasWidget( - partialGrades: partialGrades, - gradeTextFieldControllers: gradeTextFieldControllers, - percentageTextFieldControllers: percentageTextFieldControllers, + Obx(() => EditarNotasWidget( + partialGrades: _controller.partialGrades, + gradeTextFieldControllers: _controller.gradeTextFieldControllers, + percentageTextFieldControllers: _controller.percentageTextFieldControllers, onAddGrade: () => _controller.addGrade(IEvaluacion( nota: null, porcentaje: null, )), onRemoveGrade: (idx) => _controller.removeGradeAt(idx), - ) + onChanged: (idx, evaluacion) => _controller.updateGradeAt(idx, evaluacion), + )), ], ), ); diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index 1071609..6ff1c87 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -1,23 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_windowmanager/flutter_windowmanager.dart'; +import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; import 'package:mi_utem/widgets/credencial_card.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/flip_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; -import 'package:watch_it/watch_it.dart'; -class CredencialScreen extends StatefulWidget with WatchItStatefulWidgetMixin { - CredencialScreen({ - Key? key, - }) : super(key: key); +class CredencialScreen extends StatefulWidget { + const CredencialScreen({ + super.key, + }); @override State createState() => _CredencialScreenState(); @@ -29,27 +30,18 @@ class _CredencialScreenState extends State { @override void initState() { ReviewService.addScreen("CredencialScreen"); - _secureScreen(); + FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); super.initState(); } - Future _secureScreen() async { - await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); - } - - Future _desecureScreen() async { - await FlutterWindowManager.clearFlags(FlutterWindowManager.FLAG_SECURE); - } - @override void dispose() { - _desecureScreen(); + FlutterWindowManager.clearFlags(FlutterWindowManager.FLAG_SECURE); super.dispose(); } @override Widget build(BuildContext context) { - Carrera? carreraActiva = watchValue((CarrerasService service) => service.selectedCarrera); return Scaffold( appBar: CustomAppBar( @@ -64,14 +56,19 @@ class _CredencialScreenState extends State { ], ), backgroundColor: Colors.grey[200], - body: FutureBuilder( + body: FutureBuilder>( future: () async { - final user = await di.get().getUser(); - if(carreraActiva == null) { - await di.get().getCarreras(); + final authService = Get.find(); + final carrerasService = Get.find(); + + if(carrerasService.selectedCarrera == null) { + await carrerasService.getCarreras(); } - return user; + final user = await authService.getUser(); + final carrera = carrerasService.selectedCarrera; + + return Pair(user, carrera); }(), builder: (context, snapshot) { if (snapshot.hasError) { @@ -81,7 +78,9 @@ class _CredencialScreenState extends State { ); } - final user = snapshot.data; + final pair = snapshot.data; + final user = pair?.a; + final carreraActiva = pair?.b; if (!snapshot.hasData) { return Container( @@ -99,21 +98,24 @@ class _CredencialScreenState extends State { ); } - if (user == null) { + if (user == null || user.rut == null || carreraActiva == null || carreraActiva.nombre == null) { return CustomErrorWidget( title: "Ocurrió un error al generar tu crendencial. Por favor, intenta nuevamente.", error: snapshot.error, ); } - if (user.rut != null && carreraActiva!.nombre != null && carreraActiva.nombre!.isNotEmpty) { + if (carreraActiva.nombre?.isNotEmpty == true) { return Center( child: SafeArea( child: CredencialCard( user: user, carrera: carreraActiva, controller: _flipController, - onFlip: (direction) => _onFlip(), + onFlip: (_) { + AnalyticsService.logEvent("credencial_flip"); + setState(() {}); + }, ), ), ); @@ -149,9 +151,4 @@ class _CredencialScreenState extends State { ), ); } - - void _onFlip() { - AnalyticsService.logEvent("credencial_flip"); - setState(() {}); - } } diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index 158044c..7418a29 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -2,12 +2,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; @@ -15,9 +16,8 @@ import 'package:mi_utem/widgets/snackbar.dart'; import 'package:path_provider/path_provider.dart'; import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:watch_it/watch_it.dart'; -class HorarioScreen extends StatefulWidget with WatchItStatefulWidgetMixin { +class HorarioScreen extends StatefulWidget { const HorarioScreen({super.key}); @override @@ -28,11 +28,11 @@ class _HorarioScreenState extends State { final ScreenshotController _screenshotController = ScreenshotController(); - final controller = di.get(); + final horarioController = Get.find(); void _moveViewportToCurrentTime() { AnalyticsService.logEvent("horario_move_viewport_to_current_time"); - controller.moveViewportToCurrentPeriodAndDay(context); + horarioController.moveViewportToCurrentPeriodAndDay(context); } void _captureAndShareScreenshot(Horario horario) async { @@ -59,7 +59,7 @@ class _HorarioScreenState extends State { @override void initState() { - controller.getHorarioData().catchError((err) => { + horarioController.getHorarioData().catchError((err) => { logger.e("Error al cargar el horario", err), showErrorSnackbar(context, "Ocurrió un error al cargar el horario! Por favor intenta más tarde.") }); @@ -75,39 +75,33 @@ class _HorarioScreenState extends State { DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); - controller.init(context); + horarioController.init(context); - final horario = watchValue((HorarioController controller) => controller.horario); - final isLoadingHorario = watchValue((HorarioController controller) => controller.loadingHorario); - final isCenteredInCurrentPeriodAndDay = watchValue((HorarioController controller) => controller.isCenteredInCurrentPeriodAndDay); - - return Scaffold( + return Obx(() => Scaffold( appBar: CustomAppBar( title: Text("Horario"), actions: [ - horario != null ? IconButton( - onPressed: () { - controller.getHorarioData(forceRefresh: true).then((value) { - setState(() {}); - }); - }, + if(horarioController.horario.value != null) IconButton( + onPressed: () => horarioController.getHorarioData(forceRefresh: true).then((value) { + setState(() {}); + }), icon: Icon(Icons.refresh_sharp), tooltip: "Forzar actualización del horario", - ) : Container(), - horario != null && !isCenteredInCurrentPeriodAndDay ? IconButton( + ), + if(horarioController.horario.value != null && !horarioController.isCenteredInCurrentPeriodAndDay.value) IconButton( onPressed: () => _moveViewportToCurrentTime(), icon: Icon(Icons.center_focus_strong), tooltip: "Centrar Horario En Hora Actual", - ) : Container(), - horario != null ? IconButton( - onPressed: () => _captureAndShareScreenshot(horario), + ), + if(horarioController.horario.value != null) IconButton( + onPressed: () => _captureAndShareScreenshot(horarioController.horario.value!), icon: Icon(Icons.share), tooltip: "Compartir Horario", - ) : Container() + ) ], ), - body: ((isLoadingHorario && horario == null) || horario == null) ? Container( + body: ((horarioController.loadingHorario.value && horarioController.horario.value == null) || horarioController.horario.value == null) ? Container( padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -122,9 +116,9 @@ class _HorarioScreenState extends State { ) : Screenshot( controller: _screenshotController, child: HorarioMainScroller( - horario: horario, + horario: horarioController.horario.value!, ), ), - ); + )); } } diff --git a/lib/screens/horario/widgets/horario_days_header.dart b/lib/screens/horario/widgets/horario_days_header.dart index ab35443..6deb8e9 100644 --- a/lib/screens/horario/widgets/horario_days_header.dart +++ b/lib/screens/horario/widgets/horario_days_header.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/horario/bloque_dias_card.dart'; -import 'package:watch_it/watch_it.dart'; class HorarioDaysHeader extends StatelessWidget { final Horario horario; @@ -25,43 +25,33 @@ class HorarioDaysHeader extends StatelessWidget { this.borderWidth = 2, }); - List get _children { - return [ - TableRow( - children: horario.diasHorario - .asMap() - .entries - .map( - (entry) => BloqueDiasCard( - day: entry.value, - height: height, - width: dayWidth, - active: showActiveDay && entry.key == di.get().indexOfCurrentDayStartingAtMonday, - backgroundColor: backgroundColor, - ), - ) - .toList(), - ), - ]; - } + List get _children => [ + TableRow( + children: horario.diasHorario.asMap().entries.map((entry) => BloqueDiasCard( + day: entry.value, + height: height, + width: dayWidth, + active: showActiveDay && entry.key == Get.find().indexOfCurrentDayStartingAtMonday, + backgroundColor: backgroundColor, + )).toList(), + ), + ]; @override - Widget build(BuildContext context) { - return Table( - defaultColumnWidth: FixedColumnWidth(dayWidth), - border: TableBorder( - verticalInside: BorderSide( - color: borderColor, - style: BorderStyle.solid, - width: borderWidth, - ), - bottom: BorderSide( - color: borderColor, - style: BorderStyle.solid, - width: borderWidth, - ), + Widget build(BuildContext context) => Table( + defaultColumnWidth: FixedColumnWidth(dayWidth), + border: TableBorder( + verticalInside: BorderSide( + color: borderColor, + style: BorderStyle.solid, + width: borderWidth, + ), + bottom: BorderSide( + color: borderColor, + style: BorderStyle.solid, + width: borderWidth, ), - children: _children, - ); - } + ), + children: _children, + ); } diff --git a/lib/screens/horario/widgets/horario_indicator.dart b/lib/screens/horario/widgets/horario_indicator.dart index 5d4fde0..18c8660 100644 --- a/lib/screens/horario/widgets/horario_indicator.dart +++ b/lib/screens/horario/widgets/horario_indicator.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; -import 'package:watch_it/watch_it.dart'; -class HorarioIndicator extends StatefulWidget with WatchItStatefulWidgetMixin { +class HorarioIndicator extends StatefulWidget { static const _height = 2.0; static const _circleRadius = 10.0; static const _tapAreaRadius = 15.0; @@ -45,7 +45,9 @@ class _HorarioIndicatorState extends State { super.dispose(); } - double get _centerLineYPosition => (di.get().minutesFromStart * widget.heightByMinute); + HorarioController _horarioController = Get.find(); + + double get _centerLineYPosition => (_horarioController.minutesFromStart * widget.heightByMinute); double get _startLineXPosition => widget.initialMargin.left; @@ -57,8 +59,6 @@ class _HorarioIndicatorState extends State { final lineTop = _centerLineYPosition - HorarioIndicator._height / 2; final lineLeft = _startLineXPosition; - final indicatorIsOpen = watchValue((HorarioController controller) => controller.indicatorIsOpen); - return Stack( children: [ if (lineTop > 0 && lineLeft > 0) @@ -80,26 +80,26 @@ class _HorarioIndicatorState extends State { child: GestureDetector( onTap: () { AnalyticsService.logEvent('horario_indicator_dot_tap'); - di.get().setIndicatorIsOpen(!indicatorIsOpen); + _horarioController.setIndicatorIsOpen(!_horarioController.indicatorIsOpen.value); }, child: Container( padding: EdgeInsets.all(HorarioIndicator._tapAreaRadius), decoration: BoxDecoration( borderRadius: BorderRadius.circular(HorarioIndicator._tapAreaRadius * 2), ), - child: AnimatedContainer( + child: Obx(() => AnimatedContainer( duration: const Duration(milliseconds: 300), height: HorarioIndicator._circleRadius * 2, - width: indicatorIsOpen ? 50 : HorarioIndicator._circleRadius * 2, + width: _horarioController.indicatorIsOpen.value ? 50 : HorarioIndicator._circleRadius * 2, decoration: BoxDecoration( color: widget.color, borderRadius: BorderRadius.circular(HorarioIndicator._circleRadius), ), - child: indicatorIsOpen ? Center( + child: _horarioController.indicatorIsOpen.value ? Center( child: _TickerTimeText(time: DateTime.now()), ) : Container(), - ), + )), ), ), ), diff --git a/lib/screens/horario/widgets/horario_main_scroller.dart b/lib/screens/horario/widgets/horario_main_scroller.dart index 3bb071e..26433d6 100644 --- a/lib/screens/horario/widgets/horario_main_scroller.dart +++ b/lib/screens/horario/widgets/horario_main_scroller.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_blocks_content.dart'; import 'package:mi_utem/screens/horario/widgets/horario_corner.dart'; import 'package:mi_utem/screens/horario/widgets/horario_days_header.dart'; import 'package:mi_utem/screens/horario/widgets/horario_indicator.dart'; import 'package:mi_utem/screens/horario/widgets/horario_periods_header.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; -import 'package:watch_it/watch_it.dart'; class HorarioMainScroller extends StatefulWidget { static const double blockWidth = 320.0; @@ -23,7 +23,7 @@ class HorarioMainScroller extends StatefulWidget { final Horario horario; final bool showActive; - final controller = di.get(); + final controller = Get.find(); HorarioMainScroller({ super.key, @@ -34,8 +34,8 @@ class HorarioMainScroller extends StatefulWidget { @override _HorarioMainScrollerState createState() => _HorarioMainScrollerState(); - static double get daysWidth => dayWidth * di.get().daysCount; - static double get periodsHeight => periodHeight * di.get().periodsCount; + static double get daysWidth => dayWidth * Get.find().daysCount; + static double get periodsHeight => periodHeight * Get.find().periodsCount; static double get totalWidth => daysWidth + periodWidth; static double get totalHeight => periodsHeight + dayHeight; diff --git a/lib/screens/horario/widgets/horario_periods_header.dart b/lib/screens/horario/widgets/horario_periods_header.dart index f50a60f..b7487ae 100644 --- a/lib/screens/horario/widgets/horario_periods_header.dart +++ b/lib/screens/horario/widgets/horario_periods_header.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/horario/bloque_periodo_card.dart'; -import 'package:watch_it/watch_it.dart'; class HorarioPeriodsHeader extends StatelessWidget { final Horario horario; @@ -15,7 +15,7 @@ class HorarioPeriodsHeader extends StatelessWidget { final bool showActivePeriod; const HorarioPeriodsHeader({ - Key? key, + super.key, required this.horario, required this.periodHeight, required this.width, @@ -25,45 +25,33 @@ class HorarioPeriodsHeader extends StatelessWidget { this.borderWidth = 2, }); - List get _children { - return horario.horasInicio - .asMap() - .entries - .map( - (e) => TableRow( - children: [ - BloquePeriodoCard( - inicio: horario.horasInicio[e.key], - intermedio: horario.horasIntermedio[e.key], - fin: horario.horasTermino[e.key], - active: showActivePeriod && di.get().indexOfCurrentPeriod == e.key, - height: periodHeight, - width: width, - backgroundColor: backgroundColor, - ), - ], - ), - ) - .toList(); - } - @override - Widget build(BuildContext context) { - return Table( - defaultColumnWidth: FixedColumnWidth(width), - border: TableBorder( - horizontalInside: BorderSide( - color: borderColor, - style: BorderStyle.solid, - width: borderWidth, - ), - right: BorderSide( - color: borderColor, - style: BorderStyle.solid, - width: borderWidth, - ), + Widget build(BuildContext context) => Table( + defaultColumnWidth: FixedColumnWidth(width), + border: TableBorder( + horizontalInside: BorderSide( + color: borderColor, + style: BorderStyle.solid, + width: borderWidth, ), - children: _children, - ); - } + right: BorderSide( + color: borderColor, + style: BorderStyle.solid, + width: borderWidth, + ), + ), + children: horario.horasInicio.asMap().entries.map((e) => TableRow( + children: [ + BloquePeriodoCard( + inicio: horario.horasInicio[e.key], + intermedio: horario.horasIntermedio[e.key], + fin: horario.horasTermino[e.key], + active: showActivePeriod && Get.find().indexOfCurrentPeriod == e.key, + height: periodHeight, + width: width, + backgroundColor: backgroundColor, + ), + ], + )).toList(), + ); } diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 97125f7..1420feb 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -5,12 +5,13 @@ import 'package:flutter/foundation.dart'; import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_markdown/flutter_markdown.dart"; +import "package:get/get.dart"; import "package:mi_utem/config/logger.dart"; import "package:mi_utem/models/user/user.dart"; +import "package:mi_utem/services/interfaces/auth_service.dart"; +import "package:mi_utem/services/interfaces/grades_service.dart"; import "package:mi_utem/services/remote_config/remote_config.dart"; import "package:mi_utem/services/review_service.dart"; -import "package:mi_utem/services_new/interfaces/auth_service.dart"; -import "package:mi_utem/services_new/interfaces/grades_service.dart"; import "package:mi_utem/widgets/banner.dart"; import 'package:mi_utem/widgets/banners_section.dart'; import "package:mi_utem/widgets/custom_app_bar.dart"; @@ -19,7 +20,6 @@ import "package:mi_utem/widgets/noticias/noticias_carrusel_widget.dart"; import "package:mi_utem/widgets/permisos_section.dart"; import "package:mi_utem/widgets/pull_to_refresh.dart"; import "package:mi_utem/widgets/quick_menu_section.dart"; -import "package:watch_it/watch_it.dart"; class MainScreen extends StatefulWidget { MainScreen({ @@ -34,7 +34,7 @@ class _MainScreenState extends State { List _banners = const []; User? _user; - final _authService = di.get(); + final _authService = Get.find(); @override void initState() { @@ -75,7 +75,7 @@ class _MainScreenState extends State { appBar: CustomAppBar(title: Text("Inicio")), drawer: CustomDrawer(), floatingActionButton: kDebugMode ? FloatingActionButton( - onPressed: () => di.get().lookForGradeUpdates(), + onPressed: () => Get.find().lookForGradeUpdates(), tooltip: "Probar notificaciones de notas", child: Icon(Icons.notifications, color: Colors.white, diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index 733350d..77f0751 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -4,13 +4,14 @@ import 'package:barcode_image/barcode_image.dart'; import 'package:barcode_widget/barcode_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; +import 'package:get/get.dart'; import 'package:image/image.dart' as dartImage; import 'package:intl/intl.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; @@ -18,7 +19,6 @@ import 'package:mi_utem/widgets/image_view_screen.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; -import 'package:watch_it/watch_it.dart'; class PermisoCovidScreen extends StatefulWidget { final String passId; @@ -37,7 +37,7 @@ class _PermisoCovidScreenState extends State { body: PullToRefresh( onRefresh: () async => setState(() {}), child: FutureBuilder( - future: di.get().getDetallesPermiso(widget.passId), + future: Get.find().getDetallesPermiso(widget.passId), builder: (context, snapshot) { if (snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "No sabemos lo que ocurrió. Por favor intenta más tarde."; diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 360015a..e5bd02e 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -3,17 +3,17 @@ import 'dart:convert'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/notification_service.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:watch_it/watch_it.dart'; class SplashScreen extends StatefulWidget { SplashScreen({Key? key}) : super(key: key); @@ -24,7 +24,7 @@ class SplashScreen extends StatefulWidget { class _SplashScreenState extends State { - final _authService = di.get(); + final _authService = Get.find(); @override void initState() { diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index bb77746..9cb7429 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -2,11 +2,12 @@ import 'dart:core'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/docentes_service.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/image_view_screen.dart'; @@ -15,7 +16,6 @@ import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:watch_it/watch_it.dart'; class UsuarioScreen extends StatefulWidget { final int tipo; @@ -34,7 +34,7 @@ class UsuarioScreen extends StatefulWidget { } class _UsuarioScreenState extends State { - final _authService = di.get(); + final _authService = Get.find(); Future? _userFuture; User? _user; diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index cb6a1e6..2f8f55d 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:background_fetch/background_fetch.dart'; -import 'package:mi_utem/services_new/interfaces/grades_service.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/services/interfaces/grades_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:watch_it/watch_it.dart'; class BackgroundController { // [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true` @@ -39,7 +39,7 @@ class BackgroundService { requiredNetworkType: NetworkType.NONE, ), (String taskId) async { - await di.get().lookForGradeUpdates(); + await Get.find().lookForGradeUpdates(); BackgroundFetch.finish(taskId); }, diff --git a/lib/services_new/implementations/auth_service.dart b/lib/services/implementations/auth_service.dart similarity index 93% rename from lib/services_new/implementations/auth_service.dart rename to lib/services/implementations/auth_service.dart index 9a97215..c749456 100644 --- a/lib/services_new/implementations/auth_service.dart +++ b/lib/services/implementations/auth_service.dart @@ -2,21 +2,21 @@ import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; +import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/notification_service.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/auth_repository.dart'; -import 'package:watch_it/watch_it.dart'; class AuthServiceImplementation implements AuthService { - final _authRepository = di.get(); - final _credentialsService = di.get(); + AuthRepository _authRepository = Get.find(); + CredentialsRepository _credentialsService = Get.find(); @override Future isFirstTime() async => !(await secureStorage.containsKey(key: "last_login")); diff --git a/lib/services_new/implementations/carreras_service.dart b/lib/services/implementations/carreras_service.dart similarity index 73% rename from lib/services_new/implementations/carreras_service.dart rename to lib/services/implementations/carreras_service.dart index 60e9b66..ec3c1b5 100644 --- a/lib/services_new/implementations/carreras_service.dart +++ b/lib/services/implementations/carreras_service.dart @@ -1,22 +1,20 @@ -import 'package:flutter/widgets.dart'; -import 'package:listenable_collections/listenable_collections.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/carreras_repository.dart'; -import 'package:watch_it/watch_it.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; class CarrerasServiceImplementation implements CarrerasService { - final _carrerasRepository = di.get(); + final _carrerasRepository = Get.find(); @override - ListNotifier carreras = ListNotifier(); + List carreras = []; @override - ValueNotifier selectedCarrera = ValueNotifier(null); + Carrera? selectedCarrera; @override Future getCarreras() async { @@ -29,7 +27,7 @@ class CarrerasServiceImplementation implements CarrerasService { } @override - void changeSelectedCarrera(Carrera carrera) => selectedCarrera.value = carrera; + void changeSelectedCarrera(Carrera carrera) => selectedCarrera = carrera; @override void autoSelectCarreraActiva() { diff --git a/lib/services_new/implementations/grades_service.dart b/lib/services/implementations/grades_service.dart similarity index 89% rename from lib/services_new/implementations/grades_service.dart rename to lib/services/implementations/grades_service.dart index 02b6bdb..7c4cb57 100644 --- a/lib/services_new/implementations/grades_service.dart +++ b/lib/services/implementations/grades_service.dart @@ -1,20 +1,20 @@ import 'dart:convert'; +import 'package:get/get.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; -import 'package:mi_utem/services_new/interfaces/grades_service.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/grades_repository.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; +import 'package:mi_utem/services/interfaces/grades_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:watch_it/watch_it.dart'; class GradesServiceImplementation implements GradesService { static const savedGradesPrefix = 'savedGrades_'; static const subscribedAsignaturasPrefix = 'subscribedAsignaturas_'; - final _gradesRepository = di.get(); + GradesRepository _gradesRepository = Get.find(); @override Future getGrades(String carreraId, String asignaturaId, {bool forceRefresh = false, bool saveGrades = true}) async { @@ -36,13 +36,17 @@ class GradesServiceImplementation implements GradesService { @override Future> lookForGradeUpdates() async { - final isLoggedIn = await di.get().isLoggedIn(); + final isLoggedIn = await Get.find().isLoggedIn(); if(!isLoggedIn) { return {}; } - final carrera = watchValue((CarrerasService service) => service.selectedCarrera); + final carrerasService = Get.find(); + if(carrerasService.selectedCarrera == null) { + await carrerasService.getCarreras(); + } + final carrera = carrerasService.selectedCarrera; final carreraId = carrera?.id; if(carreraId == null) { @@ -52,7 +56,7 @@ class GradesServiceImplementation implements GradesService { final subscribedAsignaturasJson = await secureStorage.read(key: '$subscribedAsignaturasPrefix$carreraId'); List subscribedAsignaturas; if(subscribedAsignaturasJson == null) { - subscribedAsignaturas = (await di.get().getAsignaturas(carreraId)) ?? []; + subscribedAsignaturas = (await Get.find().getAsignaturas(carreraId)) ?? []; await secureStorage.write(key: '$subscribedAsignaturasPrefix$carreraId', value: jsonEncode(subscribedAsignaturas.map((it) => it.toJson()).toList())); } else { subscribedAsignaturas = Asignatura.fromJsonList(jsonDecode(subscribedAsignaturasJson) as List); diff --git a/lib/services_new/interfaces/auth_service.dart b/lib/services/interfaces/auth_service.dart similarity index 100% rename from lib/services_new/interfaces/auth_service.dart rename to lib/services/interfaces/auth_service.dart diff --git a/lib/services/interfaces/carreras_service.dart b/lib/services/interfaces/carreras_service.dart new file mode 100644 index 0000000..ee28e10 --- /dev/null +++ b/lib/services/interfaces/carreras_service.dart @@ -0,0 +1,14 @@ +import 'package:mi_utem/models/carrera.dart'; + +abstract class CarrerasService { + + abstract List carreras; + abstract Carrera? selectedCarrera; + + Future getCarreras(); + + void changeSelectedCarrera(Carrera carrera); + + void autoSelectCarreraActiva(); + +} \ No newline at end of file diff --git a/lib/services_new/interfaces/grades_service.dart b/lib/services/interfaces/grades_service.dart similarity index 100% rename from lib/services_new/interfaces/grades_service.dart rename to lib/services/interfaces/grades_service.dart diff --git a/lib/services_new/legacy_controllers/notification_controller.dart b/lib/services/legacy_controllers/notification_controller.dart similarity index 100% rename from lib/services_new/legacy_controllers/notification_controller.dart rename to lib/services/legacy_controllers/notification_controller.dart diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index df293b6..68d9f94 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -4,8 +4,8 @@ import 'dart:math'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/services_new/legacy_controllers/notification_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/services/legacy_controllers/notification_controller.dart'; import 'package:mi_utem/widgets/custom_alert_dialog.dart'; class NotificationService { diff --git a/lib/services/service_manager.dart b/lib/services/service_manager.dart new file mode 100644 index 0000000..1b0450b --- /dev/null +++ b/lib/services/service_manager.dart @@ -0,0 +1,49 @@ +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/implementations/calculator_controller.dart'; +import 'package:mi_utem/controllers/implementations/horario_controller.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/repositories/implementations/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/implementations/auth_repository.dart'; +import 'package:mi_utem/repositories/implementations/carreras_repository.dart'; +import 'package:mi_utem/repositories/implementations/credentials_repository.dart'; +import 'package:mi_utem/repositories/implementations/grades_repository.dart'; +import 'package:mi_utem/repositories/implementations/horario_repository.dart'; +import 'package:mi_utem/repositories/implementations/noticias_repository.dart'; +import 'package:mi_utem/repositories/implementations/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; +import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; +import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; +import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; +import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; +import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; +import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; +import 'package:mi_utem/services/implementations/auth_service.dart'; +import 'package:mi_utem/services/implementations/carreras_service.dart'; +import 'package:mi_utem/services/implementations/grades_service.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; +import 'package:mi_utem/services/interfaces/grades_service.dart'; + +Future registerServices() async { + /* Repositorios (Para conectarse a la REST Api */ + Get.lazyPut(() => AuthRepositoryImplementation()); + Get.lazyPut(() => AsignaturasRepositoryImplementation()); + Get.lazyPut(() => CredentialsRepositoryImplementation()); + Get.lazyPut(() => CarrerasRepositoryImplementation()); + Get.lazyPut(() => GradesRepositoryImplementation()); + Get.lazyPut(() => PermisoIngresoRepositoryImplementation()); + Get.lazyPut(() => NoticiasRepositoryImplementation()); + Get.lazyPut(() => HorarioRepositoryImplementation()); + + + /* Servicios (Para procesar datos REST) */ + Get.lazyPut(() => AuthServiceImplementation()); + Get.lazyPut(() => CarrerasServiceImplementation()); + Get.lazyPut(() => GradesServiceImplementation()); + + /* Controladores (Para procesar datos de interfaz) */ + Get.lazyPut(() => HorarioControllerImplementation()); + Get.lazyPut(() => CalculatorControllerImplementation()); +} \ No newline at end of file diff --git a/lib/services_new/interfaces/carreras_service.dart b/lib/services_new/interfaces/carreras_service.dart deleted file mode 100644 index a301906..0000000 --- a/lib/services_new/interfaces/carreras_service.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:listenable_collections/listenable_collections.dart'; -import 'package:mi_utem/models/carrera.dart'; - -abstract class CarrerasService { - - abstract ListNotifier carreras; - abstract ValueNotifier selectedCarrera; - - Future getCarreras(); - - void changeSelectedCarrera(Carrera carrera); - - void autoSelectCarreraActiva(); - -} \ No newline at end of file diff --git a/lib/services_new/service_manager.dart b/lib/services_new/service_manager.dart deleted file mode 100644 index 6cf6f37..0000000 --- a/lib/services_new/service_manager.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:mi_utem/services_new/implementations/repositories/asignaturas_repository.dart'; -import 'package:mi_utem/services_new/implementations/auth_service.dart'; -import 'package:mi_utem/services_new/implementations/carreras_service.dart'; -import 'package:mi_utem/services_new/implementations/controllers/calculator_controller.dart'; -import 'package:mi_utem/services_new/implementations/controllers/horario_controller.dart'; -import 'package:mi_utem/services_new/implementations/repositories/carreras_repository.dart'; -import 'package:mi_utem/services_new/implementations/repositories/credentials_repository.dart'; -import 'package:mi_utem/services_new/implementations/grades_service.dart'; -import 'package:mi_utem/services_new/implementations/repositories/horario_repository.dart'; -import 'package:mi_utem/services_new/implementations/repositories/noticias_repository.dart'; -import 'package:mi_utem/services_new/implementations/repositories/auth_repository.dart'; -import 'package:mi_utem/services_new/implementations/repositories/grades_repository.dart'; -import 'package:mi_utem/services_new/implementations/repositories/permiso_ingreso_repository.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/asignaturas_repository.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; -import 'package:mi_utem/services_new/interfaces/carreras_service.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/carreras_repository.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; -import 'package:mi_utem/services_new/interfaces/grades_service.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/horario_repository.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/noticias_repository.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/auth_repository.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/grades_repository.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/permiso_ingreso_repository.dart'; -import 'package:watch_it/watch_it.dart'; - -Future registerServices() async { - /* Repositorios (Para conectarse a la REST Api */ - di.registerLazySingleton(() => AuthRepositoryImplementation()); - di.registerLazySingleton(() => AsignaturasRepositoryImplementation()); - di.registerLazySingleton(() => CredentialsRepositoryImplementation()); - di.registerLazySingleton(() => CarrerasRepositoryImplementation()); - di.registerLazySingleton(() => GradesRepositoryImplementation()); - di.registerLazySingleton(() => PermisoIngresoRepositoryImplementation()); - di.registerLazySingleton(() => NoticiasRepositoryImplementation()); - di.registerLazySingleton(() => HorarioRepositoryImplementation()); - - - /* Servicios (Para procesar datos REST) */ - di.registerLazySingleton(() => AuthServiceImplementation()); - di.registerLazySingleton(() => CarrerasServiceImplementation()); - di.registerLazySingleton(() => GradesServiceImplementation()); - - /* Controladores (Para procesar datos de interfaz) */ - di.registerLazySingleton(() => HorarioControllerImplementation()); - di.registerLazySingleton(() => CalculatorControllerImplementation()); -} \ No newline at end of file diff --git a/lib/utils/dio_wordpress_client.dart b/lib/utils/dio_wordpress_client.dart deleted file mode 100644 index 2ff069d..0000000 --- a/lib/utils/dio_wordpress_client.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:dio/dio.dart'; - -class DioWordpressClient { - static const String url = 'https://www.utem.cl/wp-json/wp/v2'; - - static Dio _dio = Dio(BaseOptions( - baseUrl: url, - )); - - static Dio get initDio => _dio - //..interceptors.add(DioCacheManager(CacheConfig(baseUrl: url)).interceptor) - ; -} diff --git a/lib/widgets/acerca/acerca_screen.dart b/lib/widgets/acerca/acerca_screen.dart index 55971c7..ad35e68 100644 --- a/lib/widgets/acerca/acerca_screen.dart +++ b/lib/widgets/acerca/acerca_screen.dart @@ -6,37 +6,33 @@ import 'package:mi_utem/widgets/acerca/club/acerca_club_desarrolladores.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; class AcercaScreen extends StatelessWidget { - AcercaScreen({ - Key? key, - }) : super(key: key); + const AcercaScreen({ + super.key, + }); @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey[200], - appBar: CustomAppBar( - title: Text("Acerca de Mi UTEM"), - ), - body: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(10), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - AcercaClub(), - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - child: AcercaAplicacionContent(), - ), - AcercaClubDesarrolladores() - ], - ), + Widget build(BuildContext context) => Scaffold( + backgroundColor: Colors.grey[200], + appBar: CustomAppBar( + title: Text("Acerca de Mi UTEM"), + ), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + AcercaClub(), + Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), + child: AcercaAplicacionContent(), + ), + AcercaClubDesarrolladores() + ], ), ), - ); - } + ), + ); } diff --git a/lib/widgets/asignatura/lista/lista_asignaturas.dart b/lib/widgets/asignatura/lista/lista_asignaturas.dart index 645789d..9e0ee19 100644 --- a/lib/widgets/asignatura/lista/lista_asignaturas.dart +++ b/lib/widgets/asignatura/lista/lista_asignaturas.dart @@ -6,9 +6,9 @@ class ListaAsignaturas extends StatelessWidget { final List asignaturas; const ListaAsignaturas({ - Key? key, + super.key, required this.asignaturas, - }) : super(key: key); + }); @override Widget build(BuildContext context) => ListView.builder( diff --git a/lib/widgets/asignatura/lista/sin_asignaturas_mensaje.dart b/lib/widgets/asignatura/lista/sin_asignaturas_mensaje.dart index a161909..fd74001 100644 --- a/lib/widgets/asignatura/lista/sin_asignaturas_mensaje.dart +++ b/lib/widgets/asignatura/lista/sin_asignaturas_mensaje.dart @@ -6,10 +6,10 @@ class SinAsignaturasMensaje extends StatelessWidget { final String mensaje, emoji; const SinAsignaturasMensaje({ - Key? key, + super.key, required this.mensaje, required this.emoji, - }) : super(key: key); + }); @override Widget build(BuildContext context) => Center( diff --git a/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart b/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart index 16dab8f..c3ec31b 100644 --- a/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart +++ b/lib/widgets/asignatura/notas_tab/labeled_nota_display.dart @@ -3,18 +3,19 @@ import 'package:flutter_masked_text/flutter_masked_text.dart'; import 'package:mi_utem/themes/theme.dart'; class LabeledNotaDisplayWidget extends StatefulWidget { - final String _label; - final num? _nota; - final String? _hint; + final String label; + final num? nota; + final String? hint; - LabeledNotaDisplayWidget({required String label, num? nota, String? hint}) - : _label = label, - _nota = nota, - _hint = hint; + const LabeledNotaDisplayWidget({ + super.key, + required this.label, + this.nota, + this.hint, + }); @override - _LabeledNotaDisplayWidgetState createState() => - _LabeledNotaDisplayWidgetState(); + _LabeledNotaDisplayWidgetState createState() => _LabeledNotaDisplayWidgetState(); } class _LabeledNotaDisplayWidgetState extends State { @@ -22,14 +23,14 @@ class _LabeledNotaDisplayWidgetState extends State { Widget build(BuildContext context) { final notaController = MaskedTextController( mask: '0.0', - text: widget._nota?.toStringAsFixed(1) ?? "", + text: widget.nota?.toStringAsFixed(1) ?? "", ); return Row( crossAxisAlignment: CrossAxisAlignment.center, - children: [ + children: [ Text( - widget._label, + widget.label, style: TextStyle(fontSize: 16), ), Container( @@ -40,13 +41,10 @@ class _LabeledNotaDisplayWidgetState extends State { textAlign: TextAlign.center, enabled: false, decoration: InputDecoration( - hintText: widget._hint, - disabledBorder: MainTheme.theme.inputDecorationTheme.border! - .copyWith(borderSide: BorderSide(color: Colors.transparent)), - ), - keyboardType: TextInputType.numberWithOptions( - decimal: true, + hintText: widget.hint, + disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith(borderSide: BorderSide(color: Colors.transparent)), ), + keyboardType: TextInputType.numberWithOptions(decimal: true), ), ), ], diff --git a/lib/widgets/asignatura/notas_tab/notas_display.dart b/lib/widgets/asignatura/notas_tab/notas_display.dart index 89b0d71..2190aa0 100644 --- a/lib/widgets/asignatura/notas_tab/notas_display.dart +++ b/lib/widgets/asignatura/notas_tab/notas_display.dart @@ -9,7 +9,8 @@ class NotasDisplayWidget extends StatelessWidget { final String? estado; final Color? colorPorEstado; - NotasDisplayWidget({ + const NotasDisplayWidget({ + super.key, this.notaFinal, this.notaExamen, this.notaPresentacion, diff --git a/lib/widgets/calculadora_notas/editar_notas_widget.dart b/lib/widgets/calculadora_notas/editar_notas_widget.dart index 1dabda8..4a423e3 100644 --- a/lib/widgets/calculadora_notas/editar_notas_widget.dart +++ b/lib/widgets/calculadora_notas/editar_notas_widget.dart @@ -4,14 +4,14 @@ import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/calculadora_notas/modo_simulacion_widget.dart'; import 'package:mi_utem/widgets/calculadora_notas/notas_calculadora_widget.dart'; -import 'package:watch_it/watch_it.dart'; -class EditarNotasWidget extends StatelessWidget with WatchItMixin { +class EditarNotasWidget extends StatelessWidget { final List partialGrades; final List gradeTextFieldControllers; final List percentageTextFieldControllers; final Function() onAddGrade; final Function(int) onRemoveGrade; + final Function(int, IEvaluacion) onChanged; const EditarNotasWidget({ super.key, @@ -20,6 +20,7 @@ class EditarNotasWidget extends StatelessWidget with WatchItMixin { required this.percentageTextFieldControllers, required this.onAddGrade, required this.onRemoveGrade, + required this.onChanged, }); @override @@ -37,6 +38,7 @@ class EditarNotasWidget extends StatelessWidget with WatchItMixin { gradeTextFieldControllers: gradeTextFieldControllers, percentageTextFieldControllers: percentageTextFieldControllers, onRemoveGrade: onRemoveGrade, + onChanged: onChanged, ), const SizedBox(height: 16), TextButton( diff --git a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart index 39c1b0a..88a1db4 100644 --- a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:watch_it/watch_it.dart'; -class NotaExamenDisplayWidget extends StatelessWidget with WatchItMixin{ +class NotaExamenDisplayWidget extends StatelessWidget { const NotaExamenDisplayWidget({ super.key, @@ -12,9 +12,7 @@ class NotaExamenDisplayWidget extends StatelessWidget with WatchItMixin{ @override Widget build(BuildContext context) { - final examGradeTextFieldController = watchValue((CalculatorController controller) => controller.examGradeTextFieldController); - final canTakeExam = watchValue((CalculatorController controller) => controller.canTakeExam); - final minimumRequiredExamGrade = watchValue((CalculatorController controller) => controller.minimumRequiredExamGrade); + CalculatorController _calculatorController = Get.find(); return Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -25,14 +23,14 @@ class NotaExamenDisplayWidget extends StatelessWidget with WatchItMixin{ Container( width: 80, margin: const EdgeInsets.only(left: 15), - child: TextField( - controller: examGradeTextFieldController, + child: Obx(() => TextField( + controller: _calculatorController.examGradeTextFieldController.value, textAlign: TextAlign.center, - onChanged: (String value) => di.get().setExamGrade(double.tryParse(value.replaceAll(",", "."))), - enabled: canTakeExam, + onChanged: (String value) => _calculatorController.setExamGrade(double.tryParse(value.replaceAll(",", "."))), + enabled: _calculatorController.canTakeExam.value, decoration: InputDecoration( - hintText: minimumRequiredExamGrade?.toStringAsFixed(1) ?? "--", - filled: canTakeExam, + hintText: _calculatorController.minimumRequiredExamGrade.value?.toStringAsFixed(1) ?? "--", + filled: _calculatorController.canTakeExam.value, fillColor: Colors.grey.withOpacity(0.2), disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( @@ -65,7 +63,7 @@ class NotaExamenDisplayWidget extends StatelessWidget with WatchItMixin{ return input; }), ], - ), + )), ), ], ); diff --git a/lib/widgets/calculadora_notas/nota_final_display_widget.dart b/lib/widgets/calculadora_notas/nota_final_display_widget.dart index 4b4eaad..76d386e 100644 --- a/lib/widgets/calculadora_notas/nota_final_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_final_display_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; -import 'package:watch_it/watch_it.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; -class NotaFinalDisplayWidget extends StatelessWidget with WatchItMixin { +class NotaFinalDisplayWidget extends StatelessWidget { const NotaFinalDisplayWidget({ super.key, @@ -10,16 +10,16 @@ class NotaFinalDisplayWidget extends StatelessWidget with WatchItMixin { @override Widget build(BuildContext context) { - final calculatedFinalGrade = watchValue((CalculatorController controller) => controller.calculatedFinalGrade); + CalculatorController _calculatorController = Get.find(); return Column( children: [ - Text(calculatedFinalGrade?.toStringAsFixed(1) ?? "--", + Obx(() => Text(_calculatorController.calculatedFinalGrade.value?.toStringAsFixed(1) ?? "--", style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, ), - ) + )), ], ); } diff --git a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart index cc8c02e..f654efb 100644 --- a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:watch_it/watch_it.dart'; -class NotaPresentacionDisplayWidget extends StatelessWidget with WatchItMixin { +class NotaPresentacionDisplayWidget extends StatelessWidget { const NotaPresentacionDisplayWidget({ super.key, }); @override Widget build(BuildContext context) { - final calculatedPresentationGrade = watchValue((CalculatorController controller) => controller.calculatedPresentationGrade); + final _calculatorController = Get.find(); return Row( children: [ const Text("Pres.", @@ -19,8 +19,8 @@ class NotaPresentacionDisplayWidget extends StatelessWidget with WatchItMixin { Container( width: 80, margin: const EdgeInsets.only(left: 15), - child: TextField( - controller: TextEditingController(text: calculatedPresentationGrade?.toStringAsFixed(1) ?? ""), + child: Obx(() => TextField( + controller: TextEditingController(text: _calculatorController.calculatedPresentationGrade.value?.toStringAsFixed(1) ?? ""), textAlign: TextAlign.center, enabled: false, decoration: InputDecoration( @@ -32,7 +32,7 @@ class NotaPresentacionDisplayWidget extends StatelessWidget with WatchItMixin { ), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), + )), ), ], ); diff --git a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart index 760782d..1df253e 100644 --- a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart +++ b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart @@ -2,16 +2,15 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; -import 'package:watch_it/watch_it.dart'; -class NotasCalculadoraWidget extends StatelessWidget with WatchItMixin { +class NotasCalculadoraWidget extends StatelessWidget { final List partialGrades; final List gradeTextFieldControllers; final List percentageTextFieldControllers; final Function(int) onRemoveGrade; + final Function(int, IEvaluacion) onChanged; const NotasCalculadoraWidget({ super.key, @@ -19,6 +18,7 @@ class NotasCalculadoraWidget extends StatelessWidget with WatchItMixin { required this.gradeTextFieldControllers, required this.percentageTextFieldControllers, required this.onRemoveGrade, + required this.onChanged, }); @override @@ -31,7 +31,7 @@ class NotasCalculadoraWidget extends StatelessWidget with WatchItMixin { editable: true, gradeController: gradeTextFieldControllers[idx], percentageController: percentageTextFieldControllers[idx], - onChanged: (evaluacion) => di.get().updateGradeAt(idx, evaluacion), + onChanged: (evaluacion) => onChanged(idx, evaluacion), onDelete: () { AnalyticsService.logEvent("calculator_delete_grade"); onRemoveGrade(idx); diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 9d8ea54..2a7ca50 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:badges/badges.dart' as badge; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; @@ -9,13 +10,12 @@ import 'package:mi_utem/screens/credencial_screen.dart'; import 'package:mi_utem/screens/horario/horario_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/usuario_screen.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; -import 'package:watch_it/watch_it.dart'; class CustomDrawer extends StatelessWidget { @@ -49,7 +49,7 @@ class CustomDrawer extends StatelessWidget { @override Widget build(BuildContext context) { - final _authService = di.get(); + final _authService = Get.find(); return Drawer( semanticLabel: "Abrir menú", @@ -139,9 +139,7 @@ class CustomDrawer extends StatelessWidget { ListTile( leading: const Icon(Mdi.closeCircle), title: const Text('Cerrar sesión'), - onTap: () async { - await _authService.logout(context); - }, + onTap: () async => await _authService.logout(context), ), ], ), diff --git a/lib/widgets/horario/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart index fd7d1ee..236cb4b 100644 --- a/lib/widgets/horario/bloque_ramo_card.dart +++ b/lib/widgets/horario/bloque_ramo_card.dart @@ -1,10 +1,10 @@ import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/horario_controller.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:watch_it/watch_it.dart'; class ClassBlockCard extends StatelessWidget { final BloqueHorario? block; @@ -109,7 +109,7 @@ class _ClassBlock extends StatelessWidget { @override Widget build(BuildContext context) { - final _controller = di.get(); + HorarioController _controller = Get.find(); return Container( decoration: BoxDecoration( @@ -121,8 +121,7 @@ class _ClassBlock extends StatelessWidget { child: InkWell( borderRadius: BorderRadius.circular(15), onTap: onTap != null ? () => onTap?.call(block) : null, - onLongPress: - onLongPress != null ? () => onLongPress?.call(block) : null, + onLongPress: onLongPress != null ? () => onLongPress?.call(block) : null, child: Column( children: [ HorarioText.classCode( diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index 0f6fef2..3568ef2 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/credential.dart'; +import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services_new/interfaces/auth_service.dart' as NewAuthService; -import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/snackbar.dart'; -import 'package:watch_it/watch_it.dart'; class LoginButton extends StatefulWidget { @@ -36,8 +36,8 @@ class LoginButton extends StatefulWidget { class _LoginButtonState extends State { - final _authService = di.get(); - final _credentialsService = di.get(); + final _authService = Get.find(); + final _credentialsService = Get.find(); @override Widget build(BuildContext context) => TextButton( diff --git a/lib/widgets/login_screen/login_form.dart b/lib/widgets/login_screen/login_form.dart index 3fb341e..d9f15b7 100644 --- a/lib/widgets/login_screen/login_form.dart +++ b/lib/widgets/login_screen/login_form.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; import 'package:mi_utem/services/update_service.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/credentials_repository.dart'; import 'package:mi_utem/widgets/login_screen/creditos_app.dart'; import 'package:mi_utem/widgets/login_screen/formulario_credenciales.dart'; import 'package:mi_utem/widgets/login_screen/login_button.dart'; -import 'package:watch_it/watch_it.dart'; class LoginForm extends StatefulWidget { final BoxConstraints constraints; @@ -25,7 +25,7 @@ class _LoginFormState extends State { final TextEditingController _correoController = TextEditingController(); final TextEditingController _contraseniaController = TextEditingController(); - final _credentialService = di.get(); + final _credentialService = Get.find(); @override void initState() { diff --git a/lib/widgets/nota_list_item.dart b/lib/widgets/nota_list_item.dart index 1c1dd9a..e89b2e0 100644 --- a/lib/widgets/nota_list_item.dart +++ b/lib/widgets/nota_list_item.dart @@ -1,12 +1,12 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; -import 'package:mi_utem/services_new/interfaces/controllers/calculator_controller.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:watch_it/watch_it.dart'; -class NotaListItem extends StatelessWidget with WatchItMixin { +class NotaListItem extends StatelessWidget { final IEvaluacion evaluacion; final bool editable; final TextEditingController? gradeController; @@ -36,12 +36,8 @@ class NotaListItem extends StatelessWidget with WatchItMixin { ); final showSuggestedGrade = editable; + CalculatorController calculatorController = Get.find(); - final suggestedGrade = watchValue((CalculatorController controller) => controller.suggestedGrade); - final suggestedPercentage = watchValue((CalculatorController controller) => controller.suggestedPercentage); - - - final hintText = showSuggestedGrade ? (suggestedGrade?.toStringAsFixed(0) ?? "--") : "--"; return Flex( direction: Axis.horizontal, @@ -57,7 +53,7 @@ class NotaListItem extends StatelessWidget with WatchItMixin { Flexible( flex: 3, child: Center( - child: TextField( + child: Obx(() => TextField( controller: gradeController ?? defaultGradeController, enabled: editable, onChanged: (String value) { @@ -70,7 +66,7 @@ class NotaListItem extends StatelessWidget with WatchItMixin { }, textAlign: TextAlign.center, decoration: InputDecoration( - hintText: hintText, + hintText: showSuggestedGrade ? (calculatorController.suggestedGrade.value?.toStringAsFixed(0) ?? "--") : "--", disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( color: Colors.transparent, @@ -102,14 +98,14 @@ class NotaListItem extends StatelessWidget with WatchItMixin { return input; }), ], - ), + )), ), ), SizedBox(width: 16), Flexible( flex: 4, child: Center( - child: TextField( + child: Obx(() => TextField( controller: percentageController ?? defaultPercentageController, textAlign: TextAlign.center, onChanged: (String value) { @@ -120,7 +116,7 @@ class NotaListItem extends StatelessWidget with WatchItMixin { }, enabled: editable, decoration: InputDecoration( - hintText: suggestedPercentage?.toStringAsFixed(0) ?? "Peso", + hintText: calculatorController.suggestedPercentage.value?.toStringAsFixed(0) ?? "Peso", suffixText: "%", disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( @@ -148,20 +144,17 @@ class NotaListItem extends StatelessWidget with WatchItMixin { }), ], - ), + )), ), ), SizedBox(width: 20), - if (onDelete != null) - GestureDetector( - onTap: () { - onDelete?.call(); - }, - child: Icon( - Icons.delete, - color: Theme.of(context).primaryColor, - ), - ) + if (onDelete != null)GestureDetector( + onTap: () => onDelete?.call(), + child: Icon( + Icons.delete, + color: Theme.of(context).primaryColor, + ), + ), ], ); } diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index dea38e2..6460de9 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -1,12 +1,12 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/noticias_repository.dart'; +import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/noticias/noticia_card_widget.dart'; -import 'package:watch_it/watch_it.dart'; class NoticiasCarruselWidget extends StatelessWidget { @@ -29,7 +29,7 @@ class NoticiasCarruselWidget extends StatelessWidget { ), const SizedBox(height: 10), FutureBuilder( - future: di.get().getNoticias(), + future: Get.find().getNoticias(), builder: (context, AsyncSnapshot?> snapshot) { if (snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException) : CustomException.custom("No pudimos obtener las noticias."); diff --git a/lib/widgets/permisos_section.dart b/lib/widgets/permisos_section.dart index bc450ff..2f5fdd4 100644 --- a/lib/widgets/permisos_section.dart +++ b/lib/widgets/permisos_section.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; -import 'package:mi_utem/services_new/interfaces/repositories/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/permiso_card.dart'; -import 'package:watch_it/watch_it.dart'; class PermisosCovidSection extends StatelessWidget { @@ -32,7 +32,7 @@ class PermisosCovidSection extends StatelessWidget { SizedBox( height: 155, child: FutureBuilder?>( - future: di.get().getPermisos(), + future: Get.find().getPermisos(), builder: (context, snapshot) { if(snapshot.connectionState == ConnectionState.waiting) { return LoadingIndicator( diff --git a/pubspec.lock b/pubspec.lock index 1dc5ce6..9a4862f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -745,30 +745,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.4.0" - functional_listener: - dependency: transitive - description: - name: functional_listener - sha256: "026d1bd4f66367f11d9ec9f1f1ddb42b89e4484b356972c76d983266cf82f33f" - url: "https://pub.dev" - source: hosted - version: "2.3.1" get: - dependency: transitive + dependency: "direct main" description: name: get sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" url: "https://pub.dev" source: hosted version: "4.6.5" - get_it: - dependency: "direct main" - description: - name: get_it - sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" - url: "https://pub.dev" - source: hosted - version: "7.6.0" get_storage: dependency: "direct main" description: @@ -1017,14 +1001,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.20" - listenable_collections: - dependency: "direct main" - description: - name: listenable_collections - sha256: "49dd3be0955d8c109580184b5f43ee88a269df64baba20a35c16fd9ddd9fb62e" - url: "https://pub.dev" - source: hosted - version: "1.0.0" logger: dependency: "direct main" description: @@ -1678,14 +1654,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0+2" - watch_it: - dependency: "direct main" - description: - name: watch_it - sha256: d2f69c17c927d947d7faa90d6fa064fe63b73f7c49eac2d3dd6de3255eb18081 - url: "https://pub.dev" - source: hosted - version: "1.0.2" watcher: dependency: transitive description: @@ -1727,5 +1695,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index abc3c6c..c0cd2b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,11 +74,9 @@ dependencies: screenshot: ^1.3.0 extended_image: ^7.0.2 cached_network_image: ^3.2.3 - get_it: ^7.6.0 - watch_it: ^1.0.2 extended_masked_text: ^2.3.1 crypto: ^3.0.3 - listenable_collections: ^1.0.0 + get: ^4.6.5 dependency_overrides: qr: ^3.0.0 From 1ecb79ba29c4da273229df65d3ed87be0e6d30c1 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:33:43 -0300 Subject: [PATCH 042/194] patch: arreglado error en auth_service --- lib/services/implementations/auth_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/implementations/auth_service.dart b/lib/services/implementations/auth_service.dart index c749456..7d73d0f 100644 --- a/lib/services/implementations/auth_service.dart +++ b/lib/services/implementations/auth_service.dart @@ -50,7 +50,7 @@ class AuthServiceImplementation implements AuthService { } try { - final token = _authRepository.refresh(token: userToken, credentials: credentials); + final token = await _authRepository.refresh(token: userToken, credentials: credentials); final userJson = user.toJson(); userJson["token"] = token; From e8a15f2a4d5bc7307d816da8185c5d7d50d487eb Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:46:34 -0300 Subject: [PATCH 043/194] patch: arreglado error al cargar notas Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../calculator_controller.dart | 16 +++--- lib/screens/calculadora_notas_screen.dart | 19 ++----- .../editar_notas_widget.dart | 28 +++------- .../notas_calculadora_widget.dart | 53 +++++++++---------- 4 files changed, 42 insertions(+), 74 deletions(-) diff --git a/lib/controllers/implementations/calculator_controller.dart b/lib/controllers/implementations/calculator_controller.dart index c63cca5..f8ad7ce 100644 --- a/lib/controllers/implementations/calculator_controller.dart +++ b/lib/controllers/implementations/calculator_controller.dart @@ -1,8 +1,8 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; class CalculatorControllerImplementation implements CalculatorController { @@ -38,7 +38,7 @@ class CalculatorControllerImplementation implements CalculatorController { /* Nota del examen */ @override - Rx examGrade = null.obs; + Rx examGrade = Rx(null); /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ @override @@ -48,10 +48,10 @@ class CalculatorControllerImplementation implements CalculatorController { RxBool freeEditable = false.obs; @override - Rx calculatedFinalGrade = null.obs; + Rx calculatedFinalGrade = Rx(null); @override - Rx calculatedPresentationGrade = null.obs; + Rx calculatedPresentationGrade = Rx(null); @override Rx amountOfPartialGradesWithoutGrade = 0.obs; @@ -66,7 +66,7 @@ class CalculatorControllerImplementation implements CalculatorController { RxBool canTakeExam = false.obs; @override - Rx minimumRequiredExamGrade = null.obs; + Rx minimumRequiredExamGrade = Rx(null); @override RxDouble percentageOfPartialGrades = 0.0.obs; @@ -78,10 +78,10 @@ class CalculatorControllerImplementation implements CalculatorController { RxBool hasMissingPercentage = false.obs; @override - Rx suggestedPercentage = null.obs; + Rx suggestedPercentage = Rx(null); @override - Rx suggestedPresentationGrade = null.obs; + Rx suggestedPresentationGrade = Rx(null); @override RxDouble percentageWithoutGrade = 0.0.obs; @@ -90,7 +90,7 @@ class CalculatorControllerImplementation implements CalculatorController { RxBool hasCorrectPercentage = false.obs; @override - Rx suggestedGrade = null.obs; + Rx suggestedGrade = Rx(null); @override void updateWithGrades(Grades grades) { diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index 12d27b0..0faea1d 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/widgets/calculadora_notas/display_notas_widget.dart'; import 'package:mi_utem/widgets/calculadora_notas/editar_notas_widget.dart'; @@ -15,11 +14,9 @@ class CalculadoraNotasScreen extends StatefulWidget { class _CalculadoraNotasScreenState extends State { - CalculatorController _controller = Get.find(); - @override void initState() { - _controller.makeEditable(); + Get.find().makeEditable(); super.initState(); } @@ -32,18 +29,8 @@ class _CalculadoraNotasScreenState extends State { body: ListView( padding: const EdgeInsets.all(10), children: [ - DisplayNotasWidget(), - Obx(() => EditarNotasWidget( - partialGrades: _controller.partialGrades, - gradeTextFieldControllers: _controller.gradeTextFieldControllers, - percentageTextFieldControllers: _controller.percentageTextFieldControllers, - onAddGrade: () => _controller.addGrade(IEvaluacion( - nota: null, - porcentaje: null, - )), - onRemoveGrade: (idx) => _controller.removeGradeAt(idx), - onChanged: (idx, evaluacion) => _controller.updateGradeAt(idx, evaluacion), - )), + const DisplayNotasWidget(), + const EditarNotasWidget(), ], ), ); diff --git a/lib/widgets/calculadora_notas/editar_notas_widget.dart b/lib/widgets/calculadora_notas/editar_notas_widget.dart index 4a423e3..f8d82aa 100644 --- a/lib/widgets/calculadora_notas/editar_notas_widget.dart +++ b/lib/widgets/calculadora_notas/editar_notas_widget.dart @@ -1,26 +1,15 @@ -import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/calculadora_notas/modo_simulacion_widget.dart'; import 'package:mi_utem/widgets/calculadora_notas/notas_calculadora_widget.dart'; class EditarNotasWidget extends StatelessWidget { - final List partialGrades; - final List gradeTextFieldControllers; - final List percentageTextFieldControllers; - final Function() onAddGrade; - final Function(int) onRemoveGrade; - final Function(int, IEvaluacion) onChanged; const EditarNotasWidget({ super.key, - required this.partialGrades, - required this.gradeTextFieldControllers, - required this.percentageTextFieldControllers, - required this.onAddGrade, - required this.onRemoveGrade, - required this.onChanged, }); @override @@ -33,18 +22,15 @@ class EditarNotasWidget extends StatelessWidget { padding: EdgeInsets.all(20), child: Column( children: [ - NotasCalculadoraWidget( - partialGrades: partialGrades, - gradeTextFieldControllers: gradeTextFieldControllers, - percentageTextFieldControllers: percentageTextFieldControllers, - onRemoveGrade: onRemoveGrade, - onChanged: onChanged, - ), + const NotasCalculadoraWidget(), const SizedBox(height: 16), TextButton( onPressed: () { AnalyticsService.logEvent("calculator_add_grade"); - onAddGrade(); + Get.find().addGrade(IEvaluacion( + nota: null, + porcentaje: null, + )); }, child: const Text("Agregar nota"), ), diff --git a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart index 1df253e..0b71f51 100644 --- a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart +++ b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart @@ -1,42 +1,37 @@ -import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; class NotasCalculadoraWidget extends StatelessWidget { - final List partialGrades; - final List gradeTextFieldControllers; - final List percentageTextFieldControllers; - final Function(int) onRemoveGrade; - final Function(int, IEvaluacion) onChanged; - const NotasCalculadoraWidget({ super.key, - required this.partialGrades, - required this.gradeTextFieldControllers, - required this.percentageTextFieldControllers, - required this.onRemoveGrade, - required this.onChanged, }); + @override - Widget build(BuildContext context) => ListView.separated( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - separatorBuilder: (context, index) => const SizedBox(height: 10), - itemBuilder: (context, idx) => NotaListItem( - evaluacion: IEvaluacion.fromRemote(partialGrades[idx]), - editable: true, - gradeController: gradeTextFieldControllers[idx], - percentageController: percentageTextFieldControllers[idx], - onChanged: (evaluacion) => onChanged(idx, evaluacion), - onDelete: () { - AnalyticsService.logEvent("calculator_delete_grade"); - onRemoveGrade(idx); - }, - ), - itemCount: partialGrades.length, - ); + Widget build(BuildContext context) { + final CalculatorController calculatorController = Get.find(); + + return Obx(() => ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, idx) => NotaListItem( + evaluacion: IEvaluacion.fromRemote(calculatorController.partialGrades[idx]), + editable: true, + gradeController: calculatorController.gradeTextFieldControllers[idx], + percentageController: calculatorController.percentageTextFieldControllers[idx], + onChanged: (evaluacion) => calculatorController.updateGradeAt(idx, evaluacion), + onDelete: () { + AnalyticsService.logEvent("calculator_delete_grade"); + calculatorController.removeGradeAt(idx); + }, + ), + itemCount: calculatorController.partialGrades.length, + )); + } } From 880c5a3aa8d74d7e2e06415c716dcbf4abcc80c9 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:47:59 -0300 Subject: [PATCH 044/194] patch: parchado constructor del archivo Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/horario/bloque_ramo_card.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/horario/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart index 236cb4b..838a155 100644 --- a/lib/widgets/horario/bloque_ramo_card.dart +++ b/lib/widgets/horario/bloque_ramo_card.dart @@ -14,13 +14,13 @@ class ClassBlockCard extends StatelessWidget { final Color textColor; ClassBlockCard({ - Key? key, + super.key, required this.block, required this.width, required this.height, this.internalMargin = 0, this.textColor = Colors.white, - }) : super(key: key); + }); @override Widget build(BuildContext context) { From e417c4414284e692be353afb9bea93bb3f05b698 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 27 Mar 2024 19:04:06 -0300 Subject: [PATCH 045/194] feat: agregado onbboarding y parchado algunos archivos Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/main.dart | 2 +- .../preferences_repository.dart | 36 ++++ .../interfaces/preferences_repository.dart | 30 ++++ lib/screens/main_screen.dart | 2 +- .../onboarding/notifications_screen.dart | 127 ++++++++++++++ lib/screens/onboarding/set_alias_screen.dart | 161 ++++++++++++++++++ lib/screens/onboarding/welcome_screen.dart | 111 ++++++++++++ lib/screens/splash_screen.dart | 8 +- lib/{services => }/service_manager.dart | 6 +- .../implementations/auth_service.dart | 19 ++- lib/services/notification_service.dart | 4 +- lib/widgets/gradient_background.dart | 29 ++++ lib/widgets/login_screen/background.dart | 17 +- lib/widgets/login_screen/login_button.dart | 18 +- pubspec.yaml | 1 + 15 files changed, 543 insertions(+), 28 deletions(-) create mode 100644 lib/repositories/implementations/preferences_repository.dart create mode 100644 lib/repositories/interfaces/preferences_repository.dart create mode 100644 lib/screens/onboarding/notifications_screen.dart create mode 100644 lib/screens/onboarding/set_alias_screen.dart create mode 100644 lib/screens/onboarding/welcome_screen.dart rename lib/{services => }/service_manager.dart (90%) create mode 100644 lib/widgets/gradient_background.dart diff --git a/lib/main.dart b/lib/main.dart index f3343fd..341dacb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,9 +8,9 @@ import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/screens/splash_screen.dart'; +import 'package:mi_utem/service_manager.dart'; import 'package:mi_utem/services/background_service.dart'; import 'package:mi_utem/services/notification_service.dart'; -import 'package:mi_utem/services/service_manager.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/lib/repositories/implementations/preferences_repository.dart b/lib/repositories/implementations/preferences_repository.dart new file mode 100644 index 0000000..f847fab --- /dev/null +++ b/lib/repositories/implementations/preferences_repository.dart @@ -0,0 +1,36 @@ +import 'package:mi_utem/config/secure_storage.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; + +class PreferencesRepositoryImplementation extends PreferencesRepository { + + @override + Future hasAlias() async => await secureStorage.containsKey(key: 'alias'); + + @override + Future setAlias(String? alias) async => await secureStorage.write(key: 'alias', value: alias); + + @override + Future getAlias() async => await secureStorage.read(key: 'alias'); + + @override + Future hasLastLogin() async => await secureStorage.containsKey(key: 'last_login'); + + @override + Future setLastLogin(DateTime? lastLogin) async => await secureStorage.write(key: 'last_login', value: lastLogin?.toIso8601String()); + + @override + Future getLastLogin() async { + final lastLogin = await secureStorage.read(key: 'last_login'); + return lastLogin != null ? DateTime.parse(lastLogin) : null; + } + + @override + Future hasCompletedOnboarding() async => await secureStorage.containsKey(key: 'onboarding_step') && await getOnboardingStep() == 'complete'; + + @override + Future setOnboardingStep(String? step) async => await secureStorage.write(key: 'onboarding_step', value: step); + + @override + Future getOnboardingStep() async => await secureStorage.read(key: 'onboarding_step'); + +} \ No newline at end of file diff --git a/lib/repositories/interfaces/preferences_repository.dart b/lib/repositories/interfaces/preferences_repository.dart new file mode 100644 index 0000000..f37db00 --- /dev/null +++ b/lib/repositories/interfaces/preferences_repository.dart @@ -0,0 +1,30 @@ +abstract class PreferencesRepository { + + /* Revisa si el usuario tiene un alias guardado */ + Future hasAlias(); + + /* Guarda el alias del usuario */ + Future setAlias(String? alias); + + /* Obtiene el alias del usuario */ + Future getAlias(); + + /* Revisa si el usuario tiene último inicio de sesión */ + Future hasLastLogin(); + + /* Guarda el último inicio de sesión */ + Future setLastLogin(DateTime? lastLogin); + + /* Obtiene el último inicio de sesión */ + Future getLastLogin(); + + /* Revisa si ha hecho el onboarding */ + Future hasCompletedOnboarding(); + + /* Guarda el paso actual del onboarding */ + Future setOnboardingStep(String? step); + + /* Obtiene el paso actual del onboarding */ + Future getOnboardingStep(); + +} \ No newline at end of file diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 1420feb..7d4c695 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -22,7 +22,7 @@ import "package:mi_utem/widgets/pull_to_refresh.dart"; import "package:mi_utem/widgets/quick_menu_section.dart"; class MainScreen extends StatefulWidget { - MainScreen({ + const MainScreen({ super.key, }); diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart new file mode 100644 index 0000000..5cb1e0f --- /dev/null +++ b/lib/screens/onboarding/notifications_screen.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/screens/main_screen.dart'; +import 'package:mi_utem/services/notification_service.dart'; +import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/widgets/gradient_background.dart'; + +class NotificationsScreen extends StatefulWidget { + const NotificationsScreen({super.key}); + + @override + State createState() => _NotificationsScreenState(); +} + +class _NotificationsScreenState extends State { + + PreferencesRepository _preferencesRepository = Get.find(); + bool _hasAllowedNotifications = false; + + @override + void initState() { + _preferencesRepository.setOnboardingStep("notifications"); + NotificationService.hasAllowedNotifications().then((value) { + if(value) { + setState(() => _hasAllowedNotifications = value); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) => GradientBackground( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + GestureDetector( + onTap: () => setState(() => _hasAllowedNotifications = !_hasAllowedNotifications), + child: Icon(Icons.notifications_active_outlined, size: 100, color: Colors.white), + ), + const SizedBox(height: 50), + Column( + children: [ + Text(_hasAllowedNotifications ? "Muchas Gracias!" : "Permítenos avisarte", + style: const TextStyle( + color: Colors.white, + fontSize: 26, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + Text("Olvídate sobre revisar la app a cada rato, ${_hasAllowedNotifications ? "te avisaremos" : "permítenos avisarte"} cuando haya novedades!", + style: TextStyle( + color: Colors.white, + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + if(!_hasAllowedNotifications) FilledButton( + onPressed: () { + NotificationService.requestUserPermissionIfNecessary(context).then((value) => setState(() => _hasAllowedNotifications = value)); + }, + style: ElevatedButton.styleFrom( + backgroundColor: MainTheme.primaryDarkColor, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text("Permitir Notificaciones", + style: TextStyle( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + ) + ], + ), + + const Spacer(), + + FilledButton( + onPressed: () async { + await _preferencesRepository.setOnboardingStep("complete"); + if(!context.mounted) { + return; + } + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); + }, + style: ElevatedButton.styleFrom( + backgroundColor: MainTheme.primaryColor, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), + maximumSize: const Size(double.infinity, 60), + minimumSize: const Size(double.infinity, 60), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text(_hasAllowedNotifications ? "Finalizar" : "No Gracias, Finalizar", + style: const TextStyle( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 25), + ], + ), + ), + ), + ], + ), + ); +} + diff --git a/lib/screens/onboarding/set_alias_screen.dart b/lib/screens/onboarding/set_alias_screen.dart new file mode 100644 index 0000000..3f1aa2e --- /dev/null +++ b/lib/screens/onboarding/set_alias_screen.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/screens/main_screen.dart'; +import 'package:mi_utem/screens/onboarding/notifications_screen.dart'; +import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/widgets/gradient_background.dart'; + +class SetAliasScreen extends StatefulWidget { + const SetAliasScreen({super.key}); + + @override + State createState() => _SetAliasScreenState(); +} + +class _SetAliasScreenState extends State { + + PreferencesRepository _preferencesRepository = Get.find(); + + final _focusNode = FocusNode(); + final _formKey = GlobalKey(); + final _aliasController = TextEditingController(); + + @override + void initState() { + _preferencesRepository.getOnboardingStep().then((step) { + if(step == null) { + _preferencesRepository.setOnboardingStep("alias"); + } else if (step == 'complete') { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => const MainScreen())); + } else { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => const NotificationsScreen())); + } + }); + _preferencesRepository.getAlias().then((value) => _aliasController.text = value ?? ''); + super.initState(); + } + + @override + Widget build(BuildContext context) => GradientBackground( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + Icon(Icons.tag_faces, size: 100, color: Colors.white), + const SizedBox(height: 50), + Column( + children: [ + const Text("Comencemos con tu Apodo", + style: TextStyle( + color: Colors.white, + fontSize: 26, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const Text("¿Cómo quieres que te llamemos?", + style: TextStyle( + color: Colors.white, + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Form( + key: _formKey, + child: TextFormField( + focusNode: _focusNode, + controller: _aliasController, + decoration: InputDecoration( + errorBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.red)), + enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.white)), + focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.white)), + focusedErrorBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.red)), + hintText: "Juanin", + labelText: "Apodo", + labelStyle: const TextStyle( + color: Colors.white, + ), + hintStyle: const TextStyle( + color: Colors.grey, + ), + suffixIcon: _aliasController.text.isNotEmpty == true ? GestureDetector ( + onTap: () => _aliasController.clear(), + behavior: HitTestBehavior.opaque, + child: IconButton( + icon: const Icon(Icons.clear, color: Colors.white), + onPressed: () => _aliasController.clear(), + ), + ) : null, + ), + keyboardType: TextInputType.name, + textCapitalization: TextCapitalization.words, + style: const TextStyle( + color: Colors.white, + ), + autofillHints: const [AutofillHints.nickname], + validator: (value) { + final val = value?.trim() ?? ""; + if(val.isEmpty) { + return "Debes ingresar un apodo."; + } + + if(val.length > 24) { + return "El apodo es muy largo."; + } + + return null; + }, + onTapOutside: (event) => FocusScope.of(context).unfocus(), + ), + ), + ], + ), + + const Spacer(), + + FilledButton( + onPressed: () { + if (_formKey.currentState?.validate() != true) { + return; + } + _preferencesRepository.setAlias(_aliasController.text); + Navigator.of(context).push(MaterialPageRoute(builder: (context) => const NotificationsScreen())); + }, + style: ElevatedButton.styleFrom( + backgroundColor: MainTheme.primaryColor, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), + maximumSize: const Size(double.infinity, 60), + minimumSize: const Size(double.infinity, 60), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text("Continuar", + style: TextStyle( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 25), + ], + ), + ), + ), + ], + ), + ); + +} \ No newline at end of file diff --git a/lib/screens/onboarding/welcome_screen.dart b/lib/screens/onboarding/welcome_screen.dart new file mode 100644 index 0000000..6a826a2 --- /dev/null +++ b/lib/screens/onboarding/welcome_screen.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/screens/main_screen.dart'; +import 'package:mi_utem/screens/onboarding/set_alias_screen.dart'; +import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/widgets/gradient_background.dart'; + +class WelcomeScreen extends StatefulWidget { + const WelcomeScreen({super.key}); + + @override + State createState() => _WelcomeScreenState(); +} + +class _WelcomeScreenState extends State { + + PreferencesRepository _preferencesRepository = Get.find(); + + @override + void initState() { + _preferencesRepository.getOnboardingStep().then((step) { + if(step == null) { + return; + } else if (step == 'complete') { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => const MainScreen())); + } else { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => const SetAliasScreen())); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) => GradientBackground( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + Container( + width: 100, + height: 100, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/launcher_icons/prod/full_icon.png"), + fit: BoxFit.contain, + ), + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + ), + const SizedBox(height: 30), + Column( + children: [ + Text("Te damos la bienvenida a Mi UTEM", + style: TextStyle( + color: Colors.white, + fontSize: 30, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + Text("¡Estás a unos pasos de disfrutar las funcionalidades de Mi UTEM!", + style: TextStyle( + color: Colors.white, + fontSize: 20, + ), + textAlign: TextAlign.center, + ) + ], + ), + + const Spacer(), + + FilledButton( + onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (ctx) => const SetAliasScreen())), + style: ElevatedButton.styleFrom( + backgroundColor: MainTheme.primaryColor, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), + maximumSize: const Size(double.infinity, 60), + minimumSize: const Size(double.infinity, 60), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text("Comenzar", + style: TextStyle( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 25), + ], + ), + ), + ), + ], + ), + ); +} + diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index e5bd02e..bf88fee 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -6,11 +6,12 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; +import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; -import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -90,7 +91,6 @@ class _SplashScreenState extends State { ); return; } - await NotificationService.requestUserPermissionIfNecessary(context); final isLoggedIn = await _authService.isLoggedIn(); if(!isLoggedIn) { AnalyticsService.removeUser(); @@ -101,7 +101,9 @@ class _SplashScreenState extends State { } } Navigator.popUntil(context, (route) => route.isFirst); - Navigator.pushReplacement(context, MaterialPageRoute(builder: (ctx) => isLoggedIn ? MainScreen() : LoginScreen())); + + final hasCompletedOnboarding = await Get.find().hasCompletedOnboarding(); + Navigator.pushReplacement(context, MaterialPageRoute(builder: (ctx) => isLoggedIn ? (hasCompletedOnboarding ? MainScreen() : WelcomeScreen()) : LoginScreen())); }, ), ), diff --git a/lib/services/service_manager.dart b/lib/service_manager.dart similarity index 90% rename from lib/services/service_manager.dart rename to lib/service_manager.dart index 1b0450b..1ee8f79 100644 --- a/lib/services/service_manager.dart +++ b/lib/service_manager.dart @@ -11,6 +11,7 @@ import 'package:mi_utem/repositories/implementations/grades_repository.dart'; import 'package:mi_utem/repositories/implementations/horario_repository.dart'; import 'package:mi_utem/repositories/implementations/noticias_repository.dart'; import 'package:mi_utem/repositories/implementations/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/implementations/preferences_repository.dart'; import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; @@ -19,6 +20,7 @@ import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/services/implementations/auth_service.dart'; import 'package:mi_utem/services/implementations/carreras_service.dart'; import 'package:mi_utem/services/implementations/grades_service.dart'; @@ -27,7 +29,7 @@ import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/interfaces/grades_service.dart'; Future registerServices() async { - /* Repositorios (Para conectarse a la REST Api */ + /* Repositorios (Para conectarse a la REST Api o servicios locales) */ Get.lazyPut(() => AuthRepositoryImplementation()); Get.lazyPut(() => AsignaturasRepositoryImplementation()); Get.lazyPut(() => CredentialsRepositoryImplementation()); @@ -37,6 +39,8 @@ Future registerServices() async { Get.lazyPut(() => NoticiasRepositoryImplementation()); Get.lazyPut(() => HorarioRepositoryImplementation()); + Get.lazyPut(() => PreferencesRepositoryImplementation()); + /* Servicios (Para procesar datos REST) */ Get.lazyPut(() => AuthServiceImplementation()); diff --git a/lib/services/implementations/auth_service.dart b/lib/services/implementations/auth_service.dart index 7d73d0f..ffce755 100644 --- a/lib/services/implementations/auth_service.dart +++ b/lib/services/implementations/auth_service.dart @@ -9,17 +9,19 @@ import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/notification_service.dart'; class AuthServiceImplementation implements AuthService { + PreferencesRepository _preferencesRepository = Get.find(); AuthRepository _authRepository = Get.find(); CredentialsRepository _credentialsService = Get.find(); @override - Future isFirstTime() async => !(await secureStorage.containsKey(key: "last_login")); + Future isFirstTime() async => !(await _preferencesRepository.hasLastLogin()); @override Future isLoggedIn({ bool forceRefresh = false }) async { @@ -36,16 +38,16 @@ class AuthServiceImplementation implements AuthService { return false; } - final lastLogin = await secureStorage.read(key: "last_login"); - if(lastLogin == null) { + final hasLastLogin = await _preferencesRepository.hasLastLogin(); + if(!hasLastLogin) { logger.d("[AuthService#isLoggedIn]: no last login"); return false; } - final lastLoginDate = DateTime.fromMillisecondsSinceEpoch(int.parse(lastLogin)); final now = DateTime.now(); + final lastLoginDate = await _preferencesRepository.getLastLogin() ?? now; final difference = now.difference(lastLoginDate); - if(difference.inMinutes < 5 && !forceRefresh) { + if(difference.inMinutes < 5 && now != lastLoginDate && !forceRefresh) { return true; } @@ -55,7 +57,7 @@ class AuthServiceImplementation implements AuthService { final userJson = user.toJson(); userJson["token"] = token; await setUser(User.fromJson(userJson)); - await secureStorage.write(key: "last_login", value: "${DateTime.now().millisecondsSinceEpoch}"); + _preferencesRepository.setLastLogin(DateTime.now()); return true; } catch (e) { logger.e("[AuthService#isLoggedIn]: Error al refrescar token", e); @@ -74,13 +76,16 @@ class AuthServiceImplementation implements AuthService { final user = await _authRepository.auth(credentials: credentials); await setUser(user); - await secureStorage.write(key: "last_login", value: "${DateTime.now().millisecondsSinceEpoch}"); + _preferencesRepository.setLastLogin(DateTime.now()); } @override Future logout(BuildContext? context) async { setUser(null); _credentialsService.setCredentials(null); + _preferencesRepository.setOnboardingStep(null); + _preferencesRepository.setLastLogin(null); + _preferencesRepository.setAlias(null); if(context != null) { Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (ctx) => LoginScreen())); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 68d9f94..d25552d 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -67,8 +67,10 @@ class NotificationService { ); } + static Future hasAllowedNotifications() async => await notifications.isNotificationAllowed(); + static Future requestUserPermissionIfNecessary(BuildContext context) async { - bool isAllowed = await notifications.isNotificationAllowed(); + bool isAllowed = await hasAllowedNotifications(); if (!isAllowed) { isAllowed = await showDialog(context: context, builder: (ctx) => CustomAlertDialog( titulo: "Activa las notificaciones", diff --git a/lib/widgets/gradient_background.dart b/lib/widgets/gradient_background.dart new file mode 100644 index 0000000..169dcfb --- /dev/null +++ b/lib/widgets/gradient_background.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/themes/theme.dart'; + +class GradientBackground extends StatelessWidget { + final Widget child; + + const GradientBackground({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) => Scaffold( + body: Stack( + children: [ + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomLeft, + end: Alignment.topRight, + colors: [MainTheme.utemAzul, MainTheme.utemVerde], + ), + ), + ), + SafeArea(child: child), + ], + ), + ); +} \ No newline at end of file diff --git a/lib/widgets/login_screen/background.dart b/lib/widgets/login_screen/background.dart index 456436f..4e7934a 100644 --- a/lib/widgets/login_screen/background.dart +++ b/lib/widgets/login_screen/background.dart @@ -5,9 +5,9 @@ class LoginBackground extends StatefulWidget { final Widget child; const LoginBackground({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override _LoginBackgroundState createState() => _LoginBackgroundState(); @@ -18,15 +18,14 @@ class _LoginBackgroundState extends State { @override void initState() { - _controller = VideoPlayerController.asset( - 'assets/videos/login_bg.mp4', - videoPlayerOptions: VideoPlayerOptions( - mixWithOthers: true, - ), + _controller = VideoPlayerController.asset('assets/videos/login_bg.mp4', + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), )..setVolume(0) ..play() ..setLooping(true) - ..initialize(); + ..initialize().then((value) { + setState(() {}); // Esto debido a que hay veces que no inicia el video a menos que se haga un hot-reload + }); super.initState(); } @@ -49,7 +48,7 @@ class _LoginBackgroundState extends State { return Stack( alignment: Alignment.center, - children: [ + children: [ Container( decoration: backgroundDecoration, ), diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index 3568ef2..fefb0a3 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -4,7 +4,9 @@ import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/screens/main_screen.dart'; +import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; @@ -42,7 +44,7 @@ class _LoginButtonState extends State { @override Widget build(BuildContext context) => TextButton( onPressed: () => _login(context), - child: Text("Iniciar"), + child: Text("Iniciar Sesión"), ); Future _login(BuildContext context) async { @@ -98,11 +100,17 @@ class _LoginButtonState extends State { AnalyticsService.setUser(user); Navigator.of(context).popUntil((route) => route.isFirst); // Esto elimina todas las pantallas anteriores - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); // Y esto reemplaza la pantalla actual por la nueva, cosa de que no pueda "volver" al login a menos que cierre la sesión. - - if(isFirstTime) { - showDialog(context: context, builder: (ctx) => AcercaDialog()); + // Y esto reemplaza la pantalla actual por la nueva, cosa de que no pueda "volver" al login a menos que cierre la sesión. + PreferencesRepository preferencesRepository = Get.find(); + if(await preferencesRepository.hasCompletedOnboarding()) { + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); + if(isFirstTime) { + showDialog(context: context, builder: (ctx) => AcercaDialog()); + } + return; } + + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => WelcomeScreen())); } catch (e) { logger.e(e); Navigator.pop(context); diff --git a/pubspec.yaml b/pubspec.yaml index 9027d2d..6f7a466 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,4 +100,5 @@ flutter: - assets/animations/monito.flr - assets/animations/utem.flr - assets/images/sibutem.jpg + - assets/launcher_icons/prod/full_icon.png - .env From bced587f83c9e106081496238f12060dd22cf154 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 27 Mar 2024 22:39:39 -0300 Subject: [PATCH 046/194] =?UTF-8?q?patch:=20arreglado=20error=20al=20inici?= =?UTF-8?q?ar=20sesi=C3=B3n=20y=20desactivada=20actualizaci=C3=B3n=20de=20?= =?UTF-8?q?la=20app.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 15 +++++------ .../credentials_repository.dart | 4 +-- .../preferences_repository.dart | 25 +++++++++++-------- .../onboarding/notifications_screen.dart | 5 +--- lib/screens/splash_screen.dart | 5 +++- lib/services/update_service.dart | 12 ++++----- 6 files changed, 34 insertions(+), 32 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 341dacb..42ecdde 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,17 +28,14 @@ void main() async { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await Firebase.initializeApp(); await RemoteConfigService.initialize(); + await registerServices(); await NotificationService.initialize(); await BackgroundService.initAndStart(); - await registerServices(); - await SentryFlutter.init( - (options) { - options.dsn = Constants.sentryDsn; - options.attachScreenshot = true; - options.tracesSampleRate = 1.0; - }, - appRunner: () => runApp(MiUtem()), - ); + await SentryFlutter.init((options) { + options.dsn = Constants.sentryDsn; + options.attachScreenshot = true; + options.tracesSampleRate = 1.0; + }, appRunner: () => runApp(MiUtem())); } class MiUtem extends StatefulWidget { diff --git a/lib/repositories/implementations/credentials_repository.dart b/lib/repositories/implementations/credentials_repository.dart index c4dc798..47114d0 100644 --- a/lib/repositories/implementations/credentials_repository.dart +++ b/lib/repositories/implementations/credentials_repository.dart @@ -9,7 +9,7 @@ class CredentialsRepositoryImplementation implements CredentialsRepository { @override Future getCredentials() async { final data = await secureStorage.read(key: "credentials"); - if(data == null) { + if(data == null || data == "null") { return null; } @@ -20,6 +20,6 @@ class CredentialsRepositoryImplementation implements CredentialsRepository { Future hasCredentials() async => await secureStorage.containsKey(key: "credentials"); @override - Future setCredentials(Credentials? credential) async => await secureStorage.write(key: "credentials", value: credential.toString()); + Future setCredentials(Credentials? credential) async => await secureStorage.write(key: "credentials", value: credential != null ? credential.toString() : null); } \ No newline at end of file diff --git a/lib/repositories/implementations/preferences_repository.dart b/lib/repositories/implementations/preferences_repository.dart index f847fab..557751f 100644 --- a/lib/repositories/implementations/preferences_repository.dart +++ b/lib/repositories/implementations/preferences_repository.dart @@ -1,36 +1,41 @@ +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; class PreferencesRepositoryImplementation extends PreferencesRepository { + static const aliasKey = 'apodo'; + static const lastLoginKey = 'ultimo_login'; + static const onboardingStepKey = 'paso_onboarding'; @override - Future hasAlias() async => await secureStorage.containsKey(key: 'alias'); + Future hasAlias() async => await secureStorage.containsKey(key: aliasKey); @override - Future setAlias(String? alias) async => await secureStorage.write(key: 'alias', value: alias); + Future setAlias(String? alias) async => await secureStorage.write(key: aliasKey, value: alias); @override - Future getAlias() async => await secureStorage.read(key: 'alias'); + Future getAlias() async => await secureStorage.read(key: aliasKey); @override - Future hasLastLogin() async => await secureStorage.containsKey(key: 'last_login'); + Future hasLastLogin() async => await secureStorage.containsKey(key: lastLoginKey); @override - Future setLastLogin(DateTime? lastLogin) async => await secureStorage.write(key: 'last_login', value: lastLogin?.toIso8601String()); + Future setLastLogin(DateTime? lastLogin) async => await secureStorage.write(key: lastLoginKey, value: lastLogin?.toString()); @override Future getLastLogin() async { - final lastLogin = await secureStorage.read(key: 'last_login'); - return lastLogin != null ? DateTime.parse(lastLogin) : null; + final lastLogin = await secureStorage.read(key: lastLoginKey); + logger.d("Last login $lastLogin"); + return lastLogin != null ? DateTime.tryParse(lastLogin) : null; } @override - Future hasCompletedOnboarding() async => await secureStorage.containsKey(key: 'onboarding_step') && await getOnboardingStep() == 'complete'; + Future hasCompletedOnboarding() async => await secureStorage.containsKey(key: onboardingStepKey) && await getOnboardingStep() == 'complete'; @override - Future setOnboardingStep(String? step) async => await secureStorage.write(key: 'onboarding_step', value: step); + Future setOnboardingStep(String? step) async => await secureStorage.write(key: onboardingStepKey, value: step); @override - Future getOnboardingStep() async => await secureStorage.read(key: 'onboarding_step'); + Future getOnboardingStep() async => await secureStorage.read(key: onboardingStepKey); } \ No newline at end of file diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart index 5cb1e0f..9ad88b3 100644 --- a/lib/screens/onboarding/notifications_screen.dart +++ b/lib/screens/onboarding/notifications_screen.dart @@ -45,10 +45,7 @@ class _NotificationsScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ const Spacer(), - GestureDetector( - onTap: () => setState(() => _hasAllowedNotifications = !_hasAllowedNotifications), - child: Icon(Icons.notifications_active_outlined, size: 100, color: Colors.white), - ), + Icon(Icons.notifications_active_outlined, size: 100, color: Colors.white), const SizedBox(height: 50), Column( children: [ diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index bf88fee..728dca5 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; @@ -17,7 +18,9 @@ import 'package:mi_utem/widgets/snackbar.dart'; import 'package:package_info_plus/package_info_plus.dart'; class SplashScreen extends StatefulWidget { - SplashScreen({Key? key}) : super(key: key); + const SplashScreen({ + super.key, + }); @override _SplashScreenState createState() => _SplashScreenState(); diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index 6013ba0..c1b3488 100644 --- a/lib/services/update_service.dart +++ b/lib/services/update_service.dart @@ -50,11 +50,11 @@ class UpdateService { } */ Future _checkAndPerformUpdate() async { - if (Platform.isAndroid) { - final AppUpdateInfo appUpdateInfo = await InAppUpdate.checkForUpdate(); - if (appUpdateInfo.immediateUpdateAllowed) { - await InAppUpdate.performImmediateUpdate(); - } - } + // if (Platform.isAndroid) { + // final AppUpdateInfo appUpdateInfo = await InAppUpdate.checkForUpdate(); + // if (appUpdateInfo.immediateUpdateAllowed) { + // await InAppUpdate.performImmediateUpdate(); + // } + // } } } \ No newline at end of file From 8da542686ea5358747967a86c4b2bf267750f36e Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 27 Mar 2024 23:15:06 -0300 Subject: [PATCH 047/194] patch: parchado nombrado y constructor de algunas variables --- .../login_screen/formulario_credenciales.dart | 15 ++++++++---- lib/widgets/login_screen/login_button.dart | 24 +++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/widgets/login_screen/formulario_credenciales.dart b/lib/widgets/login_screen/formulario_credenciales.dart index d98ba75..0be83ff 100644 --- a/lib/widgets/login_screen/formulario_credenciales.dart +++ b/lib/widgets/login_screen/formulario_credenciales.dart @@ -4,10 +4,14 @@ import 'package:mi_utem/widgets/login_text_form_field.dart'; class FormularioCredenciales extends StatefulWidget { - final TextEditingController _correoController; - final TextEditingController _contraseniaController; + final TextEditingController correoController; + final TextEditingController contraseniaController; - FormularioCredenciales({ required TextEditingController correoController, required TextEditingController contraseniaController }) : _contraseniaController = contraseniaController, _correoController = correoController; + const FormularioCredenciales({ + super.key, + required this.correoController, + required this.contraseniaController, + }); @override State createState() => _FormularioCredencialesState(); @@ -17,10 +21,11 @@ class _FormularioCredencialesState extends State { @override Widget build(BuildContext context) => AutofillGroup( + onDisposeAction: AutofillContextAction.commit, child: Column( children: [ LoginTextFormField( - controller: widget._correoController, + controller: widget.correoController, hintText: 'usuario@utem.cl', labelText: 'Usuario/Correo UTEM', textCapitalization: TextCapitalization.none, @@ -39,7 +44,7 @@ class _FormularioCredencialesState extends State { }, ), LoginTextFormField( - controller: widget._contraseniaController, + controller: widget.contraseniaController, hintText: '• • • • • • • • •', labelText: 'Contraseña', textCapitalization: TextCapitalization.none, diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index fefb0a3..63ed202 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -17,19 +17,17 @@ import 'package:mi_utem/widgets/snackbar.dart'; class LoginButton extends StatefulWidget { - final TextEditingController _correoController; - final TextEditingController _contraseniaController; + final TextEditingController correoController; + final TextEditingController contraseniaController; - final GlobalKey _formKey; + final GlobalKey formKey; LoginButton({ - required TextEditingController correoController, - required TextEditingController contraseniaController, - required GlobalKey formKey, - }) : - _correoController = correoController, - _contraseniaController = contraseniaController, - _formKey = formKey; + super.key, + required this.correoController, + required this.contraseniaController, + required this.formKey, + }); @override _LoginButtonState createState() => _LoginButtonState(); @@ -48,8 +46,8 @@ class _LoginButtonState extends State { ); Future _login(BuildContext context) async { - final correo = widget._correoController.text; - final contrasenia = widget._contraseniaController.text; + final correo = widget.correoController.text; + final contrasenia = widget.contraseniaController.text; if (correo == "error@utem.cl") { showDialog(context: context, builder: (ctx) => MonkeyErrorDialog()); @@ -62,7 +60,7 @@ class _LoginButtonState extends State { return; } - if(widget._formKey.currentState?.validate() == false) { + if(widget.formKey.currentState?.validate() == false) { return; } From c98718412da642ff4a1cc6089dfb4f82db751e90 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:48:18 -0300 Subject: [PATCH 048/194] =?UTF-8?q?feat:=20agregado=20apodo=20donde=20se?= =?UTF-8?q?=20muestra=20el=20nombre=20*=20Ahora=20se=20muestra=20el=20apod?= =?UTF-8?q?o=20en=20donde=20antes=20se=20mostraba=20el=20nombre=20(pantall?= =?UTF-8?q?a=20inicial,=20menu=20lateral)=20*=20Se=20agrega=20secci=C3=B3n?= =?UTF-8?q?=20de=20alias=20al=20perfil.=20*=20Se=20repara=20un=20divisor?= =?UTF-8?q?=20extra=20que=20se=20encontraba=20en=20el=20resumen=20de=20asi?= =?UTF-8?q?gnatura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../asignatura/asignatura_resumen_tab.dart | 54 +++++++++---------- lib/screens/main_screen.dart | 6 ++- lib/screens/usuario_screen.dart | 31 ++++++++--- lib/widgets/custom_drawer.dart | 21 +++++--- 4 files changed, 70 insertions(+), 42 deletions(-) diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/asignatura_resumen_tab.dart index 6e560d0..10611f1 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/asignatura_resumen_tab.dart @@ -37,40 +37,38 @@ class AsignaturaResumenTab extends StatelessWidget { ), ), GestureDetector( - child: FieldListTile( - title: "Docente", - value: asignatura.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ").replaceAll("- ", "") ?? "Sin docente", // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. - ), - onTap: () async { - // TODO: Mostrar perfil del docente. - }, + child: FieldListTile( + title: "Docente", + value: asignatura.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ").replaceAll("- ", "") ?? "Sin docente", // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. ), + onTap: () async { + // TODO: Mostrar perfil del docente. + }, + ), if (asignatura.seccion?.isNotEmpty == true) ...[ - Divider(height: 5, indent: 20, endIndent: 20), - FieldListTile( - title: "Sección", - value: asignatura.seccion.toString(), - ), - ], - Divider(height: 5, indent: 20, endIndent: 20), - FieldListTile( - title: "Código Asignatura", - value: asignatura.codigo, + Divider(height: 5, indent: 20, endIndent: 20), + FieldListTile( + title: "Sección", + value: asignatura.seccion.toString(), ), - if (asignatura.tipoAsignatura != null) ...[ - Divider(height: 5, indent: 20, endIndent: 20), - FieldListTile( - title: "Tipo de Asignatura", - value: asignatura.tipoAsignatura, - ), - Divider(height: 5, indent: 20, endIndent: 20), - ], + ], Divider(height: 5, indent: 20, endIndent: 20), FieldListTile( - title: "Intentos", - value: "${asignatura.intentos}", + title: "Código Asignatura", + value: asignatura.codigo, + ), + if (asignatura.tipoAsignatura != null) ...[ + Divider(height: 5, indent: 20, endIndent: 20), + FieldListTile( + title: "Tipo de Asignatura", + value: asignatura.tipoAsignatura, ), - + Divider(height: 5, indent: 20, endIndent: 20), + ], + FieldListTile( + title: "Intentos", + value: "${asignatura.intentos}", + ), Divider(height: 5), Container( diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 7d4c695..8652670 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -8,6 +8,7 @@ import "package:flutter_markdown/flutter_markdown.dart"; import "package:get/get.dart"; import "package:mi_utem/config/logger.dart"; import "package:mi_utem/models/user/user.dart"; +import "package:mi_utem/repositories/interfaces/preferences_repository.dart"; import "package:mi_utem/services/interfaces/auth_service.dart"; import "package:mi_utem/services/interfaces/grades_service.dart"; import "package:mi_utem/services/remote_config/remote_config.dart"; @@ -34,7 +35,9 @@ class _MainScreenState extends State { List _banners = const []; User? _user; + String? _alias; final _authService = Get.find(); + final _preferencesRepository = Get.find(); @override void initState() { @@ -55,6 +58,7 @@ class _MainScreenState extends State { loadData(); + _preferencesRepository.getAlias().then((alias) => setState(() => _alias = alias)); _authService.getUser().then((user) => setState(() => _user = user)); } @@ -67,7 +71,7 @@ class _MainScreenState extends State { String get _greetingText { List texts = jsonDecode(RemoteConfigService.greetings); - return texts[Random().nextInt(texts.length)].replaceAll("%name", _user?.primerNombre ?? "N/N"); + return texts[Random().nextInt(texts.length)].replaceAll("%name", _alias ?? _user?.primerNombre ?? "N/N"); } @override diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index 9cb7429..fdd5459 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/services/docentes_service.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/review_service.dart'; @@ -35,9 +36,11 @@ class UsuarioScreen extends StatefulWidget { class _UsuarioScreenState extends State { final _authService = Get.find(); + final _preferencesRepository = Get.find(); Future? _userFuture; User? _user; + String? _alias; @override void initState() { @@ -54,19 +57,16 @@ class _UsuarioScreenState extends State { if (widget.asignatura == null) { user = await DocentesService.traerUnDocente(widget.query!["nombre"]); } else { - user = await DocentesService.asignarUnDocente( - widget.query!["nombre"], - widget.asignatura!.codigo, - widget.asignatura!.nombre); + user = await DocentesService.asignarUnDocente(widget.query!["nombre"], widget.asignatura!.codigo, widget.asignatura!.nombre); } - setState(() { - _user = user; - }); + setState(() => _user = user); } else { user = await _authService.getUser(); + final alias = await _preferencesRepository.getAlias(); setState(() { _user = user; + _alias = alias; }); } @@ -147,6 +147,23 @@ class _UsuarioScreenState extends State { } } + if(_alias != null) { + lista.add(Divider(height: 1)); + lista.add( + ListTile( + title: Text("Alias", + style: TextStyle(color: Colors.grey), + ), + subtitle: Text(_alias!, + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + ), + ); + } + if (_user?.correoUtem?.isEmpty == false) { lista.add(Divider(height: 1)); lista.add(ListTile( diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 2a7ca50..1aded68 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -4,7 +4,9 @@ import 'package:badges/badges.dart' as badge; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; +import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; import 'package:mi_utem/screens/credencial_screen.dart'; import 'package:mi_utem/screens/horario/horario_screen.dart'; @@ -50,6 +52,7 @@ class CustomDrawer extends StatelessWidget { @override Widget build(BuildContext context) { final _authService = Get.find(); + final _preferencesRepository = Get.find(); return Drawer( semanticLabel: "Abrir menú", @@ -60,11 +63,17 @@ class CustomDrawer extends StatelessWidget { minHeight: constraints.maxHeight, maxHeight: double.infinity, ), - child: FutureBuilder( - future: _authService.getUser(), - builder: (context, AsyncSnapshot snapshot) { - User? user = snapshot.data; - if(user == null) { + child: FutureBuilder>( + future: () async { + final user = await _authService.getUser(); + final alias = await _preferencesRepository.getAlias(); + return Pair(alias, user); + }(), + builder: (context, snapshot) { + final pair = snapshot.data; + String? alias = pair?.a; + User? user = pair?.b; + if(!snapshot.hasData || snapshot.hasError || user == null) { return Container(); } @@ -75,7 +84,7 @@ class CustomDrawer extends StatelessWidget { children: [ UserAccountsDrawerHeader( accountEmail: Text(user.correoUtem ?? user.correoPersonal ?? ""), - accountName: Text(user.nombreCompleto, + accountName: Text(alias ?? user.nombreCompleto, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, From aa9dc45c8e882165daafb02bfe7e12f0c48a6737 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 28 Mar 2024 14:38:15 -0300 Subject: [PATCH 049/194] patch: lista de estudiantes * Se arreglan errores al cargar detalles de asignatura * Se agrega lista de estudiantes * Se agrega modal de usuario para tener vista previa de dicho usuario Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/asignaturas/asignatura.dart | 10 +- lib/models/exceptions/custom_exception.dart | 4 +- lib/models/user/user.dart | 30 +++--- .../asignaturas_repository.dart | 14 ++- .../interfaces/asignaturas_repository.dart | 4 +- .../asignatura/asignatura_detalle_screen.dart | 39 +++++-- .../asignatura_estudiantes_tab.dart | 102 ++++++++++++++---- .../asignatura/asignatura_notas_tab.dart | 50 +++------ .../asignatura/asignatura_resumen_tab.dart | 78 +++----------- lib/screens/usuario_screen.dart | 1 - lib/widgets/field_list_tile.dart | 46 +++++--- lib/widgets/modals/user_modal.dart | 79 ++++++++++++++ 12 files changed, 282 insertions(+), 175 deletions(-) create mode 100644 lib/widgets/modals/user_modal.dart diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart index 9e1f778..1e1aa5f 100644 --- a/lib/models/asignaturas/asignatura.dart +++ b/lib/models/asignaturas/asignatura.dart @@ -59,13 +59,13 @@ class Asignatura { estado: capitalize(json['estado'] ?? ''), docente: capitalize(json['docente'] ?? ''), seccion: json['seccion'], - grades: json.containsKey('notas') ? Grades.fromJson(json['notas']) : null, - estudiantes: User.fromJsonList(json["estudiantes"]), + grades: json.containsKey('notas') ? Grades.fromJson(json['notas']) : Grades(), + estudiantes: json.containsKey("estudiantes") ? User.fromJsonList(json["estudiantes"]) : [], asistencia: Asistencia(asistidos: json['asistenciaAlDia']), tipoAsignatura: capitalize(json['tipoAsignatura'] as String? ?? ''), sala: capitalize(json['sala'] ?? ''), horario: json['horario'], - intentos: int.tryParse(json['intentos'] ?? '0') ?? 0, + intentos: json.containsKey('intentos') ? (json['intentos'] is num ? json['intentos'] as num? : num.tryParse(json['intentos'])) : null, tipoSala: capitalize(json['tipoSala'] ?? ''), ) : Asignatura(); @@ -80,8 +80,8 @@ class Asignatura { 'docente': docente, 'seccion': seccion, 'estudiantes': estudiantes, - 'notas': grades?.toJson(), - 'asistencia': asistencia?.toJson(), + 'notas': grades?.toJson() ?? [], + 'asistencia': asistencia?.toJson() ?? {}, 'tipoAsignatura': tipoAsignatura, 'sala': sala, 'horario': horario, diff --git a/lib/models/exceptions/custom_exception.dart b/lib/models/exceptions/custom_exception.dart index 2a309bd..eb272db 100644 --- a/lib/models/exceptions/custom_exception.dart +++ b/lib/models/exceptions/custom_exception.dart @@ -5,7 +5,7 @@ class CustomException implements Exception { final String message; final String? error; final int? statusCode; - final int? internalCode; + final double? internalCode; CustomException({ this.message = 'Ocurrió un error inesperado. Por favor, inténtalo nuevamente.', @@ -20,7 +20,7 @@ class CustomException implements Exception { message: json['mensaje'] as String, error: json['error'] as String?, statusCode: json['codigoHttp'] as int?, - internalCode: json['codigoInterno'] as int?, + internalCode: json['codigoInterno'] as double?, ); toJson() => { diff --git a/lib/models/user/user.dart b/lib/models/user/user.dart index 7b515fd..a5dd144 100644 --- a/lib/models/user/user.dart +++ b/lib/models/user/user.dart @@ -39,26 +39,20 @@ class User { this.fotoUrl }); - static List fromJsonList(List? list) { - if(list == null) { - return []; - } - - return list.map((json) => User.fromJson(json as Map)).toList(); - } + static List fromJsonList(List? list) => list != null ? list.map((json) => User.fromJson(json as Map)).toList() : []; factory User.fromJson(Map json) => User( - token: json['token'], - rut: Rut.fromString("${json['rut']}"), - correoPersonal: json['correoPersonal'], - correoUtem: json['correoUtem'], - fotoBase64: json['fotoBase64'], - perfiles: ((json['perfiles'] as List?) ?? []).map((it) => it.toString()).toList(), - nombreCompleto: json['nombreCompleto'], - nombres: json['nombres'], - apellidos: json['apellidos'], - username: json['username'], - fotoUrl: json['fotoUrl'] + token: json['token'], + rut: json.containsKey('rut') ? Rut.fromString("${json['rut']}") : null, + correoPersonal: json['correoPersonal'], + correoUtem: json['correoUtem'], + fotoBase64: json['fotoBase64'], + perfiles: ((json['perfiles'] as List?) ?? []).map((it) => it.toString()).toList(), + nombreCompleto: json['nombreCompleto'], + nombres: json['nombres'], + apellidos: json['apellidos'], + username: json['username'], + fotoUrl: json['fotoUrl'], ); Map toJson() => { diff --git a/lib/repositories/implementations/asignaturas_repository.dart b/lib/repositories/implementations/asignaturas_repository.dart index ca40146..c6494b4 100644 --- a/lib/repositories/implementations/asignaturas_repository.dart +++ b/lib/repositories/implementations/asignaturas_repository.dart @@ -31,8 +31,12 @@ class AsignaturasRepositoryImplementation implements AsignaturasRepository { } @override - Future getDetalleAsignatura(String? asignaturaId, {bool forceRefresh = false}) async { - final response = await authClient.get(Uri.parse('$apiUrl/v1/asignaturas/$asignaturaId')); + Future getDetalleAsignatura(Asignatura? asignatura, {bool forceRefresh = false}) async { + if(asignatura == null) { + return null; + } + + final response = await authClient.get(Uri.parse('$apiUrl/v1/asignaturas/${asignatura.codigo}')); final json = jsonDecode(response.body); if(response.statusCode != 200) { @@ -44,7 +48,11 @@ class AsignaturasRepositoryImplementation implements AsignaturasRepository { throw CustomException.custom(response.reasonPhrase); } - return Asignatura.fromJson(json as Map); + // Por ahora solo se actualizan los estudiantes + return Asignatura.fromJson({ + ...asignatura.toJson(), + 'estudiantes': json['estudiantes'], + }); } } \ No newline at end of file diff --git a/lib/repositories/interfaces/asignaturas_repository.dart b/lib/repositories/interfaces/asignaturas_repository.dart index 57c4358..c17b645 100644 --- a/lib/repositories/interfaces/asignaturas_repository.dart +++ b/lib/repositories/interfaces/asignaturas_repository.dart @@ -5,6 +5,6 @@ abstract class AsignaturasRepository { /* Obtiene las asignaturas */ Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}); - /* Obtiene el detalle de una asignatura */ - Future getDetalleAsignatura(String? asignaturaId, {bool forceRefresh = false}); + /* Obtiene los detalles faltantes de la asignatura */ + Future getDetalleAsignatura(Asignatura? asignatura, {bool forceRefresh = false}); } \ No newline at end of file diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 0b22488..ee989c2 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -1,27 +1,41 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; +import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; -import 'package:mi_utem/screens/asignatura/asignatura_estudiantes_tab.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; -class AsignaturaDetalleScreen extends StatelessWidget { +class AsignaturaDetalleScreen extends StatefulWidget { final Asignatura asignatura; - AsignaturaDetalleScreen({ + const AsignaturaDetalleScreen({ super.key, required this.asignatura, }); + @override + State createState() => _AsignaturaDetalleScreenState(); +} + +class _AsignaturaDetalleScreenState extends State { + + final _asignaturasRepository = Get.find(); + late Asignatura asignatura; bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; + @override + void initState() { + asignatura = widget.asignatura; + super.initState(); + } + @override Widget build(BuildContext context) { ReviewService.addScreen("AsignaturaScreen"); @@ -33,13 +47,17 @@ class AsignaturaDetalleScreen extends StatelessWidget { ), NavigationTab( label: "Notas", - child: AsignaturaNotasTab(asignatura: asignatura), + child: AsignaturaNotasTab( + asignatura: asignatura, + onRefresh: () async { + final asignatura = await _asignaturasRepository.getDetalleAsignatura(this.asignatura); + if (asignatura != null) { + setState(() => this.asignatura = asignatura); + } + }, + ), initial: true, ), - if ((asignatura.estudiantes?.length ?? 0) > 0) NavigationTab( - label: "Estudiantes", - child: AsignaturaEstudiantesTab(asignatura: asignatura), - ), ]; final index = tabs.indexWhere((tab) => tab.initial); @@ -73,6 +91,5 @@ class AsignaturaDetalleScreen extends StatelessWidget { ), ); } - - } + diff --git a/lib/screens/asignatura/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/asignatura_estudiantes_tab.dart index 1d1a31d..9fd36cf 100644 --- a/lib/screens/asignatura/asignatura_estudiantes_tab.dart +++ b/lib/screens/asignatura/asignatura_estudiantes_tab.dart @@ -1,35 +1,97 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; +import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/modals/user_modal.dart'; +import 'package:mi_utem/widgets/pull_to_refresh.dart'; -class AsignaturaEstudiantesTab extends StatelessWidget { + +class AsignaturaEstudiantesTab extends StatefulWidget { final Asignatura? asignatura; - AsignaturaEstudiantesTab({ - Key? key, + const AsignaturaEstudiantesTab({ + super.key, this.asignatura, - }) : super(key: key); + }); @override - Widget build(BuildContext context) => asignatura == null ? CustomErrorWidget( - title: "Ocurrió un error trayendo a los estudiantes", - error: '', - ) : ListView.separated( - itemCount: 0, - separatorBuilder: (context, index) => Divider( - height: 5, - indent: 20, - endIndent: 20, + State createState() => _AsignaturaEstudiantesTabState(); +} + +class _AsignaturaEstudiantesTabState extends State { + + AsignaturasRepository _asignaturasRepository = Get.find(); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: CustomAppBar( + title: Text("Estudiantes"), ), - itemBuilder: (context, i) { - return ListTile( - onTap: () { - AnalyticsService.logEvent( - 'asignatura_estudiante_tap', + body: PullToRefresh( + onRefresh: () async => setState(() => {}), + child: FutureBuilder?>( + future: () async { + return (await _asignaturasRepository.getDetalleAsignatura(widget.asignatura))?.estudiantes; + }(), + builder: (ctx, snapshot) { + if(snapshot.connectionState == ConnectionState.waiting) { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: LoadingIndicator(), + ), + ), + ], + ), + ); + } + + List? estudiantes = snapshot.data; + if(snapshot.hasError || !snapshot.hasData || estudiantes == null) { + return Center( + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: CustomErrorWidget( + emoji: "\u{1F622}", + title: (snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al obtener los estudiantes"), + ), + ), + ); + } + + return ListView.separated( + itemCount: estudiantes.length, + separatorBuilder: (context, index) => Divider( + height: 5, + indent: 20, + endIndent: 20, + ), + itemBuilder: (context, i) => ListTile( + title: Text(estudiantes[i].nombreCompletoCapitalizado), + subtitle: Text(estudiantes[i].correoUtem ?? ''), + onTap: () { + showModalBottomSheet(context: context, builder: (ctx) => UserModal( + user: estudiantes[i], + )); + AnalyticsService.logEvent('asignatura_estudiante_tap', parameters: { + 'asignatura': widget.asignatura?.codigo, + 'estudiante': estudiantes[i].correoUtem, + }); + }, + ), ); }, - ); - }, + ), + ), ); } diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index bb8c71e..d14adc6 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -1,70 +1,48 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/nota_list_item.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; -import 'package:mi_utem/widgets/snackbar.dart'; -class AsignaturaNotasTab extends StatefulWidget { +class AsignaturaNotasTab extends StatelessWidget { + final Asignatura asignatura; + final Future Function() onRefresh; const AsignaturaNotasTab({ super.key, required this.asignatura, + required this.onRefresh, }); - @override - State createState() => _AsignaturaNotasTabState(); -} - -class _AsignaturaNotasTabState extends State { - - late Asignatura asignatura; - - @override - void initState() { - this.asignatura = widget.asignatura; - super.initState(); - } - @override Widget build(BuildContext context) => PullToRefresh( - onRefresh: () async { - final asignatura = await Get.find().getDetalleAsignatura(this.asignatura.codigo); - if(asignatura == null) { - showErrorSnackbar(context, "Ocurrió un error actualizar la asignatura. Por favor intenta más tarde."); - return; - } - - setState(() => this.asignatura = asignatura); - }, + onRefresh: onRefresh, child: ListView( physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.all(10), children: [ NotasDisplayWidget( - notaFinal: widget.asignatura.grades?.notaFinal, - notaExamen: widget.asignatura.grades?.notaExamen, - notaPresentacion: widget.asignatura.grades?.notaPresentacion, - estado: widget.asignatura.estado, - colorPorEstado: widget.asignatura.colorPorEstado, + notaFinal: asignatura.grades?.notaFinal, + notaExamen: asignatura.grades?.notaExamen, + notaPresentacion: asignatura.grades?.notaPresentacion, + estado: asignatura.estado, + colorPorEstado: asignatura.colorPorEstado, ), Card( child: Container( padding: EdgeInsets.all(20), - child: widget.asignatura.grades?.notasParciales.isNotEmpty == true ? ListView.builder( + child: asignatura.grades?.notasParciales.isNotEmpty == true ? ListView.builder( shrinkWrap: true, physics: ClampingScrollPhysics(), itemBuilder: (context, i) { - REvaluacion evaluacion = widget.asignatura.grades!.notasParciales[i]; + REvaluacion evaluacion = asignatura.grades!.notasParciales[i]; return NotaListItem(evaluacion: IEvaluacion.fromRemote(evaluacion)); }, - itemCount: widget.asignatura.grades!.notasParciales.length, - ) : CustomErrorWidget( + itemCount: asignatura.grades!.notasParciales.length, + ) : const CustomErrorWidget( emoji: "🤔", title: "Parece que aún no hay notas ni ponderadores", ), diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/asignatura_resumen_tab.dart index 10611f1..6516d38 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/asignatura_resumen_tab.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/screens/asignatura/asignatura_estudiantes_tab.dart'; import 'package:mi_utem/utils/string_utils.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; class AsignaturaResumenTab extends StatelessWidget { final Asignatura asignatura; - AsignaturaResumenTab({ - Key? key, + const AsignaturaResumenTab({ + super.key, required this.asignatura, - }) : super(key: key); + }); List>? get datosHorario => asignatura.horario?.split("||").map((e) => e.trim()).map((it) => { 'dia': capitalize(it.split("|")[0]).trim(), @@ -63,11 +64,22 @@ class AsignaturaResumenTab extends StatelessWidget { title: "Tipo de Asignatura", value: asignatura.tipoAsignatura, ), + ], + if (asignatura.intentos != null) ...[ + Divider(height: 5, indent: 20, endIndent: 20), + FieldListTile( + title: "Intentos", + value: asignatura.intentos.toString(), + ), Divider(height: 5, indent: 20, endIndent: 20), ], FieldListTile( - title: "Intentos", - value: "${asignatura.intentos}", + title: "Estudiantes", + value: "Presiona para ver los estudiantes", + suffixIcon: Icon(Icons.arrow_forward_ios, size: 15, color: Colors.grey), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaEstudiantesTab(asignatura: this.asignatura))); + } ), Divider(height: 5), @@ -113,62 +125,6 @@ class AsignaturaResumenTab extends StatelessWidget { ), ), ), - // Card( - // child: Container( - // child: ListView( - // padding: EdgeInsets.only(top: 20.0, bottom: 5.0), - // shrinkWrap: true, - // physics: ScrollPhysics(), - // children: [ - // Container( - // padding: EdgeInsets.symmetric(horizontal: 15), - // width: double.infinity, - // child: Text("Sala".toUpperCase(), - // style: Get.textTheme.bodyLarge, - // textAlign: TextAlign.left, - // ), - // ), - // FieldListTile( - // title: "Tipo de hora", - // value: asignatura.tipoHora, - // ), - // Divider(height: 5, indent: 20, endIndent: 20), - // FieldListTile( - // title: "Sala", - // value: asignatura.sala, - // ), - // ], - // ), - // ) - // ), - // Card( - // child: Container( - // child: ListView( - // padding: EdgeInsets.only(top: 20.0, bottom: 5.0), - // shrinkWrap: true, - // physics: ScrollPhysics(), - // children: [ - // Container( - // padding: EdgeInsets.symmetric(horizontal: 15), - // width: double.infinity, - // child: Text("Horario".toUpperCase(), - // style: Get.textTheme.bodyLarge, - // textAlign: TextAlign.left, - // ), - // ), - // ...datosHorario?.map((it) => Column( - // children: [ - // FieldListTile( - // title: it['dia'] ?? "", - // value: it['horas'] - // ), - // if ("${datosHorario?.last['dia']} ${datosHorario?.last['horas']}" != "${it['dia']} ${it['horas']}") Divider(height: 5, indent: 20, endIndent: 20), - // ], - // )).toList() ?? [], - // ], - // ), - // ), - // ), ], ), ); diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index fdd5459..294a160 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -53,7 +53,6 @@ class _UsuarioScreenState extends State { try { User? user; if (widget.tipo == 2) { - print(widget.query); if (widget.asignatura == null) { user = await DocentesService.traerUnDocente(widget.query!["nombre"]); } else { diff --git a/lib/widgets/field_list_tile.dart b/lib/widgets/field_list_tile.dart index 58a9b8a..5c8e92b 100644 --- a/lib/widgets/field_list_tile.dart +++ b/lib/widgets/field_list_tile.dart @@ -4,6 +4,8 @@ class FieldListTile extends StatelessWidget { final String title; final String? value; final EdgeInsetsGeometry padding; + final Widget? suffixIcon; + final Function()? onTap; const FieldListTile({ super.key, @@ -13,28 +15,40 @@ class FieldListTile extends StatelessWidget { vertical: 12, horizontal: 16, ), + this.suffixIcon, + this.onTap, }); @override Widget build(BuildContext context) => Padding( padding: padding, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title.toUpperCase(), - maxLines: 2, - style: Theme.of(context).textTheme.bodySmall!.copyWith(fontWeight: FontWeight.bold), - ), - Text(value ?? "Sin información", - style: Theme.of(context).textTheme.bodyMedium, - ), - ], + child: GestureDetector( + onTap: onTap, + behavior: onTap != null ? HitTestBehavior.opaque : HitTestBehavior.deferToChild, + child: Row( + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title.toUpperCase(), + maxLines: 2, + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontWeight: FontWeight.bold), + ), + Text(value ?? "Sin información", + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + if(suffixIcon != null) suffixIcon!, + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/widgets/modals/user_modal.dart b/lib/widgets/modals/user_modal.dart new file mode 100644 index 0000000..4542db2 --- /dev/null +++ b/lib/widgets/modals/user_modal.dart @@ -0,0 +1,79 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/widgets/profile_photo.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class UserModal extends StatelessWidget { + final User user; + + const UserModal({ + super.key, + required this.user, + }); + + @override + Widget build(BuildContext context) => SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Stack( + children: [ + Container( + alignment: Alignment.topCenter, + margin: const EdgeInsets.only(top: 80), + child: Card( + margin: const EdgeInsets.all(20), + child: ListView( + padding: const EdgeInsets.only(bottom: 10, top: 20), + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + ListTile( + title: Text("Nombre Completo", + style: TextStyle(color: Colors.grey), + ), + onLongPress: () async { + await FlutterClipboard.copy(user.nombreCompleto); + showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); + }, + subtitle: Text(user.nombreCompletoCapitalizado, + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + ), + Divider(height: 5), + ListTile( + title: Text("Correo Personal", + style: TextStyle(color: Colors.grey), + ), + onLongPress: () async { + await FlutterClipboard.copy(user.correoUtem!); + showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); + }, + onTap: () async { + await launchUrl(Uri.parse("mailto:${user.correoUtem ?? ""}")); + }, + subtitle: Text(user.correoUtem!, + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + ), + ], + ), + ), + ), + Center( + child: ProfilePhoto( + user: user, + radius: 60, + editable: false, + ), + ), + ], + ), + ); +} From c55f5914d743fcff411e95bea399cc9adc7a8420 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 28 Mar 2024 14:41:32 -0300 Subject: [PATCH 050/194] =?UTF-8?q?patch:=20reparado=20que=20puedes=20volv?= =?UTF-8?q?er=20al=20onboarding=20(despu=C3=A9s=20de=20terminarlo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/onboarding/notifications_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart index 9ad88b3..bb895d8 100644 --- a/lib/screens/onboarding/notifications_screen.dart +++ b/lib/screens/onboarding/notifications_screen.dart @@ -94,6 +94,7 @@ class _NotificationsScreenState extends State { if(!context.mounted) { return; } + Navigator.popUntil(context, (route) => route.isFirst); Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); }, style: ElevatedButton.styleFrom( From 112f9d7e0431785bae2069cdb0307a41c3ca53ea Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 28 Mar 2024 14:42:16 -0300 Subject: [PATCH 051/194] =?UTF-8?q?patch:=20reordenado=20c=C3=B3digo=20par?= =?UTF-8?q?a=20evitar=20problemas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/onboarding/notifications_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart index bb895d8..76ad5f2 100644 --- a/lib/screens/onboarding/notifications_screen.dart +++ b/lib/screens/onboarding/notifications_screen.dart @@ -90,10 +90,10 @@ class _NotificationsScreenState extends State { FilledButton( onPressed: () async { - await _preferencesRepository.setOnboardingStep("complete"); if(!context.mounted) { return; } + await _preferencesRepository.setOnboardingStep("complete"); Navigator.popUntil(context, (route) => route.isFirst); Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); }, From 80ed7d71d6c14cebe6a9eaee89fbe3ab000a5707 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:04:59 -0300 Subject: [PATCH 052/194] =?UTF-8?q?patch:=20reparado=20organizaci=C3=B3n?= =?UTF-8?q?=20de=20archivos=20en=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5f755f2..9aa945e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Aplicación multiplataforma hecha por estudiantes de la [Universidad Tecnológic | |-- utils (Utilidades de la aplicación) | |-- widgets (Widgets de la aplicación) |-- main.dart (Punto de entrada de la aplicación) +|-- service_manager.dart (Registra los servicios de la aplicación) ``` ## Créditos From 6da663f19bd32b8c07f228abc000e5e3665aee07 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:58:29 -0300 Subject: [PATCH 053/194] =?UTF-8?q?patch:=20actualizado=20changelog=20y=20?= =?UTF-8?q?pubspec=CF=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- CHANGELOG.md | 14 ++++++++++---- pubspec.yaml | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d316b1e..bcfa2f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,16 +28,22 @@ Tipos de cambios ## [Unreleased] +### Added +- Se agrega "apodo" para permitir a los usuarios tener un nombre más personalizado. +- Se agregó lista de estudiantes al resumen de asignatura. +- Se agregó modal de usuario para tener una vista previa y pequeña del perfil del usuario. + ### Changed -- Se actualizan dependencias de Flutter -- Se actualiza a enrutador nativo de flutter -- Se mejora el rendimiento de la aplicación -- Se ordenan clases y widgets +- Se actualizan dependencias de Flutter. +- Se actualiza a enrutador nativo de flutter. +- Se mejora el rendimiento de la aplicación. +- Se ordenan clases y widgets. ### Removed - Dependencia GetX +- Lista de estudiantes de las pestañas de asignatura. ## [2.11.9] - 2023-10-11Z diff --git a/pubspec.yaml b/pubspec.yaml index 6f7a466..928fd99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: clipboard: ^0.1.3 cloud_firestore: ^4.3.1 custom_refresh_indicator: ^2.0.1 - dio: ^4.0.4 + dio: ^4.0.6 dio_cache_interceptor: ^3.2.6 dio_cache_interceptor_hive_store: ^3.1.1 dotted_border: ^2.0.0+2 From 6cb57a7434261bcd745c8d48e91aa41037fda37b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:29:29 -0300 Subject: [PATCH 054/194] patch: agregado pod de GoogleUtilities * Se agrega el pod de GoogleUtilities para resolver error de "multiple targets" Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Podfile | 2 ++ ios/Podfile.lock | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ios/Podfile b/ios/Podfile index 16c1ff0..2ddf34e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -34,6 +34,7 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + pod 'GoogleUtilities' end post_install do |installer| @@ -112,6 +113,7 @@ target 'MiUtemNotificationServiceExtension' do use_modular_headers! install_awesome_fcm_ios_pod_target File.dirname(File.realpath(__FILE__)) + pod 'GoogleUtilities' end update_awesome_fcm_service_target('MiUtemNotificationServiceExtension', File.dirname(File.realpath(__FILE__)), flutter_root) ################ Awesome Notifications FCM pod mod ################### \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5f0d223..b2052c1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -778,6 +778,18 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities (7.13.0): + - GoogleUtilities/AppDelegateSwizzler (= 7.13.0) + - GoogleUtilities/Environment (= 7.13.0) + - GoogleUtilities/ISASwizzler (= 7.13.0) + - GoogleUtilities/Logger (= 7.13.0) + - GoogleUtilities/MethodSwizzler (= 7.13.0) + - GoogleUtilities/Network (= 7.13.0) + - "GoogleUtilities/NSData+zlib (= 7.13.0)" + - GoogleUtilities/Privacy (= 7.13.0) + - GoogleUtilities/Reachability (= 7.13.0) + - GoogleUtilities/SwizzlerTestHelpers (= 7.13.0) + - GoogleUtilities/UserDefaults (= 7.13.0) - GoogleUtilities/AppDelegateSwizzler (7.13.0): - GoogleUtilities/Environment - GoogleUtilities/Logger @@ -786,6 +798,8 @@ PODS: - GoogleUtilities/Environment (7.13.0): - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/ISASwizzler (7.13.0): + - GoogleUtilities/Privacy - GoogleUtilities/Logger (7.13.0): - GoogleUtilities/Environment - GoogleUtilities/Privacy @@ -803,6 +817,8 @@ PODS: - GoogleUtilities/Reachability (7.13.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - GoogleUtilities/SwizzlerTestHelpers (7.13.0): + - GoogleUtilities/MethodSwizzler - GoogleUtilities/UserDefaults (7.13.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy @@ -925,6 +941,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_uxcam (from `.symlinks/plugins/flutter_uxcam/ios`) + - GoogleUtilities - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) @@ -1082,6 +1099,6 @@ SPEC CHECKSUMS: UXCam: 7f200dd8f0829ae92a2ef82cadbb35dadff6108d video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 -PODFILE CHECKSUM: 816280d49dfc997733647f095bf1e4c52c291937 +PODFILE CHECKSUM: ae166eb081d1fe0b450a3271d7130468d146603c COCOAPODS: 1.15.2 From f0d6324757835948c59c0230ffb6a5e5a216dbb4 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:30:33 -0300 Subject: [PATCH 055/194] patch: actualizado changelog Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- CHANGELOG.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcfa2f4..85a7ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,22 +28,22 @@ Tipos de cambios ## [Unreleased] +## [3.0.0] - 2024-04-01Z + ### Added -- Se agrega "apodo" para permitir a los usuarios tener un nombre más personalizado. +- Se agrega "apodo" para permitir a los usuarios personalizar la aplicación. - Se agregó lista de estudiantes al resumen de asignatura. -- Se agregó modal de usuario para tener una vista previa y pequeña del perfil del usuario. +- Se agregó una vista previa de los datos de los estudiantes y profesores. ### Changed -- Se actualizan dependencias de Flutter. -- Se actualiza a enrutador nativo de flutter. +- Se actualizaron algunas dependencias. - Se mejora el rendimiento de la aplicación. -- Se ordenan clases y widgets. +- Mejor orden en el backend. ### Removed -- Dependencia GetX -- Lista de estudiantes de las pestañas de asignatura. +- Lista de estudiantes de la pestaña de asignatura. ## [2.11.9] - 2023-10-11Z @@ -153,3 +153,4 @@ Esta versión del changelog contiene cambios hechos en 2.10, debido a que no se - Perfil de profesores - Perfil de profesores - Perfil de profesores +- Perfil de profesores From da84d0cbc4b3d9c8151b3c044bc08276b120e7a9 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:49:39 -0300 Subject: [PATCH 056/194] =?UTF-8?q?patch:=20volviendo=20a=20dio=20(mejor?= =?UTF-8?q?=20orden=20e=20implementaci=C3=B3n=20de=20cach=C3=A9)=20*=20Se?= =?UTF-8?q?=20vuelve=20a=20dio=20(lo=20siento=20por=20el=20cambio=20jeje)?= =?UTF-8?q?=20*=20Se=20mejora=20el=20orden=20de=20clientes=20http?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../asignaturas_repository.dart | 43 +++---- .../implementations/auth_repository.dart | 76 ++----------- .../implementations/carreras_repository.dart | 24 ++-- .../implementations/grades_repository.dart | 24 +--- .../implementations/horario_repository.dart | 23 +--- .../implementations/noticias_repository.dart | 27 ++--- .../permiso_ingreso_repository.dart | 42 ++----- .../interfaces/carreras_repository.dart | 2 +- .../interfaces/grades_repository.dart | 1 + .../interfaces/noticias_repository.dart | 2 +- .../permiso_ingreso_repository.dart | 4 +- lib/screens/main_screen.dart | 6 +- .../implementations/auth_service.dart | 2 +- lib/services/interfaces/auth_service.dart | 2 +- lib/utils/dio_miutem_client.dart | 106 ------------------ lib/utils/http/http_client.dart | 60 ++++++++++ .../http/interceptors/auth_interceptor.dart | 85 ++++++++++++++ .../interceptors/headers_interceptor.dart | 18 +++ lib/widgets/custom_drawer.dart | 2 +- .../noticias/noticias_carrusel_widget.dart | 2 + 20 files changed, 239 insertions(+), 312 deletions(-) delete mode 100644 lib/utils/dio_miutem_client.dart create mode 100644 lib/utils/http/http_client.dart create mode 100644 lib/utils/http/interceptors/auth_interceptor.dart create mode 100644 lib/utils/http/interceptors/headers_interceptor.dart diff --git a/lib/repositories/implementations/asignaturas_repository.dart b/lib/repositories/implementations/asignaturas_repository.dart index c6494b4..ada535e 100644 --- a/lib/repositories/implementations/asignaturas_repository.dart +++ b/lib/repositories/implementations/asignaturas_repository.dart @@ -1,33 +1,28 @@ -import 'dart:convert'; - +import 'package:dio/dio.dart'; +import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; class AsignaturasRepositoryImplementation implements AsignaturasRepository { + final _authClient = HttpClient.authClient; + @override Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}) async { if(carreraId == null) { return null; } - final response = await authClient.get(Uri.parse('$apiUrl/v1/carreras/$carreraId/asignaturas')); + final _cacheOptions = buildCacheOptions(Duration(days: 7), + forceRefresh: forceRefresh, + subKey: 'carreras/$carreraId/asignaturas', + ); - final json = jsonDecode(response.body); + final Response response = await _authClient.get('$apiUrl/v1/carreras/$carreraId/asignaturas', options: _cacheOptions); - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); - } - - throw CustomException.custom(response.reasonPhrase); - } - - return Asignatura.fromJsonList(json); + return Asignatura.fromJsonList(response.data); } @override @@ -36,17 +31,13 @@ class AsignaturasRepositoryImplementation implements AsignaturasRepository { return null; } - final response = await authClient.get(Uri.parse('$apiUrl/v1/asignaturas/${asignatura.codigo}')); - - final json = jsonDecode(response.body); - if(response.statusCode != 200) { - logger.e("Error al obtener detalle de asignatura: ${response.reasonPhrase}", json); - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); - } + final _cacheOptions = buildCacheOptions(Duration(days: 7), + forceRefresh: forceRefresh, + subKey: 'asignaturas/${asignatura.codigo}', + ); - throw CustomException.custom(response.reasonPhrase); - } + final response = await _authClient.get('$apiUrl/v1/asignaturas/${asignatura.codigo}', options: _cacheOptions); + final json = response.data as Map; // Por ahora solo se actualizan los estudiantes return Asignatura.fromJson({ diff --git a/lib/repositories/implementations/auth_repository.dart b/lib/repositories/implementations/auth_repository.dart index 865b2f0..aceb322 100644 --- a/lib/repositories/implementations/auth_repository.dart +++ b/lib/repositories/implementations/auth_repository.dart @@ -1,83 +1,31 @@ -import 'dart:convert'; - +import 'package:dio/dio.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; class AuthRepositoryImplementation extends AuthRepository { - @override - Future auth({ - required Credentials credentials, - }) async { - final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth"), - body: credentials.toString(), - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - }, - ); - - - final json = jsonDecode(response.body); + final _httpClient = HttpClient.httpClient; - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json); - } - - throw CustomException.custom(response.reasonPhrase); - } - - return User.fromJson(json as Map); + @override + Future auth({required Credentials credentials}) async { + final response = await _httpClient.post("$apiUrl/v1/auth", data: credentials.toJson()); + return User.fromJson(response.data as Map); } @override - Future refresh({ - required String token, - required Credentials credentials - }) async { - final response = await httpClient.post(Uri.parse("$apiUrl/v1/auth/refresh"), - body: credentials.toString(), - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'App/MiUTEM', - 'Authorization': 'Bearer $token', - }, - ); - - - final json = jsonDecode(response.body); - - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json); - } - - throw CustomException.custom(response.reasonPhrase); - } - + Future refresh({required String token, required Credentials credentials}) async { + final response = await _httpClient.post("$apiUrl/v1/auth/refresh", data: credentials.toJson(), options: Options(headers: {"Authorization": "Bearer $token"})); + final json = response.data as Map; return json["token"]; } @override Future updateProfilePicture({required String image}) async { - final response = await authClient.put(Uri.parse("$apiUrl/v1/usuarios/foto"), - body: jsonEncode({"imagen": image}), - ); - - final json = jsonDecode(response.body); - - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json); - } - - throw CustomException.custom(response.reasonPhrase); - } + final response = await HttpClient.authClient.put("$apiUrl/v1/usuarios/foto", data: ({"imagen": image})); + final json = response.data; return json["fotoUrl"] as String; } diff --git a/lib/repositories/implementations/carreras_repository.dart b/lib/repositories/implementations/carreras_repository.dart index 35fc657..918306c 100644 --- a/lib/repositories/implementations/carreras_repository.dart +++ b/lib/repositories/implementations/carreras_repository.dart @@ -1,27 +1,17 @@ -import 'dart:convert'; - +import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; class CarrerasRepositoryImplementation extends CarrerasRepository { - @override - Future> getCarreras() async { - final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras")); - - final json = jsonDecode(response.body); - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); - } + final _authClient = HttpClient.authClient; - throw CustomException.custom(response.reasonPhrase); - } - - return Carrera.fromJsonList(json as List); + @override + Future> getCarreras({ bool forceRefresh = false }) async { + final response = await _authClient.get("$apiUrl/v1/carreras", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: '/carreras')); + return Carrera.fromJsonList(response.data as List); } } \ No newline at end of file diff --git a/lib/repositories/implementations/grades_repository.dart b/lib/repositories/implementations/grades_repository.dart index acb105e..f69ded0 100644 --- a/lib/repositories/implementations/grades_repository.dart +++ b/lib/repositories/implementations/grades_repository.dart @@ -1,27 +1,15 @@ -import 'dart:convert'; - +import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; class GradesRepositoryImplementation extends GradesRepository { + final _authClient = HttpClient.authClient; @override - Future getGrades({required String carreraId, required String asignaturaId}) async { - final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas")); - - final json = jsonDecode(response.body); - - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); - } - - throw CustomException.custom(response.reasonPhrase); - } - - return Grades.fromJson(json as Map); + Future getGrades({required String carreraId, required String asignaturaId, bool forceRefresh = false}) async { + final response = await _authClient.get("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "carreras/$carreraId/asignaturas/$asignaturaId/notas")); + return Grades.fromJson(response.data as Map); } } \ No newline at end of file diff --git a/lib/repositories/implementations/horario_repository.dart b/lib/repositories/implementations/horario_repository.dart index db505c5..67d26b0 100644 --- a/lib/repositories/implementations/horario_repository.dart +++ b/lib/repositories/implementations/horario_repository.dart @@ -1,26 +1,15 @@ -import 'dart:convert'; - +import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; class HorarioRepositoryImplementation implements HorarioRepository { + final _authClient = HttpClient.authClient; + @override Future getHorario(String carreraId, {bool forceRefresh = false}) async { - final response = await authClient.get(Uri.parse("$apiUrl/v1/carreras/$carreraId/horarios")); - - final json = jsonDecode(response.body); - - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); - } - - throw CustomException.custom(response.reasonPhrase); - } - - return Horario.fromJson(json); + final response = await _authClient.get("$apiUrl/v1/carreras/$carreraId/horarios", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "carreras/$carreraId/horarios")); + return Horario.fromJson(response.data); } } diff --git a/lib/repositories/implementations/noticias_repository.dart b/lib/repositories/implementations/noticias_repository.dart index d6e8dab..c89188a 100644 --- a/lib/repositories/implementations/noticias_repository.dart +++ b/lib/repositories/implementations/noticias_repository.dart @@ -1,30 +1,17 @@ -import 'dart:convert'; - +import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; class NoticiasRepositoryImplementation implements NoticiasRepository { - @override - Future?> getNoticias() async { - final response = await httpClient.get(Uri.parse("$apiUrl/v1/noticias"), headers: { - 'X-MiUTEM-Use-Cache': 'true', - }); - - final json = jsonDecode(response.body); + final _httpClient = HttpClient.httpClient; - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); - } - - throw CustomException.custom(response.reasonPhrase); - } - - return Noticia.fromJsonList(json as List); + @override + Future?> getNoticias({ bool forceRefresh = false }) async { + final response = await _httpClient.get("$apiUrl/v1/noticias", options: buildCacheOptions(Duration(days: 14), forceRefresh: forceRefresh, subKey: "/noticias")); + return Noticia.fromJsonList(response.data as List); } } \ No newline at end of file diff --git a/lib/repositories/implementations/permiso_ingreso_repository.dart b/lib/repositories/implementations/permiso_ingreso_repository.dart index 0504738..a69f130 100644 --- a/lib/repositories/implementations/permiso_ingreso_repository.dart +++ b/lib/repositories/implementations/permiso_ingreso_repository.dart @@ -1,45 +1,23 @@ -import 'dart:convert'; - +import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; class PermisoIngresoRepositoryImplementation extends PermisoIngresoRepository { - @override - Future getDetallesPermiso(String id) async { - final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos/$id")); - - final json = jsonDecode(response.body) as Map; - - if(response.statusCode != 200) { - if(json.containsKey("error")) { - throw CustomException.fromJson(json); - } - - throw CustomException.custom(response.reasonPhrase); - } + final _authClient = HttpClient.authClient; - return PermisoIngreso.fromJson(json); + @override + Future getDetallesPermiso(String id, { bool forceRefresh = false }) async { + final response = await _authClient.post("$apiUrl/v1/permisos/$id", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "/permisos/$id")); + return PermisoIngreso.fromJson(response.data as Map?); } @override - Future> getPermisos() async { - final response = await authClient.post(Uri.parse("$apiUrl/v1/permisos")); - - final json = jsonDecode(response.body); - - if(response.statusCode != 200) { - if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json); - } - - throw CustomException.custom(response.reasonPhrase); - } - - return PermisoIngreso.fromJsonList(json as List); + Future> getPermisos({ bool forceRefresh = false }) async { + final response = await _authClient.post("$apiUrl/v1/permisos", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "/permisos")); + return PermisoIngreso.fromJsonList(response.data as List?); } } \ No newline at end of file diff --git a/lib/repositories/interfaces/carreras_repository.dart b/lib/repositories/interfaces/carreras_repository.dart index 67e5c0b..b4430ae 100644 --- a/lib/repositories/interfaces/carreras_repository.dart +++ b/lib/repositories/interfaces/carreras_repository.dart @@ -3,5 +3,5 @@ import 'package:mi_utem/models/carrera.dart'; abstract class CarrerasRepository { /* Obtiene las carreras */ - Future> getCarreras(); + Future> getCarreras({ bool forceRefresh = false }); } \ No newline at end of file diff --git a/lib/repositories/interfaces/grades_repository.dart b/lib/repositories/interfaces/grades_repository.dart index 7dc61d4..1999ccf 100644 --- a/lib/repositories/interfaces/grades_repository.dart +++ b/lib/repositories/interfaces/grades_repository.dart @@ -6,5 +6,6 @@ abstract class GradesRepository { Future getGrades({ required String carreraId, required String asignaturaId, + bool forceRefresh = false, }); } \ No newline at end of file diff --git a/lib/repositories/interfaces/noticias_repository.dart b/lib/repositories/interfaces/noticias_repository.dart index 18835c8..1a1b110 100644 --- a/lib/repositories/interfaces/noticias_repository.dart +++ b/lib/repositories/interfaces/noticias_repository.dart @@ -2,5 +2,5 @@ import 'package:mi_utem/models/noticia.dart'; abstract class NoticiasRepository { - Future?> getNoticias(); + Future?> getNoticias({ bool forceRefresh = false }); } \ No newline at end of file diff --git a/lib/repositories/interfaces/permiso_ingreso_repository.dart b/lib/repositories/interfaces/permiso_ingreso_repository.dart index ebb07a6..f59a355 100644 --- a/lib/repositories/interfaces/permiso_ingreso_repository.dart +++ b/lib/repositories/interfaces/permiso_ingreso_repository.dart @@ -2,7 +2,7 @@ import 'package:mi_utem/models/permiso_ingreso.dart'; abstract class PermisoIngresoRepository { - Future getDetallesPermiso(String id); + Future getDetallesPermiso(String id, { bool forceRefresh = false}); - Future> getPermisos(); + Future> getPermisos({ bool forceRefresh = false }); } \ No newline at end of file diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 8652670..a243a79 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -6,7 +6,6 @@ import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_markdown/flutter_markdown.dart"; import "package:get/get.dart"; -import "package:mi_utem/config/logger.dart"; import "package:mi_utem/models/user/user.dart"; import "package:mi_utem/repositories/interfaces/preferences_repository.dart"; import "package:mi_utem/services/interfaces/auth_service.dart"; @@ -63,10 +62,7 @@ class _MainScreenState extends State { } Future loadData() async { - setState(() { - _banners = RemoteConfigService.banners; - logger.d("Banners: $_banners"); - }); + setState(() => _banners = RemoteConfigService.banners); } String get _greetingText { diff --git a/lib/services/implementations/auth_service.dart b/lib/services/implementations/auth_service.dart index ffce755..d350571 100644 --- a/lib/services/implementations/auth_service.dart +++ b/lib/services/implementations/auth_service.dart @@ -80,7 +80,7 @@ class AuthServiceImplementation implements AuthService { } @override - Future logout(BuildContext? context) async { + Future logout({BuildContext? context}) async { setUser(null); _credentialsService.setCredentials(null); _preferencesRepository.setOnboardingStep(null); diff --git a/lib/services/interfaces/auth_service.dart b/lib/services/interfaces/auth_service.dart index cb32743..0851a2f 100644 --- a/lib/services/interfaces/auth_service.dart +++ b/lib/services/interfaces/auth_service.dart @@ -9,7 +9,7 @@ abstract class AuthService { Future login(); - Future logout(BuildContext? context); + Future logout({BuildContext? context}); Future getUser(); diff --git a/lib/utils/dio_miutem_client.dart b/lib/utils/dio_miutem_client.dart deleted file mode 100644 index 12af595..0000000 --- a/lib/utils/dio_miutem_client.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:developer'; - -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -class DioMiUtemClient { - static const bool isProduction = bool.fromEnvironment('dart.vm.product'); - static String debugUrl = - dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl/'; - static const String productionUrl = 'https://api.exdev.cl/'; - - static String url = isProduction ? productionUrl : debugUrl; - - static Dio get initDio => Dio(BaseOptions(baseUrl: url)); - - static CacheConfig cacheConfig = CacheConfig( - baseUrl: url, - defaultMaxAge: Duration(days: 7), - defaultMaxStale: Duration(days: 60), - ); - static DioCacheManager dioCacheManager = DioCacheManager(cacheConfig); - - static Dio baseDio = Dio(BaseOptions(baseUrl: url)) - ..interceptors.add(dioCacheManager.interceptor); - - static Dio get authDio => initDio - ..interceptors.add( - AuthInterceptor(), - ); -} - -class AuthInterceptor extends QueuedInterceptor { - AuthInterceptor({ - this.retries = 3, - }); - - /// The number of retries in case of 401 - final int retries; - - @override - Future onRequest( - final RequestOptions options, - final RequestInterceptorHandler handler, - ) async { - try { - final token = ""; - - options._setAuthenticationHeader(token); - - return handler.next(options); - } catch (e) { - final error = DioError(requestOptions: options, error: e); - handler.reject(error); - } - } - - @override - Future onError( - final DioError err, final ErrorInterceptorHandler handler) async { - final options = err.requestOptions; - - if (err.response?.statusCode != 401) { - return super.onError(err, handler); - } - - final attempt = err.requestOptions._retryAttempt + 1; - if (attempt > retries) { - await _onErrorRefreshingToken(); - return super.onError(err, handler); - } - err.requestOptions._retryAttempt = attempt; - await Future.delayed(const Duration(seconds: 1)); - - // Force refresh auth token - try { - final token = ""; - - log("Refreshing token, attempt $attempt..."); - - options._setAuthenticationHeader(token); - final response = await DioMiUtemClient.baseDio.fetch(options); - return handler.resolve(response); - } on DioError catch (e) { - super.onError(e, handler); - } catch (e) { - super.onError( - DioError(requestOptions: options, error: e), - handler, - ); - } - } - - Future _onErrorRefreshingToken() async { - - } -} - -extension AuthRequestOptionsX on RequestOptions { - void _setAuthenticationHeader(final String token) => - headers['Authorization'] = 'Bearer $token'; - - int get _retryAttempt => (extra['auth_retry_attempt'] as int?) ?? 0; - - set _retryAttempt(final int attempt) => extra['auth_retry_attempt'] = attempt; -} diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart new file mode 100644 index 0000000..9a88f06 --- /dev/null +++ b/lib/utils/http/http_client.dart @@ -0,0 +1,60 @@ +import 'package:dio/dio.dart'; +import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/utils/http/interceptors/auth_interceptor.dart'; +import 'package:mi_utem/utils/http/interceptors/headers_interceptor.dart'; + +class HttpClient { + static const bool isProduction = bool.fromEnvironment('dart.vm.product'); + static String debugUrl = dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl/'; + static const String productionUrl = 'https://api.exdev.cl/'; + static String url = isProduction ? productionUrl : debugUrl; + static Dio get initDio => Dio(BaseOptions(baseUrl: url)); + + static CacheConfig cacheConfig = CacheConfig( + baseUrl: url, + defaultMaxAge: Duration(days: 7), + defaultMaxStale: Duration(days: 60), + ); + + static DioCacheManager dioCacheManager = DioCacheManager(cacheConfig); + + static Dio httpClient = Dio(BaseOptions(baseUrl: url))..interceptors.addAll([ + HeadersInterceptor(), + _errorInterceptor, + _logInterceptor, + dioCacheManager.interceptor, + ]); + + static Dio get authClient => initDio..interceptors.add(AuthInterceptor()); + + static InterceptorsWrapper _errorInterceptor = InterceptorsWrapper( + onError: (DioError err, ErrorInterceptorHandler handler) { + final json = err.response?.data ?? {}; + if(json is Map && json.containsKey("error")) { + throw CustomException.fromJson(json as Map); + } + + throw CustomException.custom(err.response?.statusMessage); + }, + ); + + static InterceptorsWrapper _logInterceptor = InterceptorsWrapper( + onRequest: (RequestOptions options, RequestInterceptorHandler handler) { + logger.d("[HttpClient]: ${options.method.toUpperCase()} ${options.uri}"); + options.extra["request_created_at"] = DateTime.now().toIso8601String(); + return handler.next(options); + }, + onResponse: (Response response, ResponseInterceptorHandler handler) { + final now = DateTime.now(); + final request = DateTime.tryParse(response.requestOptions.extra["request_created_at"] as String); + if(request != null) { + final duration = now.difference(request); + logger.d("[HttpClient]: ${response.statusCode} > ${response.requestOptions.method.toUpperCase()} ${response.requestOptions.uri} ${duration.inMilliseconds}ms"); + } + return handler.next(response); + }, + ); +} diff --git a/lib/utils/http/interceptors/auth_interceptor.dart b/lib/utils/http/interceptors/auth_interceptor.dart new file mode 100644 index 0000000..bf0b7ca --- /dev/null +++ b/lib/utils/http/interceptors/auth_interceptor.dart @@ -0,0 +1,85 @@ +import 'package:dio/dio.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; + +class AuthInterceptor extends QueuedInterceptor { + + /* Cantidad de reintentos máximos si se recibe un error 401 */ + final int retries; + final _authService = Get.find(); + + AuthInterceptor({ + this.retries = 3, + }); + + @override + Future onRequest(final RequestOptions options, final RequestInterceptorHandler handler) async { + try { + if(!options.headers.containsKey("authorization")) { + final user = await _authService.getUser(); + final token = user?.token; + if(user != null && token != null) { + options._setAuthenticationHeader(token); + } + } + + return handler.next(options); + } catch (e) { + return handler.reject(DioError( + requestOptions: options, + error: e, + )); + } + } + + @override + Future onError(final DioError err, final ErrorInterceptorHandler handler) async { + final options = err.requestOptions; + + if (err.response?.statusCode != 401) { + return super.onError(err, handler); + } + + final attempt = err.requestOptions._retryAttempt + 1; + if (attempt > retries) { + await _onErrorRefreshingToken(); + return super.onError(err, handler); + } + + err.requestOptions._retryAttempt = attempt; + await Future.delayed(const Duration(seconds: 1)); + + /* Forzar el refresco de la token de autenticación */ + try { + await _authService.isLoggedIn(forceRefresh: true); + final token = (await _authService.getUser())?.token; + + if(token == null) { + await _onErrorRefreshingToken(); + return super.onError(err, handler); + } + + options._setAuthenticationHeader(token); + final response = await HttpClient.httpClient.fetch(options); + return handler.resolve(response); + } on DioError catch (e) { + super.onError(e, handler); + } catch (e) { + super.onError( + DioError(requestOptions: options, error: e), + handler, + ); + } + } + + Future _onErrorRefreshingToken() async => await _authService.logout(); +} + +extension AuthRequestOptionsX on RequestOptions { + void _setAuthenticationHeader(final String token) => headers['Authorization'] = 'Bearer $token'; + + int get _retryAttempt => (extra['auth_retry_attempt'] as int?) ?? 0; + + set _retryAttempt(final int attempt) => extra['auth_retry_attempt'] = attempt; +} \ No newline at end of file diff --git a/lib/utils/http/interceptors/headers_interceptor.dart b/lib/utils/http/interceptors/headers_interceptor.dart new file mode 100644 index 0000000..b2e1d60 --- /dev/null +++ b/lib/utils/http/interceptors/headers_interceptor.dart @@ -0,0 +1,18 @@ +import 'package:dio/dio.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class HeadersInterceptor extends Interceptor { + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { + final headers = options.headers; + final info = await PackageInfo.fromPlatform(); + + headers['User-Agent'] = 'App/MiUTEM v${info.version} (${info.buildNumber})'; + if(options.data != null) { + headers['Content-Type'] = 'application/json'; + } + options.headers = headers; + super.onRequest(options, handler); + } +} \ No newline at end of file diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 1aded68..e8151a3 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -148,7 +148,7 @@ class CustomDrawer extends StatelessWidget { ListTile( leading: const Icon(Mdi.closeCircle), title: const Text('Cerrar sesión'), - onTap: () async => await _authService.logout(context), + onTap: () async => await _authService.logout(context: context), ), ], ), diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index 6460de9..8c8b444 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -1,6 +1,7 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; @@ -33,6 +34,7 @@ class NoticiasCarruselWidget extends StatelessWidget { builder: (context, AsyncSnapshot?> snapshot) { if (snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException) : CustomException.custom("No pudimos obtener las noticias."); + logger.d("[NoticiasCarruselWidget] ${error.message} (${error.statusCode})", snapshot.error); return CustomErrorWidget( title: "Ocurrió un error al obtener las noticias", From 9c3eafdb2a008ea0aa2b4e8d1d75c73eb2ea7097 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:38:00 -0300 Subject: [PATCH 057/194] patch: reparado authClient no tiene cache Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/utils/http/http_client.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index 9a88f06..fb4b971 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -11,7 +11,6 @@ class HttpClient { static String debugUrl = dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl/'; static const String productionUrl = 'https://api.exdev.cl/'; static String url = isProduction ? productionUrl : debugUrl; - static Dio get initDio => Dio(BaseOptions(baseUrl: url)); static CacheConfig cacheConfig = CacheConfig( baseUrl: url, @@ -28,7 +27,7 @@ class HttpClient { dioCacheManager.interceptor, ]); - static Dio get authClient => initDio..interceptors.add(AuthInterceptor()); + static Dio get authClient => httpClient..interceptors.add(AuthInterceptor()); static InterceptorsWrapper _errorInterceptor = InterceptorsWrapper( onError: (DioError err, ErrorInterceptorHandler handler) { From 50f77505a434e6edb122e8d6f5498065a5fa43c4 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:47:52 -0300 Subject: [PATCH 058/194] =?UTF-8?q?patch:=20reparado=20cach=C3=A9=20de=20v?= =?UTF-8?q?arias=20pantallas=20y=20mejorado=20orden=20*=20Se=20repara=20el?= =?UTF-8?q?=20cach=C3=A9=20para=20horario,=20carrera,=20asignaturas=20y=20?= =?UTF-8?q?permisos.=20*=20Se=20repara=20recarga=20y=20mejora=20la=20panta?= =?UTF-8?q?lla=20de=20horario=20(no=20muestra=20horario=20vac=C3=ADo).=20*?= =?UTF-8?q?=20Se=20mejora=20la=20manera=20de=20cargar=20el=20quick=20menu?= =?UTF-8?q?=20de=20remote=20config.=20*=20Se=20agrega=20actualizaci=C3=B3n?= =?UTF-8?q?=20forzada=20de=20firebase=20al=20recargar=20app.=20*=20Se=20or?= =?UTF-8?q?denan=20m=C3=A1s=20clases=20a=20sus=20propias=20carpetas=20y=20?= =?UTF-8?q?archivos=20*=20Se=20mueve=20ibanner=20a=20su=20propio=20archivo?= =?UTF-8?q?=20de=20modelo=20*=20Se=20mueve=20banners=20a=20carpeta=20de=20?= =?UTF-8?q?novedades=20*=20Se=20mueven=20tarjeta=20de=20permisos=20y=20sec?= =?UTF-8?q?ci=C3=B3n=20de=20permisos=20a=20su=20carpeta=20*=20Se=20agrega?= =?UTF-8?q?=20carpeta=20de=20permiso=5Fingreso=20*=20Se=20agrega=20indicad?= =?UTF-8?q?or=20de=20carga=20"por=20defecto"=20para=20una=20generaci=C3=B3?= =?UTF-8?q?n=20m=C3=A1s=20r=C3=A1pida=20de=20widget.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../implementations/horario_controller.dart | 53 +--- .../interfaces/horario_controller.dart | 5 +- lib/models/exceptions/custom_exception.dart | 2 +- lib/models/novedades/ibanner.dart | 28 ++ .../asignatura/asignaturas_lista_screen.dart | 12 +- lib/screens/horario/horario_screen.dart | 171 ++++++++----- lib/screens/main_screen.dart | 15 +- lib/screens/permiso_covid_screen.dart | 240 ++---------------- .../implementations/carreras_service.dart | 4 +- lib/services/interfaces/carreras_service.dart | 2 +- lib/services/remote_config/remote_config.dart | 76 ++---- lib/utils/http/http_client.dart | 8 +- .../http/interceptors/auth_interceptor.dart | 5 + lib/widgets/banner.dart | 83 ------ lib/widgets/loading_indicator.dart | 10 +- lib/widgets/main_screen/novedades/banner.dart | 42 +++ .../novedades}/banners_section.dart | 13 +- .../permisos}/permiso_card.dart | 0 .../permisos}/permisos_section.dart | 6 +- .../permiso_ingreso/detalles_permiso.dart | 71 ++++++ lib/widgets/permiso_ingreso/qr_card.dart | 94 +++++++ .../permiso_ingreso/usuario_detalle.dart | 40 +++ lib/widgets/quick_menu_section.dart | 60 ++--- 23 files changed, 517 insertions(+), 523 deletions(-) create mode 100644 lib/models/novedades/ibanner.dart delete mode 100644 lib/widgets/banner.dart create mode 100644 lib/widgets/main_screen/novedades/banner.dart rename lib/widgets/{ => main_screen/novedades}/banners_section.dart (78%) rename lib/widgets/{ => main_screen/permisos}/permiso_card.dart (100%) rename lib/widgets/{ => main_screen/permisos}/permisos_section.dart (92%) create mode 100644 lib/widgets/permiso_ingreso/detalles_permiso.dart create mode 100644 lib/widgets/permiso_ingreso/qr_card.dart create mode 100644 lib/widgets/permiso_ingreso/usuario_detalle.dart diff --git a/lib/controllers/implementations/horario_controller.dart b/lib/controllers/implementations/horario_controller.dart index b66d312..b4bc5b6 100644 --- a/lib/controllers/implementations/horario_controller.dart +++ b/lib/controllers/implementations/horario_controller.dart @@ -31,12 +31,6 @@ class HorarioControllerImplementation implements HorarioController { @override Duration periodGap = Duration(minutes: 5); - @override - Rx horario = Rx(Horario()); - - @override - RxBool loadingHorario = false.obs; - @override List usedColors = []; @@ -96,37 +90,21 @@ class HorarioControllerImplementation implements HorarioController { } @override - Future getHorarioData({ bool forceRefresh = false }) async { - loadingHorario.value = true; - final now = DateTime.now(); - final lastUpdate = _storage.read("last_update_horario") ?? "${now.toIso8601String()}"; - final lastUpdateDate = DateTime.parse(lastUpdate); - final difference = now.difference(lastUpdateDate).inMinutes; - if(difference < 15 && forceRefresh == false && horario.value != null && lastUpdate != now.toIso8601String()) { - loadingHorario.value = false; - return; - } - - - horario.value = null; - + Future getHorario({ bool forceRefresh = false }) async { final carrerasService = Get.find(); Carrera? carrera = carrerasService.selectedCarrera; - if (carrera == null) { - await carrerasService.getCarreras(); + if (carrera == null || forceRefresh) { + await carrerasService.getCarreras(forceRefresh: forceRefresh); } carrera = carrerasService.selectedCarrera; final carreraId = carrera?.id; if(carreraId == null) { - loadingHorario.value = false; - return; + return null; } - horario.value = await Get.find().getHorario(carreraId); - _setRandomColorsByHorario(); - - loadingHorario.value = false; - _storage.write("last_update_horario", DateTime.now().toIso8601String()); + final horario = await Get.find().getHorario(carreraId, forceRefresh: forceRefresh); + if(horario != null) _setRandomColorsByHorario(horario); + return horario; } @override @@ -209,21 +187,14 @@ class HorarioControllerImplementation implements HorarioController { indicatorIsOpen.value = isOpen; } - void _setRandomColorsByHorario() { - final _horario = horario.value?.horario; - if (_horario == null) { + void _setRandomColorsByHorario(Horario horario) => horario.horario?.forEach((dia) => dia.forEach((bloque) { + final _asignatura = bloque.asignatura; + if (_asignatura == null) { return; } - _horario.forEach((dia) => dia.forEach((bloque) { - final _asignatura = bloque.asignatura; - if (_asignatura == null) { - return; - } - - addAsignaturaAndSetColor(bloque.asignatura!); - })); - } + addAsignaturaAndSetColor(bloque.asignatura!); + })); void _onChangeAnyController() { setIndicatorIsOpen(true); diff --git a/lib/controllers/interfaces/horario_controller.dart b/lib/controllers/interfaces/horario_controller.dart index 19c3fc3..032b470 100644 --- a/lib/controllers/interfaces/horario_controller.dart +++ b/lib/controllers/interfaces/horario_controller.dart @@ -11,9 +11,6 @@ abstract class HorarioController { abstract Duration periodDuration; abstract Duration periodGap; - abstract Rx horario; - abstract RxBool loadingHorario; - abstract List usedColors; abstract RxDouble zoom; abstract RxBool indicatorIsOpen; @@ -34,7 +31,7 @@ abstract class HorarioController { void init(BuildContext context); - Future getHorarioData({ bool forceRefresh = false }); + Future getHorario({ bool forceRefresh = false }); void moveViewportToCurrentPeriodAndDay(BuildContext context); diff --git a/lib/models/exceptions/custom_exception.dart b/lib/models/exceptions/custom_exception.dart index eb272db..8d2eb07 100644 --- a/lib/models/exceptions/custom_exception.dart +++ b/lib/models/exceptions/custom_exception.dart @@ -20,7 +20,7 @@ class CustomException implements Exception { message: json['mensaje'] as String, error: json['error'] as String?, statusCode: json['codigoHttp'] as int?, - internalCode: json['codigoInterno'] as double?, + internalCode: json['codigoInterno'] is num ? double.tryParse("${json['codigoInterno']}") : json['codigoInterno'] as double?, ); toJson() => { diff --git a/lib/models/novedades/ibanner.dart b/lib/models/novedades/ibanner.dart new file mode 100644 index 0000000..8cc80ff --- /dev/null +++ b/lib/models/novedades/ibanner.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:hexcolor/hexcolor.dart'; + +class IBanner { + final String id; + final String name; + final Color backgroundColor; + final String url; + final String imageUrl; + + const IBanner({ + required this.id, + required this.name, + required this.backgroundColor, + required this.url, + required this.imageUrl, + }); + + factory IBanner.fromJson(Map json) => IBanner( + id: json["id"], + name: json["name"], + backgroundColor: HexColor(json["backgroundColor"]), + url: json["url"], + imageUrl: json["imageUrl"], + ); + + static List fromJsonList(dynamic json) => json != null ? (json as List).map((x) => IBanner.fromJson(x)).toList() : []; +} \ No newline at end of file diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index e19dfd4..d85ab11 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -3,10 +3,10 @@ import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/widgets/asignatura/lista/lista_asignaturas.dart'; import 'package:mi_utem/widgets/asignatura/lista/sin_asignaturas_mensaje.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; @@ -22,6 +22,7 @@ class AsignaturasListaScreen extends StatefulWidget { class _AsignaturasListaScreenState extends State { final _asignaturasService = Get.find(); + bool _forceRefresh = false; bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; @@ -38,9 +39,7 @@ class _AsignaturasListaScreenState extends State { ] : [], ), body: PullToRefresh( - onRefresh: () async { - setState(() => {}); - }, + onRefresh: () async => setState(() => _forceRefresh = true), child: FutureBuilder?>( future: () async { final carrerasService = Get.find(); @@ -49,8 +48,9 @@ class _AsignaturasListaScreenState extends State { } final selectedCarrera = carrerasService.selectedCarrera; - - return await _asignaturasService.getAsignaturas(selectedCarrera?.id); + final data = await _asignaturasService.getAsignaturas(selectedCarrera?.id, forceRefresh: _forceRefresh); + _forceRefresh = false; + return data; }(), builder: (context, snapshot) { if(snapshot.hasError) { diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index 7418a29..bbc0392 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -3,16 +3,16 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; +import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; -import 'package:mi_utem/widgets/snackbar.dart'; import 'package:path_provider/path_provider.dart'; import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; @@ -28,8 +28,110 @@ class _HorarioScreenState extends State { final ScreenshotController _screenshotController = ScreenshotController(); + bool _forceRefresh = false; final horarioController = Get.find(); + @override + void initState() { + ReviewService.addScreen("HorarioScreen"); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeRight, + DeviceOrientation.landscapeLeft, + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + _forceRefresh = false; + super.initState(); + } + + @override + Widget build(BuildContext context) { + horarioController.init(context); + return FutureBuilder( + future: () async { + final data = await horarioController.getHorario(forceRefresh: _forceRefresh); + _forceRefresh = false; + return data; + }(), + builder: (context, snapshot) { + if(snapshot.hasError) { + final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al cargar el horario! Por favor intenta más tarde."; + return Scaffold( + appBar: CustomAppBar( + title: Text("Horario"), + ), + body: Center( + child: CustomErrorWidget( + title: "Error al cargar el horario", + error: error, + ), + ), + ); + } + + if(snapshot.connectionState == ConnectionState.waiting) { + return Scaffold( + appBar: CustomAppBar( + title: Text("Horario"), + ), + body: LoadingIndicator.centeredDefault(), + ); + } + + final horario = snapshot.data; + if(!snapshot.hasData || horario == null) { + return Scaffold( + appBar: CustomAppBar( + title: Text("Horario"), + actions: [ + IconButton( + onPressed: _reloadData, + icon: Icon(Icons.refresh_sharp), + tooltip: "Forzar actualización del horario", + ) + ], + ), + body: Center( + child: CustomErrorWidget( + title: "Error al cargar el horario", + error: "Ocurrió un error al cargar el horario! Por favor intenta más tarde.", + ), + ), + ); + } + + return Obx(() => Scaffold( + appBar: CustomAppBar( + title: Text("Horario"), + actions: [ + IconButton( + onPressed: _reloadData, + icon: Icon(Icons.refresh_sharp), + tooltip: "Forzar actualización del horario", + ), + if(!horarioController.isCenteredInCurrentPeriodAndDay.value) IconButton( + onPressed: _moveViewportToCurrentTime, + icon: Icon(Icons.center_focus_strong), + tooltip: "Centrar Horario En Hora Actual", + ), + IconButton( + onPressed: () => _captureAndShareScreenshot(horario), + icon: Icon(Icons.share), + tooltip: "Compartir Horario", + ) + ], + ), + body: Screenshot( + controller: _screenshotController, + child: HorarioMainScroller( + horario: horario, + ), + ), + )); + }, + ); + } + void _moveViewportToCurrentTime() { AnalyticsService.logEvent("horario_move_viewport_to_current_time"); horarioController.moveViewportToCurrentPeriodAndDay(context); @@ -57,68 +159,5 @@ class _HorarioScreenState extends State { await Share.shareXFiles([XFile(imagePath.path)]); } - @override - void initState() { - horarioController.getHorarioData().catchError((err) => { - logger.e("Error al cargar el horario", err), - showErrorSnackbar(context, "Ocurrió un error al cargar el horario! Por favor intenta más tarde.") - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - ReviewService.addScreen("HorarioScreen"); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - horarioController.init(context); - - - return Obx(() => Scaffold( - appBar: CustomAppBar( - title: Text("Horario"), - actions: [ - if(horarioController.horario.value != null) IconButton( - onPressed: () => horarioController.getHorarioData(forceRefresh: true).then((value) { - setState(() {}); - }), - icon: Icon(Icons.refresh_sharp), - tooltip: "Forzar actualización del horario", - ), - if(horarioController.horario.value != null && !horarioController.isCenteredInCurrentPeriodAndDay.value) IconButton( - onPressed: () => _moveViewportToCurrentTime(), - icon: Icon(Icons.center_focus_strong), - tooltip: "Centrar Horario En Hora Actual", - ), - if(horarioController.horario.value != null) IconButton( - onPressed: () => _captureAndShareScreenshot(horarioController.horario.value!), - icon: Icon(Icons.share), - tooltip: "Compartir Horario", - ) - ], - ), - body: ((horarioController.loadingHorario.value && horarioController.horario.value == null) || horarioController.horario.value == null) ? Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Expanded( - child: Center( - child: LoadingIndicator(), - ), - ), - ], - ), - ) : Screenshot( - controller: _screenshotController, - child: HorarioMainScroller( - horario: horarioController.horario.value!, - ), - ), - )); - } + void _reloadData() => setState(() => _forceRefresh = true); } diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index a243a79..203309f 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -6,18 +6,20 @@ import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_markdown/flutter_markdown.dart"; import "package:get/get.dart"; +import "package:mi_utem/models/novedades/ibanner.dart"; import "package:mi_utem/models/user/user.dart"; +import "package:mi_utem/repositories/interfaces/noticias_repository.dart"; +import "package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart"; import "package:mi_utem/repositories/interfaces/preferences_repository.dart"; import "package:mi_utem/services/interfaces/auth_service.dart"; import "package:mi_utem/services/interfaces/grades_service.dart"; import "package:mi_utem/services/remote_config/remote_config.dart"; import "package:mi_utem/services/review_service.dart"; -import "package:mi_utem/widgets/banner.dart"; -import 'package:mi_utem/widgets/banners_section.dart'; import "package:mi_utem/widgets/custom_app_bar.dart"; import "package:mi_utem/widgets/custom_drawer.dart"; +import "package:mi_utem/widgets/main_screen/novedades/banners_section.dart"; +import "package:mi_utem/widgets/main_screen/permisos/permisos_section.dart"; import "package:mi_utem/widgets/noticias/noticias_carrusel_widget.dart"; -import "package:mi_utem/widgets/permisos_section.dart"; import "package:mi_utem/widgets/pull_to_refresh.dart"; import "package:mi_utem/widgets/quick_menu_section.dart"; @@ -62,7 +64,10 @@ class _MainScreenState extends State { } Future loadData() async { - setState(() => _banners = RemoteConfigService.banners); + await RemoteConfigService.update(); + await Get.find().getPermisos(forceRefresh: true); // Forzar re-descarga de los permisos + await Get.find().getNoticias(forceRefresh: true); // Forzar re-descarga de las noticias + setState(() => _banners = RemoteConfigService.banners); // Actualizar los banners y se re-renderiza } String get _greetingText { @@ -101,7 +106,7 @@ class _MainScreenState extends State { const SizedBox(height: 20), PermisosCovidSection(), const SizedBox(height: 20), - QuickMenuSection(), + const QuickMenuSection(), const SizedBox(height: 20), if (_banners.isNotEmpty) ...[ BannersSection(banners: _banners), diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index 77f0751..f78cc95 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -1,29 +1,22 @@ -import 'dart:typed_data'; - -import 'package:barcode_image/barcode_image.dart'; -import 'package:barcode_widget/barcode_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:get/get.dart'; -import 'package:image/image.dart' as dartImage; -import 'package:intl/intl.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; -import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/field_list_tile.dart'; -import 'package:mi_utem/widgets/image_view_screen.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; -import 'package:mi_utem/widgets/profile_photo.dart'; +import 'package:mi_utem/widgets/permiso_ingreso/qr_card.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; class PermisoCovidScreen extends StatefulWidget { final String passId; - const PermisoCovidScreen({super.key, required this.passId}); + const PermisoCovidScreen({ + super.key, + required this.passId, + }); @override State createState() => _PermisoCovidScreenState(); @@ -31,233 +24,44 @@ class PermisoCovidScreen extends StatefulWidget { class _PermisoCovidScreenState extends State { + PermisoIngresoRepository _permisoIngresoRepository = Get.find(); + @override Widget build(BuildContext context) => Scaffold( appBar: CustomAppBar(title: Text("Permiso de ingreso")), body: PullToRefresh( - onRefresh: () async => setState(() {}), + onRefresh: () async { + await _permisoIngresoRepository.getDetallesPermiso(widget.passId, forceRefresh: true); + setState(() {}); + }, child: FutureBuilder( - future: Get.find().getDetallesPermiso(widget.passId), + future: _permisoIngresoRepository.getDetallesPermiso(widget.passId), builder: (context, snapshot) { if (snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "No sabemos lo que ocurrió. Por favor intenta más tarde."; logger.e("Error al cargar permiso", snapshot.error); - return Center( - child: CustomErrorWidget(error: error), - ); + return CustomErrorWidget(error: error); } final permiso = snapshot.data; if(!snapshot.hasData) { - return Center( - child: LoadingIndicator( - message: "Esto tardará un poco, paciencia...", - ), - ); + return LoadingIndicator.centeredDefault(); } if(permiso == null) { - return const Center( - child: CustomErrorWidget( - title: "Permiso no encontrado", - emoji: "\u{1F914}", - ), + return const CustomErrorWidget( + title: "Permiso no encontrado", + error: "Lo sentimos, no pudimos encontrar el permiso de ingreso. Por favor intenta más tarde.", + emoji: "\u{1F914}", ); } - return SingleChildScrollView(child: QRCard(permiso: permiso)); + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: QRCard(permiso: permiso), + ); }, ), ), ); } - - -class QRCard extends StatelessWidget { - const QRCard({ - Key? key, - required this.permiso, - }) : super(key: key); - - final PermisoIngreso permiso; - - _openQr(BuildContext context, String heroTag) { - final image = dartImage.Image(500, 500); - - dartImage.fill(image, dartImage.getColor(255, 255, 255)); - drawBarcode( - image, - Barcode.qrCode(), - permiso.codigoQr!, - x: 25, - y: 25, - width: 450, - height: 450, - ); - - Uint8List data = Uint8List.fromList(dartImage.encodePng(image)); - - Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen( - imageProvider: MemoryImage(data), - heroTag: heroTag, - occlude: true, - ))); - } - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(20.0), - child: Card( - child: Column( - children: [ - UsuarioDetalle( - user: permiso.user!, - ), - Divider(thickness: 1, color: Color(0xFFFEEEEE)), - DetallesPermiso( - campus: permiso.campus, - dependencias: permiso.dependencia, - jornada: permiso.jornada, - vigencia: permiso.vigencia, - motivo: permiso.motivo, - ), - Divider(thickness: 1, color: Color(0xFFFEEEEE)), - Container(height: 20), - Center( - child: InkWell( - onTap: () => _openQr(context, "qr_${permiso.codigoQr!}"), - child: Hero( - tag: "qr_${permiso.codigoQr!}", - child: Container( - color: Colors.white, - padding: EdgeInsets.all(10), - child: OccludeWrapper( - child: BarcodeWidget( - barcode: Barcode.qrCode(), - height: 200, - width: 200, - data: permiso.codigoQr!, - drawText: false, - ), - ), - ), - ), - ), - ), - Container(height: 20), - Text("Permiso generado el ${DateFormat('dd/MM/yyyy').format(permiso.fechaSolicitud!)}", - style: Theme.of(context).textTheme.bodySmall, - ), - Container(height: 20), - ], - ), - ), - ); -} - -class UsuarioDetalle extends StatelessWidget { - final User user; - - const UsuarioDetalle({ - super.key, - required this.user, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20.0), - child: Row( - children: [ - ProfilePhoto(user: user), - Container(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(user.nombreCompletoCapitalizado, - maxLines: 2, - style: Theme.of(context).textTheme.bodyLarge, - ), - Container(height: 4), - Text("${user.rut}", - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - ), - ], - ), - ); - } -} - -class DetallesPermiso extends StatelessWidget { - const DetallesPermiso({ - Key? key, - this.campus, - this.dependencias, - this.jornada, - this.vigencia, - this.motivo, - }) : super(key: key); - - final String? campus, dependencias, jornada, vigencia, motivo; - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FieldListTile( - padding: EdgeInsets.zero, - title: "Motivo", - value: motivo, - ), - if (campus != null || dependencias != null) Container(height: 20), - if (campus != null || dependencias != null) - Row( - children: [ - Expanded( - child: FieldListTile( - padding: EdgeInsets.zero, - title: "Campus", - value: campus, - ), - ), - Expanded( - child: FieldListTile( - padding: EdgeInsets.zero, - title: "Dependencias", - value: dependencias, - ), - ), - ], - ), - if (jornada != null || vigencia != null) Container(height: 20), - if (jornada != null || vigencia != null) - Row( - children: [ - Expanded( - child: FieldListTile( - padding: EdgeInsets.zero, - title: "Jornada", - value: jornada, - ), - ), - Expanded( - child: FieldListTile( - padding: EdgeInsets.zero, - title: "Vigencia", - value: vigencia, - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/services/implementations/carreras_service.dart b/lib/services/implementations/carreras_service.dart index ec3c1b5..13c0a5b 100644 --- a/lib/services/implementations/carreras_service.dart +++ b/lib/services/implementations/carreras_service.dart @@ -17,9 +17,9 @@ class CarrerasServiceImplementation implements CarrerasService { Carrera? selectedCarrera; @override - Future getCarreras() async { + Future getCarreras({ bool forceRefresh = false }) async { logger.d("[CarrerasService#getCarreras]: Obteniendo carreras..."); - final _carreras = await _carrerasRepository.getCarreras(); + final _carreras = await _carrerasRepository.getCarreras(forceRefresh: forceRefresh); carreras.clear(); carreras.addAll(_carreras); diff --git a/lib/services/interfaces/carreras_service.dart b/lib/services/interfaces/carreras_service.dart index ee28e10..35101e1 100644 --- a/lib/services/interfaces/carreras_service.dart +++ b/lib/services/interfaces/carreras_service.dart @@ -5,7 +5,7 @@ abstract class CarrerasService { abstract List carreras; abstract Carrera? selectedCarrera; - Future getCarreras(); + Future getCarreras({ bool forceRefresh = false }); void changeSelectedCarrera(Carrera carrera); diff --git a/lib/services/remote_config/remote_config.dart b/lib/services/remote_config/remote_config.dart index c288086..8264ea7 100644 --- a/lib/services/remote_config/remote_config.dart +++ b/lib/services/remote_config/remote_config.dart @@ -4,8 +4,9 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mdi/mdi.dart'; +import 'package:mi_utem/config/logger.dart'; +import 'package:mi_utem/models/novedades/ibanner.dart'; import 'package:mi_utem/services/remote_config/keys.dart'; -import 'package:mi_utem/widgets/banner.dart'; part 'defaults.dart'; @@ -16,48 +17,33 @@ class RemoteConfigService { static final defaults = _defaults; - static String _getString(String key) => - _firebaseRemoteConfigInstance.getString(key); - static bool _getBool(String key) => - _firebaseRemoteConfigInstance.getBool(key); - static double _getDouble(String key) => - _firebaseRemoteConfigInstance.getDouble(key); + static String _getString(String key) => _firebaseRemoteConfigInstance.getString(key); + static bool _getBool(String key) => _firebaseRemoteConfigInstance.getBool(key); + static double _getDouble(String key) => _firebaseRemoteConfigInstance.getDouble(key); static int _getInt(String key) => _firebaseRemoteConfigInstance.getInt(key); - static final banners = IBanner.fromJsonList( - jsonDecode(_getString(RemoteConfigServiceKeys.banners))); + static final banners = IBanner.fromJsonList(jsonDecode(_getString(RemoteConfigServiceKeys.banners))); static final creditos = _getString(RemoteConfigServiceKeys.creditos); static final clubRedes = _getString(RemoteConfigServiceKeys.clubRedes); static final clubNombre = _getString(RemoteConfigServiceKeys.clubNombre); - static final clubDescripcion = - _getString(RemoteConfigServiceKeys.clubDescripcion); + static final clubDescripcion = _getString(RemoteConfigServiceKeys.clubDescripcion); static final clubLogo = _getString(RemoteConfigServiceKeys.clubLogo); - static final miutemDescripcion = - _getString(RemoteConfigServiceKeys.miutemDescripcion); - static final miutemDesarrolladores = - _getString(RemoteConfigServiceKeys.miutemDesarrolladores); - static final miutemPortada = - _getString(RemoteConfigServiceKeys.miutemPortada); - static final credencialBarras = - _getString(RemoteConfigServiceKeys.credencialBarras); - static final credencialDisclaimer = - _getString(RemoteConfigServiceKeys.credencialDisclaimer); - static final credencialInfo = - _getString(RemoteConfigServiceKeys.credencialInfo); - static final credencialSibutemLogo = - _getString(RemoteConfigServiceKeys.credencialSibutemLogo); - static final calculadoraMostrar = - _getBool(RemoteConfigServiceKeys.calculadoraMostrar); + static final miutemDescripcion = _getString(RemoteConfigServiceKeys.miutemDescripcion); + static final miutemDesarrolladores = _getString(RemoteConfigServiceKeys.miutemDesarrolladores); + static final miutemPortada = _getString(RemoteConfigServiceKeys.miutemPortada); + static final credencialBarras = _getString(RemoteConfigServiceKeys.credencialBarras); + static final credencialDisclaimer = _getString(RemoteConfigServiceKeys.credencialDisclaimer); + static final credencialInfo = _getString(RemoteConfigServiceKeys.credencialInfo); + static final credencialSibutemLogo = _getString(RemoteConfigServiceKeys.credencialSibutemLogo); + static final calculadoraMostrar = _getBool(RemoteConfigServiceKeys.calculadoraMostrar); static final horarioZoom = _getDouble(RemoteConfigServiceKeys.horarioZoom); - static final homeProntoIcono = - _getInt(RemoteConfigServiceKeys.homeProntoIcono); - static final homeProntoTexto = - _getString(RemoteConfigServiceKeys.homeProntoTexto); + static final homeProntoIcono = _getInt(RemoteConfigServiceKeys.homeProntoIcono); + static final homeProntoTexto = _getString(RemoteConfigServiceKeys.homeProntoTexto); static final prontoEg = _getString(RemoteConfigServiceKeys.prontoEg); static final egHabilitados = _getBool(RemoteConfigServiceKeys.egHabilitados); static final drawerMenu = _getString(RemoteConfigServiceKeys.drawerMenu); static final greetings = _getString(RemoteConfigServiceKeys.greetings); - static final quickMenu = _getString(RemoteConfigServiceKeys.quickMenu); + static final quickMenu = (jsonDecode(_getString(RemoteConfigServiceKeys.quickMenu)) as List).map((e) => e as Map).toList(); factory RemoteConfigService() => instance; @@ -68,29 +54,23 @@ class RemoteConfigService { await _firebaseRemoteConfigInstance.setDefaults(defaults); await _firebaseRemoteConfigInstance.fetchAndActivate(); } catch (exception) { - print( - 'Unable to fetch remote config. Cached or default values will be used'); + logger.e('Error al descargar la configuración remota. Se usarán los valores guardados en cache o los valores por defecto', exception); } } static Future update() async { try { - await _firebaseRemoteConfigInstance.setConfigSettings( - RemoteConfigSettings( - minimumFetchInterval: Duration.zero, - fetchTimeout: Duration(minutes: 1), - ), - ); + await _firebaseRemoteConfigInstance.setConfigSettings(RemoteConfigSettings( + minimumFetchInterval: Duration.zero, + fetchTimeout: Duration(minutes: 1), + )); await _firebaseRemoteConfigInstance.fetchAndActivate(); - await _firebaseRemoteConfigInstance.setConfigSettings( - RemoteConfigSettings( - minimumFetchInterval: Duration(hours: 12), - fetchTimeout: Duration(minutes: 1), - ), - ); + await _firebaseRemoteConfigInstance.setConfigSettings(RemoteConfigSettings( + minimumFetchInterval: Duration(hours: 12), + fetchTimeout: Duration(minutes: 1), + )); } catch (exception) { - print( - 'Unable to fetch remote config. Cached or default values will be used'); + logger.e('Error al descargar la configuración remota. Se usarán los valores guardados en cache o los valores por defecto', exception); } } } diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index fb4b971..a1e7d10 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -31,12 +31,16 @@ class HttpClient { static InterceptorsWrapper _errorInterceptor = InterceptorsWrapper( onError: (DioError err, ErrorInterceptorHandler handler) { + if(err.response?.statusCode == 401) { + return handler.next(err); + } + final json = err.response?.data ?? {}; if(json is Map && json.containsKey("error")) { - throw CustomException.fromJson(json as Map); + return handler.reject(DioError(requestOptions: err.requestOptions, error: CustomException.fromJson(json as Map))); } - throw CustomException.custom(err.response?.statusMessage); + return handler.reject(DioError(requestOptions: err.requestOptions, error: CustomException.custom(err.response?.statusMessage))); }, ); diff --git a/lib/utils/http/interceptors/auth_interceptor.dart b/lib/utils/http/interceptors/auth_interceptor.dart index bf0b7ca..b1387de 100644 --- a/lib/utils/http/interceptors/auth_interceptor.dart +++ b/lib/utils/http/interceptors/auth_interceptor.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/utils/http/http_client.dart'; @@ -41,19 +42,23 @@ class AuthInterceptor extends QueuedInterceptor { return super.onError(err, handler); } + logger.d("*"); final attempt = err.requestOptions._retryAttempt + 1; if (attempt > retries) { await _onErrorRefreshingToken(); return super.onError(err, handler); } + logger.d("**"); err.requestOptions._retryAttempt = attempt; await Future.delayed(const Duration(seconds: 1)); /* Forzar el refresco de la token de autenticación */ try { + logger.d("***"); await _authService.isLoggedIn(forceRefresh: true); final token = (await _authService.getUser())?.token; + logger.d("****", token); if(token == null) { await _onErrorRefreshingToken(); diff --git a/lib/widgets/banner.dart b/lib/widgets/banner.dart deleted file mode 100644 index bbd416d..0000000 --- a/lib/widgets/banner.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:hexcolor/hexcolor.dart'; -import 'package:mi_utem/services/analytics_service.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class IBanner { - final String id; - final String name; - final Color backgroundColor; - final String url; - final String imageUrl; - - IBanner({ - required this.id, - required this.name, - required this.backgroundColor, - required this.url, - required this.imageUrl, - }); - - factory IBanner.fromJson(Map json) { - return IBanner( - id: json["id"], - name: json["name"], - backgroundColor: HexColor(json["backgroundColor"]), - url: json["url"], - imageUrl: json["imageUrl"], - ); - } - - static List fromJsonList(dynamic json) { - if (json == null) { - return []; - } - List list = []; - for (var item in json) { - list.add(IBanner.fromJson(item)); - } - return list; - } -} - -class MiUtemBanner extends StatelessWidget { - final IBanner banner; - - const MiUtemBanner({ - Key? key, - required this.banner, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) => Container( - width: constraints.maxWidth, - height: constraints.maxWidth * 0.4, - child: InkWell( - onTap: _onTap, - child: Card( - margin: EdgeInsets.zero, - color: banner.backgroundColor, - borderOnForeground: false, - child: CachedNetworkImage( - imageUrl: banner.imageUrl, - fit: BoxFit.cover, - ), - ), - ), - ), - ); - } - - void _onTap() async { - AnalyticsService.logEvent( - "banner_tap", - parameters: { - "banner_id": banner.id, - }, - ); - await launchUrl(Uri.parse(banner.url)); - } -} diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart index 686e790..95c1de9 100644 --- a/lib/widgets/loading_indicator.dart +++ b/lib/widgets/loading_indicator.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_spinkit/flutter_spinkit.dart'; class LoadingIndicator extends StatelessWidget { @@ -36,4 +35,13 @@ class LoadingIndicator extends StatelessWidget { ), ), ); + + static Widget centered({required String message}) => Center( + child: LoadingIndicator( + message: message, + ), + ); + + static Widget centeredDefault() => centered(message: "Esto tardará un poco, paciencia..."); + } diff --git a/lib/widgets/main_screen/novedades/banner.dart b/lib/widgets/main_screen/novedades/banner.dart new file mode 100644 index 0000000..d9318cc --- /dev/null +++ b/lib/widgets/main_screen/novedades/banner.dart @@ -0,0 +1,42 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/novedades/ibanner.dart'; +import 'package:mi_utem/services/analytics_service.dart'; +import 'package:url_launcher/url_launcher.dart'; + + +class MiUtemBanner extends StatelessWidget { + final IBanner banner; + + const MiUtemBanner({ + super.key, + required this.banner, + }); + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) => Container( + width: constraints.maxWidth, + height: constraints.maxWidth * 0.4, + child: InkWell( + onTap: _onTap, + child: Card( + margin: EdgeInsets.zero, + color: banner.backgroundColor, + borderOnForeground: false, + child: CachedNetworkImage( + imageUrl: banner.imageUrl, + fit: BoxFit.cover, + ), + ), + ), + ), + ); + + void _onTap() async { + AnalyticsService.logEvent("banner_tap", parameters: { + "banner_id": banner.id, + }); + await launchUrl(Uri.parse(banner.url)); + } +} diff --git a/lib/widgets/banners_section.dart b/lib/widgets/main_screen/novedades/banners_section.dart similarity index 78% rename from lib/widgets/banners_section.dart rename to lib/widgets/main_screen/novedades/banners_section.dart index 4e6c3d1..0f71051 100644 --- a/lib/widgets/banners_section.dart +++ b/lib/widgets/main_screen/novedades/banners_section.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/banner.dart'; +import 'package:mi_utem/models/novedades/ibanner.dart'; +import 'package:mi_utem/widgets/main_screen/novedades/banner.dart'; class BannersSection extends StatelessWidget { final List banners; const BannersSection({ - Key? key, + super.key, required this.banners, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -21,8 +22,7 @@ class BannersSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - "Novedades".toUpperCase(), + Text("Novedades".toUpperCase(), textAlign: TextAlign.left, style: Theme.of(context).textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, @@ -32,8 +32,7 @@ class BannersSection extends StatelessWidget { ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => - MiUtemBanner(banner: banners[index]), + itemBuilder: (context, index) => MiUtemBanner(banner: banners[index]), separatorBuilder: (context, index) => Container(height: 10), itemCount: banners.length, ), diff --git a/lib/widgets/permiso_card.dart b/lib/widgets/main_screen/permisos/permiso_card.dart similarity index 100% rename from lib/widgets/permiso_card.dart rename to lib/widgets/main_screen/permisos/permiso_card.dart diff --git a/lib/widgets/permisos_section.dart b/lib/widgets/main_screen/permisos/permisos_section.dart similarity index 92% rename from lib/widgets/permisos_section.dart rename to lib/widgets/main_screen/permisos/permisos_section.dart index 2f5fdd4..fec64fa 100644 --- a/lib/widgets/permisos_section.dart +++ b/lib/widgets/main_screen/permisos/permisos_section.dart @@ -4,7 +4,7 @@ import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; -import 'package:mi_utem/widgets/permiso_card.dart'; +import 'package:mi_utem/widgets/main_screen/permisos/permiso_card.dart'; class PermisosCovidSection extends StatelessWidget { @@ -35,9 +35,7 @@ class PermisosCovidSection extends StatelessWidget { future: Get.find().getPermisos(), builder: (context, snapshot) { if(snapshot.connectionState == ConnectionState.waiting) { - return LoadingIndicator( - message: "Esto tardará un poco, paciencia...", - ); + return LoadingIndicator.centeredDefault(); } if(snapshot.hasError) { diff --git a/lib/widgets/permiso_ingreso/detalles_permiso.dart b/lib/widgets/permiso_ingreso/detalles_permiso.dart new file mode 100644 index 0000000..ee95a82 --- /dev/null +++ b/lib/widgets/permiso_ingreso/detalles_permiso.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/widgets/field_list_tile.dart'; + +class DetallesPermiso extends StatelessWidget { + const DetallesPermiso({ + super.key, + this.campus, + this.dependencias, + this.jornada, + this.vigencia, + this.motivo, + }); + + final String? campus, dependencias, jornada, vigencia, motivo; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FieldListTile( + padding: EdgeInsets.zero, + title: "Motivo", + value: motivo, + ), + if (campus != null || dependencias != null) Container(height: 20), + if (campus != null || dependencias != null) + Row( + children: [ + Expanded( + child: FieldListTile( + padding: EdgeInsets.zero, + title: "Campus", + value: campus, + ), + ), + Expanded( + child: FieldListTile( + padding: EdgeInsets.zero, + title: "Dependencias", + value: dependencias, + ), + ), + ], + ), + if (jornada != null || vigencia != null) Container(height: 20), + if (jornada != null || vigencia != null)Row( + children: [ + Expanded( + child: FieldListTile( + padding: EdgeInsets.zero, + title: "Jornada", + value: jornada, + ), + ), + Expanded( + child: FieldListTile( + padding: EdgeInsets.zero, + title: "Vigencia", + value: vigencia, + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/permiso_ingreso/qr_card.dart b/lib/widgets/permiso_ingreso/qr_card.dart new file mode 100644 index 0000000..5039d7e --- /dev/null +++ b/lib/widgets/permiso_ingreso/qr_card.dart @@ -0,0 +1,94 @@ +import 'dart:typed_data'; + +import 'package:barcode_image/barcode_image.dart'; +import 'package:barcode_widget/barcode_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_uxcam/flutter_uxcam.dart'; +import 'package:image/image.dart' as dartImage; +import 'package:intl/intl.dart'; +import 'package:mi_utem/models/permiso_ingreso.dart'; +import 'package:mi_utem/widgets/image_view_screen.dart'; +import 'package:mi_utem/widgets/permiso_ingreso/detalles_permiso.dart'; +import 'package:mi_utem/widgets/permiso_ingreso/usuario_detalle.dart'; + +class QRCard extends StatelessWidget { + const QRCard({ + super.key, + required this.permiso, + }); + + final PermisoIngreso permiso; + + _openQr(BuildContext context, String heroTag) { + final image = dartImage.Image(500, 500); + + dartImage.fill(image, dartImage.getColor(255, 255, 255)); + drawBarcode( + image, + Barcode.qrCode(), + permiso.codigoQr!, + x: 25, + y: 25, + width: 450, + height: 450, + ); + + Uint8List data = Uint8List.fromList(dartImage.encodePng(image)); + + Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen( + imageProvider: MemoryImage(data), + heroTag: heroTag, + occlude: true, + ))); + } + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(20.0), + child: Card( + child: Column( + children: [ + UsuarioDetalle( + user: permiso.user!, + ), + Divider(thickness: 1, color: Color(0xFFFEEEEE)), + DetallesPermiso( + campus: permiso.campus, + dependencias: permiso.dependencia, + jornada: permiso.jornada, + vigencia: permiso.vigencia, + motivo: permiso.motivo, + ), + Divider(thickness: 1, color: Color(0xFFFEEEEE)), + Container(height: 20), + Center( + child: InkWell( + onTap: () => _openQr(context, "qr_${permiso.codigoQr!}"), + child: Hero( + tag: "qr_${permiso.codigoQr!}", + child: Container( + color: Colors.white, + padding: EdgeInsets.all(10), + child: OccludeWrapper( + child: BarcodeWidget( + barcode: Barcode.qrCode(), + height: 200, + width: 200, + data: permiso.codigoQr!, + drawText: false, + ), + ), + ), + ), + ), + ), + Container(height: 20), + Text("Permiso generado el ${DateFormat('dd/MM/yyyy').format(permiso.fechaSolicitud!)}", + style: Theme.of(context).textTheme.bodySmall, + ), + Container(height: 20), + ], + ), + ), + ); +} \ No newline at end of file diff --git a/lib/widgets/permiso_ingreso/usuario_detalle.dart b/lib/widgets/permiso_ingreso/usuario_detalle.dart new file mode 100644 index 0000000..e2b800f --- /dev/null +++ b/lib/widgets/permiso_ingreso/usuario_detalle.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/widgets/profile_photo.dart'; + +class UsuarioDetalle extends StatelessWidget { + final User user; + + const UsuarioDetalle({ + super.key, + required this.user, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + children: [ + ProfilePhoto(user: user), + Container(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(user.nombreCompletoCapitalizado, + maxLines: 2, + style: Theme.of(context).textTheme.bodyLarge, + ), + Container(height: 4), + Text("${user.rut}", + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/quick_menu_section.dart b/lib/widgets/quick_menu_section.dart index da59d1e..ce7c795 100644 --- a/lib/widgets/quick_menu_section.dart +++ b/lib/widgets/quick_menu_section.dart @@ -1,46 +1,38 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/widgets/quick_menu_card.dart'; class QuickMenuSection extends StatelessWidget { - const QuickMenuSection({Key? key}) : super(key: key); + const QuickMenuSection({ + super.key + }); - List get _quickMenu { - return jsonDecode(RemoteConfigService.quickMenu); - } + List> get _quickMenu => RemoteConfigService.quickMenu; @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 20), - child: Text( - "Acceso rápido".toUpperCase(), - style: Theme.of(context).textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.bold, - ), + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text("Acceso rápido".toUpperCase(), + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, ), ), - Container(height: 10), - Container( - height: 130, - child: ListView.separated( - itemCount: _quickMenu.length, - padding: EdgeInsets.symmetric(horizontal: 20), - scrollDirection: Axis.horizontal, - separatorBuilder: (context, index) => Container(width: 10), - itemBuilder: (context, index) { - Map e = _quickMenu[index]; - return QuickMenuCard(card: e); - }, - ), + ), + const SizedBox(height: 10), + SizedBox( + height: 130, + child: ListView.separated( + itemCount: _quickMenu.length, + padding: const EdgeInsets.symmetric(horizontal: 20), + scrollDirection: Axis.horizontal, + separatorBuilder: (context, index) => const SizedBox(width: 10), + itemBuilder: (context, index) => QuickMenuCard(card: _quickMenu[index]), ), - ], - ); - } + ), + ], + ); } From 437b829ba16fb6757828cd4da50156dcfbaeabe7 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:25:50 -0300 Subject: [PATCH 059/194] patch: reparado no se puede refrescar app si no hay novedades Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/main_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 203309f..53ffc28 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -89,6 +89,7 @@ class _MainScreenState extends State { body: PullToRefresh( onRefresh: loadData, child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ From ef6426ddaaf9b8a72bb8dc841b6226b9f16ae316 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:49:22 -0300 Subject: [PATCH 060/194] patch: actualizado podfile para mejorar tiempo de build Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Podfile | 8 ++++++-- ios/Podfile.lock | 40 +++++++++++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 2ddf34e..493edfc 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -34,7 +34,9 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - pod 'GoogleUtilities' + pod 'GoogleUtilities', :git => 'https://github.com/google/GoogleUtilities.git', :tag => '7.13.0' + pod 'Firebase', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => '10.7.0' + pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.20.0' end post_install do |installer| @@ -113,7 +115,9 @@ target 'MiUtemNotificationServiceExtension' do use_modular_headers! install_awesome_fcm_ios_pod_target File.dirname(File.realpath(__FILE__)) - pod 'GoogleUtilities' + pod 'GoogleUtilities', :git => 'https://github.com/google/GoogleUtilities.git', :tag => '7.13.0' + pod 'Firebase', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => '10.7.0' + pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.20.0' end update_awesome_fcm_service_target('MiUtemNotificationServiceExtension', File.dirname(File.realpath(__FILE__)), flutter_root) ################ Awesome Notifications FCM pod mod ################### \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b2052c1..20bf587 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -621,6 +621,8 @@ PODS: - firebase_core - Flutter - nanopb (< 2.30910.0, >= 2.30908.0) + - Firebase (10.7.0): + - Firebase/Core (= 10.7.0) - Firebase/Analytics (10.7.0): - Firebase/Core - Firebase/Core (10.7.0): @@ -906,6 +908,11 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) + - Sentry (8.20.0): + - Sentry/Core (= 8.20.0) + - SentryPrivate (= 8.20.0) + - Sentry/Core (8.20.0): + - SentryPrivate (= 8.20.0) - Sentry/HybridSDK (8.20.0): - SentryPrivate (= 8.20.0) - sentry_flutter (0.0.1): @@ -923,7 +930,7 @@ PODS: - FMDB (>= 2.7.5) - url_launcher_ios (0.0.1): - Flutter - - UXCam (3.6.6) + - UXCam (3.6.11) - video_player_avfoundation (0.0.1): - Flutter @@ -933,6 +940,7 @@ DEPENDENCIES: - awesome_notifications_fcm (from `.symlinks/plugins/awesome_notifications_fcm/ios`) - background_fetch (from `.symlinks/plugins/background_fetch/ios`) - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) + - Firebase (from `https://github.com/firebase/firebase-ios-sdk.git`, tag `10.7.0`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) @@ -941,13 +949,14 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_uxcam (from `.symlinks/plugins/flutter_uxcam/ios`) - - GoogleUtilities + - GoogleUtilities (from `https://github.com/google/GoogleUtilities.git`, tag `7.13.0`) - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - Sentry (from `https://github.com/getsentry/sentry-cocoa.git`, tag `8.20.0`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) @@ -959,7 +968,6 @@ SPEC REPOS: trunk: - abseil - BoringSSL-GRPC - - Firebase - FirebaseABTesting - FirebaseAnalytics - FirebaseCore @@ -975,7 +983,6 @@ SPEC REPOS: - FMDB - GoogleAppMeasurement - GoogleDataTransport - - GoogleUtilities - "gRPC-C++" - gRPC-Core - IosAwnCore @@ -985,7 +992,6 @@ SPEC REPOS: - nanopb - PromisesObjC - PromisesSwift - - Sentry - SentryPrivate - UXCam @@ -1000,6 +1006,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/background_fetch/ios" cloud_firestore: :path: ".symlinks/plugins/cloud_firestore/ios" + Firebase: + :git: https://github.com/firebase/firebase-ios-sdk.git + :tag: 10.7.0 firebase_analytics: :path: ".symlinks/plugins/firebase_analytics/ios" firebase_core: @@ -1016,6 +1025,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_uxcam: :path: ".symlinks/plugins/flutter_uxcam/ios" + GoogleUtilities: + :git: https://github.com/google/GoogleUtilities.git + :tag: 7.13.0 image_editor_common: :path: ".symlinks/plugins/image_editor_common/ios" image_picker_ios: @@ -1028,6 +1040,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + Sentry: + :git: https://github.com/getsentry/sentry-cocoa.git + :tag: 8.20.0 sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: @@ -1041,6 +1056,17 @@ EXTERNAL SOURCES: video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/ios" +CHECKOUT OPTIONS: + Firebase: + :git: https://github.com/firebase/firebase-ios-sdk.git + :tag: 10.7.0 + GoogleUtilities: + :git: https://github.com/google/GoogleUtilities.git + :tag: 7.13.0 + Sentry: + :git: https://github.com/getsentry/sentry-cocoa.git + :tag: 8.20.0 + SPEC CHECKSUMS: abseil: ebe5b5529fb05d93a8bdb7951607be08b7fa71bc awesome_notifications: db394d2e061e4583ba0f738ddea611e3986cc3fb @@ -1096,9 +1122,9 @@ SPEC CHECKSUMS: shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 - UXCam: 7f200dd8f0829ae92a2ef82cadbb35dadff6108d + UXCam: 73a2718c84c547012c5bd47959179d900b1948c2 video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 -PODFILE CHECKSUM: ae166eb081d1fe0b450a3271d7130468d146603c +PODFILE CHECKSUM: 6fb22b937e4fec334b715ea2295056c707e48f36 COCOAPODS: 1.15.2 From 4a33246a39c91bd86226dc4844a8d5d5ffe7ea67 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:52:18 -0300 Subject: [PATCH 061/194] patch: removido sentry de podfile Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Podfile | 2 -- ios/Podfile.lock | 15 ++------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 493edfc..30af010 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -36,7 +36,6 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) pod 'GoogleUtilities', :git => 'https://github.com/google/GoogleUtilities.git', :tag => '7.13.0' pod 'Firebase', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => '10.7.0' - pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.20.0' end post_install do |installer| @@ -117,7 +116,6 @@ target 'MiUtemNotificationServiceExtension' do install_awesome_fcm_ios_pod_target File.dirname(File.realpath(__FILE__)) pod 'GoogleUtilities', :git => 'https://github.com/google/GoogleUtilities.git', :tag => '7.13.0' pod 'Firebase', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => '10.7.0' - pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.20.0' end update_awesome_fcm_service_target('MiUtemNotificationServiceExtension', File.dirname(File.realpath(__FILE__)), flutter_root) ################ Awesome Notifications FCM pod mod ################### \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 20bf587..e49456a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -908,11 +908,6 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) - - Sentry (8.20.0): - - Sentry/Core (= 8.20.0) - - SentryPrivate (= 8.20.0) - - Sentry/Core (8.20.0): - - SentryPrivate (= 8.20.0) - Sentry/HybridSDK (8.20.0): - SentryPrivate (= 8.20.0) - sentry_flutter (0.0.1): @@ -956,7 +951,6 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - Sentry (from `https://github.com/getsentry/sentry-cocoa.git`, tag `8.20.0`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) @@ -992,6 +986,7 @@ SPEC REPOS: - nanopb - PromisesObjC - PromisesSwift + - Sentry - SentryPrivate - UXCam @@ -1040,9 +1035,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - Sentry: - :git: https://github.com/getsentry/sentry-cocoa.git - :tag: 8.20.0 sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: @@ -1063,9 +1055,6 @@ CHECKOUT OPTIONS: GoogleUtilities: :git: https://github.com/google/GoogleUtilities.git :tag: 7.13.0 - Sentry: - :git: https://github.com/getsentry/sentry-cocoa.git - :tag: 8.20.0 SPEC CHECKSUMS: abseil: ebe5b5529fb05d93a8bdb7951607be08b7fa71bc @@ -1125,6 +1114,6 @@ SPEC CHECKSUMS: UXCam: 73a2718c84c547012c5bd47959179d900b1948c2 video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 -PODFILE CHECKSUM: 6fb22b937e4fec334b715ea2295056c707e48f36 +PODFILE CHECKSUM: 9287638659646e3d02f76f057c40ab838e669b0e COCOAPODS: 1.15.2 From 729ea98d67319b61eea589c09c4f4c241c4e1ae7 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:18:49 -0300 Subject: [PATCH 062/194] patch: mejoras al proceso de refresco en segundo plano Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- README.md | 5 +- ios/Podfile | 6 +-- ios/Podfile.lock | 33 +++++-------- ios/PrivacyInfo.xcprivacy | 33 +++++++++++++ ios/Runner.xcodeproj/project.pbxproj | 6 +++ lib/services/background_service.dart | 69 ++++++++++++++++------------ 6 files changed, 96 insertions(+), 56 deletions(-) create mode 100644 ios/PrivacyInfo.xcprivacy diff --git a/README.md b/README.md index 9aa945e..cc6401e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@ Aplicación multiplataforma hecha por estudiantes de la [Universidad Tecnológica Metropolitana de Chile](https://www.utem.cl/) enfocada en adaptar la [plataforma académica Mi.UTEM](https://mi.utem.cl/) de la institución a dispositivos móviles. ## Requisitos técnicos -- Flutter 3.13.6 +- Flutter 3.7.12 +- macOS + XCode (para compilar en iOS) +- Android Studio (para compilar en Android) +- ruby 3.3.0 (para compilar y subir app a App Store y Google Play) ## Organización de carpetas ``` diff --git a/ios/Podfile b/ios/Podfile index 30af010..1cdce05 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -34,8 +34,7 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - pod 'GoogleUtilities', :git => 'https://github.com/google/GoogleUtilities.git', :tag => '7.13.0' - pod 'Firebase', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => '10.7.0' + pod 'GoogleUtilities', :git => 'https://github.com/google/GoogleUtilities.git' end post_install do |installer| @@ -114,8 +113,7 @@ target 'MiUtemNotificationServiceExtension' do use_modular_headers! install_awesome_fcm_ios_pod_target File.dirname(File.realpath(__FILE__)) - pod 'GoogleUtilities', :git => 'https://github.com/google/GoogleUtilities.git', :tag => '7.13.0' - pod 'Firebase', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => '10.7.0' + pod 'GoogleUtilities', :git => 'https://github.com/google/GoogleUtilities.git' end update_awesome_fcm_service_target('MiUtemNotificationServiceExtension', File.dirname(File.realpath(__FILE__)), flutter_root) ################ Awesome Notifications FCM pod mod ################### \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e49456a..481bdfa 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -621,8 +621,6 @@ PODS: - firebase_core - Flutter - nanopb (< 2.30910.0, >= 2.30908.0) - - Firebase (10.7.0): - - Firebase/Core (= 10.7.0) - Firebase/Analytics (10.7.0): - Firebase/Core - Firebase/Core (10.7.0): @@ -664,7 +662,7 @@ PODS: - Firebase/RemoteConfig (= 10.7.0) - firebase_core - Flutter - - FirebaseABTesting (10.22.0): + - FirebaseABTesting (10.23.0): - FirebaseCore (~> 10.0) - FirebaseAnalytics (10.7.0): - FirebaseAnalytics/AdIdSupport (= 10.7.0) @@ -688,7 +686,7 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreExtension (10.22.0): + - FirebaseCoreExtension (10.23.0): - FirebaseCore (~> 10.0) - FirebaseCoreInternal (10.16.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" @@ -719,7 +717,7 @@ PODS: - FirebaseInstallations (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseInstallations (10.22.0): + - FirebaseInstallations (10.23.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -739,7 +737,7 @@ PODS: - FirebaseInstallations (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseSessions (10.22.0): + - FirebaseSessions (10.23.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) - FirebaseInstallations (~> 10.0) @@ -935,7 +933,6 @@ DEPENDENCIES: - awesome_notifications_fcm (from `.symlinks/plugins/awesome_notifications_fcm/ios`) - background_fetch (from `.symlinks/plugins/background_fetch/ios`) - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - - Firebase (from `https://github.com/firebase/firebase-ios-sdk.git`, tag `10.7.0`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) @@ -944,7 +941,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_uxcam (from `.symlinks/plugins/flutter_uxcam/ios`) - - GoogleUtilities (from `https://github.com/google/GoogleUtilities.git`, tag `7.13.0`) + - GoogleUtilities (from `https://github.com/google/GoogleUtilities.git`) - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) @@ -962,6 +959,7 @@ SPEC REPOS: trunk: - abseil - BoringSSL-GRPC + - Firebase - FirebaseABTesting - FirebaseAnalytics - FirebaseCore @@ -1001,9 +999,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/background_fetch/ios" cloud_firestore: :path: ".symlinks/plugins/cloud_firestore/ios" - Firebase: - :git: https://github.com/firebase/firebase-ios-sdk.git - :tag: 10.7.0 firebase_analytics: :path: ".symlinks/plugins/firebase_analytics/ios" firebase_core: @@ -1022,7 +1017,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_uxcam/ios" GoogleUtilities: :git: https://github.com/google/GoogleUtilities.git - :tag: 7.13.0 image_editor_common: :path: ".symlinks/plugins/image_editor_common/ios" image_picker_ios: @@ -1049,12 +1043,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/video_player_avfoundation/ios" CHECKOUT OPTIONS: - Firebase: - :git: https://github.com/firebase/firebase-ios-sdk.git - :tag: 10.7.0 GoogleUtilities: + :commit: 26c898aed8bed13b8a63057ee26500abbbcb8d55 :git: https://github.com/google/GoogleUtilities.git - :tag: 7.13.0 SPEC CHECKSUMS: abseil: ebe5b5529fb05d93a8bdb7951607be08b7fa71bc @@ -1070,18 +1061,18 @@ SPEC CHECKSUMS: firebase_crashlytics: 1b3768f2e118b0281ad30f2931369e0d1921789c firebase_in_app_messaging: 2b36a1746f4fefbd6f578a2f6659358a0d3f2c94 firebase_remote_config: e5f1ed5b29191424280b7b249228f0ed64bc90ee - FirebaseABTesting: 66d2594b36d4ff6e7d3c8719802100990de05857 + FirebaseABTesting: aec61ed9a34d85a95e2013a3fdf051426a2419df FirebaseAnalytics: f8133442ee6f8512e28ff19e62ce15398bfaeace FirebaseCore: e317665b9d744727a97e623edbbed009320afdd7 - FirebaseCoreExtension: 6394c00b887d0bebadbc7049c464aa0cbddc5d41 + FirebaseCoreExtension: cb88851781a24e031d1b58e0bd01eb1f46b044b5 FirebaseCoreInternal: 26233f705cc4531236818a07ac84d20c333e505a FirebaseCrashlytics: 35fdd1a433b31e28adcf5c8933f4c526691a1e0b FirebaseFirestore: 3963a6edd1c84b4748dab3e2c62624a29d03eca1 FirebaseInAppMessaging: d04732fe9c37c3d026d66435abba60120087a7f5 - FirebaseInstallations: 763814908793c0da14c18b3dcffdec71e29ed55e + FirebaseInstallations: 42d6ead4605d6eafb3b6683674e80e18eb6f2c35 FirebaseMessaging: ac9062bcc35ed56e15a0241d8fd317022499baf8 FirebaseRemoteConfig: d5de62211e2eaa2152d8ee85a23c301b70887a74 - FirebaseSessions: cd97fb07674f3906619c871eefbd260a1546c9d3 + FirebaseSessions: f06853e30f99fe42aa511014d7ee6c8c319f08a3 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_uxcam: 49ca783e481233911475724eb1366064183fffc1 @@ -1114,6 +1105,6 @@ SPEC CHECKSUMS: UXCam: 73a2718c84c547012c5bd47959179d900b1948c2 video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 -PODFILE CHECKSUM: 9287638659646e3d02f76f057c40ab838e669b0e +PODFILE CHECKSUM: 4d4f03dca85c6d30067635d550a818b62d393f0e COCOAPODS: 1.15.2 diff --git a/ios/PrivacyInfo.xcprivacy b/ios/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..5f16350 --- /dev/null +++ b/ios/PrivacyInfo.xcprivacy @@ -0,0 +1,33 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + 7D9E.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 792c73f..bb1e76d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 79C1453E2BBDF10B0064A7E0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 79FBEAF12BBDC94D0075EACE /* PrivacyInfo.xcprivacy */; }; + 79FBEAF22BBDC94D0075EACE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 79FBEAF12BBDC94D0075EACE /* PrivacyInfo.xcprivacy */; }; 8F4ED3CFA3A6AA57DAA4905A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6121D4E2A474C9DE828CDCAE /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -72,6 +74,7 @@ 6DE503BB399A91B6C5520D76 /* Pods-Runner.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-dev.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 79FBEAF12BBDC94D0075EACE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7FE2002C76C7F77E5A50737A /* Pods-MiUtemNotificationServiceExtension.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MiUtemNotificationServiceExtension.debug-dev.xcconfig"; path = "Target Support Files/Pods-MiUtemNotificationServiceExtension/Pods-MiUtemNotificationServiceExtension.debug-dev.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -159,6 +162,7 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + 79FBEAF12BBDC94D0075EACE /* PrivacyInfo.xcprivacy */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, EBE5B6BC29D4D890005924AB /* MiUtemNotificationServiceExtension */, @@ -298,6 +302,7 @@ EBCDF3152A08916400C04BA3 /* prod.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 79FBEAF22BBDC94D0075EACE /* PrivacyInfo.xcprivacy in Resources */, EBBC93162A9CF30F00452092 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -306,6 +311,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 79C1453E2BBDF10B0064A7E0 /* PrivacyInfo.xcprivacy in Resources */, EBCDF3192A0891B800C04BA3 /* dev.xcconfig in Resources */, EBCDF3162A08916400C04BA3 /* prod.xcconfig in Resources */, ); diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 2f8f55d..8b2f727 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -2,12 +2,27 @@ import 'dart:async'; import 'package:background_fetch/background_fetch.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/interfaces/grades_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +final _backgroundFetchConfig = BackgroundFetchConfig( + minimumFetchInterval: 30, + startOnBoot: true, + stopOnTerminate: false, + enableHeadless: true, + requiresBatteryNotLow: false, + requiresCharging: false, + requiresStorageNotLow: false, + requiresDeviceIdle: false, + + // Se necesita conexión a internet para funcionar. + requiredNetworkType: NetworkType.ANY, +); + class BackgroundController { -// [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true` -// Be sure to annotate your callback function to avoid issues in release mode on Flutter >= 3.3.0 + // [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true` + // Be sure to annotate your callback function to avoid issues in release mode on Flutter >= 3.3.0 @pragma('vm:entry-point') static void backgroundFetchHeadlessTask(HeadlessTask task) async { String taskId = task.taskId; @@ -21,39 +36,33 @@ class BackgroundController { } class BackgroundService { + static Future initAndStart() async { BackgroundFetch.registerHeadlessTask( - BackgroundController.backgroundFetchHeadlessTask, - ); - + BackgroundController.backgroundFetchHeadlessTask); await BackgroundFetch.configure( - BackgroundFetchConfig( - minimumFetchInterval: 30, - startOnBoot: true, - stopOnTerminate: false, - enableHeadless: true, - requiresBatteryNotLow: false, - requiresCharging: false, - requiresStorageNotLow: false, - requiresDeviceIdle: false, - requiredNetworkType: NetworkType.NONE, - ), - (String taskId) async { - await Get.find().lookForGradeUpdates(); - - BackgroundFetch.finish(taskId); - }, - (String taskId) async { - Sentry.captureMessage( - "BackgroundFetch task timeout $taskId", - level: SentryLevel.warning, - ); - BackgroundFetch.finish(taskId); - }, - ); + _backgroundFetchConfig, _onFetch, _onTimeout); - BackgroundFetch.start().then((int status) {}).catchError((e, stackTrace) { + BackgroundFetch.start().then((_) {}).catchError((e, stackTrace) { Sentry.captureException(e, stackTrace: stackTrace); }); } + + static _onFetch(String taskId) async { + // Refresca el token de autenticación + await Get.find().isLoggedIn(forceRefresh: true); + + // Revisa si hubo un cambio en las notas + await Get.find().lookForGradeUpdates(); + + // Termina la tarea de segundo plano + BackgroundFetch.finish(taskId); + } + + static _onTimeout(String taskId) async { + Sentry.captureMessage("Se agotó el tiempo de espera para la tarea '$taskId'", + level: SentryLevel.warning, + ); + BackgroundFetch.finish(taskId); + } } From 8d5dc80cc1cb5e99ebb82044d75bee1e517a10bb Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:07:12 -0300 Subject: [PATCH 063/194] =?UTF-8?q?patch:=20se=20mueve=20controlador=20de?= =?UTF-8?q?=20notificaciones=20y=20arreglado=20horario=20*=20Se=20mueve=20?= =?UTF-8?q?el=20controlador=20de=20notificaciones=20a=20la=20carpeta=20de?= =?UTF-8?q?=20controladores=20*=20Se=20repara=20el=20horario=20(no=20se=20?= =?UTF-8?q?mov=C3=ADa=20los=20d=C3=ADas=20y=20horas=20como=20debe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../implementations/horario_controller.dart | 34 +++++---- .../interfaces/horario_controller.dart | 2 + .../notification_controller.dart | 0 .../widgets/horario_main_scroller.dart | 70 ++++++++----------- lib/services/background_service.dart | 39 ++++++++--- lib/services/notification_service.dart | 2 +- 6 files changed, 84 insertions(+), 63 deletions(-) rename lib/{services/legacy_controllers => controllers}/notification_controller.dart (100%) diff --git a/lib/controllers/implementations/horario_controller.dart b/lib/controllers/implementations/horario_controller.dart index b4bc5b6..ba489f7 100644 --- a/lib/controllers/implementations/horario_controller.dart +++ b/lib/controllers/implementations/horario_controller.dart @@ -55,6 +55,8 @@ class HorarioControllerImplementation implements HorarioController { @override TransformationController cornerController = TransformationController(); + Function? _onUpdate; + @override List get unusedColors { List availableColors = [..._randomColors].where((Color color) => !usedColors.contains(color)).toList(); @@ -187,6 +189,9 @@ class HorarioControllerImplementation implements HorarioController { indicatorIsOpen.value = isOpen; } + @override + void setOnUpdate(Function? onUpdate) => _onUpdate = onUpdate; + void _setRandomColorsByHorario(Horario horario) => horario.horario?.forEach((dia) => dia.forEach((bloque) { final _asignatura = bloque.asignatura; if (_asignatura == null) { @@ -199,6 +204,7 @@ class HorarioControllerImplementation implements HorarioController { void _onChangeAnyController() { setIndicatorIsOpen(true); isCenteredInCurrentPeriodAndDay.value = false; + _onUpdate?.call(); } void _setScrollControllerListeners() { @@ -207,12 +213,12 @@ class HorarioControllerImplementation implements HorarioController { final yPosition = blockContentController.value.getTranslation().y; final currentZoom = blockContentController.value.getMaxScaleOnAxis(); - daysHeaderController.value = daysHeaderController.value..setTranslationRaw(xPosition, 0, 0); - periodHeaderController.value = periodHeaderController.value..setTranslationRaw(0, yPosition, 0); + daysHeaderController.value.setTranslationRaw(xPosition, 0, 0); + periodHeaderController.value.setTranslationRaw(0, yPosition, 0); - daysHeaderController.value = daysHeaderController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1),); - periodHeaderController.value = periodHeaderController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - cornerController.value = cornerController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + daysHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1),); + periodHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); zoom.value = currentZoom; _onChangeAnyController(); @@ -223,11 +229,11 @@ class HorarioControllerImplementation implements HorarioController { final xPosition = daysHeaderController.value.getTranslation().x; final contentYPosition = blockContentController.value.getTranslation().y; - blockContentController.value = blockContentController.value..setTranslationRaw(xPosition, contentYPosition, 0); + blockContentController.value.setTranslationRaw(xPosition, contentYPosition, 0); - blockContentController.value = blockContentController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - periodHeaderController.value = periodHeaderController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - cornerController.value = cornerController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + blockContentController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + periodHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); zoom.value = currentZoom; _onChangeAnyController(); @@ -239,13 +245,13 @@ class HorarioControllerImplementation implements HorarioController { final contentXPosition = blockContentController.value.getTranslation().x; - periodHeaderController.value = periodHeaderController.value..setTranslationRaw(0, yPosition, 0); + periodHeaderController.value.setTranslationRaw(0, yPosition, 0); - blockContentController.value = blockContentController.value..setTranslationRaw(contentXPosition, yPosition, 0); + blockContentController.value.setTranslationRaw(contentXPosition, yPosition, 0); - blockContentController.value = blockContentController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - daysHeaderController.value = daysHeaderController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); - cornerController.value = cornerController.value..setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + blockContentController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + daysHeaderController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); + cornerController.value.setDiagonal(vector.Vector4(currentZoom, currentZoom, currentZoom, 1)); zoom.value = currentZoom; _onChangeAnyController(); diff --git a/lib/controllers/interfaces/horario_controller.dart b/lib/controllers/interfaces/horario_controller.dart index 032b470..34c9acb 100644 --- a/lib/controllers/interfaces/horario_controller.dart +++ b/lib/controllers/interfaces/horario_controller.dart @@ -47,4 +47,6 @@ abstract class HorarioController { void setIndicatorIsOpen(bool isOpen); + void setOnUpdate(Function? onUpdate); + } \ No newline at end of file diff --git a/lib/services/legacy_controllers/notification_controller.dart b/lib/controllers/notification_controller.dart similarity index 100% rename from lib/services/legacy_controllers/notification_controller.dart rename to lib/controllers/notification_controller.dart diff --git a/lib/screens/horario/widgets/horario_main_scroller.dart b/lib/screens/horario/widgets/horario_main_scroller.dart index 26433d6..7d0d201 100644 --- a/lib/screens/horario/widgets/horario_main_scroller.dart +++ b/lib/screens/horario/widgets/horario_main_scroller.dart @@ -40,48 +40,22 @@ class HorarioMainScroller extends StatefulWidget { static double get totalWidth => daysWidth + periodWidth; static double get totalHeight => periodsHeight + dayHeight; - Widget get _horarioBlocksContent => HorarioBlocksContent( - horario: horario, - blockHeight: blockHeight, - blockWidth: blockWidth, - blockInternalMargin: blockInternalMargin, - ); - - Widget get _horarioDaysHeader => HorarioDaysHeader( - horario: horario, - height: dayHeight, - dayWidth: dayWidth, - showActiveDay: showActive, - ); - - Widget get _horarioPeriodsHeader => HorarioPeriodsHeader( - horario: horario, - width: periodWidth, - periodHeight: periodHeight, - showActivePeriod: showActive, - ); - - Widget get _horarioCorner => HorarioCorner( - height: dayHeight, - width: periodWidth, - ); - + Widget get _horarioBlocksContent => HorarioBlocksContent(horario: horario, blockHeight: blockHeight, blockWidth: blockWidth, blockInternalMargin: blockInternalMargin); + Widget get _horarioDaysHeader => HorarioDaysHeader(horario: horario, height: dayHeight, dayWidth: dayWidth, showActiveDay: showActive); + Widget get _horarioPeriodsHeader => HorarioPeriodsHeader(horario: horario, width: periodWidth, periodHeight: periodHeight, showActivePeriod: showActive); + Widget get _horarioCorner => HorarioCorner(height: dayHeight, width: periodWidth); Widget get basicHorario => Container( color: Colors.white, child: Column( children: [ - Row( - children: [ - _horarioCorner, - _horarioDaysHeader, - ], - ), - Row( - children: [ - _horarioPeriodsHeader, - _horarioBlocksContent, - ], - ) + Row(children: [ + _horarioCorner, + _horarioDaysHeader, + ]), + Row(children: [ + _horarioPeriodsHeader, + _horarioBlocksContent, + ]) ], ), ); @@ -90,10 +64,23 @@ class HorarioMainScroller extends StatefulWidget { class _HorarioMainScrollerState extends State { @override - Widget build(BuildContext context) => Container( + void initState() { + widget.controller.setOnUpdate(() => setState(() {})); + super.initState(); + } + + @override + void dispose() { + widget.controller.setOnUpdate(null); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Obx(() => Container( color: Colors.white, child: Stack( children: [ + /* Bloques del Horario */ Container( height: HorarioMainScroller.periodsHeight, width: HorarioMainScroller.daysWidth, @@ -114,6 +101,7 @@ class _HorarioMainScrollerState extends State { ), ), ), + /* Lista de Días */ Container( width: HorarioMainScroller.daysWidth, height: HorarioMainScroller.dayHeight, @@ -132,6 +120,7 @@ class _HorarioMainScrollerState extends State { ), ), ), + /* Esquina del Horario */ Container( width: HorarioMainScroller.periodWidth, height: HorarioMainScroller.dayHeight, @@ -150,6 +139,7 @@ class _HorarioMainScrollerState extends State { ), ), ), + /* Lista de Horas */ Container( width: HorarioMainScroller.periodWidth, height: HorarioMainScroller.periodsHeight, @@ -182,5 +172,5 @@ class _HorarioMainScrollerState extends State { ), ], ), - ); + )); } diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 8b2f727..2a34021 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -2,7 +2,11 @@ import 'dart:async'; import 'package:background_fetch/background_fetch.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; +import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/interfaces/grades_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -38,10 +42,8 @@ class BackgroundController { class BackgroundService { static Future initAndStart() async { - BackgroundFetch.registerHeadlessTask( - BackgroundController.backgroundFetchHeadlessTask); - await BackgroundFetch.configure( - _backgroundFetchConfig, _onFetch, _onTimeout); + BackgroundFetch.registerHeadlessTask(BackgroundController.backgroundFetchHeadlessTask); + await BackgroundFetch.configure(_backgroundFetchConfig, _onFetch, _onTimeout); BackgroundFetch.start().then((_) {}).catchError((e, stackTrace) { Sentry.captureException(e, stackTrace: stackTrace); @@ -49,11 +51,32 @@ class BackgroundService { } static _onFetch(String taskId) async { - // Refresca el token de autenticación - await Get.find().isLoggedIn(forceRefresh: true); + await Get.find().isLoggedIn(forceRefresh: true); // Refresca el token de autenticación + await Get.find().getCarreras(forceRefresh: true); // Refresca las carreras + await Get.find().lookForGradeUpdates(); // Revisa si hubo un cambio en las notas - // Revisa si hubo un cambio en las notas - await Get.find().lookForGradeUpdates(); + // Actualiza los permisos de ingreso + try { + PermisoIngresoRepository permisoIngresoRepository = Get.find(); + final permisos = await permisoIngresoRepository.getPermisos(forceRefresh: true); + for(final permiso in permisos) { + final id = permiso.id; + if(id == null) continue; + await permisoIngresoRepository.getDetallesPermiso(id, forceRefresh: true); + } + } catch (_){} + + // Actualiza los datos de las carreras y asignaturas + try { + final carreraId = Get.find().selectedCarrera?.id; + if(carreraId != null) { + AsignaturasRepository asignaturasRepository = Get.find(); + final asignaturas = await asignaturasRepository.getAsignaturas(carreraId, forceRefresh: true) ?? []; + for(final asignatura in asignaturas) { + await asignaturasRepository.getDetalleAsignatura(asignatura, forceRefresh: true); + } + } + } catch(_){} // Termina la tarea de segundo plano BackgroundFetch.finish(taskId); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index d25552d..ca8ccb4 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -4,8 +4,8 @@ import 'dart:math'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:flutter/material.dart'; +import 'package:mi_utem/controllers/notification_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/services/legacy_controllers/notification_controller.dart'; import 'package:mi_utem/widgets/custom_alert_dialog.dart'; class NotificationService { From f3979e067acfc7e93c4fffa0d092ff829a95ea6a Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:46:03 -0300 Subject: [PATCH 064/194] patch: actualizado fastlane Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- Gemfile.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b00e642..4957f90 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,8 +24,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.899.0) - aws-sdk-core (3.191.4) + aws-partitions (1.907.0) + aws-sdk-core (3.191.6) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) @@ -33,7 +33,7 @@ GEM aws-sdk-kms (1.78.0) aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.146.0) + aws-sdk-s3 (1.146.1) aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) @@ -125,15 +125,15 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.3.0) - fastlane (2.219.0) + fastimage (2.3.1) + fastlane (2.220.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -154,10 +154,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (>= 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -166,7 +166,7 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-changelog (0.16.0) ffi (1.16.3) fourflusher (2.3.1) @@ -215,7 +215,7 @@ GEM i18n (1.14.4) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.1) + json (2.7.2) jwt (2.8.1) base64 mini_magick (4.12.0) @@ -234,7 +234,7 @@ GEM os (1.1.4) plist (3.7.1) public_suffix (4.0.7) - rake (13.1.0) + rake (13.2.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -245,7 +245,7 @@ GEM ruby-macho (2.5.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) + security (0.1.5) signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) From a12a70f14a6370de2ed2ff9dbdd9fc55e09895ba Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:46:22 -0300 Subject: [PATCH 065/194] =?UTF-8?q?patch:=20agregado=20configuraci=C3=B3n?= =?UTF-8?q?=20de=20encriptaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Runner/Info-dev.plist | 2 ++ ios/Runner/Info-prod.plist | 2 ++ 2 files changed, 4 insertions(+) diff --git a/ios/Runner/Info-dev.plist b/ios/Runner/Info-dev.plist index 17a1138..5bf9072 100644 --- a/ios/Runner/Info-dev.plist +++ b/ios/Runner/Info-dev.plist @@ -60,5 +60,7 @@ UIViewControllerBasedStatusBarAppearance + ITSAppUsesNonExemptEncryption + diff --git a/ios/Runner/Info-prod.plist b/ios/Runner/Info-prod.plist index 37909b4..6521637 100644 --- a/ios/Runner/Info-prod.plist +++ b/ios/Runner/Info-prod.plist @@ -62,5 +62,7 @@ UIViewControllerBasedStatusBarAppearance + ITSAppUsesNonExemptEncryption + From 2c7c432361cdf415d7a0bc47f603f612a2af5255 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:47:23 -0300 Subject: [PATCH 066/194] patch: ordenados algunos widgets * Se ordenan widgets de bloque_clase y bloque_vacio a sus propios archivos * Se mueve user_modal a la carpeta asignatura/modals * Se agrega vista previa de asignatura * Se mejora el CHANGELOG Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- CHANGELOG.md | 13 ++- .../asignatura/asignatura_detalle_screen.dart | 5 +- .../asignatura_estudiantes_tab.dart | 2 +- .../{ => asignatura}/modals/user_modal.dart | 0 lib/widgets/horario/bloque_clase.dart | 60 ++++++++++ lib/widgets/horario/bloque_ramo_card.dart | 110 +++--------------- lib/widgets/horario/bloque_vacio.dart | 24 ++++ .../modals/asignatura_vista_previa_modal.dart | 58 +++++++++ 8 files changed, 174 insertions(+), 98 deletions(-) rename lib/widgets/{ => asignatura}/modals/user_modal.dart (100%) create mode 100644 lib/widgets/horario/bloque_clase.dart create mode 100644 lib/widgets/horario/bloque_vacio.dart create mode 100644 lib/widgets/horario/modals/asignatura_vista_previa_modal.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a7ccc..74dd663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,18 +28,20 @@ Tipos de cambios ## [Unreleased] -## [3.0.0] - 2024-04-01Z +## [3.0.0] - 2024-04-04Z ### Added -- Se agrega "apodo" para permitir a los usuarios personalizar la aplicación. -- Se agregó lista de estudiantes al resumen de asignatura. -- Se agregó una vista previa de los datos de los estudiantes y profesores. + +- Apodo para personalizar la aplicación. +- Lista de estudiantes al resumen de asignatura. +- Vista previa de estudiantes y profesores. +- Acceso rápido a la asignatura desde el horario +- Vista previa de la asignatura al mantenerla presionada en horario ### Changed - Se actualizaron algunas dependencias. - Se mejora el rendimiento de la aplicación. -- Mejor orden en el backend. ### Removed @@ -154,3 +156,4 @@ Esta versión del changelog contiene cambios hechos en 2.10, debido a que no se - Perfil de profesores - Perfil de profesores - Perfil de profesores +- Perfil de profesores diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index ee989c2..f03cacf 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -8,6 +8,7 @@ import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; @@ -50,7 +51,9 @@ class _AsignaturaDetalleScreenState extends State { child: AsignaturaNotasTab( asignatura: asignatura, onRefresh: () async { - final asignatura = await _asignaturasRepository.getDetalleAsignatura(this.asignatura); + final carrera = Get.find().selectedCarrera; + final asignaturaBase = (await _asignaturasRepository.getAsignaturas(carrera?.id))?.firstWhere((asignatura) => asignatura.codigo == this.asignatura.codigo && asignatura.id == this.asignatura.id); + final asignatura = await _asignaturasRepository.getDetalleAsignatura(asignaturaBase); if (asignatura != null) { setState(() => this.asignatura = asignatura); } diff --git a/lib/screens/asignatura/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/asignatura_estudiantes_tab.dart index 9fd36cf..dd7361d 100644 --- a/lib/screens/asignatura/asignatura_estudiantes_tab.dart +++ b/lib/screens/asignatura/asignatura_estudiantes_tab.dart @@ -5,10 +5,10 @@ import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/widgets/asignatura/modals/user_modal.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; -import 'package:mi_utem/widgets/modals/user_modal.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; diff --git a/lib/widgets/modals/user_modal.dart b/lib/widgets/asignatura/modals/user_modal.dart similarity index 100% rename from lib/widgets/modals/user_modal.dart rename to lib/widgets/asignatura/modals/user_modal.dart diff --git a/lib/widgets/horario/bloque_clase.dart b/lib/widgets/horario/bloque_clase.dart new file mode 100644 index 0000000..dc36ffa --- /dev/null +++ b/lib/widgets/horario/bloque_clase.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/models/horario.dart'; +import 'package:mi_utem/themes/theme.dart'; + +class BloqueClase extends StatelessWidget { + final BloqueHorario block; + final double width; + final double height; + final Color textColor; + final Color? color; + final void Function(BloqueHorario)? onTap; + final void Function(BloqueHorario)? onLongPress; + + const BloqueClase({ + super.key, + required this.block, + required this.width, + required this.height, + required this.textColor, + this.color = Colors.teal, + this.onTap, + this.onLongPress, + }); + + @override + Widget build(BuildContext context) { + HorarioController _controller = Get.find(); + + return Container( + decoration: BoxDecoration( + color: _controller.getColor(block.asignatura!) ?? this.color, + borderRadius: BorderRadius.circular(15), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(15), + onTap: onTap != null ? () => onTap?.call(block) : null, + onLongPress: onLongPress != null ? () => onLongPress?.call(block) : null, + child: Column( + children: [ + HorarioText.classCode(block.codigo!, + color: textColor, + ), + HorarioText.className(block.asignatura!.nombre!.toUpperCase(), + color: textColor, + ), + HorarioText.classLocation(block.sala ?? "Sin sala", + color: textColor, + ), + ], + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/horario/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart index 838a155..36749de 100644 --- a/lib/widgets/horario/bloque_ramo_card.dart +++ b/lib/widgets/horario/bloque_ramo_card.dart @@ -1,10 +1,13 @@ -import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; +import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/services/interfaces/carreras_service.dart'; +import 'package:mi_utem/widgets/horario/bloque_clase.dart'; +import 'package:mi_utem/widgets/horario/bloque_vacio.dart'; +import 'package:mi_utem/widgets/horario/modals/asignatura_vista_previa_modal.dart'; class ClassBlockCard extends StatelessWidget { final BloqueHorario? block; @@ -29,31 +32,35 @@ class ClassBlockCard extends StatelessWidget { width: width, child: Padding( padding: EdgeInsets.all(internalMargin), - child: block?.asignatura == null ? _EmptyBlock() : _ClassBlock( + child: block?.asignatura == null ? BloqueVacio() : BloqueClase( block: block!, width: width, height: height, textColor: textColor, - onTap: _onTap, - onLongPress: _onLongPress, + onTap: (block) => _onTap(block, context), + onLongPress: (block) => _onLongPress(block, context), ), ), ); } - _onTap(BloqueHorario block) { + _onTap(BloqueHorario block, BuildContext context) async { + final asignatura = (await Get.find().getAsignaturas((Get.find().selectedCarrera)?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); + if(asignatura == null) return; AnalyticsService.logEvent( "horario_class_block_tap", parameters: { - "asignatura": block.asignatura?.nombre, - "codigo": block.asignatura?.codigo, + "asignatura": asignatura.nombre, + "codigo": asignatura.codigo, }, ); - // TODO: Navegar a la asignatura + Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignatura: asignatura))); } - _onLongPress(BloqueHorario block) { + _onLongPress(BloqueHorario block, BuildContext context) async { + final asignatura = (await Get.find().getAsignaturas((Get.find().selectedCarrera)?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); + if(asignatura == null) return; AnalyticsService.logEvent( "horario_class_block_long_press", parameters: { @@ -62,85 +69,6 @@ class ClassBlockCard extends StatelessWidget { }, ); - // TODO: Acá podríamos agregar una vista "rápida" como: Hora, Sala y Profesor. - } -} - -class _EmptyBlock extends StatelessWidget { - const _EmptyBlock({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: MainTheme.lightGrey, - borderRadius: BorderRadius.circular(15), - ), - child: DottedBorder( - strokeWidth: 2, - color: MainTheme.grey, - borderType: BorderType.RRect, - radius: Radius.circular(15), - child: Container(), - ), - ); - } -} - -class _ClassBlock extends StatelessWidget { - final BloqueHorario block; - final double width; - final double height; - final Color textColor; - final Color? color; - final void Function(BloqueHorario)? onTap; - final void Function(BloqueHorario)? onLongPress; - - const _ClassBlock({ - super.key, - required this.block, - required this.width, - required this.height, - required this.textColor, - this.color = Colors.teal, - this.onTap, - this.onLongPress, - }); - - @override - Widget build(BuildContext context) { - HorarioController _controller = Get.find(); - - return Container( - decoration: BoxDecoration( - color: _controller.getColor(block.asignatura!) ?? this.color, - borderRadius: BorderRadius.circular(15), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(15), - onTap: onTap != null ? () => onTap?.call(block) : null, - onLongPress: onLongPress != null ? () => onLongPress?.call(block) : null, - child: Column( - children: [ - HorarioText.classCode( - block.codigo!, - color: textColor, - ), - HorarioText.className( - block.asignatura!.nombre!.toUpperCase(), - color: textColor, - ), - HorarioText.classLocation( - block.sala ?? "Sin sala", - color: textColor, - ), - ], - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - ), - ), - ), - ); + showModalBottomSheet(context: context, builder: (ctx) => AsignaturaVistaPreviaModal(asignatura: asignatura, bloque: block)); } } diff --git a/lib/widgets/horario/bloque_vacio.dart b/lib/widgets/horario/bloque_vacio.dart new file mode 100644 index 0000000..b788adb --- /dev/null +++ b/lib/widgets/horario/bloque_vacio.dart @@ -0,0 +1,24 @@ +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/themes/theme.dart'; + +class BloqueVacio extends StatelessWidget { + const BloqueVacio({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: MainTheme.lightGrey, + borderRadius: BorderRadius.circular(15), + ), + child: DottedBorder( + strokeWidth: 2, + color: MainTheme.grey, + borderType: BorderType.RRect, + radius: Radius.circular(15), + child: Container(), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart b/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart new file mode 100644 index 0000000..8e29dad --- /dev/null +++ b/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/horario.dart'; + +class AsignaturaVistaPreviaModal extends StatelessWidget { + final Asignatura asignatura; + final BloqueHorario bloque; + + const AsignaturaVistaPreviaModal({ + super.key, + required this.asignatura, + required this.bloque, + }); + + @override + Widget build(BuildContext context) => SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Container( + alignment: Alignment.topCenter, + child: Card( + margin: const EdgeInsets.all(20), + child: ListView( + padding: EdgeInsets.only(top: 5.0, bottom: 5.0), + shrinkWrap: true, + physics: ScrollPhysics(), + children: [ + GestureDetector( + child: ListTile( + title: Text(asignatura.nombre ?? "Sin nombre"), + subtitle: Text(asignatura.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ").replaceAll("- ", "") ?? "Sin docente"), // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. + ), + onTap: () async { + // TODO: Mostrar perfil del docente. + }, + ), + if (asignatura.seccion?.isNotEmpty == true) ...[ + Divider(height: 5, indent: 20, endIndent: 20), + ListTile( + title: Text("Sección"), + subtitle: Text(asignatura.seccion.toString()), + ), + ], + Divider(height: 5, indent: 20, endIndent: 20), + ListTile( + title: Text("Código Asignatura"), + subtitle: Text(asignatura.codigo.toString()), + ), + Divider(height: 5, indent: 20, endIndent: 20), + ListTile( + title: Text("Sala"), + subtitle: Text(bloque.sala!), + ), + ], + ), + ), + ), + ); +} From df2dc60f777ab8266f775c56a9a79b96dce489ea Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:42:47 -0400 Subject: [PATCH 067/194] =?UTF-8?q?patch:=20reparada=20la=20calculadora=20?= =?UTF-8?q?(y=20la=20edici=C3=B3n=20de=20notas=20de=20examen)=20*=20Se=20r?= =?UTF-8?q?epara=20error=20que=20imped=C3=ADa=20ingresar=20correctamente?= =?UTF-8?q?=20la=20nota=20de=20examen=20en=20la=20calculadora=20*=20Se=20r?= =?UTF-8?q?epara=20el=20mostrar=20el=20fondo=20oscuro=20cuando=20si=20se?= =?UTF-8?q?=20puede=20editar=20la=20nota.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../implementations/calculator_controller.dart | 11 +++++------ lib/controllers/interfaces/calculator_controller.dart | 3 ++- lib/screens/splash_screen.dart | 1 - lib/services/update_service.dart | 3 --- .../calculadora_notas/nota_examen_display_widget.dart | 4 ++-- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/controllers/implementations/calculator_controller.dart b/lib/controllers/implementations/calculator_controller.dart index f8ad7ce..9314d8c 100644 --- a/lib/controllers/implementations/calculator_controller.dart +++ b/lib/controllers/implementations/calculator_controller.dart @@ -103,7 +103,6 @@ class CalculatorControllerImplementation implements CalculatorController { } setExamGrade(grades.notaExamen); - _updateCalculations(); } @override @@ -163,16 +162,16 @@ class CalculatorControllerImplementation implements CalculatorController { @override void clearExamGrade() { examGrade.value = null; - final examGradeTextFieldController = this.examGradeTextFieldController.value; - examGradeTextFieldController.updateText(""); - this.examGradeTextFieldController.value = examGradeTextFieldController; + examGradeTextFieldController.value.updateText(""); _updateCalculations(); } @override - void setExamGrade(num? grade) { + void setExamGrade(num? grade, { bool updateTextController = true }) { examGrade.value = grade?.toDouble(); - examGradeTextFieldController.value.updateText(grade?.toDouble().toStringAsFixed(1) ?? ""); + if(updateTextController) { + examGradeTextFieldController.value.updateText(grade?.toStringAsFixed(1) ?? "--"); + } _updateCalculations(); } diff --git a/lib/controllers/interfaces/calculator_controller.dart b/lib/controllers/interfaces/calculator_controller.dart index 2969817..bf17291 100644 --- a/lib/controllers/interfaces/calculator_controller.dart +++ b/lib/controllers/interfaces/calculator_controller.dart @@ -73,7 +73,8 @@ abstract class CalculatorController { void clearExamGrade(); - void setExamGrade(num? grade); + /* Actualiza la nota del examen, y si es verdadero también el controlador de texto */ + void setExamGrade(num? grade, { bool updateTextController = true }); void addGrade(IEvaluacion grade); diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 728dca5..705f235 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index c1b3488..85c8a15 100644 --- a/lib/services/update_service.dart +++ b/lib/services/update_service.dart @@ -1,7 +1,4 @@ -import 'dart:io'; - import 'package:flutter/scheduler.dart'; -import 'package:in_app_update/in_app_update.dart'; /* * Clase que se encarga de verificar si hay una nueva versión de la aplicación diff --git a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart index 88a1db4..a67440c 100644 --- a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart @@ -26,11 +26,11 @@ class NotaExamenDisplayWidget extends StatelessWidget { child: Obx(() => TextField( controller: _calculatorController.examGradeTextFieldController.value, textAlign: TextAlign.center, - onChanged: (String value) => _calculatorController.setExamGrade(double.tryParse(value.replaceAll(",", "."))), + onChanged: (String value) => _calculatorController.setExamGrade(double.tryParse(value.replaceAll(",", ".")), updateTextController: false), enabled: _calculatorController.canTakeExam.value, decoration: InputDecoration( hintText: _calculatorController.minimumRequiredExamGrade.value?.toStringAsFixed(1) ?? "--", - filled: _calculatorController.canTakeExam.value, + filled: !_calculatorController.canTakeExam.value, fillColor: Colors.grey.withOpacity(0.2), disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( From 7229b8547527c05df7317fa377273ad23863028f Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:43:30 -0400 Subject: [PATCH 068/194] patch: se ordena archivo de la calculadora en la carpeta correcta Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/{ => calculadora_notas}/nota_list_item.dart | 6 +++--- lib/widgets/calculadora_notas/notas_calculadora_widget.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename lib/widgets/{ => calculadora_notas}/nota_list_item.dart (97%) diff --git a/lib/widgets/nota_list_item.dart b/lib/widgets/calculadora_notas/nota_list_item.dart similarity index 97% rename from lib/widgets/nota_list_item.dart rename to lib/widgets/calculadora_notas/nota_list_item.dart index e89b2e0..f5e3599 100644 --- a/lib/widgets/nota_list_item.dart +++ b/lib/widgets/calculadora_notas/nota_list_item.dart @@ -2,8 +2,8 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/themes/theme.dart'; class NotaListItem extends StatelessWidget { @@ -68,12 +68,12 @@ class NotaListItem extends StatelessWidget { decoration: InputDecoration( hintText: showSuggestedGrade ? (calculatorController.suggestedGrade.value?.toStringAsFixed(0) ?? "--") : "--", disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( - borderSide: BorderSide( + borderSide: const BorderSide( color: Colors.transparent, ), ), ), - keyboardType: TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ TextInputFormatter.withFunction((prev, input) { final val = input.text; diff --git a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart index 0b71f51..ca56eac 100644 --- a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart +++ b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/widgets/nota_list_item.dart'; +import 'package:mi_utem/widgets/calculadora_notas/nota_list_item.dart'; class NotasCalculadoraWidget extends StatelessWidget { From e6825375f1d1ee21a013063810d48f2d6ae54ec1 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:43:56 -0400 Subject: [PATCH 069/194] patch: se reparan declaraciones regulares con declaraciones de flecha Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/horario/bloque_periodo_card.dart | 83 +++++++++----------- lib/widgets/horario/bloque_ramo_card.dart | 30 ++++--- 2 files changed, 52 insertions(+), 61 deletions(-) diff --git a/lib/widgets/horario/bloque_periodo_card.dart b/lib/widgets/horario/bloque_periodo_card.dart index ea06ff7..642d3ad 100644 --- a/lib/widgets/horario/bloque_periodo_card.dart +++ b/lib/widgets/horario/bloque_periodo_card.dart @@ -10,8 +10,8 @@ class BloquePeriodoCard extends StatelessWidget { final bool active; final Color backgroundColor; - BloquePeriodoCard({ - Key? key, + const BloquePeriodoCard({ + super.key, required this.inicio, required this.intermedio, required this.fin, @@ -19,51 +19,44 @@ class BloquePeriodoCard extends StatelessWidget { required this.width, this.backgroundColor = MainTheme.lightGrey, this.active = false, - }) : super(key: key); + }); @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: backgroundColor, - ), - height: height, - width: width, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - inicio!, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black54, - fontSize: 18, - fontWeight: active ? FontWeight.bold : FontWeight.normal, - ), + Widget build(BuildContext context) => Container( + decoration: BoxDecoration(color: backgroundColor), + height: height, + width: width, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(inicio!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black54, + fontSize: 18, + fontWeight: active ? FontWeight.bold : FontWeight.normal, ), - Text( - intermedio!, - maxLines: 3, - textAlign: TextAlign.center, - style: TextStyle( - letterSpacing: 0.5, - wordSpacing: 1, - color: Colors.black54, - fontSize: 14, - fontWeight: active ? FontWeight.bold : FontWeight.normal, - ), + ), + Text(intermedio!, + maxLines: 3, + textAlign: TextAlign.center, + style: TextStyle( + letterSpacing: 0.5, + wordSpacing: 1, + color: Colors.black54, + fontSize: 14, + fontWeight: active ? FontWeight.bold : FontWeight.normal, ), - Text( - fin!, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black54, - fontSize: 18, - fontWeight: active ? FontWeight.bold : FontWeight.normal, - ), - ) - ], - ), - ); - } + ), + Text(fin!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black54, + fontSize: 18, + fontWeight: active ? FontWeight.bold : FontWeight.normal, + ), + ) + ], + ), + ); } diff --git a/lib/widgets/horario/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart index 36749de..356b599 100644 --- a/lib/widgets/horario/bloque_ramo_card.dart +++ b/lib/widgets/horario/bloque_ramo_card.dart @@ -26,23 +26,21 @@ class ClassBlockCard extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Container( - height: height, - width: width, - child: Padding( - padding: EdgeInsets.all(internalMargin), - child: block?.asignatura == null ? BloqueVacio() : BloqueClase( - block: block!, - width: width, - height: height, - textColor: textColor, - onTap: (block) => _onTap(block, context), - onLongPress: (block) => _onLongPress(block, context), - ), + Widget build(BuildContext context) => Container( + height: height, + width: width, + child: Padding( + padding: EdgeInsets.all(internalMargin), + child: block?.asignatura == null ? BloqueVacio() : BloqueClase( + block: block!, + width: width, + height: height, + textColor: textColor, + onTap: (block) => _onTap(block, context), + onLongPress: (block) => _onLongPress(block, context), ), - ); - } + ), + ); _onTap(BloqueHorario block, BuildContext context) async { final asignatura = (await Get.find().getAsignaturas((Get.find().selectedCarrera)?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); From 53be20736ce45df1d71821782e5226766bed28f9 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:44:14 -0400 Subject: [PATCH 070/194] patch: reparado importe de nota_list_item Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/asignatura/asignatura_notas_tab.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index d14adc6..9b5771f 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; +import 'package:mi_utem/widgets/calculadora_notas/nota_list_item.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/nota_list_item.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; class AsignaturaNotasTab extends StatelessWidget { From bc1a06d6b65c416951e622b3c09544b14ded1dd6 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:33:56 -0400 Subject: [PATCH 071/194] =?UTF-8?q?feat:=20agregado=20archivo=20de=20prueb?= =?UTF-8?q?as=20(preparaci=C3=B3n=20de=20pruebas)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- test/widget_test.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/widget_test.dart diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..8f38974 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + + + +// void main() { +// testWidgets('Counter increments smoke test', (WidgetTester tester) async { +// // Build our app and trigger a frame. +// await tester.pumpWidget(const MyApp()); +// +// // Verify that our counter starts at 0. +// expect(find.text('0'), findsOneWidget); +// expect(find.text('1'), findsNothing); +// +// // Tap the '+' icon and trigger a frame. +// await tester.tap(find.byIcon(Icons.add)); +// await tester.pump(); +// +// // Verify that our counter has incremented. +// expect(find.text('0'), findsNothing); +// expect(find.text('1'), findsOneWidget); +// }); +// } From a40978b50dd2232ffe1eec12bf59879ee244ec98 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:34:19 -0400 Subject: [PATCH 072/194] =?UTF-8?q?patch:=20arreglado=20archivos=20(xcode?= =?UTF-8?q?=20los=20arregl=C3=B3=20automaticamente)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/PrivacyInfo.xcprivacy | 2 +- ios/Runner/Info-dev.plist | 4 ++-- ios/Runner/Info-prod.plist | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/PrivacyInfo.xcprivacy b/ios/PrivacyInfo.xcprivacy index 5f16350..e17b525 100644 --- a/ios/PrivacyInfo.xcprivacy +++ b/ios/PrivacyInfo.xcprivacy @@ -9,7 +9,7 @@ NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - 1C8F.1 + CA92.1 diff --git a/ios/Runner/Info-dev.plist b/ios/Runner/Info-dev.plist index 5bf9072..5491837 100644 --- a/ios/Runner/Info-dev.plist +++ b/ios/Runner/Info-dev.plist @@ -26,6 +26,8 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSAppTransportSecurity @@ -60,7 +62,5 @@ UIViewControllerBasedStatusBarAppearance - ITSAppUsesNonExemptEncryption - diff --git a/ios/Runner/Info-prod.plist b/ios/Runner/Info-prod.plist index 6521637..e0cbd33 100644 --- a/ios/Runner/Info-prod.plist +++ b/ios/Runner/Info-prod.plist @@ -26,6 +26,8 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSAppTransportSecurity @@ -62,7 +64,5 @@ UIViewControllerBasedStatusBarAppearance - ITSAppUsesNonExemptEncryption - From 00b1987fe4d36ede03d716947de83d4ef110164b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:34:37 -0400 Subject: [PATCH 073/194] =?UTF-8?q?patch:=20reparada=20y=20simplificada=20?= =?UTF-8?q?la=20capitalizaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/user/user.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/models/user/user.dart b/lib/models/user/user.dart index a5dd144..c552610 100644 --- a/lib/models/user/user.dart +++ b/lib/models/user/user.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:mi_utem/models/user/rut.dart'; +import 'package:mi_utem/utils/string_utils.dart'; class User { @@ -20,8 +21,8 @@ class User { String? username; String? fotoUrl; - get nombreCompletoCapitalizado => nombreCompleto.toLowerCase().split(' ').map((it) => it[0].toUpperCase() + it.substring(1)).join(' '); - get nombreDisplayCapitalizado => "${nombres?.split(' ')[0]} $apellidos".toLowerCase().split(' ').map((it) => it[0].toUpperCase() + it.substring(1)).join(' '); + get nombreCompletoCapitalizado => capitalize(nombreCompleto); + get nombreDisplayCapitalizado => capitalize("${nombres?.split(' ')[0]} $apellidos"); get primerNombre => nombreCompletoCapitalizado.split(' ')[0]; get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0].toUpperCase()).join(''); From 1e3d6afd76e5993eff9535b4ef4ff5028a9a8202 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:35:25 -0400 Subject: [PATCH 074/194] patch: reparada pantalla de apodo * Se arregla que el contenido es "empujado" por el teclado en la pantalla de apodo Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/onboarding/set_alias_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/screens/onboarding/set_alias_screen.dart b/lib/screens/onboarding/set_alias_screen.dart index 3f1aa2e..ea57c29 100644 --- a/lib/screens/onboarding/set_alias_screen.dart +++ b/lib/screens/onboarding/set_alias_screen.dart @@ -38,6 +38,7 @@ class _SetAliasScreenState extends State { @override Widget build(BuildContext context) => GradientBackground( + resizeToAvoidBottomInset: false, child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, From 3305a73832c16299d76ed44b07b5fcf6c1452eae Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:47:27 -0400 Subject: [PATCH 075/194] =?UTF-8?q?patch:=20agregada=20llave=20primaria=20?= =?UTF-8?q?al=20cache=20*=20Se=20agrega=20rut=20del=20usuario=20como=20lla?= =?UTF-8?q?ve=20primaria=20en=20caso=20de=20que=20en=20la=20app=20se=20ini?= =?UTF-8?q?cie=20sesi=C3=B3n=20con=20otra=20cuenta.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/utils/http/interceptors/auth_interceptor.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/utils/http/interceptors/auth_interceptor.dart b/lib/utils/http/interceptors/auth_interceptor.dart index b1387de..e2adb88 100644 --- a/lib/utils/http/interceptors/auth_interceptor.dart +++ b/lib/utils/http/interceptors/auth_interceptor.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; @@ -20,9 +21,14 @@ class AuthInterceptor extends QueuedInterceptor { if(!options.headers.containsKey("authorization")) { final user = await _authService.getUser(); final token = user?.token; - if(user != null && token != null) { + if(token != null) { options._setAuthenticationHeader(token); } + + final rut = user?.rut; + if(rut != null) { + options.extra[DIO_CACHE_KEY_PRIMARY_KEY] = rut.toString(); + } } return handler.next(options); From 8d86ea80e89ce09a35b31c6a3678b94fbac61393 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:48:17 -0400 Subject: [PATCH 076/194] =?UTF-8?q?patch:=20se=20mejora=20http=5Fclient=20?= =?UTF-8?q?*=20Se=20coloca=20la=20configuraci=C3=B3n=20de=20cach=C3=A9=20d?= =?UTF-8?q?entro=20de=20la=20inicializaci=C3=B3n=20de=20DioCacheManager=20?= =?UTF-8?q?*=20Se=20elimina=20'getter'=20de=20authClient=20(para=20evitar?= =?UTF-8?q?=20que=20sea=20computado=20y=20sea=20un=20cliente=20est=C3=A1ti?= =?UTF-8?q?co)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/utils/http/http_client.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index a1e7d10..f223f33 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -12,13 +12,11 @@ class HttpClient { static const String productionUrl = 'https://api.exdev.cl/'; static String url = isProduction ? productionUrl : debugUrl; - static CacheConfig cacheConfig = CacheConfig( + static DioCacheManager dioCacheManager = DioCacheManager(CacheConfig( baseUrl: url, defaultMaxAge: Duration(days: 7), defaultMaxStale: Duration(days: 60), - ); - - static DioCacheManager dioCacheManager = DioCacheManager(cacheConfig); + )); static Dio httpClient = Dio(BaseOptions(baseUrl: url))..interceptors.addAll([ HeadersInterceptor(), @@ -27,7 +25,7 @@ class HttpClient { dioCacheManager.interceptor, ]); - static Dio get authClient => httpClient..interceptors.add(AuthInterceptor()); + static Dio authClient = httpClient..interceptors.add(AuthInterceptor()); static InterceptorsWrapper _errorInterceptor = InterceptorsWrapper( onError: (DioError err, ErrorInterceptorHandler handler) { From 4d74b42cdaa84cdbf6b1d967b2a20bf74929da12 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:49:16 -0400 Subject: [PATCH 077/194] patch: se agrega resizeToAvoidBottomInset al gradient_background.dart Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/gradient_background.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/widgets/gradient_background.dart b/lib/widgets/gradient_background.dart index 169dcfb..72cb7c1 100644 --- a/lib/widgets/gradient_background.dart +++ b/lib/widgets/gradient_background.dart @@ -3,14 +3,17 @@ import 'package:mi_utem/themes/theme.dart'; class GradientBackground extends StatelessWidget { final Widget child; + final bool resizeToAvoidBottomInset; const GradientBackground({ super.key, required this.child, + this.resizeToAvoidBottomInset = true, }); @override Widget build(BuildContext context) => Scaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset, body: Stack( children: [ Container( From 80677881f2b83bf7e1d1ed37a32db8e06dd09b34 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:49:40 -0400 Subject: [PATCH 078/194] patch: se separa ticker_time_text como un widget a parte Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/horario/ticker_time_text.dart | 61 +++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 lib/widgets/horario/ticker_time_text.dart diff --git a/lib/widgets/horario/ticker_time_text.dart b/lib/widgets/horario/ticker_time_text.dart new file mode 100644 index 0000000..8099d7a --- /dev/null +++ b/lib/widgets/horario/ticker_time_text.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class TickerTimeText extends StatefulWidget { + final DateTime time; + + const TickerTimeText({ + super.key, + required this.time, + }); + + @override + State createState() => __TickerTimeTextState(); +} + +class __TickerTimeTextState extends State { + Timer? _timer; + bool _showColon = true; + + @override + void initState() { + _timer = Timer.periodic(Duration(seconds: 1), (Timer t) => setState(() => _showColon = !_showColon)); + super.initState(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + + @override + Widget build(BuildContext context) => RichText( + overflow: TextOverflow.fade, + maxLines: 1, + text: TextSpan( + children: [ + TextSpan( + text: "${widget.time.hour}", + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white, + ), + ), + TextSpan( + text: ":", + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: _showColon ? Colors.white : Colors.transparent, + ), + ), + TextSpan( + text: "${widget.time.minute}", + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white, + ), + ), + ], + ), + ); +} \ No newline at end of file From ab410f7f472f0c2f192c68154e42a4068228ff0b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:50:05 -0400 Subject: [PATCH 079/194] =?UTF-8?q?patch:=20se=20elimina=20el=20cach=C3=A9?= =?UTF-8?q?=20al=20hacer=20logout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/implementations/auth_service.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/services/implementations/auth_service.dart b/lib/services/implementations/auth_service.dart index d350571..0cae6ca 100644 --- a/lib/services/implementations/auth_service.dart +++ b/lib/services/implementations/auth_service.dart @@ -13,6 +13,7 @@ import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/notification_service.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; class AuthServiceImplementation implements AuthService { @@ -81,12 +82,13 @@ class AuthServiceImplementation implements AuthService { @override Future logout({BuildContext? context}) async { + await HttpClient.dioCacheManager.clearAll(); setUser(null); _credentialsService.setCredentials(null); _preferencesRepository.setOnboardingStep(null); _preferencesRepository.setLastLogin(null); _preferencesRepository.setAlias(null); - + if(context != null) { Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (ctx) => LoginScreen())); } From 34546391b83eeb6677fc3026f813df7f07db644b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:50:24 -0400 Subject: [PATCH 080/194] patch: se agrega mejor logs de debug al background_service.dart Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/background_service.dart | 35 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 2a34021..244855f 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -1,7 +1,7 @@ -import 'dart:async'; import 'package:background_fetch/background_fetch.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; @@ -11,7 +11,7 @@ import 'package:mi_utem/services/interfaces/grades_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; final _backgroundFetchConfig = BackgroundFetchConfig( - minimumFetchInterval: 30, + minimumFetchInterval: 15, startOnBoot: true, stopOnTerminate: false, enableHeadless: true, @@ -40,7 +40,6 @@ class BackgroundController { } class BackgroundService { - static Future initAndStart() async { BackgroundFetch.registerHeadlessTask(BackgroundController.backgroundFetchHeadlessTask); await BackgroundFetch.configure(_backgroundFetchConfig, _onFetch, _onTimeout); @@ -51,9 +50,24 @@ class BackgroundService { } static _onFetch(String taskId) async { - await Get.find().isLoggedIn(forceRefresh: true); // Refresca el token de autenticación - await Get.find().getCarreras(forceRefresh: true); // Refresca las carreras - await Get.find().lookForGradeUpdates(); // Revisa si hubo un cambio en las notas + final init = DateTime.now(); + var now = init; + logger.d("[BackgroundFetch]: Se ejecutó la tarea 'refreshTask' (${now.toIso8601String()})"); + + // Refresca el token de autenticación + await Get.find().isLoggedIn(forceRefresh: true); + logger.d("[BackgroundFetch]: Se refrescó el token de autenticación, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + now = DateTime.now(); + + // Refresca las carreras + await Get.find().getCarreras(forceRefresh: true); + logger.d("[BackgroundFetch]: Se refrescaron las carreras, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + now = DateTime.now(); + + // Revisa si hubo un cambio en las notas + await Get.find().lookForGradeUpdates(); + logger.d("[BackgroundFetch]: Se revisaron las notas, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + now = DateTime.now(); // Actualiza los permisos de ingreso try { @@ -65,6 +79,8 @@ class BackgroundService { await permisoIngresoRepository.getDetallesPermiso(id, forceRefresh: true); } } catch (_){} + logger.d("[BackgroundFetch]: Se refrescaron los permisos de ingreso, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + now = DateTime.now(); // Actualiza los datos de las carreras y asignaturas try { @@ -77,15 +93,18 @@ class BackgroundService { } } } catch(_){} + logger.d("[BackgroundFetch]: Se refrescaron los datos de las carreras y asignaturas, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + now = DateTime.now(); - // Termina la tarea de segundo plano - BackgroundFetch.finish(taskId); + logger.d("[BackgroundFetch]: Se terminó la tarea 'refreshTask', tomó ${DateTime.now().difference(init).inMilliseconds} ms"); } static _onTimeout(String taskId) async { + logger.w("Se agotó el tiempo de espera para la tarea '$taskId'"); Sentry.captureMessage("Se agotó el tiempo de espera para la tarea '$taskId'", level: SentryLevel.warning, ); BackgroundFetch.finish(taskId); } } + From 6d2aad110b20c8f592a263ba8d583fb6c676bf15 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:51:39 -0400 Subject: [PATCH 081/194] patch: se mejora el widget para tener funciones de flecha Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../widgets/horario_blocks_content.dart | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/screens/horario/widgets/horario_blocks_content.dart b/lib/screens/horario/widgets/horario_blocks_content.dart index 684d39c..a119072 100644 --- a/lib/screens/horario/widgets/horario_blocks_content.dart +++ b/lib/screens/horario/widgets/horario_blocks_content.dart @@ -12,7 +12,7 @@ class HorarioBlocksContent extends StatelessWidget { final double borderWidth; const HorarioBlocksContent({ - Key? key, + super.key, required this.horario, required this.blockHeight, required this.blockWidth, @@ -46,22 +46,21 @@ class HorarioBlocksContent extends StatelessWidget { } @override - Widget build(BuildContext context) { - return Table( - defaultColumnWidth: FixedColumnWidth(blockWidth), - border: TableBorder( - horizontalInside: BorderSide( - color: borderColor, - style: BorderStyle.solid, - width: borderWidth, - ), - verticalInside: BorderSide( - color: borderColor, - style: BorderStyle.solid, - width: borderWidth, - ), + Widget build(BuildContext context) => Table( + defaultColumnWidth: FixedColumnWidth(blockWidth), + border: TableBorder( + horizontalInside: BorderSide( + color: borderColor, + style: BorderStyle.solid, + width: borderWidth, ), - children: _children, - ); - } + verticalInside: BorderSide( + color: borderColor, + style: BorderStyle.solid, + width: borderWidth, + ), + ), + children: _children, + ); + } From c497dc85eb86069a53c9eafe995b67a3ade2ba3c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:52:00 -0400 Subject: [PATCH 082/194] =?UTF-8?q?patch:=20se=20reduce=20el=20tiempo=20de?= =?UTF-8?q?=20actualizaci=C3=B3n=20a=2030=20segundos=20(para=20que=20vaya?= =?UTF-8?q?=20casi=20sincronizado=20con=20el=20reloj=20del=20usuario)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../horario/widgets/horario_indicator.dart | 145 +++++------------- 1 file changed, 37 insertions(+), 108 deletions(-) diff --git a/lib/screens/horario/widgets/horario_indicator.dart b/lib/screens/horario/widgets/horario_indicator.dart index 18c8660..17bc0e9 100644 --- a/lib/screens/horario/widgets/horario_indicator.dart +++ b/lib/screens/horario/widgets/horario_indicator.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/widgets/horario/ticker_time_text.dart'; class HorarioIndicator extends StatefulWidget { static const _height = 2.0; @@ -32,10 +33,7 @@ class _HorarioIndicatorState extends State { @override void initState() { - _timer = Timer.periodic( - Duration(seconds: 60), - (Timer t) => setState(() {}), - ); + _timer = Timer.periodic(Duration(seconds: 30), (Timer t) => setState(() {})); super.initState(); } @@ -61,116 +59,47 @@ class _HorarioIndicatorState extends State { return Stack( children: [ - if (lineTop > 0 && lineLeft > 0) - Container( - margin: EdgeInsets.only( - top: lineTop, - left: lineLeft, - ), - height: HorarioIndicator._height, - width: widget.maxWidth, - color: widget.color, + if (lineTop > 0 && lineLeft > 0) Container( + margin: EdgeInsets.only( + top: lineTop, + left: lineLeft, ), - if (circleTapAreaTop > 0 && circleTapAreaLeft > 0) - Container( - margin: EdgeInsets.only( - top: circleTapAreaTop, - left: circleTapAreaLeft, - ), - child: GestureDetector( - onTap: () { - AnalyticsService.logEvent('horario_indicator_dot_tap'); - _horarioController.setIndicatorIsOpen(!_horarioController.indicatorIsOpen.value); - }, - child: Container( - padding: EdgeInsets.all(HorarioIndicator._tapAreaRadius), + height: HorarioIndicator._height, + width: widget.maxWidth, + color: widget.color, + ), + if (circleTapAreaTop > 0 && circleTapAreaLeft > 0) Container( + margin: EdgeInsets.only( + top: circleTapAreaTop, + left: circleTapAreaLeft, + ), + child: GestureDetector( + onTap: () { + AnalyticsService.logEvent('horario_indicator_dot_tap'); + _horarioController.setIndicatorIsOpen(!_horarioController.indicatorIsOpen.value); + }, + child: Container( + padding: EdgeInsets.all(HorarioIndicator._tapAreaRadius), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(HorarioIndicator._tapAreaRadius * 2), + ), + child: Obx(() => AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: HorarioIndicator._circleRadius * 2, + width: _horarioController.indicatorIsOpen.value ? 50 : HorarioIndicator._circleRadius * 2, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(HorarioIndicator._tapAreaRadius * 2), + color: widget.color, + borderRadius: + BorderRadius.circular(HorarioIndicator._circleRadius), ), - child: Obx(() => AnimatedContainer( - duration: const Duration(milliseconds: 300), - height: HorarioIndicator._circleRadius * 2, - width: _horarioController.indicatorIsOpen.value ? 50 : HorarioIndicator._circleRadius * 2, - decoration: BoxDecoration( - color: widget.color, - borderRadius: - BorderRadius.circular(HorarioIndicator._circleRadius), - ), - child: _horarioController.indicatorIsOpen.value ? Center( - child: _TickerTimeText(time: DateTime.now()), - ) : Container(), - )), - ), + child: _horarioController.indicatorIsOpen.value ? Center( + child: TickerTimeText(time: DateTime.now()), + ) : Container(), + )), ), ), + ), ], ); } } - -class _TickerTimeText extends StatefulWidget { - final DateTime time; - - const _TickerTimeText({ - Key? key, - required this.time, - }) : super(key: key); - - @override - State<_TickerTimeText> createState() => __TickerTimeTextState(); -} - -class __TickerTimeTextState extends State<_TickerTimeText> { - Timer? _timer; - bool _showColon = true; - - @override - void initState() { - _timer = Timer.periodic( - Duration(seconds: 1), - (Timer t) => setState(() { - _showColon = !_showColon; - }), - ); - super.initState(); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - String get _timeHour => "${widget.time.hour}"; - String get _timeMinutes => "${widget.time.minute}"; - - @override - Widget build(BuildContext context) { - return RichText( - overflow: TextOverflow.fade, - maxLines: 1, - text: TextSpan( - children: [ - TextSpan( - text: _timeHour, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.white, - ), - ), - TextSpan( - text: ":", - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: _showColon ? Colors.white : Colors.transparent, - ), - ), - TextSpan( - text: _timeMinutes, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.white, - ), - ), - ], - ), - ); - } -} From 4287b8b20a7487641581e80c0b612f0f8177d139 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 10 Apr 2024 19:14:36 -0400 Subject: [PATCH 083/194] =?UTF-8?q?patch:=20se=20agrega=20notificaci=C3=B3?= =?UTF-8?q?n=20de=20bienvenida=20*=20Se=20agrega=20notificaci=C3=B3n=20de?= =?UTF-8?q?=20bienvenida?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/onboarding/notifications_screen.dart | 9 +++++++++ lib/services/notification_service.dart | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart index 76ad5f2..adc6aa0 100644 --- a/lib/screens/onboarding/notifications_screen.dart +++ b/lib/screens/onboarding/notifications_screen.dart @@ -1,3 +1,4 @@ +import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; @@ -95,6 +96,14 @@ class _NotificationsScreenState extends State { } await _preferencesRepository.setOnboardingStep("complete"); Navigator.popUntil(context, (route) => route.isFirst); + final alias = await _preferencesRepository.getAlias(); + AwesomeNotifications().createNotification(content: NotificationContent( + id: 1, + channelKey: NotificationService.announcementsChannelKey, + actionType: ActionType.Default, + title: '¡Hola $alias! 🎉', + body: '¡Te damos la bienvenida a la aplicación Mi UTEM! 🚀', + )); Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); }, style: ElevatedButton.styleFrom( diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index ca8ccb4..43e6bd5 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -37,8 +37,7 @@ class NotificationService { channelGroupKey: 'grade_changes', channelKey: gradeChangesChannelKey, channelName: 'Grades changes', - channelDescription: - 'Notification channel to notify you when your grades change', + channelDescription: 'Notification channel to notify you when your grades change', channelShowBadge: true, importance: NotificationImportance.High, ), From 536bb9f9226774657914978c9471fcff055dbfef Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:29:34 -0400 Subject: [PATCH 084/194] =?UTF-8?q?patch:=20detenci=C3=B3n=20de=20segundo?= =?UTF-8?q?=20plano=20si=20no=20hay=20sesi=C3=B3n=20valida=20*=20Si=20no?= =?UTF-8?q?=20hay=20una=20sesi=C3=B3n=20v=C3=A1lida=20o=20no=20se=20pudo?= =?UTF-8?q?=20iniciar=20sesi=C3=B3n=20se=20cancela=20la=20actualizaci?= =?UTF-8?q?=C3=B3n=20en=20segundo=20plano.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/background_service.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 244855f..03b35fe 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -55,7 +55,12 @@ class BackgroundService { logger.d("[BackgroundFetch]: Se ejecutó la tarea 'refreshTask' (${now.toIso8601String()})"); // Refresca el token de autenticación - await Get.find().isLoggedIn(forceRefresh: true); + bool loggedIn = await Get.find().isLoggedIn(forceRefresh: true); + if(!loggedIn) { + logger.d("[BackgroundFetch]: No se pudo refrescar el token de autenticación"); + BackgroundFetch.finish(taskId); + return; + } logger.d("[BackgroundFetch]: Se refrescó el token de autenticación, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); now = DateTime.now(); From 22845b0a7438bd77892ae5e0e0d474ae43f0331c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:36:28 -0400 Subject: [PATCH 085/194] patch: se actualiza horario en segundo plano Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/background_service.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 03b35fe..452a590 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -4,6 +4,7 @@ import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; +import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/interfaces/carreras_service.dart'; @@ -101,6 +102,16 @@ class BackgroundService { logger.d("[BackgroundFetch]: Se refrescaron los datos de las carreras y asignaturas, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); now = DateTime.now(); + // Actualiza el horario + try { + final carreraId = Get.find().selectedCarrera?.id; + if(carreraId != null) { + await Get.find().getHorario(carreraId, forceRefresh: true); + } + } catch(_){} + logger.d("[BackgroundFetch]: Se refrescó el horario, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + now = DateTime.now(); + logger.d("[BackgroundFetch]: Se terminó la tarea 'refreshTask', tomó ${DateTime.now().difference(init).inMilliseconds} ms"); } From 94d19abffe599d1aada713b5ca517347d53c4185 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:36:47 -0400 Subject: [PATCH 086/194] =?UTF-8?q?patch:=20se=20agrega=20m=C3=A9todo=20pa?= =?UTF-8?q?ra=20eliminar=20cach=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/implementations/auth_service.dart | 2 +- lib/utils/http/http_client.dart | 9 +++++++-- lib/utils/http/interceptors/auth_interceptor.dart | 11 ----------- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/services/implementations/auth_service.dart b/lib/services/implementations/auth_service.dart index 0cae6ca..632f5b0 100644 --- a/lib/services/implementations/auth_service.dart +++ b/lib/services/implementations/auth_service.dart @@ -82,7 +82,7 @@ class AuthServiceImplementation implements AuthService { @override Future logout({BuildContext? context}) async { - await HttpClient.dioCacheManager.clearAll(); + await HttpClient.clearCache(); setUser(null); _credentialsService.setCredentials(null); _preferencesRepository.setOnboardingStep(null); diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index f223f33..c460ff1 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -12,17 +12,18 @@ class HttpClient { static const String productionUrl = 'https://api.exdev.cl/'; static String url = isProduction ? productionUrl : debugUrl; - static DioCacheManager dioCacheManager = DioCacheManager(CacheConfig( + static DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig( baseUrl: url, defaultMaxAge: Duration(days: 7), defaultMaxStale: Duration(days: 60), + databaseName: 'mi_utem_cache', )); static Dio httpClient = Dio(BaseOptions(baseUrl: url))..interceptors.addAll([ HeadersInterceptor(), _errorInterceptor, _logInterceptor, - dioCacheManager.interceptor, + _dioCacheManager.interceptor, ]); static Dio authClient = httpClient..interceptors.add(AuthInterceptor()); @@ -58,4 +59,8 @@ class HttpClient { return handler.next(response); }, ); + + static Future clearCache() async{ + await _dioCacheManager.clearAll(); + } } diff --git a/lib/utils/http/interceptors/auth_interceptor.dart b/lib/utils/http/interceptors/auth_interceptor.dart index e2adb88..3f4cc98 100644 --- a/lib/utils/http/interceptors/auth_interceptor.dart +++ b/lib/utils/http/interceptors/auth_interceptor.dart @@ -1,7 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/utils/http/http_client.dart'; @@ -24,11 +22,6 @@ class AuthInterceptor extends QueuedInterceptor { if(token != null) { options._setAuthenticationHeader(token); } - - final rut = user?.rut; - if(rut != null) { - options.extra[DIO_CACHE_KEY_PRIMARY_KEY] = rut.toString(); - } } return handler.next(options); @@ -48,23 +41,19 @@ class AuthInterceptor extends QueuedInterceptor { return super.onError(err, handler); } - logger.d("*"); final attempt = err.requestOptions._retryAttempt + 1; if (attempt > retries) { await _onErrorRefreshingToken(); return super.onError(err, handler); } - logger.d("**"); err.requestOptions._retryAttempt = attempt; await Future.delayed(const Duration(seconds: 1)); /* Forzar el refresco de la token de autenticación */ try { - logger.d("***"); await _authService.isLoggedIn(forceRefresh: true); final token = (await _authService.getUser())?.token; - logger.d("****", token); if(token == null) { await _onErrorRefreshingToken(); From 11911d330e835744092172fecd8624f87817c270 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:46:13 -0400 Subject: [PATCH 087/194] patch: se crea modelo de asistencia en su propio archivo Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/asignaturas/asignatura.dart | 33 ++--------------------- lib/models/asignaturas/asistencia.dart | 29 +++++++++++++++++++++ lib/widgets/asistencia_chart.dart | 36 ++++++++++++++------------ 3 files changed, 51 insertions(+), 47 deletions(-) create mode 100644 lib/models/asignaturas/asistencia.dart diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart index 1e1aa5f..265e960 100644 --- a/lib/models/asignaturas/asignatura.dart +++ b/lib/models/asignaturas/asignatura.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:mi_utem/models/asignaturas/asistencia.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/themes/theme.dart'; @@ -88,34 +89,4 @@ class Asignatura { 'intentos': intentos, 'tipoSala': tipoSala, }; -} - -class Asistencia { - num? total; - num? asistidos; - num? noAsistidos; - num? sinRegistro; - - Asistencia({ - this.total = 0, - this.asistidos = 0, - this.noAsistidos = 0, - this.sinRegistro = 0, - }); - - factory Asistencia.fromJson(Map? json) => json != null ? Asistencia( - total: json['total'] ?? 0, - asistidos: json['asistida'] ?? 0, - noAsistidos: json['noAsistidos'] ?? 0, - sinRegistro: json['sinRegistro'] ?? 0, - ) : Asistencia(); - - static List fromJsonList(dynamic json) => json != null ? (json as List).map((it) => Asistencia.fromJson(it)).toList() : []; - - Map toJson() => { - 'total': total, - 'asistidos': asistidos, - 'noAsistidos': noAsistidos, - 'sinRegistro': sinRegistro, - }; -} +} \ No newline at end of file diff --git a/lib/models/asignaturas/asistencia.dart b/lib/models/asignaturas/asistencia.dart new file mode 100644 index 0000000..9d2ec40 --- /dev/null +++ b/lib/models/asignaturas/asistencia.dart @@ -0,0 +1,29 @@ +class Asistencia { + num? total; + num? asistidos; + num? noAsistidos; + num? sinRegistro; + + Asistencia({ + this.total = 0, + this.asistidos = 0, + this.noAsistidos = 0, + this.sinRegistro = 0, + }); + + factory Asistencia.fromJson(Map? json) => json != null ? Asistencia( + total: json['total'] ?? 0, + asistidos: json['asistida'] ?? 0, + noAsistidos: json['noAsistidos'] ?? 0, + sinRegistro: json['sinRegistro'] ?? 0, + ) : Asistencia(); + + static List fromJsonList(dynamic json) => json != null ? (json as List).map((it) => Asistencia.fromJson(it)).toList() : []; + + Map toJson() => { + 'total': total, + 'asistidos': asistidos, + 'noAsistidos': noAsistidos, + 'sinRegistro': sinRegistro, + }; +} diff --git a/lib/widgets/asistencia_chart.dart b/lib/widgets/asistencia_chart.dart index 07f61d4..bf7de4c 100644 --- a/lib/widgets/asistencia_chart.dart +++ b/lib/widgets/asistencia_chart.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:community_charts_flutter/community_charts_flutter.dart' as charts; import 'package:flutter/material.dart'; -import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/asignaturas/asistencia.dart'; import 'package:mi_utem/themes/theme.dart'; extension StringExtension on Color { @@ -71,37 +71,41 @@ class AsistenciaChart extends StatelessWidget { height: 15, width: 15, ), - Container(width: 10), + const SizedBox(width: 10), Text("Asistidos (${asistencia!.asistidos})"), ], ), - Container(height: 5), + const SizedBox(height: 5), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - decoration: BoxDecoration( - color: MainTheme.reprobadoColor, - borderRadius: BorderRadius.circular(15)), - height: 15, - width: 15), - Container(width: 10), + decoration: BoxDecoration( + color: MainTheme.reprobadoColor, + borderRadius: BorderRadius.circular(15), + ), + height: 15, + width: 15, + ), + const SizedBox(width: 10), Text("No asistidos (${asistencia!.noAsistidos})"), ], ), - Container(height: 5), + const SizedBox(height: 5), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - decoration: BoxDecoration( - color: MainTheme.disabledColor, - borderRadius: BorderRadius.circular(15)), - height: 15, - width: 15), - Container(width: 10), + decoration: BoxDecoration( + color: MainTheme.disabledColor, + borderRadius: BorderRadius.circular(15), + ), + height: 15, + width: 15, + ), + const SizedBox(width: 10), Text("Sin registro (${asistencia!.sinRegistro})"), ], ), From 70401938d259ba9e2cf16ad907c7a4c3a0e053fa Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:46:54 -0400 Subject: [PATCH 088/194] =?UTF-8?q?patch:=20removido=20comentario=20de=20n?= =?UTF-8?q?otificaci=C3=B3n=20de=20nota?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../implementations/grades_service.dart | 7 ++++- lib/services/notification_service.dart | 27 +++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/services/implementations/grades_service.dart b/lib/services/implementations/grades_service.dart index 7c4cb57..4c956f2 100644 --- a/lib/services/implementations/grades_service.dart +++ b/lib/services/implementations/grades_service.dart @@ -9,6 +9,7 @@ import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/interfaces/grades_service.dart'; +import 'package:mi_utem/services/notification_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class GradesServiceImplementation implements GradesService { @@ -209,7 +210,11 @@ class GradesServiceImplementation implements GradesService { } ); - // NotificationService.showGradeChangeNotification(title, body, asignatura); + NotificationService.showGradeChangeNotification( + title: title, + body: body, + asignatura: asignatura, + ); } else if(changeType != GradeChangeType.noChange) { Sentry.captureMessage("Asignatura ha cambiado pero no notificado", level: SentryLevel.debug, diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 43e6bd5..59a68bb 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:math'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; @@ -91,24 +90,22 @@ class NotificationService { return isAllowed; } - static void showGradeChangeNotification( - String title, - String body, - Asignatura asignatura, - ) { + static void showGradeChangeNotification({ + required String title, + required String body, + required Asignatura asignatura, + }) { final Map payload = { 'type': 'grade_change', 'asignatura': jsonEncode(asignatura.toJson()), }; - notifications.createNotification( - content: NotificationContent( - id: Random().nextInt(1000000), - channelKey: gradeChangesChannelKey, - title: title, - body: body, - payload: payload, - ), - ); + notifications.createNotification(content: NotificationContent( + id: asignatura.hashCode, + channelKey: gradeChangesChannelKey, + title: title, + body: body, + payload: payload, + )); } } From dabe994e334bc02f7c9e6b85d2094968d43743e0 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:50:22 -0400 Subject: [PATCH 089/194] patch: movido algunos widgets a sus propias carpetas Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/credencial_screen.dart | 2 +- lib/widgets/{ => asignatura}/asistencia_chart.dart | 0 lib/widgets/{ => avance_malla}/avance_ramo_card.dart | 0 lib/widgets/{ => avance_malla}/carrera_list_item.dart | 0 lib/widgets/{ => credencial}/credencial_card.dart | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename lib/widgets/{ => asignatura}/asistencia_chart.dart (100%) rename lib/widgets/{ => avance_malla}/avance_ramo_card.dart (100%) rename lib/widgets/{ => avance_malla}/carrera_list_item.dart (100%) rename lib/widgets/{ => credencial}/credencial_card.dart (100%) diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index 6ff1c87..ef862e1 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -9,7 +9,7 @@ import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/interfaces/carreras_service.dart'; import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/widgets/credencial_card.dart'; +import 'package:mi_utem/widgets/credencial/credencial_card.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/flip_widget.dart'; diff --git a/lib/widgets/asistencia_chart.dart b/lib/widgets/asignatura/asistencia_chart.dart similarity index 100% rename from lib/widgets/asistencia_chart.dart rename to lib/widgets/asignatura/asistencia_chart.dart diff --git a/lib/widgets/avance_ramo_card.dart b/lib/widgets/avance_malla/avance_ramo_card.dart similarity index 100% rename from lib/widgets/avance_ramo_card.dart rename to lib/widgets/avance_malla/avance_ramo_card.dart diff --git a/lib/widgets/carrera_list_item.dart b/lib/widgets/avance_malla/carrera_list_item.dart similarity index 100% rename from lib/widgets/carrera_list_item.dart rename to lib/widgets/avance_malla/carrera_list_item.dart diff --git a/lib/widgets/credencial_card.dart b/lib/widgets/credencial/credencial_card.dart similarity index 100% rename from lib/widgets/credencial_card.dart rename to lib/widgets/credencial/credencial_card.dart From 3434521eff29704163803fe68c0c1e01d95adef6 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:51:11 -0400 Subject: [PATCH 090/194] patch: ahora default_network_image es constante Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/default_network_image.dart | 74 +++++++++++++------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/lib/widgets/default_network_image.dart b/lib/widgets/default_network_image.dart index 598199b..0421cfd 100644 --- a/lib/widgets/default_network_image.dart +++ b/lib/widgets/default_network_image.dart @@ -5,54 +5,52 @@ import 'package:mi_utem/widgets/image_view_screen.dart'; class DefaultNetworkImage extends StatelessWidget { final String? url; - DefaultNetworkImage({ + const DefaultNetworkImage({ this.url, }); @override - Widget build(BuildContext context) { - return Container( - height: double.infinity, - width: double.infinity, - child: CachedNetworkImage( - imageUrl: "$url", - imageBuilder: (context, imageProvider) => GestureDetector( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen(imageProvider: imageProvider))), - child: Container( - height: double.infinity, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - image: DecorationImage( - image: imageProvider, - fit: BoxFit.cover, - ), - ), - ), - ), - placeholder: (context, url) => Container( + Widget build(BuildContext context) => Container( + height: double.infinity, + width: double.infinity, + child: CachedNetworkImage( + imageUrl: "$url", + imageBuilder: (context, imageProvider) => GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen(imageProvider: imageProvider))), + child: Container( height: double.infinity, width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: Colors.grey[300], - ), - child: Center( - child: Icon(Icons.photo), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), ), ), - errorWidget: (context, url, error) => Container( - height: double.infinity, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.grey[300], - ), - child: Center( - child: Icon(Icons.photo), - ), + ), + placeholder: (context, url) => Container( + height: double.infinity, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.grey[300], + ), + child: Center( + child: Icon(Icons.photo), + ), + ), + errorWidget: (context, url, error) => Container( + height: double.infinity, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.grey[300], + ), + child: Center( + child: Icon(Icons.photo), ), ), - ); - } + ), + ); } From 7fae05389f43835c0021186da8fcbd0413ca6013 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:01:36 -0400 Subject: [PATCH 091/194] rem: se remueve footer_layout (no tiene uso actual) Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/footer_layout.dart | 63 ---------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 lib/widgets/footer_layout.dart diff --git a/lib/widgets/footer_layout.dart b/lib/widgets/footer_layout.dart deleted file mode 100644 index 3a47b9c..0000000 --- a/lib/widgets/footer_layout.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -class FooterLayout extends StatelessWidget { - const FooterLayout({ - Key? key, - required this.body, - required this.footer, - }) : super(key: key); - - final Widget body; - final Widget footer; - - @override - Widget build(BuildContext context) { - return CustomMultiChildLayout( - delegate: _FooterLayoutDelegate(MediaQuery.of(context).viewInsets), - children: [ - LayoutId( - id: _FooterLayout.body, - child: body, - ), - LayoutId( - id: _FooterLayout.footer, - child: footer, - ), - ], - ); - } -} - -enum _FooterLayout { - footer, - body, -} - -class _FooterLayoutDelegate extends MultiChildLayoutDelegate { - final EdgeInsets viewInsets; - - _FooterLayoutDelegate(this.viewInsets); - - @override - void performLayout(Size size) { - size = Size(size.width, size.height + viewInsets.bottom); - final footer = layoutChild(_FooterLayout.footer, BoxConstraints.loose(size)); - - final bodyConstraints = BoxConstraints.tightFor( - height: size.height - max(footer.height, viewInsets.bottom), - width: size.width, - ); - - final body = layoutChild(_FooterLayout.body, bodyConstraints); - - positionChild(_FooterLayout.body, Offset.zero); - positionChild(_FooterLayout.footer, Offset(0, body.height)); - } - - @override - bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) { - return true; - } -} From 70f58f3e1c39ae6c4f4fdab1f76b7924d3b019a7 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:02:18 -0400 Subject: [PATCH 092/194] patch: se mueven widgets de imagen a su propia carpeta Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/usuario_screen.dart | 2 +- lib/widgets/acerca/club/acerca_club.dart | 2 +- lib/widgets/acerca/club/acerca_club_desarrolladores.dart | 2 +- lib/widgets/{ => image}/default_network_image.dart | 2 +- lib/widgets/{ => image}/image_view_screen.dart | 0 lib/widgets/{ => image}/imagen_editor_modal.dart | 0 lib/widgets/permiso_ingreso/qr_card.dart | 2 +- lib/widgets/profile_photo.dart | 2 +- 8 files changed, 6 insertions(+), 6 deletions(-) rename lib/widgets/{ => image}/default_network_image.dart (96%) rename lib/widgets/{ => image}/image_view_screen.dart (100%) rename lib/widgets/{ => image}/imagen_editor_modal.dart (100%) diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index 294a160..a750201 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -11,7 +11,7 @@ import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/image_view_screen.dart'; +import 'package:mi_utem/widgets/image/image_view_screen.dart'; import 'package:mi_utem/widgets/loading_dialog.dart'; import 'package:mi_utem/widgets/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; diff --git a/lib/widgets/acerca/club/acerca_club.dart b/lib/widgets/acerca/club/acerca_club.dart index 218e978..c589997 100644 --- a/lib/widgets/acerca/club/acerca_club.dart +++ b/lib/widgets/acerca/club/acerca_club.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/widgets/default_network_image.dart'; +import 'package:mi_utem/widgets/image/default_network_image.dart'; import 'acerca_club_redes.dart'; diff --git a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart index 6b21328..cdb1a1d 100644 --- a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart +++ b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart @@ -5,7 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/widgets/image_view_screen.dart'; +import 'package:mi_utem/widgets/image/image_view_screen.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/widgets/default_network_image.dart b/lib/widgets/image/default_network_image.dart similarity index 96% rename from lib/widgets/default_network_image.dart rename to lib/widgets/image/default_network_image.dart index 0421cfd..859347a 100644 --- a/lib/widgets/default_network_image.dart +++ b/lib/widgets/image/default_network_image.dart @@ -1,6 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/image_view_screen.dart'; +import 'package:mi_utem/widgets/image/image_view_screen.dart'; class DefaultNetworkImage extends StatelessWidget { final String? url; diff --git a/lib/widgets/image_view_screen.dart b/lib/widgets/image/image_view_screen.dart similarity index 100% rename from lib/widgets/image_view_screen.dart rename to lib/widgets/image/image_view_screen.dart diff --git a/lib/widgets/imagen_editor_modal.dart b/lib/widgets/image/imagen_editor_modal.dart similarity index 100% rename from lib/widgets/imagen_editor_modal.dart rename to lib/widgets/image/imagen_editor_modal.dart diff --git a/lib/widgets/permiso_ingreso/qr_card.dart b/lib/widgets/permiso_ingreso/qr_card.dart index 5039d7e..0e7a7bc 100644 --- a/lib/widgets/permiso_ingreso/qr_card.dart +++ b/lib/widgets/permiso_ingreso/qr_card.dart @@ -7,7 +7,7 @@ import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:image/image.dart' as dartImage; import 'package:intl/intl.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; -import 'package:mi_utem/widgets/image_view_screen.dart'; +import 'package:mi_utem/widgets/image/image_view_screen.dart'; import 'package:mi_utem/widgets/permiso_ingreso/detalles_permiso.dart'; import 'package:mi_utem/widgets/permiso_ingreso/usuario_detalle.dart'; diff --git a/lib/widgets/profile_photo.dart b/lib/widgets/profile_photo.dart index 9a48f74..4e1cc7e 100644 --- a/lib/widgets/profile_photo.dart +++ b/lib/widgets/profile_photo.dart @@ -8,7 +8,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:mi_utem/widgets/imagen_editor_modal.dart'; +import 'package:mi_utem/widgets/image/imagen_editor_modal.dart'; import 'package:mi_utem/widgets/snackbar.dart'; class ProfilePhoto extends StatefulWidget { From 359af87b15cbf17df5a385a15bb35504fe1b9372 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:03:12 -0400 Subject: [PATCH 093/194] patch: se mueven widgets de carga a su propia carpeta Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/asignatura/asignatura_estudiantes_tab.dart | 2 +- lib/screens/asignatura/asignaturas_lista_screen.dart | 2 +- lib/screens/credencial_screen.dart | 2 +- lib/screens/docentes_screen.dart | 2 +- lib/screens/horario/horario_screen.dart | 4 ++-- lib/screens/permiso_covid_screen.dart | 2 +- lib/screens/splash_screen.dart | 2 +- lib/screens/usuario_screen.dart | 4 ++-- lib/widgets/{ => loading}/loading_dialog.dart | 2 +- lib/widgets/{ => loading}/loading_indicator.dart | 0 lib/widgets/login_screen/login_button.dart | 2 +- lib/widgets/main_screen/permisos/permisos_section.dart | 2 +- lib/widgets/noticias/noticias_carrusel_widget.dart | 2 +- lib/widgets/pull_to_refresh.dart | 6 ++---- 14 files changed, 16 insertions(+), 18 deletions(-) rename lib/widgets/{ => loading}/loading_dialog.dart (83%) rename lib/widgets/{ => loading}/loading_indicator.dart (100%) diff --git a/lib/screens/asignatura/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/asignatura_estudiantes_tab.dart index dd7361d..12cece0 100644 --- a/lib/screens/asignatura/asignatura_estudiantes_tab.dart +++ b/lib/screens/asignatura/asignatura_estudiantes_tab.dart @@ -8,7 +8,7 @@ import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/asignatura/modals/user_modal.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index d85ab11..a1cbda7 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -10,7 +10,7 @@ import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/widgets/asignatura/lista/lista_asignaturas.dart'; import 'package:mi_utem/widgets/asignatura/lista/sin_asignaturas_mensaje.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; class AsignaturasListaScreen extends StatefulWidget { diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index ef862e1..f954dba 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -13,7 +13,7 @@ import 'package:mi_utem/widgets/credencial/credencial_card.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/flip_widget.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; class CredencialScreen extends StatefulWidget { const CredencialScreen({ diff --git a/lib/screens/docentes_screen.dart b/lib/screens/docentes_screen.dart index 3f457bb..898e70f 100644 --- a/lib/screens/docentes_screen.dart +++ b/lib/screens/docentes_screen.dart @@ -5,7 +5,7 @@ import 'package:mi_utem/services/docentes_service.dart'; import 'package:mi_utem/utils/debounce.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; class DocentesScreen extends StatefulWidget { diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index bbc0392..cfb92db 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -11,8 +11,8 @@ import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading_dialog.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_dialog.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:path_provider/path_provider.dart'; import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index f78cc95..cb48bed 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -6,7 +6,7 @@ import 'package:mi_utem/models/permiso_ingreso.dart'; import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/permiso_ingreso/qr_card.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 705f235..020ff78 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -12,7 +12,7 @@ import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; -import 'package:mi_utem/widgets/loading_dialog.dart'; +import 'package:mi_utem/widgets/loading/loading_dialog.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:package_info_plus/package_info_plus.dart'; diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index a750201..4f41484 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -12,8 +12,8 @@ import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/image/image_view_screen.dart'; -import 'package:mi_utem/widgets/loading_dialog.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_dialog.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/widgets/loading_dialog.dart b/lib/widgets/loading/loading_dialog.dart similarity index 83% rename from lib/widgets/loading_dialog.dart rename to lib/widgets/loading/loading_dialog.dart index f8e13fa..803f1db 100644 --- a/lib/widgets/loading_dialog.dart +++ b/lib/widgets/loading/loading_dialog.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; class LoadingDialog extends StatelessWidget { @override diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading/loading_indicator.dart similarity index 100% rename from lib/widgets/loading_indicator.dart rename to lib/widgets/loading/loading_indicator.dart diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index 63ed202..09caf6b 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -11,7 +11,7 @@ import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; -import 'package:mi_utem/widgets/loading_dialog.dart'; +import 'package:mi_utem/widgets/loading/loading_dialog.dart'; import 'package:mi_utem/widgets/snackbar.dart'; diff --git a/lib/widgets/main_screen/permisos/permisos_section.dart b/lib/widgets/main_screen/permisos/permisos_section.dart index fec64fa..696c68e 100644 --- a/lib/widgets/main_screen/permisos/permisos_section.dart +++ b/lib/widgets/main_screen/permisos/permisos_section.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/main_screen/permisos/permiso_card.dart'; class PermisosCovidSection extends StatelessWidget { diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index 8c8b444..8b3c74a 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -6,7 +6,7 @@ import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/noticias/noticia_card_widget.dart'; class NoticiasCarruselWidget extends StatelessWidget { diff --git a/lib/widgets/pull_to_refresh.dart b/lib/widgets/pull_to_refresh.dart index aa4c7f1..fb7a6eb 100644 --- a/lib/widgets/pull_to_refresh.dart +++ b/lib/widgets/pull_to_refresh.dart @@ -1,10 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:custom_refresh_indicator/custom_refresh_indicator.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; - import 'package:mi_utem/themes/theme.dart'; -import 'package:mi_utem/widgets/loading_indicator.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; class PullToRefresh extends StatelessWidget { final Widget? child; From 28063fc6c70190f85611829a3769c6a992c15ab4 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:03:23 -0400 Subject: [PATCH 094/194] patch: se mueve widget al avance de malla Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/{ => avance_malla}/info_boletin_card.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/widgets/{ => avance_malla}/info_boletin_card.dart (100%) diff --git a/lib/widgets/info_boletin_card.dart b/lib/widgets/avance_malla/info_boletin_card.dart similarity index 100% rename from lib/widgets/info_boletin_card.dart rename to lib/widgets/avance_malla/info_boletin_card.dart From 013fd4811042b239e10abe45ba256f3f7c365db6 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:03:50 -0400 Subject: [PATCH 095/194] patch: se mueve widget al avance de malla Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/{ => avance_malla}/semestre_boletin_card.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/widgets/{ => avance_malla}/semestre_boletin_card.dart (100%) diff --git a/lib/widgets/semestre_boletin_card.dart b/lib/widgets/avance_malla/semestre_boletin_card.dart similarity index 100% rename from lib/widgets/semestre_boletin_card.dart rename to lib/widgets/avance_malla/semestre_boletin_card.dart From 7a27cc4d5dc550c8b3ad6cf9704749dd940295e6 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:04:17 -0400 Subject: [PATCH 096/194] patch: se mueve widget a su carpeta correspondiente Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/login_screen/formulario_credenciales.dart | 2 +- lib/widgets/{ => login_screen}/login_text_form_field.dart | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/widgets/{ => login_screen}/login_text_form_field.dart (100%) diff --git a/lib/widgets/login_screen/formulario_credenciales.dart b/lib/widgets/login_screen/formulario_credenciales.dart index 0be83ff..abbe2f6 100644 --- a/lib/widgets/login_screen/formulario_credenciales.dart +++ b/lib/widgets/login_screen/formulario_credenciales.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:mi_utem/widgets/login_text_form_field.dart'; +import 'package:mi_utem/widgets/login_screen/login_text_form_field.dart'; class FormularioCredenciales extends StatefulWidget { diff --git a/lib/widgets/login_text_form_field.dart b/lib/widgets/login_screen/login_text_form_field.dart similarity index 100% rename from lib/widgets/login_text_form_field.dart rename to lib/widgets/login_screen/login_text_form_field.dart From 0dcce14a390a6b4ec6b6b3ea375413234cc39a60 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:04:44 -0400 Subject: [PATCH 097/194] patch: se mueve widget a su carpeta correspondiente Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/{ => loading}/progress_button.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/widgets/{ => loading}/progress_button.dart (100%) diff --git a/lib/widgets/progress_button.dart b/lib/widgets/loading/progress_button.dart similarity index 100% rename from lib/widgets/progress_button.dart rename to lib/widgets/loading/progress_button.dart From 23d0fa81f1230a456df444d0e2af8a2b01f7765c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:05:16 -0400 Subject: [PATCH 098/194] patch: se mueven widgets a la carpeta de quick_access Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/main_screen.dart | 2 +- lib/widgets/{ => quick_access}/quick_menu_card.dart | 0 lib/widgets/{ => quick_access}/quick_menu_section.dart | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/widgets/{ => quick_access}/quick_menu_card.dart (100%) rename lib/widgets/{ => quick_access}/quick_menu_section.dart (94%) diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 53ffc28..a253020 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -21,7 +21,7 @@ import "package:mi_utem/widgets/main_screen/novedades/banners_section.dart"; import "package:mi_utem/widgets/main_screen/permisos/permisos_section.dart"; import "package:mi_utem/widgets/noticias/noticias_carrusel_widget.dart"; import "package:mi_utem/widgets/pull_to_refresh.dart"; -import "package:mi_utem/widgets/quick_menu_section.dart"; +import 'package:mi_utem/widgets/quick_access/quick_menu_section.dart'; class MainScreen extends StatefulWidget { const MainScreen({ diff --git a/lib/widgets/quick_menu_card.dart b/lib/widgets/quick_access/quick_menu_card.dart similarity index 100% rename from lib/widgets/quick_menu_card.dart rename to lib/widgets/quick_access/quick_menu_card.dart diff --git a/lib/widgets/quick_menu_section.dart b/lib/widgets/quick_access/quick_menu_section.dart similarity index 94% rename from lib/widgets/quick_menu_section.dart rename to lib/widgets/quick_access/quick_menu_section.dart index ce7c795..9c55615 100644 --- a/lib/widgets/quick_menu_section.dart +++ b/lib/widgets/quick_access/quick_menu_section.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/widgets/quick_menu_card.dart'; +import 'package:mi_utem/widgets/quick_access/quick_menu_card.dart'; class QuickMenuSection extends StatelessWidget { const QuickMenuSection({ From a492fd7abe45cdd9ef75f6e1565b4facf358e72a Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:05:57 -0400 Subject: [PATCH 099/194] =?UTF-8?q?patch:=20se=20mueven=20widgets=20a=20la?= =?UTF-8?q?=20carpeta=20de=20di=C3=A1logos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/notification_service.dart | 2 +- lib/services/review_service.dart | 2 +- lib/widgets/{ => dialogs}/custom_alert_dialog.dart | 0 lib/widgets/{ => dialogs}/sad_dialog.dart | 0 lib/widgets/{ => dialogs}/saludo_dialog.dart | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename lib/widgets/{ => dialogs}/custom_alert_dialog.dart (100%) rename lib/widgets/{ => dialogs}/sad_dialog.dart (100%) rename lib/widgets/{ => dialogs}/saludo_dialog.dart (100%) diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 59a68bb..27e30a8 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -5,7 +5,7 @@ import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:flutter/material.dart'; import 'package:mi_utem/controllers/notification_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/widgets/custom_alert_dialog.dart'; +import 'package:mi_utem/widgets/dialogs/custom_alert_dialog.dart'; class NotificationService { static const announcementsChannelKey = 'announcements_channel'; diff --git a/lib/services/review_service.dart b/lib/services/review_service.dart index 6e4e1cc..1cd0750 100644 --- a/lib/services/review_service.dart +++ b/lib/services/review_service.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_storage/get_storage.dart'; import 'package:in_app_review/in_app_review.dart'; import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/widgets/custom_alert_dialog.dart'; +import 'package:mi_utem/widgets/dialogs/custom_alert_dialog.dart'; class ReviewService { static final GetStorage box = GetStorage(); diff --git a/lib/widgets/custom_alert_dialog.dart b/lib/widgets/dialogs/custom_alert_dialog.dart similarity index 100% rename from lib/widgets/custom_alert_dialog.dart rename to lib/widgets/dialogs/custom_alert_dialog.dart diff --git a/lib/widgets/sad_dialog.dart b/lib/widgets/dialogs/sad_dialog.dart similarity index 100% rename from lib/widgets/sad_dialog.dart rename to lib/widgets/dialogs/sad_dialog.dart diff --git a/lib/widgets/saludo_dialog.dart b/lib/widgets/dialogs/saludo_dialog.dart similarity index 100% rename from lib/widgets/saludo_dialog.dart rename to lib/widgets/dialogs/saludo_dialog.dart From 6ac95e8e0a07244b04046ef485aa693f1bbf4562 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:06:35 -0400 Subject: [PATCH 100/194] =?UTF-8?q?patch:=20se=20mueven=20widgets=20a=20la?= =?UTF-8?q?=20carpeta=20de=20di=C3=A1logos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/{ => dialogs}/error_dialog.dart | 0 lib/widgets/dialogs/monkey_error_dialog.dart | 2 +- lib/widgets/dialogs/not_ready_dialog.dart | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/widgets/{ => dialogs}/error_dialog.dart (100%) diff --git a/lib/widgets/error_dialog.dart b/lib/widgets/dialogs/error_dialog.dart similarity index 100% rename from lib/widgets/error_dialog.dart rename to lib/widgets/dialogs/error_dialog.dart diff --git a/lib/widgets/dialogs/monkey_error_dialog.dart b/lib/widgets/dialogs/monkey_error_dialog.dart index 7c05f38..d00f2c4 100644 --- a/lib/widgets/dialogs/monkey_error_dialog.dart +++ b/lib/widgets/dialogs/monkey_error_dialog.dart @@ -1,6 +1,6 @@ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/error_dialog.dart'; +import 'package:mi_utem/widgets/dialogs/error_dialog.dart'; class MonkeyErrorDialog extends StatelessWidget { const MonkeyErrorDialog({ diff --git a/lib/widgets/dialogs/not_ready_dialog.dart b/lib/widgets/dialogs/not_ready_dialog.dart index 1d1dabd..3fd893a 100644 --- a/lib/widgets/dialogs/not_ready_dialog.dart +++ b/lib/widgets/dialogs/not_ready_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; -import 'package:mi_utem/widgets/error_dialog.dart'; +import 'package:mi_utem/widgets/dialogs/error_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; class NotReadyDialog extends StatelessWidget { From f72c5b49cd41c1a471de82c7ff07d456906105ef Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 14 May 2024 17:44:46 -0400 Subject: [PATCH 101/194] =?UTF-8?q?patch:=20arreglado=20noticias=20y=20ord?= =?UTF-8?q?en=20de=20clases=20*=20Se=20elimina=20separaci=C3=B3n=20de=20"i?= =?UTF-8?q?nterfaz"=20e=20"implementaci=C3=B3n"=20para=20usar=20directamen?= =?UTF-8?q?te=20las=20clases=20*=20Se=20implementan=20las=20noticias=20den?= =?UTF-8?q?tro=20de=20la=20app=20(utem=20tiene=20bloqueo=20de=20pa=C3=ADse?= =?UTF-8?q?s,=20asi=20que=20no=20podemos=20usar=20nuestra=20api)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/config/http_clients.dart | 2 +- .../calculator_controller.dart | 32 +--- .../horario_controller.dart | 34 +--- .../interfaces/calculator_controller.dart | 86 --------- .../interfaces/horario_controller.dart | 52 ------ lib/models/noticia.dart | 8 +- lib/models/user/persona.dart | 28 +++ lib/models/user/user.dart | 16 +- .../asignaturas_repository.dart | 5 +- .../auth_repository.dart | 6 +- .../carreras_repository.dart | 4 +- .../credentials_repository.dart | 6 +- .../grades_repository.dart | 4 +- .../horario_repository.dart | 4 +- .../implementations/noticias_repository.dart | 17 -- .../interfaces/asignaturas_repository.dart | 10 - .../interfaces/auth_repository.dart | 21 --- .../interfaces/carreras_repository.dart | 7 - .../interfaces/credentials_repository.dart | 22 --- .../interfaces/grades_repository.dart | 11 -- .../interfaces/horario_repository.dart | 6 - .../interfaces/noticias_repository.dart | 6 - .../permiso_ingreso_repository.dart | 8 - .../interfaces/preferences_repository.dart | 30 --- lib/repositories/noticias_repository.dart | 31 ++++ .../permiso_ingreso_repository.dart | 5 +- .../preferences_repository.dart | 20 +- .../asignatura/asignatura_detalle_screen.dart | 6 +- .../asignatura_estudiantes_tab.dart | 16 +- .../asignatura/asignaturas_lista_screen.dart | 4 +- lib/screens/calculadora_notas_screen.dart | 2 +- lib/screens/credencial_screen.dart | 4 +- lib/screens/horario/horario_screen.dart | 2 +- .../horario/widgets/horario_days_header.dart | 2 +- .../horario/widgets/horario_indicator.dart | 2 +- .../widgets/horario_main_scroller.dart | 2 +- .../widgets/horario_periods_header.dart | 2 +- lib/screens/main_screen.dart | 26 +-- .../onboarding/notifications_screen.dart | 2 +- lib/screens/onboarding/set_alias_screen.dart | 2 +- lib/screens/onboarding/welcome_screen.dart | 2 +- lib/screens/perfil/perfil_screen.dart | 173 ++++++++++++++++++ lib/screens/permiso_covid_screen.dart | 2 +- lib/screens/splash_screen.dart | 4 +- lib/screens/usuario_screen.dart | 4 +- lib/service_manager.dart | 71 +++---- .../{implementations => }/auth_service.dart | 9 +- lib/services/background_service.dart | 14 +- .../carreras_service.dart | 10 +- .../{implementations => }/grades_service.dart | 25 ++- lib/services/interfaces/auth_service.dart | 23 --- lib/services/interfaces/carreras_service.dart | 14 -- lib/services/interfaces/grades_service.dart | 22 --- lib/utils/http/http_client.dart | 4 +- .../http/interceptors/auth_interceptor.dart | 2 +- .../editar_notas_widget.dart | 2 +- .../nota_examen_display_widget.dart | 2 +- .../nota_final_display_widget.dart | 2 +- .../calculadora_notas/nota_list_item.dart | 2 +- .../nota_presentacion_display_widget.dart | 2 +- .../notas_calculadora_widget.dart | 2 +- lib/widgets/custom_drawer.dart | 8 +- lib/widgets/horario/bloque_clase.dart | 2 +- lib/widgets/horario/bloque_ramo_card.dart | 4 +- lib/widgets/login_screen/login_button.dart | 6 +- lib/widgets/login_screen/login_form.dart | 2 +- .../permisos/permisos_section.dart | 2 +- .../noticias/noticias_carrusel_widget.dart | 2 +- lib/widgets/profile_photo.dart | 57 +++--- 69 files changed, 414 insertions(+), 613 deletions(-) rename lib/controllers/{implementations => }/calculator_controller.dart (95%) rename lib/controllers/{implementations => }/horario_controller.dart (93%) delete mode 100644 lib/controllers/interfaces/calculator_controller.dart delete mode 100644 lib/controllers/interfaces/horario_controller.dart create mode 100644 lib/models/user/persona.dart rename lib/repositories/{implementations => }/asignaturas_repository.dart (88%) rename lib/repositories/{implementations => }/auth_repository.dart (86%) rename lib/repositories/{implementations => }/carreras_repository.dart (78%) rename lib/repositories/{implementations => }/credentials_repository.dart (77%) rename lib/repositories/{implementations => }/grades_repository.dart (82%) rename lib/repositories/{implementations => }/horario_repository.dart (79%) delete mode 100644 lib/repositories/implementations/noticias_repository.dart delete mode 100644 lib/repositories/interfaces/asignaturas_repository.dart delete mode 100644 lib/repositories/interfaces/auth_repository.dart delete mode 100644 lib/repositories/interfaces/carreras_repository.dart delete mode 100644 lib/repositories/interfaces/credentials_repository.dart delete mode 100644 lib/repositories/interfaces/grades_repository.dart delete mode 100644 lib/repositories/interfaces/horario_repository.dart delete mode 100644 lib/repositories/interfaces/noticias_repository.dart delete mode 100644 lib/repositories/interfaces/permiso_ingreso_repository.dart delete mode 100644 lib/repositories/interfaces/preferences_repository.dart create mode 100644 lib/repositories/noticias_repository.dart rename lib/repositories/{implementations => }/permiso_ingreso_repository.dart (83%) rename lib/repositories/{implementations => }/preferences_repository.dart (76%) create mode 100644 lib/screens/perfil/perfil_screen.dart rename lib/services/{implementations => }/auth_service.dart (94%) rename lib/services/{implementations => }/carreras_service.dart (84%) rename lib/services/{implementations => }/grades_service.dart (94%) delete mode 100644 lib/services/interfaces/auth_service.dart delete mode 100644 lib/services/interfaces/carreras_service.dart delete mode 100644 lib/services/interfaces/grades_service.dart diff --git a/lib/config/http_clients.dart b/lib/config/http_clients.dart index edfb362..ff0e5b2 100644 --- a/lib/config/http_clients.dart +++ b/lib/config/http_clients.dart @@ -2,7 +2,7 @@ import 'package:get/get.dart'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/services/auth_service.dart'; final httpClient = InterceptedClient.build( interceptors: [ diff --git a/lib/controllers/implementations/calculator_controller.dart b/lib/controllers/calculator_controller.dart similarity index 95% rename from lib/controllers/implementations/calculator_controller.dart rename to lib/controllers/calculator_controller.dart index 9314d8c..420c393 100644 --- a/lib/controllers/implementations/calculator_controller.dart +++ b/lib/controllers/calculator_controller.dart @@ -1,10 +1,9 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -class CalculatorControllerImplementation implements CalculatorController { +class CalculatorController { /* Porcentaje máximo de todas las notas */ static const maxPercentage = 100; @@ -25,74 +24,52 @@ class CalculatorControllerImplementation implements CalculatorController { static const presentationFinalWeight = 1 - examFinalWeight; /* Notas parciales */ - @override RxList partialGrades = [].obs; /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ - @override RxList percentageTextFieldControllers = [].obs; /* Controlador de texto para las notas con máscara (para autocompletar formato) */ - @override RxList gradeTextFieldControllers = [].obs; /* Nota del examen */ - @override Rx examGrade = Rx(null); /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ - @override Rx examGradeTextFieldController = MaskedTextController(mask: "0.0").obs; - @override RxBool freeEditable = false.obs; - @override Rx calculatedFinalGrade = Rx(null); - @override Rx calculatedPresentationGrade = Rx(null); - @override Rx amountOfPartialGradesWithoutGrade = 0.obs; - @override Rx amountOfPartialGradesWithoutPercentage = 0.obs; - @override RxBool hasMissingPartialGrade = false.obs; - @override RxBool canTakeExam = false.obs; - @override Rx minimumRequiredExamGrade = Rx(null); - @override RxDouble percentageOfPartialGrades = 0.0.obs; - @override RxDouble missingPercentage = 0.0.obs; - @override RxBool hasMissingPercentage = false.obs; - @override Rx suggestedPercentage = Rx(null); - @override Rx suggestedPresentationGrade = Rx(null); - @override RxDouble percentageWithoutGrade = 0.0.obs; - @override RxBool hasCorrectPercentage = false.obs; - @override Rx suggestedGrade = Rx(null); - @override void updateWithGrades(Grades grades) { partialGrades.clear(); percentageTextFieldControllers.clear(); @@ -105,7 +82,6 @@ class CalculatorControllerImplementation implements CalculatorController { setExamGrade(grades.notaExamen); } - @override void updateGradeAt(int index, IEvaluacion updatedGrade) { final grade = partialGrades[index]; if(!(grade.editable || freeEditable.value)) { @@ -120,7 +96,6 @@ class CalculatorControllerImplementation implements CalculatorController { } } - @override void addGrade(IEvaluacion grade) { partialGrades.add(grade); percentageTextFieldControllers.add(MaskedTextController( @@ -134,7 +109,6 @@ class CalculatorControllerImplementation implements CalculatorController { _updateCalculations(); } - @override void removeGradeAt(int index) { final grade = partialGrades[index]; if(!(grade.editable || freeEditable.value)) { @@ -147,26 +121,22 @@ class CalculatorControllerImplementation implements CalculatorController { _updateCalculations(); } - @override void makeEditable() { freeEditable.value = true; _updateCalculations(); } - @override void makeNonEditable() { freeEditable.value = false; _updateCalculations(); } - @override void clearExamGrade() { examGrade.value = null; examGradeTextFieldController.value.updateText(""); _updateCalculations(); } - @override void setExamGrade(num? grade, { bool updateTextController = true }) { examGrade.value = grade?.toDouble(); if(updateTextController) { diff --git a/lib/controllers/implementations/horario_controller.dart b/lib/controllers/horario_controller.dart similarity index 93% rename from lib/controllers/implementations/horario_controller.dart rename to lib/controllers/horario_controller.dart index ba489f7..6fc0ef5 100644 --- a/lib/controllers/implementations/horario_controller.dart +++ b/lib/controllers/horario_controller.dart @@ -1,75 +1,57 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; +import 'package:mi_utem/repositories/horario_repository.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; +import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:vector_math/vector_math_64.dart' as vector; -class HorarioControllerImplementation implements HorarioController { +class HorarioController { final _storage = GetStorage(); final _randomColors = Colors.primaries.toList()..shuffle(); final _now = DateTime.now(); - @override num daysCount = 6; - @override num periodsCount = 9; - @override String startTime = "7:55"; - @override Duration periodDuration = Duration(minutes: 90); - @override Duration periodGap = Duration(minutes: 5); - @override List usedColors = []; - @override RxDouble zoom = 0.5.obs; - @override RxBool indicatorIsOpen = false.obs; - @override RxBool isCenteredInCurrentPeriodAndDay = false.obs; - @override TransformationController blockContentController = TransformationController(); - @override TransformationController daysHeaderController = TransformationController(); - @override TransformationController periodHeaderController = TransformationController(); - @override TransformationController cornerController = TransformationController(); Function? _onUpdate; - @override List get unusedColors { List availableColors = [..._randomColors].where((Color color) => !usedColors.contains(color)).toList(); return availableColors.isEmpty ? [..._randomColors] : availableColors; } - @override double get minutesFromStart => _now.difference(DateTime(_now.year, _now.month, _now.day, int.parse(startTime.split(":")[0]), int.parse(startTime.split(":")[1]))).inMinutes.toDouble(); - @override int? get indexOfCurrentDayStartingAtMonday => _now.weekday > daysCount ? null : _now.weekday - 1; - @override int? get indexOfCurrentPeriod { final periodBlockDuration = periodDuration.inMinutes + (periodGap.inMinutes * 2); @@ -82,7 +64,6 @@ class HorarioControllerImplementation implements HorarioController { return null; } - @override void init(BuildContext context) { zoom.value = RemoteConfigService.horarioZoom; moveViewportToCurrentPeriodAndDay(context); @@ -91,7 +72,6 @@ class HorarioControllerImplementation implements HorarioController { _setScrollControllerListeners(); } - @override Future getHorario({ bool forceRefresh = false }) async { final carrerasService = Get.find(); Carrera? carrera = carrerasService.selectedCarrera; @@ -109,7 +89,6 @@ class HorarioControllerImplementation implements HorarioController { return horario; } - @override void moveViewportTo(BuildContext context, double x, double y) { final viewportWidth = MediaQuery.of(context).size.width - MediaQuery.of(context).padding.horizontal; final viewportHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.vertical; @@ -133,7 +112,6 @@ class HorarioControllerImplementation implements HorarioController { _onChangeAnyController(); } - @override void moveViewportToPeriodIndexAndDayIndex(BuildContext context, int periodIndex, int dayIndex) { final blockWidth = HorarioMainScroller.blockWidth; final x = (dayIndex * blockWidth) + (blockWidth / 2); @@ -144,7 +122,6 @@ class HorarioControllerImplementation implements HorarioController { moveViewportTo(context, x, y); } - @override void moveViewportToCurrentPeriodAndDay(BuildContext context) { final periodIndex = indexOfCurrentPeriod ?? 0; final dayIndex = indexOfCurrentDayStartingAtMonday ?? 0; @@ -154,7 +131,6 @@ class HorarioControllerImplementation implements HorarioController { isCenteredInCurrentPeriodAndDay.value = true; } - @override void setZoom(double zoom) { blockContentController.value = blockContentController.value..setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); periodHeaderController.value = periodHeaderController.value..setDiagonal(vector.Vector4(zoom, zoom, zoom, 1)); @@ -164,7 +140,6 @@ class HorarioControllerImplementation implements HorarioController { _onChangeAnyController(); } - @override void addAsignaturaAndSetColor(Asignatura asignatura, {Color? color}) { bool hasColor = getColor(asignatura) != null; if (hasColor) { @@ -177,19 +152,16 @@ class HorarioControllerImplementation implements HorarioController { _storage.write(_key, _newColor.value); } - @override Color? getColor(Asignatura asignatura) { final _key = '${asignatura.codigo}_${asignatura.tipoHora}'; final _colorValue = _storage.read(_key); return _colorValue != null ? Color(_colorValue) : null; } - @override void setIndicatorIsOpen(bool isOpen) { indicatorIsOpen.value = isOpen; } - @override void setOnUpdate(Function? onUpdate) => _onUpdate = onUpdate; void _setRandomColorsByHorario(Horario horario) => horario.horario?.forEach((dia) => dia.forEach((bloque) { diff --git a/lib/controllers/interfaces/calculator_controller.dart b/lib/controllers/interfaces/calculator_controller.dart deleted file mode 100644 index bf17291..0000000 --- a/lib/controllers/interfaces/calculator_controller.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:extended_masked_text/extended_masked_text.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/models/evaluacion/evaluacion.dart'; -import 'package:mi_utem/models/evaluacion/grades.dart'; - -abstract class CalculatorController { - - /* Notas parciales */ - abstract RxList partialGrades; - - /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ - abstract RxList percentageTextFieldControllers; - - /* Controlador de texto para las notas con máscara (para autocompletar formato) */ - abstract RxList gradeTextFieldControllers; - - /* Nota del examen */ - abstract Rx examGrade; - - /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ - abstract Rx examGradeTextFieldController; - - abstract RxBool freeEditable; - - /* Nota final calculada */ - abstract Rx calculatedFinalGrade; - - /* Nota de presentación calculada */ - abstract Rx calculatedPresentationGrade; - - /* Cantidad de notas parciales sin nota */ - abstract Rx amountOfPartialGradesWithoutGrade; - - /* Cantidad de notas parciales sin porcentaje */ - abstract Rx amountOfPartialGradesWithoutPercentage; - - /* Si hay notas parciales sin nota */ - abstract RxBool hasMissingPartialGrade; - - /* Si puede tomar examen */ - abstract RxBool canTakeExam; - - /* Nota mínima requerida para el examen */ - abstract Rx minimumRequiredExamGrade; - - /* Porcentaje de las notas parciales */ - abstract RxDouble percentageOfPartialGrades; - - /* Porcentaje faltante */ - abstract RxDouble missingPercentage; - - /* Si hay porcentaje faltante */ - abstract RxBool hasMissingPercentage; - - /* Porcentaje sugerido */ - abstract Rx suggestedPercentage; - - /* Nota de presentación sugerida */ - abstract Rx suggestedPresentationGrade; - - /* Porcentaje sin nota */ - abstract RxDouble percentageWithoutGrade; - - /* Si hay porcentaje sin nota */ - abstract RxBool hasCorrectPercentage; - - /* Nota sugerida */ - abstract Rx suggestedGrade; - - void updateWithGrades(Grades grades); - - void updateGradeAt(int index, IEvaluacion updatedGrade); - - void clearExamGrade(); - - /* Actualiza la nota del examen, y si es verdadero también el controlador de texto */ - void setExamGrade(num? grade, { bool updateTextController = true }); - - void addGrade(IEvaluacion grade); - - void removeGradeAt(int index); - - void makeEditable(); - - void makeNonEditable(); -} \ No newline at end of file diff --git a/lib/controllers/interfaces/horario_controller.dart b/lib/controllers/interfaces/horario_controller.dart deleted file mode 100644 index 34c9acb..0000000 --- a/lib/controllers/interfaces/horario_controller.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/models/horario.dart'; - -abstract class HorarioController { - abstract num daysCount; - abstract num periodsCount; - - abstract String startTime; - abstract Duration periodDuration; - abstract Duration periodGap; - - abstract List usedColors; - abstract RxDouble zoom; - abstract RxBool indicatorIsOpen; - abstract RxBool isCenteredInCurrentPeriodAndDay; - - abstract TransformationController blockContentController; - abstract TransformationController daysHeaderController; - abstract TransformationController periodHeaderController; - abstract TransformationController cornerController; - - List get unusedColors; - - double get minutesFromStart; - - int? get indexOfCurrentDayStartingAtMonday; - - int? get indexOfCurrentPeriod; - - void init(BuildContext context); - - Future getHorario({ bool forceRefresh = false }); - - void moveViewportToCurrentPeriodAndDay(BuildContext context); - - void moveViewportToPeriodIndexAndDayIndex(BuildContext context, int periodIndex, int dayIndex); - - void moveViewportTo(BuildContext context, double x, double y); - - void setZoom(double zoom); - - void addAsignaturaAndSetColor(Asignatura asignatura, {Color? color}); - - Color? getColor(Asignatura asignatura); - - void setIndicatorIsOpen(bool isOpen); - - void setOnUpdate(Function? onUpdate); - -} \ No newline at end of file diff --git a/lib/models/noticia.dart b/lib/models/noticia.dart index fba9622..65e26af 100644 --- a/lib/models/noticia.dart +++ b/lib/models/noticia.dart @@ -14,10 +14,10 @@ class Noticia { factory Noticia.fromJson(Map? json) => json != null ? Noticia( id: json["id"], - titulo: json["titulo"], - subtitulo: json["subtitulo"], - link: json["link"], - imagen: json["imagen"], + titulo: json['yoast_head_json']['title'], + subtitulo: json['yoast_head_json']['og_description'], + imagen: json['yoast_head_json']['og_image'][0]['url'], + link: "https://noticias.utem.cl/?p=${json['id']}", ) : Noticia.empty(); static List fromJsonList(List json) => json.map((e) => Noticia.fromJson(e)).toList(); diff --git a/lib/models/user/persona.dart b/lib/models/user/persona.dart new file mode 100644 index 0000000..bfcdc45 --- /dev/null +++ b/lib/models/user/persona.dart @@ -0,0 +1,28 @@ +import 'package:mi_utem/models/user/rut.dart'; +import 'package:mi_utem/utils/string_utils.dart'; + +class Persona { + + final String nombreCompleto; + final Rut? rut; + + Persona({ + required this.nombreCompleto, + required this.rut + }); + + get nombreCompletoCapitalizado => capitalize(nombreCompleto); + get primerNombre => nombreCompletoCapitalizado.split(' ')[0]; + get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0].toUpperCase()).join(''); + + factory Persona.fromJson(Map json) => Persona( + nombreCompleto: json['nombreCompleto'], + rut: json.containsKey("rut") ? Rut.fromString("${json['rut']}") : null + ); + + Map toJson() => { + 'nombreCompleto': nombreCompleto, + 'rut': rut?.rut, + }; + +} \ No newline at end of file diff --git a/lib/models/user/user.dart b/lib/models/user/user.dart index c552610..2621c74 100644 --- a/lib/models/user/user.dart +++ b/lib/models/user/user.dart @@ -1,12 +1,12 @@ import 'dart:convert'; +import 'package:mi_utem/models/user/persona.dart'; import 'package:mi_utem/models/user/rut.dart'; import 'package:mi_utem/utils/string_utils.dart'; -class User { +class User extends Persona { String? token; - Rut? rut; String? correoPersonal; String? correoUtem; @@ -14,26 +14,22 @@ class User { String? fotoBase64; List perfiles; - String nombreCompleto; String? nombres; String? apellidos; String? username; String? fotoUrl; - get nombreCompletoCapitalizado => capitalize(nombreCompleto); get nombreDisplayCapitalizado => capitalize("${nombres?.split(' ')[0]} $apellidos"); - get primerNombre => nombreCompletoCapitalizado.split(' ')[0]; - get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0].toUpperCase()).join(''); User({ + super.rut, + super.nombreCompleto = "N/N", this.token, - this.rut, this.correoPersonal = "N/N", this.correoUtem, this.fotoBase64, this.perfiles = const [], - this.nombreCompleto = "N/N", this.nombres, this.apellidos, this.username, @@ -43,13 +39,13 @@ class User { static List fromJsonList(List? list) => list != null ? list.map((json) => User.fromJson(json as Map)).toList() : []; factory User.fromJson(Map json) => User( - token: json['token'], rut: json.containsKey('rut') ? Rut.fromString("${json['rut']}") : null, + nombreCompleto: json['nombreCompleto'], + token: json['token'], correoPersonal: json['correoPersonal'], correoUtem: json['correoUtem'], fotoBase64: json['fotoBase64'], perfiles: ((json['perfiles'] as List?) ?? []).map((it) => it.toString()).toList(), - nombreCompleto: json['nombreCompleto'], nombres: json['nombres'], apellidos: json['apellidos'], username: json['username'], diff --git a/lib/repositories/implementations/asignaturas_repository.dart b/lib/repositories/asignaturas_repository.dart similarity index 88% rename from lib/repositories/implementations/asignaturas_repository.dart rename to lib/repositories/asignaturas_repository.dart index ada535e..7f15ac8 100644 --- a/lib/repositories/implementations/asignaturas_repository.dart +++ b/lib/repositories/asignaturas_repository.dart @@ -2,14 +2,12 @@ import 'package:dio/dio.dart'; import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; import 'package:mi_utem/utils/http/http_client.dart'; -class AsignaturasRepositoryImplementation implements AsignaturasRepository { +class AsignaturasRepository { final _authClient = HttpClient.authClient; - @override Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}) async { if(carreraId == null) { return null; @@ -25,7 +23,6 @@ class AsignaturasRepositoryImplementation implements AsignaturasRepository { return Asignatura.fromJsonList(response.data); } - @override Future getDetalleAsignatura(Asignatura? asignatura, {bool forceRefresh = false}) async { if(asignatura == null) { return null; diff --git a/lib/repositories/implementations/auth_repository.dart b/lib/repositories/auth_repository.dart similarity index 86% rename from lib/repositories/implementations/auth_repository.dart rename to lib/repositories/auth_repository.dart index aceb322..08f89fc 100644 --- a/lib/repositories/implementations/auth_repository.dart +++ b/lib/repositories/auth_repository.dart @@ -2,27 +2,23 @@ import 'package:dio/dio.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; import 'package:mi_utem/utils/http/http_client.dart'; -class AuthRepositoryImplementation extends AuthRepository { +class AuthRepository { final _httpClient = HttpClient.httpClient; - @override Future auth({required Credentials credentials}) async { final response = await _httpClient.post("$apiUrl/v1/auth", data: credentials.toJson()); return User.fromJson(response.data as Map); } - @override Future refresh({required String token, required Credentials credentials}) async { final response = await _httpClient.post("$apiUrl/v1/auth/refresh", data: credentials.toJson(), options: Options(headers: {"Authorization": "Bearer $token"})); final json = response.data as Map; return json["token"]; } - @override Future updateProfilePicture({required String image}) async { final response = await HttpClient.authClient.put("$apiUrl/v1/usuarios/foto", data: ({"imagen": image})); final json = response.data; diff --git a/lib/repositories/implementations/carreras_repository.dart b/lib/repositories/carreras_repository.dart similarity index 78% rename from lib/repositories/implementations/carreras_repository.dart rename to lib/repositories/carreras_repository.dart index 918306c..02f74a6 100644 --- a/lib/repositories/implementations/carreras_repository.dart +++ b/lib/repositories/carreras_repository.dart @@ -1,14 +1,12 @@ import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; import 'package:mi_utem/utils/http/http_client.dart'; -class CarrerasRepositoryImplementation extends CarrerasRepository { +class CarrerasRepository { final _authClient = HttpClient.authClient; - @override Future> getCarreras({ bool forceRefresh = false }) async { final response = await _authClient.get("$apiUrl/v1/carreras", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: '/carreras')); return Carrera.fromJsonList(response.data as List); diff --git a/lib/repositories/implementations/credentials_repository.dart b/lib/repositories/credentials_repository.dart similarity index 77% rename from lib/repositories/implementations/credentials_repository.dart rename to lib/repositories/credentials_repository.dart index 47114d0..7bf7a6d 100644 --- a/lib/repositories/implementations/credentials_repository.dart +++ b/lib/repositories/credentials_repository.dart @@ -2,11 +2,9 @@ import 'dart:convert'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/user/credential.dart'; -import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; -class CredentialsRepositoryImplementation implements CredentialsRepository { +class CredentialsRepository { - @override Future getCredentials() async { final data = await secureStorage.read(key: "credentials"); if(data == null || data == "null") { @@ -16,10 +14,8 @@ class CredentialsRepositoryImplementation implements CredentialsRepository { return Credentials.fromJson(jsonDecode(data) as Map); } - @override Future hasCredentials() async => await secureStorage.containsKey(key: "credentials"); - @override Future setCredentials(Credentials? credential) async => await secureStorage.write(key: "credentials", value: credential != null ? credential.toString() : null); } \ No newline at end of file diff --git a/lib/repositories/implementations/grades_repository.dart b/lib/repositories/grades_repository.dart similarity index 82% rename from lib/repositories/implementations/grades_repository.dart rename to lib/repositories/grades_repository.dart index f69ded0..df9d47a 100644 --- a/lib/repositories/implementations/grades_repository.dart +++ b/lib/repositories/grades_repository.dart @@ -1,13 +1,11 @@ import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; import 'package:mi_utem/utils/http/http_client.dart'; -class GradesRepositoryImplementation extends GradesRepository { +class GradesRepository { final _authClient = HttpClient.authClient; - @override Future getGrades({required String carreraId, required String asignaturaId, bool forceRefresh = false}) async { final response = await _authClient.get("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "carreras/$carreraId/asignaturas/$asignaturaId/notas")); return Grades.fromJson(response.data as Map); diff --git a/lib/repositories/implementations/horario_repository.dart b/lib/repositories/horario_repository.dart similarity index 79% rename from lib/repositories/implementations/horario_repository.dart rename to lib/repositories/horario_repository.dart index 67d26b0..5d69c8c 100644 --- a/lib/repositories/implementations/horario_repository.dart +++ b/lib/repositories/horario_repository.dart @@ -1,13 +1,11 @@ import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; import 'package:mi_utem/utils/http/http_client.dart'; -class HorarioRepositoryImplementation implements HorarioRepository { +class HorarioRepository { final _authClient = HttpClient.authClient; - @override Future getHorario(String carreraId, {bool forceRefresh = false}) async { final response = await _authClient.get("$apiUrl/v1/carreras/$carreraId/horarios", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "carreras/$carreraId/horarios")); return Horario.fromJson(response.data); diff --git a/lib/repositories/implementations/noticias_repository.dart b/lib/repositories/implementations/noticias_repository.dart deleted file mode 100644 index c89188a..0000000 --- a/lib/repositories/implementations/noticias_repository.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; -import 'package:mi_utem/utils/http/http_client.dart'; - -class NoticiasRepositoryImplementation implements NoticiasRepository { - - final _httpClient = HttpClient.httpClient; - - @override - Future?> getNoticias({ bool forceRefresh = false }) async { - final response = await _httpClient.get("$apiUrl/v1/noticias", options: buildCacheOptions(Duration(days: 14), forceRefresh: forceRefresh, subKey: "/noticias")); - return Noticia.fromJsonList(response.data as List); - } - -} \ No newline at end of file diff --git a/lib/repositories/interfaces/asignaturas_repository.dart b/lib/repositories/interfaces/asignaturas_repository.dart deleted file mode 100644 index c17b645..0000000 --- a/lib/repositories/interfaces/asignaturas_repository.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:mi_utem/models/asignaturas/asignatura.dart'; - -abstract class AsignaturasRepository { - - /* Obtiene las asignaturas */ - Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}); - - /* Obtiene los detalles faltantes de la asignatura */ - Future getDetalleAsignatura(Asignatura? asignatura, {bool forceRefresh = false}); -} \ No newline at end of file diff --git a/lib/repositories/interfaces/auth_repository.dart b/lib/repositories/interfaces/auth_repository.dart deleted file mode 100644 index 3ccb8b9..0000000 --- a/lib/repositories/interfaces/auth_repository.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:mi_utem/models/user/credential.dart'; -import 'package:mi_utem/models/user/user.dart'; - -abstract class AuthRepository { - - /* Envía las credenciales y retorna al usuario */ - Future auth({ - required Credentials credentials, - }); - - /* Refresca y retorna el nuevo token */ - Future refresh({ - required String token, - required Credentials credentials, - }); - - /* Actualiza la foto de perfil y retorna la url */ - Future updateProfilePicture({ - required String image, - }); -} \ No newline at end of file diff --git a/lib/repositories/interfaces/carreras_repository.dart b/lib/repositories/interfaces/carreras_repository.dart deleted file mode 100644 index b4430ae..0000000 --- a/lib/repositories/interfaces/carreras_repository.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:mi_utem/models/carrera.dart'; - -abstract class CarrerasRepository { - - /* Obtiene las carreras */ - Future> getCarreras({ bool forceRefresh = false }); -} \ No newline at end of file diff --git a/lib/repositories/interfaces/credentials_repository.dart b/lib/repositories/interfaces/credentials_repository.dart deleted file mode 100644 index 8221eab..0000000 --- a/lib/repositories/interfaces/credentials_repository.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:mi_utem/models/user/credential.dart'; - -abstract class CredentialsRepository { - - /* - Verifica si existen credenciales guardadas - @return true si existen credenciales guardadas, false en caso contrario - */ - Future hasCredentials(); - - /* - Obtiene las credenciales guardadas - @return Credenciales guardadas - */ - Future getCredentials(); - - /* - Guarda las credenciales - @param credential Credenciales a guardar - */ - Future setCredentials(Credentials? credential); -} \ No newline at end of file diff --git a/lib/repositories/interfaces/grades_repository.dart b/lib/repositories/interfaces/grades_repository.dart deleted file mode 100644 index 1999ccf..0000000 --- a/lib/repositories/interfaces/grades_repository.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:mi_utem/models/evaluacion/grades.dart'; - -abstract class GradesRepository { - - /* Obtiene las notas de la carrera y asignatura especificada */ - Future getGrades({ - required String carreraId, - required String asignaturaId, - bool forceRefresh = false, - }); -} \ No newline at end of file diff --git a/lib/repositories/interfaces/horario_repository.dart b/lib/repositories/interfaces/horario_repository.dart deleted file mode 100644 index 34ceef6..0000000 --- a/lib/repositories/interfaces/horario_repository.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:mi_utem/models/horario.dart'; - -abstract class HorarioRepository { - - Future getHorario(String carreraId, {bool forceRefresh = false}); -} \ No newline at end of file diff --git a/lib/repositories/interfaces/noticias_repository.dart b/lib/repositories/interfaces/noticias_repository.dart deleted file mode 100644 index 1a1b110..0000000 --- a/lib/repositories/interfaces/noticias_repository.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:mi_utem/models/noticia.dart'; - -abstract class NoticiasRepository { - - Future?> getNoticias({ bool forceRefresh = false }); -} \ No newline at end of file diff --git a/lib/repositories/interfaces/permiso_ingreso_repository.dart b/lib/repositories/interfaces/permiso_ingreso_repository.dart deleted file mode 100644 index f59a355..0000000 --- a/lib/repositories/interfaces/permiso_ingreso_repository.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:mi_utem/models/permiso_ingreso.dart'; - -abstract class PermisoIngresoRepository { - - Future getDetallesPermiso(String id, { bool forceRefresh = false}); - - Future> getPermisos({ bool forceRefresh = false }); -} \ No newline at end of file diff --git a/lib/repositories/interfaces/preferences_repository.dart b/lib/repositories/interfaces/preferences_repository.dart deleted file mode 100644 index f37db00..0000000 --- a/lib/repositories/interfaces/preferences_repository.dart +++ /dev/null @@ -1,30 +0,0 @@ -abstract class PreferencesRepository { - - /* Revisa si el usuario tiene un alias guardado */ - Future hasAlias(); - - /* Guarda el alias del usuario */ - Future setAlias(String? alias); - - /* Obtiene el alias del usuario */ - Future getAlias(); - - /* Revisa si el usuario tiene último inicio de sesión */ - Future hasLastLogin(); - - /* Guarda el último inicio de sesión */ - Future setLastLogin(DateTime? lastLogin); - - /* Obtiene el último inicio de sesión */ - Future getLastLogin(); - - /* Revisa si ha hecho el onboarding */ - Future hasCompletedOnboarding(); - - /* Guarda el paso actual del onboarding */ - Future setOnboardingStep(String? step); - - /* Obtiene el paso actual del onboarding */ - Future getOnboardingStep(); - -} \ No newline at end of file diff --git a/lib/repositories/noticias_repository.dart b/lib/repositories/noticias_repository.dart new file mode 100644 index 0000000..4900b54 --- /dev/null +++ b/lib/repositories/noticias_repository.dart @@ -0,0 +1,31 @@ +import 'package:dio/dio.dart'; +import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:mi_utem/models/noticia.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/utils/http/interceptors/headers_interceptor.dart'; + +class NoticiasRepository { + + final _httpClient = Dio(BaseOptions(baseUrl: "https://noticias.utem.cl"))..interceptors.addAll([ + HeadersInterceptor(), + HttpClient.logInterceptor, + DioCacheManager(CacheConfig( + baseUrl: "https://noticias.utem.cl", + defaultMaxAge: Duration(days: 7), + defaultMaxStale: Duration(days: 60), + databaseName: 'utem_noticias_cache', + )).interceptor, + ]); + + Future?> getNoticias({ bool forceRefresh = false }) async { + final hasta = DateTime.now().toUtc().toIso8601String(); + final desde = DateTime.now().subtract(Duration(days: 180)).toUtc().toIso8601String(); + final response = await _httpClient.get("/wp-json/wp/v2/posts?_embed&_fields=id,yoast_head_json&per_page=10&before=$hasta&after=$desde", options: buildCacheOptions(Duration(days: 14), forceRefresh: forceRefresh, subKey: "/noticias")); + if (response.statusCode != 200) { + return null; + } + + return Noticia.fromJsonList(response.data as List); + } + +} \ No newline at end of file diff --git a/lib/repositories/implementations/permiso_ingreso_repository.dart b/lib/repositories/permiso_ingreso_repository.dart similarity index 83% rename from lib/repositories/implementations/permiso_ingreso_repository.dart rename to lib/repositories/permiso_ingreso_repository.dart index a69f130..c1f0bb6 100644 --- a/lib/repositories/implementations/permiso_ingreso_repository.dart +++ b/lib/repositories/permiso_ingreso_repository.dart @@ -1,20 +1,17 @@ import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; -import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; import 'package:mi_utem/utils/http/http_client.dart'; -class PermisoIngresoRepositoryImplementation extends PermisoIngresoRepository { +class PermisoIngresoRepository { final _authClient = HttpClient.authClient; - @override Future getDetallesPermiso(String id, { bool forceRefresh = false }) async { final response = await _authClient.post("$apiUrl/v1/permisos/$id", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "/permisos/$id")); return PermisoIngreso.fromJson(response.data as Map?); } - @override Future> getPermisos({ bool forceRefresh = false }) async { final response = await _authClient.post("$apiUrl/v1/permisos", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "/permisos")); return PermisoIngreso.fromJsonList(response.data as List?); diff --git a/lib/repositories/implementations/preferences_repository.dart b/lib/repositories/preferences_repository.dart similarity index 76% rename from lib/repositories/implementations/preferences_repository.dart rename to lib/repositories/preferences_repository.dart index 557751f..7dcd648 100644 --- a/lib/repositories/implementations/preferences_repository.dart +++ b/lib/repositories/preferences_repository.dart @@ -1,41 +1,39 @@ +import 'package:get_storage/get_storage.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/config/secure_storage.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; -class PreferencesRepositoryImplementation extends PreferencesRepository { +class PreferencesRepository { static const aliasKey = 'apodo'; static const lastLoginKey = 'ultimo_login'; static const onboardingStepKey = 'paso_onboarding'; + static GetStorage _storage = GetStorage('MiUTEM_Preferences'); - @override Future hasAlias() async => await secureStorage.containsKey(key: aliasKey); - @override Future setAlias(String? alias) async => await secureStorage.write(key: aliasKey, value: alias); - @override Future getAlias() async => await secureStorage.read(key: aliasKey); - @override Future hasLastLogin() async => await secureStorage.containsKey(key: lastLoginKey); - @override Future setLastLogin(DateTime? lastLogin) async => await secureStorage.write(key: lastLoginKey, value: lastLogin?.toString()); - @override Future getLastLogin() async { final lastLogin = await secureStorage.read(key: lastLoginKey); logger.d("Last login $lastLogin"); return lastLogin != null ? DateTime.tryParse(lastLogin) : null; } - @override Future hasCompletedOnboarding() async => await secureStorage.containsKey(key: onboardingStepKey) && await getOnboardingStep() == 'complete'; - @override Future setOnboardingStep(String? step) async => await secureStorage.write(key: onboardingStepKey, value: step); - @override Future getOnboardingStep() async => await secureStorage.read(key: onboardingStepKey); + Future hasProfilePicture() async => _storage.hasData("profile_photo"); + + Future setProfilePicture(String? photo) async => await _storage.write("profile_photo", photo); + + Future getProfilePicture() async => _storage.read("profile_photo"); + } \ No newline at end of file diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index f03cacf..29c42d6 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; +import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; diff --git a/lib/screens/asignatura/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/asignatura_estudiantes_tab.dart index 12cece0..b55c989 100644 --- a/lib/screens/asignatura/asignatura_estudiantes_tab.dart +++ b/lib/screens/asignatura/asignatura_estudiantes_tab.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/asignatura/modals/user_modal.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; @@ -35,10 +35,8 @@ class _AsignaturaEstudiantesTabState extends State { ), body: PullToRefresh( onRefresh: () async => setState(() => {}), - child: FutureBuilder?>( - future: () async { - return (await _asignaturasRepository.getDetalleAsignatura(widget.asignatura))?.estudiantes; - }(), + child: FutureBuilder( + future: _asignaturasRepository.getDetalleAsignatura(widget.asignatura), builder: (ctx, snapshot) { if(snapshot.connectionState == ConnectionState.waiting) { return Padding( @@ -56,7 +54,7 @@ class _AsignaturaEstudiantesTabState extends State { ); } - List? estudiantes = snapshot.data; + List? estudiantes = snapshot.data?.estudiantes; if(snapshot.hasError || !snapshot.hasData || estudiantes == null) { return Center( child: SingleChildScrollView( @@ -80,13 +78,13 @@ class _AsignaturaEstudiantesTabState extends State { title: Text(estudiantes[i].nombreCompletoCapitalizado), subtitle: Text(estudiantes[i].correoUtem ?? ''), onTap: () { - showModalBottomSheet(context: context, builder: (ctx) => UserModal( - user: estudiantes[i], - )); AnalyticsService.logEvent('asignatura_estudiante_tap', parameters: { 'asignatura': widget.asignatura?.codigo, 'estudiante': estudiantes[i].correoUtem, }); + showModalBottomSheet(context: context, builder: (ctx) => UserModal( + user: estudiantes[i], + )); }, ), ); diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index a1cbda7..7dab0bf 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -3,9 +3,9 @@ import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; +import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/widgets/asignatura/lista/lista_asignaturas.dart'; import 'package:mi_utem/widgets/asignatura/lista/sin_asignaturas_mensaje.dart'; diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index 0faea1d..57d1578 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/widgets/calculadora_notas/display_notas_widget.dart'; import 'package:mi_utem/widgets/calculadora_notas/editar_notas_widget.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index f954dba..c901835 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -6,8 +6,8 @@ import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; +import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/credencial/credencial_card.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index cfb92db..11fbb6e 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; diff --git a/lib/screens/horario/widgets/horario_days_header.dart b/lib/screens/horario/widgets/horario_days_header.dart index 6deb8e9..5dfa255 100644 --- a/lib/screens/horario/widgets/horario_days_header.dart +++ b/lib/screens/horario/widgets/horario_days_header.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/horario/bloque_dias_card.dart'; diff --git a/lib/screens/horario/widgets/horario_indicator.dart b/lib/screens/horario/widgets/horario_indicator.dart index 17bc0e9..a5ec740 100644 --- a/lib/screens/horario/widgets/horario_indicator.dart +++ b/lib/screens/horario/widgets/horario_indicator.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/horario/ticker_time_text.dart'; diff --git a/lib/screens/horario/widgets/horario_main_scroller.dart b/lib/screens/horario/widgets/horario_main_scroller.dart index 7d0d201..cb2ae84 100644 --- a/lib/screens/horario/widgets/horario_main_scroller.dart +++ b/lib/screens/horario/widgets/horario_main_scroller.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/screens/horario/widgets/horario_blocks_content.dart'; import 'package:mi_utem/screens/horario/widgets/horario_corner.dart'; diff --git a/lib/screens/horario/widgets/horario_periods_header.dart b/lib/screens/horario/widgets/horario_periods_header.dart index b7487ae..f9050c7 100644 --- a/lib/screens/horario/widgets/horario_periods_header.dart +++ b/lib/screens/horario/widgets/horario_periods_header.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/horario/bloque_periodo_card.dart'; diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index a253020..86efa94 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -8,11 +8,11 @@ import "package:flutter_markdown/flutter_markdown.dart"; import "package:get/get.dart"; import "package:mi_utem/models/novedades/ibanner.dart"; import "package:mi_utem/models/user/user.dart"; -import "package:mi_utem/repositories/interfaces/noticias_repository.dart"; -import "package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart"; -import "package:mi_utem/repositories/interfaces/preferences_repository.dart"; -import "package:mi_utem/services/interfaces/auth_service.dart"; -import "package:mi_utem/services/interfaces/grades_service.dart"; +import "package:mi_utem/repositories/noticias_repository.dart"; +import "package:mi_utem/repositories/permiso_ingreso_repository.dart"; +import "package:mi_utem/repositories/preferences_repository.dart"; +import "package:mi_utem/services/auth_service.dart"; +import "package:mi_utem/services/grades_service.dart"; import "package:mi_utem/services/remote_config/remote_config.dart"; import "package:mi_utem/services/review_service.dart"; import "package:mi_utem/widgets/custom_app_bar.dart"; @@ -36,7 +36,6 @@ class _MainScreenState extends State { List _banners = const []; User? _user; - String? _alias; final _authService = Get.find(); final _preferencesRepository = Get.find(); @@ -59,7 +58,6 @@ class _MainScreenState extends State { loadData(); - _preferencesRepository.getAlias().then((alias) => setState(() => _alias = alias)); _authService.getUser().then((user) => setState(() => _user = user)); } @@ -72,7 +70,7 @@ class _MainScreenState extends State { String get _greetingText { List texts = jsonDecode(RemoteConfigService.greetings); - return texts[Random().nextInt(texts.length)].replaceAll("%name", _alias ?? _user?.primerNombre ?? "N/N"); + return texts[Random().nextInt(texts.length)]; } @override @@ -97,10 +95,14 @@ class _MainScreenState extends State { Container( padding: EdgeInsets.symmetric(horizontal: 20), width: double.infinity, - child: MarkdownBody( - data: _greetingText, - styleSheet: MarkdownStyleSheet( - p: Theme.of(context).textTheme.displayMedium!.copyWith(fontWeight: FontWeight.normal), + child: FutureBuilder( + future: _preferencesRepository.getAlias(), + initialData: _user?.primerNombre ?? "N/N", + builder: (ctx, snapshot) => MarkdownBody( + data: _greetingText.replaceAll("%name", snapshot.data ?? "N/N"), + styleSheet: MarkdownStyleSheet( + p: Theme.of(context).textTheme.displayMedium!.copyWith(fontWeight: FontWeight.normal), + ), ), ), ), diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart index adc6aa0..158a0a2 100644 --- a/lib/screens/onboarding/notifications_screen.dart +++ b/lib/screens/onboarding/notifications_screen.dart @@ -1,7 +1,7 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/themes/theme.dart'; diff --git a/lib/screens/onboarding/set_alias_screen.dart b/lib/screens/onboarding/set_alias_screen.dart index ea57c29..01ec582 100644 --- a/lib/screens/onboarding/set_alias_screen.dart +++ b/lib/screens/onboarding/set_alias_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/notifications_screen.dart'; import 'package:mi_utem/themes/theme.dart'; diff --git a/lib/screens/onboarding/welcome_screen.dart b/lib/screens/onboarding/welcome_screen.dart index 6a826a2..b8f28e1 100644 --- a/lib/screens/onboarding/welcome_screen.dart +++ b/lib/screens/onboarding/welcome_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/set_alias_screen.dart'; import 'package:mi_utem/themes/theme.dart'; diff --git a/lib/screens/perfil/perfil_screen.dart b/lib/screens/perfil/perfil_screen.dart new file mode 100644 index 0000000..96c5d32 --- /dev/null +++ b/lib/screens/perfil/perfil_screen.dart @@ -0,0 +1,173 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; +import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/widgets/custom_app_bar.dart'; +import 'package:mi_utem/widgets/custom_error_widget.dart'; +import 'package:mi_utem/widgets/image/image_view_screen.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; +import 'package:mi_utem/widgets/profile_photo.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PerfilScreen extends StatefulWidget { + + const PerfilScreen({ + super.key, + }); + + @override + State createState() => _PerfilScreenState(); +} + +class _PerfilScreenState extends State { + + String? alias; + + @override + void initState() { + Get.find().getAlias().then((alias) => setState(() => this.alias = alias)); + super.initState(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: CustomAppBar( + title: Text("Perfil"), + ), + body: FutureBuilder( + future: Get.find().getUser(), + builder: (ctx, snapshot) { + if(snapshot.connectionState == ConnectionState.waiting) { + return LoadingIndicator.centeredDefault(); + } + + final user = snapshot.data; + if(snapshot.hasError || !snapshot.hasData || user == null) { + return Center( + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: CustomErrorWidget( + emoji: "\u{1F622}", + title: (snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al obtener los estudiantes"), + ), + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Stack( + children: [ + Container( + alignment: Alignment.topCenter, + margin: const EdgeInsets.only(top: 80), + child: Card( + margin: const EdgeInsets.all(20), + child: ListView( + padding: const EdgeInsets.only(bottom: 10, top: 20), + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + ListTile( + title: Text("Nombre", + style: TextStyle(color: Colors.grey), + ), + subtitle: Text("${user.nombreCompletoCapitalizado}", + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + ), + if(alias != null) Divider(height: 1), + if(alias != null) ListTile( + title: Text("Apodo", + style: TextStyle(color: Colors.grey), + ), + subtitle: Text("$alias", + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + ), + Divider(height: 1), + ListTile( + title: Text("RUT", + style: TextStyle(color: Colors.grey), + ), + subtitle: Text("${user.rut}", + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + onLongPress: () async { + await FlutterClipboard.copy(user.rut.toString()); + showTextSnackbar(context, title: "¡Copiado!", message: "Rut copiado al portapapeles"); + }, + ), + if(user.correoUtem != null) Divider(height: 1), + if(user.correoUtem != null) ListTile( + title: Text("Correo Institucional", + style: TextStyle(color: Colors.grey), + ), + subtitle: Text("${user.correoUtem}", + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + onTap: () => launchUrl(Uri.parse("mailto:${user.correoUtem}")), + onLongPress: () async { + await FlutterClipboard.copy(user.correoUtem!); + showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); + }, + ), + if(user.correoPersonal != null) Divider(height: 1), + if(user.correoPersonal != null) ListTile( + title: Text("Correo Personal", + style: TextStyle(color: Colors.grey), + ), + subtitle: Text("${user.correoPersonal}", + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + onTap: () => launchUrl(Uri.parse("mailto:${user.correoPersonal}")), + onLongPress: () async { + await FlutterClipboard.copy(user.correoPersonal!); + showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); + }, + ), + ], + ), + ), + ), + Center( + child: ProfilePhoto( + user: user, + radius: 60, + editable: false, + onImage: (imagenBase64) async { + // Actualizar foto de perfil en miutem + showTextSnackbar(context, title: "¡Listo!", message: "Guardamos tu foto de perfil"); + setState(() {}); + }, + onImageTap: (context, imageProvider) => Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen( + imageProvider: imageProvider, + ))), + ), + ), + ], + ), + ); + }, + ), + ); +} diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index cb48bed..91d66cd 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; -import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading/loading_indicator.dart'; diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 020ff78..a2e60c7 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -6,12 +6,12 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/widgets/loading/loading_dialog.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:package_info_plus/package_info_plus.dart'; diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index 4f41484..ec5ebdc 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -5,9 +5,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; +import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/services/docentes_service.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; diff --git a/lib/service_manager.dart b/lib/service_manager.dart index 1ee8f79..62f7c0a 100644 --- a/lib/service_manager.dart +++ b/lib/service_manager.dart @@ -1,53 +1,38 @@ import 'package:get/get.dart'; -import 'package:mi_utem/controllers/implementations/calculator_controller.dart'; -import 'package:mi_utem/controllers/implementations/horario_controller.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; -import 'package:mi_utem/repositories/implementations/asignaturas_repository.dart'; -import 'package:mi_utem/repositories/implementations/auth_repository.dart'; -import 'package:mi_utem/repositories/implementations/carreras_repository.dart'; -import 'package:mi_utem/repositories/implementations/credentials_repository.dart'; -import 'package:mi_utem/repositories/implementations/grades_repository.dart'; -import 'package:mi_utem/repositories/implementations/horario_repository.dart'; -import 'package:mi_utem/repositories/implementations/noticias_repository.dart'; -import 'package:mi_utem/repositories/implementations/permiso_ingreso_repository.dart'; -import 'package:mi_utem/repositories/implementations/preferences_repository.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; -import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; -import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; -import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; -import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; -import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; -import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; -import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; -import 'package:mi_utem/services/implementations/auth_service.dart'; -import 'package:mi_utem/services/implementations/carreras_service.dart'; -import 'package:mi_utem/services/implementations/grades_service.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; -import 'package:mi_utem/services/interfaces/grades_service.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/controllers/horario_controller.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/auth_repository.dart'; +import 'package:mi_utem/repositories/carreras_repository.dart'; +import 'package:mi_utem/repositories/credentials_repository.dart'; +import 'package:mi_utem/repositories/grades_repository.dart'; +import 'package:mi_utem/repositories/horario_repository.dart'; +import 'package:mi_utem/repositories/noticias_repository.dart'; +import 'package:mi_utem/repositories/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; +import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/services/carreras_service.dart'; +import 'package:mi_utem/services/grades_service.dart'; Future registerServices() async { /* Repositorios (Para conectarse a la REST Api o servicios locales) */ - Get.lazyPut(() => AuthRepositoryImplementation()); - Get.lazyPut(() => AsignaturasRepositoryImplementation()); - Get.lazyPut(() => CredentialsRepositoryImplementation()); - Get.lazyPut(() => CarrerasRepositoryImplementation()); - Get.lazyPut(() => GradesRepositoryImplementation()); - Get.lazyPut(() => PermisoIngresoRepositoryImplementation()); - Get.lazyPut(() => NoticiasRepositoryImplementation()); - Get.lazyPut(() => HorarioRepositoryImplementation()); - - Get.lazyPut(() => PreferencesRepositoryImplementation()); + Get.lazyPut(() => AuthRepository()); + Get.lazyPut(() => AsignaturasRepository()); + Get.lazyPut(() => CredentialsRepository()); + Get.lazyPut(() => CarrerasRepository()); + Get.lazyPut(() => GradesRepository()); + Get.lazyPut(() => PermisoIngresoRepository()); + Get.lazyPut(() => NoticiasRepository()); + Get.lazyPut(() => HorarioRepository()); + Get.lazyPut(() => PreferencesRepository()); /* Servicios (Para procesar datos REST) */ - Get.lazyPut(() => AuthServiceImplementation()); - Get.lazyPut(() => CarrerasServiceImplementation()); - Get.lazyPut(() => GradesServiceImplementation()); + Get.lazyPut(() => AuthService()); + Get.lazyPut(() => CarrerasService()); + Get.lazyPut(() => GradesService()); /* Controladores (Para procesar datos de interfaz) */ - Get.lazyPut(() => HorarioControllerImplementation()); - Get.lazyPut(() => CalculatorControllerImplementation()); + Get.lazyPut(() => HorarioController()); + Get.lazyPut(() => CalculatorController()); } \ No newline at end of file diff --git a/lib/services/implementations/auth_service.dart b/lib/services/auth_service.dart similarity index 94% rename from lib/services/implementations/auth_service.dart rename to lib/services/auth_service.dart index 632f5b0..eb7443f 100644 --- a/lib/services/implementations/auth_service.dart +++ b/lib/services/auth_service.dart @@ -7,15 +7,14 @@ import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/repositories/interfaces/auth_repository.dart'; -import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/repositories/auth_repository.dart'; +import 'package:mi_utem/repositories/credentials_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/utils/http/http_client.dart'; -class AuthServiceImplementation implements AuthService { +class AuthService { PreferencesRepository _preferencesRepository = Get.find(); AuthRepository _authRepository = Get.find(); diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 452a590..949f1dc 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -2,13 +2,13 @@ import 'package:background_fetch/background_fetch.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; -import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; -import 'package:mi_utem/repositories/interfaces/horario_repository.dart'; -import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; -import 'package:mi_utem/services/interfaces/grades_service.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/carreras_repository.dart'; +import 'package:mi_utem/repositories/horario_repository.dart'; +import 'package:mi_utem/repositories/permiso_ingreso_repository.dart'; +import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/services/carreras_service.dart'; +import 'package:mi_utem/services/grades_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; final _backgroundFetchConfig = BackgroundFetchConfig( diff --git a/lib/services/implementations/carreras_service.dart b/lib/services/carreras_service.dart similarity index 84% rename from lib/services/implementations/carreras_service.dart rename to lib/services/carreras_service.dart index 13c0a5b..c43194f 100644 --- a/lib/services/implementations/carreras_service.dart +++ b/lib/services/carreras_service.dart @@ -2,21 +2,17 @@ import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/repositories/interfaces/carreras_repository.dart'; +import 'package:mi_utem/repositories/carreras_repository.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; -class CarrerasServiceImplementation implements CarrerasService { +class CarrerasService { final _carrerasRepository = Get.find(); - @override List carreras = []; - @override Carrera? selectedCarrera; - @override Future getCarreras({ bool forceRefresh = false }) async { logger.d("[CarrerasService#getCarreras]: Obteniendo carreras..."); final _carreras = await _carrerasRepository.getCarreras(forceRefresh: forceRefresh); @@ -26,10 +22,8 @@ class CarrerasServiceImplementation implements CarrerasService { autoSelectCarreraActiva(); } - @override void changeSelectedCarrera(Carrera carrera) => selectedCarrera = carrera; - @override void autoSelectCarreraActiva() { logger.d("[CarrerasService#autoSelectCarreraActiva]: Seleccionando carrera activa... ${carreras.map((e) => e.toJson()).toList()}"); final estados = ["Regular", "Causal de Eliminacion"] diff --git a/lib/services/implementations/grades_service.dart b/lib/services/grades_service.dart similarity index 94% rename from lib/services/implementations/grades_service.dart rename to lib/services/grades_service.dart index 4c956f2..55d58e5 100644 --- a/lib/services/implementations/grades_service.dart +++ b/lib/services/grades_service.dart @@ -4,20 +4,18 @@ import 'package:get/get.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; -import 'package:mi_utem/repositories/interfaces/grades_repository.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; -import 'package:mi_utem/services/interfaces/grades_service.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/grades_repository.dart'; +import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -class GradesServiceImplementation implements GradesService { +class GradesService { static const savedGradesPrefix = 'savedGrades_'; static const subscribedAsignaturasPrefix = 'subscribedAsignaturas_'; GradesRepository _gradesRepository = Get.find(); - @override Future getGrades(String carreraId, String asignaturaId, {bool forceRefresh = false, bool saveGrades = true}) async { final grades = await _gradesRepository.getGrades(carreraId: carreraId, asignaturaId: asignaturaId); @@ -28,14 +26,12 @@ class GradesServiceImplementation implements GradesService { return grades; } - @override Future saveGrades(String asignaturaId, Grades grades) { final jsonGrades = grades.toJson(); jsonGrades['lastUpdate'] = DateTime.now().toIso8601String(); return secureStorage.write(key: '$savedGradesPrefix$asignaturaId', value: jsonEncode(jsonGrades)); } - @override Future> lookForGradeUpdates() async { final isLoggedIn = await Get.find().isLoggedIn(); @@ -87,7 +83,6 @@ class GradesServiceImplementation implements GradesService { return response; } - @override Future compareGrades(String asignaturaId, Grades grades) async { final prevGradesJson = await secureStorage.read(key: '$savedGradesPrefix$asignaturaId'); if(prevGradesJson == null) { @@ -227,4 +222,14 @@ class GradesServiceImplementation implements GradesService { } } +} + +enum GradeChangeType { + weightingsSet, + weightingsUpdated, + weightingsDeleted, + gradeSet, + gradeUpdated, + gradeDeleted, + noChange } \ No newline at end of file diff --git a/lib/services/interfaces/auth_service.dart b/lib/services/interfaces/auth_service.dart deleted file mode 100644 index 0851a2f..0000000 --- a/lib/services/interfaces/auth_service.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:mi_utem/models/user/user.dart'; - -abstract class AuthService { - - Future isFirstTime(); - - Future isLoggedIn({ bool forceRefresh = false }); - - Future login(); - - Future logout({BuildContext? context}); - - Future getUser(); - - Future setUser(User? user); - - Future updateProfilePicture(String image); - - Future saveFCMToken(); - - Future deleteFCMToken(); -} \ No newline at end of file diff --git a/lib/services/interfaces/carreras_service.dart b/lib/services/interfaces/carreras_service.dart deleted file mode 100644 index 35101e1..0000000 --- a/lib/services/interfaces/carreras_service.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:mi_utem/models/carrera.dart'; - -abstract class CarrerasService { - - abstract List carreras; - abstract Carrera? selectedCarrera; - - Future getCarreras({ bool forceRefresh = false }); - - void changeSelectedCarrera(Carrera carrera); - - void autoSelectCarreraActiva(); - -} \ No newline at end of file diff --git a/lib/services/interfaces/grades_service.dart b/lib/services/interfaces/grades_service.dart deleted file mode 100644 index 2ccd629..0000000 --- a/lib/services/interfaces/grades_service.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:mi_utem/models/evaluacion/grades.dart'; - -abstract class GradesService { - - Future getGrades(String carreraId, String asignaturaId, {bool forceRefresh = false, bool saveGrades = true}); - - Future saveGrades(String asignaturaId, Grades grades); - - Future compareGrades(String asignaturaId, Grades grades); - - Future> lookForGradeUpdates(); -} - -enum GradeChangeType { - weightingsSet, - weightingsUpdated, - weightingsDeleted, - gradeSet, - gradeUpdated, - gradeDeleted, - noChange -} \ No newline at end of file diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index c460ff1..5e8b229 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -22,7 +22,7 @@ class HttpClient { static Dio httpClient = Dio(BaseOptions(baseUrl: url))..interceptors.addAll([ HeadersInterceptor(), _errorInterceptor, - _logInterceptor, + logInterceptor, _dioCacheManager.interceptor, ]); @@ -43,7 +43,7 @@ class HttpClient { }, ); - static InterceptorsWrapper _logInterceptor = InterceptorsWrapper( + static InterceptorsWrapper logInterceptor = InterceptorsWrapper( onRequest: (RequestOptions options, RequestInterceptorHandler handler) { logger.d("[HttpClient]: ${options.method.toUpperCase()} ${options.uri}"); options.extra["request_created_at"] = DateTime.now().toIso8601String(); diff --git a/lib/utils/http/interceptors/auth_interceptor.dart b/lib/utils/http/interceptors/auth_interceptor.dart index 3f4cc98..164acc4 100644 --- a/lib/utils/http/interceptors/auth_interceptor.dart +++ b/lib/utils/http/interceptors/auth_interceptor.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/utils/http/http_client.dart'; class AuthInterceptor extends QueuedInterceptor { diff --git a/lib/widgets/calculadora_notas/editar_notas_widget.dart b/lib/widgets/calculadora_notas/editar_notas_widget.dart index f8d82aa..7be86c1 100644 --- a/lib/widgets/calculadora_notas/editar_notas_widget.dart +++ b/lib/widgets/calculadora_notas/editar_notas_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/calculadora_notas/modo_simulacion_widget.dart'; diff --git a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart index a67440c..9a5f6b6 100644 --- a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; class NotaExamenDisplayWidget extends StatelessWidget { diff --git a/lib/widgets/calculadora_notas/nota_final_display_widget.dart b/lib/widgets/calculadora_notas/nota_final_display_widget.dart index 76d386e..705beae 100644 --- a/lib/widgets/calculadora_notas/nota_final_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_final_display_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; class NotaFinalDisplayWidget extends StatelessWidget { diff --git a/lib/widgets/calculadora_notas/nota_list_item.dart b/lib/widgets/calculadora_notas/nota_list_item.dart index f5e3599..0dea08d 100644 --- a/lib/widgets/calculadora_notas/nota_list_item.dart +++ b/lib/widgets/calculadora_notas/nota_list_item.dart @@ -2,7 +2,7 @@ import 'package:extended_masked_text/extended_masked_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/themes/theme.dart'; diff --git a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart index f654efb..9d47af1 100644 --- a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; class NotaPresentacionDisplayWidget extends StatelessWidget { diff --git a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart index ca56eac..0345f1e 100644 --- a/lib/widgets/calculadora_notas/notas_calculadora_widget.dart +++ b/lib/widgets/calculadora_notas/notas_calculadora_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/calculator_controller.dart'; +import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/widgets/calculadora_notas/nota_list_item.dart'; diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index e8151a3..b3c408a 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -6,13 +6,13 @@ import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; import 'package:mi_utem/screens/credencial_screen.dart'; import 'package:mi_utem/screens/horario/horario_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; -import 'package:mi_utem/screens/usuario_screen.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/screens/perfil/perfil_screen.dart'; +import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/themes/theme.dart'; @@ -28,7 +28,7 @@ class CustomDrawer extends StatelessWidget { Widget? _getRoute(String? name) { switch (name) { case "Perfil": - return UsuarioScreen(); + return PerfilScreen(); case "Asignaturas": return AsignaturasListaScreen(); case "Horario": diff --git a/lib/widgets/horario/bloque_clase.dart b/lib/widgets/horario/bloque_clase.dart index dc36ffa..0f426d9 100644 --- a/lib/widgets/horario/bloque_clase.dart +++ b/lib/widgets/horario/bloque_clase.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/controllers/interfaces/horario_controller.dart'; +import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/themes/theme.dart'; diff --git a/lib/widgets/horario/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart index 356b599..03aec89 100644 --- a/lib/widgets/horario/bloque_ramo_card.dart +++ b/lib/widgets/horario/bloque_ramo_card.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/repositories/interfaces/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/interfaces/carreras_service.dart'; +import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/widgets/horario/bloque_clase.dart'; import 'package:mi_utem/widgets/horario/bloque_vacio.dart'; import 'package:mi_utem/widgets/horario/modals/asignatura_vista_previa_modal.dart'; diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index 09caf6b..89c15ad 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -3,12 +3,12 @@ import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/user/credential.dart'; -import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; -import 'package:mi_utem/repositories/interfaces/preferences_repository.dart'; +import 'package:mi_utem/repositories/credentials_repository.dart'; +import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; -import 'package:mi_utem/services/interfaces/auth_service.dart'; +import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/widgets/acerca/dialog/acerca_dialog.dart'; import 'package:mi_utem/widgets/dialogs/monkey_error_dialog.dart'; import 'package:mi_utem/widgets/loading/loading_dialog.dart'; diff --git a/lib/widgets/login_screen/login_form.dart b/lib/widgets/login_screen/login_form.dart index d9f15b7..b352e3f 100644 --- a/lib/widgets/login_screen/login_form.dart +++ b/lib/widgets/login_screen/login_form.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/repositories/interfaces/credentials_repository.dart'; +import 'package:mi_utem/repositories/credentials_repository.dart'; import 'package:mi_utem/services/update_service.dart'; import 'package:mi_utem/widgets/login_screen/creditos_app.dart'; import 'package:mi_utem/widgets/login_screen/formulario_credenciales.dart'; diff --git a/lib/widgets/main_screen/permisos/permisos_section.dart b/lib/widgets/main_screen/permisos/permisos_section.dart index 696c68e..82734f9 100644 --- a/lib/widgets/main_screen/permisos/permisos_section.dart +++ b/lib/widgets/main_screen/permisos/permisos_section.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; -import 'package:mi_utem/repositories/interfaces/permiso_ingreso_repository.dart'; +import 'package:mi_utem/repositories/permiso_ingreso_repository.dart'; import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/main_screen/permisos/permiso_card.dart'; diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index 8b3c74a..c733f82 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -4,7 +4,7 @@ import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/noticia.dart'; -import 'package:mi_utem/repositories/interfaces/noticias_repository.dart'; +import 'package:mi_utem/repositories/noticias_repository.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/noticias/noticia_card_widget.dart'; diff --git a/lib/widgets/profile_photo.dart b/lib/widgets/profile_photo.dart index 4e1cc7e..37e5ecc 100644 --- a/lib/widgets/profile_photo.dart +++ b/lib/widgets/profile_photo.dart @@ -46,35 +46,40 @@ class _ProfilePhotoState extends State { height: widget.radius * 2, child: Stack( children: [ - CircularProfileAvatar(widget.user!.fotoUrl ?? "", - onTap: () => widget.onTap != null && widget.onImageTap == null ? widget.onTap : null, - borderColor: widget.borderColor, - borderWidth: widget.borderWidth, - radius: widget.radius, - backgroundColor: MainTheme.primaryColor, - imageBuilder: (context, imageProvider) => GestureDetector( - onTap: () { - if (widget.onImageTap != null) { - widget.onImageTap!(context, imageProvider); - } - }, - child: Container( - height: double.infinity, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - image: DecorationImage( - image: imageProvider, - fit: BoxFit.cover, + FutureBuilder( + // future: Get.find().getProfilePicture(), + initialData: null, + builder: (ctx, snapshot) => CircularProfileAvatar(snapshot.data != null ? '' : (widget.user!.fotoUrl ?? ""), + child: snapshot.data != null ? Image.memory(Base64Decoder().convert(snapshot.data!)) : null, + onTap: () => widget.onTap != null && widget.onImageTap == null ? widget.onTap : null, + borderColor: widget.borderColor, + borderWidth: widget.borderWidth, + radius: widget.radius, + backgroundColor: MainTheme.primaryColor, + imageBuilder: (context, imageProvider) => GestureDetector( + onTap: () { + if (widget.onImageTap != null) { + widget.onImageTap!(context, imageProvider); + } + }, + child: Container( + height: double.infinity, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), ), ), ), - ), - initialsText: Text( - widget.user!.iniciales, - style: TextStyle( - fontSize: widget.radius * 0.5, - color: Colors.white, + initialsText: Text( + widget.user!.iniciales, + style: TextStyle( + fontSize: widget.radius * 0.5, + color: Colors.white, + ), ), ), ), From 66c097ebfaa350b1d5411f83d4227f49fa62ed1d Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 15 May 2024 10:10:57 -0400 Subject: [PATCH 102/194] patch: preferencias y notas * Se elimina repositorio de preferencias a favor de la clase preferencia.dart * Se arregla que las notas no eran visibles. * Se mejora el sistema de lista de estudiantes Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/asignaturas/asignatura.dart | 2 +- lib/models/preferencia.dart | 33 +++++++++++++ lib/repositories/asignaturas_repository.dart | 46 ++++++++++++++----- lib/repositories/preferences_repository.dart | 39 ---------------- .../asignatura/asignatura_detalle_screen.dart | 3 +- .../asignatura_estudiantes_tab.dart | 13 ++++-- .../asignatura/asignaturas_lista_screen.dart | 8 +++- lib/screens/main_screen.dart | 5 +- .../onboarding/notifications_screen.dart | 12 ++--- lib/screens/onboarding/set_alias_screen.dart | 14 +++--- lib/screens/onboarding/welcome_screen.dart | 7 +-- lib/screens/perfil/perfil_screen.dart | 12 ++--- lib/screens/splash_screen.dart | 4 +- lib/screens/usuario_screen.dart | 13 +++--- lib/service_manager.dart | 2 - lib/services/auth_service.dart | 31 +++++-------- lib/services/background_service.dart | 2 +- lib/utils/utils.dart | 21 +++++++++ .../calculadora_notas/nota_list_item.dart | 4 +- lib/widgets/custom_drawer.dart | 7 ++- lib/widgets/login_screen/login_button.dart | 6 +-- 21 files changed, 155 insertions(+), 129 deletions(-) create mode 100644 lib/models/preferencia.dart delete mode 100644 lib/repositories/preferences_repository.dart create mode 100644 lib/utils/utils.dart diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart index 265e960..0574014 100644 --- a/lib/models/asignaturas/asignatura.dart +++ b/lib/models/asignaturas/asignatura.dart @@ -60,7 +60,7 @@ class Asignatura { estado: capitalize(json['estado'] ?? ''), docente: capitalize(json['docente'] ?? ''), seccion: json['seccion'], - grades: json.containsKey('notas') ? Grades.fromJson(json['notas']) : Grades(), + grades: json['notas'] != null ? Grades.fromJson(json['notas']) : Grades(), estudiantes: json.containsKey("estudiantes") ? User.fromJsonList(json["estudiantes"]) : [], asistencia: Asistencia(asistidos: json['asistenciaAlDia']), tipoAsignatura: capitalize(json['tipoAsignatura'] as String? ?? ''), diff --git a/lib/models/preferencia.dart b/lib/models/preferencia.dart new file mode 100644 index 0000000..71f9687 --- /dev/null +++ b/lib/models/preferencia.dart @@ -0,0 +1,33 @@ +import 'package:mi_utem/config/secure_storage.dart'; + +enum Preferencia { + apodo, + lastLogin, + onboardingStep, + ; + + /// Revisa si la preferencia existe + Future exists() async => await secureStorage.containsKey(key: this.name); + + /// Obtiene la preferencia del storage, pero si no existe retorna el valor por defecto + /// Si [guardar] es verdadero, entonces si no existe la preferencia, se guardará el valor por defecto + Future get({ String? defaultValue, bool guardar = false }) async { + await add(defaultValue); + return await secureStorage.read(key: this.name) ?? defaultValue; + } + + /// Guarda la preferencia en el storage + set(String? value) => () async => await secureStorage.write(key: this.name, value: value); + + /// Guarda la preferencia en el storage solo si no existe + /// Si la preferencia ya existe, no se guardará nada + add(String? value) => () async { + if (!(await exists())) { + await set(value); + } + }; + + /// Elimina la preferencia del storage + delete() => () async => await secureStorage.delete(key: this.name); + +} \ No newline at end of file diff --git a/lib/repositories/asignaturas_repository.dart b/lib/repositories/asignaturas_repository.dart index 7f15ac8..d693ba7 100644 --- a/lib/repositories/asignaturas_repository.dart +++ b/lib/repositories/asignaturas_repository.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/utils/http/http_client.dart'; class AsignaturasRepository { @@ -13,33 +14,54 @@ class AsignaturasRepository { return null; } - final _cacheOptions = buildCacheOptions(Duration(days: 7), + final path = "carreras/$carreraId/asignaturas"; + final Response response = await _authClient.get("$apiUrl/v1/$path", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, - subKey: 'carreras/$carreraId/asignaturas', - ); + subKey: path, + )); - final Response response = await _authClient.get('$apiUrl/v1/carreras/$carreraId/asignaturas', options: _cacheOptions); + final asignaturas = Asignatura.fromJsonList(response.data); + for(int i = 0; i < asignaturas.length; i++) { + final asignatura = await getNotasAsignatura(carreraId, asignaturas[i], forceRefresh: forceRefresh); + if(asignatura != null) { + asignaturas[i] = asignatura; + } + } - return Asignatura.fromJsonList(response.data); + return asignaturas; } - Future getDetalleAsignatura(Asignatura? asignatura, {bool forceRefresh = false}) async { + Future?> getEstudiantesAsignatura(Asignatura? asignatura, {bool forceRefresh = false}) async { if(asignatura == null) { return null; } - final _cacheOptions = buildCacheOptions(Duration(days: 7), + final path = "asignaturas/${asignatura.codigo}"; + final response = await _authClient.get("$apiUrl/v1/$path", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, - subKey: 'asignaturas/${asignatura.codigo}', - ); - - final response = await _authClient.get('$apiUrl/v1/asignaturas/${asignatura.codigo}', options: _cacheOptions); + subKey: path, + )); final json = response.data as Map; // Por ahora solo se actualizan los estudiantes + return User.fromJsonList(json['estudiantes']); + } + + Future getNotasAsignatura(String? carreraId, Asignatura? asignatura, { bool forceRefresh = false }) async { + if(asignatura == null || carreraId == null) { + return null; + } + + final path = "carreras/$carreraId/asignaturas/${asignatura.id}/notas"; + + final response = await _authClient.get("$apiUrl/v1/$path", options: buildCacheOptions(Duration(days: 7), + forceRefresh: forceRefresh, + subKey: path, + )); + return Asignatura.fromJson({ ...asignatura.toJson(), - 'estudiantes': json['estudiantes'], + 'notas': response.data as Map, }); } diff --git a/lib/repositories/preferences_repository.dart b/lib/repositories/preferences_repository.dart deleted file mode 100644 index 7dcd648..0000000 --- a/lib/repositories/preferences_repository.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:get_storage/get_storage.dart'; -import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/config/secure_storage.dart'; - -class PreferencesRepository { - static const aliasKey = 'apodo'; - static const lastLoginKey = 'ultimo_login'; - static const onboardingStepKey = 'paso_onboarding'; - static GetStorage _storage = GetStorage('MiUTEM_Preferences'); - - Future hasAlias() async => await secureStorage.containsKey(key: aliasKey); - - Future setAlias(String? alias) async => await secureStorage.write(key: aliasKey, value: alias); - - Future getAlias() async => await secureStorage.read(key: aliasKey); - - Future hasLastLogin() async => await secureStorage.containsKey(key: lastLoginKey); - - Future setLastLogin(DateTime? lastLogin) async => await secureStorage.write(key: lastLoginKey, value: lastLogin?.toString()); - - Future getLastLogin() async { - final lastLogin = await secureStorage.read(key: lastLoginKey); - logger.d("Last login $lastLogin"); - return lastLogin != null ? DateTime.tryParse(lastLogin) : null; - } - - Future hasCompletedOnboarding() async => await secureStorage.containsKey(key: onboardingStepKey) && await getOnboardingStep() == 'complete'; - - Future setOnboardingStep(String? step) async => await secureStorage.write(key: onboardingStepKey, value: step); - - Future getOnboardingStep() async => await secureStorage.read(key: onboardingStepKey); - - Future hasProfilePicture() async => _storage.hasData("profile_photo"); - - Future setProfilePicture(String? photo) async => await _storage.write("profile_photo", photo); - - Future getProfilePicture() async => _storage.read("profile_photo"); - -} \ No newline at end of file diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 29c42d6..9c49b7c 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -52,8 +52,7 @@ class _AsignaturaDetalleScreenState extends State { asignatura: asignatura, onRefresh: () async { final carrera = Get.find().selectedCarrera; - final asignaturaBase = (await _asignaturasRepository.getAsignaturas(carrera?.id))?.firstWhere((asignatura) => asignatura.codigo == this.asignatura.codigo && asignatura.id == this.asignatura.id); - final asignatura = await _asignaturasRepository.getDetalleAsignatura(asignaturaBase); + final asignatura = (await _asignaturasRepository.getAsignaturas(carrera?.id, forceRefresh: true))?.firstWhere((asignatura) => asignatura.codigo == this.asignatura.codigo && asignatura.id == this.asignatura.id); if (asignatura != null) { setState(() => this.asignatura = asignatura); } diff --git a/lib/screens/asignatura/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/asignatura_estudiantes_tab.dart index b55c989..8d6272d 100644 --- a/lib/screens/asignatura/asignatura_estudiantes_tab.dart +++ b/lib/screens/asignatura/asignatura_estudiantes_tab.dart @@ -26,6 +26,7 @@ class AsignaturaEstudiantesTab extends StatefulWidget { class _AsignaturaEstudiantesTabState extends State { + bool _forceRefresh = false; AsignaturasRepository _asignaturasRepository = Get.find(); @override @@ -34,9 +35,13 @@ class _AsignaturaEstudiantesTabState extends State { title: Text("Estudiantes"), ), body: PullToRefresh( - onRefresh: () async => setState(() => {}), - child: FutureBuilder( - future: _asignaturasRepository.getDetalleAsignatura(widget.asignatura), + onRefresh: () async => setState(() => _forceRefresh = true), + child: FutureBuilder?>( + future: () async { + final estudiantes = await _asignaturasRepository.getEstudiantesAsignatura(widget.asignatura, forceRefresh: _forceRefresh); + _forceRefresh = false; + return estudiantes; + }(), builder: (ctx, snapshot) { if(snapshot.connectionState == ConnectionState.waiting) { return Padding( @@ -54,7 +59,7 @@ class _AsignaturaEstudiantesTabState extends State { ); } - List? estudiantes = snapshot.data?.estudiantes; + List? estudiantes = snapshot.data; if(snapshot.hasError || !snapshot.hasData || estudiantes == null) { return Center( child: SingleChildScrollView( diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index 7dab0bf..f973239 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/repositories/asignaturas_repository.dart'; @@ -14,13 +15,17 @@ import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; class AsignaturasListaScreen extends StatefulWidget { - const AsignaturasListaScreen({super.key}); + + const AsignaturasListaScreen({ + super.key + }); @override State createState() => _AsignaturasListaScreenState(); } class _AsignaturasListaScreenState extends State { + final _asignaturasService = Get.find(); bool _forceRefresh = false; @@ -54,6 +59,7 @@ class _AsignaturasListaScreenState extends State { }(), builder: (context, snapshot) { if(snapshot.hasError) { + logger.e(snapshot.error); final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al obtener las asignaturas"; return _errorWidget(error); } diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 86efa94..e6f90d9 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -7,10 +7,10 @@ import "package:flutter/services.dart"; import "package:flutter_markdown/flutter_markdown.dart"; import "package:get/get.dart"; import "package:mi_utem/models/novedades/ibanner.dart"; +import "package:mi_utem/models/preferencia.dart"; import "package:mi_utem/models/user/user.dart"; import "package:mi_utem/repositories/noticias_repository.dart"; import "package:mi_utem/repositories/permiso_ingreso_repository.dart"; -import "package:mi_utem/repositories/preferences_repository.dart"; import "package:mi_utem/services/auth_service.dart"; import "package:mi_utem/services/grades_service.dart"; import "package:mi_utem/services/remote_config/remote_config.dart"; @@ -37,7 +37,6 @@ class _MainScreenState extends State { List _banners = const []; User? _user; final _authService = Get.find(); - final _preferencesRepository = Get.find(); @override void initState() { @@ -96,7 +95,7 @@ class _MainScreenState extends State { padding: EdgeInsets.symmetric(horizontal: 20), width: double.infinity, child: FutureBuilder( - future: _preferencesRepository.getAlias(), + future: Preferencia.apodo.get(defaultValue: "N/N"), initialData: _user?.primerNombre ?? "N/N", builder: (ctx, snapshot) => MarkdownBody( data: _greetingText.replaceAll("%name", snapshot.data ?? "N/N"), diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart index 158a0a2..c496455 100644 --- a/lib/screens/onboarding/notifications_screen.dart +++ b/lib/screens/onboarding/notifications_screen.dart @@ -1,7 +1,6 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/themes/theme.dart'; @@ -16,12 +15,11 @@ class NotificationsScreen extends StatefulWidget { class _NotificationsScreenState extends State { - PreferencesRepository _preferencesRepository = Get.find(); bool _hasAllowedNotifications = false; @override void initState() { - _preferencesRepository.setOnboardingStep("notifications"); + Preferencia.onboardingStep.set("notifications"); NotificationService.hasAllowedNotifications().then((value) { if(value) { setState(() => _hasAllowedNotifications = value); @@ -94,14 +92,14 @@ class _NotificationsScreenState extends State { if(!context.mounted) { return; } - await _preferencesRepository.setOnboardingStep("complete"); + await Preferencia.onboardingStep.set("complete"); Navigator.popUntil(context, (route) => route.isFirst); - final alias = await _preferencesRepository.getAlias(); + final alias = await Preferencia.apodo.get(); AwesomeNotifications().createNotification(content: NotificationContent( id: 1, channelKey: NotificationService.announcementsChannelKey, actionType: ActionType.Default, - title: '¡Hola $alias! 🎉', + title: alias != null ? "¡Hola $alias! 🎉" : "¡Hola! 🎉", body: '¡Te damos la bienvenida a la aplicación Mi UTEM! 🚀', )); Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); diff --git a/lib/screens/onboarding/set_alias_screen.dart b/lib/screens/onboarding/set_alias_screen.dart index 01ec582..3a96194 100644 --- a/lib/screens/onboarding/set_alias_screen.dart +++ b/lib/screens/onboarding/set_alias_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/notifications_screen.dart'; import 'package:mi_utem/themes/theme.dart'; @@ -15,24 +14,22 @@ class SetAliasScreen extends StatefulWidget { class _SetAliasScreenState extends State { - PreferencesRepository _preferencesRepository = Get.find(); - final _focusNode = FocusNode(); final _formKey = GlobalKey(); final _aliasController = TextEditingController(); @override void initState() { - _preferencesRepository.getOnboardingStep().then((step) { + Preferencia.onboardingStep.get().then((step) { if(step == null) { - _preferencesRepository.setOnboardingStep("alias"); + Preferencia.onboardingStep.set("alias"); } else if (step == 'complete') { Navigator.push(context, MaterialPageRoute(builder: (ctx) => const MainScreen())); } else { Navigator.push(context, MaterialPageRoute(builder: (ctx) => const NotificationsScreen())); } }); - _preferencesRepository.getAlias().then((value) => _aliasController.text = value ?? ''); + Preferencia.apodo.get().then((value) => _aliasController.text = value ?? ''); super.initState(); } @@ -131,7 +128,8 @@ class _SetAliasScreenState extends State { if (_formKey.currentState?.validate() != true) { return; } - _preferencesRepository.setAlias(_aliasController.text); + + Preferencia.apodo.set(_aliasController.text); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const NotificationsScreen())); }, style: ElevatedButton.styleFrom( diff --git a/lib/screens/onboarding/welcome_screen.dart b/lib/screens/onboarding/welcome_screen.dart index b8f28e1..4f3c95e 100644 --- a/lib/screens/onboarding/welcome_screen.dart +++ b/lib/screens/onboarding/welcome_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/set_alias_screen.dart'; import 'package:mi_utem/themes/theme.dart'; @@ -15,11 +14,9 @@ class WelcomeScreen extends StatefulWidget { class _WelcomeScreenState extends State { - PreferencesRepository _preferencesRepository = Get.find(); - @override void initState() { - _preferencesRepository.getOnboardingStep().then((step) { + Preferencia.onboardingStep.get().then((step) { if(step == null) { return; } else if (step == 'complete') { diff --git a/lib/screens/perfil/perfil_screen.dart b/lib/screens/perfil/perfil_screen.dart index 96c5d32..8c9f1c4 100644 --- a/lib/screens/perfil/perfil_screen.dart +++ b/lib/screens/perfil/perfil_screen.dart @@ -2,8 +2,8 @@ import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; @@ -25,11 +25,11 @@ class PerfilScreen extends StatefulWidget { class _PerfilScreenState extends State { - String? alias; + String? apodo; @override void initState() { - Get.find().getAlias().then((alias) => setState(() => this.alias = alias)); + Preferencia.apodo.get().then((apodo) => setState(() => this.apodo = apodo)); super.initState(); } @@ -83,12 +83,12 @@ class _PerfilScreenState extends State { ), ), ), - if(alias != null) Divider(height: 1), - if(alias != null) ListTile( + if(apodo != null) Divider(height: 1), + if(apodo != null) ListTile( title: Text("Apodo", style: TextStyle(color: Colors.grey), ), - subtitle: Text("$alias", + subtitle: Text("$apodo", style: TextStyle( color: Colors.grey[900], fontSize: 18, diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index a2e60c7..efbb867 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/config/http_clients.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; @@ -104,7 +104,7 @@ class _SplashScreenState extends State { } Navigator.popUntil(context, (route) => route.isFirst); - final hasCompletedOnboarding = await Get.find().hasCompletedOnboarding(); + final hasCompletedOnboarding = (await Preferencia.onboardingStep.get()) == "complete"; Navigator.pushReplacement(context, MaterialPageRoute(builder: (ctx) => isLoggedIn ? (hasCompletedOnboarding ? MainScreen() : WelcomeScreen()) : LoginScreen())); }, ), diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index ec5ebdc..ffa0ec5 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -4,8 +4,8 @@ import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/services/docentes_service.dart'; import 'package:mi_utem/services/review_service.dart'; @@ -36,11 +36,10 @@ class UsuarioScreen extends StatefulWidget { class _UsuarioScreenState extends State { final _authService = Get.find(); - final _preferencesRepository = Get.find(); Future? _userFuture; User? _user; - String? _alias; + String? _apodo; @override void initState() { @@ -62,10 +61,10 @@ class _UsuarioScreenState extends State { setState(() => _user = user); } else { user = await _authService.getUser(); - final alias = await _preferencesRepository.getAlias(); + final apodo = await Preferencia.apodo.get(); setState(() { _user = user; - _alias = alias; + _apodo = apodo; }); } @@ -146,14 +145,14 @@ class _UsuarioScreenState extends State { } } - if(_alias != null) { + if(_apodo != null) { lista.add(Divider(height: 1)); lista.add( ListTile( title: Text("Alias", style: TextStyle(color: Colors.grey), ), - subtitle: Text(_alias!, + subtitle: Text(_apodo!, style: TextStyle( color: Colors.grey[900], fontSize: 18, diff --git a/lib/service_manager.dart b/lib/service_manager.dart index 62f7c0a..a14ab9c 100644 --- a/lib/service_manager.dart +++ b/lib/service_manager.dart @@ -9,7 +9,6 @@ import 'package:mi_utem/repositories/grades_repository.dart'; import 'package:mi_utem/repositories/horario_repository.dart'; import 'package:mi_utem/repositories/noticias_repository.dart'; import 'package:mi_utem/repositories/permiso_ingreso_repository.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/grades_service.dart'; @@ -24,7 +23,6 @@ Future registerServices() async { Get.lazyPut(() => PermisoIngresoRepository()); Get.lazyPut(() => NoticiasRepository()); Get.lazyPut(() => HorarioRepository()); - Get.lazyPut(() => PreferencesRepository()); /* Servicios (Para procesar datos REST) */ diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index eb7443f..1216f85 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -5,25 +5,23 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/config/secure_storage.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/repositories/auth_repository.dart'; import 'package:mi_utem/repositories/credentials_repository.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/utils/utils.dart'; class AuthService { - PreferencesRepository _preferencesRepository = Get.find(); AuthRepository _authRepository = Get.find(); CredentialsRepository _credentialsService = Get.find(); - @override - Future isFirstTime() async => !(await _preferencesRepository.hasLastLogin()); + Future isFirstTime() async => await Preferencia.lastLogin.exists() == false; - @override Future isLoggedIn({ bool forceRefresh = false }) async { final credentials = await _getCredential(); if(credentials == null) { @@ -38,14 +36,14 @@ class AuthService { return false; } - final hasLastLogin = await _preferencesRepository.hasLastLogin(); + final hasLastLogin = await isFirstTime(); if(!hasLastLogin) { logger.d("[AuthService#isLoggedIn]: no last login"); return false; } final now = DateTime.now(); - final lastLoginDate = await _preferencesRepository.getLastLogin() ?? now; + final lastLoginDate = let(await Preferencia.lastLogin.get(), (String _lastLogin) => DateTime.tryParse(_lastLogin)) ?? now; final difference = now.difference(lastLoginDate); if(difference.inMinutes < 5 && now != lastLoginDate && !forceRefresh) { return true; @@ -57,7 +55,7 @@ class AuthService { final userJson = user.toJson(); userJson["token"] = token; await setUser(User.fromJson(userJson)); - _preferencesRepository.setLastLogin(DateTime.now()); + Preferencia.lastLogin.set(now.toIso8601String()); return true; } catch (e) { logger.e("[AuthService#isLoggedIn]: Error al refrescar token", e); @@ -66,7 +64,6 @@ class AuthService { return false; } - @override Future login() async { final credentials = await _getCredential(); if(credentials == null) { @@ -76,24 +73,22 @@ class AuthService { final user = await _authRepository.auth(credentials: credentials); await setUser(user); - _preferencesRepository.setLastLogin(DateTime.now()); + Preferencia.lastLogin.set(DateTime.now().toIso8601String()); } - @override Future logout({BuildContext? context}) async { - await HttpClient.clearCache(); setUser(null); _credentialsService.setCredentials(null); - _preferencesRepository.setOnboardingStep(null); - _preferencesRepository.setLastLogin(null); - _preferencesRepository.setAlias(null); + Preferencia.onboardingStep.delete(); + Preferencia.lastLogin.delete(); + Preferencia.apodo.delete(); + await HttpClient.clearCache(); if(context != null) { Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (ctx) => LoginScreen())); } } - @override Future getUser() async { final data = await secureStorage.read(key: "user"); if(data == null || data == "null") { @@ -103,7 +98,6 @@ class AuthService { return User.fromJson(jsonDecode(data) as Map); } - @override Future setUser(User? user) async => await secureStorage.write(key: "user", value: user.toString()); Future _getCredential() async { @@ -116,7 +110,6 @@ class AuthService { return credential; } - @override Future updateProfilePicture(String image) async { final user = await getUser(); if(user == null) { @@ -130,7 +123,6 @@ class AuthService { return user; } - @override Future saveFCMToken() async { final user = await this.getUser(); if(user == null) { @@ -162,7 +154,6 @@ class AuthService { } } - @override Future deleteFCMToken() async { String? fcmToken; try { diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 949f1dc..47d2529 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -95,7 +95,7 @@ class BackgroundService { AsignaturasRepository asignaturasRepository = Get.find(); final asignaturas = await asignaturasRepository.getAsignaturas(carreraId, forceRefresh: true) ?? []; for(final asignatura in asignaturas) { - await asignaturasRepository.getDetalleAsignatura(asignatura, forceRefresh: true); + await asignaturasRepository.getEstudiantesAsignatura(asignatura, forceRefresh: true); } } } catch(_){} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 0000000..f701669 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,21 @@ +/// Esta función ejecuta la función `op` si es que el objeto no es nulo, además retorna lo que retorne la función op. +/// +/// Modo de uso: +/// +/// ```dart +/// num? numeroPosiblementeNulo = 5; +/// num? resultado = let(numeroPosiblementeNulo, (numero) => numero*2); +/// print(resultado); // 10 +/// ``` +/// +/// Ahora, si el objeto es nulo, la función retornará null. +/// +/// Además puedes especificar los tipos de datos para ayudar a los IDEs a inferir el tipo de dato de retorno. +/// Ejemplo: +/// +/// ```dart +/// String? fechaPosiblementeNula = "2021-10-10"; +/// DateTime? fecha = let(fechaPosiblementeNula, (fecha) => DateTime.tryParse(fecha)); +/// print(fecha); // 2021-10-10 00:00:00.000 +/// ``` +V? let(K? object, V Function(K) op) => object != null ? op(object) : null; \ No newline at end of file diff --git a/lib/widgets/calculadora_notas/nota_list_item.dart b/lib/widgets/calculadora_notas/nota_list_item.dart index 0dea08d..9a0c678 100644 --- a/lib/widgets/calculadora_notas/nota_list_item.dart +++ b/lib/widgets/calculadora_notas/nota_list_item.dart @@ -53,7 +53,7 @@ class NotaListItem extends StatelessWidget { Flexible( flex: 3, child: Center( - child: Obx(() => TextField( + child: TextField( controller: gradeController ?? defaultGradeController, enabled: editable, onChanged: (String value) { @@ -98,7 +98,7 @@ class NotaListItem extends StatelessWidget { return input; }), ], - )), + ), ), ), SizedBox(width: 16), diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index b3c408a..0d7e724 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/pair.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; import 'package:mi_utem/screens/credencial_screen.dart'; import 'package:mi_utem/screens/horario/horario_screen.dart'; @@ -52,7 +52,6 @@ class CustomDrawer extends StatelessWidget { @override Widget build(BuildContext context) { final _authService = Get.find(); - final _preferencesRepository = Get.find(); return Drawer( semanticLabel: "Abrir menú", @@ -66,8 +65,8 @@ class CustomDrawer extends StatelessWidget { child: FutureBuilder>( future: () async { final user = await _authService.getUser(); - final alias = await _preferencesRepository.getAlias(); - return Pair(alias, user); + final apodo = await Preferencia.apodo.get(); + return Pair(apodo, user); }(), builder: (context, snapshot) { final pair = snapshot.data; diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index 89c15ad..b8a2461 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/repositories/credentials_repository.dart'; -import 'package:mi_utem/repositories/preferences_repository.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; @@ -99,8 +99,8 @@ class _LoginButtonState extends State { Navigator.of(context).popUntil((route) => route.isFirst); // Esto elimina todas las pantallas anteriores // Y esto reemplaza la pantalla actual por la nueva, cosa de que no pueda "volver" al login a menos que cierre la sesión. - PreferencesRepository preferencesRepository = Get.find(); - if(await preferencesRepository.hasCompletedOnboarding()) { + final hasCompletedOnboarding = (await Preferencia.onboardingStep.get()) == 'complete'; + if(hasCompletedOnboarding) { Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); if(isFirstTime) { showDialog(context: context, builder: (ctx) => AcercaDialog()); From b482235e7633e09bb1de3244e9e645f4e332ae01 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 15 May 2024 16:56:40 -0400 Subject: [PATCH 103/194] =?UTF-8?q?patch:=20cache,=20utilidades=20y=20mejo?= =?UTF-8?q?ras=20*=20Se=20mejora=20sistema=20de=20cach=C3=A9=20(al=20usar?= =?UTF-8?q?=20dio=5Fhttp=5Fcache=20en=20lugar=20de=20dio=5Fcache=5Finterce?= =?UTF-8?q?ptor)=20*=20Se=20agrega=20el=20archivo=20functions.dart=20que?= =?UTF-8?q?=20incluye=20utilidades=20para=20realizar=20solicitudes=20web?= =?UTF-8?q?=20*=20Se=20separa=20interceptores=20de=20'dio'=20en=20distinta?= =?UTF-8?q?s=20clases=20*=20Se=20elimina=20http=5Fclients.dart=20(clase=20?= =?UTF-8?q?antigua)=20*=20Se=20mejora=20archivo=20constants.dart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/config/constants.dart | 10 +- lib/config/http_clients.dart | 99 ------------ lib/main.dart | 6 +- lib/models/preferencia.dart | 12 +- lib/repositories/asignaturas_repository.dart | 51 +++--- lib/repositories/auth_repository.dart | 9 +- lib/repositories/carreras_repository.dart | 11 +- lib/repositories/grades_repository.dart | 9 +- lib/repositories/horario_repository.dart | 10 +- lib/repositories/noticias_repository.dart | 10 +- .../permiso_ingreso_repository.dart | 23 +-- lib/screens/docentes_screen.dart | 3 +- lib/screens/splash_screen.dart | 37 ++--- lib/screens/usuario_screen.dart | 11 +- lib/service_manager.dart | 20 +++ lib/services/auth_service.dart | 7 +- lib/services/docentes_service.dart | 148 +++++++++--------- lib/utils/dio_docente_client.dart | 18 --- lib/utils/http/functions.dart | 26 +++ lib/utils/http/http_client.dart | 55 ++----- .../http/interceptors/error_interceptor.dart | 19 +++ .../interceptors/headers_interceptor.dart | 1 + .../http/interceptors/log_interceptor.dart | 20 +++ lib/widgets/snackbar.dart | 34 ++-- pubspec.lock | 24 --- pubspec.yaml | 6 +- 26 files changed, 287 insertions(+), 392 deletions(-) delete mode 100644 lib/config/http_clients.dart create mode 100644 lib/utils/http/functions.dart create mode 100644 lib/utils/http/interceptors/error_interceptor.dart create mode 100644 lib/utils/http/interceptors/log_interceptor.dart diff --git a/lib/config/constants.dart b/lib/config/constants.dart index 05a37b5..9d4204a 100644 --- a/lib/config/constants.dart +++ b/lib/config/constants.dart @@ -1,10 +1,6 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; final apiUrl = bool.fromEnvironment('dart.vm.product') ? 'https://api.exdev.cl' : (dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl'); - -class Constants { - static const String sentryDsn = 'https://c03edae5839c62f95de91c1cbabb65d7@o4506938204553216.ingest.us.sentry.io/4506938205470720'; - static const String uxCamDevKey = '0y6p88obpgiug1g'; - static const String uxCamProdKey = 'fxkjj5ulr7vb4yf'; - static String apiUrl = bool.fromEnvironment('dart.vm.product') ? 'https://api.exdev.cl' : (dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl'); -} +const String sentryDsn = 'https://c03edae5839c62f95de91c1cbabb65d7@o4506938204553216.ingest.us.sentry.io/4506938205470720'; +const String uxCamDevKey = '0y6p88obpgiug1g'; +const String uxCamProdKey = 'fxkjj5ulr7vb4yf'; diff --git a/lib/config/http_clients.dart b/lib/config/http_clients.dart deleted file mode 100644 index ff0e5b2..0000000 --- a/lib/config/http_clients.dart +++ /dev/null @@ -1,99 +0,0 @@ - -import 'package:get/get.dart'; -import 'package:http_interceptor/http_interceptor.dart'; -import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/services/auth_service.dart'; - -final httpClient = InterceptedClient.build( - interceptors: [ - LoggerInterceptor(), - ], -); -final authClient = InterceptedClient.build( - interceptors: [ - LoggerInterceptor(), - AuthInterceptor(), - ], - retryPolicy: ExpiredTokenRetryPolicy(), -); - -class AuthInterceptor implements InterceptorContract { - - @override - Future interceptRequest({required RequestData data}) async { - if(!data.headers.containsKey('user-agent')) { - data.headers['user-agent'] = "App/MiUTEM"; - } - - if(!data.headers.containsKey('content-type')) { - data.headers['content-type'] = "application/json"; - } - - if (!data.headers.containsKey('authorization') && !data.url.contains("/v1/auth/login") && !data.url.contains("/v1/auth/refresh")) { // No enviar token si es login o refresh - final user = await Get.find().getUser(); - final token = user?.token; - if (token != null) { - logger.d("[AuthInterceptor]: Agregado token de autorización al request"); - data.headers['authorization'] = 'Bearer $token'; - } - } - - return data; - } - - @override - Future interceptResponse({required ResponseData data}) async => data; -} - -class ExpiredTokenRetryPolicy extends RetryPolicy { - - @override - int get maxRetryAttempts => 6; - - @override - Future shouldAttemptRetryOnResponse(ResponseData response) async { - if(response.statusCode != 401) { - return false; - } - - logger.d("[ExpiredTokenRetryPolicy]: ${response.request?.method.name.toUpperCase()} ${response.request?.url} Recibió un 401, refrescando token..."); - final _authService = Get.find(); - final currentToken = (await _authService.getUser())?.token; - if(currentToken == null) { - await _authService.login(); - } else { - await _authService.isLoggedIn(forceRefresh: true); - } - - final token = (await _authService.getUser())?.token; - if(token == null) { - logger.d("[ExpiredTokenRetryPolicy]: No se pudo refrescar el token!"); - return false; - } - - return true; - } -} - -class LoggerInterceptor implements InterceptorContract { - - final _times = {}; - - @override - Future interceptRequest({required RequestData data}) async { - logger.d("[LoggerInterceptor#request]: ${data.method.name.toUpperCase()} ${data.url}"); - _times["${data.method}:${data.url}"] = DateTime.now(); - return data; - } - - @override - Future interceptResponse({required ResponseData data}) async { - String? diff; - final time = _times["${data.method}:${data.url}"]; - if(time != null) { - diff = " (${DateTime.now().difference(time).inMilliseconds}ms)"; - } - logger.d("[LoggerInterceptor#response]: ${data.statusCode} ${data.url}$diff"); - return data; - } -} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 42ecdde..d3021be 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,10 +19,8 @@ import 'services/remote_config/remote_config.dart'; final GlobalKey navigatorKey = GlobalKey(); - void main() async { WidgetsFlutterBinding.ensureInitialized(); - await dotenv.load(); await GetStorage.init(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); @@ -32,7 +30,7 @@ void main() async { await NotificationService.initialize(); await BackgroundService.initAndStart(); await SentryFlutter.init((options) { - options.dsn = Constants.sentryDsn; + options.dsn = sentryDsn; options.attachScreenshot = true; options.tracesSampleRate = 1.0; }, appRunner: () => runApp(MiUtem())); @@ -50,7 +48,7 @@ class _MiUtemState extends State { Widget build(BuildContext context) { FlutterUxcam.optIntoSchematicRecordings(); FlutterUxcam.startWithConfiguration(FlutterUxConfig( - userAppKey: kDebugMode ? Constants.uxCamDevKey : Constants.uxCamProdKey, + userAppKey: kDebugMode ? uxCamDevKey : uxCamProdKey, enableAutomaticScreenNameTagging: true, enableMultiSessionRecord: true, )); diff --git a/lib/models/preferencia.dart b/lib/models/preferencia.dart index 71f9687..aef6b9f 100644 --- a/lib/models/preferencia.dart +++ b/lib/models/preferencia.dart @@ -12,22 +12,24 @@ enum Preferencia { /// Obtiene la preferencia del storage, pero si no existe retorna el valor por defecto /// Si [guardar] es verdadero, entonces si no existe la preferencia, se guardará el valor por defecto Future get({ String? defaultValue, bool guardar = false }) async { - await add(defaultValue); + if(defaultValue != null) { + await add(defaultValue); + } return await secureStorage.read(key: this.name) ?? defaultValue; } /// Guarda la preferencia en el storage - set(String? value) => () async => await secureStorage.write(key: this.name, value: value); + set(String value) async => await secureStorage.write(key: this.name, value: value); /// Guarda la preferencia en el storage solo si no existe /// Si la preferencia ya existe, no se guardará nada - add(String? value) => () async { + add(String value) async { if (!(await exists())) { await set(value); } - }; + } /// Elimina la preferencia del storage - delete() => () async => await secureStorage.delete(key: this.name); + delete() async => await secureStorage.delete(key: this.name); } \ No newline at end of file diff --git a/lib/repositories/asignaturas_repository.dart b/lib/repositories/asignaturas_repository.dart index d693ba7..1d72907 100644 --- a/lib/repositories/asignaturas_repository.dart +++ b/lib/repositories/asignaturas_repository.dart @@ -1,24 +1,16 @@ -import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/utils/http/functions.dart'; class AsignaturasRepository { - final _authClient = HttpClient.authClient; - + /// Obtiene las asignaturas de una carrera Future?> getAsignaturas(String? carreraId, {bool forceRefresh = false}) async { if(carreraId == null) { return null; } - final path = "carreras/$carreraId/asignaturas"; - final Response response = await _authClient.get("$apiUrl/v1/$path", options: buildCacheOptions(Duration(days: 7), - forceRefresh: forceRefresh, - subKey: path, - )); + final response = await authClientRequest("carreras/$carreraId/asignaturas", forceRefresh: forceRefresh); final asignaturas = Asignatura.fromJsonList(response.data); for(int i = 0; i < asignaturas.length; i++) { @@ -31,38 +23,31 @@ class AsignaturasRepository { return asignaturas; } + /// Obtiene las notas de una asignatura + Future getNotasAsignatura(String? carreraId, Asignatura? asignatura, { bool forceRefresh = false }) async { + if(asignatura == null || carreraId == null) { + return null; + } + + final response = await authClientRequest("carreras/$carreraId/asignaturas/${asignatura.id}/notas", forceRefresh: forceRefresh); + return Asignatura.fromJson({ + ...asignatura.toJson(), + 'notas': response.data as Map, + }); + } + + /// Obtiene los estudiantes de una asignatura Future?> getEstudiantesAsignatura(Asignatura? asignatura, {bool forceRefresh = false}) async { if(asignatura == null) { return null; } - final path = "asignaturas/${asignatura.codigo}"; - final response = await _authClient.get("$apiUrl/v1/$path", options: buildCacheOptions(Duration(days: 7), - forceRefresh: forceRefresh, - subKey: path, - )); + final response = await authClientRequest("asignaturas/${asignatura.codigo}", forceRefresh: forceRefresh); final json = response.data as Map; // Por ahora solo se actualizan los estudiantes return User.fromJsonList(json['estudiantes']); } - Future getNotasAsignatura(String? carreraId, Asignatura? asignatura, { bool forceRefresh = false }) async { - if(asignatura == null || carreraId == null) { - return null; - } - - final path = "carreras/$carreraId/asignaturas/${asignatura.id}/notas"; - - final response = await _authClient.get("$apiUrl/v1/$path", options: buildCacheOptions(Duration(days: 7), - forceRefresh: forceRefresh, - subKey: path, - )); - - return Asignatura.fromJson({ - ...asignatura.toJson(), - 'notas': response.data as Map, - }); - } } \ No newline at end of file diff --git a/lib/repositories/auth_repository.dart b/lib/repositories/auth_repository.dart index 08f89fc..6492e4c 100644 --- a/lib/repositories/auth_repository.dart +++ b/lib/repositories/auth_repository.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/user/credential.dart'; import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/utils/http/functions.dart'; import 'package:mi_utem/utils/http/http_client.dart'; class AuthRepository { @@ -14,13 +15,17 @@ class AuthRepository { } Future refresh({required String token, required Credentials credentials}) async { - final response = await _httpClient.post("$apiUrl/v1/auth/refresh", data: credentials.toJson(), options: Options(headers: {"Authorization": "Bearer $token"})); + final response = await _httpClient.post("$apiUrl/v1/auth/refresh", data: credentials.toJson(), options: Options( + headers: { + "Authorization": "Bearer $token", + }, + )); final json = response.data as Map; return json["token"]; } Future updateProfilePicture({required String image}) async { - final response = await HttpClient.authClient.put("$apiUrl/v1/usuarios/foto", data: ({"imagen": image})); + final response = await authClientRequest("usuarios/foto", method: "PUT", data: ({"imagen": image})); final json = response.data; return json["fotoUrl"] as String; diff --git a/lib/repositories/carreras_repository.dart b/lib/repositories/carreras_repository.dart index 02f74a6..05289e2 100644 --- a/lib/repositories/carreras_repository.dart +++ b/lib/repositories/carreras_repository.dart @@ -1,14 +1,13 @@ -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/utils/http/functions.dart'; class CarrerasRepository { - final _authClient = HttpClient.authClient; - Future> getCarreras({ bool forceRefresh = false }) async { - final response = await _authClient.get("$apiUrl/v1/carreras", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: '/carreras')); + final response = await authClientRequest("carreras", + forceRefresh: forceRefresh, + ttl: Duration(days: 60), + ); return Carrera.fromJsonList(response.data as List); } diff --git a/lib/repositories/grades_repository.dart b/lib/repositories/grades_repository.dart index df9d47a..d53e6c7 100644 --- a/lib/repositories/grades_repository.dart +++ b/lib/repositories/grades_repository.dart @@ -1,13 +1,12 @@ -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; -import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/utils/http/functions.dart'; class GradesRepository { - final _authClient = HttpClient.authClient; Future getGrades({required String carreraId, required String asignaturaId, bool forceRefresh = false}) async { - final response = await _authClient.get("$apiUrl/v1/carreras/$carreraId/asignaturas/$asignaturaId/notas", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "carreras/$carreraId/asignaturas/$asignaturaId/notas")); + final response = await authClientRequest("carreras/$carreraId/asignaturas/$asignaturaId/notas", + forceRefresh: forceRefresh, + ); return Grades.fromJson(response.data as Map); } } \ No newline at end of file diff --git a/lib/repositories/horario_repository.dart b/lib/repositories/horario_repository.dart index 5d69c8c..10fe3bd 100644 --- a/lib/repositories/horario_repository.dart +++ b/lib/repositories/horario_repository.dart @@ -1,13 +1,13 @@ -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/horario.dart'; -import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/utils/http/functions.dart'; class HorarioRepository { - final _authClient = HttpClient.authClient; Future getHorario(String carreraId, {bool forceRefresh = false}) async { - final response = await _authClient.get("$apiUrl/v1/carreras/$carreraId/horarios", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "carreras/$carreraId/horarios")); + final response = await authClientRequest("carreras/$carreraId/horarios", + ttl: const Duration(days: 14), + forceRefresh: forceRefresh, + ); return Horario.fromJson(response.data); } } diff --git a/lib/repositories/noticias_repository.dart b/lib/repositories/noticias_repository.dart index 4900b54..6c8f79f 100644 --- a/lib/repositories/noticias_repository.dart +++ b/lib/repositories/noticias_repository.dart @@ -3,18 +3,14 @@ import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/utils/http/http_client.dart'; import 'package:mi_utem/utils/http/interceptors/headers_interceptor.dart'; +import 'package:mi_utem/utils/http/interceptors/log_interceptor.dart'; class NoticiasRepository { final _httpClient = Dio(BaseOptions(baseUrl: "https://noticias.utem.cl"))..interceptors.addAll([ HeadersInterceptor(), - HttpClient.logInterceptor, - DioCacheManager(CacheConfig( - baseUrl: "https://noticias.utem.cl", - defaultMaxAge: Duration(days: 7), - defaultMaxStale: Duration(days: 60), - databaseName: 'utem_noticias_cache', - )).interceptor, + logInterceptor, + HttpClient.cacheManager.interceptor, ]); Future?> getNoticias({ bool forceRefresh = false }) async { diff --git a/lib/repositories/permiso_ingreso_repository.dart b/lib/repositories/permiso_ingreso_repository.dart index c1f0bb6..091ffb7 100644 --- a/lib/repositories/permiso_ingreso_repository.dart +++ b/lib/repositories/permiso_ingreso_repository.dart @@ -1,20 +1,25 @@ -import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/models/permiso_ingreso.dart'; -import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/utils/http/functions.dart'; class PermisoIngresoRepository { - final _authClient = HttpClient.authClient; + Future> getPermisos({ bool forceRefresh = false }) async { + final response = await authClientRequest("permisos", + method: "POST", + ttl: Duration(days: 30), + forceRefresh: forceRefresh, + ); + return PermisoIngreso.fromJsonList(response.data as List?); + } Future getDetallesPermiso(String id, { bool forceRefresh = false }) async { - final response = await _authClient.post("$apiUrl/v1/permisos/$id", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "/permisos/$id")); + final response = await authClientRequest("permisos/$id", + method: "POST", + ttl: Duration(days: 30), + forceRefresh: forceRefresh, + ); return PermisoIngreso.fromJson(response.data as Map?); } - Future> getPermisos({ bool forceRefresh = false }) async { - final response = await _authClient.post("$apiUrl/v1/permisos", options: buildCacheOptions(Duration(days: 7), forceRefresh: forceRefresh, subKey: "/permisos")); - return PermisoIngreso.fromJsonList(response.data as List?); - } } \ No newline at end of file diff --git a/lib/screens/docentes_screen.dart b/lib/screens/docentes_screen.dart index 898e70f..3a884b0 100644 --- a/lib/screens/docentes_screen.dart +++ b/lib/screens/docentes_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/screens/usuario_screen.dart'; -import 'package:mi_utem/services/docentes_service.dart'; import 'package:mi_utem/utils/debounce.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; @@ -36,7 +35,7 @@ class _DocentesScreenState extends State { setState(() { _docentes = []; _futureDocentes = null; - _futureDocentes = DocentesService.buscarDocentes(nombre); + // _futureDocentes = DocentesService.buscarDocentes(nombre); }); List docentes = await _futureDocentes!; setState(() => _docentes = docentes); diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index efbb867..9a9ff97 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -1,17 +1,16 @@ -import 'dart:convert'; - import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/http_clients.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/auth_service.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; import 'package:mi_utem/widgets/loading/loading_dialog.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -73,35 +72,37 @@ class _SplashScreenState extends State { showLoadingDialog(context); // Revisar si tenemos conexión a internet try { - final response = await httpClient.get(Uri.parse(apiUrl)); - final json = jsonDecode(response.body); - if(!(json is Map && json["funcionando"] == true)) { + final response = await HttpClient.httpClient.head(apiUrl); + if(response.statusCode != 200) { Navigator.pop(context); showTextSnackbar(context, - title: "Error", - message: "No se pudo conectar al servidor. Revisa tu conexión a internet.", + title: "Error al conectar con la API", + message: "La app funcionará en modo Offline. Revisa tu conexión a internet si quieres acceder a todas las funcionalidades.", backgroundColor: Colors.red, + duration: Duration(seconds: 20), ); - return; } } catch (e) { + logger.e("[SplashScreen]: Error al conectar con la API", e); Navigator.pop(context); showTextSnackbar(context, - title: "Error", - message: "No se pudo conectar al servidor. Revisa tu conexión a internet.", + title: "Error al conectar con la API", + message: "La app funcionará en modo Offline. Revisa tu conexión a internet si quieres acceder a todas las funcionalidades.", backgroundColor: Colors.red, + duration: Duration(seconds: 20), ); return; } + final isLoggedIn = await _authService.isLoggedIn(); - if(!isLoggedIn) { - AnalyticsService.removeUser(); - } else { - final user = await _authService.getUser(); - if(user != null) { - AnalyticsService.setUser(user); - } + logger.d("[SplashScreen]: isLoggedIn: $isLoggedIn"); + final user = await _authService.getUser(); + AnalyticsService.removeUser(); + if(isLoggedIn && user != null) { + AnalyticsService.setUser(user); } + + // Esto nos asegura de que el splash es la única ruta inicial, y resuelve el error de poder volver al login. Navigator.popUntil(context, (route) => route.isFirst); final hasCompletedOnboarding = (await Preferencia.onboardingStep.get()) == "complete"; diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart index ffa0ec5..0cbb1c4 100644 --- a/lib/screens/usuario_screen.dart +++ b/lib/screens/usuario_screen.dart @@ -7,7 +7,6 @@ import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/services/auth_service.dart'; -import 'package:mi_utem/services/docentes_service.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; @@ -52,11 +51,11 @@ class _UsuarioScreenState extends State { try { User? user; if (widget.tipo == 2) { - if (widget.asignatura == null) { - user = await DocentesService.traerUnDocente(widget.query!["nombre"]); - } else { - user = await DocentesService.asignarUnDocente(widget.query!["nombre"], widget.asignatura!.codigo, widget.asignatura!.nombre); - } + // if (widget.asignatura == null) { + // user = await DocentesService.traerUnDocente(widget.query!["nombre"]); + // } else { + // user = await DocentesService.asignarUnDocente(widget.query!["nombre"], widget.asignatura!.codigo, widget.asignatura!.nombre); + // } setState(() => _user = user); } else { diff --git a/lib/service_manager.dart b/lib/service_manager.dart index a14ab9c..5b15611 100644 --- a/lib/service_manager.dart +++ b/lib/service_manager.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/repositories/asignaturas_repository.dart'; @@ -33,4 +37,20 @@ Future registerServices() async { /* Controladores (Para procesar datos de interfaz) */ Get.lazyPut(() => HorarioController()); Get.lazyPut(() => CalculatorController()); + + final credentialsRepository = Get.find(); + if(!await credentialsRepository.hasCredentials()) { + return; + } + + String? email = (await credentialsRepository.getCredentials())?.email; + if(email == null) { + return; + } + + if(!email.contains("@")) { + email += "@utem.cl"; + } + + logger.d("[ServiceManager]: ID de usuario: ${md5.convert(utf8.encode(email)).toString()} ($email)"); } \ No newline at end of file diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 1216f85..1c8e750 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -20,7 +20,7 @@ class AuthService { AuthRepository _authRepository = Get.find(); CredentialsRepository _credentialsService = Get.find(); - Future isFirstTime() async => await Preferencia.lastLogin.exists() == false; + Future isFirstTime() async => (await Preferencia.lastLogin.exists()) == false; Future isLoggedIn({ bool forceRefresh = false }) async { final credentials = await _getCredential(); @@ -36,9 +36,8 @@ class AuthService { return false; } - final hasLastLogin = await isFirstTime(); - if(!hasLastLogin) { - logger.d("[AuthService#isLoggedIn]: no last login"); + if(await isFirstTime()) { + logger.d("[AuthService#isLoggedIn]: Es primera vez"); return false; } diff --git a/lib/services/docentes_service.dart b/lib/services/docentes_service.dart index 7efbd74..9cf6d0c 100644 --- a/lib/services/docentes_service.dart +++ b/lib/services/docentes_service.dart @@ -1,88 +1,92 @@ -import 'package:dio/dio.dart'; -import 'package:mi_utem/config/http_clients.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/utils/dio_docente_client.dart'; class DocentesService { - static final Dio _dio = DioDocenteClient.initDio; static Future generarImagenPerfil(User user) async { - String baseUrl = "https://mi.utem.cl/static/interdocs/fotos/"; - List formatos = [".jpg", ".jpeg", ".png", ".gif"]; - - String imageUrl = "$baseUrl${user.rut?.rut}${formatos[0]}"; - - for (final formato in formatos) { - String actualImageUrl = "$baseUrl${user.rut?.rut}$formato"; - final imageResponse = await authClient.head(Uri.parse(actualImageUrl)); - - if (imageResponse.statusCode == 200) { - imageUrl = actualImageUrl; - return imageUrl; - } - } - - return imageUrl; + // String baseUrl = "https://mi.utem.cl/static/interdocs/fotos/"; + // List formatos = [".jpg", ".jpeg", ".png", ".gif"]; + // + // String imageUrl = "$baseUrl${user.rut?.rut}${formatos[0]}"; + // + // for (final formato in formatos) { + // String actualImageUrl = "$baseUrl${user.rut?.rut}$formato"; + // final imageResponse = await HttpClient.authClient.head(actualImageUrl); + // + // if (imageResponse.statusCode == 200) { + // imageUrl = actualImageUrl; + // return imageUrl; + // } + // } + // + // return imageUrl; + + throw Exception("Not implemented"); } static Future> buscarDocentes(String nombre) async { - String uri = "/docentes/buscar"; - - try { - dynamic data = {"nombre": nombre}; - - Response response = await _dio.get(uri, queryParameters: data); - - List usuarios = User.fromJsonList(response.data["docentes"]); - List usuariosConFoto = []; - for (var usuario in usuarios) { - User usuarioConFoto = usuario; - - usuarioConFoto.fotoUrl = await generarImagenPerfil(usuario); - usuariosConFoto.add(usuarioConFoto); - } - return usuariosConFoto; - } on DioError catch (e) { - print(e.message); - throw e; - } + // String uri = "/docentes/buscar"; + // + // try { + // dynamic data = {"nombre": nombre}; + // + // Response response = await _dio.get(uri, queryParameters: data); + // + // List usuarios = User.fromJsonList(response.data["docentes"]); + // List usuariosConFoto = []; + // for (var usuario in usuarios) { + // User usuarioConFoto = usuario; + // + // usuarioConFoto.fotoUrl = await generarImagenPerfil(usuario); + // usuariosConFoto.add(usuarioConFoto); + // } + // return usuariosConFoto; + // } on DioError catch (e) { + // print(e.message); + // throw e; + // } + + return []; } static Future traerUnDocente(String? nombre) async { - String uri = "/docentes/buscar"; - - try { - dynamic data = {"nombre": nombre, "limit": 1}; - - Response response = await _dio.get(uri, queryParameters: data); - - User user = User.fromJson(response.data); - user.fotoUrl = await generarImagenPerfil(user); - return user; - } on DioError catch (e) { - print(e.message); - throw e; - } + // String uri = "/docentes/buscar"; + // + // try { + // dynamic data = {"nombre": nombre, "limit": 1}; + // + // Response response = await _dio.get(uri, queryParameters: data); + // + // User user = User.fromJson(response.data); + // user.fotoUrl = await generarImagenPerfil(user); + // return user; + // } on DioError catch (e) { + // print(e.message); + // throw e; + // } + + throw Exception("Not implemented"); } static Future asignarUnDocente(String? nombreDocente, String? codigoAsignatura, String? nombreAsignatura) async { - String uri = "/docentes/asignar"; - - try { - dynamic data = { - "nombreDocente": nombreDocente, - "codigoAsignatura": codigoAsignatura, - "nombreAsignatura": nombreAsignatura - }; - - Response response = await _dio.post(uri, data: data); - - User user = User.fromJson(response.data); - user.fotoUrl = await generarImagenPerfil(user); - return user; - } on DioError catch (e) { - print(e.message); - throw e; - } + // String uri = "/docentes/asignar"; + // + // try { + // dynamic data = { + // "nombreDocente": nombreDocente, + // "codigoAsignatura": codigoAsignatura, + // "nombreAsignatura": nombreAsignatura + // }; + // + // Response response = await _dio.post(uri, data: data); + // + // User user = User.fromJson(response.data); + // user.fotoUrl = await generarImagenPerfil(user); + // return user; + // } on DioError catch (e) { + // print(e.message); + // throw e; + // } + + throw Exception("Not implemented"); } } diff --git a/lib/utils/dio_docente_client.dart b/lib/utils/dio_docente_client.dart index 62df54c..a6f2b74 100644 --- a/lib/utils/dio_docente_client.dart +++ b/lib/utils/dio_docente_client.dart @@ -1,6 +1,3 @@ -import 'package:dio/dio.dart'; -import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; -import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; class DioDocenteClient { static const bool isProduction = bool.fromEnvironment('dart.vm.product'); @@ -9,19 +6,4 @@ class DioDocenteClient { static const String productionUrl = 'http://docentes.inndev.studio/v1'; static const String url = isProduction ? productionUrl : productionUrl; - - static Dio _dio = Dio(BaseOptions( - baseUrl: url, - )); - - static CacheOptions get cacheOptions => CacheOptions( - store: HiveCacheStore('docentesutem'), - policy: CachePolicy.forceCache, - maxStale: const Duration(days: 7), - ); - - static Dio get initDio => _dio - ..interceptors.add( - DioCacheInterceptor(options: cacheOptions), - ); } diff --git a/lib/utils/http/functions.dart b/lib/utils/http/functions.dart new file mode 100644 index 0000000..0da7f46 --- /dev/null +++ b/lib/utils/http/functions.dart @@ -0,0 +1,26 @@ +import 'package:dio/dio.dart'; +import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/utils/http/http_client.dart'; + + +/// Función para realizar solicitudes mediante el authClient. +/// Esta función además genera un [CacheOptions] con opciones personalizadas por defecto. +/// También esta función utiliza `$apiUrl/v1/` como prefijo. +/// +/// [path] es el endpoint al que se desea acceder. No puede tener el prefijo `/v1/` o `/`. +/// [method] es el método HTTP a utilizar. +/// [data] es un mapa con los datos a enviar. +/// [options] son las opciones personalizadas para la solicitud. +/// [forceRefresh] fuerza a que se realice una solicitud nueva. +/// [ttl] es el tiempo que se guardará en caché la solicitud. +Future authClientRequest(String path, { + String method = "GET", + Map? headers, + Map? data, + String? contentType, + ResponseType? responseType, + Options? options, + bool forceRefresh = false, + Duration? ttl = const Duration(days: 7), +}) async => await HttpClient.authClient.request("$apiUrl/v1/$path", data: data, options: options ?? buildCacheOptions(ttl ?? Duration(days: 7), forceRefresh: forceRefresh, subKey: path).copyWith(method: method, headers: headers, contentType: contentType, responseType: responseType)); \ No newline at end of file diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index 5e8b229..93bc802 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -1,10 +1,10 @@ import 'package:dio/dio.dart'; import 'package:dio_http_cache/dio_http_cache.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/utils/http/interceptors/auth_interceptor.dart'; +import 'package:mi_utem/utils/http/interceptors/error_interceptor.dart'; import 'package:mi_utem/utils/http/interceptors/headers_interceptor.dart'; +import 'package:mi_utem/utils/http/interceptors/log_interceptor.dart'; class HttpClient { static const bool isProduction = bool.fromEnvironment('dart.vm.product'); @@ -12,55 +12,18 @@ class HttpClient { static const String productionUrl = 'https://api.exdev.cl/'; static String url = isProduction ? productionUrl : debugUrl; - static DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig( - baseUrl: url, - defaultMaxAge: Duration(days: 7), - defaultMaxStale: Duration(days: 60), - databaseName: 'mi_utem_cache', + static DioCacheManager cacheManager = DioCacheManager(CacheConfig( + defaultMaxAge: const Duration(days: 7), + defaultMaxStale: const Duration(days: 14), )); - static Dio httpClient = Dio(BaseOptions(baseUrl: url))..interceptors.addAll([ HeadersInterceptor(), - _errorInterceptor, + errorInterceptor, logInterceptor, - _dioCacheManager.interceptor, + cacheManager.interceptor, ]); static Dio authClient = httpClient..interceptors.add(AuthInterceptor()); - static InterceptorsWrapper _errorInterceptor = InterceptorsWrapper( - onError: (DioError err, ErrorInterceptorHandler handler) { - if(err.response?.statusCode == 401) { - return handler.next(err); - } - - final json = err.response?.data ?? {}; - if(json is Map && json.containsKey("error")) { - return handler.reject(DioError(requestOptions: err.requestOptions, error: CustomException.fromJson(json as Map))); - } - - return handler.reject(DioError(requestOptions: err.requestOptions, error: CustomException.custom(err.response?.statusMessage))); - }, - ); - - static InterceptorsWrapper logInterceptor = InterceptorsWrapper( - onRequest: (RequestOptions options, RequestInterceptorHandler handler) { - logger.d("[HttpClient]: ${options.method.toUpperCase()} ${options.uri}"); - options.extra["request_created_at"] = DateTime.now().toIso8601String(); - return handler.next(options); - }, - onResponse: (Response response, ResponseInterceptorHandler handler) { - final now = DateTime.now(); - final request = DateTime.tryParse(response.requestOptions.extra["request_created_at"] as String); - if(request != null) { - final duration = now.difference(request); - logger.d("[HttpClient]: ${response.statusCode} > ${response.requestOptions.method.toUpperCase()} ${response.requestOptions.uri} ${duration.inMilliseconds}ms"); - } - return handler.next(response); - }, - ); - - static Future clearCache() async{ - await _dioCacheManager.clearAll(); - } -} + static Future clearCache() async => await cacheManager.clearAll(); +} \ No newline at end of file diff --git a/lib/utils/http/interceptors/error_interceptor.dart b/lib/utils/http/interceptors/error_interceptor.dart new file mode 100644 index 0000000..da03286 --- /dev/null +++ b/lib/utils/http/interceptors/error_interceptor.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; + +InterceptorsWrapper errorInterceptor = InterceptorsWrapper( + onError: (DioError err, ErrorInterceptorHandler handler) { + if(err.response?.statusCode == 401) { + return handler.next(err); + } + + final json = err.response?.data ?? {}; + if(json is Map && json.containsKey("error")) { + return handler.reject(DioError(requestOptions: err.requestOptions, error: CustomException.fromJson(json as Map))); + } + + final error = DioError(requestOptions: err.requestOptions, error: CustomException.custom(err.response?.statusMessage), response: err.response, type: err.type); + error.stackTrace = err.stackTrace; + return handler.reject(error); + }, +); \ No newline at end of file diff --git a/lib/utils/http/interceptors/headers_interceptor.dart b/lib/utils/http/interceptors/headers_interceptor.dart index b2e1d60..8379286 100644 --- a/lib/utils/http/interceptors/headers_interceptor.dart +++ b/lib/utils/http/interceptors/headers_interceptor.dart @@ -13,6 +13,7 @@ class HeadersInterceptor extends Interceptor { headers['Content-Type'] = 'application/json'; } options.headers = headers; + super.onRequest(options, handler); } } \ No newline at end of file diff --git a/lib/utils/http/interceptors/log_interceptor.dart b/lib/utils/http/interceptors/log_interceptor.dart new file mode 100644 index 0000000..3abcebb --- /dev/null +++ b/lib/utils/http/interceptors/log_interceptor.dart @@ -0,0 +1,20 @@ +import 'package:dio/dio.dart'; +import 'package:mi_utem/config/logger.dart'; + +InterceptorsWrapper logInterceptor = InterceptorsWrapper( + onRequest: (RequestOptions options, RequestInterceptorHandler handler) { + final now = DateTime.now(); + logger.d("[HttpClient - ${now.toIso8601String()}]: ${options.method.toUpperCase()} ${options.uri}"); + options.extra["request_created_at"] = now.toIso8601String(); + return handler.next(options); + }, + onResponse: (Response response, ResponseInterceptorHandler handler) { + final now = DateTime.now(); + final requestCreatedAt = DateTime.tryParse(response.requestOptions.extra["request_created_at"] as String); + if(requestCreatedAt != null) { + final difference = now.difference(requestCreatedAt).inMilliseconds; + logger.d("[HttpClient - $requestCreatedAt]: ${response.statusCode} > ${response.requestOptions.method.toUpperCase()} ${response.requestOptions.uri} ${difference}ms"); + } + return handler.next(response); + }, +); \ No newline at end of file diff --git a/lib/widgets/snackbar.dart b/lib/widgets/snackbar.dart index 97aa337..2adc3b2 100644 --- a/lib/widgets/snackbar.dart +++ b/lib/widgets/snackbar.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/themes/theme.dart'; -void showErrorSnackbar(BuildContext context, String message) => showTextSnackbar(context, title: "Error", message: message, backgroundColor: Colors.red); +void showErrorSnackbar(BuildContext context, String message, { Function()? onTap }) => showTextSnackbar(context, title: "Error", message: message, backgroundColor: Colors.red, onTap: onTap); void showTextSnackbar(BuildContext context, { required String title, @@ -9,14 +9,18 @@ void showTextSnackbar(BuildContext context, { Color? backgroundColor, Color? textColor, Duration? duration, + Function()? onTap, }) => showSnackbar(context, - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: textColor ?? Colors.white)), - Text(message, style: TextStyle(color: textColor ?? Colors.white)), - ], + content: GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: textColor ?? Colors.white)), + Text(message, style: TextStyle(color: textColor ?? Colors.white)), + ], + ), ), backgroundColor: backgroundColor, duration: duration, @@ -26,11 +30,9 @@ void showSnackbar(BuildContext context, { required Widget content, Color? backgroundColor, Duration? duration, -}) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: content, - backgroundColor: backgroundColor ?? MainTheme.primaryColor, - behavior: SnackBarBehavior.floating, - duration: duration ?? const Duration(seconds: 5), - )); -} \ No newline at end of file +}) => ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: content, + backgroundColor: backgroundColor ?? MainTheme.primaryColor, + behavior: SnackBarBehavior.floating, + duration: duration ?? const Duration(seconds: 5), +)); \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 71a3aa2..089a8a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -321,22 +321,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.6" - dio_cache_interceptor: - dependency: "direct main" - description: - name: dio_cache_interceptor - sha256: a89166e6fb9c90a4bf2b7f20b5c055087969bc445ced3282e32505543e296e0f - url: "https://pub.dev" - source: hosted - version: "3.4.2" - dio_cache_interceptor_hive_store: - dependency: "direct main" - description: - name: dio_cache_interceptor_hive_store - sha256: "7a376b1db0a153e16ad51ce0cf1d2549ca14a2ddf462523c362fac9e077c5f14" - url: "https://pub.dev" - source: hosted - version: "3.2.1" dio_http_cache: dependency: "direct main" description: @@ -737,14 +721,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" - hive: - dependency: transitive - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" html: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 928fd99..a4c60dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,8 +21,6 @@ dependencies: cloud_firestore: ^4.3.1 custom_refresh_indicator: ^2.0.1 dio: ^4.0.6 - dio_cache_interceptor: ^3.2.6 - dio_cache_interceptor_hive_store: ^3.1.1 dotted_border: ^2.0.0+2 firebase_core: 2.16.0 firebase_analytics: 10.2.1 @@ -48,7 +46,7 @@ dependencies: logger: ^1.3.0 mdi: ^5.0.0-nullsafety.0 package_info_plus: ^4.2.0 - path_provider: ^2.0.2 + path_provider: ^2.1.1 permission_handler: ^10.2.0 photo_view: ^0.14.0 qr_flutter: ^4.0.0 @@ -59,7 +57,6 @@ dependencies: simple_gesture_detector: ^0.2.0 url_launcher: ^6.1.7 video_player: ^2.6.1 - dio_http_cache: ^0.3.0 image: ^3.1.0 intl: ^0.18.1 get_storage: ^2.0.3 @@ -76,6 +73,7 @@ dependencies: extended_masked_text: ^2.3.1 crypto: ^3.0.3 get: ^4.6.5 + dio_http_cache: ^0.3.0 dependency_overrides: qr: ^3.0.0 From a484fdd90c100b262ab670511b92d97347d37c39 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 15 May 2024 18:12:52 -0400 Subject: [PATCH 104/194] =?UTF-8?q?patch:=20reparado=20mensaje=20de=20erro?= =?UTF-8?q?r=20al=20iniciar=20sesi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/login_screen/login_button.dart | 57 ++++++++++------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index b8a2461..acd01b7 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; @@ -82,42 +83,32 @@ class _LoginButtonState extends State { await _authService.login(); - try { - final isFirstTime = await _authService.isFirstTime(); - final user = await _authService.getUser(); - if(user == null) { - Navigator.pop(context); - showTextSnackbar(context, - title: "Error", - message: "Ha ocurrido un error desconocido. Por favor intenta más tarde.", - ); - return; - } - - AnalyticsService.logEvent('login'); - AnalyticsService.setUser(user); - - Navigator.of(context).popUntil((route) => route.isFirst); // Esto elimina todas las pantallas anteriores - // Y esto reemplaza la pantalla actual por la nueva, cosa de que no pueda "volver" al login a menos que cierre la sesión. - final hasCompletedOnboarding = (await Preferencia.onboardingStep.get()) == 'complete'; - if(hasCompletedOnboarding) { - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); - if(isFirstTime) { - showDialog(context: context, builder: (ctx) => AcercaDialog()); - } - return; - } - - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => WelcomeScreen())); - } catch (e) { - logger.e(e); + final isFirstTime = await _authService.isFirstTime(); + final user = await _authService.getUser(); + if(user == null) { Navigator.pop(context); showTextSnackbar(context, title: "Error", message: "Ha ocurrido un error desconocido. Por favor intenta más tarde.", ); + return; } - return; + + AnalyticsService.logEvent('login'); + AnalyticsService.setUser(user); + + Navigator.of(context).popUntil((route) => route.isFirst); // Esto elimina todas las pantallas anteriores + // Y esto reemplaza la pantalla actual por la nueva, cosa de que no pueda "volver" al login a menos que cierre la sesión. + final hasCompletedOnboarding = (await Preferencia.onboardingStep.get()) == 'complete'; + if(hasCompletedOnboarding) { + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); + if(isFirstTime) { + showDialog(context: context, builder: (ctx) => AcercaDialog()); + } + return; + } + + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => WelcomeScreen())); } on CustomException catch (e) { logger.e(e); Navigator.pop(context); @@ -125,6 +116,12 @@ class _LoginButtonState extends State { title: "Error", message: e.message, ); + } on DioError catch (e) { + Navigator.pop(context); + showTextSnackbar(context, + title: "Error", + message: (e.error as CustomException).message, + ); } catch (e) { logger.e(e); Navigator.pop(context); From a4fa4cf80251977524d62ebfdecc6d568da82a7d Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 16 May 2024 17:19:55 -0400 Subject: [PATCH 105/194] =?UTF-8?q?patch:=20mejoras=20al=20cach=C3=A9,=20p?= =?UTF-8?q?recarga=20de=20datos=20y=20mejoras=20generales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Cambios * Se mejora eliminación de caché en Httpclient * Se ha mejorado el caché y reparado algunos errores que mantenían datos de una sesión anterior * Se pre-cargan los datos de horario y permisos en el onboarding para "acelerar" la carga de datos. * Se mejora repositorio de Noticias para cargar solo los datos necesarios y de la categoría 'todas-las-noticias'. ### Agregado * Se ha agregado CachedImage a las imágenes de las noticias * Se ha agregado una muestra de 2 decimales en caso de que la nota sea 3.95 en la calculadora * Se agrega un decimal para mostrar las notas en la calculadora (en todo momento) * Se agrega función #formatoNota para realizar el formato a la nota si es 3.95 o no. (Mostrando 2 decimales si es 3.95 o 1 en caso contrario) * Se agrega primaryKey al caché de las solicitudes por authClient * Ahora la función CarrerasService#getCarreras retorna la carrera seleccionada. ### Eliminado * Se eliminan constantes inutilizadas en HttpClient * Se elimina la posibilidad de colocar valores nulos a una Noticia Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/controllers/calculator_controller.dart | 4 +- lib/controllers/horario_controller.dart | 11 +---- lib/models/noticia.dart | 18 +++------ lib/models/user/persona.dart | 4 +- lib/repositories/noticias_repository.dart | 23 +++++++++-- lib/screens/onboarding/welcome_screen.dart | 17 ++++++++ lib/services/auth_service.dart | 13 +++--- lib/services/carreras_service.dart | 3 +- lib/services/grades_service.dart | 7 +--- lib/utils/http/functions.dart | 15 ++++++- lib/utils/http/http_client.dart | 20 +++++----- lib/utils/utils.dart | 5 ++- .../nota_examen_display_widget.dart | 3 +- .../nota_final_display_widget.dart | 3 +- .../calculadora_notas/nota_list_item.dart | 5 ++- .../nota_presentacion_display_widget.dart | 3 +- lib/widgets/noticias/noticia_card_widget.dart | 40 +++++++++---------- .../noticias/noticias_carrusel_widget.dart | 8 ++-- 18 files changed, 120 insertions(+), 82 deletions(-) diff --git a/lib/controllers/calculator_controller.dart b/lib/controllers/calculator_controller.dart index 420c393..e6acce4 100644 --- a/lib/controllers/calculator_controller.dart +++ b/lib/controllers/calculator_controller.dart @@ -104,7 +104,7 @@ class CalculatorController { )); gradeTextFieldControllers.add(MaskedTextController( mask: "0.0", - text: grade.nota?.toStringAsFixed(1) ?? "", + text: grade.nota?.toStringAsFixed(2) ?? "", )); _updateCalculations(); } @@ -140,7 +140,7 @@ class CalculatorController { void setExamGrade(num? grade, { bool updateTextController = true }) { examGrade.value = grade?.toDouble(); if(updateTextController) { - examGradeTextFieldController.value.updateText(grade?.toStringAsFixed(1) ?? "--"); + examGradeTextFieldController.value.updateText(grade?.toStringAsFixed(2) ?? "--"); } _updateCalculations(); } diff --git a/lib/controllers/horario_controller.dart b/lib/controllers/horario_controller.dart index 6fc0ef5..fecfcf2 100644 --- a/lib/controllers/horario_controller.dart +++ b/lib/controllers/horario_controller.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/repositories/horario_repository.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; @@ -73,17 +72,11 @@ class HorarioController { } Future getHorario({ bool forceRefresh = false }) async { - final carrerasService = Get.find(); - Carrera? carrera = carrerasService.selectedCarrera; - if (carrera == null || forceRefresh) { - await carrerasService.getCarreras(forceRefresh: forceRefresh); - } - carrera = carrerasService.selectedCarrera; - - final carreraId = carrera?.id; + final carreraId = (await Get.find().getCarreras(forceRefresh: forceRefresh))?.id; if(carreraId == null) { return null; } + final horario = await Get.find().getHorario(carreraId, forceRefresh: forceRefresh); if(horario != null) _setRandomColorsByHorario(horario); return horario; diff --git a/lib/models/noticia.dart b/lib/models/noticia.dart index 65e26af..ff5aa82 100644 --- a/lib/models/noticia.dart +++ b/lib/models/noticia.dart @@ -1,24 +1,16 @@ class Noticia { - int? id; - String? titulo, subtitulo, link, imagen; + int id; + String titulo, subtitulo, link, imagen; - Noticia({this.id, this.titulo, this.subtitulo, this.link, this.imagen}); + Noticia({required this.id, required this.titulo, required this.subtitulo, required this.link, required this.imagen}); - Noticia.empty() : this( - id: null, - titulo: '', - subtitulo: '', - link: '', - imagen: '', - ); - - factory Noticia.fromJson(Map? json) => json != null ? Noticia( + factory Noticia.fromJson(Map json) => Noticia( id: json["id"], titulo: json['yoast_head_json']['title'], subtitulo: json['yoast_head_json']['og_description'], imagen: json['yoast_head_json']['og_image'][0]['url'], link: "https://noticias.utem.cl/?p=${json['id']}", - ) : Noticia.empty(); + ); static List fromJsonList(List json) => json.map((e) => Noticia.fromJson(e)).toList(); } diff --git a/lib/models/user/persona.dart b/lib/models/user/persona.dart index bfcdc45..de2634e 100644 --- a/lib/models/user/persona.dart +++ b/lib/models/user/persona.dart @@ -16,8 +16,8 @@ class Persona { get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0].toUpperCase()).join(''); factory Persona.fromJson(Map json) => Persona( - nombreCompleto: json['nombreCompleto'], - rut: json.containsKey("rut") ? Rut.fromString("${json['rut']}") : null + nombreCompleto: json['nombreCompleto'], + rut: json.containsKey("rut") ? Rut.fromString("${json['rut']}") : null, ); Map toJson() => { diff --git a/lib/repositories/noticias_repository.dart b/lib/repositories/noticias_repository.dart index 6c8f79f..977ce0e 100644 --- a/lib/repositories/noticias_repository.dart +++ b/lib/repositories/noticias_repository.dart @@ -13,12 +13,29 @@ class NoticiasRepository { HttpClient.cacheManager.interceptor, ]); - Future?> getNoticias({ bool forceRefresh = false }) async { + Future> getNoticias({ bool forceRefresh = false }) async { final hasta = DateTime.now().toUtc().toIso8601String(); final desde = DateTime.now().subtract(Duration(days: 180)).toUtc().toIso8601String(); - final response = await _httpClient.get("/wp-json/wp/v2/posts?_embed&_fields=id,yoast_head_json&per_page=10&before=$hasta&after=$desde", options: buildCacheOptions(Duration(days: 14), forceRefresh: forceRefresh, subKey: "/noticias")); + final categoryIdResponse = await _httpClient.get("/wp-json/wp/v2/categories", + options: buildCacheOptions(Duration(days: 14), forceRefresh: forceRefresh, subKey: "/noticias/categorias"), + queryParameters: { + "_fields": "id", + "slug": "todas-las-noticias", + }, + ); + final categoryId = ((categoryIdResponse.data as List).first as Map)['id']; + final response = await _httpClient.get("/wp-json/wp/v2/posts", + options: buildCacheOptions(Duration(days: 14), forceRefresh: forceRefresh, subKey: "/noticias"), + queryParameters: { + "_fields": ["id", "yoast_head_json.title", "yoast_head_json.og_description", "yoast_head_json.og_image"].join(','), + "categories": categoryId, + "per_page": 12, + "before": hasta, + "after": desde, + }, + ); if (response.statusCode != 200) { - return null; + return []; } return Noticia.fromJsonList(response.data as List); diff --git a/lib/screens/onboarding/welcome_screen.dart b/lib/screens/onboarding/welcome_screen.dart index 4f3c95e..2378452 100644 --- a/lib/screens/onboarding/welcome_screen.dart +++ b/lib/screens/onboarding/welcome_screen.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/preferencia.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/horario_repository.dart'; +import 'package:mi_utem/repositories/permiso_ingreso_repository.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/set_alias_screen.dart'; +import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/gradient_background.dart'; @@ -25,6 +30,18 @@ class _WelcomeScreenState extends State { Navigator.push(context, MaterialPageRoute(builder: (ctx) => const SetAliasScreen())); } }); + + // Aprovechamos de utilizar el tiempo que tarde en el onboarding para pre-cargar algunos datos + Get.find().getCarreras().then((carrera) { + final carreraId = carrera?.id; + if(carreraId == null) { + return; + } + + Get.find().getHorario(carreraId, forceRefresh: true); + Get.find().getPermisos(forceRefresh: true); + Get.find().getAsignaturas(carreraId, forceRefresh: true); + }); super.initState(); } diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 1c8e750..cd82ef4 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -76,15 +76,16 @@ class AuthService { } Future logout({BuildContext? context}) async { - setUser(null); - _credentialsService.setCredentials(null); - Preferencia.onboardingStep.delete(); - Preferencia.lastLogin.delete(); - Preferencia.apodo.delete(); + await setUser(null); + await _credentialsService.setCredentials(null); + await Preferencia.onboardingStep.delete(); + await Preferencia.lastLogin.delete(); + await Preferencia.apodo.delete(); await HttpClient.clearCache(); if(context != null) { - Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (ctx) => LoginScreen())); + Navigator.popUntil(context, (route) => route.isFirst); + Navigator.pushReplacement(context, MaterialPageRoute(builder: (ctx) => LoginScreen())); } } diff --git a/lib/services/carreras_service.dart b/lib/services/carreras_service.dart index c43194f..95bb971 100644 --- a/lib/services/carreras_service.dart +++ b/lib/services/carreras_service.dart @@ -13,13 +13,14 @@ class CarrerasService { Carrera? selectedCarrera; - Future getCarreras({ bool forceRefresh = false }) async { + Future getCarreras({ bool forceRefresh = false }) async { logger.d("[CarrerasService#getCarreras]: Obteniendo carreras..."); final _carreras = await _carrerasRepository.getCarreras(forceRefresh: forceRefresh); carreras.clear(); carreras.addAll(_carreras); autoSelectCarreraActiva(); + return selectedCarrera; } void changeSelectedCarrera(Carrera carrera) => selectedCarrera = carrera; diff --git a/lib/services/grades_service.dart b/lib/services/grades_service.dart index 55d58e5..48ecf79 100644 --- a/lib/services/grades_service.dart +++ b/lib/services/grades_service.dart @@ -39,12 +39,7 @@ class GradesService { return {}; } - final carrerasService = Get.find(); - if(carrerasService.selectedCarrera == null) { - await carrerasService.getCarreras(); - } - final carrera = carrerasService.selectedCarrera; - final carreraId = carrera?.id; + final carreraId = (await Get.find().getCarreras())?.id; if(carreraId == null) { return {}; diff --git a/lib/utils/http/functions.dart b/lib/utils/http/functions.dart index 0da7f46..9787488 100644 --- a/lib/utils/http/functions.dart +++ b/lib/utils/http/functions.dart @@ -23,4 +23,17 @@ Future authClientRequest(String path, { Options? options, bool forceRefresh = false, Duration? ttl = const Duration(days: 7), -}) async => await HttpClient.authClient.request("$apiUrl/v1/$path", data: data, options: options ?? buildCacheOptions(ttl ?? Duration(days: 7), forceRefresh: forceRefresh, subKey: path).copyWith(method: method, headers: headers, contentType: contentType, responseType: responseType)); \ No newline at end of file +}) async => await HttpClient.authClient.request("$apiUrl/v1/$path", + data: data, + options: options ?? buildCacheOptions(ttl ?? Duration(days: 7), + forceRefresh: forceRefresh, + primaryKey: 'miutem', + subKey: path, + maxStale: const Duration(days: 14), + ).copyWith( + method: method, + headers: headers, + contentType: contentType, + responseType: responseType, + ), +); \ No newline at end of file diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index 93bc802..74c19b2 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -1,29 +1,31 @@ import 'package:dio/dio.dart'; import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/utils/http/interceptors/auth_interceptor.dart'; import 'package:mi_utem/utils/http/interceptors/error_interceptor.dart'; import 'package:mi_utem/utils/http/interceptors/headers_interceptor.dart'; import 'package:mi_utem/utils/http/interceptors/log_interceptor.dart'; class HttpClient { - static const bool isProduction = bool.fromEnvironment('dart.vm.product'); - static String debugUrl = dotenv.env['MI_UTEM_API_DEBUG'] ?? 'https://api.exdev.cl/'; - static const String productionUrl = 'https://api.exdev.cl/'; - static String url = isProduction ? productionUrl : debugUrl; - static DioCacheManager cacheManager = DioCacheManager(CacheConfig( + static final DioCacheManager cacheManager = DioCacheManager(CacheConfig( + baseUrl: apiUrl, defaultMaxAge: const Duration(days: 7), defaultMaxStale: const Duration(days: 14), )); - static Dio httpClient = Dio(BaseOptions(baseUrl: url))..interceptors.addAll([ + + static final Dio httpClient = Dio(BaseOptions(baseUrl: apiUrl))..interceptors.addAll([ HeadersInterceptor(), errorInterceptor, logInterceptor, cacheManager.interceptor, ]); - static Dio authClient = httpClient..interceptors.add(AuthInterceptor()); + static final Dio authClient = httpClient..interceptors.add(AuthInterceptor()); - static Future clearCache() async => await cacheManager.clearAll(); + static Future clearCache() async { + await cacheManager.deleteByPrimaryKey("miutem"); + await cacheManager.clearExpired(); + await cacheManager.clearAll(); + } } \ No newline at end of file diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index f701669..acb2649 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -18,4 +18,7 @@ /// DateTime? fecha = let(fechaPosiblementeNula, (fecha) => DateTime.tryParse(fecha)); /// print(fecha); // 2021-10-10 00:00:00.000 /// ``` -V? let(K? object, V Function(K) op) => object != null ? op(object) : null; \ No newline at end of file +V? let(K? object, V Function(K) op) => object != null ? op(object) : null; + +/// Esta función muestra una nota en formato de 1 o 2 decimales dependiendo de si es un 3.95 o no. +String? formatoNota(num? nota) => nota == 3.95 ? nota?.toStringAsFixed(2) : nota?.toStringAsFixed(1); \ No newline at end of file diff --git a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart index 9a5f6b6..5789bf8 100644 --- a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/utils/utils.dart'; class NotaExamenDisplayWidget extends StatelessWidget { @@ -29,7 +30,7 @@ class NotaExamenDisplayWidget extends StatelessWidget { onChanged: (String value) => _calculatorController.setExamGrade(double.tryParse(value.replaceAll(",", ".")), updateTextController: false), enabled: _calculatorController.canTakeExam.value, decoration: InputDecoration( - hintText: _calculatorController.minimumRequiredExamGrade.value?.toStringAsFixed(1) ?? "--", + hintText: formatoNota(_calculatorController.minimumRequiredExamGrade.value) ?? "--", filled: !_calculatorController.canTakeExam.value, fillColor: Colors.grey.withOpacity(0.2), disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( diff --git a/lib/widgets/calculadora_notas/nota_final_display_widget.dart b/lib/widgets/calculadora_notas/nota_final_display_widget.dart index 705beae..0699443 100644 --- a/lib/widgets/calculadora_notas/nota_final_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_final_display_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/utils/utils.dart'; class NotaFinalDisplayWidget extends StatelessWidget { @@ -14,7 +15,7 @@ class NotaFinalDisplayWidget extends StatelessWidget { return Column( children: [ - Obx(() => Text(_calculatorController.calculatedFinalGrade.value?.toStringAsFixed(1) ?? "--", + Obx(() => Text(formatoNota(_calculatorController.calculatedFinalGrade.value) ?? '--', style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, diff --git a/lib/widgets/calculadora_notas/nota_list_item.dart b/lib/widgets/calculadora_notas/nota_list_item.dart index 9a0c678..6f9e0d2 100644 --- a/lib/widgets/calculadora_notas/nota_list_item.dart +++ b/lib/widgets/calculadora_notas/nota_list_item.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/utils/utils.dart'; class NotaListItem extends StatelessWidget { final IEvaluacion evaluacion; @@ -28,7 +29,7 @@ class NotaListItem extends StatelessWidget { Widget build(BuildContext context) { final defaultGradeController = MaskedTextController( mask: "0.0", - text: evaluacion.nota?.toStringAsFixed(1) ?? "", + text: formatoNota(evaluacion.nota) ?? "", ); final defaultPercentageController = MaskedTextController( mask: "000", @@ -66,7 +67,7 @@ class NotaListItem extends StatelessWidget { }, textAlign: TextAlign.center, decoration: InputDecoration( - hintText: showSuggestedGrade ? (calculatorController.suggestedGrade.value?.toStringAsFixed(0) ?? "--") : "--", + hintText: showSuggestedGrade ? (formatoNota(calculatorController.suggestedGrade.value) ?? "--") : "--", disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: const BorderSide( color: Colors.transparent, diff --git a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart index 9d47af1..2e5838b 100644 --- a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/utils/utils.dart'; class NotaPresentacionDisplayWidget extends StatelessWidget { const NotaPresentacionDisplayWidget({ @@ -20,7 +21,7 @@ class NotaPresentacionDisplayWidget extends StatelessWidget { width: 80, margin: const EdgeInsets.only(left: 15), child: Obx(() => TextField( - controller: TextEditingController(text: _calculatorController.calculatedPresentationGrade.value?.toStringAsFixed(1) ?? ""), + controller: TextEditingController(text: formatoNota(_calculatorController.calculatedPresentationGrade.value) ?? ""), textAlign: TextAlign.center, enabled: false, decoration: InputDecoration( diff --git a/lib/widgets/noticias/noticia_card_widget.dart b/lib/widgets/noticias/noticia_card_widget.dart index c6340ce..7efd269 100644 --- a/lib/widgets/noticias/noticia_card_widget.dart +++ b/lib/widgets/noticias/noticia_card_widget.dart @@ -1,14 +1,18 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/noticia.dart'; import 'package:mi_utem/services/analytics_service.dart'; +import 'package:mi_utem/widgets/loading/loading_indicator.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; import 'package:url_launcher/url_launcher.dart'; class NoticiaCardWidget extends StatefulWidget { - final Noticia _noticia; + final Noticia noticia; - NoticiaCardWidget(this._noticia); + const NoticiaCardWidget({ + required this.noticia, + }); @override State createState() => _NoticiaCardWidgetState(); @@ -24,33 +28,27 @@ class _NoticiaCardWidgetState extends State { child: Material( color: Colors.transparent, child: InkWell( - onTap: () { - AnalyticsService.logEvent("noticia_card_tap"); - this._launchURL(widget._noticia.link!); - }, + onTap: _onTapNoticia, borderRadius: BorderRadius.all(Radius.circular(15)), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, - children: [ - widget._noticia.imagen != null ? Image.network(widget._noticia.imagen!, height: 110, fit: BoxFit.cover) : Container( + children: [ + CachedNetworkImage( + imageUrl: widget.noticia.imagen, + placeholder: (ctx, url) => LoadingIndicator(), + errorWidget: (ctx, url, error) => Icon(Icons.error, size: 110, color: Theme.of(context).colorScheme.error), height: 110, - width: double.infinity, - color: Colors.grey, - child: Icon( - Mdi.imageOff, - color: Colors.white, - ), + fit: BoxFit.cover, ), Container( padding: const EdgeInsets.symmetric(horizontal: 10), height: 70, child: Column( crossAxisAlignment: CrossAxisAlignment.center, - children: [ + children: [ Spacer(), - Text( - widget._noticia.titulo!, + Text(widget.noticia.titulo, maxLines: 3, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge, @@ -66,11 +64,13 @@ class _NoticiaCardWidgetState extends State { ), ); - Future _launchURL(String url) async { + _onTapNoticia() async { + final url = widget.noticia.link; if (await canLaunchUrl(Uri.parse(url))) { + AnalyticsService.logEvent("noticia_card_tap"); await launchUrl(Uri.parse(url)); } else { - throw 'Could not launch $url'; + showErrorSnackbar(context, "No se pudo abrir la noticia. Intenta más tarde."); } } } diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index c733f82..8bde18c 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -31,13 +31,13 @@ class NoticiasCarruselWidget extends StatelessWidget { const SizedBox(height: 10), FutureBuilder( future: Get.find().getNoticias(), - builder: (context, AsyncSnapshot?> snapshot) { + builder: (context, AsyncSnapshot> snapshot) { if (snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException) : CustomException.custom("No pudimos obtener las noticias."); - logger.d("[NoticiasCarruselWidget] ${error.message} (${error.statusCode})", snapshot.error); + logger.d("[NoticiasCarruselWidget] ${error.message} (${error.statusCode})", snapshot.error, snapshot.stackTrace); return CustomErrorWidget( - title: "Ocurrió un error al obtener las noticias", + title: "Ocurrió un error al cargar las noticias", error: error.message, ); } @@ -55,7 +55,7 @@ class NoticiasCarruselWidget extends StatelessWidget { viewportFraction: MediaQuery.of(context).orientation == Orientation.landscape ? 0.3 : 0.5, initialPage: 0, ), - itemBuilder: (BuildContext context, int i, int rI) => NoticiaCardWidget(noticias[i]), + itemBuilder: (BuildContext context, int i, int rI) => NoticiaCardWidget(noticia: noticias[i]), itemCount: noticias.length, ); }, From 12402baaba61521b8cda616ab88571f3dde2fd9d Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Sat, 18 May 2024 11:03:18 -0400 Subject: [PATCH 106/194] patch: se mejora la forma de obtener el id de la carrera Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/asignatura/asignatura_detalle_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 9c49b7c..048feca 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -51,8 +51,8 @@ class _AsignaturaDetalleScreenState extends State { child: AsignaturaNotasTab( asignatura: asignatura, onRefresh: () async { - final carrera = Get.find().selectedCarrera; - final asignatura = (await _asignaturasRepository.getAsignaturas(carrera?.id, forceRefresh: true))?.firstWhere((asignatura) => asignatura.codigo == this.asignatura.codigo && asignatura.id == this.asignatura.id); + final carreraId = (await Get.find().getCarreras())?.id; + final asignatura = (await _asignaturasRepository.getAsignaturas(carreraId, forceRefresh: true))?.firstWhere((asignatura) => asignatura.codigo == this.asignatura.codigo && asignatura.id == this.asignatura.id); if (asignatura != null) { setState(() => this.asignatura = asignatura); } From f4547d7ebccd9c7cd64c462d51810b7e1f3c3ca9 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 10:00:09 -0400 Subject: [PATCH 107/194] =?UTF-8?q?patch:=20se=20agrega=20bot=C3=B3n=20par?= =?UTF-8?q?a=20limpiar=20las=20notas=20de=20la=20calculadora?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/controllers/calculator_controller.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/controllers/calculator_controller.dart b/lib/controllers/calculator_controller.dart index e6acce4..92da48b 100644 --- a/lib/controllers/calculator_controller.dart +++ b/lib/controllers/calculator_controller.dart @@ -109,6 +109,13 @@ class CalculatorController { _updateCalculations(); } + void clearGrades() { + partialGrades.clear(); + percentageTextFieldControllers.clear(); + gradeTextFieldControllers.clear(); + _updateCalculations(); + } + void removeGradeAt(int index) { final grade = partialGrades[index]; if(!(grade.editable || freeEditable.value)) { From 21098de0c9bd2000b35b02de771e55058419a4d4 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 10:00:29 -0400 Subject: [PATCH 108/194] patch: se mejora y optimiza la carga de asignaturas Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/controllers/notification_controller.dart | 16 ++++- lib/models/asignaturas/asignatura.dart | 34 +++++++++ lib/models/noticia.dart | 5 +- lib/repositories/asignaturas_repository.dart | 11 +-- lib/repositories/grades_repository.dart | 10 +-- .../asignatura/asignatura_detalle_screen.dart | 17 ++--- .../asignatura/asignatura_notas_tab.dart | 69 ++++++++++--------- .../asignatura/asignaturas_lista_screen.dart | 34 +++++---- lib/screens/calculadora_notas_screen.dart | 11 ++- lib/services/auth_service.dart | 2 +- lib/services/grades_service.dart | 2 +- .../lista/asignatura_list_tile.dart | 35 ++++++++-- .../asignatura/lista/lista_asignaturas.dart | 8 ++- lib/widgets/custom_drawer.dart | 1 + lib/widgets/horario/bloque_ramo_card.dart | 39 ++++++----- 15 files changed, 193 insertions(+), 101 deletions(-) diff --git a/lib/controllers/notification_controller.dart b/lib/controllers/notification_controller.dart index ac9a159..fb8b1a0 100644 --- a/lib/controllers/notification_controller.dart +++ b/lib/controllers/notification_controller.dart @@ -4,8 +4,11 @@ import 'dart:developer'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/main.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/repositories/grades_repository.dart'; import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; @@ -47,10 +50,19 @@ class NotificationController { if (type == 'grade_change') { final asignaturaJsonString = payload?['asignatura']; - if (asignaturaJsonString != null) { + final carreraPayload = payload?['carrera']; + if (asignaturaJsonString != null && carreraPayload != null) { AnalyticsService.logEvent('notification_tap_grade_change'); + final carrera = Carrera.fromJson(jsonDecode(carreraPayload)); final asignatura = Asignatura.fromJson(jsonDecode(asignaturaJsonString)); - navigatorKey.currentState?.push(MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignatura: asignatura))); + try { // Intenta actualizar las notas. + asignatura.grades = await Get.find().getGrades(carreraId: carrera.id, asignaturaId: asignatura.id, forceRefresh: true); + } catch(_){} + + navigatorKey.currentState?.push(MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen( + carrera: carrera, + asignatura: asignatura, + ))); } } } diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart index 0574014..7e6ba53 100644 --- a/lib/models/asignaturas/asignatura.dart +++ b/lib/models/asignaturas/asignatura.dart @@ -89,4 +89,38 @@ class Asignatura { 'intentos': intentos, 'tipoSala': tipoSala, }; + + Asignatura copyWith({ + String? id, + String? nombre, + String? codigo, + String? tipoHora, + String? estado, + String? docente, + String? seccion, + Asistencia? asistencia, + Grades? grades, + List? estudiantes, + String? tipoAsignatura, + String? sala, + String? horario, + num? intentos, + String? tipoSala, + }) => Asignatura( + id: id ?? this.id, + nombre: nombre ?? this.nombre, + codigo: codigo ?? this.codigo, + tipoHora: tipoHora ?? this.tipoHora, + estado: estado ?? this.estado, + docente: docente ?? this.docente, + seccion: seccion ?? this.seccion, + asistencia: asistencia ?? this.asistencia, + grades: grades ?? this.grades, + estudiantes: estudiantes ?? this.estudiantes, + tipoAsignatura: tipoAsignatura ?? this.tipoAsignatura, + sala: sala ?? this.sala, + horario: horario ?? this.horario, + intentos: intentos ?? this.intentos, + tipoSala: tipoSala ?? this.tipoSala, + ); } \ No newline at end of file diff --git a/lib/models/noticia.dart b/lib/models/noticia.dart index ff5aa82..eb51116 100644 --- a/lib/models/noticia.dart +++ b/lib/models/noticia.dart @@ -1,13 +1,12 @@ class Noticia { int id; - String titulo, subtitulo, link, imagen; + String titulo, link, imagen; - Noticia({required this.id, required this.titulo, required this.subtitulo, required this.link, required this.imagen}); + Noticia({required this.id, required this.titulo, required this.link, required this.imagen}); factory Noticia.fromJson(Map json) => Noticia( id: json["id"], titulo: json['yoast_head_json']['title'], - subtitulo: json['yoast_head_json']['og_description'], imagen: json['yoast_head_json']['og_image'][0]['url'], link: "https://noticias.utem.cl/?p=${json['id']}", ); diff --git a/lib/repositories/asignaturas_repository.dart b/lib/repositories/asignaturas_repository.dart index 1d72907..6c88462 100644 --- a/lib/repositories/asignaturas_repository.dart +++ b/lib/repositories/asignaturas_repository.dart @@ -11,16 +11,7 @@ class AsignaturasRepository { } final response = await authClientRequest("carreras/$carreraId/asignaturas", forceRefresh: forceRefresh); - - final asignaturas = Asignatura.fromJsonList(response.data); - for(int i = 0; i < asignaturas.length; i++) { - final asignatura = await getNotasAsignatura(carreraId, asignaturas[i], forceRefresh: forceRefresh); - if(asignatura != null) { - asignaturas[i] = asignatura; - } - } - - return asignaturas; + return Asignatura.fromJsonList(response.data); } /// Obtiene las notas de una asignatura diff --git a/lib/repositories/grades_repository.dart b/lib/repositories/grades_repository.dart index d53e6c7..30fc7de 100644 --- a/lib/repositories/grades_repository.dart +++ b/lib/repositories/grades_repository.dart @@ -3,10 +3,12 @@ import 'package:mi_utem/utils/http/functions.dart'; class GradesRepository { - Future getGrades({required String carreraId, required String asignaturaId, bool forceRefresh = false}) async { - final response = await authClientRequest("carreras/$carreraId/asignaturas/$asignaturaId/notas", - forceRefresh: forceRefresh, - ); + Future getGrades({required String? carreraId, required String? asignaturaId, bool forceRefresh = false}) async { + if(carreraId == null || asignaturaId == null) { + return null; + } + + final response = await authClientRequest("carreras/$carreraId/asignaturas/$asignaturaId/notas", forceRefresh: forceRefresh); return Grades.fromJson(response.data as Map); } } \ No newline at end of file diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/asignatura_detalle_screen.dart index 048feca..3c59922 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/asignatura_detalle_screen.dart @@ -4,20 +4,22 @@ import 'package:mdi/mdi.dart'; import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; -import 'package:mi_utem/repositories/asignaturas_repository.dart'; +import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/repositories/grades_repository.dart'; import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; -import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; class AsignaturaDetalleScreen extends StatefulWidget { + final Carrera carrera; final Asignatura asignatura; const AsignaturaDetalleScreen({ super.key, + required this.carrera, required this.asignatura, }); @@ -27,9 +29,7 @@ class AsignaturaDetalleScreen extends StatefulWidget { class _AsignaturaDetalleScreenState extends State { - final _asignaturasRepository = Get.find(); late Asignatura asignatura; - bool get _mostrarCalculadora => RemoteConfigService.calculadoraMostrar; @override void initState() { @@ -51,11 +51,8 @@ class _AsignaturaDetalleScreenState extends State { child: AsignaturaNotasTab( asignatura: asignatura, onRefresh: () async { - final carreraId = (await Get.find().getCarreras())?.id; - final asignatura = (await _asignaturasRepository.getAsignaturas(carreraId, forceRefresh: true))?.firstWhere((asignatura) => asignatura.codigo == this.asignatura.codigo && asignatura.id == this.asignatura.id); - if (asignatura != null) { - setState(() => this.asignatura = asignatura); - } + asignatura.grades = await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: asignatura.id, forceRefresh: true); + setState(() => this.asignatura = asignatura); }, ), initial: true, @@ -69,7 +66,7 @@ class _AsignaturaDetalleScreenState extends State { child: Scaffold( appBar: CustomAppBar( title: Text(asignatura.nombre ?? "Asignatura sin nombre"), - actions: _mostrarCalculadora ? [ + actions: RemoteConfigService.calculadoraMostrar ? [ IconButton( icon: Icon(Mdi.calculator), tooltip: "Calculadora", diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/asignatura_notas_tab.dart index 9b5771f..aca37dc 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/asignatura_notas_tab.dart @@ -7,9 +7,8 @@ import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; class AsignaturaNotasTab extends StatelessWidget { - final Asignatura asignatura; - final Future Function() onRefresh; + final Future Function() onRefresh; const AsignaturaNotasTab({ super.key, @@ -17,39 +16,43 @@ class AsignaturaNotasTab extends StatelessWidget { required this.onRefresh, }); + @override - Widget build(BuildContext context) => PullToRefresh( - onRefresh: onRefresh, - child: ListView( - physics: AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.all(10), - children: [ - NotasDisplayWidget( - notaFinal: asignatura.grades?.notaFinal, - notaExamen: asignatura.grades?.notaExamen, - notaPresentacion: asignatura.grades?.notaPresentacion, - estado: asignatura.estado, - colorPorEstado: asignatura.colorPorEstado, - ), - Card( - child: Container( - padding: EdgeInsets.all(20), - child: asignatura.grades?.notasParciales.isNotEmpty == true ? ListView.builder( - shrinkWrap: true, - physics: ClampingScrollPhysics(), - itemBuilder: (context, i) { - REvaluacion evaluacion = asignatura.grades!.notasParciales[i]; - return NotaListItem(evaluacion: IEvaluacion.fromRemote(evaluacion)); - }, - itemCount: asignatura.grades!.notasParciales.length, - ) : const CustomErrorWidget( - emoji: "🤔", - title: "Parece que aún no hay notas ni ponderadores", + Widget build(BuildContext context) { + final grades = asignatura.grades; + final notasParciales = grades?.notasParciales; + + return PullToRefresh( + onRefresh: onRefresh, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(10), + children: [ + NotasDisplayWidget( + notaFinal: grades?.notaFinal, + notaExamen: grades?.notaExamen, + notaPresentacion: grades?.notaPresentacion, + estado: asignatura.estado, + colorPorEstado: asignatura.colorPorEstado, + ), + Card( + child: Padding ( + padding: const EdgeInsets.all(20), + child: notasParciales != null && notasParciales.isNotEmpty ? ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemBuilder: (context, i) => NotaListItem(evaluacion: IEvaluacion.fromRemote(notasParciales[i])), + itemCount: notasParciales.length, + ) : const CustomErrorWidget( + emoji: "🤔", + title: "Parece que aún no hay notas ni ponderadores", + ), ), ), - ), - ], - ), - ); + ], + ), + ); + } } + diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index f973239..0cc1941 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -3,7 +3,9 @@ import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/repositories/asignaturas_repository.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/carreras_service.dart'; @@ -17,7 +19,7 @@ import 'package:mi_utem/widgets/pull_to_refresh.dart'; class AsignaturasListaScreen extends StatefulWidget { const AsignaturasListaScreen({ - super.key + super.key, }); @override @@ -45,17 +47,20 @@ class _AsignaturasListaScreenState extends State { ), body: PullToRefresh( onRefresh: () async => setState(() => _forceRefresh = true), - child: FutureBuilder?>( + child: FutureBuilder>>( future: () async { - final carrerasService = Get.find(); - if (carrerasService.selectedCarrera == null) { - await carrerasService.getCarreras(); + final carrera = await Get.find().getCarreras(); + if(carrera == null) { + _forceRefresh = false; + throw CustomException.custom("No pudimos cargar los datos de tu carrera."); + } + final asignaturas = await _asignaturasService.getAsignaturas(carrera.id, forceRefresh: _forceRefresh); + if(asignaturas == null) { + _forceRefresh = false; + throw CustomException.custom("No pudimos cargar las asignaturas."); } - - final selectedCarrera = carrerasService.selectedCarrera; - final data = await _asignaturasService.getAsignaturas(selectedCarrera?.id, forceRefresh: _forceRefresh); _forceRefresh = false; - return data; + return Pair(carrera, asignaturas); }(), builder: (context, snapshot) { if(snapshot.hasError) { @@ -68,12 +73,17 @@ class _AsignaturasListaScreenState extends State { return _loadingWidget(); } - final asignaturas = snapshot.data ?? []; - if(asignaturas.isEmpty) { + final datos = snapshot.data; + final carrera = datos?.a; + final asignaturas = datos?.b; + if(carrera == null || asignaturas == null) { return _errorWidget("No encontramos asignaturas. Por favor intenta más tarde."); } - return ListaAsignaturas(asignaturas: asignaturas); + return ListaAsignaturas( + carrera: carrera, + asignaturas: asignaturas, + ); }, ), ), diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index 57d1578..c2157b9 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -14,9 +14,11 @@ class CalculadoraNotasScreen extends StatefulWidget { class _CalculadoraNotasScreenState extends State { + final CalculatorController calculatorController = Get.find(); + @override void initState() { - Get.find().makeEditable(); + calculatorController.makeEditable(); super.initState(); } @@ -25,6 +27,13 @@ class _CalculadoraNotasScreenState extends State { return Scaffold( appBar: CustomAppBar( title: const Text("Calculadora de notas"), + actions: [ + IconButton( + icon: const Icon(Icons.delete_outline), + tooltip: "Limpiar notas", + onPressed: calculatorController.clearGrades, + ), + ], ), body: ListView( padding: const EdgeInsets.all(10), diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index cd82ef4..6ff0943 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -44,7 +44,7 @@ class AuthService { final now = DateTime.now(); final lastLoginDate = let(await Preferencia.lastLogin.get(), (String _lastLogin) => DateTime.tryParse(_lastLogin)) ?? now; final difference = now.difference(lastLoginDate); - if(difference.inMinutes < 5 && now != lastLoginDate && !forceRefresh) { + if(difference.inMinutes < 4 && now != lastLoginDate && !forceRefresh) { return true; } diff --git a/lib/services/grades_service.dart b/lib/services/grades_service.dart index 48ecf79..4d06076 100644 --- a/lib/services/grades_service.dart +++ b/lib/services/grades_service.dart @@ -19,7 +19,7 @@ class GradesService { Future getGrades(String carreraId, String asignaturaId, {bool forceRefresh = false, bool saveGrades = true}) async { final grades = await _gradesRepository.getGrades(carreraId: carreraId, asignaturaId: asignaturaId); - if(saveGrades) { + if(saveGrades && grades != null) { await this.saveGrades(asignaturaId, grades); } diff --git a/lib/widgets/asignatura/lista/asignatura_list_tile.dart b/lib/widgets/asignatura/lista/asignatura_list_tile.dart index 0130f4c..c37ba37 100644 --- a/lib/widgets/asignatura/lista/asignatura_list_tile.dart +++ b/lib/widgets/asignatura/lista/asignatura_list_tile.dart @@ -1,24 +1,48 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/repositories/grades_repository.dart'; import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; import 'package:mi_utem/themes/theme.dart'; -class AsignaturaListTile extends StatelessWidget { +class AsignaturaListTile extends StatefulWidget { + final Carrera carrera; final Asignatura asignatura; const AsignaturaListTile({ super.key, + required this.carrera, required this.asignatura, }); + @override + State createState() => _AsignaturaListTileState(); +} + +class _AsignaturaListTileState extends State { + late Asignatura asignatura; + + @override + void initState() { + asignatura = widget.asignatura; + super.initState(); + } + @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Card( child: InkWell( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen( - asignatura: asignatura, - ))), + onTap: () async { + final grades = await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: asignatura.id); + setState(() => asignatura = asignatura.copyWith(grades: grades)); + + Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen( + carrera: widget.carrera, + asignatura: asignatura, + ))); + }, child: Container( padding: const EdgeInsets.all(20), width: double.infinity, @@ -47,5 +71,4 @@ class AsignaturaListTile extends StatelessWidget { ), ), ); - -} \ No newline at end of file +} diff --git a/lib/widgets/asignatura/lista/lista_asignaturas.dart b/lib/widgets/asignatura/lista/lista_asignaturas.dart index 9e0ee19..109f6b7 100644 --- a/lib/widgets/asignatura/lista/lista_asignaturas.dart +++ b/lib/widgets/asignatura/lista/lista_asignaturas.dart @@ -1,19 +1,25 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/widgets/asignatura/lista/asignatura_list_tile.dart'; class ListaAsignaturas extends StatelessWidget { + final Carrera carrera; final List asignaturas; const ListaAsignaturas({ super.key, + required this.carrera, required this.asignaturas, }); @override Widget build(BuildContext context) => ListView.builder( physics: AlwaysScrollableScrollPhysics(), - itemBuilder: (BuildContext context, int i) => AsignaturaListTile(asignatura: asignaturas[i]), + itemBuilder: (BuildContext context, int i) => AsignaturaListTile( + carrera: carrera, + asignatura: asignaturas[i], + ), itemCount: asignaturas.length, ); } diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 0d7e724..868cccd 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -21,6 +21,7 @@ import 'package:mi_utem/widgets/profile_photo.dart'; class CustomDrawer extends StatelessWidget { + const CustomDrawer({ super.key, }); diff --git a/lib/widgets/horario/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart index 03aec89..23cafcc 100644 --- a/lib/widgets/horario/bloque_ramo_card.dart +++ b/lib/widgets/horario/bloque_ramo_card.dart @@ -45,28 +45,33 @@ class ClassBlockCard extends StatelessWidget { _onTap(BloqueHorario block, BuildContext context) async { final asignatura = (await Get.find().getAsignaturas((Get.find().selectedCarrera)?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); if(asignatura == null) return; - AnalyticsService.logEvent( - "horario_class_block_tap", - parameters: { - "asignatura": asignatura.nombre, - "codigo": asignatura.codigo, - }, - ); - Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen(asignatura: asignatura))); + final carrera = await Get.find().getCarreras(); + if(carrera == null){ + return; + } + + AnalyticsService.logEvent("horario_class_block_tap", parameters: { + "asignatura": asignatura.nombre, + "codigo": asignatura.codigo, + }); + Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen( + carrera: carrera, + asignatura: asignatura, + ))); } _onLongPress(BloqueHorario block, BuildContext context) async { - final asignatura = (await Get.find().getAsignaturas((Get.find().selectedCarrera)?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); - if(asignatura == null) return; - AnalyticsService.logEvent( - "horario_class_block_long_press", - parameters: { - "asignatura": block.asignatura?.nombre, - "codigo": block.asignatura?.codigo, - }, - ); + final carrera = await Get.find().getCarreras(); + final asignatura = (await Get.find().getAsignaturas(carrera?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); + if(asignatura == null) { + return; + } + AnalyticsService.logEvent("horario_class_block_long_press", parameters: { + "asignatura": block.asignatura?.nombre, + "codigo": block.asignatura?.codigo, + }); showModalBottomSheet(context: context, builder: (ctx) => AsignaturaVistaPreviaModal(asignatura: asignatura, bloque: block)); } } From a70b1d14b8f4ee15c2519dc2cf7eb825e1c77677 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 13:03:20 -0400 Subject: [PATCH 109/194] patch: se optimiza y mejora la carga de asignaturas y notas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Cambios * Se mejora la carga de asignaturas * Ahora las notas se cargan en segundo plano para optimizar la muestra de datos * Se organizan archivos de asignaturas * La clase AsignaturaNotasTab ahora solo muestra datos en lugar de cargarlos, todo es cargado en el detalle siguiendo los lineamientos de flutter para el estado de la app. * Se reduce el codigo en AsignaturaNotasTab al realizar primero la revision si las notas son nulas o si existen * Se agrega indicador de carga al realizar acciones en el horario * Se repara error donde no se podía ver la vista previa de una asignatura en horario si no tiene sala. ### Agregado * Se agrega función CustomException#unknown para generar un error desconocido. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/controllers/horario_controller.dart | 8 +-- lib/controllers/notification_controller.dart | 2 +- lib/models/exceptions/custom_exception.dart | 2 + lib/repositories/asignaturas_repository.dart | 13 ----- .../asignatura/asignaturas_lista_screen.dart | 4 +- .../asignatura_detalle_screen.dart | 38 ++++++------- .../asignatura_estudiantes_tab.dart | 0 .../{ => detalle}/asignatura_notas_tab.dart | 25 +++++---- .../{ => detalle}/asignatura_resumen_tab.dart | 2 +- .../widgets/horario_blocks_content.dart | 14 +++-- .../lista/asignatura_list_tile.dart | 9 ++-- lib/widgets/horario/bloque_clase.dart | 54 +++++++++---------- lib/widgets/horario/bloque_ramo_card.dart | 25 +++++---- .../modals/asignatura_vista_previa_modal.dart | 2 +- 14 files changed, 97 insertions(+), 101 deletions(-) rename lib/screens/asignatura/{ => detalle}/asignatura_detalle_screen.dart (65%) rename lib/screens/asignatura/{ => detalle}/asignatura_estudiantes_tab.dart (100%) rename lib/screens/asignatura/{ => detalle}/asignatura_notas_tab.dart (73%) rename lib/screens/asignatura/{ => detalle}/asignatura_resumen_tab.dart (98%) diff --git a/lib/controllers/horario_controller.dart b/lib/controllers/horario_controller.dart index fecfcf2..5184a58 100644 --- a/lib/controllers/horario_controller.dart +++ b/lib/controllers/horario_controller.dart @@ -7,6 +7,7 @@ import 'package:mi_utem/repositories/horario_repository.dart'; import 'package:mi_utem/screens/horario/widgets/horario_main_scroller.dart'; import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:mi_utem/utils/utils.dart'; import 'package:vector_math/vector_math_64.dart' as vector; class HorarioController { @@ -145,10 +146,9 @@ class HorarioController { _storage.write(_key, _newColor.value); } - Color? getColor(Asignatura asignatura) { - final _key = '${asignatura.codigo}_${asignatura.tipoHora}'; - final _colorValue = _storage.read(_key); - return _colorValue != null ? Color(_colorValue) : null; + Color? getColor(Asignatura? asignatura) { + if(asignatura == null) return null; + return let(_storage.read('${asignatura.codigo}_${asignatura.tipoHora}'), (dynamic element) => Color(element)); } void setIndicatorIsOpen(bool isOpen) { diff --git a/lib/controllers/notification_controller.dart b/lib/controllers/notification_controller.dart index fb8b1a0..bf281f7 100644 --- a/lib/controllers/notification_controller.dart +++ b/lib/controllers/notification_controller.dart @@ -9,7 +9,7 @@ import 'package:mi_utem/main.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/repositories/grades_repository.dart'; -import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; +import 'package:mi_utem/screens/asignatura/detalle/asignatura_detalle_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; class NotificationController { diff --git a/lib/models/exceptions/custom_exception.dart b/lib/models/exceptions/custom_exception.dart index 8d2eb07..b669748 100644 --- a/lib/models/exceptions/custom_exception.dart +++ b/lib/models/exceptions/custom_exception.dart @@ -14,6 +14,8 @@ class CustomException implements Exception { this.internalCode, }); + factory CustomException.unknown() => CustomException.custom(null); + factory CustomException.custom(String? errorMessage) => CustomException(message: "Ha ocurrido un error inesperado. ${errorMessage ?? "Por favor intenta más tarde."}"); factory CustomException.fromJson(Map json) => CustomException( diff --git a/lib/repositories/asignaturas_repository.dart b/lib/repositories/asignaturas_repository.dart index 6c88462..7552731 100644 --- a/lib/repositories/asignaturas_repository.dart +++ b/lib/repositories/asignaturas_repository.dart @@ -14,19 +14,6 @@ class AsignaturasRepository { return Asignatura.fromJsonList(response.data); } - /// Obtiene las notas de una asignatura - Future getNotasAsignatura(String? carreraId, Asignatura? asignatura, { bool forceRefresh = false }) async { - if(asignatura == null || carreraId == null) { - return null; - } - - final response = await authClientRequest("carreras/$carreraId/asignaturas/${asignatura.id}/notas", forceRefresh: forceRefresh); - return Asignatura.fromJson({ - ...asignatura.toJson(), - 'notas': response.data as Map, - }); - } - /// Obtiene los estudiantes de una asignatura Future?> getEstudiantesAsignatura(Asignatura? asignatura, {bool forceRefresh = false}) async { if(asignatura == null) { diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index 0cc1941..0a2ef5c 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -7,6 +7,7 @@ import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/repositories/asignaturas_repository.dart'; +import 'package:mi_utem/repositories/grades_repository.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; @@ -47,7 +48,7 @@ class _AsignaturasListaScreenState extends State { ), body: PullToRefresh( onRefresh: () async => setState(() => _forceRefresh = true), - child: FutureBuilder>>( + child: FutureBuilder?>>( future: () async { final carrera = await Get.find().getCarreras(); if(carrera == null) { @@ -60,6 +61,7 @@ class _AsignaturasListaScreenState extends State { throw CustomException.custom("No pudimos cargar las asignaturas."); } _forceRefresh = false; + asignaturas.forEach((asignatura) => Get.find().getGrades(carreraId: carrera.id, asignaturaId: asignatura.id)); return Pair(carrera, asignaturas); }(), builder: (context, snapshot) { diff --git a/lib/screens/asignatura/asignatura_detalle_screen.dart b/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart similarity index 65% rename from lib/screens/asignatura/asignatura_detalle_screen.dart rename to lib/screens/asignatura/detalle/asignatura_detalle_screen.dart index 3c59922..9e5711b 100644 --- a/lib/screens/asignatura/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart @@ -5,9 +5,10 @@ import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; import 'package:mi_utem/models/carrera.dart'; +import 'package:mi_utem/repositories/asignaturas_repository.dart'; import 'package:mi_utem/repositories/grades_repository.dart'; -import 'package:mi_utem/screens/asignatura/asignatura_notas_tab.dart'; -import 'package:mi_utem/screens/asignatura/asignatura_resumen_tab.dart'; +import 'package:mi_utem/screens/asignatura/detalle/asignatura_notas_tab.dart'; +import 'package:mi_utem/screens/asignatura/detalle/asignatura_resumen_tab.dart'; import 'package:mi_utem/screens/calculadora_notas_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; @@ -24,18 +25,16 @@ class AsignaturaDetalleScreen extends StatefulWidget { }); @override - State createState() => _AsignaturaDetalleScreenState(); + State createState() => _AsignaturaDetalleScreenState(asignatura: asignatura); } class _AsignaturaDetalleScreenState extends State { - late Asignatura asignatura; + Asignatura asignatura; - @override - void initState() { - asignatura = widget.asignatura; - super.initState(); - } + _AsignaturaDetalleScreenState({ + required this.asignatura, + }); @override Widget build(BuildContext context) { @@ -51,17 +50,20 @@ class _AsignaturaDetalleScreenState extends State { child: AsignaturaNotasTab( asignatura: asignatura, onRefresh: () async { - asignatura.grades = await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: asignatura.id, forceRefresh: true); - setState(() => this.asignatura = asignatura); + final asignatura = (await Get.find().getAsignaturas(widget.carrera.id, forceRefresh: true))?.firstWhere((it) => it.id == this.asignatura.id).copyWith( + grades: await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: this.asignatura.id, forceRefresh: true), + ); + if(asignatura != null) { + setState(() => this.asignatura = asignatura); + } }, ), initial: true, ), ]; - final index = tabs.indexWhere((tab) => tab.initial); return DefaultTabController( - initialIndex: index == -1 ? 0 : index, + initialIndex: tabs.indexWhere((it) => it.initial), length: tabs.length, child: Scaffold( appBar: CustomAppBar( @@ -70,9 +72,9 @@ class _AsignaturaDetalleScreenState extends State { IconButton( icon: Icon(Mdi.calculator), tooltip: "Calculadora", - onPressed: () { + onPressed: () async { Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())); - final grades = asignatura.grades; + final grades = await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: asignatura.id); if (grades != null) { Get.find().updateWithGrades(grades); } @@ -81,12 +83,10 @@ class _AsignaturaDetalleScreenState extends State { ] : [], bottom: TabBar( indicatorColor: Colors.white.withOpacity(0.8), - tabs: tabs.map((tab) => Tab(text: tab.label)).toList(), + tabs: tabs.map((e) => Tab(text: e.label)).toList(), ), ), - body: TabBarView( - children: tabs.map((tab) => tab.child).toList(), - ), + body: TabBarView(children: tabs.map((e) => e.child).toList()), ), ); } diff --git a/lib/screens/asignatura/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/detalle/asignatura_estudiantes_tab.dart similarity index 100% rename from lib/screens/asignatura/asignatura_estudiantes_tab.dart rename to lib/screens/asignatura/detalle/asignatura_estudiantes_tab.dart diff --git a/lib/screens/asignatura/asignatura_notas_tab.dart b/lib/screens/asignatura/detalle/asignatura_notas_tab.dart similarity index 73% rename from lib/screens/asignatura/asignatura_notas_tab.dart rename to lib/screens/asignatura/detalle/asignatura_notas_tab.dart index aca37dc..d601d60 100644 --- a/lib/screens/asignatura/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/detalle/asignatura_notas_tab.dart @@ -6,9 +6,10 @@ import 'package:mi_utem/widgets/calculadora_notas/nota_list_item.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; + class AsignaturaNotasTab extends StatelessWidget { final Asignatura asignatura; - final Future Function() onRefresh; + final Future Function() onRefresh; const AsignaturaNotasTab({ super.key, @@ -16,11 +17,16 @@ class AsignaturaNotasTab extends StatelessWidget { required this.onRefresh, }); - @override Widget build(BuildContext context) { final grades = asignatura.grades; - final notasParciales = grades?.notasParciales; + final notasParciales = asignatura.grades?.notasParciales; + if(grades == null || notasParciales == null || notasParciales.isEmpty) { + return const CustomErrorWidget( + emoji: "🤔", + title: "Parece que aún no hay notas ni ponderadores", + ); + } return PullToRefresh( onRefresh: onRefresh, @@ -29,23 +35,20 @@ class AsignaturaNotasTab extends StatelessWidget { padding: const EdgeInsets.all(10), children: [ NotasDisplayWidget( - notaFinal: grades?.notaFinal, - notaExamen: grades?.notaExamen, - notaPresentacion: grades?.notaPresentacion, + notaFinal: grades.notaFinal, + notaExamen: grades.notaExamen, + notaPresentacion: grades.notaPresentacion, estado: asignatura.estado, colorPorEstado: asignatura.colorPorEstado, ), Card( - child: Padding ( + child: Padding( padding: const EdgeInsets.all(20), - child: notasParciales != null && notasParciales.isNotEmpty ? ListView.builder( + child: ListView.builder( shrinkWrap: true, physics: const ClampingScrollPhysics(), itemBuilder: (context, i) => NotaListItem(evaluacion: IEvaluacion.fromRemote(notasParciales[i])), itemCount: notasParciales.length, - ) : const CustomErrorWidget( - emoji: "🤔", - title: "Parece que aún no hay notas ni ponderadores", ), ), ), diff --git a/lib/screens/asignatura/asignatura_resumen_tab.dart b/lib/screens/asignatura/detalle/asignatura_resumen_tab.dart similarity index 98% rename from lib/screens/asignatura/asignatura_resumen_tab.dart rename to lib/screens/asignatura/detalle/asignatura_resumen_tab.dart index 6516d38..e03a14a 100644 --- a/lib/screens/asignatura/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/detalle/asignatura_resumen_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/screens/asignatura/asignatura_estudiantes_tab.dart'; +import 'package:mi_utem/screens/asignatura/detalle/asignatura_estudiantes_tab.dart'; import 'package:mi_utem/utils/string_utils.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; diff --git a/lib/screens/horario/widgets/horario_blocks_content.dart b/lib/screens/horario/widgets/horario_blocks_content.dart index a119072..2f9a652 100644 --- a/lib/screens/horario/widgets/horario_blocks_content.dart +++ b/lib/screens/horario/widgets/horario_blocks_content.dart @@ -30,14 +30,12 @@ class HorarioBlocksContent extends StatelessWidget { List bloquePorDias = horario.horarioEnlazado[blockIndex]; for (num dia = 0; dia < bloquePorDias.length; dia++) { BloqueHorario block = horario.horarioEnlazado[blockIndex][dia as int]; - currentRow.add( - ClassBlockCard( - block: block, - height: blockHeight, - width: blockWidth, - internalMargin: blockInternalMargin, - ), - ); + currentRow.add(ClassBlockCard( + block: block, + height: blockHeight, + width: blockWidth, + internalMargin: blockInternalMargin, + )); } rows.add(TableRow(children: currentRow)); } diff --git a/lib/widgets/asignatura/lista/asignatura_list_tile.dart b/lib/widgets/asignatura/lista/asignatura_list_tile.dart index c37ba37..98d8651 100644 --- a/lib/widgets/asignatura/lista/asignatura_list_tile.dart +++ b/lib/widgets/asignatura/lista/asignatura_list_tile.dart @@ -3,8 +3,9 @@ import 'package:get/get.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/repositories/grades_repository.dart'; -import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; +import 'package:mi_utem/screens/asignatura/detalle/asignatura_detalle_screen.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/widgets/loading/loading_dialog.dart'; class AsignaturaListTile extends StatefulWidget { final Carrera carrera; @@ -35,12 +36,12 @@ class _AsignaturaListTileState extends State { child: Card( child: InkWell( onTap: () async { + showLoadingDialog(context); final grades = await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: asignatura.id); - setState(() => asignatura = asignatura.copyWith(grades: grades)); - + Navigator.pop(context); Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen( carrera: widget.carrera, - asignatura: asignatura, + asignatura: asignatura.copyWith(grades: grades), ))); }, child: Container( diff --git a/lib/widgets/horario/bloque_clase.dart b/lib/widgets/horario/bloque_clase.dart index 0f426d9..e089675 100644 --- a/lib/widgets/horario/bloque_clase.dart +++ b/lib/widgets/horario/bloque_clase.dart @@ -25,36 +25,32 @@ class BloqueClase extends StatelessWidget { }); @override - Widget build(BuildContext context) { - HorarioController _controller = Get.find(); - - return Container( - decoration: BoxDecoration( - color: _controller.getColor(block.asignatura!) ?? this.color, + Widget build(BuildContext context) => DecoratedBox( + decoration: BoxDecoration( + color: Get.find().getColor(block.asignatura) ?? this.color, + borderRadius: BorderRadius.circular(15), + ), + child: Material( + color: Colors.transparent, + child: InkWell( borderRadius: BorderRadius.circular(15), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(15), - onTap: onTap != null ? () => onTap?.call(block) : null, - onLongPress: onLongPress != null ? () => onLongPress?.call(block) : null, - child: Column( - children: [ - HorarioText.classCode(block.codigo!, - color: textColor, - ), - HorarioText.className(block.asignatura!.nombre!.toUpperCase(), - color: textColor, - ), - HorarioText.classLocation(block.sala ?? "Sin sala", - color: textColor, - ), - ], - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - ), + onTap: () => onTap?.call(block), + onLongPress: () => onLongPress?.call(block), + child: Column( + children: [ + HorarioText.classCode("${block.codigo}", + color: textColor, + ), + HorarioText.className("${block.asignatura?.nombre?.toUpperCase()}", + color: textColor, + ), + HorarioText.classLocation(block.sala ?? "Sin sala", + color: textColor, + ), + ], + mainAxisAlignment: MainAxisAlignment.spaceEvenly, ), ), - ); - } + ), + ); } \ No newline at end of file diff --git a/lib/widgets/horario/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart index 23cafcc..0f61671 100644 --- a/lib/widgets/horario/bloque_ramo_card.dart +++ b/lib/widgets/horario/bloque_ramo_card.dart @@ -2,12 +2,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/models/horario.dart'; import 'package:mi_utem/repositories/asignaturas_repository.dart'; -import 'package:mi_utem/screens/asignatura/asignatura_detalle_screen.dart'; +import 'package:mi_utem/repositories/grades_repository.dart'; +import 'package:mi_utem/screens/asignatura/detalle/asignatura_detalle_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/carreras_service.dart'; import 'package:mi_utem/widgets/horario/bloque_clase.dart'; import 'package:mi_utem/widgets/horario/bloque_vacio.dart'; import 'package:mi_utem/widgets/horario/modals/asignatura_vista_previa_modal.dart'; +import 'package:mi_utem/widgets/loading/loading_dialog.dart'; class ClassBlockCard extends StatelessWidget { final BloqueHorario? block; @@ -16,7 +18,7 @@ class ClassBlockCard extends StatelessWidget { final double internalMargin; final Color textColor; - ClassBlockCard({ + const ClassBlockCard({ super.key, required this.block, required this.width, @@ -26,7 +28,7 @@ class ClassBlockCard extends StatelessWidget { }); @override - Widget build(BuildContext context) => Container( + Widget build(BuildContext context) => SizedBox( height: height, width: width, child: Padding( @@ -43,11 +45,12 @@ class ClassBlockCard extends StatelessWidget { ); _onTap(BloqueHorario block, BuildContext context) async { - final asignatura = (await Get.find().getAsignaturas((Get.find().selectedCarrera)?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); - if(asignatura == null) return; - + showLoadingDialog(context); final carrera = await Get.find().getCarreras(); - if(carrera == null){ + final asignatura = (await Get.find().getAsignaturas(carrera?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); + final grades = await Get.find().getGrades(carreraId: carrera?.id, asignaturaId: asignatura?.id); + if(carrera == null || asignatura == null) { + Navigator.pop(context); return; } @@ -55,16 +58,19 @@ class ClassBlockCard extends StatelessWidget { "asignatura": asignatura.nombre, "codigo": asignatura.codigo, }); + Navigator.pop(context); Navigator.push(context, MaterialPageRoute(builder: (ctx) => AsignaturaDetalleScreen( carrera: carrera, - asignatura: asignatura, + asignatura: asignatura.copyWith(grades: grades), ))); } _onLongPress(BloqueHorario block, BuildContext context) async { + showLoadingDialog(context); final carrera = await Get.find().getCarreras(); final asignatura = (await Get.find().getAsignaturas(carrera?.id))?.firstWhereOrNull((asignatura) => asignatura.id == block.asignatura?.id || asignatura.codigo == block.asignatura?.codigo); - if(asignatura == null) { + if(carrera == null || asignatura == null) { + Navigator.pop(context); return; } @@ -72,6 +78,7 @@ class ClassBlockCard extends StatelessWidget { "asignatura": block.asignatura?.nombre, "codigo": block.asignatura?.codigo, }); + Navigator.pop(context); showModalBottomSheet(context: context, builder: (ctx) => AsignaturaVistaPreviaModal(asignatura: asignatura, bloque: block)); } } diff --git a/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart b/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart index 8e29dad..0b28911 100644 --- a/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart +++ b/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart @@ -48,7 +48,7 @@ class AsignaturaVistaPreviaModal extends StatelessWidget { Divider(height: 5, indent: 20, endIndent: 20), ListTile( title: Text("Sala"), - subtitle: Text(bloque.sala!), + subtitle: Text(bloque.sala ?? "Sin sala"), ), ], ), From 5fa5988114c28c541e6928b843ca337c607dbc2a Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 15:24:34 -0400 Subject: [PATCH 110/194] =?UTF-8?q?patch:=20se=20agrega=20informaci=C3=B3n?= =?UTF-8?q?=20de=20la=20app=20en=20modo=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/remote_config/defaults.dart | 50 +++-- lib/services/remote_config/keys.dart | 4 +- lib/services/remote_config/remote_config.dart | 1 + lib/widgets/acerca/acerca_screen.dart | 15 +- lib/widgets/acerca/club/acerca_app.dart | 62 ++++++ lib/widgets/acerca/club/acerca_club.dart | 79 ++++---- .../club/acerca_club_desarrolladores.dart | 182 +++++++++--------- .../acerca/club/acerca_club_redes.dart | 65 +++---- 8 files changed, 264 insertions(+), 194 deletions(-) create mode 100644 lib/widgets/acerca/club/acerca_app.dart diff --git a/lib/services/remote_config/defaults.dart b/lib/services/remote_config/defaults.dart index eaba945..2c331b7 100644 --- a/lib/services/remote_config/defaults.dart +++ b/lib/services/remote_config/defaults.dart @@ -2,13 +2,10 @@ part of 'remote_config.dart'; final _defaults = { RemoteConfigServiceKeys.banners: jsonEncode([]), - RemoteConfigServiceKeys.creditos: jsonEncode( - ['Hecho con ❤ por el *Club de Desarrollo Experimental* junto a SISEI']), + RemoteConfigServiceKeys.creditos: jsonEncode(['Hecho con ❤ por el *Club de Desarrollo Experimental* junto a SISEI']), RemoteConfigServiceKeys.clubNombre: "Club de Desarrollo Experimental", - RemoteConfigServiceKeys.clubDescripcion: - "El Club de Desarrollo Experimental es una iniciativa de estudiantes y para estudiantes de la UTEM que busca realzar el lado tecnológico que debería tener la universidad, impulsando y desarrollando ideas y proyectos de caracter innovador.", - RemoteConfigServiceKeys.clubLogo: - "https://user-images.githubusercontent.com/16374322/114324335-737b6b80-9af7-11eb-841d-9d14aca0f988.png", + RemoteConfigServiceKeys.clubDescripcion: "El Club de Desarrollo Experimental es una iniciativa de estudiantes y para estudiantes de la UTEM que busca realzar el lado tecnológico que debería tener la universidad, impulsando y desarrollando ideas y proyectos de caracter innovador.", + RemoteConfigServiceKeys.clubLogo: "https://user-images.githubusercontent.com/16374322/114324335-737b6b80-9af7-11eb-841d-9d14aca0f988.png", RemoteConfigServiceKeys.clubRedes: jsonEncode([ { "nombre": "Facebook", @@ -35,10 +32,9 @@ final _defaults = { "url": "https://www.linkedin.com/company/exdevutem/" } ]), - RemoteConfigServiceKeys.miutemDescripcion: - "Esta aplicación surgió a principios del 2019 como un proyecto independiente **creado completamente por estudiantes** del Club de Desarrollo Experimental (ExDev) de la UTEM ❤️. \nActualmente nos encontramos trabajando **junto al equipo de SISEI** para que esta aplicación se convierta en la aplicación institucional oficial de la universidad 🎉 \nToda la información corresponde a datos referenciales, y debe ser validada por la Dirección General de Docencia.", - RemoteConfigServiceKeys.miutemPortada: - "https://user-images.githubusercontent.com/16374322/114324046-16cb8100-9af6-11eb-9a95-11da425e2fbd.png", + RemoteConfigServiceKeys.miutemAcercaDeLaApp: "- Paquete: **%paquete**\n- Aplicación: **%nombre**\n- Versión: **%version**\n- Número de Compilación: **%compilacion**", + RemoteConfigServiceKeys.miutemDescripcion: "Esta aplicación surgió a principios del 2019 como un proyecto independiente **creado completamente por estudiantes** del Club de Desarrollo Experimental (ExDev) de la UTEM ❤️. \nActualmente nos encontramos trabajando **junto al equipo de SISEI** para que esta aplicación se convierta en la aplicación institucional oficial de la universidad 🎉 \nToda la información corresponde a datos referenciales, y debe ser validada por la Dirección General de Docencia.", + RemoteConfigServiceKeys.miutemPortada: "https://user-images.githubusercontent.com/16374322/114324046-16cb8100-9af6-11eb-9a95-11da425e2fbd.png", RemoteConfigServiceKeys.miutemDesarrolladores: jsonEncode([ { "nombre": "Sebastián Albornoz Medina", @@ -121,6 +117,25 @@ final _defaults = { } ] }, + { + "nombre": "Francisco Solís Maturana", + "rol": "Desarrollador", + "fotoUrl": "https://avatars.githubusercontent.com/u/30329003", + "redes": [ + { + "nombre": "LinkedIn", + "color": Color(0xFF0077b5).value, + "icono": FontAwesomeIcons.linkedinIn.codePoint, + "url": "https://www.linkedin.com/in/franciscosolismat/" + }, + { + "nombre": "GitHub", + "color": Color(0xFF333333).value, + "icono": FontAwesomeIcons.github.codePoint, + "url": "https://github.com/Im-Fran", + } + ] + }, { "nombre": "Jorge Verdugo Chacón", "rol": "Desarrollador", @@ -168,19 +183,14 @@ final _defaults = { ] } ]), - RemoteConfigServiceKeys.credencialBarras: - "Código de barras compatible con Sistema de Bibliotecas", - RemoteConfigServiceKeys.credencialInfo: - "**Mayor información:** \n\n [biblioteca.utem.cl](https://biblioteca.utem.cl/)\n\nEn caso de dudas con su credencial, consultar a su biblioteca o al correo electrónico [**credenciales@utem.cl**](mailto:credenciales@utem.cl)", - RemoteConfigServiceKeys.credencialDisclaimer: - "**Esta credencial virtual es generada automáticamente y es de uso personal e intransferible. El atraso en la devolución de libros y revistas será sancionado por la biblioteca.**", - RemoteConfigServiceKeys.credencialSibutemLogo: - "https://user-images.githubusercontent.com/16374322/114325090-42ea0080-9afc-11eb-9cc8-ef4846d4ad8f.jpg", + RemoteConfigServiceKeys.credencialBarras: "Código de barras compatible con Sistema de Bibliotecas", + RemoteConfigServiceKeys.credencialInfo: "**Mayor información:** \n\n [biblioteca.utem.cl](https://biblioteca.utem.cl/)\n\nEn caso de dudas con su credencial, consultar a su biblioteca o al correo electrónico [**credenciales@utem.cl**](mailto:credenciales@utem.cl)", + RemoteConfigServiceKeys.credencialDisclaimer: "**Esta credencial virtual es generada automáticamente y es de uso personal e intransferible. El atraso en la devolución de libros y revistas será sancionado por la biblioteca.**", + RemoteConfigServiceKeys.credencialSibutemLogo: "https://user-images.githubusercontent.com/16374322/114325090-42ea0080-9afc-11eb-9cc8-ef4846d4ad8f.jpg", RemoteConfigServiceKeys.calculadoraMostrar: true, RemoteConfigServiceKeys.horarioZoom: 0.5, RemoteConfigServiceKeys.homeProntoIcono: Icons.pregnant_woman.codePoint, - RemoteConfigServiceKeys.homeProntoTexto: - "Se están gestando nuevas funciones 😎", + RemoteConfigServiceKeys.homeProntoTexto: "Se están gestando nuevas funciones 😎", RemoteConfigServiceKeys.prontoEg: "Este recurso no esta disponible", RemoteConfigServiceKeys.egHabilitados: true, RemoteConfigServiceKeys.drawerMenu: jsonEncode([ diff --git a/lib/services/remote_config/keys.dart b/lib/services/remote_config/keys.dart index f6fae07..d8e1652 100644 --- a/lib/services/remote_config/keys.dart +++ b/lib/services/remote_config/keys.dart @@ -5,14 +5,14 @@ class RemoteConfigServiceKeys { static const String clubNombre = 'club_nombre'; static const String clubDescripcion = 'club_descripcion'; static const String clubLogo = 'club_logo_url'; + static const String miutemAcercaDeLaApp = 'miutem_acerca_de_la_app'; static const String miutemDescripcion = 'miutem_descripcion'; static const String miutemDesarrolladores = 'miutem_desarrolladores'; static const String miutemPortada = 'miutem_portada_url'; static const String credencialBarras = 'miutem_credencial_barras_detalle'; static const String credencialDisclaimer = 'miutem_credencial_disclaimer'; static const String credencialInfo = 'miutem_credencial_info'; - static const String credencialSibutemLogo = - 'miutem_credencial_sibutem_logo_url'; + static const String credencialSibutemLogo = 'miutem_credencial_sibutem_logo_url'; static const String calculadoraMostrar = 'miutem_calculadora_mostrar'; static const String horarioZoom = 'miutem_horario_zoom'; static const String homeProntoIcono = 'miutem_home_pronto_icono'; diff --git a/lib/services/remote_config/remote_config.dart b/lib/services/remote_config/remote_config.dart index 8264ea7..d5124de 100644 --- a/lib/services/remote_config/remote_config.dart +++ b/lib/services/remote_config/remote_config.dart @@ -28,6 +28,7 @@ class RemoteConfigService { static final clubNombre = _getString(RemoteConfigServiceKeys.clubNombre); static final clubDescripcion = _getString(RemoteConfigServiceKeys.clubDescripcion); static final clubLogo = _getString(RemoteConfigServiceKeys.clubLogo); + static final miutemAcercaDeLaApp = _getString(RemoteConfigServiceKeys.miutemAcercaDeLaApp); static final miutemDescripcion = _getString(RemoteConfigServiceKeys.miutemDescripcion); static final miutemDesarrolladores = _getString(RemoteConfigServiceKeys.miutemDesarrolladores); static final miutemPortada = _getString(RemoteConfigServiceKeys.miutemPortada); diff --git a/lib/widgets/acerca/acerca_screen.dart b/lib/widgets/acerca/acerca_screen.dart index ad35e68..75b957b 100644 --- a/lib/widgets/acerca/acerca_screen.dart +++ b/lib/widgets/acerca/acerca_screen.dart @@ -1,8 +1,10 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/acerca/dialog/acerca_aplicacion_content.dart'; +import 'package:mi_utem/widgets/acerca/club/acerca_app.dart'; import 'package:mi_utem/widgets/acerca/club/acerca_club.dart'; import 'package:mi_utem/widgets/acerca/club/acerca_club_desarrolladores.dart'; +import 'package:mi_utem/widgets/acerca/dialog/acerca_aplicacion_content.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; class AcercaScreen extends StatelessWidget { @@ -14,22 +16,23 @@ class AcercaScreen extends StatelessWidget { Widget build(BuildContext context) => Scaffold( backgroundColor: Colors.grey[200], appBar: CustomAppBar( - title: Text("Acerca de Mi UTEM"), + title: const Text("Acerca de Mi UTEM"), ), body: SingleChildScrollView( child: Padding( - padding: EdgeInsets.all(10), + padding: const EdgeInsets.all(10), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, - children: [ - AcercaClub(), + children: [ + const AcercaClub(), Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), child: AcercaAplicacionContent(), ), - AcercaClubDesarrolladores() + const AcercaClubDesarrolladores(), + kDebugMode ? const AcercaApp() : const SizedBox(), ], ), ), diff --git a/lib/widgets/acerca/club/acerca_app.dart b/lib/widgets/acerca/club/acerca_app.dart new file mode 100644 index 0000000..14297a1 --- /dev/null +++ b/lib/widgets/acerca/club/acerca_app.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:mi_utem/services/remote_config/remote_config.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class AcercaApp extends StatelessWidget { + + const AcercaApp({ + super.key, + }); + + @override + Widget build(BuildContext context) => FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (ctx, snapshot) { + final data = snapshot.data; + if(snapshot.connectionState != ConnectionState.done || data == null){ + return const SizedBox(); + } + + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: Text("Acerca de la App", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + color: Colors.grey[700], + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 20), + MarkdownBody( + selectable: false, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.start, + p: TextStyle( + fontSize: 16, + color: Colors.grey[700], + ), + ), + data: RemoteConfigService.miutemAcercaDeLaApp.replaceAll("%version", data.version) + .replaceAll("%compilacion", data.buildNumber == data.version ? "1" : data.buildNumber) + .replaceAll("%paquete", data.packageName) + .replaceAll("%nombre", data.appName), + ), + ], + ), + ), + ); + }, + ); +} \ No newline at end of file diff --git a/lib/widgets/acerca/club/acerca_club.dart b/lib/widgets/acerca/club/acerca_club.dart index c589997..20f28bb 100644 --- a/lib/widgets/acerca/club/acerca_club.dart +++ b/lib/widgets/acerca/club/acerca_club.dart @@ -7,49 +7,48 @@ import 'acerca_club_redes.dart'; class AcercaClub extends StatelessWidget { + const AcercaClub({ + super.key, + }); + @override - Widget build(BuildContext context) { - return Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - child: Container( - padding: EdgeInsets.all(20), - child: Column( - children: [ - Container( - width: 150, - height: 150, - child: DefaultNetworkImage( - url: RemoteConfigService.clubLogo, - )), - Container(height: 20), - Text( - RemoteConfigService.clubNombre, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - color: Colors.grey[700], - fontWeight: FontWeight.bold, - ), + Widget build(BuildContext context) => Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + SizedBox( + width: 150, + height: 150, + child: DefaultNetworkImage(url: RemoteConfigService.clubLogo), + ), + const SizedBox(height: 20), + Text( + RemoteConfigService.clubNombre, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + color: Colors.grey[700], + fontWeight: FontWeight.bold, ), - Container(height: 20), - MarkdownBody( - selectable: false, - styleSheet: MarkdownStyleSheet( - textAlign: WrapAlignment.center, - p: TextStyle( - fontSize: 16, - color: Colors.grey[700], - ), + ), + const SizedBox(height: 20), + MarkdownBody( + selectable: false, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: TextStyle( + fontSize: 16, + color: Colors.grey[700], ), - data: RemoteConfigService.clubDescripcion, ), - Container(height: 20), - AcercaClubRedes(), - ], - ), + data: RemoteConfigService.clubDescripcion, + ), + const SizedBox(height: 20), + const AcercaClubRedes(), + ], ), - ); - } + ), + ); } \ No newline at end of file diff --git a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart index cdb1a1d..8076afb 100644 --- a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart +++ b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart @@ -11,103 +11,105 @@ import 'package:url_launcher/url_launcher.dart'; class AcercaClubDesarrolladores extends StatelessWidget { + const AcercaClubDesarrolladores({ + super.key, + }); + @override - Widget build(BuildContext context) { - return Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - child: Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text("Desarrolladores", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - color: Colors.grey[700], - fontWeight: FontWeight.bold, - ), + Widget build(BuildContext context) => Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text("Desarrolladores", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + color: Colors.grey[700], + fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 10), - ...jsonDecode(RemoteConfigService.miutemDesarrolladores).map((developer) => Container( - padding: EdgeInsets.symmetric(vertical: 10), - child: Row( - children: [ - ProfilePhoto( - user: User(nombres: developer['nombre'], fotoUrl: developer['fotoUrl']), - onImageTap: (context, imageProvider) { - AnalyticsService.logEvent("acerca_person_image_tap", parameters: { - "persona": developer['nombre'], - }); - Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen(imageProvider: imageProvider), fullscreenDialog: true)); - }, - ), - const SizedBox(width: 20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(developer["nombre"], - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.grey[800], - fontSize: 16, - ), + ), + const SizedBox(height: 10), + ...jsonDecode(RemoteConfigService.miutemDesarrolladores).map((developer) => Container( + padding: EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + ProfilePhoto( + user: User(nombres: developer['nombre'], fotoUrl: developer['fotoUrl']), + onImageTap: (context, imageProvider) { + AnalyticsService.logEvent("acerca_person_image_tap", parameters: { + "persona": developer['nombre'], + }); + Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen(imageProvider: imageProvider), fullscreenDialog: true)); + }, + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(developer["nombre"], + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey[800], + fontSize: 16, ), - Text(developer["rol"], - style: TextStyle( - fontSize: 16, - color: Colors.grey[700], - ), + ), + Text(developer["rol"], + style: TextStyle( + fontSize: 16, + color: Colors.grey[700], ), - const SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: (developer['redes'] as List).map((socialNetwork) => Container( - margin: const EdgeInsets.only(right: 8), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(socialNetwork["color"]), - ), - child: InkWell( - customBorder: - const CircleBorder(), - onTap: () async { - AnalyticsService.logEvent("acerca_person_social_tap", - parameters: { - "persona": developer['nombre'], - "red": socialNetwork['nombre'], - }, - ); - await launchUrl(Uri.parse(socialNetwork["url"])); - }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: const BoxDecoration(shape: BoxShape.circle), - child: Icon( - IconDataBrands(socialNetwork["icono"]), - size: 15, - color: Colors.white, - ), + ), + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: (developer['redes'] as List).map((socialNetwork) => Container( + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(socialNetwork["color"]), + ), + child: InkWell( + customBorder: + const CircleBorder(), + onTap: () async { + AnalyticsService.logEvent("acerca_person_social_tap", + parameters: { + "persona": developer['nombre'], + "red": socialNetwork['nombre'], + }, + ); + await launchUrl(Uri.parse(socialNetwork["url"])); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: const BoxDecoration(shape: BoxShape.circle), + child: Icon( + IconDataBrands(socialNetwork["icono"]), + size: 15, + color: Colors.white, ), ), - )).toList(), - ), - ], - ), + ), + )).toList(), + ), + ], ), - ], - ), - )).toList() - ], - ), + ), + ], + ), + )).toList() + ], ), - ); - } + ), + ); } \ No newline at end of file diff --git a/lib/widgets/acerca/club/acerca_club_redes.dart b/lib/widgets/acerca/club/acerca_club_redes.dart index 6049ae0..0482ec2 100644 --- a/lib/widgets/acerca/club/acerca_club_redes.dart +++ b/lib/widgets/acerca/club/acerca_club_redes.dart @@ -8,44 +8,37 @@ import 'package:url_launcher/url_launcher.dart'; class AcercaClubRedes extends StatelessWidget { + const AcercaClubRedes({ + super.key, + }); @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: jsonDecode(RemoteConfigService.clubRedes) - .map( - (red) => Container( - margin: EdgeInsets.symmetric(horizontal: 5), - decoration: new BoxDecoration( - shape: BoxShape.circle, - color: Color(red["color"]), - ), - child: InkWell( - customBorder: CircleBorder(), - onTap: () async { - AnalyticsService.logEvent("acerca_club_social_tap", - parameters: { - "red": red['nombre'], - }, - ); - await launchUrl(Uri.parse(red["url"])); - }, - child: Container( - padding: const EdgeInsets.all(10), - decoration: new BoxDecoration( - shape: BoxShape.circle, - ), - child: Icon( - IconDataBrands(red["icono"]), - size: 20, - color: Colors.white, - ), - ), - ), + Widget build(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: (jsonDecode(RemoteConfigService.clubRedes) as List).map((red) => Container( + margin: EdgeInsets.symmetric(horizontal: 5), + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Color(red["color"]), + ), + child: InkWell( + customBorder: CircleBorder(), + onTap: () async { + AnalyticsService.logEvent("acerca_club_social_tap", parameters: { + "red": red['nombre'], + }); + await launchUrl(Uri.parse(red["url"])); + }, + child: Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration(shape: BoxShape.circle), + child: Icon(IconDataBrands(red["icono"]), + size: 20, + color: Colors.white, ), - ).toList() - ); - } + ), + ), + )).toList(), + ); } \ No newline at end of file From 719921ec9a94e6e11e8bb39b4a4c603539bbc6a3 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 15:58:30 -0400 Subject: [PATCH 111/194] =?UTF-8?q?patch:=20se=20agrega=20obtenci=C3=B3n?= =?UTF-8?q?=20e=20incremento=20de=20version=20autom=C3=A1tica=20en=20fastl?= =?UTF-8?q?ane=20desde=20pubspec.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- fastlane/Fastfile | 47 +++++++++++++++++++++++++++++++++++++++++++++-- pubspec.yaml | 2 +- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e9c5e71..fe08190 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -95,9 +95,39 @@ platform :mobile do end end + desc "Lee el archivo ../pubspec.yaml, obtiene la version y su numero de compilación" + lane :get_pubspec_version do + pubspec = File.read("../pubspec.yaml") + version = pubspec.match(/version: (.+)/)[1] + build_name = version.match(/(\d+\.\d+\.\d+)/)[1] + build_number = ((version.match(/(\d+)$/)[1]).to_i + 1).to_s + + { + "build_name" => build_name, + "build_number" => build_number + } + end + + desc "Incrementa el numero de compilación en el archivo ../pubspec.yaml" + lane :increment_pubspec_build_number do + version = get_pubspec_version() + build_number = version["build_number"] + build_name = version["build_name"] + + puts "Incrementando el numero de compilación en el archivo ../pubspec.yaml" + new_version = "version: #{build_name}+#{build_number}" + + pubspec = File.read("../pubspec.yaml") + pubspec = pubspec.gsub(/version: (.+)/, new_version) + File.write("../pubspec.yaml", pubspec) + + puts "Numero de compilación incrementado en el archivo ../pubspec.yaml" + end + lane :upload do |options| - build_number = options[:build_number] - build_name = options[:build_name] + version = get_pubspec_version() + build_number = version["build_number"] # Obtiene la version del pubspec.yaml + build_name = version["build_name"] # Obtiene el numero de compilación del pubspec.yaml changelog = read_changelog emojified_changelog = emojify_changelog changelog = translate_changelog(changelog: changelog) @@ -112,6 +142,16 @@ platform :mobile do skip_slack = false is_ci = false + puts "Construyendo app con version #{build_name} (#{build_number})" + + if options[:build_number] + build_number = options[:build_number] + end + + if options[:build_name] + build_name = options[:build_name] + end + if options[:type] == "release" || options[:type] == "beta" type = options[:type] end @@ -217,5 +257,8 @@ platform :mobile do }, ) end + + puts "App construida y publicada con éxito" + increment_pubspec_build_number() end end \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index a4c60dc..46ce686 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0 +version: 3.0.0+103 environment: sdk: ">=2.17.0 <3.0.0" From fed51955d4ec8cfeb0733c57110a591f20becab2 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 17:11:49 -0400 Subject: [PATCH 112/194] =?UTF-8?q?patch:=20arreglado=20readme=20con=20ins?= =?UTF-8?q?trucciones=20de=20compilaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cc6401e..66b9cb1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Aplicación multiplataforma hecha por estudiantes de la [Universidad Tecnológica Metropolitana de Chile](https://www.utem.cl/) enfocada en adaptar la [plataforma académica Mi.UTEM](https://mi.utem.cl/) de la institución a dispositivos móviles. ## Requisitos técnicos -- Flutter 3.7.12 +- Flutter 3.7.12 (te recomendamos utilizar [fvm](https://fvm.app) para facilitar la administración de versiones) - macOS + XCode (para compilar en iOS) - Android Studio (para compilar en Android) - ruby 3.3.0 (para compilar y subir app a App Store y Google Play) @@ -12,16 +12,10 @@ Aplicación multiplataforma hecha por estudiantes de la [Universidad Tecnológic |-- lib | |-- config (Configuración de la aplicación) | |-- controllers (Controladores de la aplicación, para procesar datos de una vista especifica) -| | |-- implementations (Implementaciones de controladores) -| | |-- interfaces (Interfaces de controladores) | |-- models (Modelos de datos) | |-- repositories (Repositorios de datos, para obtener datos desde la API) -| | |-- implementations (Implementaciones de repositorios) -| | |-- interfaces (Interfaces de repositorios) | |-- screens (Pantallas de la aplicación) | |-- services (Servicios de la aplicación, maneja y procesa los datos de repositorios) -| | |-- implementations (Implementaciones de servicios) -| | |-- interfaces (Interfaces de servicios) | |-- themes (Temas de la aplicación) | |-- utils (Utilidades de la aplicación) | |-- widgets (Widgets de la aplicación) @@ -29,6 +23,47 @@ Aplicación multiplataforma hecha por estudiantes de la [Universidad Tecnológic |-- service_manager.dart (Registra los servicios de la aplicación) ``` +### Construcción de la app + +Para construir la app utilizamos fastlane. +Comienza revisando la documentación de Fastlane para [iOS](https://docs.fastlane.tools/getting-started/ios/setup/) y [Android](https://docs.fastlane.tools/getting-started/android/setup/). +Utilizaremos la instalación de bundler para instalar las dependencias y ejecutar fastlane. + +
+Instalación de Bundler + +(Se asume que tienes instalado Ruby 3.3.0 o superior) +```bash +gem install bundler +``` +
+ +
+Instalación de las Dependencias + +Una vez instalado ejecutarás este comando para instalar las dependencias: +```bash +bundle install +``` +
+ +
+Subiendo una nueva Actualización + +Para construir la app ejecuta este comando: +```bash +bundle exec fastlane upload type:"beta" skip_ios:false skip_android:false skip_clean:true skip_cocoapods:true skip_git_push:true skip_slack:true is_ci:true +``` +Este comando subirá el archivo binario a AppStore y Google Play. Esto hacen las variables: +- `skip_ios`: Si es `true` no subirá la app a AppStore, si es `false` subirá la app a AppStore. +- `skip_android`: Si es `true` no subirá la app a Google Play, si es `false` subirá la app a Google Play. +- `skip_clean`: Si es `true` no limpiará los archivos temporales, si es `false` limpiará los archivos temporales (se recomienda utilizar para construcciones en producción y evitar problemas con archivos guardados en caché). +- `skip_cocoapods`: Si es `true` no instalará las dependencias de CocoaPods, si es `false` instalará las dependencias de CocoaPods (se recomienda utilizar para construcciones en producción y evitar problemas con dependencias de CocoaPods). +- `skip_git_push`: Si es `true` no creará una nueva etiqueta, si es `false` creará una nueva etiqueta con la lista de cambios formateada. +- `skip_slack`: Si es `true` no enviará un mensaje a Slack, si es `false` enviará un mensaje a Slack. +- `is_ci`: Si es `true` se ejecutará en modo de integración continua, es decir, no esperará a ver si la versión aparece en AppStore o Google Play, si es `false` esperará a ver si la versión aparece en AppStore o Google Play. Además, al ser `true` también evitará editar el repositorio de Match (el cual contiene los certificados de distribución de la app). +
+ ## Créditos Este proyecto fue creado por el Club de Desarrollo Experimental (ExDev) de la Universidad Tecnológica Metropolitana y es mantenido por los propios estudiantes con el apoyo del equipo de SISEI. Mira los perfiles que han contribuido a este proyecto: From 03d7a48feb78b0617a0cf4563633e679d5c45ead Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 17:12:12 -0400 Subject: [PATCH 113/194] =?UTF-8?q?patch:=20incrementado=20numero=20de=20c?= =?UTF-8?q?ompilaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 46ce686..3d345f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+103 +version: 3.0.0+104 environment: sdk: ">=2.17.0 <3.0.0" From 70e82b799ddb0489f2a80689282100e0caec77b8 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 17:52:01 -0400 Subject: [PATCH 114/194] patch: eliminado modal de permitir notificaciones a favor de modal nativo. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Para reducir la redundancia de la solicitud de enviar notificaciones, se eliminó el modal de permitir notificaciones, ya que se puede customizar el mensaje en iOS. Además, para android (porque no se puede cambiar el diálogo) basta con el mensaje de la pantalla de Onboarding. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Runner/Info-dev.plist | 2 + ios/Runner/Info-prod.plist | 2 + lib/models/asignaturas/asignatura.dart | 5 ++ .../onboarding/notifications_screen.dart | 6 +-- lib/services/grades_service.dart | 11 +++-- lib/services/notification_service.dart | 46 ++++++------------- 6 files changed, 32 insertions(+), 40 deletions(-) diff --git a/ios/Runner/Info-dev.plist b/ios/Runner/Info-dev.plist index 5491837..81f1a81 100644 --- a/ios/Runner/Info-dev.plist +++ b/ios/Runner/Info-dev.plist @@ -39,6 +39,8 @@ Mi UTEM quiere acceder a tu cámara para que puedas subir contenido NSPhotoLibraryUsageDescription Mi UTEM quiere acceder a tus fotos para que puedas subir contenido + NSUserNotificationsUsageDescription + Queremos avisarte cuando existan cambios en tus notas, o sobre otras novedades. UIBackgroundModes fetch diff --git a/ios/Runner/Info-prod.plist b/ios/Runner/Info-prod.plist index e0cbd33..9ee5183 100644 --- a/ios/Runner/Info-prod.plist +++ b/ios/Runner/Info-prod.plist @@ -39,6 +39,8 @@ Mi UTEM quiere acceder a tu cámara para que puedas subir contenido NSPhotoLibraryUsageDescription Mi UTEM quiere acceder a tus fotos para que puedas subir contenido + NSUserNotificationsUsageDescription + Queremos avisarte cuando existan cambios en tus notas, o sobre otras novedades. UIBackgroundModes fetch diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart index 7e6ba53..b882eb5 100644 --- a/lib/models/asignaturas/asignatura.dart +++ b/lib/models/asignaturas/asignatura.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asistencia.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; @@ -90,6 +92,9 @@ class Asignatura { 'tipoSala': tipoSala, }; + @override + String toString() => jsonEncode(toJson()); + Asignatura copyWith({ String? id, String? nombre, diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart index c496455..63eaf18 100644 --- a/lib/screens/onboarding/notifications_screen.dart +++ b/lib/screens/onboarding/notifications_screen.dart @@ -56,7 +56,7 @@ class _NotificationsScreenState extends State { ), textAlign: TextAlign.center, ), - Text("Olvídate sobre revisar la app a cada rato, ${_hasAllowedNotifications ? "te avisaremos" : "permítenos avisarte"} cuando haya novedades!", + Text("Olvídate sobre revisar la app a cada rato, ${_hasAllowedNotifications ? "te avisaremos" : "permítenos avisarte"} cuando tus notas cambien, o existan otras novedades!", style: TextStyle( color: Colors.white, fontSize: 20, @@ -65,9 +65,7 @@ class _NotificationsScreenState extends State { ), const SizedBox(height: 20), if(!_hasAllowedNotifications) FilledButton( - onPressed: () { - NotificationService.requestUserPermissionIfNecessary(context).then((value) => setState(() => _hasAllowedNotifications = value)); - }, + onPressed: () async => await NotificationService.requestUserPermissionIfNecessary(context).then((value) => setState(() => _hasAllowedNotifications = value)), style: ElevatedButton.styleFrom( backgroundColor: MainTheme.primaryDarkColor, padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), diff --git a/lib/services/grades_service.dart b/lib/services/grades_service.dart index 4d06076..1f79a48 100644 --- a/lib/services/grades_service.dart +++ b/lib/services/grades_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:get/get.dart'; import 'package:mi_utem/config/secure_storage.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; +import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; import 'package:mi_utem/repositories/asignaturas_repository.dart'; import 'package:mi_utem/repositories/grades_repository.dart'; @@ -39,9 +40,10 @@ class GradesService { return {}; } - final carreraId = (await Get.find().getCarreras())?.id; + final carrera = await Get.find().getCarreras(); + final carreraId = carrera?.id; - if(carreraId == null) { + if(carrera == null || carreraId == null) { return {}; } @@ -70,7 +72,7 @@ class GradesService { final changeType = await this.compareGrades(asignaturaId, updatedGrades); await this.saveGrades(asignaturaId, updatedGrades); - this._notifyGradeUpdate(asignatura, changeType); + this._notifyGradeUpdate(carrera, asignatura, changeType); response[asignaturaId] = changeType; } @@ -167,7 +169,7 @@ class GradesService { bool _hasAGradeWithValue(Grades asignatura) => asignatura.notasParciales.any((it) => it.nota != null); - void _notifyGradeUpdate(Asignatura asignatura, GradeChangeType changeType) { + void _notifyGradeUpdate(Carrera carrera, Asignatura asignatura, GradeChangeType changeType) { final name = asignatura.nombre ?? asignatura.codigo; String? title; @@ -204,6 +206,7 @@ class GradesService { title: title, body: body, asignatura: asignatura, + carrera: carrera, ); } else if(changeType != GradeChangeType.noChange) { Sentry.captureMessage("Asignatura ha cambiado pero no notificado", diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 27e30a8..4b11834 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,11 +1,9 @@ -import 'dart:convert'; - import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:awesome_notifications_fcm/awesome_notifications_fcm.dart'; import 'package:flutter/material.dart'; import 'package:mi_utem/controllers/notification_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/widgets/dialogs/custom_alert_dialog.dart'; +import 'package:mi_utem/models/carrera.dart'; class NotificationService { static const announcementsChannelKey = 'announcements_channel'; @@ -70,22 +68,8 @@ class NotificationService { static Future requestUserPermissionIfNecessary(BuildContext context) async { bool isAllowed = await hasAllowedNotifications(); if (!isAllowed) { - isAllowed = await showDialog(context: context, builder: (ctx) => CustomAlertDialog( - titulo: "Activa las notificaciones", - emoji: "🔔", - descripcion: "Necesitamos tu permiso para poder enviarte notificaciones. Nada de spam, lo prometemos.", - onCancelar: () async { - bool isAllowed = await notifications.isNotificationAllowed(); - Navigator.pop(ctx, isAllowed); - }, - onConfirmar: () async { - await notifications.requestPermissionToSendNotifications(); - bool isAllowed = await notifications.isNotificationAllowed(); - Navigator.pop(ctx, isAllowed); - }, - cancelarTextoBoton: "No permitir", - confirmarTextoBoton: "Permitir", - )); + await notifications.requestPermissionToSendNotifications(); + isAllowed = await notifications.isNotificationAllowed(); } return isAllowed; } @@ -93,19 +77,17 @@ class NotificationService { static void showGradeChangeNotification({ required String title, required String body, + required Carrera carrera, required Asignatura asignatura, - }) { - final Map payload = { + }) => notifications.createNotification(content: NotificationContent( + id: asignatura.hashCode, + channelKey: gradeChangesChannelKey, + title: title, + body: body, + payload: { 'type': 'grade_change', - 'asignatura': jsonEncode(asignatura.toJson()), - }; - - notifications.createNotification(content: NotificationContent( - id: asignatura.hashCode, - channelKey: gradeChangesChannelKey, - title: title, - body: body, - payload: payload, - )); - } + 'asignatura': asignatura.toString(), + 'carrera': carrera.toString(), + }, + )); } From 958f40caa6d58eaa8112c40faf83f695097522b1 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 22 May 2024 17:53:22 -0400 Subject: [PATCH 115/194] patch: se modifica el changelog Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- CHANGELOG.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74dd663..123a30d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,24 +28,28 @@ Tipos de cambios ## [Unreleased] -## [3.0.0] - 2024-04-04Z - ### Added - - Apodo para personalizar la aplicación. +- Se agrega onboarding (con configuración de apodo, solicitud de permiso de notificaciones y bienvenida a la app). - Lista de estudiantes al resumen de asignatura. - Vista previa de estudiantes y profesores. -- Acceso rápido a la asignatura desde el horario -- Vista previa de la asignatura al mantenerla presionada en horario +- Se agrega modal de vista previa de la asignatura desde horario (al mantener presionado un bloque). +- Se agrega navegación hacia la asignatura desde el horario al presionar un bloque. +- Botón para limpiar las notas en la calculadora. +- Sección de `Acerca de la App` en modo depuración. ### Changed - - Se actualizaron algunas dependencias. -- Se mejora el rendimiento de la aplicación. +- Se ordenan las clases y widgets de la app. +- Se optimiza el código y la aplicación en general. +- Se separan clases en formato de repositorios, servicios y controladores. +- Se mejora la documentación de algunos archivos y métodos. +- Ahora se cargan las noticias de la página [noticias.utem.cl](https://noticias.utem.cl). +- Se utiliza navegación de flutter nativa (Usando [`Navigator`](https://docs.flutter.dev/cookbook/navigation/navigation-basics) en lugar de rutas nombradas). ### Removed - -- Lista de estudiantes de la pestaña de asignatura. +- Se elimina pestaña de estudiantes del detalle de asignatura (a favor del botón de lista de estudiantes en el resumen). +- Modal de Permitir Notificaciones a favor de modal nativa del sistema. Solo se editará el mensaje de notificaciones, ya que es redundante el botón de permitir y cancelar. ## [2.11.9] - 2023-10-11Z From e65005156b78517f6e43b5ff6b7e5fb6f8515370 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 23 May 2024 10:48:15 -0400 Subject: [PATCH 116/194] =?UTF-8?q?patch:=20actualizado=20sentry=20y=20n?= =?UTF-8?q?=C3=BAmero=20de=20compilaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.lock | 8 ++++---- pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 089a8a0..6216266 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1261,18 +1261,18 @@ packages: dependency: transitive description: name: sentry - sha256: d2ee9c850d876d285f22e2e662f400ec2438df9939fe4acd5d780df9841794ce + sha256: fd1fbfe860c05f5c52820ec4dbf2b6473789e83ead26cfc18bca4fe80bf3f008 url: "https://pub.dev" source: hosted - version: "7.16.1" + version: "8.2.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "5b428c189c825f16fb14e9166529043f06b965d5b59bfc3a1415e39c082398c0" + sha256: c64f0aec5332bec87083b61514d1b6b29e435b9045d03ce1575861192b9a5680 url: "https://pub.dev" source: hosted - version: "7.16.1" + version: "8.2.0" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d345f6..8447891 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+104 +version: 3.0.0+105 environment: sdk: ">=2.17.0 <3.0.0" @@ -52,7 +52,7 @@ dependencies: qr_flutter: ^4.0.0 recase: ^4.0.0 responsive_framework: ^0.2.0 - sentry_flutter: ^7.12.0 + sentry_flutter: ^8.2.0 share_plus: ^7.2.1 simple_gesture_detector: ^0.2.0 url_launcher: ^6.1.7 From 0686f84db55e5afe2d596e862820a95b0346c554 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 23 May 2024 10:49:01 -0400 Subject: [PATCH 117/194] =?UTF-8?q?patch:=20se=20configuran=20algunos=20se?= =?UTF-8?q?rvicios=20en=20formato=20'fenix'=20que=20har=C3=A1=20que=20se?= =?UTF-8?q?=20eliminen=20si=20no=20est=C3=A1n=20en=20uso.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/service_manager.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/service_manager.dart b/lib/service_manager.dart index 5b15611..d3f50ca 100644 --- a/lib/service_manager.dart +++ b/lib/service_manager.dart @@ -21,13 +21,12 @@ Future registerServices() async { /* Repositorios (Para conectarse a la REST Api o servicios locales) */ Get.lazyPut(() => AuthRepository()); Get.lazyPut(() => AsignaturasRepository()); - Get.lazyPut(() => CredentialsRepository()); + Get.lazyPut(() => CredentialsRepository(), fenix: true); Get.lazyPut(() => CarrerasRepository()); Get.lazyPut(() => GradesRepository()); - Get.lazyPut(() => PermisoIngresoRepository()); + Get.lazyPut(() => PermisoIngresoRepository(), fenix: true); Get.lazyPut(() => NoticiasRepository()); - Get.lazyPut(() => HorarioRepository()); - + Get.lazyPut(() => HorarioRepository(), fenix: true); /* Servicios (Para procesar datos REST) */ Get.lazyPut(() => AuthService()); @@ -35,8 +34,8 @@ Future registerServices() async { Get.lazyPut(() => GradesService()); /* Controladores (Para procesar datos de interfaz) */ - Get.lazyPut(() => HorarioController()); - Get.lazyPut(() => CalculatorController()); + Get.lazyPut(() => HorarioController(), fenix: true); + Get.lazyPut(() => CalculatorController(), fenix: true); final credentialsRepository = Get.find(); if(!await credentialsRepository.hasCredentials()) { From c4ca206be77b2ac2f73035fc967d3de2df1ac1e5 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 23 May 2024 10:49:29 -0400 Subject: [PATCH 118/194] =?UTF-8?q?patch:=20se=20habilitan=20m=C3=A9tricas?= =?UTF-8?q?=20y=20jerarqu=C3=ADa=20de=20vistas=20a=20sentry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/main.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index d3021be..e5c61c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,7 +33,9 @@ void main() async { options.dsn = sentryDsn; options.attachScreenshot = true; options.tracesSampleRate = 1.0; - }, appRunner: () => runApp(MiUtem())); + options.enableMetrics = true; + options.attachViewHierarchy = true; + }, appRunner: () => runApp(SentryWidget(child: MiUtem()))); } class MiUtem extends StatefulWidget { From f21b964e70fb977864719359df6718e36277054c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 23 May 2024 10:49:52 -0400 Subject: [PATCH 119/194] =?UTF-8?q?patch:=20se=20agregan=20datos=20a=20sen?= =?UTF-8?q?try=20para=20la=20identificaci=C3=B3n=20del=20usuario,=20adem?= =?UTF-8?q?=C3=A1s=20de=20m=C3=A9tricas.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/analytics_service.dart | 44 +++++++++++++++++++---------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart index c317491..b88a73f 100644 --- a/lib/services/analytics_service.dart +++ b/lib/services/analytics_service.dart @@ -1,7 +1,11 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/utils/string_utils.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class AnalyticsService { @@ -24,17 +28,20 @@ class AnalyticsService { await FlutterUxcam.setUserProperty("last_name", user.apellidos!); } - await Sentry.configureScope((scope) => scope.setUser( - SentryUser( - id: user.correoUtem, - email: user.correoPersonal, - name: user.nombreCompleto, - data: { - "rut": user.rut?.toString(), - }, - ipAddress: "{{auto}}", - ), - )); + final correoUtem = user.correoUtem; + + await Sentry.configureScope((scope) => scope.setUser(SentryUser( + id: correoUtem, + email: user.correoPersonal, + name: capitalize(user.nombreCompleto), + data: { + "rut": user.rut?.toString(), + "usuario": user.username, + "backend_uid": correoUtem != null ? md5.convert(utf8.encode(correoUtem)) : "desconocido", + "perfiles": user.perfiles, + }, + ipAddress: "{{auto}}", + ))); } static Future setCarreraToUser(Carrera carrera) async { @@ -47,6 +54,14 @@ class AnalyticsService { await FirebaseAnalytics.instance.setUserProperty(name: "estadoCarreraActiva", value: carrera.estado!); await FlutterUxcam.setUserProperty("estadoCarreraActiva", carrera.estado!); } + + await Sentry.configureScope((scope) => scope.setUser(scope.user?.copyWith( + data: { + ...?scope.user?.data, + "carrera": carrera.nombre, + "estado_carrera": carrera.estado, + }, + ))); } static Future removeUser() async { @@ -56,15 +71,14 @@ class AnalyticsService { } static Future logEvent(String name, {Map? parameters}) async { - await FirebaseAnalytics.instance.logEvent( - name: name, - parameters: parameters, - ); + await FirebaseAnalytics.instance.logEvent(name: name, parameters: parameters); if (parameters != null) { await FlutterUxcam.logEventWithProperties(name, parameters); } else { await FlutterUxcam.logEvent(name); } + + Sentry.metrics().increment(name, tags: parameters?.map((key, value) => MapEntry(key, value.toString()))); } } From 7b96e97dd35af293d8ec113d4e85e71af7df0e4e Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 23 May 2024 10:50:01 -0400 Subject: [PATCH 120/194] patch: actualizado sentry en ios Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- ios/Podfile.lock | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 481bdfa..f9d141f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -906,13 +906,11 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) - - Sentry/HybridSDK (8.20.0): - - SentryPrivate (= 8.20.0) - - sentry_flutter (0.0.1): + - Sentry/HybridSDK (8.25.2) + - sentry_flutter (8.2.0): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.20.0) - - SentryPrivate (8.20.0) + - Sentry/HybridSDK (= 8.25.2) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -985,7 +983,6 @@ SPEC REPOS: - PromisesObjC - PromisesSwift - Sentry - - SentryPrivate - UXCam EXTERNAL SOURCES: @@ -1095,9 +1092,8 @@ SPEC CHECKSUMS: permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - Sentry: a8d7b373b9f9868442b02a0c425192f693103cbf - sentry_flutter: 03e7660857a8cdb236e71456a7e8447b65c8a788 - SentryPrivate: 006b24af16828441f70e2ab6adf241bd0a8ad130 + Sentry: 51b056d96914a741f63eca774d118678b1eb05a1 + sentry_flutter: e8397d13e297a5d4b6be8a752e33140b21c5cc97 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a From 10fd16378a7a0ff342a3bffd04befe11eb737f7f Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 23 May 2024 10:50:31 -0400 Subject: [PATCH 121/194] =?UTF-8?q?patch:=20se=20actualiza=20el=20controla?= =?UTF-8?q?dor=20de=20calculadora=20a=20su=20versi=C3=B3n=20original=20inc?= =?UTF-8?q?luyendo=20algunos=20arreglos=20del=20nuevo=20sistema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/controllers/calculator_controller.dart | 373 ++++++------------ .../nota_examen_display_widget.dart | 8 +- .../nota_final_display_widget.dart | 2 +- .../calculadora_notas/nota_list_item.dart | 4 +- .../nota_presentacion_display_widget.dart | 2 +- 5 files changed, 138 insertions(+), 251 deletions(-) diff --git a/lib/controllers/calculator_controller.dart b/lib/controllers/calculator_controller.dart index 92da48b..259a26f 100644 --- a/lib/controllers/calculator_controller.dart +++ b/lib/controllers/calculator_controller.dart @@ -4,314 +4,201 @@ import 'package:mi_utem/models/evaluacion/evaluacion.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; class CalculatorController { - - /* Porcentaje máximo de todas las notas */ static const maxPercentage = 100; - - /* Nota máxima */ static const maxGrade = 7; - - /* Nota mínima para presentarse al examen */ static const minimumGradeForExam = 2.95; - - /* Nota para pasar el ramo */ static const passingGrade = 3.95; - - /* Porcentaje de la nota del examen */ static const examFinalWeight = 0.4; - - /* Porcentaje de la nota de presentación */ static const presentationFinalWeight = 1 - examFinalWeight; - /* Notas parciales */ - RxList partialGrades = [].obs; - - /* Controlador de texto para los porcentajes con máscara (para autocompletar formato) */ - RxList percentageTextFieldControllers = [].obs; - - /* Controlador de texto para las notas con máscara (para autocompletar formato) */ - RxList gradeTextFieldControllers = [].obs; - - /* Nota del examen */ - Rx examGrade = Rx(null); - - /* Controlador de texto para la nota del examen con máscara (para autocompletar formato) */ - Rx examGradeTextFieldController = MaskedTextController(mask: "0.0").obs; - - RxBool freeEditable = false.obs; - - Rx calculatedFinalGrade = Rx(null); - - Rx calculatedPresentationGrade = Rx(null); - - Rx amountOfPartialGradesWithoutGrade = 0.obs; - - Rx amountOfPartialGradesWithoutPercentage = 0.obs; - - RxBool hasMissingPartialGrade = false.obs; - - RxBool canTakeExam = false.obs; - - Rx minimumRequiredExamGrade = Rx(null); - - RxDouble percentageOfPartialGrades = 0.0.obs; - - RxDouble missingPercentage = 0.0.obs; - - RxBool hasMissingPercentage = false.obs; - - Rx suggestedPercentage = Rx(null); - - Rx suggestedPresentationGrade = Rx(null); - - RxDouble percentageWithoutGrade = 0.0.obs; - - RxBool hasCorrectPercentage = false.obs; - - Rx suggestedGrade = Rx(null); - - void updateWithGrades(Grades grades) { - partialGrades.clear(); - percentageTextFieldControllers.clear(); - gradeTextFieldControllers.clear(); - - for(final grade in grades.notasParciales) { - addGrade(IEvaluacion.fromRemote(grade)); + final partialGrades = [].obs; + final percentageTextFieldControllers = [].obs; + final gradeTextFieldControllers = [].obs; + final examGrade = Rxn(); + final examGradeTextFieldController = MaskedTextController(mask: "0.0"); + final freeEditable = false.obs; + + double? get calculatedFinalGrade { + if (calculatedPresentationGrade != null) { + if (examGrade.value != null) { + final weightedFinalGrade = + calculatedPresentationGrade! * presentationFinalWeight; + final weightedExamGrade = examGrade.value! * examFinalWeight; + + return weightedFinalGrade + weightedExamGrade; + } + return calculatedPresentationGrade; } - - setExamGrade(grades.notaExamen); + return null; } - void updateGradeAt(int index, IEvaluacion updatedGrade) { - final grade = partialGrades[index]; - if(!(grade.editable || freeEditable.value)) { - return; + double? get calculatedPresentationGrade { + double presentationGrade = 0; + for (var partialGrade in partialGrades) { + final weight = (partialGrade.porcentaje ?? 0) / maxPercentage; + presentationGrade += (partialGrade.nota ?? 0) * weight; } - partialGrades[index] = updatedGrade; + return presentationGrade != 0 ? presentationGrade : null; + } - _updateCalculations(); - if(hasMissingPartialGrade.value) { - clearExamGrade(); - } + int get numOfPartialGradesWithoutGrade { + return partialGrades + .where((partialGrade) => partialGrade.nota == null) + .length; } - void addGrade(IEvaluacion grade) { - partialGrades.add(grade); - percentageTextFieldControllers.add(MaskedTextController( - mask: "000", - text: grade.porcentaje?.toStringAsFixed(0) ?? "", - )); - gradeTextFieldControllers.add(MaskedTextController( - mask: "0.0", - text: grade.nota?.toStringAsFixed(2) ?? "", - )); - _updateCalculations(); + bool get hasMissingPartialGrade { + return numOfPartialGradesWithoutGrade > 0; } - void clearGrades() { - partialGrades.clear(); - percentageTextFieldControllers.clear(); - gradeTextFieldControllers.clear(); - _updateCalculations(); + bool get canTakeExam { + return !hasMissingPartialGrade && + calculatedPresentationGrade != null && + calculatedPresentationGrade! >= minimumGradeForExam && + calculatedPresentationGrade! < passingGrade; } - void removeGradeAt(int index) { - final grade = partialGrades[index]; - if(!(grade.editable || freeEditable.value)) { - return; + double? get minimumRequiredExamGrade { + if (canTakeExam) { + final weightedPresentationGrade = + calculatedPresentationGrade! * presentationFinalWeight; + return (passingGrade - weightedPresentationGrade) / examFinalWeight; } - partialGrades.removeAt(index); - percentageTextFieldControllers.removeAt(index); - gradeTextFieldControllers.removeAt(index); - _updateCalculations(); + return null; } - void makeEditable() { - freeEditable.value = true; - _updateCalculations(); + double get percentageOfPartialGrades { + double percentage = 0; + for (var partialGrade in partialGrades) { + percentage += (partialGrade.porcentaje ?? 0); + } + return percentage; } - void makeNonEditable() { - freeEditable.value = false; - _updateCalculations(); + double get missingPercentage { + return maxPercentage - percentageOfPartialGrades; } - void clearExamGrade() { - examGrade.value = null; - examGradeTextFieldController.value.updateText(""); - _updateCalculations(); + int get numOfPartialGradesWithoutPercentage { + return partialGrades + .where((partialGrade) => partialGrade.porcentaje == null) + .length; } - void setExamGrade(num? grade, { bool updateTextController = true }) { - examGrade.value = grade?.toDouble(); - if(updateTextController) { - examGradeTextFieldController.value.updateText(grade?.toStringAsFixed(2) ?? "--"); - } - _updateCalculations(); + bool get hasMissingPercentage { + return numOfPartialGradesWithoutPercentage > 0; } - void _updateCalculations() { - _calculateFinalGrade(); - _calculatePresentationGrade(); - _calculateAmountOfPartialGradesWithoutGrade(); - _calculateAmountOfPartialGradesWithoutPercentage(); - _checkHasMissingPartialGrade(); - _checkCanTakeExam(); - _calculateMinimumRequiredExamGrade(); - _calculatePercentageOfPartialGrades(); - _calculateMissingPercentage(); - _checkMissingPercentage(); - _calculateSuggestedPercentage(); - _calculateSuggestedPresentationGrade(); - _calculatePercentageWithoutGrade(); - _checkCorrectPercentage(); - _calculateSuggestedGrade(); + double? get suggestedPercentage { + final percentage = missingPercentage / numOfPartialGradesWithoutPercentage; + return 0 <= percentage && percentage <= maxPercentage ? percentage : null; } - void _calculateFinalGrade() { - _calculatePresentationGrade(); - final calculatedPresentationGrade = this.calculatedPresentationGrade.value; - if (calculatedPresentationGrade == null) { - calculatedFinalGrade.value = null; - return; + double? get suggestedPresentationGrade { + double presentationGrade = 0; + for (var partialGrade in partialGrades) { + final weight = (partialGrade.porcentaje ?? (suggestedPercentage ?? 0)) / + maxPercentage; + presentationGrade += (partialGrade.nota ?? 0) * weight; } + return 0 <= presentationGrade && presentationGrade <= maxGrade + ? presentationGrade + : null; + } - final examGradeValue = examGrade.value; - if(examGradeValue == null) { - calculatedFinalGrade.value = calculatedPresentationGrade; - return; + double get percentageWithoutGrade { + double percentage = 0; + for (var partialGrade in partialGrades) { + if (partialGrade.nota == null) { + percentage += (partialGrade.porcentaje ?? (suggestedPercentage ?? 0)); + } } + return percentage; + } - final weightedFinalGrade = calculatedPresentationGrade * presentationFinalWeight; - final weightedExamGrade = examGradeValue * examFinalWeight; - - calculatedFinalGrade.value = weightedFinalGrade + weightedExamGrade; + bool get hasCorrectPercentage { + return percentageOfPartialGrades == maxPercentage; } - void _calculatePresentationGrade() { - double presentationGrade = 0; - for (final partialGrade in partialGrades) { - final weight = (partialGrade.porcentaje ?? 0) / maxPercentage; - presentationGrade += (partialGrade.nota ?? 0) * weight; + double? get suggestedGrade { + if (hasMissingPartialGrade && percentageWithoutGrade > 0) { + final weightOfMissingGrades = percentageWithoutGrade / maxPercentage; + final requiredGradeValue = + passingGrade - (suggestedPresentationGrade ?? 0); + final missingGradesValue = requiredGradeValue / weightOfMissingGrades; + return missingGradesValue; } - - calculatedPresentationGrade.value = presentationGrade != 0 ? presentationGrade : null; + return null; } - void _calculateAmountOfPartialGradesWithoutGrade() => amountOfPartialGradesWithoutGrade.value = partialGrades - .where((partialGrade) => partialGrade.nota == null) - .length; + void makeEditable() { + freeEditable.value = true; + } - void _calculateAmountOfPartialGradesWithoutPercentage() => amountOfPartialGradesWithoutPercentage.value = partialGrades - .where((partialGrade) => partialGrade.porcentaje == null) - .length; + void makeNonEditable() { + freeEditable.value = false; + } - void _checkHasMissingPartialGrade() { - _calculateAmountOfPartialGradesWithoutGrade(); - hasMissingPartialGrade.value = amountOfPartialGradesWithoutGrade.value > 0; + void clearGrades() { + partialGrades.clear(); + percentageTextFieldControllers.clear(); + gradeTextFieldControllers.clear(); + clearExamGrade(); } - void _checkCanTakeExam() { - _checkHasMissingPartialGrade(); - if(hasMissingPartialGrade.value) { - canTakeExam.value = false; - return; - } + void updateWithGrades(Grades grades) { + partialGrades.clear(); + percentageTextFieldControllers.clear(); + gradeTextFieldControllers.clear(); - _calculatePresentationGrade(); - final calculatedPresentationGrade = this.calculatedPresentationGrade.value; - if(calculatedPresentationGrade == null) { - canTakeExam.value = false; - return; + for(final grade in grades.notasParciales) { + addGrade(IEvaluacion.fromRemote(grade)); } - canTakeExam.value = calculatedPresentationGrade >= minimumGradeForExam && calculatedPresentationGrade < passingGrade; + setExamGrade(grades.notaExamen); } - void _calculateMinimumRequiredExamGrade() { - _checkCanTakeExam(); - if(!canTakeExam.value) { - minimumRequiredExamGrade.value = null; - return; - } - - _calculatePresentationGrade(); - final calculatedPresentationGrade = this.calculatedPresentationGrade.value; - if(calculatedPresentationGrade == null) { - minimumRequiredExamGrade.value = null; + void updateGradeAt(int index, IEvaluacion updatedGrade) { + final grade = partialGrades[index]; + if(!(grade.editable || freeEditable.value)) { return; } - final weightedPresentationGrade = calculatedPresentationGrade * presentationFinalWeight; - minimumRequiredExamGrade.value = (passingGrade - weightedPresentationGrade) / examFinalWeight; - } + partialGrades[index] = updatedGrade; - void _calculatePercentageOfPartialGrades() { - double percentage = 0; - for (final partialGrade in partialGrades) { - percentage += (partialGrade.porcentaje ?? 0); + if (hasMissingPartialGrade) { + clearExamGrade(); } - percentageOfPartialGrades.value = percentage; - } - - void _calculateMissingPercentage() { - _calculatePercentageOfPartialGrades(); - missingPercentage.value = maxPercentage - percentageOfPartialGrades.value; - } - - void _checkMissingPercentage() { - _calculateAmountOfPartialGradesWithoutPercentage(); - hasMissingPercentage.value = amountOfPartialGradesWithoutPercentage.value > 0; - } - - void _calculateSuggestedPercentage() { - _calculateAmountOfPartialGradesWithoutPercentage(); - _calculateMissingPercentage(); - final percentage = missingPercentage.value / amountOfPartialGradesWithoutPercentage.value; - suggestedPercentage.value = 0 <= percentage && percentage <= maxPercentage ? percentage : null; } - void _calculateSuggestedPresentationGrade() { - _calculateSuggestedPercentage(); - double presentationGrade = 0; - for(final partialGrade in partialGrades) { - final weight = (partialGrade.porcentaje ?? (suggestedPercentage.value ?? 0)) / maxPercentage; - presentationGrade += (partialGrade.nota ?? 0) * weight; - } - - suggestedPresentationGrade.value = 0 <= presentationGrade && presentationGrade <= maxGrade ? presentationGrade : null; + void clearExamGrade() { + examGrade.value = null; + examGradeTextFieldController.text = ""; } - void _calculatePercentageWithoutGrade() { - _calculateSuggestedPercentage(); - double percentage = 0; - for(final partialGrade in partialGrades) { - if(partialGrade.nota == null) { - percentage += (partialGrade.porcentaje ?? (suggestedPercentage.value ?? 0)); - } + void setExamGrade(num? grade, { bool updateTextController = true }) { + examGrade.value = grade?.toDouble(); + if(updateTextController) { + examGradeTextFieldController.updateText(grade?.toDouble().toStringAsFixed(1) ?? ""); } - - percentageWithoutGrade.value = percentage; } - void _checkCorrectPercentage() { - _calculatePercentageOfPartialGrades(); - hasCorrectPercentage.value = percentageOfPartialGrades.value == maxPercentage; + void addGrade(IEvaluacion grade) { + partialGrades.add(grade); + percentageTextFieldControllers.add(MaskedTextController(mask: "000", text: grade.porcentaje?.toStringAsFixed(0) ?? "")); + gradeTextFieldControllers.add(MaskedTextController(mask: "0.0", text: grade.nota?.toStringAsFixed(1) ?? "")); } - void _calculateSuggestedGrade() { - _calculatePercentageWithoutGrade(); - final percentageWithoutGrade = this.percentageWithoutGrade.value; - if(!(hasMissingPartialGrade.value && percentageWithoutGrade > 0)) { - suggestedGrade.value = null; - return; + void removeGradeAt(int index) { + final grade = partialGrades[index]; + if (grade.editable || freeEditable.value) { + partialGrades.removeRange(index, index + 1); + percentageTextFieldControllers.removeRange(index, index + 1); + gradeTextFieldControllers.removeRange(index, index + 1); + } else { + throw Exception("No se puede eliminar una nota que está asignada"); } - - _calculateSuggestedPresentationGrade(); - final weightOfMissingGrades = percentageWithoutGrade / maxPercentage; - final requiredGradeValue = passingGrade - (suggestedPresentationGrade.value ?? 0); - suggestedGrade.value = requiredGradeValue / weightOfMissingGrades; } } \ No newline at end of file diff --git a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart index 5789bf8..caf87a2 100644 --- a/lib/widgets/calculadora_notas/nota_examen_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_examen_display_widget.dart @@ -25,13 +25,13 @@ class NotaExamenDisplayWidget extends StatelessWidget { width: 80, margin: const EdgeInsets.only(left: 15), child: Obx(() => TextField( - controller: _calculatorController.examGradeTextFieldController.value, + controller: _calculatorController.examGradeTextFieldController, textAlign: TextAlign.center, onChanged: (String value) => _calculatorController.setExamGrade(double.tryParse(value.replaceAll(",", ".")), updateTextController: false), - enabled: _calculatorController.canTakeExam.value, + enabled: _calculatorController.canTakeExam, decoration: InputDecoration( - hintText: formatoNota(_calculatorController.minimumRequiredExamGrade.value) ?? "--", - filled: !_calculatorController.canTakeExam.value, + hintText: formatoNota(_calculatorController.minimumRequiredExamGrade) ?? "--", + filled: !_calculatorController.canTakeExam, fillColor: Colors.grey.withOpacity(0.2), disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( diff --git a/lib/widgets/calculadora_notas/nota_final_display_widget.dart b/lib/widgets/calculadora_notas/nota_final_display_widget.dart index 0699443..cf83d54 100644 --- a/lib/widgets/calculadora_notas/nota_final_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_final_display_widget.dart @@ -15,7 +15,7 @@ class NotaFinalDisplayWidget extends StatelessWidget { return Column( children: [ - Obx(() => Text(formatoNota(_calculatorController.calculatedFinalGrade.value) ?? '--', + Obx(() => Text(formatoNota(_calculatorController.calculatedFinalGrade) ?? '--', style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, diff --git a/lib/widgets/calculadora_notas/nota_list_item.dart b/lib/widgets/calculadora_notas/nota_list_item.dart index 6f9e0d2..784639a 100644 --- a/lib/widgets/calculadora_notas/nota_list_item.dart +++ b/lib/widgets/calculadora_notas/nota_list_item.dart @@ -67,7 +67,7 @@ class NotaListItem extends StatelessWidget { }, textAlign: TextAlign.center, decoration: InputDecoration( - hintText: showSuggestedGrade ? (formatoNota(calculatorController.suggestedGrade.value) ?? "--") : "--", + hintText: showSuggestedGrade ? (formatoNota(calculatorController.suggestedGrade) ?? "--") : "--", disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: const BorderSide( color: Colors.transparent, @@ -117,7 +117,7 @@ class NotaListItem extends StatelessWidget { }, enabled: editable, decoration: InputDecoration( - hintText: calculatorController.suggestedPercentage.value?.toStringAsFixed(0) ?? "Peso", + hintText: calculatorController.suggestedPercentage?.toStringAsFixed(0) ?? "Peso", suffixText: "%", disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( borderSide: BorderSide( diff --git a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart index 2e5838b..7269b02 100644 --- a/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart +++ b/lib/widgets/calculadora_notas/nota_presentacion_display_widget.dart @@ -21,7 +21,7 @@ class NotaPresentacionDisplayWidget extends StatelessWidget { width: 80, margin: const EdgeInsets.only(left: 15), child: Obx(() => TextField( - controller: TextEditingController(text: formatoNota(_calculatorController.calculatedPresentationGrade.value) ?? ""), + controller: TextEditingController(text: formatoNota(_calculatorController.calculatedPresentationGrade) ?? ""), textAlign: TextAlign.center, enabled: false, decoration: InputDecoration( From 68e82c5b95664c4e8892f34df59638c6f7975cf2 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 23 May 2024 15:55:53 -0400 Subject: [PATCH 122/194] patch: ahora el modal de vista previa de una asignatura tiene bordes redondos Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/horario/bloque_ramo_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/horario/bloque_ramo_card.dart b/lib/widgets/horario/bloque_ramo_card.dart index 0f61671..4df4640 100644 --- a/lib/widgets/horario/bloque_ramo_card.dart +++ b/lib/widgets/horario/bloque_ramo_card.dart @@ -79,6 +79,6 @@ class ClassBlockCard extends StatelessWidget { "codigo": block.asignatura?.codigo, }); Navigator.pop(context); - showModalBottomSheet(context: context, builder: (ctx) => AsignaturaVistaPreviaModal(asignatura: asignatura, bloque: block)); + showModalBottomSheet(context: context, builder: (ctx) => AsignaturaVistaPreviaModal(asignatura: asignatura, bloque: block), shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20)))); } } From e9ed33615609891d12144030e31f00c476b3140e Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 24 May 2024 10:46:54 -0400 Subject: [PATCH 123/194] =?UTF-8?q?patch:=20se=20agrega=20medici=C3=B3n=20?= =?UTF-8?q?de=20tiempo=20del=20servicio=20de=20segundo=20plano.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/background_service.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 47d2529..715312d 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -43,7 +43,12 @@ class BackgroundController { class BackgroundService { static Future initAndStart() async { BackgroundFetch.registerHeadlessTask(BackgroundController.backgroundFetchHeadlessTask); - await BackgroundFetch.configure(_backgroundFetchConfig, _onFetch, _onTimeout); + await BackgroundFetch.configure(_backgroundFetchConfig, (taskId) { + Sentry.metrics().timing("BackgroundFetch_$taskId", + function: _onFetch(taskId), + unit: DurationSentryMeasurementUnit.milliSecond, + ); + }, _onTimeout); BackgroundFetch.start().then((_) {}).catchError((e, stackTrace) { Sentry.captureException(e, stackTrace: stackTrace); @@ -53,7 +58,7 @@ class BackgroundService { static _onFetch(String taskId) async { final init = DateTime.now(); var now = init; - logger.d("[BackgroundFetch]: Se ejecutó la tarea 'refreshTask' (${now.toIso8601String()})"); + logger.d("[BackgroundFetch]: Se ejecutó la tarea '$taskId' (${now.toIso8601String()})"); // Refresca el token de autenticación bool loggedIn = await Get.find().isLoggedIn(forceRefresh: true); @@ -112,7 +117,7 @@ class BackgroundService { logger.d("[BackgroundFetch]: Se refrescó el horario, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); now = DateTime.now(); - logger.d("[BackgroundFetch]: Se terminó la tarea 'refreshTask', tomó ${DateTime.now().difference(init).inMilliseconds} ms"); + logger.d("[BackgroundFetch]: Se terminó la tarea '$taskId', tomó ${DateTime.now().difference(init).inMilliseconds} ms"); } static _onTimeout(String taskId) async { From 398ae83829968bd0641d76a33f9917b69648b7ed Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 24 May 2024 10:47:38 -0400 Subject: [PATCH 124/194] patch: ahora solo se agrega fecha al CHANGELOG si es una release y no beta. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- fastlane/Fastfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index fe08190..5b850cb 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -216,10 +216,12 @@ platform :mobile do ) end - stamp_changelog( - section_identifier: build_name, - git_tag: build_name, - ) + if options[:type] == "release" + stamp_changelog( + section_identifier: build_name, + git_tag: build_name, + ) + end unless skip_git_push push_to_git_remote From 62e831591ce70966b3c3f75728216f10b0bc4d00 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 24 May 2024 10:48:05 -0400 Subject: [PATCH 125/194] =?UTF-8?q?patch:=20se=20agrega=20informaci=C3=B3n?= =?UTF-8?q?=20de=20m=C3=A9trica=20y=20eliminado=20l=C3=ADneas=20duplicadas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- CHANGELOG.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 123a30d..fc448c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,10 +33,11 @@ Tipos de cambios - Se agrega onboarding (con configuración de apodo, solicitud de permiso de notificaciones y bienvenida a la app). - Lista de estudiantes al resumen de asignatura. - Vista previa de estudiantes y profesores. -- Se agrega modal de vista previa de la asignatura desde horario (al mantener presionado un bloque). +- Se agrega ventana de vista previa de la asignatura desde horario (al mantener presionado un bloque). - Se agrega navegación hacia la asignatura desde el horario al presionar un bloque. - Botón para limpiar las notas en la calculadora. - Sección de `Acerca de la App` en modo depuración. +- Se agrega métricas y toma de errores de Sentry (esta vez están bien configurados) ### Changed - Se actualizaron algunas dependencias. @@ -154,10 +155,3 @@ Esta versión del changelog contiene cambios hechos en 2.10, debido a que no se - Lista de estudiantes en la pantalla de asignatura - Perfil de profesores -- Perfil de profesores -- Perfil de profesores -- Perfil de profesores -- Perfil de profesores -- Perfil de profesores -- Perfil de profesores -- Perfil de profesores From c0d9d75bb54bf17df2edc8da5dc657e8e63267bc Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 24 May 2024 10:48:15 -0400 Subject: [PATCH 126/194] patch: bump version Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8447891..35372aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+105 +version: 3.0.0+107 environment: sdk: ">=2.17.0 <3.0.0" From 8c8d0f631dc83cc7dc00f143c2f5ee98901fa78c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 24 May 2024 16:44:23 -0400 Subject: [PATCH 127/194] =?UTF-8?q?patch:=20se=20eliminan=20clases=20antig?= =?UTF-8?q?uas,=20reparan=20errores=20y=20agregan=20caracter=C3=ADsticas.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Se repara "protección de datos" de la credencial utilizando el paquete "screen_protector" en lugar de "flutter_windowmanager", protegiendo de capturas de pantalla y la imagen del código de barras. * Se agrega modal para mostrar nombre y rut de una persona. * Se cambia texto de docente a Persona en asignatura, para así tener nombre y rut del docente * Se repara lista de estudiantes (ahora muestra nombres en orden) * Se agrega utilidad de List#reverse * Se elimina vistas de docentes y usuario (ya que no estaban en uso) * Se mejora vista de profile picture para funcionar con cualquier widget, en lugar de solicitar un usuario, solicita iniciales y url de la foto. * Se mejora indicador de carga para no mostrar mensaje de carga si no es requerido. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- CHANGELOG.md | 2 + fastlane/Fastfile | 32 +- ios/Podfile.lock | 10 + lib/models/asignaturas/asignatura.dart | 50 +-- lib/models/user/persona.dart | 10 +- lib/repositories/asignaturas_repository.dart | 9 +- .../detalle/asignatura_resumen_tab.dart | 9 +- lib/screens/credencial_screen.dart | 198 ++++++------ lib/screens/docentes_screen.dart | 142 --------- lib/screens/perfil/perfil_screen.dart | 3 +- lib/screens/usuario_screen.dart | 296 ------------------ lib/utils/utils.dart | 16 +- .../club/acerca_club_desarrolladores.dart | 5 +- lib/widgets/asignatura/modals/user_modal.dart | 5 +- lib/widgets/credencial/credencial_card.dart | 3 +- lib/widgets/custom_drawer.dart | 3 +- .../modals/asignatura_vista_previa_modal.dart | 11 +- lib/widgets/loading/loading_indicator.dart | 6 +- lib/widgets/modals/persona_modal.dart | 79 +++++ .../permiso_ingreso/usuario_detalle.dart | 9 +- lib/widgets/profile_photo.dart | 12 +- pubspec.lock | 16 +- pubspec.yaml | 2 +- 23 files changed, 306 insertions(+), 622 deletions(-) delete mode 100644 lib/screens/docentes_screen.dart delete mode 100644 lib/screens/usuario_screen.dart create mode 100644 lib/widgets/modals/persona_modal.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index fc448c1..31f7804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Tipos de cambios - Se agrega onboarding (con configuración de apodo, solicitud de permiso de notificaciones y bienvenida a la app). - Lista de estudiantes al resumen de asignatura. - Vista previa de estudiantes y profesores. +- Vista previa de los datos del profesor - Se agrega ventana de vista previa de la asignatura desde horario (al mantener presionado un bloque). - Se agrega navegación hacia la asignatura desde el horario al presionar un bloque. - Botón para limpiar las notas en la calculadora. @@ -51,6 +52,7 @@ Tipos de cambios ### Removed - Se elimina pestaña de estudiantes del detalle de asignatura (a favor del botón de lista de estudiantes en el resumen). - Modal de Permitir Notificaciones a favor de modal nativa del sistema. Solo se editará el mensaje de notificaciones, ya que es redundante el botón de permitir y cancelar. +- Vistas sin uso en la aplicación. ## [2.11.9] - 2023-10-11Z diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5b850cb..5248997 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -221,23 +221,23 @@ platform :mobile do section_identifier: build_name, git_tag: build_name, ) - end - unless skip_git_push - push_to_git_remote - git_add - git_commit(path: "*", message: "Stamp changelog for #{build_name} (#{build_number})") - push_to_git_remote - - github_release = set_github_release( - repository_name: "exdevutem/mi-utem", - api_token: ENV["GITHUB_TOKEN"], - name: "v#{build_name}", - tag_name: "v#{build_name}", - description: emojified_changelog, - commitish: "dev", - upload_assets: ["./build/app/outputs/bundle/release/app-release.aab", "./build/app/outputs/apk/release/app-release.apk"] - ) + unless skip_git_push + push_to_git_remote + git_add + git_commit(path: "*", message: "Stamp changelog for #{build_name} (#{build_number})") + push_to_git_remote + + github_release = set_github_release( + repository_name: "exdevutem/mi-utem", + api_token: ENV["GITHUB_TOKEN"], + name: "v#{build_name}+#{build_number}", + tag_name: "v#{build_name}+#{build_number}", + description: emojified_changelog, + commitish: "dev", + upload_assets: ["./build/app/outputs/bundle/release/app-release.aab", "./build/app/outputs/apk/release/app-release.apk"] + ) + end end unless skip_slack diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f9d141f..c21e7ce 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -906,6 +906,10 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) + - screen_protector (1.2.1): + - Flutter + - ScreenProtectorKit (~> 1.3.1) + - ScreenProtectorKit (1.3.1) - Sentry/HybridSDK (8.25.2) - sentry_flutter (8.2.0): - Flutter @@ -946,6 +950,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - screen_protector (from `.symlinks/plugins/screen_protector/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) @@ -982,6 +987,7 @@ SPEC REPOS: - nanopb - PromisesObjC - PromisesSwift + - ScreenProtectorKit - Sentry - UXCam @@ -1026,6 +1032,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + screen_protector: + :path: ".symlinks/plugins/screen_protector/ios" sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: @@ -1092,6 +1100,8 @@ SPEC CHECKSUMS: permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 + screen_protector: 6f92086bd2f2f4b54f54913289b9d1310610140b + ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 Sentry: 51b056d96914a741f63eca774d118678b1eb05a1 sentry_flutter: e8397d13e297a5d4b6be8a752e33140b21c5cc97 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart index b882eb5..615d269 100644 --- a/lib/models/asignaturas/asignatura.dart +++ b/lib/models/asignaturas/asignatura.dart @@ -3,18 +3,21 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asistencia.dart'; import 'package:mi_utem/models/evaluacion/grades.dart'; +import 'package:mi_utem/models/user/persona.dart'; +import 'package:mi_utem/models/user/rut.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/utils/string_utils.dart'; +import 'package:mi_utem/utils/utils.dart'; class Asignatura { - String? id; - String? nombre; - String? codigo; - String? tipoHora; - String? estado; - String? docente; - String? seccion; + String id; + String nombre; + String codigo; + String tipoHora; + String estado; + String seccion; + Persona docente; Asistencia? asistencia; Grades? grades; @@ -26,13 +29,13 @@ class Asignatura { String? tipoSala; Asignatura({ - this.id, - this.nombre, - this.codigo, - this.tipoHora, - this.estado, - this.docente, - this.seccion, + required this.id, + required this.nombre, + required this.codigo, + required this.tipoHora, + required this.estado, + required this.seccion, + required this.docente, this.asistencia, this.grades, this.estudiantes, @@ -54,13 +57,24 @@ class Asignatura { } } - factory Asignatura.fromJson(Map? json) => json != null ? Asignatura( + factory Asignatura.fromJson(Map json) => Asignatura( id: json['id'], codigo: json['codigo'], nombre: capitalize(json['nombre'] ?? ''), tipoHora: capitalize(json['tipoHora'] ?? ''), estado: capitalize(json['estado'] ?? ''), - docente: capitalize(json['docente'] ?? ''), + docente: let(json['docente'], (String? docente) { + if(docente == null) { + return null; + } + + final nombreCompleto = docente.split(" ").where((element) => int.tryParse(element) == null).join(" ").replaceAll("- ", ""); + final digitos = int.tryParse(docente.split(" ").where((element) => int.tryParse(element) != null).join(" ").replaceAll("-", "")); + return Persona( + nombreCompleto: capitalize(nombreCompleto), + rut: digitos != null ? Rut(digitos) : null, + ); + }) ?? Persona(nombreCompleto: "Sin Docente"), seccion: json['seccion'], grades: json['notas'] != null ? Grades.fromJson(json['notas']) : Grades(), estudiantes: json.containsKey("estudiantes") ? User.fromJsonList(json["estudiantes"]) : [], @@ -70,7 +84,7 @@ class Asignatura { horario: json['horario'], intentos: json.containsKey('intentos') ? (json['intentos'] is num ? json['intentos'] as num? : num.tryParse(json['intentos'])) : null, tipoSala: capitalize(json['tipoSala'] ?? ''), - ) : Asignatura(); + ); static List fromJsonList(dynamic json) => json != null ? (json as List).map((it) => Asignatura.fromJson(it)).toList() : []; @@ -101,7 +115,7 @@ class Asignatura { String? codigo, String? tipoHora, String? estado, - String? docente, + Persona? docente, String? seccion, Asistencia? asistencia, Grades? grades, diff --git a/lib/models/user/persona.dart b/lib/models/user/persona.dart index de2634e..dce186e 100644 --- a/lib/models/user/persona.dart +++ b/lib/models/user/persona.dart @@ -6,14 +6,14 @@ class Persona { final String nombreCompleto; final Rut? rut; - Persona({ + const Persona({ required this.nombreCompleto, - required this.rut + this.rut }); - get nombreCompletoCapitalizado => capitalize(nombreCompleto); - get primerNombre => nombreCompletoCapitalizado.split(' ')[0]; - get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0].toUpperCase()).join(''); + String get nombreCompletoCapitalizado => capitalize(nombreCompleto); + String get primerNombre => nombreCompletoCapitalizado.split(' ')[0]; + String get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0].toUpperCase()).join(''); factory Persona.fromJson(Map json) => Persona( nombreCompleto: json['nombreCompleto'], diff --git a/lib/repositories/asignaturas_repository.dart b/lib/repositories/asignaturas_repository.dart index 7552731..3fe8b28 100644 --- a/lib/repositories/asignaturas_repository.dart +++ b/lib/repositories/asignaturas_repository.dart @@ -1,6 +1,7 @@ import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/utils/http/functions.dart'; +import 'package:mi_utem/utils/utils.dart'; class AsignaturasRepository { @@ -23,8 +24,12 @@ class AsignaturasRepository { final response = await authClientRequest("asignaturas/${asignatura.codigo}", forceRefresh: forceRefresh); final json = response.data as Map; - // Por ahora solo se actualizan los estudiantes - return User.fromJsonList(json['estudiantes']); + final estudiantes = (json['estudiantes'] as List).map((it) { + it['nombreCompleto'] = (it['nombreCompleto'] as String).split(" ").rotate(2).join(" "); + return it; + }).toList(); + + return User.fromJsonList(estudiantes); } diff --git a/lib/screens/asignatura/detalle/asignatura_resumen_tab.dart b/lib/screens/asignatura/detalle/asignatura_resumen_tab.dart index e03a14a..08083b1 100644 --- a/lib/screens/asignatura/detalle/asignatura_resumen_tab.dart +++ b/lib/screens/asignatura/detalle/asignatura_resumen_tab.dart @@ -3,6 +3,7 @@ import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/screens/asignatura/detalle/asignatura_estudiantes_tab.dart'; import 'package:mi_utem/utils/string_utils.dart'; import 'package:mi_utem/widgets/field_list_tile.dart'; +import 'package:mi_utem/widgets/modals/persona_modal.dart'; class AsignaturaResumenTab extends StatelessWidget { final Asignatura asignatura; @@ -40,13 +41,11 @@ class AsignaturaResumenTab extends StatelessWidget { GestureDetector( child: FieldListTile( title: "Docente", - value: asignatura.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ").replaceAll("- ", "") ?? "Sin docente", // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. + value: asignatura.docente.nombreCompleto, ), - onTap: () async { - // TODO: Mostrar perfil del docente. - }, + onTap: () async => showModalBottomSheet(context: context, builder: (ctx) => PersonaModal(persona: asignatura.docente)), ), - if (asignatura.seccion?.isNotEmpty == true) ...[ + if (asignatura.seccion.isNotEmpty) ...[ Divider(height: 5, indent: 20, endIndent: 20), FieldListTile( title: "Sección", diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index c901835..0872c0a 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_windowmanager/flutter_windowmanager.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/carrera.dart'; @@ -14,6 +13,7 @@ import 'package:mi_utem/widgets/custom_app_bar.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/flip_widget.dart'; import 'package:mi_utem/widgets/loading/loading_indicator.dart'; +import 'package:screen_protector/screen_protector.dart'; class CredencialScreen extends StatefulWidget { const CredencialScreen({ @@ -25,130 +25,126 @@ class CredencialScreen extends StatefulWidget { } class _CredencialScreenState extends State { - FlipController _flipController = FlipController(); + final FlipController _flipController = FlipController(); @override void initState() { ReviewService.addScreen("CredencialScreen"); - FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); + ScreenProtector.preventScreenshotOn(); + ScreenProtector.protectDataLeakageOn(); + ScreenProtector.protectDataLeakageWithBlur(); super.initState(); } @override void dispose() { - FlutterWindowManager.clearFlags(FlutterWindowManager.FLAG_SECURE); + ScreenProtector.preventScreenshotOff(); + ScreenProtector.protectDataLeakageOff(); super.dispose(); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context) => Scaffold( + appBar: CustomAppBar( + title: const Text("Credencial universitaria"), + actions: [ + IconButton( + icon: Icon(_flipController.actualFace == FlipController.front ? Icons.info : Mdi.accountCircle), + onPressed: _flipController.flip?.call(), + ), + ], + ), + backgroundColor: Colors.grey[200], + body: FutureBuilder>( + future: () async { + final authService = Get.find(); + final carrerasService = Get.find(); - return Scaffold( - appBar: CustomAppBar( - title: const Text("Credencial universitaria"), - actions: [ - IconButton( - icon: Icon(_flipController.actualFace == FlipController.front ? Icons.info : Mdi.accountCircle), - onPressed: () { - _flipController.flip!(); - }, - ), - ], - ), - backgroundColor: Colors.grey[200], - body: FutureBuilder>( - future: () async { - final authService = Get.find(); - final carrerasService = Get.find(); - - if(carrerasService.selectedCarrera == null) { - await carrerasService.getCarreras(); - } - - final user = await authService.getUser(); - final carrera = carrerasService.selectedCarrera; - - return Pair(user, carrera); - }(), - builder: (context, snapshot) { - if (snapshot.hasError) { - return CustomErrorWidget( - title: "Ocurrió un error al generar tu crendencial", - error: snapshot.error, - ); - } + if(carrerasService.selectedCarrera == null) { + await carrerasService.getCarreras(); + } - final pair = snapshot.data; - final user = pair?.a; - final carreraActiva = pair?.b; + final user = await authService.getUser(); + final carrera = carrerasService.selectedCarrera; - if (!snapshot.hasData) { - return Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Expanded( - child: Center( - child: LoadingIndicator(), - ), - ), - ], - ), - ); - } + return Pair(user, carrera); + }(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return CustomErrorWidget( + title: "Ocurrió un error al generar tu crendencial", + error: snapshot.error, + ); + } - if (user == null || user.rut == null || carreraActiva == null || carreraActiva.nombre == null) { - return CustomErrorWidget( - title: "Ocurrió un error al generar tu crendencial. Por favor, intenta nuevamente.", - error: snapshot.error, - ); - } + final pair = snapshot.data; + final user = pair?.a; + final carreraActiva = pair?.b; - if (carreraActiva.nombre?.isNotEmpty == true) { - return Center( - child: SafeArea( - child: CredencialCard( - user: user, - carrera: carreraActiva, - controller: _flipController, - onFlip: (_) { - AnalyticsService.logEvent("credencial_flip"); - setState(() {}); - }, - ), - ), - ); - } - - return Container( + if (!snapshot.hasData) { + return Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text("😕", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 50, - ), - ), - const SizedBox(height: 15), - const Text("Ocurrió un error al generar tu credencial", - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - const SizedBox(height: 15), - Text(snapshot.error.toString(), - textAlign: TextAlign.center, + Expanded( + child: LoadingIndicator.centered(), ), ], ), ); - }, - ), - ); - } + } + + if (user == null || user.rut == null || carreraActiva == null || carreraActiva.nombre == null) { + return CustomErrorWidget( + title: "Ocurrió un error al generar tu credencial. Por favor, intenta nuevamente.", + error: snapshot.error, + ); + } + + if (carreraActiva.nombre?.isNotEmpty == true) { + return Center( + child: SafeArea( + child: CredencialCard( + user: user, + carrera: carreraActiva, + controller: _flipController, + onFlip: (_) { + AnalyticsService.logEvent("credencial_flip"); + setState(() {}); + }, + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("😕", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 50, + ), + ), + const SizedBox(height: 15), + const Text("Ocurrió un error al generar tu credencial", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 15), + Text(snapshot.error.toString(), + textAlign: TextAlign.center, + ), + ], + ), + ); + }, + ), + ); } diff --git a/lib/screens/docentes_screen.dart b/lib/screens/docentes_screen.dart deleted file mode 100644 index 3a884b0..0000000 --- a/lib/screens/docentes_screen.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/screens/usuario_screen.dart'; -import 'package:mi_utem/utils/debounce.dart'; -import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/loading/loading_indicator.dart'; -import 'package:mi_utem/widgets/profile_photo.dart'; - -class DocentesScreen extends StatefulWidget { - const DocentesScreen({ - super.key, - }); - - @override - _DocentesScreenState createState() => _DocentesScreenState(); -} - -class _DocentesScreenState extends State { - Future>? _futureDocentes; - late List _docentes; - late Debounce d; - - TextEditingController _controller = TextEditingController(); - - @override - void initState() { - super.initState(); - d = Debounce(Duration(seconds: 1), () { - _getDocentes(_controller.text); - }); - } - - Future> _getDocentes(String nombre) async { - setState(() { - _docentes = []; - _futureDocentes = null; - // _futureDocentes = DocentesService.buscarDocentes(nombre); - }); - List docentes = await _futureDocentes!; - setState(() => _docentes = docentes); - return docentes; - } - - _search(String query) { - if (query.trim().length > 3) { - d.schedule(); - } else { - d.clear(); - setState(() { - _futureDocentes = null; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: const Text("Docentes"), - ), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: TextField( - decoration: const InputDecoration( - hintText: "Escribe para buscar un docente", - prefixIcon: Icon(Icons.search), - ), - keyboardType: TextInputType.name, - onSubmitted: _search, - textCapitalization: TextCapitalization.words, - textInputAction: TextInputAction.search, - controller: _controller, - onChanged: _search, - ), - ), - const SizedBox(height: 20), - _futureDocentes != null ? FutureBuilder>( - future: _futureDocentes, - builder: (context, snapshot) { - if (snapshot.hasError) { - return CustomErrorWidget( - title: "Ocurrió un error al obtener los docentes", - error: snapshot.error, - ); - } - - final listaDocentes = snapshot.data; - - if(!snapshot.hasData || listaDocentes == null) { - return const Padding( - padding: EdgeInsets.all(20), - child: Center( - child: LoadingIndicator(), - ), - ); - } - - if (listaDocentes.isEmpty) { - return const CustomErrorWidget( - emoji: "\u{1F914}", // Emoji de cara pensando (mejor usar códigos unicode) - title: "Parece que no se encontraron docentes que coincidan con tu búsqueda", - ); - } - - return ListView.separated( - physics: ScrollPhysics(), - shrinkWrap: true, - itemCount: _docentes.length, - separatorBuilder: (context, index) => const Divider(height: 5, indent: 20, endIndent: 20), - itemBuilder: (BuildContext context, int i) { - User docente = _docentes[i]; - return ListTile( - leading: ProfilePhoto( - user: docente, - radius: 20, - editable: false, - ), - title: Text(docente.nombreCompleto, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(docente.correoUtem ?? docente.correoPersonal ?? "N/N"), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (ctx) => UsuarioScreen(tipo: 2, query: {"nombre": docente.nombreDisplayCapitalizado}))), - ); - }, - ); - }, - ) : CustomErrorWidget( - emoji: "💅", - title: "Escribe para buscar un docente", - ), - ], - ), - ), - ); - } -} diff --git a/lib/screens/perfil/perfil_screen.dart b/lib/screens/perfil/perfil_screen.dart index 8c9f1c4..f20aadb 100644 --- a/lib/screens/perfil/perfil_screen.dart +++ b/lib/screens/perfil/perfil_screen.dart @@ -151,7 +151,8 @@ class _PerfilScreenState extends State { ), Center( child: ProfilePhoto( - user: user, + fotoUrl: user.fotoUrl, + iniciales: user.iniciales, radius: 60, editable: false, onImage: (imagenBase64) async { diff --git a/lib/screens/usuario_screen.dart b/lib/screens/usuario_screen.dart deleted file mode 100644 index 0cbb1c4..0000000 --- a/lib/screens/usuario_screen.dart +++ /dev/null @@ -1,296 +0,0 @@ -import 'dart:core'; - -import 'package:clipboard/clipboard.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:mi_utem/models/asignaturas/asignatura.dart'; -import 'package:mi_utem/models/preferencia.dart'; -import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/services/auth_service.dart'; -import 'package:mi_utem/services/review_service.dart'; -import 'package:mi_utem/widgets/custom_app_bar.dart'; -import 'package:mi_utem/widgets/custom_error_widget.dart'; -import 'package:mi_utem/widgets/image/image_view_screen.dart'; -import 'package:mi_utem/widgets/loading/loading_dialog.dart'; -import 'package:mi_utem/widgets/loading/loading_indicator.dart'; -import 'package:mi_utem/widgets/profile_photo.dart'; -import 'package:mi_utem/widgets/snackbar.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class UsuarioScreen extends StatefulWidget { - final int tipo; - final Map? query; - final Asignatura? asignatura; - - UsuarioScreen({ - super.key, - this.tipo = 0, - this.query, - this.asignatura, - }); - - @override - _UsuarioScreenState createState() => _UsuarioScreenState(); -} - -class _UsuarioScreenState extends State { - final _authService = Get.find(); - - Future? _userFuture; - User? _user; - String? _apodo; - - @override - void initState() { - ReviewService.addScreen("UsuarioScreen"); - super.initState(); - _userFuture = _getUsuario(); - } - - Future _getUsuario() async { - try { - User? user; - if (widget.tipo == 2) { - // if (widget.asignatura == null) { - // user = await DocentesService.traerUnDocente(widget.query!["nombre"]); - // } else { - // user = await DocentesService.asignarUnDocente(widget.query!["nombre"], widget.asignatura!.codigo, widget.asignatura!.nombre); - // } - - setState(() => _user = user); - } else { - user = await _authService.getUser(); - final apodo = await Preferencia.apodo.get(); - setState(() { - _user = user; - _apodo = apodo; - }); - } - - if (user == null) { - throw "No se pudo obtener el usuario"; - } - - return user; - } catch (e) { - throw e; - } - } - - Future _changeFoto(String imagen) async { - showLoadingDialog(context); - - try { - final user = await _authService.updateProfilePicture(imagen); - Navigator.pop(context); - setState(() => _user = user); - } catch (e) { - Navigator.pop(context); - print("Error cambiando la imagen ${e.toString()}"); - showErrorSnackbar(context, "No se pudo cambiar la foto."); - } - } - - List get _datosPersonales { - List lista = []; - if(_user == null) { - return lista; - } - - if (_user?.nombreDisplayCapitalizado?.isEmpty == false) { - lista.add(ListTile( - title: Text("Nombre", - style: TextStyle(color: Colors.grey), - ), - subtitle: Text(_user!.nombreCompleto, - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), - ), - )); - } else { - if (_user?.nombres?.isEmpty == false) { - lista.add( - ListTile( - title: Text("Nombres", - style: TextStyle(color: Colors.grey), - ), - subtitle: Text(_user!.nombres!, - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), - ), - ), - ); - } - - if (_user?.apellidos?.isEmpty == false) { - lista.add(Divider(height: 1)); - lista.add( - ListTile( - title: Text("Apellidos", - style: TextStyle(color: Colors.grey), - ), - subtitle: Text(_user!.apellidos!, - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), - ), - ), - ); - } - } - - if(_apodo != null) { - lista.add(Divider(height: 1)); - lista.add( - ListTile( - title: Text("Alias", - style: TextStyle(color: Colors.grey), - ), - subtitle: Text(_apodo!, - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), - ), - ), - ); - } - - if (_user?.correoUtem?.isEmpty == false) { - lista.add(Divider(height: 1)); - lista.add(ListTile( - title: Text("Correo Institucional", - style: TextStyle(color: Colors.grey), - ), - onLongPress: widget.tipo != 0 ? () async { - await FlutterClipboard.copy(_user!.correoUtem!); - showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); - } : null, - onTap: widget.tipo != 0 ? () async { - await launchUrl(Uri.parse("mailto:${_user?.correoUtem ?? ""}")); - } : null, - subtitle: Text(_user!.correoUtem ?? "", - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), - ), - )); - } - if (_user?.correoPersonal?.isEmpty == false) { - lista.add(Divider(height: 1)); - lista.add( - ListTile( - title: Text("Correo Personal", - style: TextStyle(color: Colors.grey), - ), - onLongPress: widget.tipo != 0 ? () async { - await FlutterClipboard.copy(_user!.correoPersonal!); - showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); - } : null, - onTap: widget.tipo != 0 ? () async { - await launchUrl(Uri.parse("mailto:${_user?.correoPersonal ?? ""}")); - } : null, - subtitle: Text(_user!.correoPersonal!, - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), - ), - ), - ); - } - - if (widget.tipo == 0 && _user!.rut != null) { - lista.add(Divider(height: 1)); - lista.add(ListTile( - title: Text("RUT", - style: TextStyle(color: Colors.grey), - ), - subtitle: Text("${_user!.rut}", - style: TextStyle( - color: Colors.grey[900], - fontSize: 18, - ), - ), - )); - } - - return lista; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: Text(widget.tipo == 0 ? "Perfil" : widget.query!["nombre"]), - ), - body: FutureBuilder( - future: _userFuture, - builder: (context, snapshot) { - if (snapshot.hasError) { - return CustomErrorWidget( - title: "Ocurrió un error al obtener el perfil", - error: snapshot.error, - ); - } - - if (snapshot.hasData) { - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Stack( - children: [ - Container( - alignment: Alignment.topCenter, - margin: const EdgeInsets.only(top: 80), - child: Card( - margin: const EdgeInsets.all(20), - child: ListView( - padding: const EdgeInsets.only(bottom: 10, top: 20), - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - children: _datosPersonales, - ), - ), - ), - Center( - child: ProfilePhoto( - user: _user, - radius: 60, - editable: widget.tipo == 0, - onImage: widget.tipo == 0 ? _changeFoto : null, - onImageTap: (context, imageProvider) { - Navigator.push(context, MaterialPageRoute(builder: (ctx) => ImageViewScreen( - imageProvider: imageProvider, - ))); - }, - ), - ), - ], - ), - ); - } - - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Expanded( - child: Center( - child: LoadingIndicator(), - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index acb2649..9da0462 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -21,4 +21,18 @@ V? let(K? object, V Function(K) op) => object != null ? op(object) : null; /// Esta función muestra una nota en formato de 1 o 2 decimales dependiendo de si es un 3.95 o no. -String? formatoNota(num? nota) => nota == 3.95 ? nota?.toStringAsFixed(2) : nota?.toStringAsFixed(1); \ No newline at end of file +String? formatoNota(num? nota) => nota == 3.95 ? nota?.toStringAsFixed(2) : nota?.toStringAsFixed(1); + +/// Esta extensión rotará los elementos de una lista "rotándolos" al final las veces que sea especificado. +/// Ejemplo: +/// ```dart +/// List lista = [1, 2, 3, 4, 5]; +/// print(lista.rotate(2)); // [3, 4, 5, 1, 2] +/// +/// // Si el número es negativo, se rotará hacia la izquierda +/// print(lista.rotate(-2)); // [4, 5, 1, 2, 3] +/// ``` +extension RotateList on List { + + List rotate(int count) => count > 0 ? (this.sublist(count)..addAll(this.sublist(0, count))) : (this.sublist(this.length + count)..addAll(this.sublist(0, this.length + count))); +} \ No newline at end of file diff --git a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart index 8076afb..e3cd747 100644 --- a/lib/widgets/acerca/club/acerca_club_desarrolladores.dart +++ b/lib/widgets/acerca/club/acerca_club_desarrolladores.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/models/user/persona.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/widgets/image/image_view_screen.dart'; @@ -43,7 +43,8 @@ class AcercaClubDesarrolladores extends StatelessWidget { child: Row( children: [ ProfilePhoto( - user: User(nombres: developer['nombre'], fotoUrl: developer['fotoUrl']), + fotoUrl: developer['fotoUrl'], + iniciales: Persona(nombreCompleto: developer['nombre']).iniciales, onImageTap: (context, imageProvider) { AnalyticsService.logEvent("acerca_person_image_tap", parameters: { "persona": developer['nombre'], diff --git a/lib/widgets/asignatura/modals/user_modal.dart b/lib/widgets/asignatura/modals/user_modal.dart index 4542db2..0752429 100644 --- a/lib/widgets/asignatura/modals/user_modal.dart +++ b/lib/widgets/asignatura/modals/user_modal.dart @@ -45,7 +45,7 @@ class UserModal extends StatelessWidget { ), Divider(height: 5), ListTile( - title: Text("Correo Personal", + title: Text("Correo", style: TextStyle(color: Colors.grey), ), onLongPress: () async { @@ -68,7 +68,8 @@ class UserModal extends StatelessWidget { ), Center( child: ProfilePhoto( - user: user, + fotoUrl: user.fotoUrl, + iniciales: user.iniciales, radius: 60, editable: false, ), diff --git a/lib/widgets/credencial/credencial_card.dart b/lib/widgets/credencial/credencial_card.dart index 88032a8..4a8d3be 100644 --- a/lib/widgets/credencial/credencial_card.dart +++ b/lib/widgets/credencial/credencial_card.dart @@ -39,7 +39,8 @@ class CredencialCard extends StatelessWidget { Container( margin: EdgeInsets.only(top: altoBanner - 40), child: ProfilePhoto( - user: user, + fotoUrl: user?.fotoUrl, + iniciales: user?.iniciales ?? "N/N", radius: 50, borderWidth: 5, ), diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 868cccd..877aad2 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -91,7 +91,8 @@ class CustomDrawer extends StatelessWidget { ), ), currentAccountPicture: ProfilePhoto( - user: user, + fotoUrl: user.fotoUrl, + iniciales: user.iniciales, radius: 30, ), decoration: BoxDecoration( diff --git a/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart b/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart index 0b28911..191cd16 100644 --- a/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart +++ b/lib/widgets/horario/modals/asignatura_vista_previa_modal.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/horario.dart'; +import 'package:mi_utem/widgets/modals/persona_modal.dart'; class AsignaturaVistaPreviaModal extends StatelessWidget { final Asignatura asignatura; @@ -26,14 +27,12 @@ class AsignaturaVistaPreviaModal extends StatelessWidget { children: [ GestureDetector( child: ListTile( - title: Text(asignatura.nombre ?? "Sin nombre"), - subtitle: Text(asignatura.docente?.split(" ").where((element) => int.tryParse(element) == null).join(" ").replaceAll("- ", "") ?? "Sin docente"), // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. + title: Text(asignatura.nombre), + subtitle: Text(asignatura.docente.nombreCompleto), // Se filtran enteros, al parecer hay algunos textos que incluyen un identificador. ), - onTap: () async { - // TODO: Mostrar perfil del docente. - }, + onTap: () async => showModalBottomSheet(context: context, builder: (ctx) => PersonaModal(persona: asignatura.docente)), ), - if (asignatura.seccion?.isNotEmpty == true) ...[ + if (asignatura.seccion.isNotEmpty) ...[ Divider(height: 5, indent: 20, endIndent: 20), ListTile( title: Text("Sección"), diff --git a/lib/widgets/loading/loading_indicator.dart b/lib/widgets/loading/loading_indicator.dart index 95c1de9..992b50e 100644 --- a/lib/widgets/loading/loading_indicator.dart +++ b/lib/widgets/loading/loading_indicator.dart @@ -36,11 +36,7 @@ class LoadingIndicator extends StatelessWidget { ), ); - static Widget centered({required String message}) => Center( - child: LoadingIndicator( - message: message, - ), - ); + static Widget centered({String? message}) => Center(child: LoadingIndicator(message: message)); static Widget centeredDefault() => centered(message: "Esto tardará un poco, paciencia..."); diff --git a/lib/widgets/modals/persona_modal.dart b/lib/widgets/modals/persona_modal.dart new file mode 100644 index 0000000..164515c --- /dev/null +++ b/lib/widgets/modals/persona_modal.dart @@ -0,0 +1,79 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/user/persona.dart'; +import 'package:mi_utem/widgets/profile_photo.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PersonaModal extends StatelessWidget { + final Persona persona; + + const PersonaModal({ + super.key, + required this.persona, + }); + + @override + Widget build(BuildContext context) => SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Stack( + children: [ + Container( + alignment: Alignment.topCenter, + margin: const EdgeInsets.only(top: 80), + child: Card( + margin: const EdgeInsets.all(20), + child: ListView( + padding: const EdgeInsets.only(bottom: 10, top: 20), + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + ListTile( + title: Text("Nombre Completo", + style: TextStyle(color: Colors.grey), + ), + onLongPress: () async { + await FlutterClipboard.copy(persona.nombreCompleto); + showTextSnackbar(context, title: "¡Copiado!", message: "Correo copiado al portapapeles"); + }, + subtitle: Text(persona.nombreCompletoCapitalizado, + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + ), + Divider(height: 5), + ListTile( + title: Text("Rut", + style: TextStyle(color: Colors.grey), + ), + onLongPress: () async { + await FlutterClipboard.copy(persona.rut.toString()); + showTextSnackbar(context, title: "¡Copiado!", message: "Rut copiado al portapapeles"); + }, + onTap: () async { + await launchUrl(Uri.parse("mailto:${persona.rut.toString()}")); + }, + subtitle: Text(persona.rut.toString(), + style: TextStyle( + color: Colors.grey[900], + fontSize: 18, + ), + ), + ), + ], + ), + ), + ), + Center( + child: ProfilePhoto( + iniciales: persona.iniciales, + radius: 60, + editable: false, + ), + ), + ], + ), + ); +} diff --git a/lib/widgets/permiso_ingreso/usuario_detalle.dart b/lib/widgets/permiso_ingreso/usuario_detalle.dart index e2b800f..f7044a7 100644 --- a/lib/widgets/permiso_ingreso/usuario_detalle.dart +++ b/lib/widgets/permiso_ingreso/usuario_detalle.dart @@ -16,8 +16,11 @@ class UsuarioDetalle extends StatelessWidget { padding: const EdgeInsets.all(20.0), child: Row( children: [ - ProfilePhoto(user: user), - Container(width: 10), + ProfilePhoto( + fotoUrl: user.fotoUrl, + iniciales: user.iniciales, + ), + const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -26,7 +29,7 @@ class UsuarioDetalle extends StatelessWidget { maxLines: 2, style: Theme.of(context).textTheme.bodyLarge, ), - Container(height: 4), + const SizedBox(height: 4), Text("${user.rut}", style: Theme.of(context).textTheme.bodyMedium, ), diff --git a/lib/widgets/profile_photo.dart b/lib/widgets/profile_photo.dart index 37e5ecc..e688e70 100644 --- a/lib/widgets/profile_photo.dart +++ b/lib/widgets/profile_photo.dart @@ -6,14 +6,14 @@ import 'package:circular_profile_avatar/circular_profile_avatar.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mi_utem/config/logger.dart'; -import 'package:mi_utem/models/user/user.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/widgets/image/imagen_editor_modal.dart'; import 'package:mi_utem/widgets/snackbar.dart'; class ProfilePhoto extends StatefulWidget { final double radius; - final User? user; + final String? fotoUrl; + final String iniciales; final Function(BuildContext, ImageProvider)? onImageTap; final Function()? onTap; final double borderWidth; @@ -23,7 +23,8 @@ class ProfilePhoto extends StatefulWidget { ProfilePhoto({ super.key, - required this.user, + required this.iniciales, + this.fotoUrl, this.onTap, this.onImageTap, this.radius = 25, @@ -49,7 +50,7 @@ class _ProfilePhotoState extends State { FutureBuilder( // future: Get.find().getProfilePicture(), initialData: null, - builder: (ctx, snapshot) => CircularProfileAvatar(snapshot.data != null ? '' : (widget.user!.fotoUrl ?? ""), + builder: (ctx, snapshot) => CircularProfileAvatar(snapshot.data != null ? '' : (widget.fotoUrl ?? ""), child: snapshot.data != null ? Image.memory(Base64Decoder().convert(snapshot.data!)) : null, onTap: () => widget.onTap != null && widget.onImageTap == null ? widget.onTap : null, borderColor: widget.borderColor, @@ -74,8 +75,7 @@ class _ProfilePhotoState extends State { ), ), ), - initialsText: Text( - widget.user!.iniciales, + initialsText: Text(widget.iniciales, style: TextStyle( fontSize: widget.radius * 0.5, color: Colors.white, diff --git a/pubspec.lock b/pubspec.lock index 6216266..5951d1b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -665,14 +665,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_windowmanager: - dependency: "direct main" - description: - name: flutter_windowmanager - sha256: b4d0bc06f6777952b729c0cdb7ce9ad1ecabd8b8b1cb0acb57a36621457dab1b - url: "https://pub.dev" - source: hosted - version: "0.2.0" font_awesome_flutter: dependency: "direct main" description: @@ -1249,6 +1241,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + screen_protector: + dependency: "direct main" + description: + name: screen_protector + sha256: "541bdcd341de1e38026b5b94cc2a74cd95299d2c51150735165c4b445fa0209a" + url: "https://pub.dev" + source: hosted + version: "1.4.2" screenshot: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 35372aa..be48b74 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,6 @@ dependencies: url: https://github.com/vitorhm/flutter-masked-text.git flutter_secure_storage: ^7.0.1 flutter_spinkit: ^5.1.0 - flutter_windowmanager: ^0.2.0 font_awesome_flutter: ^10.0.0 gradient_widgets: ^0.6.0 html: ^0.15.0 @@ -74,6 +73,7 @@ dependencies: crypto: ^3.0.3 get: ^4.6.5 dio_http_cache: ^0.3.0 + screen_protector: ^1.4.2 dependency_overrides: qr: ^3.0.0 From 1b389ac7aaa3828d9b76798f021c474845c55363 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:06:23 -0400 Subject: [PATCH 128/194] patch: bump version Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index be48b74..b5beed3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+107 +version: 3.0.0+108 environment: sdk: ">=2.17.0 <3.0.0" From 9011520964fd56432a118a6f1155bc79bc7ba2e1 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:07:34 -0400 Subject: [PATCH 129/194] patch: se agrega User#copyWith y se optimiza refresco de token Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/user/user.dart | 26 ++++++++++++++++++++++++++ lib/services/auth_service.dart | 7 ++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/models/user/user.dart b/lib/models/user/user.dart index 2621c74..ca0bc41 100644 --- a/lib/models/user/user.dart +++ b/lib/models/user/user.dart @@ -68,4 +68,30 @@ class User extends Persona { @override String toString() => jsonEncode(toJson()); + + User copyWith({ + Rut? rut, + String? nombreCompleto, + String? token, + String? correoPersonal, + String? correoUtem, + String? fotoBase64, + List? perfiles, + String? nombres, + String? apellidos, + String? username, + String? fotoUrl + }) => User( + rut: rut ?? this.rut, + nombreCompleto: nombreCompleto ?? this.nombreCompleto, + token: token ?? this.token, + correoPersonal: correoPersonal ?? this.correoPersonal, + correoUtem: correoUtem ?? this.correoUtem, + fotoBase64: fotoBase64 ?? this.fotoBase64, + perfiles: perfiles ?? this.perfiles, + nombres: nombres ?? this.nombres, + apellidos: apellidos ?? this.apellidos, + username: username ?? this.username, + fotoUrl: fotoUrl ?? this.fotoUrl + ); } \ No newline at end of file diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 6ff0943..59fef21 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -32,7 +32,7 @@ class AuthService { final user = await getUser(); final userToken = user?.token; if(user == null || userToken == null) { - logger.d("[AuthService#isLoggedIn]: user || token => false => ${user == null} || ${userToken == null}"); + logger.d("[AuthService#isLoggedIn]: Usuario o token nulo (user: ${user == null}, token: ${userToken == null})"); return false; } @@ -50,10 +50,7 @@ class AuthService { try { final token = await _authRepository.refresh(token: userToken, credentials: credentials); - - final userJson = user.toJson(); - userJson["token"] = token; - await setUser(User.fromJson(userJson)); + await setUser(user.copyWith(token: token)); Preferencia.lastLogin.set(now.toIso8601String()); return true; } catch (e) { From a182c6a9001f67c2e2ef2b64bd16adb281ed3166 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:09:00 -0400 Subject: [PATCH 130/194] patch: se repara iniciales de la persona y optimiza carga de docente Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/asignaturas/asignatura.dart | 16 ++++++++++++---- lib/models/user/persona.dart | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/models/asignaturas/asignatura.dart b/lib/models/asignaturas/asignatura.dart index 615d269..4eb3348 100644 --- a/lib/models/asignaturas/asignatura.dart +++ b/lib/models/asignaturas/asignatura.dart @@ -68,11 +68,19 @@ class Asignatura { return null; } - final nombreCompleto = docente.split(" ").where((element) => int.tryParse(element) == null).join(" ").replaceAll("- ", ""); - final digitos = int.tryParse(docente.split(" ").where((element) => int.tryParse(element) != null).join(" ").replaceAll("-", "")); + final partes = docente.split("-"); + if(partes.isEmpty) { + return null; + } + + if(partes.length == 1) { + return Persona(nombreCompleto: capitalize(partes.first)); + } + + final numeroRut = int.tryParse(partes.first); return Persona( - nombreCompleto: capitalize(nombreCompleto), - rut: digitos != null ? Rut(digitos) : null, + nombreCompleto: capitalize(partes.last.trim()), + rut: numeroRut != null ? Rut(numeroRut) : null, ); }) ?? Persona(nombreCompleto: "Sin Docente"), seccion: json['seccion'], diff --git a/lib/models/user/persona.dart b/lib/models/user/persona.dart index dce186e..c0afb10 100644 --- a/lib/models/user/persona.dart +++ b/lib/models/user/persona.dart @@ -11,9 +11,9 @@ class Persona { this.rut }); - String get nombreCompletoCapitalizado => capitalize(nombreCompleto); + String get nombreCompletoCapitalizado => capitalize(nombreCompleto.trim()); String get primerNombre => nombreCompletoCapitalizado.split(' ')[0]; - String get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0].toUpperCase()).join(''); + String get iniciales => nombreCompletoCapitalizado.split(' ').map((it) => it[0]).join(''); factory Persona.fromJson(Map json) => Persona( nombreCompleto: json['nombreCompleto'], From ead888459839f18388372ecf0453df27b6967b7b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:11:42 -0400 Subject: [PATCH 131/194] feat: se agrega workflow para publicar beta de android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (iOS está a la espera porque necesitamos la App Store Connect API) Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/android-dev.yml | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/android-dev.yml diff --git a/.github/workflows/android-dev.yml b/.github/workflows/android-dev.yml new file mode 100644 index 0000000..bcdc6a9 --- /dev/null +++ b/.github/workflows/android-dev.yml @@ -0,0 +1,44 @@ +name: Publicar Aplicación Beta +on: + push: + branches: [dev] + +jobs: + build: + runs-on: ubuntu-latest + environment: development + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3.0' + bundler-cache: true + - name: Publicar Beta Android + uses: maierj/fastlane-action@v3.1.0 + env: + APP_IDENTIFIER_IOS: ${{ secrets.APP_IDENTIFIER_IOS }} + APP_IDENTIFIER_ANDROID: ${{ secrets.APP_IDENTIFIER_ANDROID }} + + SLACK_URL: ${{ secrets.SLACK_URL }} + APPLE_ID: ${{ secrets.APPLE_ID }} + + FASTLANE_USER: ${{ secrets.FASTLANE_USER }} + FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} + FASTLANE_ITC_TEAM_ID: ${{ secrets.FASTLANE_ITC_TEAM_ID }} + FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + MATCH_REPO_GIT_URL: ${{ secrets.MATCH_REPO_GIT_URL }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + + KEYCHAIN_NAME: ${{ secrets.KEYCHAIN_NAME }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + + FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} + with: + lane: 'upload' + options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' \ No newline at end of file From fd4c3117525763cc9bf6b05b5a55889a1f84ffbd Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:31:22 -0400 Subject: [PATCH 132/194] patch: bump version y mejora a workflow Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/{android-dev.yml => dev.yml} | 13 ++++++++++--- pubspec.yaml | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) rename .github/workflows/{android-dev.yml => dev.yml} (78%) diff --git a/.github/workflows/android-dev.yml b/.github/workflows/dev.yml similarity index 78% rename from .github/workflows/android-dev.yml rename to .github/workflows/dev.yml index bcdc6a9..3d7fc0e 100644 --- a/.github/workflows/android-dev.yml +++ b/.github/workflows/dev.yml @@ -5,7 +5,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: macos-latest environment: development steps: - uses: actions/checkout@v4 @@ -13,7 +13,7 @@ jobs: with: ruby-version: '3.3.0' bundler-cache: true - - name: Publicar Beta Android + - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: APP_IDENTIFIER_IOS: ${{ secrets.APP_IDENTIFIER_IOS }} @@ -41,4 +41,11 @@ jobs: FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} with: lane: 'upload' - options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' \ No newline at end of file + options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' + - name: Publicar Version en Git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add pubspec.yaml + git commit -m "ci(bump-version): github action bump version" + git push \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index b5beed3..84b7284 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+108 +version: 3.0.0+109 environment: sdk: ">=2.17.0 <3.0.0" From d6ef92158dfff9bad9f9259e50a5f48028c9f11b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:34:20 -0400 Subject: [PATCH 133/194] patch: actualizados triggers de workflow Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 3d7fc0e..1d87583 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,7 +1,8 @@ name: Publicar Aplicación Beta on: - push: + pull_request: branches: [dev] + types: [opened, synchronize] jobs: build: From 66ede76fef99ed20bbcedb35fd4e14862a7e968a Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:39:15 -0400 Subject: [PATCH 134/194] patch: se agrega setup de flutter Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 10 +++++++++- fastlane/Fastfile | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 1d87583..4d1cbf4 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -10,10 +10,18 @@ jobs: environment: development steps: - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 + - name: Instalar Ruby + uses: ruby/setup-ruby@v1 with: ruby-version: '3.3.0' bundler-cache: true + - name: Instalar Flutter + uses: flutter-actions/setup-flutter@v3 + with: + channel: stable + version: 3.7.12 + - name: Instalar Dependencias + run: flutter pub get - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5248997..fa0ccea 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -141,6 +141,7 @@ platform :mobile do skip_git_push = false skip_slack = false is_ci = false + increment_build_number = false puts "Construyendo app con version #{build_name} (#{build_number})" @@ -188,6 +189,10 @@ platform :mobile do is_ci = options[:is_ci] end + if options[:increment_build_number] + increment_build_number = options[:increment_build_number] + end + if build_name build_name = build_name.tr('v', '') end @@ -261,6 +266,8 @@ platform :mobile do end puts "App construida y publicada con éxito" - increment_pubspec_build_number() + unless increment_build_number + increment_pubspec_build_number() + end end end \ No newline at end of file From 1d23bfb068fba919e1867ec92ef452eb4cd1a1d4 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:44:49 -0400 Subject: [PATCH 135/194] patch: se agrega setup de java Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 4d1cbf4..417742c 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -15,6 +15,10 @@ jobs: with: ruby-version: '3.3.0' bundler-cache: true + - uses: actions/setup-java@v4 + with: + distribution: 'zulu' # See 'Supported distributions' for available options + java-version: '17' - name: Instalar Flutter uses: flutter-actions/setup-flutter@v3 with: From fd88ef5821303b4a4d12be82d565dd4be3172f45 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 08:56:49 -0400 Subject: [PATCH 136/194] patch: se repara workflow Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 417742c..7c59024 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -15,7 +15,8 @@ jobs: with: ruby-version: '3.3.0' bundler-cache: true - - uses: actions/setup-java@v4 + - name: Instalar Java + uses: actions/setup-java@v4 with: distribution: 'zulu' # See 'Supported distributions' for available options java-version: '17' @@ -26,6 +27,8 @@ jobs: version: 3.7.12 - name: Instalar Dependencias run: flutter pub get + - name: Copiar .env + run: cp .env.example .env - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: From c9ac4e8f0ddeee2c5ab1dd1c604a6642b8f5da9a Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 09:17:11 -0400 Subject: [PATCH 137/194] patch: se agrega google-services al workflow Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 7c59024..294813f 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -29,6 +29,10 @@ jobs: run: flutter pub get - name: Copiar .env run: cp .env.example .env + - name: Generar Google Services + env: + GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} + run: echo $GOOGLE_SERVICES > android/app/google-services.json - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: From 4707826593e8c0b4415355debca01a9d4f6dd4f6 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Mon, 27 May 2024 10:52:16 -0400 Subject: [PATCH 138/194] patch: generar api playstore en workflow Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 294813f..9e578bb 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -33,6 +33,10 @@ jobs: env: GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} run: echo $GOOGLE_SERVICES > android/app/google-services.json + - name: Generar Api PlayStore + env: + API_PLAY_STORE: ${{ secrets.API_PLAY_STORE }} + run: echo $API_PLAY_STORE > android/key.properties - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: From d77137e956b5b7ca595c432f38239da8aa391f56 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 11:30:18 -0400 Subject: [PATCH 139/194] patch: se repara api-playstore Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 9e578bb..1ed0696 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -36,7 +36,7 @@ jobs: - name: Generar Api PlayStore env: API_PLAY_STORE: ${{ secrets.API_PLAY_STORE }} - run: echo $API_PLAY_STORE > android/key.properties + run: echo $API_PLAY_STORE > android/api-playstore.json - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: From 324dd6391f1383ca026a34fba0be574b31720302 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 11:54:02 -0400 Subject: [PATCH 140/194] patch: se agregan archivos de keystore Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 1ed0696..4faca95 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -29,14 +29,18 @@ jobs: run: flutter pub get - name: Copiar .env run: cp .env.example .env - - name: Generar Google Services + - name: Generar Archivos de Google Play env: GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} - run: echo $GOOGLE_SERVICES > android/app/google-services.json - - name: Generar Api PlayStore - env: API_PLAY_STORE: ${{ secrets.API_PLAY_STORE }} - run: echo $API_PLAY_STORE > android/api-playstore.json + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} + run: | + echo $GOOGLE_SERVICES > android/app/google-services.json + echo $API_PLAY_STORE > android/api-playstore.json + echo $KEYSTORE_PROPERTIES > android/keystores/key.properties + echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks + - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: From e43e52acc9e3ecf7902266139e1b7f31011aec3d Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 11:57:32 -0400 Subject: [PATCH 141/194] patch: se crea carpeta keystores Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 4faca95..0c3c783 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -38,6 +38,7 @@ jobs: run: | echo $GOOGLE_SERVICES > android/app/google-services.json echo $API_PLAY_STORE > android/api-playstore.json + mkdir -p android/keystores echo $KEYSTORE_PROPERTIES > android/keystores/key.properties echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks From e2ac46403f8440ccc0c7986c0dd1a0b01b3002f8 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 12:23:33 -0400 Subject: [PATCH 142/194] =?UTF-8?q?patch:=20se=20agrega=20chmod=20a=20la?= =?UTF-8?q?=20generaci=C3=B3n=20de=20archivos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 4 ++++ android/app/build.gradle | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 0c3c783..0adc0aa 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -38,10 +38,14 @@ jobs: run: | echo $GOOGLE_SERVICES > android/app/google-services.json echo $API_PLAY_STORE > android/api-playstore.json + mkdir -p android/keystores + chmod 755 android/keystores + echo $KEYSTORE_PROPERTIES > android/keystores/key.properties echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks + chmod 644 android/keystores/key* - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: diff --git a/android/app/build.gradle b/android/app/build.gradle index fe0da47..77e7f4e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,6 @@ android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "cl.inndev.miutem" minSdkVersion 23 multiDexEnabled true From 31a9379761a31c30744a13479842e1e8a78f77d7 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 13:01:37 -0400 Subject: [PATCH 143/194] patch: se utilizan datos codificados en base64 Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 0adc0aa..429ca0d 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -18,13 +18,17 @@ jobs: - name: Instalar Java uses: actions/setup-java@v4 with: - distribution: 'zulu' # See 'Supported distributions' for available options + distribution: 'zulu' java-version: '17' + cache: 'gradle' - name: Instalar Flutter uses: flutter-actions/setup-flutter@v3 with: channel: stable version: 3.7.12 + cache: true + cache-sdk: true + cache-key: mi-utem - name: Instalar Dependencias run: flutter pub get - name: Copiar .env @@ -36,13 +40,13 @@ jobs: KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} run: | - echo $GOOGLE_SERVICES > android/app/google-services.json - echo $API_PLAY_STORE > android/api-playstore.json + echo $GOOGLE_SERVICES | base64 --decode > android/app/google-services.json + echo $API_PLAY_STORE | base64 --decode > android/api-playstore.json mkdir -p android/keystores chmod 755 android/keystores - echo $KEYSTORE_PROPERTIES > android/keystores/key.properties + echo $KEYSTORE_PROPERTIES | base64 --decode > android/keystores/key.properties echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks chmod 644 android/keystores/key* From 03679d335390a22a0796808fb5ece3c127176b8c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 13:17:53 -0400 Subject: [PATCH 144/194] =?UTF-8?q?patch:=20se=20repara=20git=20push=20y?= =?UTF-8?q?=20se=20hace=20bump=20a=20la=20versi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 1 + pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 429ca0d..e650ad3 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -80,6 +80,7 @@ jobs: lane: 'upload' options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' - name: Publicar Version en Git + uses: actions/checkout@v4 run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" diff --git a/pubspec.yaml b/pubspec.yaml index 84b7284..ea5f7bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+109 +version: 3.0.0+110 environment: sdk: ">=2.17.0 <3.0.0" From 5715c1e790c478f31062484040ebc0bdea3f3882 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 13:26:20 -0400 Subject: [PATCH 145/194] patch: se agrega que solo se construya la app usando un comando Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index e650ad3..026c521 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,11 +1,10 @@ name: Publicar Aplicación Beta -on: - pull_request: - branches: [dev] - types: [opened, synchronize] +on: issue_comment jobs: build: + # Formato de comentario: /deploy-beta + if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/deploy-beta') }} runs-on: macos-latest environment: development steps: From 9948c68aa3583a6553025aef6fbc4ce32e98b641 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 13:30:35 -0400 Subject: [PATCH 146/194] patch: se agrega referencia de la pull_request Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 026c521..c4e46f4 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -9,6 +9,8 @@ jobs: environment: development steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} - name: Instalar Ruby uses: ruby/setup-ruby@v1 with: @@ -79,7 +81,6 @@ jobs: lane: 'upload' options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' - name: Publicar Version en Git - uses: actions/checkout@v4 run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" From 13701e353a282c8db19c866eaea7ce12f02e5758 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 13:38:41 -0400 Subject: [PATCH 147/194] patch: solo miembros pueden realizar deploy a la app Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index c4e46f4..876014a 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -4,7 +4,7 @@ on: issue_comment jobs: build: # Formato de comentario: /deploy-beta - if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/deploy-beta') }} + if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/deploy-beta') && github.event.comment.author_association == 'MEMBER' && github.event.issue.author_association == 'MEMBER' && github.event.issue.state == 'open' && github.event.issue.locked == false && github.event.issue.active_lock_reason == null }} runs-on: macos-latest environment: development steps: From dddeaf25370a0bc68180472eb398cc1786e3ca28 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 13:44:52 -0400 Subject: [PATCH 148/194] =?UTF-8?q?patch:=20se=20mejora=20condici=C3=B3n?= =?UTF-8?q?=20para=20ejecutar=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 876014a..3cddcfd 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -4,7 +4,8 @@ on: issue_comment jobs: build: # Formato de comentario: /deploy-beta - if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/deploy-beta') && github.event.comment.author_association == 'MEMBER' && github.event.issue.author_association == 'MEMBER' && github.event.issue.state == 'open' && github.event.issue.locked == false && github.event.issue.active_lock_reason == null }} + # Si es pull request, y el comentario contiene '/deploy-beta', y el autor del comentario y del issue son miembros, y el issue está abierto, no está bloqueado, y el pull request no está fusionado + if: github.event.issue.pull_request && contains(github.event.comment.body, '/deploy-beta') && github.event.comment.author_association == 'MEMBER' && github.event.issue.state == 'open' && github.event.issue.locked == false && github.event.issue.active_lock_reason == null && github.event.issue.pull_request.merged_at == null runs-on: macos-latest environment: development steps: From fbb2ad97dfb5a0a53ee98392db30c23520d63372 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 13:49:33 -0400 Subject: [PATCH 149/194] =?UTF-8?q?patch:=20se=20agrega=20informaci=C3=B3n?= =?UTF-8?q?=20de=20testeo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 3cddcfd..8937653 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -2,10 +2,22 @@ name: Publicar Aplicación Beta on: issue_comment jobs: + test: + runs-on: ubuntu-latest + steps: + - name: print-permissions + run: | + echo "GitHub Is Pull Request: ${{ !!github.event.issue.pull_request }}" + echo "GitHub Comment Body: ${{ github.event.comment.body }}" + echo "GitHub Comment Author Association: ${{ github.event.comment.author_association }}" + echo "GitHub Issue State: ${{ github.event.issue.state }}" + echo "GitHub Issue Locked: ${{ github.event.issue.locked }}" + echo "GitHub Issue Active Lock Reason: ${{ github.event.issue.active_lock_reason }}" + echo "GitHub Issue Pull Request Merged At: ${{ github.event.issue.pull_request.merged_at }}" build: # Formato de comentario: /deploy-beta # Si es pull request, y el comentario contiene '/deploy-beta', y el autor del comentario y del issue son miembros, y el issue está abierto, no está bloqueado, y el pull request no está fusionado - if: github.event.issue.pull_request && contains(github.event.comment.body, '/deploy-beta') && github.event.comment.author_association == 'MEMBER' && github.event.issue.state == 'open' && github.event.issue.locked == false && github.event.issue.active_lock_reason == null && github.event.issue.pull_request.merged_at == null + if: github.event.issue.pull_request && github.event.comment.body == '/deploy-beta' && github.event.comment.author_association == 'MEMBER' && github.event.issue.state == 'open' && github.event.issue.locked == false && github.event.issue.active_lock_reason == null && github.event.issue.pull_request.merged_at == null runs-on: macos-latest environment: development steps: From df9e2e5ffab1455367ad7bd1dccb48fbe9346910 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 13:54:13 -0400 Subject: [PATCH 150/194] patch: se ejecuta al crear un comentario de issue Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 8937653..14de4e8 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,5 +1,7 @@ name: Publicar Aplicación Beta -on: issue_comment +on: + issue_comment: + types: [created] jobs: test: From 1d87f28506f5860b6ce4ec16771785f16aff2667 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 14:04:40 -0400 Subject: [PATCH 151/194] =?UTF-8?q?patch:=20mejoras=20a=20la=20condici?= =?UTF-8?q?=C3=B3n=20del=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 14de4e8..72352b5 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -4,22 +4,17 @@ on: types: [created] jobs: - test: - runs-on: ubuntu-latest - steps: - - name: print-permissions - run: | - echo "GitHub Is Pull Request: ${{ !!github.event.issue.pull_request }}" - echo "GitHub Comment Body: ${{ github.event.comment.body }}" - echo "GitHub Comment Author Association: ${{ github.event.comment.author_association }}" - echo "GitHub Issue State: ${{ github.event.issue.state }}" - echo "GitHub Issue Locked: ${{ github.event.issue.locked }}" - echo "GitHub Issue Active Lock Reason: ${{ github.event.issue.active_lock_reason }}" - echo "GitHub Issue Pull Request Merged At: ${{ github.event.issue.pull_request.merged_at }}" build: # Formato de comentario: /deploy-beta # Si es pull request, y el comentario contiene '/deploy-beta', y el autor del comentario y del issue son miembros, y el issue está abierto, no está bloqueado, y el pull request no está fusionado - if: github.event.issue.pull_request && github.event.comment.body == '/deploy-beta' && github.event.comment.author_association == 'MEMBER' && github.event.issue.state == 'open' && github.event.issue.locked == false && github.event.issue.active_lock_reason == null && github.event.issue.pull_request.merged_at == null + if: | + (github.event_name == 'issue_comment' && github.event.issue.pull_request) && + (github.event.comment.body == '/deploy-beta') && + (github.event.comment.author_association == 'MEMBER' && github.event.issue.author_association == 'MEMBER') && + (!github.event.pull_request.draft && github.event.pull_request.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open') && + (!github.event.issue.locked && !github.event.issue.active_lock_reason) && + (!github.event.issue.pull_request.merged_at) + runs-on: macos-latest environment: development steps: From dca73df5308efe81f76d87f6568f80736b34eb31 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 14:06:44 -0400 Subject: [PATCH 152/194] =?UTF-8?q?patch:=20mejoras=20a=20la=20condici?= =?UTF-8?q?=C3=B3n=20del=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 72352b5..7aaab7f 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,5 +1,8 @@ name: Publicar Aplicación Beta on: + pull_request: + branches: [ dev ] + types: [ opened, synchronize ] issue_comment: types: [created] From 0e87c8419efed8419d03bac2bb5eb443298cc214 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 14:11:02 -0400 Subject: [PATCH 153/194] =?UTF-8?q?patch:=20arreglada=20ejecuci=C3=B3n=20d?= =?UTF-8?q?el=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ahora necesita que el commit contenga '@exdevutembot mi-utem deploy-beta' para publicar la beta de la app. Esto solo funciona con PRs Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 7aaab7f..5de8df0 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -3,20 +3,18 @@ on: pull_request: branches: [ dev ] types: [ opened, synchronize ] - issue_comment: - types: [created] jobs: build: # Formato de comentario: /deploy-beta - # Si es pull request, y el comentario contiene '/deploy-beta', y el autor del comentario y del issue son miembros, y el issue está abierto, no está bloqueado, y el pull request no está fusionado + # Si es pull request y el autor del comentario y del issue son miembros, y el issue está abierto, no está bloqueado, y el pull request no está fusionado y el último commit contiene '@exdevutembot mi-utem deploy-beta' if: | (github.event_name == 'issue_comment' && github.event.issue.pull_request) && - (github.event.comment.body == '/deploy-beta') && (github.event.comment.author_association == 'MEMBER' && github.event.issue.author_association == 'MEMBER') && (!github.event.pull_request.draft && github.event.pull_request.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open') && (!github.event.issue.locked && !github.event.issue.active_lock_reason) && - (!github.event.issue.pull_request.merged_at) + (!github.event.issue.pull_request.merged_at) && + contains(github.event.pull_request.head.message, '@exdevutembot mi-utem deploy-beta') runs-on: macos-latest environment: development From a51d13737135e07fdb5d5132e0e117eb66ff4812 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 14:53:18 -0400 Subject: [PATCH 154/194] patch: se mejora condiciones del workflow @exdevutembot mi-utem deploy-beta Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 5de8df0..563ad2a 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -9,9 +9,9 @@ jobs: # Formato de comentario: /deploy-beta # Si es pull request y el autor del comentario y del issue son miembros, y el issue está abierto, no está bloqueado, y el pull request no está fusionado y el último commit contiene '@exdevutembot mi-utem deploy-beta' if: | - (github.event_name == 'issue_comment' && github.event.issue.pull_request) && + github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' && github.event.issue.author_association == 'MEMBER') && - (!github.event.pull_request.draft && github.event.pull_request.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open') && + (github.event.issue.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open') && (!github.event.issue.locked && !github.event.issue.active_lock_reason) && (!github.event.issue.pull_request.merged_at) && contains(github.event.pull_request.head.message, '@exdevutembot mi-utem deploy-beta') From 10144206761d3d3a28c7fe36d1db4009d775f9e5 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 14:55:58 -0400 Subject: [PATCH 155/194] =?UTF-8?q?patch:=20imprimir=20informaci=C3=B3n=20?= =?UTF-8?q?de=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 563ad2a..7890b98 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -5,13 +5,28 @@ on: types: [ opened, synchronize ] jobs: + debug-print: + runs-on: ubuntu-latest + steps: + - run: | + echo "Tiene PR: ${{ !!github.event.issue.pull_request }}" + echo "Autor del Comentario: ${{ github.event.comment.author_association }}" + echo "Autor del Issue: ${{ github.event.issue.author_association }}" + echo "Estado del Issue: ${{ github.event.issue.state }}" + echo "Bloqueado: ${{ github.event.issue.locked }}" + echo "Razón del Bloqueo: ${{ github.event.issue.active_lock_reason }}" + echo "Fusionado: ${{ github.event.issue.pull_request.merged_at }}" + echo "Mensaje del Commit: ${{ github.event.pull_request.head.message }}" + echo "Contiene: ${{ contains(github.event.pull_request.head.message, '@exdevutembot mi-utem deploy-beta') }}" + echo "Comentario: ${{ github.event.comment.body }}" + build: # Formato de comentario: /deploy-beta # Si es pull request y el autor del comentario y del issue son miembros, y el issue está abierto, no está bloqueado, y el pull request no está fusionado y el último commit contiene '@exdevutembot mi-utem deploy-beta' if: | github.event.issue.pull_request && - (github.event.comment.author_association == 'MEMBER' && github.event.issue.author_association == 'MEMBER') && - (github.event.issue.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open') && + github.event.issue.author_association == 'MEMBER' && + (github.event.issue.state == 'open' && !github.event.issue.draft && github.event.issue.state == 'open') && (!github.event.issue.locked && !github.event.issue.active_lock_reason) && (!github.event.issue.pull_request.merged_at) && contains(github.event.pull_request.head.message, '@exdevutembot mi-utem deploy-beta') From f9f7d335d2347ffdf7569e07fc9a46a3f1adc774 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 14:57:55 -0400 Subject: [PATCH 156/194] patch: cambio de trigger del workflow Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 7890b98..5d019df 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,8 +1,5 @@ name: Publicar Aplicación Beta -on: - pull_request: - branches: [ dev ] - types: [ opened, synchronize ] +on: issue_comment jobs: debug-print: @@ -16,13 +13,11 @@ jobs: echo "Bloqueado: ${{ github.event.issue.locked }}" echo "Razón del Bloqueo: ${{ github.event.issue.active_lock_reason }}" echo "Fusionado: ${{ github.event.issue.pull_request.merged_at }}" - echo "Mensaje del Commit: ${{ github.event.pull_request.head.message }}" - echo "Contiene: ${{ contains(github.event.pull_request.head.message, '@exdevutembot mi-utem deploy-beta') }}" echo "Comentario: ${{ github.event.comment.body }}" - + echo "Último Commit: ${{ github.event.pull_request.head.message }}" + echo "Ref: ${{ github.event.pull_request.head.ref }}" build: # Formato de comentario: /deploy-beta - # Si es pull request y el autor del comentario y del issue son miembros, y el issue está abierto, no está bloqueado, y el pull request no está fusionado y el último commit contiene '@exdevutembot mi-utem deploy-beta' if: | github.event.issue.pull_request && github.event.issue.author_association == 'MEMBER' && From 0d4302b801eb0fccef61b8cd4f34956505b49efb Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 15:05:01 -0400 Subject: [PATCH 157/194] patch: test del workflow Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 190 +++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 93 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 5d019df..d8012f2 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,5 +1,9 @@ name: Publicar Aplicación Beta -on: issue_comment +on: + issues: + issue_comment: + pull_request: + types: [opened, reopened, synchronize, closed] jobs: debug-print: @@ -16,95 +20,95 @@ jobs: echo "Comentario: ${{ github.event.comment.body }}" echo "Último Commit: ${{ github.event.pull_request.head.message }}" echo "Ref: ${{ github.event.pull_request.head.ref }}" - build: - # Formato de comentario: /deploy-beta - if: | - github.event.issue.pull_request && - github.event.issue.author_association == 'MEMBER' && - (github.event.issue.state == 'open' && !github.event.issue.draft && github.event.issue.state == 'open') && - (!github.event.issue.locked && !github.event.issue.active_lock_reason) && - (!github.event.issue.pull_request.merged_at) && - contains(github.event.pull_request.head.message, '@exdevutembot mi-utem deploy-beta') - - runs-on: macos-latest - environment: development - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - - name: Instalar Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3.0' - bundler-cache: true - - name: Instalar Java - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - cache: 'gradle' - - name: Instalar Flutter - uses: flutter-actions/setup-flutter@v3 - with: - channel: stable - version: 3.7.12 - cache: true - cache-sdk: true - cache-key: mi-utem - - name: Instalar Dependencias - run: flutter pub get - - name: Copiar .env - run: cp .env.example .env - - name: Generar Archivos de Google Play - env: - GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} - API_PLAY_STORE: ${{ secrets.API_PLAY_STORE }} - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} - KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} - run: | - echo $GOOGLE_SERVICES | base64 --decode > android/app/google-services.json - echo $API_PLAY_STORE | base64 --decode > android/api-playstore.json - - mkdir -p android/keystores - chmod 755 android/keystores - - echo $KEYSTORE_PROPERTIES | base64 --decode > android/keystores/key.properties - echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks - - chmod 644 android/keystores/key* - - name: Publicar Beta - uses: maierj/fastlane-action@v3.1.0 - env: - APP_IDENTIFIER_IOS: ${{ secrets.APP_IDENTIFIER_IOS }} - APP_IDENTIFIER_ANDROID: ${{ secrets.APP_IDENTIFIER_ANDROID }} - - SLACK_URL: ${{ secrets.SLACK_URL }} - APPLE_ID: ${{ secrets.APPLE_ID }} - - FASTLANE_USER: ${{ secrets.FASTLANE_USER }} - FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} - FASTLANE_ITC_TEAM_ID: ${{ secrets.FASTLANE_ITC_TEAM_ID }} - FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} - FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} - - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - MATCH_REPO_GIT_URL: ${{ secrets.MATCH_REPO_GIT_URL }} - MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} - MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }} - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - - KEYCHAIN_NAME: ${{ secrets.KEYCHAIN_NAME }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - - FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} - with: - lane: 'upload' - options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' - - name: Publicar Version en Git - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add pubspec.yaml - git commit -m "ci(bump-version): github action bump version" - git push \ No newline at end of file +# build: +# # Formato de comentario: /deploy-beta +# if: | +# (github.event_name == 'issue_comment' && github.event.issue.pull_request) && +# github.event.issue.author_association == 'MEMBER' && +# (github.event.issue.state == 'open' && !github.event.issue.draft && github.event.issue.state == 'open') && +# (!github.event.issue.locked && !github.event.issue.active_lock_reason) && +# (!github.event.issue.pull_request.merged_at) && +# contains(github.event.pull_request.head.message, '@exdevutembot mi-utem deploy-beta') +# +# runs-on: macos-latest +# environment: development +# steps: +# - uses: actions/checkout@v4 +# with: +# ref: ${{ github.event.pull_request.head.ref }} +# - name: Instalar Ruby +# uses: ruby/setup-ruby@v1 +# with: +# ruby-version: '3.3.0' +# bundler-cache: true +# - name: Instalar Java +# uses: actions/setup-java@v4 +# with: +# distribution: 'zulu' +# java-version: '17' +# cache: 'gradle' +# - name: Instalar Flutter +# uses: flutter-actions/setup-flutter@v3 +# with: +# channel: stable +# version: 3.7.12 +# cache: true +# cache-sdk: true +# cache-key: mi-utem +# - name: Instalar Dependencias +# run: flutter pub get +# - name: Copiar .env +# run: cp .env.example .env +# - name: Generar Archivos de Google Play +# env: +# GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} +# API_PLAY_STORE: ${{ secrets.API_PLAY_STORE }} +# KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} +# KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} +# run: | +# echo $GOOGLE_SERVICES | base64 --decode > android/app/google-services.json +# echo $API_PLAY_STORE | base64 --decode > android/api-playstore.json +# +# mkdir -p android/keystores +# chmod 755 android/keystores +# +# echo $KEYSTORE_PROPERTIES | base64 --decode > android/keystores/key.properties +# echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks +# +# chmod 644 android/keystores/key* +# - name: Publicar Beta +# uses: maierj/fastlane-action@v3.1.0 +# env: +# APP_IDENTIFIER_IOS: ${{ secrets.APP_IDENTIFIER_IOS }} +# APP_IDENTIFIER_ANDROID: ${{ secrets.APP_IDENTIFIER_ANDROID }} +# +# SLACK_URL: ${{ secrets.SLACK_URL }} +# APPLE_ID: ${{ secrets.APPLE_ID }} +# +# FASTLANE_USER: ${{ secrets.FASTLANE_USER }} +# FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} +# FASTLANE_ITC_TEAM_ID: ${{ secrets.FASTLANE_ITC_TEAM_ID }} +# FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} +# FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} +# +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# +# MATCH_REPO_GIT_URL: ${{ secrets.MATCH_REPO_GIT_URL }} +# MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} +# MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }} +# MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} +# +# KEYCHAIN_NAME: ${{ secrets.KEYCHAIN_NAME }} +# KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} +# +# FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} +# with: +# lane: 'upload' +# options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' +# - name: Publicar Version en Git +# run: | +# git config user.name "github-actions[bot]" +# git config user.email "41898282+github-actions[bot]@users.noreply.github.com" +# git add pubspec.yaml +# git commit -m "ci(bump-version): github action bump version" +# git push \ No newline at end of file From 96753d6d53efbccac898c71da5613a0fbd3bdd4b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 16:12:13 -0400 Subject: [PATCH 158/194] patch: workflow se ejecuta de todas formas al hacer push, pero solo si la PR es de un miembro Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 195 +++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 107 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index d8012f2..76cf506 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,114 +1,95 @@ name: Publicar Aplicación Beta on: - issues: - issue_comment: pull_request: types: [opened, reopened, synchronize, closed] jobs: - debug-print: - runs-on: ubuntu-latest + build: + # Formato de comentario: /deploy-beta + if: | + github.event.issue.author_association == 'MEMBER' && + (github.event.issue.state == 'open' && !github.event.issue.draft && github.event.issue.state == 'open') && + (!github.event.issue.locked && !github.event.issue.active_lock_reason) && + (!github.event.issue.pull_request.merged_at) + runs-on: macos-latest + environment: development steps: - - run: | - echo "Tiene PR: ${{ !!github.event.issue.pull_request }}" - echo "Autor del Comentario: ${{ github.event.comment.author_association }}" - echo "Autor del Issue: ${{ github.event.issue.author_association }}" - echo "Estado del Issue: ${{ github.event.issue.state }}" - echo "Bloqueado: ${{ github.event.issue.locked }}" - echo "Razón del Bloqueo: ${{ github.event.issue.active_lock_reason }}" - echo "Fusionado: ${{ github.event.issue.pull_request.merged_at }}" - echo "Comentario: ${{ github.event.comment.body }}" - echo "Último Commit: ${{ github.event.pull_request.head.message }}" - echo "Ref: ${{ github.event.pull_request.head.ref }}" -# build: -# # Formato de comentario: /deploy-beta -# if: | -# (github.event_name == 'issue_comment' && github.event.issue.pull_request) && -# github.event.issue.author_association == 'MEMBER' && -# (github.event.issue.state == 'open' && !github.event.issue.draft && github.event.issue.state == 'open') && -# (!github.event.issue.locked && !github.event.issue.active_lock_reason) && -# (!github.event.issue.pull_request.merged_at) && -# contains(github.event.pull_request.head.message, '@exdevutembot mi-utem deploy-beta') -# -# runs-on: macos-latest -# environment: development -# steps: -# - uses: actions/checkout@v4 -# with: -# ref: ${{ github.event.pull_request.head.ref }} -# - name: Instalar Ruby -# uses: ruby/setup-ruby@v1 -# with: -# ruby-version: '3.3.0' -# bundler-cache: true -# - name: Instalar Java -# uses: actions/setup-java@v4 -# with: -# distribution: 'zulu' -# java-version: '17' -# cache: 'gradle' -# - name: Instalar Flutter -# uses: flutter-actions/setup-flutter@v3 -# with: -# channel: stable -# version: 3.7.12 -# cache: true -# cache-sdk: true -# cache-key: mi-utem -# - name: Instalar Dependencias -# run: flutter pub get -# - name: Copiar .env -# run: cp .env.example .env -# - name: Generar Archivos de Google Play -# env: -# GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} -# API_PLAY_STORE: ${{ secrets.API_PLAY_STORE }} -# KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} -# KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} -# run: | -# echo $GOOGLE_SERVICES | base64 --decode > android/app/google-services.json -# echo $API_PLAY_STORE | base64 --decode > android/api-playstore.json -# -# mkdir -p android/keystores -# chmod 755 android/keystores -# -# echo $KEYSTORE_PROPERTIES | base64 --decode > android/keystores/key.properties -# echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks -# -# chmod 644 android/keystores/key* -# - name: Publicar Beta -# uses: maierj/fastlane-action@v3.1.0 -# env: -# APP_IDENTIFIER_IOS: ${{ secrets.APP_IDENTIFIER_IOS }} -# APP_IDENTIFIER_ANDROID: ${{ secrets.APP_IDENTIFIER_ANDROID }} -# -# SLACK_URL: ${{ secrets.SLACK_URL }} -# APPLE_ID: ${{ secrets.APPLE_ID }} -# -# FASTLANE_USER: ${{ secrets.FASTLANE_USER }} -# FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} -# FASTLANE_ITC_TEAM_ID: ${{ secrets.FASTLANE_ITC_TEAM_ID }} -# FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} -# FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} -# -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# -# MATCH_REPO_GIT_URL: ${{ secrets.MATCH_REPO_GIT_URL }} -# MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} -# MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }} -# MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} -# -# KEYCHAIN_NAME: ${{ secrets.KEYCHAIN_NAME }} -# KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} -# -# FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} -# with: -# lane: 'upload' -# options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' -# - name: Publicar Version en Git -# run: | -# git config user.name "github-actions[bot]" -# git config user.email "41898282+github-actions[bot]@users.noreply.github.com" -# git add pubspec.yaml -# git commit -m "ci(bump-version): github action bump version" -# git push \ No newline at end of file + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + - name: Instalar Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3.0' + bundler-cache: true + - name: Instalar Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + - name: Instalar Flutter + uses: flutter-actions/setup-flutter@v3 + with: + channel: stable + version: 3.7.12 + cache: true + cache-sdk: true + cache-key: mi-utem + - name: Instalar Dependencias + run: flutter pub get + - name: Copiar .env + run: cp .env.example .env + - name: Generar Archivos de Google Play + env: + GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} + API_PLAY_STORE: ${{ secrets.API_PLAY_STORE }} + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} + run: | + echo $GOOGLE_SERVICES | base64 --decode > android/app/google-services.json + echo $API_PLAY_STORE | base64 --decode > android/api-playstore.json + + mkdir -p android/keystores + chmod 755 android/keystores + + echo $KEYSTORE_PROPERTIES | base64 --decode > android/keystores/key.properties + echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks + + chmod 644 android/keystores/key* + - name: Publicar Beta + uses: maierj/fastlane-action@v3.1.0 + env: + APP_IDENTIFIER_IOS: ${{ secrets.APP_IDENTIFIER_IOS }} + APP_IDENTIFIER_ANDROID: ${{ secrets.APP_IDENTIFIER_ANDROID }} + + SLACK_URL: ${{ secrets.SLACK_URL }} + APPLE_ID: ${{ secrets.APPLE_ID }} + + FASTLANE_USER: ${{ secrets.FASTLANE_USER }} + FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} + FASTLANE_ITC_TEAM_ID: ${{ secrets.FASTLANE_ITC_TEAM_ID }} + FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + MATCH_REPO_GIT_URL: ${{ secrets.MATCH_REPO_GIT_URL }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + + KEYCHAIN_NAME: ${{ secrets.KEYCHAIN_NAME }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + + FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} + with: + lane: 'upload' + options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' + - name: Publicar Version en Git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add pubspec.yaml + git commit -m "ci(bump-version): github action bump version" + git push \ No newline at end of file From 85e045333ea81eb0c0d443b5252579db2f592acb Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 28 May 2024 17:20:10 -0400 Subject: [PATCH 159/194] patch: solo ejecutar el workflow bajo ciertas condiciones Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 76cf506..4aa7256 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -8,9 +8,9 @@ jobs: # Formato de comentario: /deploy-beta if: | github.event.issue.author_association == 'MEMBER' && - (github.event.issue.state == 'open' && !github.event.issue.draft && github.event.issue.state == 'open') && - (!github.event.issue.locked && !github.event.issue.active_lock_reason) && - (!github.event.issue.pull_request.merged_at) + (github.event.issue.state == 'open' && !github.event.issue.draft) && + (!github.event.issue.locked && github.event.issue.active_lock_reason == null) && + (github.event.issue.pull_request.merged_at == null) runs-on: macos-latest environment: development steps: From b9859d3a8a1836a4aaf3aa5380a2d039bab9ba0c Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Wed, 29 May 2024 09:10:39 -0400 Subject: [PATCH 160/194] patch: se repara referencias al evento de PR Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 4aa7256..9e58c89 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -7,10 +7,10 @@ jobs: build: # Formato de comentario: /deploy-beta if: | - github.event.issue.author_association == 'MEMBER' && - (github.event.issue.state == 'open' && !github.event.issue.draft) && - (!github.event.issue.locked && github.event.issue.active_lock_reason == null) && - (github.event.issue.pull_request.merged_at == null) + github.event.pull_request.author_association == 'MEMBER' && + (github.event.pull_request.state == 'open' && !github.event.pull_request.draft) && + (!github.event.pull_request.locked && !github.event.pull_request.active_lock_reason) && + (!github.event.pull_request.merged_at) runs-on: macos-latest environment: development steps: From e981749df5cadb97fa26fbe116cd213cf7ac848d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 13:20:41 +0000 Subject: [PATCH 161/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ea5f7bd..8461852 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+110 +version: 3.0.0+111 environment: sdk: ">=2.17.0 <3.0.0" From 853f2652cea0683a34dc3ae4469aae7a0d9ff03e Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 31 May 2024 10:00:31 -0400 Subject: [PATCH 162/194] =?UTF-8?q?patch:=20se=20agrega=20compilaci=C3=B3n?= =?UTF-8?q?=20autom=C3=A1tica=20de=20iOS=20*=20Se=20agrega=20compilaci?= =?UTF-8?q?=C3=B3n=20autom=C3=A1tica=20de=20iOS=20*=20Se=20elimina=20infor?= =?UTF-8?q?maci=C3=B3n=20de=20debug=20de=20splash=5Fscreen.dart=20*=20Se?= =?UTF-8?q?=20eliminan=20perfiles,=20rut=20y=20correo=20personal=20de=20an?= =?UTF-8?q?al=C3=ADticas=20*=20Se=20elimina=20revisi=C3=B3n=20de=20primera?= =?UTF-8?q?=20vez=20en=20AuthService#isLoggedIn=20*=20Se=20agregan=20clave?= =?UTF-8?q?s=20de=20App=20Store=20Connect=20a=20`.gitignore`=20(archivos?= =?UTF-8?q?=20`.p8`)=20*=20Se=20agrega=20uso=20de=20App=20Store=20Connect?= =?UTF-8?q?=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 10 ++++++++-- .gitignore | 5 +++-- ios/fastlane/Fastfile | 15 +++++++++++++++ lib/screens/splash_screen.dart | 1 - lib/services/analytics_service.dart | 9 +++------ lib/services/auth_service.dart | 5 ----- pubspec.yaml | 2 +- 7 files changed, 30 insertions(+), 17 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 9e58c89..93db7c4 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -40,12 +40,13 @@ jobs: run: flutter pub get - name: Copiar .env run: cp .env.example .env - - name: Generar Archivos de Google Play + - name: Generar Archivos env: GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} API_PLAY_STORE: ${{ secrets.API_PLAY_STORE }} KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} + APP_STORE_CONNECT_AUTHKEY: ${{ secrets.APP_STORE_CONNECT_AUTHKEY }} run: | echo $GOOGLE_SERVICES | base64 --decode > android/app/google-services.json echo $API_PLAY_STORE | base64 --decode > android/api-playstore.json @@ -57,6 +58,8 @@ jobs: echo $KEYSTORE_FILE | base64 --decode > android/keystores/keystore.jks chmod 644 android/keystores/key* + + echo $APP_STORE_CONNECT_AUTHKEY > ios/fastlane/AuthKey.p8 - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: @@ -83,9 +86,12 @@ jobs: KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} + + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} with: lane: 'upload' - options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true", "skip_ios": "true" }' + options: '{ "type": "beta", "skip_git_push": "true", "skip_slack": "true", "is_ci": "true" }' - name: Publicar Version en Git run: | git config user.name "github-actions[bot]" diff --git a/.gitignore b/.gitignore index 4421ce2..11b0d65 100644 --- a/.gitignore +++ b/.gitignore @@ -1060,7 +1060,6 @@ FodyWeavers.xsd # Custom ignores *.pem -*.code-workspace **/prod/google-services.json android/app/google-services.json api-playstore.json @@ -1068,4 +1067,6 @@ GoogleService-Info*.plist !GoogleService-Info-dev.plist android/keystores/* */fastlane/report.xml -.flutter-plugins-dependencies \ No newline at end of file + +# Llave de API de App Store Connect +**/ios/**/*.p8 \ No newline at end of file diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index bf9b28f..2e19a34 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -84,19 +84,34 @@ lane :ios_build_and_upload do |options| end lane :ios_beta_upload do |options| + api_key = app_store_connect_api_key( + key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"], + issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"], + key_filepath: "./ios/fastlane/AuthKey.p8", + in_house: true, # Esto debe ser true porque estamos usando match + ) + changelog = options[:changelog] is_ci = options[:is_ci] upload_to_testflight( + api_key: api_key, changelog: changelog, skip_waiting_for_build_processing: is_ci ) end lane :ios_release_upload do |options| + api_key = app_store_connect_api_key( + key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"], + issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"], + key_filepath: "./ios/fastlane/AuthKey.p8", + in_house: true, # Esto debe ser true porque estamos usando match + ) changelog = options[:changelog] upload_to_app_store( + api_key: api_key, release_notes: changelog ) end diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index 9a9ff97..f4c22ef 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -95,7 +95,6 @@ class _SplashScreenState extends State { } final isLoggedIn = await _authService.isLoggedIn(); - logger.d("[SplashScreen]: isLoggedIn: $isLoggedIn"); final user = await _authService.getUser(); AnalyticsService.removeUser(); if(isLoggedIn && user != null) { diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart index b88a73f..5e1b622 100644 --- a/lib/services/analytics_service.dart +++ b/lib/services/analytics_service.dart @@ -5,7 +5,6 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/models/user/user.dart'; -import 'package:mi_utem/utils/string_utils.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class AnalyticsService { @@ -32,13 +31,11 @@ class AnalyticsService { await Sentry.configureScope((scope) => scope.setUser(SentryUser( id: correoUtem, - email: user.correoPersonal, - name: capitalize(user.nombreCompleto), + email: correoUtem, + name: user.nombreCompletoCapitalizado, + username: user.username, data: { - "rut": user.rut?.toString(), - "usuario": user.username, "backend_uid": correoUtem != null ? md5.convert(utf8.encode(correoUtem)) : "desconocido", - "perfiles": user.perfiles, }, ipAddress: "{{auto}}", ))); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 59fef21..5ef1a42 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -36,11 +36,6 @@ class AuthService { return false; } - if(await isFirstTime()) { - logger.d("[AuthService#isLoggedIn]: Es primera vez"); - return false; - } - final now = DateTime.now(); final lastLoginDate = let(await Preferencia.lastLogin.get(), (String _lastLogin) => DateTime.tryParse(_lastLogin)) ?? now; final difference = now.difference(lastLoginDate); diff --git a/pubspec.yaml b/pubspec.yaml index 8461852..6294050 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+111 +version: 3.0.0+113 environment: sdk: ">=2.17.0 <3.0.0" From 19c70125191e2309b43ae84bd9e51b647a811fe9 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 31 May 2024 12:12:21 -0400 Subject: [PATCH 163/194] =?UTF-8?q?patch:=20se=20agrega=20GOOGLE=5FSERVICE?= =?UTF-8?q?=5FINFO=5FPLIST=20a=20la=20generaci=C3=B3n=20de=20archivos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 93db7c4..5f8b93a 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -47,6 +47,7 @@ jobs: KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} APP_STORE_CONNECT_AUTHKEY: ${{ secrets.APP_STORE_CONNECT_AUTHKEY }} + GOOGLE_SERVICE_INFO_PLIST: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST }} run: | echo $GOOGLE_SERVICES | base64 --decode > android/app/google-services.json echo $API_PLAY_STORE | base64 --decode > android/api-playstore.json @@ -60,6 +61,7 @@ jobs: chmod 644 android/keystores/key* echo $APP_STORE_CONNECT_AUTHKEY > ios/fastlane/AuthKey.p8 + echo $GOOGLE_SERVICE_INFO_PLIST > ios/Runner/GoogleService-Info.plist - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 env: From 2e2fa327869e9eb187e52ff138ba572ac945e2ce Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 31 May 2024 12:14:09 -0400 Subject: [PATCH 164/194] patch: bump version Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6294050..2d45dff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+113 +version: 3.0.0+115 environment: sdk: ">=2.17.0 <3.0.0" From ed4717effd11878acbac75e6f7b9ef5f1480b674 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 31 May 2024 12:45:28 -0400 Subject: [PATCH 165/194] patch: utilizar AuthKey codificada en base64 Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .github/workflows/dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 5f8b93a..9cbb387 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -60,7 +60,7 @@ jobs: chmod 644 android/keystores/key* - echo $APP_STORE_CONNECT_AUTHKEY > ios/fastlane/AuthKey.p8 + echo $APP_STORE_CONNECT_AUTHKEY | base64 --decode > ios/fastlane/AuthKey.p8 echo $GOOGLE_SERVICE_INFO_PLIST > ios/Runner/GoogleService-Info.plist - name: Publicar Beta uses: maierj/fastlane-action@v3.1.0 From 17eca3b910875efc14cd78db1cc1ed01c39604dd Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 31 May 2024 20:49:42 -0400 Subject: [PATCH 166/194] patch: bump version Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2d45dff..a230c5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+115 +version: 3.0.0+117 environment: sdk: ">=2.17.0 <3.0.0" From 6f81fe20eba274d4b701ee2d1240f7af7229becd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 01:19:15 +0000 Subject: [PATCH 167/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index a230c5e..aae95dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+117 +version: 3.0.0+118 environment: sdk: ">=2.17.0 <3.0.0" From ab235cdec0cbcee46e2d0849ee5c98148b9e2642 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:06:44 -0400 Subject: [PATCH 168/194] patch: se actualiza constructor de la calculadora de notas * Ahora (opcionalmente) se solicita las notas para inicializar la calculadora Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/controllers/calculator_controller.dart | 22 +++++++++---------- .../detalle/asignatura_detalle_screen.dart | 16 ++++++-------- lib/screens/calculadora_notas_screen.dart | 21 +++++++++++++----- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/lib/controllers/calculator_controller.dart b/lib/controllers/calculator_controller.dart index 259a26f..a415542 100644 --- a/lib/controllers/calculator_controller.dart +++ b/lib/controllers/calculator_controller.dart @@ -125,21 +125,17 @@ class CalculatorController { double? get suggestedGrade { if (hasMissingPartialGrade && percentageWithoutGrade > 0) { final weightOfMissingGrades = percentageWithoutGrade / maxPercentage; - final requiredGradeValue = - passingGrade - (suggestedPresentationGrade ?? 0); - final missingGradesValue = requiredGradeValue / weightOfMissingGrades; - return missingGradesValue; + final requiredGradeValue = passingGrade - (suggestedPresentationGrade ?? 0); + return requiredGradeValue / weightOfMissingGrades; } return null; } - void makeEditable() { - freeEditable.value = true; - } + bool get hasGrades => partialGrades.isNotEmpty; - void makeNonEditable() { - freeEditable.value = false; - } + void makeEditable() => freeEditable.value = true; + + void makeNonEditable() => freeEditable.value = false; void clearGrades() { partialGrades.clear(); @@ -148,11 +144,15 @@ class CalculatorController { clearExamGrade(); } - void updateWithGrades(Grades grades) { + void updateWithGrades(Grades? grades) { partialGrades.clear(); percentageTextFieldControllers.clear(); gradeTextFieldControllers.clear(); + if(grades == null) { + return; + } + for(final grade in grades.notasParciales) { addGrade(IEvaluacion.fromRemote(grade)); } diff --git a/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart b/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart index 9e5711b..67f0bee 100644 --- a/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; -import 'package:mi_utem/controllers/calculator_controller.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; import 'package:mi_utem/models/carrera.dart'; @@ -67,18 +66,12 @@ class _AsignaturaDetalleScreenState extends State { length: tabs.length, child: Scaffold( appBar: CustomAppBar( - title: Text(asignatura.nombre ?? "Asignatura sin nombre"), + title: Text(asignatura.nombre), actions: RemoteConfigService.calculadoraMostrar ? [ IconButton( icon: Icon(Mdi.calculator), tooltip: "Calculadora", - onPressed: () async { - Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen())); - final grades = await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: asignatura.id); - if (grades != null) { - Get.find().updateWithGrades(grades); - } - }, + onPressed: _onTapCalculadora, ), ] : [], bottom: TabBar( @@ -90,5 +83,10 @@ class _AsignaturaDetalleScreenState extends State { ), ); } + + _onTapCalculadora() async { + final grades = await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: asignatura.id); + Navigator.push(context, MaterialPageRoute(builder: (ctx) => CalculadoraNotasScreen(grades: grades))); + } } diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index c2157b9..e945b6d 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -1,12 +1,19 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mi_utem/controllers/calculator_controller.dart'; +import 'package:mi_utem/models/evaluacion/grades.dart'; import 'package:mi_utem/widgets/calculadora_notas/display_notas_widget.dart'; import 'package:mi_utem/widgets/calculadora_notas/editar_notas_widget.dart'; import 'package:mi_utem/widgets/custom_app_bar.dart'; +import 'package:mi_utem/widgets/loading/loading_dialog.dart'; class CalculadoraNotasScreen extends StatefulWidget { - const CalculadoraNotasScreen({super.key}); + final Grades? grades; + + const CalculadoraNotasScreen({ + super.key, + this.grades, + }); @override State createState() => _CalculadoraNotasScreenState(); @@ -14,12 +21,16 @@ class CalculadoraNotasScreen extends StatefulWidget { class _CalculadoraNotasScreenState extends State { - final CalculatorController calculatorController = Get.find(); - @override void initState() { - calculatorController.makeEditable(); super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + showLoadingDialog(context); + final CalculatorController calculatorController = Get.find(); + calculatorController.makeEditable(); + calculatorController.updateWithGrades(widget.grades); + Navigator.pop(context); + }); } @override @@ -31,7 +42,7 @@ class _CalculadoraNotasScreenState extends State { IconButton( icon: const Icon(Icons.delete_outline), tooltip: "Limpiar notas", - onPressed: calculatorController.clearGrades, + onPressed: Get.find().clearGrades, ), ], ), From f075350b9775258cc9fb371267f89f4f4b7c5293 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:13:05 -0400 Subject: [PATCH 169/194] patch: se convierte en GetMaterialApp Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/main.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index e5c61c3..c97aa76 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; +import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mi_utem/config/constants.dart'; import 'package:mi_utem/screens/splash_screen.dart'; @@ -55,7 +56,7 @@ class _MiUtemState extends State { enableMultiSessionRecord: true, )); - return MaterialApp( + return GetMaterialApp( home: SplashScreen(), debugShowCheckedModeBanner: false, title: 'Mi UTEM', @@ -65,6 +66,7 @@ class _MiUtemState extends State { SentryNavigatorObserver(), FlutterUxcamNavigatorObserver() ], + defaultTransition: Transition.native, builder: (context, widget) => ResponsiveWrapper.builder( widget, maxWidth: 1200, From f5bd4f7cbeb4ee473e837bce3a74674cf79f75d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Jun 2024 01:50:35 +0000 Subject: [PATCH 170/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index aae95dd..6e63c90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+118 +version: 3.0.0+119 environment: sdk: ">=2.17.0 <3.0.0" From 43f8e0ef5e8965f22363100470ae498da5ea0cef Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:22:20 -0400 Subject: [PATCH 171/194] patch: se crea nota_list_item para muestra de notas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Para evitar crear una instancia de CalculadoraController si no es necesario se agregó nota_list_item para la muestra de las notas de una asignatura Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- .../asignatura/asignaturas_lista_screen.dart | 4 +- .../detalle/asignatura_notas_tab.dart | 2 +- lib/screens/calculadora_notas_screen.dart | 2 +- .../asignatura/notas_tab/nota_list_item.dart | 74 +++++++++++++++++++ .../calculadora_notas/nota_list_item.dart | 10 +-- 5 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 lib/widgets/asignatura/notas_tab/nota_list_item.dart diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index 0a2ef5c..cbabd42 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -97,9 +97,7 @@ class _AsignaturasListaScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: Center( - child: LoadingIndicator(), - ), + child: LoadingIndicator.centered(), ), ], ), diff --git a/lib/screens/asignatura/detalle/asignatura_notas_tab.dart b/lib/screens/asignatura/detalle/asignatura_notas_tab.dart index d601d60..256b56a 100644 --- a/lib/screens/asignatura/detalle/asignatura_notas_tab.dart +++ b/lib/screens/asignatura/detalle/asignatura_notas_tab.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/evaluacion/evaluacion.dart'; +import 'package:mi_utem/widgets/asignatura/notas_tab/nota_list_item.dart'; import 'package:mi_utem/widgets/asignatura/notas_tab/notas_display.dart'; -import 'package:mi_utem/widgets/calculadora_notas/nota_list_item.dart'; import 'package:mi_utem/widgets/custom_error_widget.dart'; import 'package:mi_utem/widgets/pull_to_refresh.dart'; diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index e945b6d..c2f4d9e 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -42,7 +42,7 @@ class _CalculadoraNotasScreenState extends State { IconButton( icon: const Icon(Icons.delete_outline), tooltip: "Limpiar notas", - onPressed: Get.find().clearGrades, + onPressed: () => Get.find().clearGrades(), ), ], ), diff --git a/lib/widgets/asignatura/notas_tab/nota_list_item.dart b/lib/widgets/asignatura/notas_tab/nota_list_item.dart new file mode 100644 index 0000000..b10789f --- /dev/null +++ b/lib/widgets/asignatura/notas_tab/nota_list_item.dart @@ -0,0 +1,74 @@ +import 'package:extended_masked_text/extended_masked_text.dart'; +import 'package:flutter/material.dart'; +import 'package:mi_utem/models/evaluacion/evaluacion.dart'; +import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/utils/utils.dart'; + +class NotaListItem extends StatelessWidget { + final IEvaluacion evaluacion; + + const NotaListItem({ + super.key, + required this.evaluacion, + }); + + @override + Widget build(BuildContext context) => Flex( + direction: Axis.horizontal, + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + width: 90, + child: Text(evaluacion.descripcion ?? "Nota", + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 16), + Flexible( + flex: 3, + child: Center( + child: TextField( + enabled: false, + controller: MaskedTextController( + mask: "0.0", + text: formatoNota(evaluacion.nota) ?? "", + ), + decoration: InputDecoration( + hintText: "--", + disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + ), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(width: 16), + Flexible( + flex: 4, + child: Center( + child: TextField( + controller: MaskedTextController( + mask: "000", + text: evaluacion.porcentaje?.toStringAsFixed(0) ?? "", + ), + enabled: false, + textAlign: TextAlign.center, + decoration: InputDecoration( + hintText: "Peso", + suffixText: "%", + disabledBorder: MainTheme.theme.inputDecorationTheme.border!.copyWith( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 20), + ], + ); +} diff --git a/lib/widgets/calculadora_notas/nota_list_item.dart b/lib/widgets/calculadora_notas/nota_list_item.dart index 784639a..2fdbb61 100644 --- a/lib/widgets/calculadora_notas/nota_list_item.dart +++ b/lib/widgets/calculadora_notas/nota_list_item.dart @@ -44,13 +44,13 @@ class NotaListItem extends StatelessWidget { direction: Axis.horizontal, mainAxisSize: MainAxisSize.max, children: [ - Container( + SizedBox( width: 90, child: Text(evaluacion.descripcion ?? "Nota", overflow: TextOverflow.ellipsis, ), ), - SizedBox(width: 16), + const SizedBox(width: 16), Flexible( flex: 3, child: Center( @@ -102,7 +102,7 @@ class NotaListItem extends StatelessWidget { ), ), ), - SizedBox(width: 16), + const SizedBox(width: 16), Flexible( flex: 4, child: Center( @@ -148,8 +148,8 @@ class NotaListItem extends StatelessWidget { )), ), ), - SizedBox(width: 20), - if (onDelete != null)GestureDetector( + const SizedBox(width: 20), + if (onDelete != null) GestureDetector( onTap: () => onDelete?.call(), child: Icon( Icons.delete, From 676bf43c5debd5e3760d89d64d2e78a73eb75a4b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 7 Jun 2024 18:28:53 -0400 Subject: [PATCH 172/194] patch: cambio a sizedbox Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/pull_to_refresh.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/pull_to_refresh.dart b/lib/widgets/pull_to_refresh.dart index fb7a6eb..3cfd530 100644 --- a/lib/widgets/pull_to_refresh.dart +++ b/lib/widgets/pull_to_refresh.dart @@ -41,10 +41,10 @@ class PullToRefresh extends StatelessWidget { builder: (context, child) => SafeArea( child: Stack( children: [ - Container( + SizedBox( height: (_offsetToArmed) * controller.value, width: double.infinity, - child: Container( + child: SizedBox( height: 30, width: 30, child: SpinKitDoubleBounce( From b696cf7717f5999a9d1aaf3801ed57b579421eea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Jun 2024 23:04:55 +0000 Subject: [PATCH 173/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6e63c90..647b8e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+119 +version: 3.0.0+120 environment: sdk: ">=2.17.0 <3.0.0" From 8152d2ed4c7b0a53cea9915ae2e491f3d742d056 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:21:51 -0400 Subject: [PATCH 174/194] patch: se agrega modo "offline" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Se agrega una preferencia para evitar un refresco forzado si la app está en modo offline. Por hacer: * Si no hay caché guardado para alguna solicitud y estamos en modo offline, mostrar como error que el problema es estar sin conexión. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/preferencia.dart | 1 + lib/screens/horario/horario_screen.dart | 2 + lib/screens/main_screen.dart | 12 +++-- lib/screens/splash_screen.dart | 26 +++++----- lib/services/analytics_service.dart | 11 +---- lib/services/auth_service.dart | 6 ++- lib/services/carreras_service.dart | 3 -- lib/services/notification_service.dart | 37 ++++++++------- lib/utils/http/functions.dart | 47 +++++++++++++------ .../http/interceptors/error_interceptor.dart | 9 +++- 10 files changed, 92 insertions(+), 62 deletions(-) diff --git a/lib/models/preferencia.dart b/lib/models/preferencia.dart index aef6b9f..5cff0a2 100644 --- a/lib/models/preferencia.dart +++ b/lib/models/preferencia.dart @@ -4,6 +4,7 @@ enum Preferencia { apodo, lastLogin, onboardingStep, + isOffline, ; /// Revisa si la preferencia existe diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index 11fbb6e..3125b74 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; @@ -56,6 +57,7 @@ class _HorarioScreenState extends State { builder: (context, snapshot) { if(snapshot.hasError) { final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al cargar el horario! Por favor intenta más tarde."; + logger.e("Error al cargar horario", snapshot.error, snapshot.stackTrace); return Scaffold( appBar: CustomAppBar( title: Text("Horario"), diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index e6f90d9..f2d0fe7 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -55,15 +55,17 @@ class _MainScreenState extends State { ReviewService.addScreen("MainScreen"); ReviewService.checkAndRequestReview(context); - loadData(); + loadData(forceRefresh: false); _authService.getUser().then((user) => setState(() => _user = user)); } - Future loadData() async { - await RemoteConfigService.update(); - await Get.find().getPermisos(forceRefresh: true); // Forzar re-descarga de los permisos - await Get.find().getNoticias(forceRefresh: true); // Forzar re-descarga de las noticias + Future loadData({ bool forceRefresh = true }) async { + if(forceRefresh) { + await RemoteConfigService.update(); + } + await Get.find().getPermisos(forceRefresh: forceRefresh); // Forzar re-descarga de los permisos + await Get.find().getNoticias(forceRefresh: forceRefresh); // Forzar re-descarga de las noticias setState(() => _banners = RemoteConfigService.banners); // Actualizar los banners y se re-renderiza } diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index f4c22ef..cdfc640 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -71,31 +71,33 @@ class _SplashScreenState extends State { callback: (String val) async { showLoadingDialog(context); // Revisar si tenemos conexión a internet + bool offlineMode = (await Preferencia.isOffline.get(defaultValue: "false", guardar: true)) == "true"; try { final response = await HttpClient.httpClient.head(apiUrl); - if(response.statusCode != 200) { - Navigator.pop(context); - showTextSnackbar(context, - title: "Error al conectar con la API", - message: "La app funcionará en modo Offline. Revisa tu conexión a internet si quieres acceder a todas las funcionalidades.", - backgroundColor: Colors.red, - duration: Duration(seconds: 20), - ); - } + offlineMode = response.statusCode != 200; } catch (e) { logger.e("[SplashScreen]: Error al conectar con la API", e); + offlineMode = true; + } + + await Preferencia.isOffline.set(offlineMode.toString()); + final user = await _authService.getUser(); + + if(offlineMode) { Navigator.pop(context); showTextSnackbar(context, title: "Error al conectar con la API", - message: "La app funcionará en modo Offline. Revisa tu conexión a internet si quieres acceder a todas las funcionalidades.", + message: user != null ? "La app funcionará en modo Offline. Revisa tu conexión a internet si quieres acceder a todas las funcionalidades." : "Ouch! Parece que no tienes una conexión a internet. Revisa tu conexión e intenta más tarde.", backgroundColor: Colors.red, duration: Duration(seconds: 20), ); - return; + + if(user == null) { + return; + } } final isLoggedIn = await _authService.isLoggedIn(); - final user = await _authService.getUser(); AnalyticsService.removeUser(); if(isLoggedIn && user != null) { AnalyticsService.setUser(user); diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart index 5e1b622..a3efc90 100644 --- a/lib/services/analytics_service.dart +++ b/lib/services/analytics_service.dart @@ -12,15 +12,8 @@ class AnalyticsService { await FirebaseAnalytics.instance.setUserId(id: user.correoUtem); await FlutterUxcam.setUserIdentity(user.correoUtem); - if (user.rut != null) { - await FirebaseAnalytics.instance.setUserProperty(name: "rut", value: user.rut?.toString()); - await FlutterUxcam.setUserProperty("rut", user.rut!.toString()); - } - - if (user.primerNombre != null) { - await FirebaseAnalytics.instance.setUserProperty(name: "name", value: user.primerNombre); - await FlutterUxcam.setUserProperty("name", user.primerNombre); - } + await FirebaseAnalytics.instance.setUserProperty(name: "name", value: user.primerNombre); + await FlutterUxcam.setUserProperty("name", user.primerNombre); if (user.apellidos != null) { await FirebaseAnalytics.instance.setUserProperty(name: "last_name", value: user.apellidos); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 5ef1a42..3459202 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -39,7 +39,8 @@ class AuthService { final now = DateTime.now(); final lastLoginDate = let(await Preferencia.lastLogin.get(), (String _lastLogin) => DateTime.tryParse(_lastLogin)) ?? now; final difference = now.difference(lastLoginDate); - if(difference.inMinutes < 4 && now != lastLoginDate && !forceRefresh) { + final offlineMode = (await Preferencia.isOffline.get()) == "true"; + if((difference.inMinutes < 4 && now != lastLoginDate && !forceRefresh) || offlineMode) { return true; } @@ -160,6 +161,9 @@ class AuthService { QuerySnapshot> snapshotRepeated; try { snapshotRepeated = await usersCollection.where('fcmTokens', arrayContains: fcmToken).get(); + } on FirebaseException catch(e) { + print(e); + return; } catch (e) { logger.e("[AuthService#deleteFCMToken]: Error al obtener usuarios con FCM Token", e); return; diff --git a/lib/services/carreras_service.dart b/lib/services/carreras_service.dart index 95bb971..1cf33c3 100644 --- a/lib/services/carreras_service.dart +++ b/lib/services/carreras_service.dart @@ -26,15 +26,12 @@ class CarrerasService { void changeSelectedCarrera(Carrera carrera) => selectedCarrera = carrera; void autoSelectCarreraActiva() { - logger.d("[CarrerasService#autoSelectCarreraActiva]: Seleccionando carrera activa... ${carreras.map((e) => e.toJson()).toList()}"); final estados = ["Regular", "Causal de Eliminacion"] .reversed .map((e) => e.toLowerCase()) .toList(); - logger.d("[CarrerasService#autoSelectCarreraActiva]: Estados: $estados"); carreras.sort((a,b) => estados.indexOf(b.estado!.toLowerCase()).compareTo(estados.indexOf(a.estado!.toLowerCase()))); - logger.d("[CarrerasService#autoSelectCarreraActiva]: Carreras ordenadas: ${carreras.map((e) => e.toJson()).toList()}"); final carreraActiva = carreras.first; AnalyticsService.setCarreraToUser(carreraActiva); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 4b11834..12cb2de 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -54,12 +54,9 @@ class NotificationService { notifications.setListeners( onActionReceivedMethod: NotificationController.onActionReceivedMethod, - onNotificationCreatedMethod: - NotificationController.onNotificationCreatedMethod, - onNotificationDisplayedMethod: - NotificationController.onNotificationDisplayedMethod, - onDismissActionReceivedMethod: - NotificationController.onDismissActionReceivedMethod, + onNotificationCreatedMethod: NotificationController.onNotificationCreatedMethod, + onNotificationDisplayedMethod: NotificationController.onNotificationDisplayedMethod, + onDismissActionReceivedMethod: NotificationController.onDismissActionReceivedMethod, ); } @@ -79,15 +76,21 @@ class NotificationService { required String body, required Carrera carrera, required Asignatura asignatura, - }) => notifications.createNotification(content: NotificationContent( - id: asignatura.hashCode, - channelKey: gradeChangesChannelKey, - title: title, - body: body, - payload: { - 'type': 'grade_change', - 'asignatura': asignatura.toString(), - 'carrera': carrera.toString(), - }, - )); + }) async { + if(!await hasAllowedNotifications()) { + return; + } + + notifications.createNotification(content: NotificationContent( + id: asignatura.hashCode, + channelKey: gradeChangesChannelKey, + title: title, + body: body, + payload: { + 'type': 'grade_change', + 'asignatura': asignatura.toString(), + 'carrera': carrera.toString(), + }, + )); + } } diff --git a/lib/utils/http/functions.dart b/lib/utils/http/functions.dart index 9787488..fdd32e2 100644 --- a/lib/utils/http/functions.dart +++ b/lib/utils/http/functions.dart @@ -1,7 +1,11 @@ import 'package:dio/dio.dart'; import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart' as getx; import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; /// Función para realizar solicitudes mediante el authClient. @@ -23,17 +27,32 @@ Future authClientRequest(String path, { Options? options, bool forceRefresh = false, Duration? ttl = const Duration(days: 7), -}) async => await HttpClient.authClient.request("$apiUrl/v1/$path", - data: data, - options: options ?? buildCacheOptions(ttl ?? Duration(days: 7), - forceRefresh: forceRefresh, - primaryKey: 'miutem', - subKey: path, - maxStale: const Duration(days: 14), - ).copyWith( - method: method, - headers: headers, - contentType: contentType, - responseType: responseType, - ), -); \ No newline at end of file +}) async { + bool _forceRefresh = forceRefresh; + if(_forceRefresh) { + // Revisa si esta en modo offline + if((await Preferencia.isOffline.get()) == "true") { + _forceRefresh = false; + final currentContext = getx.Get.context; + if(currentContext != null) { + ScaffoldMessenger.of(currentContext).removeCurrentSnackBar(); + showErrorSnackbar(currentContext, "No se puede realizar la solicitud en modo Offline. Por favor revisa tu conexión a internet e intenta nuevamente."); + } + } + } + + return await HttpClient.authClient.request("$apiUrl/v1/$path", + data: data, + options: options ?? buildCacheOptions(ttl ?? Duration(days: 7), + forceRefresh: _forceRefresh, + primaryKey: 'miutem', + subKey: path, + maxStale: const Duration(days: 14), + ).copyWith( + method: method, + headers: headers, + contentType: contentType, + responseType: responseType, + ), + ); +} \ No newline at end of file diff --git a/lib/utils/http/interceptors/error_interceptor.dart b/lib/utils/http/interceptors/error_interceptor.dart index da03286..d61a02b 100644 --- a/lib/utils/http/interceptors/error_interceptor.dart +++ b/lib/utils/http/interceptors/error_interceptor.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; InterceptorsWrapper errorInterceptor = InterceptorsWrapper( @@ -12,7 +13,13 @@ InterceptorsWrapper errorInterceptor = InterceptorsWrapper( return handler.reject(DioError(requestOptions: err.requestOptions, error: CustomException.fromJson(json as Map))); } - final error = DioError(requestOptions: err.requestOptions, error: CustomException.custom(err.response?.statusMessage), response: err.response, type: err.type); + logger.e("[ErrorInterceptor]: ${err.message}", err.error, err.stackTrace); + final error = DioError(requestOptions: err.requestOptions, error: CustomException.fromJson({ + "mensaje": err.response?.statusMessage ?? "Ocurrió un error inesperado. Por favor, inténtalo nuevamente.", + "error": err.message, + "codigoHttp": err.response?.statusCode, + "codigoInterno": 0.0, + }), response: err.response, type: err.type); error.stackTrace = err.stackTrace; return handler.reject(error); }, From 839167804fa39d501cda4b0d5ad8cd2dd4280819 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Jun 2024 00:57:34 +0000 Subject: [PATCH 175/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 647b8e8..e1633b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+120 +version: 3.0.0+121 environment: sdk: ">=2.17.0 <3.0.0" From 4f140726a98855cd9c9010c83a00a43485be570f Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:21:27 -0400 Subject: [PATCH 176/194] patch: se actualiza shared_preferences Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 5951d1b..9e0becc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1293,10 +1293,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e1633b4..1e9368d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: intl: ^0.18.1 get_storage: ^2.0.3 hexcolor: ^3.0.1 - shared_preferences: ^2.0.20 + shared_preferences: ^2.2.2 background_fetch: ^1.1.5 flutter_uxcam: ^2.3.0 in_app_update: ^4.2.2 From 75dc5c80ed3cd65affa05c666a8d3f79841b5151 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:23:02 -0400 Subject: [PATCH 177/194] patch: preferencia en memoria o local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ahora podemos tener preferencias en memoria o locales. Las preferencias en memoria usa SharedPreferences que es rápido y para valores pequeños. Mientras que en local se guardan en el secureStorage. **Nota:** Solo utilizar en memoria para valores que no importa si son perdidos, ya que es posible que algún valor en memoria sea perdido. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/models/preferencia.dart | 59 +++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/lib/models/preferencia.dart b/lib/models/preferencia.dart index 5cff0a2..1ec1585 100644 --- a/lib/models/preferencia.dart +++ b/lib/models/preferencia.dart @@ -1,14 +1,31 @@ import 'package:mi_utem/config/secure_storage.dart'; +import 'package:mi_utem/utils/utils.dart'; +import 'package:shared_preferences/shared_preferences.dart'; enum Preferencia { apodo, lastLogin, onboardingStep, - isOffline, + isOffline(memory: true), ; + final bool memory; + + /// Instancia de una preferencia + /// Si memory es falso, entonces la preferencia se guardará en el storage, si es verdadero, se guardará en memoria + const Preferencia({ + this.memory = false + }); + /// Revisa si la preferencia existe - Future exists() async => await secureStorage.containsKey(key: this.name); + Future exists() async { + if(!memory) { + return await secureStorage.containsKey(key: this.name); + } + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.containsKey(this.name); + } /// Obtiene la preferencia del storage, pero si no existe retorna el valor por defecto /// Si [guardar] es verdadero, entonces si no existe la preferencia, se guardará el valor por defecto @@ -16,11 +33,35 @@ enum Preferencia { if(defaultValue != null) { await add(defaultValue); } - return await secureStorage.read(key: this.name) ?? defaultValue; + + if(!memory) { + return await secureStorage.read(key: this.name) ?? defaultValue; + } + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(this.name) ?? defaultValue; } + /// Obtiene la preferencia del storage, pero si no existe retorna el valor por defecto + /// Si [guardar] es verdadero, entonces si no existe la preferencia, se guardará el valor por defecto + /// Retorna un valor booleano + Future getAsBool({ bool defaultValue = false, bool guardar = false }) async => await get(defaultValue: defaultValue.toString(), guardar: guardar) == "true"; + + /// Obtiene la preferencia del storage, pero si no existe retorna el valor por defecto + /// Si [guardar] es verdadero, entonces si no existe la preferencia, se guardará el valor por defecto + /// Retorna un valor entero + Future getAsNum({ int defaultValue = 0, bool guardar = false }) async => let(await get(defaultValue: defaultValue.toString(), guardar: guardar), (data) => num.tryParse(data)); + /// Guarda la preferencia en el storage - set(String value) async => await secureStorage.write(key: this.name, value: value); + set(String value) async { + if(!memory) { + await secureStorage.write(key: this.name, value: value); + return; + } + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString(this.name, value); + } /// Guarda la preferencia en el storage solo si no existe /// Si la preferencia ya existe, no se guardará nada @@ -31,6 +72,14 @@ enum Preferencia { } /// Elimina la preferencia del storage - delete() async => await secureStorage.delete(key: this.name); + delete() async { + if(!memory) { + await secureStorage.delete(key: this.name); + return; + } + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.remove(this.name); + } } \ No newline at end of file From 1add90b406c4ddd1b81085591da3a07af08adb88 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:25:32 -0400 Subject: [PATCH 178/194] feat: modo offline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se agrega un modo offline para permitir el uso de la app sin conexión. Esto funciona de la siguiente manera: Al iniciar la app en la pantalla de splash se revisa si hay conexión a nuestra api, en caso de no haber se notifica al usuario del modo offline. Cada solicitud realizada devolverá el caché, y si un usuario intenta forzar la actualización de un componente devuelve el error de estar en modo offline. Además, cuando se fuerza un refresco de todas formas se revisa si la conexión está de vuelta para devolver datos actualizados. Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/utils/http/functions.dart | 56 +++++++++---------- lib/utils/http/http_client.dart | 9 ++- .../offline_mode_interceptor.dart | 55 ++++++++++++++++++ 3 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 lib/utils/http/interceptors/offline_mode_interceptor.dart diff --git a/lib/utils/http/functions.dart b/lib/utils/http/functions.dart index fdd32e2..51c9ad2 100644 --- a/lib/utils/http/functions.dart +++ b/lib/utils/http/functions.dart @@ -1,11 +1,9 @@ import 'package:dio/dio.dart'; import 'package:dio_http_cache/dio_http_cache.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart' as getx; import 'package:mi_utem/config/constants.dart'; +import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/utils/http/http_client.dart'; -import 'package:mi_utem/widgets/snackbar.dart'; /// Función para realizar solicitudes mediante el authClient. @@ -27,32 +25,32 @@ Future authClientRequest(String path, { Options? options, bool forceRefresh = false, Duration? ttl = const Duration(days: 7), -}) async { - bool _forceRefresh = forceRefresh; - if(_forceRefresh) { - // Revisa si esta en modo offline - if((await Preferencia.isOffline.get()) == "true") { - _forceRefresh = false; - final currentContext = getx.Get.context; - if(currentContext != null) { - ScaffoldMessenger.of(currentContext).removeCurrentSnackBar(); - showErrorSnackbar(currentContext, "No se puede realizar la solicitud en modo Offline. Por favor revisa tu conexión a internet e intenta nuevamente."); - } - } +}) async => await HttpClient.authClient.request("$apiUrl/v1/$path", + data: data, + options: options ?? buildCacheOptions(ttl ?? Duration(days: 7), + forceRefresh: forceRefresh, + primaryKey: 'miutem', + subKey: path, + maxStale: const Duration(days: 14), + ).copyWith( + method: method, + headers: headers, + contentType: contentType, + responseType: responseType, + ), +); + +Future isOffline() async { + bool offlineMode = (await Preferencia.isOffline.getAsBool(defaultValue: false, guardar: true)); + + try { + final response = await HttpClient.dioClient.head(apiUrl); + offlineMode = !"${response.statusCode}".startsWith("2"); + } catch (e) { + logger.e("[HttpRequest#isOffline]: Error al conectar con la API", e); + offlineMode = true; } - return await HttpClient.authClient.request("$apiUrl/v1/$path", - data: data, - options: options ?? buildCacheOptions(ttl ?? Duration(days: 7), - forceRefresh: _forceRefresh, - primaryKey: 'miutem', - subKey: path, - maxStale: const Duration(days: 14), - ).copyWith( - method: method, - headers: headers, - contentType: contentType, - responseType: responseType, - ), - ); + await Preferencia.isOffline.set(offlineMode.toString()); + return offlineMode; } \ No newline at end of file diff --git a/lib/utils/http/http_client.dart b/lib/utils/http/http_client.dart index 74c19b2..d45f8e5 100644 --- a/lib/utils/http/http_client.dart +++ b/lib/utils/http/http_client.dart @@ -5,6 +5,7 @@ import 'package:mi_utem/utils/http/interceptors/auth_interceptor.dart'; import 'package:mi_utem/utils/http/interceptors/error_interceptor.dart'; import 'package:mi_utem/utils/http/interceptors/headers_interceptor.dart'; import 'package:mi_utem/utils/http/interceptors/log_interceptor.dart'; +import 'package:mi_utem/utils/http/interceptors/offline_mode_interceptor.dart'; class HttpClient { @@ -14,11 +15,15 @@ class HttpClient { defaultMaxStale: const Duration(days: 14), )); - static final Dio httpClient = Dio(BaseOptions(baseUrl: apiUrl))..interceptors.addAll([ + static final Dio dioClient = Dio(BaseOptions(baseUrl: apiUrl))..interceptors.addAll([ HeadersInterceptor(), - errorInterceptor, logInterceptor, + ]); + + static final Dio httpClient = dioClient..interceptors.addAll([ + OfflineModeInterceptor(), cacheManager.interceptor, + errorInterceptor, ]); static final Dio authClient = httpClient..interceptors.add(AuthInterceptor()); diff --git a/lib/utils/http/interceptors/offline_mode_interceptor.dart b/lib/utils/http/interceptors/offline_mode_interceptor.dart new file mode 100644 index 0000000..dfd7018 --- /dev/null +++ b/lib/utils/http/interceptors/offline_mode_interceptor.dart @@ -0,0 +1,55 @@ +import 'package:dio/dio.dart'; +import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:get/get.dart'; +import 'package:mi_utem/models/exceptions/custom_exception.dart'; +import 'package:mi_utem/models/preferencia.dart'; +import 'package:mi_utem/utils/http/functions.dart'; +import 'package:mi_utem/widgets/snackbar.dart'; + +class OfflineModeInterceptor extends Interceptor { + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { + bool offlineMode = (await Preferencia.isOffline.getAsBool(defaultValue: false, guardar: true)); + bool _forceRefresh = options.extra.containsKey(DIO_CACHE_KEY_FORCE_REFRESH) && options.extra[DIO_CACHE_KEY_FORCE_REFRESH] == true; + if(!offlineMode || !_forceRefresh) { + return super.onRequest(options, handler); + } + + // Revisa si sigue offline realizando solicitud a la API (solo head) + offlineMode = await isOffline(); + + if(!offlineMode) { // Si vuelve la conexión + return super.onRequest(options, handler); + } + + if(_forceRefresh) { + final context = Get.context; + if(context != null) { + showErrorSnackbar(context, "No se puede realizar la solicitud en modo Offline. Por favor revisa tu conexión a internet e intenta nuevamente."); + } + } + + options.extra[DIO_CACHE_KEY_FORCE_REFRESH] = offlineMode; + + return super.onRequest(options, handler); + } + + @override + void onError(DioError err, ErrorInterceptorHandler handler) async { + bool _forceRefresh = err.requestOptions.extra.containsKey(DIO_CACHE_KEY_FORCE_REFRESH) && err.requestOptions.extra[DIO_CACHE_KEY_FORCE_REFRESH] == true; + bool _offlineMode = (await Preferencia.isOffline.get(defaultValue: "false", guardar: true)) == "true"; + if(!_forceRefresh || !_offlineMode) { + return super.onError(err, handler); + } + + final error = DioError(requestOptions: err.requestOptions, error: CustomException.fromJson({ + "mensaje": "No se puede realizar la solicitud en modo Offline. Por favor revisa tu conexión a internet e intenta nuevamente.", + "error": "No se puede realizar la solicitud en modo Offline. Por favor revisa tu conexión a internet e intenta nuevamente.", + "codigoHttp": 400, + "codigoInterno": 0.0, + }), type: DioErrorType.cancel, response: err.response); + + return handler.next(error); + } +} \ No newline at end of file From 7ad86bc1da4f9fc0d03a905474148e0efdf9ba09 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:25:50 -0400 Subject: [PATCH 179/194] =?UTF-8?q?patch:=20se=20permite=20cualquier=20ori?= =?UTF-8?q?entaci=C3=B3n=20del=20dispositivo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/main.dart | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c97aa76..6e49ed0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'package:mi_utem/service_manager.dart'; import 'package:mi_utem/services/background_service.dart'; import 'package:mi_utem/services/notification_service.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:responsive_framework/responsive_framework.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'services/remote_config/remote_config.dart'; @@ -24,7 +23,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await dotenv.load(); await GetStorage.init(); - await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + await SystemChrome.setPreferredOrientations(DeviceOrientation.values); await Firebase.initializeApp(); await RemoteConfigService.initialize(); await registerServices(); @@ -67,17 +66,6 @@ class _MiUtemState extends State { FlutterUxcamNavigatorObserver() ], defaultTransition: Transition.native, - builder: (context, widget) => ResponsiveWrapper.builder( - widget, - maxWidth: 1200, - minWidth: 360, - breakpoints: [ - ResponsiveBreakpoint.resize(480, name: MOBILE), - ResponsiveBreakpoint.autoScale(800, name: TABLET), - ResponsiveBreakpoint.resize(1000, name: DESKTOP), - ResponsiveBreakpoint.autoScale(2460, name: '4K'), - ], - ), ); } } From cc12d8195005c750d339b44353479d9075a3b9d1 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:26:20 -0400 Subject: [PATCH 180/194] =?UTF-8?q?patch:=20se=20aumenta=20el=20ancho=20de?= =?UTF-8?q?=20las=20tarjetas=20de=20noticia=20para=20permitir=20una=20vist?= =?UTF-8?q?a=20c=C3=B3moda=20en=20modo=20landscape=20(orientaci=C3=B3n=20h?= =?UTF-8?q?orizontal)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/noticias/noticia_card_widget.dart | 50 +++++++++---------- .../noticias/noticias_carrusel_widget.dart | 2 +- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/lib/widgets/noticias/noticia_card_widget.dart b/lib/widgets/noticias/noticia_card_widget.dart index 7efd269..57dd82c 100644 --- a/lib/widgets/noticias/noticia_card_widget.dart +++ b/lib/widgets/noticias/noticia_card_widget.dart @@ -6,7 +6,7 @@ import 'package:mi_utem/widgets/loading/loading_indicator.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:url_launcher/url_launcher.dart'; -class NoticiaCardWidget extends StatefulWidget { +class NoticiaCardWidget extends StatelessWidget { final Noticia noticia; @@ -14,49 +14,45 @@ class NoticiaCardWidget extends StatefulWidget { required this.noticia, }); - @override - State createState() => _NoticiaCardWidgetState(); -} - -class _NoticiaCardWidgetState extends State { - @override Widget build(BuildContext context) => SizedBox( - height: 200, - width: 200, + height: 230, + width: 250, child: Card( child: Material( color: Colors.transparent, child: InkWell( - onTap: _onTapNoticia, + onTap: () => _onTapNoticia(context), borderRadius: BorderRadius.all(Radius.circular(15)), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ CachedNetworkImage( - imageUrl: widget.noticia.imagen, + imageUrl: noticia.imagen, placeholder: (ctx, url) => LoadingIndicator(), errorWidget: (ctx, url, error) => Icon(Icons.error, size: 110, color: Theme.of(context).colorScheme.error), height: 110, fit: BoxFit.cover, ), - Container( + Padding( padding: const EdgeInsets.symmetric(horizontal: 10), - height: 70, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Spacer(), - Text(widget.noticia.titulo, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, - ), - Spacer(), - ], + child: SizedBox( + height: 100, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + Text(noticia.titulo, + maxLines: 4, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), + const Spacer(), + ], + ), ), - ) + ), ], ), ), @@ -64,8 +60,8 @@ class _NoticiaCardWidgetState extends State { ), ); - _onTapNoticia() async { - final url = widget.noticia.link; + _onTapNoticia(BuildContext context) async { + final url = noticia.link; if (await canLaunchUrl(Uri.parse(url))) { AnalyticsService.logEvent("noticia_card_tap"); await launchUrl(Uri.parse(url)); diff --git a/lib/widgets/noticias/noticias_carrusel_widget.dart b/lib/widgets/noticias/noticias_carrusel_widget.dart index 8bde18c..4fbdf78 100644 --- a/lib/widgets/noticias/noticias_carrusel_widget.dart +++ b/lib/widgets/noticias/noticias_carrusel_widget.dart @@ -51,7 +51,7 @@ class NoticiasCarruselWidget extends StatelessWidget { return CarouselSlider.builder( options: CarouselOptions( autoPlay: true, - height: 200, + height: 230, viewportFraction: MediaQuery.of(context).orientation == Orientation.landscape ? 0.3 : 0.5, initialPage: 0, ), From 0fe672448aaa7d6f7ff9ac9e53e3efccbd4e3e5e Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:27:04 -0400 Subject: [PATCH 181/194] =?UTF-8?q?patch:=20se=20remueve=20revisi=C3=B3n?= =?UTF-8?q?=20de=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/services/grades_service.dart | 2 +- lib/widgets/horario/bloque_clase.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/grades_service.dart b/lib/services/grades_service.dart index 1f79a48..a9caea9 100644 --- a/lib/services/grades_service.dart +++ b/lib/services/grades_service.dart @@ -170,7 +170,7 @@ class GradesService { asignatura.notasParciales.any((it) => it.nota != null); void _notifyGradeUpdate(Carrera carrera, Asignatura asignatura, GradeChangeType changeType) { - final name = asignatura.nombre ?? asignatura.codigo; + final name = asignatura.nombre; String? title; String? body; diff --git a/lib/widgets/horario/bloque_clase.dart b/lib/widgets/horario/bloque_clase.dart index e089675..85897d7 100644 --- a/lib/widgets/horario/bloque_clase.dart +++ b/lib/widgets/horario/bloque_clase.dart @@ -41,7 +41,7 @@ class BloqueClase extends StatelessWidget { HorarioText.classCode("${block.codigo}", color: textColor, ), - HorarioText.className("${block.asignatura?.nombre?.toUpperCase()}", + HorarioText.className("${block.asignatura?.nombre.toUpperCase()}", color: textColor, ), HorarioText.classLocation(block.sala ?? "Sin sala", From e9ad31523e34b1c689b210659f816c8fdbbe7de1 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:27:13 -0400 Subject: [PATCH 182/194] =?UTF-8?q?patch:=20se=20convierte=20a=20funci?= =?UTF-8?q?=C3=B3n=20de=20cuerpo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/widgets/custom_drawer.dart | 58 ++++++++++++++++------------------ 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index 877aad2..e12b67b 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -16,6 +16,7 @@ import 'package:mi_utem/services/auth_service.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/themes/theme.dart'; +import 'package:mi_utem/utils/utils.dart'; import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; @@ -44,11 +45,7 @@ class CustomDrawer extends StatelessWidget { } } - List? get _menu { - return jsonDecode(RemoteConfigService.drawerMenu) - .where((e) => e['mostrar'] == true) - .toList(); - } + List get _menu => (jsonDecode(RemoteConfigService.drawerMenu).where((e) => e['mostrar'] == true).toList()) ?? []; @override Widget build(BuildContext context) { @@ -103,35 +100,36 @@ class CustomDrawer extends StatelessWidget { ), ), ), - for (final e in _menu!) - ListTile( - leading: Icon(IconData(e["icono"]["codePoint"], - fontFamily: e["icono"]["fontFamily"], - fontPackage: e["icono"]["fontPackage"], - )), - title: Text(e["nombre"]), - trailing: e["esNuevo"] ? badge.Badge( - shape: badge.BadgeShape.square, - borderRadius: BorderRadius.circular(10), - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), - elevation: 0, - badgeContent: const Text("Nuevo", - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, + SafeArea( + top: false, + child: Column( + children: _menu.map((e) => ListTile( + leading: Icon(IconData(e["icono"]["codePoint"], + fontFamily: e["icono"]["fontFamily"], + fontPackage: e["icono"]["fontPackage"], + )), + title: Text(e["nombre"]), + trailing: let(e["esNuevo"], (esNuevo) => badge.Badge( + showBadge: esNuevo, + shape: badge.BadgeShape.square, + borderRadius: BorderRadius.circular(10), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + elevation: 0, + badgeContent: const Text("Nuevo", + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), ), - ), - ) - : null, - onTap: () { - Widget? route = _getRoute(e["nombre"]); - if (route != null) { + )), + onTap: () => let(_getRoute(e["nombre"]), (route) { Navigator.push(context, MaterialPageRoute(builder: (ctx) => route)); ReviewService.checkAndRequestReview(context); - } - }, + }), + )).toList(), ), + ), Expanded( child: SafeArea( child: Column( From a714af382042af714c56d5a5ed2be3a21735b325 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:27:28 -0400 Subject: [PATCH 183/194] =?UTF-8?q?patch:=20se=20utiliza=20nueva=20funci?= =?UTF-8?q?=C3=B3n=20de=20#isOffline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/splash_screen.dart | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index cdfc640..c5c4656 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -2,15 +2,13 @@ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/constants.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/screens/login_screen/login_screen.dart'; import 'package:mi_utem/screens/main_screen.dart'; import 'package:mi_utem/screens/onboarding/welcome_screen.dart'; import 'package:mi_utem/services/analytics_service.dart'; import 'package:mi_utem/services/auth_service.dart'; -import 'package:mi_utem/utils/http/http_client.dart'; +import 'package:mi_utem/utils/http/functions.dart'; import 'package:mi_utem/widgets/loading/loading_dialog.dart'; import 'package:mi_utem/widgets/snackbar.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -70,17 +68,9 @@ class _SplashScreenState extends State { animation: "default", callback: (String val) async { showLoadingDialog(context); - // Revisar si tenemos conexión a internet - bool offlineMode = (await Preferencia.isOffline.get(defaultValue: "false", guardar: true)) == "true"; - try { - final response = await HttpClient.httpClient.head(apiUrl); - offlineMode = response.statusCode != 200; - } catch (e) { - logger.e("[SplashScreen]: Error al conectar con la API", e); - offlineMode = true; - } - await Preferencia.isOffline.set(offlineMode.toString()); + // Revisar si tenemos conexión a internet + bool offlineMode = await isOffline(); final user = await _authService.getUser(); if(offlineMode) { From 803258defd54d617b122dbb4c2e2dc48f9f6c8e9 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:27:56 -0400 Subject: [PATCH 184/194] patch: se utiliza SafeArea y se cambia Container por Padding Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/main_screen.dart | 52 +++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index f2d0fe7..495b41e 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -89,35 +89,39 @@ class _MainScreenState extends State { onRefresh: loadData, child: SingleChildScrollView( physics: AlwaysScrollableScrollPhysics(), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(height: 20), - Container( - padding: EdgeInsets.symmetric(horizontal: 20), - width: double.infinity, - child: FutureBuilder( - future: Preferencia.apodo.get(defaultValue: "N/N"), - initialData: _user?.primerNombre ?? "N/N", - builder: (ctx, snapshot) => MarkdownBody( - data: _greetingText.replaceAll("%name", snapshot.data ?? "N/N"), - styleSheet: MarkdownStyleSheet( - p: Theme.of(context).textTheme.displayMedium!.copyWith(fontWeight: FontWeight.normal), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(height: 20), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: SizedBox( + width: double.infinity, + child: FutureBuilder( + future: Preferencia.apodo.get(defaultValue: "N/N"), + initialData: _user?.primerNombre ?? "N/N", + builder: (ctx, snapshot) => MarkdownBody( + data: _greetingText.replaceAll("%name", snapshot.data ?? "N/N"), + styleSheet: MarkdownStyleSheet( + p: Theme.of(context).textTheme.displayMedium!.copyWith(fontWeight: FontWeight.normal), + ), + ), ), ), ), - ), - const SizedBox(height: 20), - PermisosCovidSection(), - const SizedBox(height: 20), - const QuickMenuSection(), - const SizedBox(height: 20), - if (_banners.isNotEmpty) ...[ - BannersSection(banners: _banners), const SizedBox(height: 20), + PermisosCovidSection(), + const SizedBox(height: 20), + const QuickMenuSection(), + const SizedBox(height: 20), + if (_banners.isNotEmpty) ...[ + BannersSection(banners: _banners), + const SizedBox(height: 20), + ], + NoticiasCarruselWidget(), ], - NoticiasCarruselWidget(), - ], + ), ), ), ), From bbb1857ccb67b87b36161dab8461524e75639d01 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:29:33 -0400 Subject: [PATCH 185/194] patch: se repara el horario en modo landscape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Se repara y mejora el horario en modo landscape (ahora no sale de sus márgenes) * Se centra automáticamente el horario al abrirlo Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/horario/horario_screen.dart | 59 ++++++++----------- .../horario/widgets/horario_corner.dart | 44 +++++++------- .../widgets/horario_main_scroller.dart | 53 +++++++++-------- 3 files changed, 71 insertions(+), 85 deletions(-) diff --git a/lib/screens/horario/horario_screen.dart b/lib/screens/horario/horario_screen.dart index 3125b74..bf7bda6 100644 --- a/lib/screens/horario/horario_screen.dart +++ b/lib/screens/horario/horario_screen.dart @@ -1,9 +1,8 @@ import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/controllers/horario_controller.dart'; import 'package:mi_utem/models/exceptions/custom_exception.dart'; import 'package:mi_utem/models/horario.dart'; @@ -35,12 +34,6 @@ class _HorarioScreenState extends State { @override void initState() { ReviewService.addScreen("HorarioScreen"); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); _forceRefresh = false; super.initState(); } @@ -50,27 +43,12 @@ class _HorarioScreenState extends State { horarioController.init(context); return FutureBuilder( future: () async { + _moveViewportToCurrentTime(); final data = await horarioController.getHorario(forceRefresh: _forceRefresh); _forceRefresh = false; return data; }(), builder: (context, snapshot) { - if(snapshot.hasError) { - final error = snapshot.error is CustomException ? (snapshot.error as CustomException).message : "Ocurrió un error al cargar el horario! Por favor intenta más tarde."; - logger.e("Error al cargar horario", snapshot.error, snapshot.stackTrace); - return Scaffold( - appBar: CustomAppBar( - title: Text("Horario"), - ), - body: Center( - child: CustomErrorWidget( - title: "Error al cargar el horario", - error: error, - ), - ), - ); - } - if(snapshot.connectionState == ConnectionState.waiting) { return Scaffold( appBar: CustomAppBar( @@ -81,7 +59,14 @@ class _HorarioScreenState extends State { } final horario = snapshot.data; - if(!snapshot.hasData || horario == null) { + final esErrorOffline = snapshot.hasError && snapshot.error is DioError && (snapshot.error as DioError).type == DioErrorType.cancel && (snapshot.error as DioError).response?.extra["offline"] == true; + if((snapshot.hasError && !esErrorOffline) || !snapshot.hasData || horario == null) { + String errorMessage = "Ocurrió un error al cargar el horario! Por favor intenta más tarde."; + final error = snapshot.error; + if(error != null) { + errorMessage = error is CustomException ? error.message : "Ocurrió un error al cargar el horario! Por favor intenta más tarde."; + } + return Scaffold( appBar: CustomAppBar( title: Text("Horario"), @@ -96,13 +81,13 @@ class _HorarioScreenState extends State { body: Center( child: CustomErrorWidget( title: "Error al cargar el horario", - error: "Ocurrió un error al cargar el horario! Por favor intenta más tarde.", + error: errorMessage, ), ), ); } - return Obx(() => Scaffold( + return Scaffold( appBar: CustomAppBar( title: Text("Horario"), actions: [ @@ -111,11 +96,11 @@ class _HorarioScreenState extends State { icon: Icon(Icons.refresh_sharp), tooltip: "Forzar actualización del horario", ), - if(!horarioController.isCenteredInCurrentPeriodAndDay.value) IconButton( + Obx(() => !horarioController.isCenteredInCurrentPeriodAndDay.value ? IconButton( onPressed: _moveViewportToCurrentTime, icon: Icon(Icons.center_focus_strong), tooltip: "Centrar Horario En Hora Actual", - ), + ) : Container()), IconButton( onPressed: () => _captureAndShareScreenshot(horario), icon: Icon(Icons.share), @@ -123,13 +108,16 @@ class _HorarioScreenState extends State { ) ], ), - body: Screenshot( - controller: _screenshotController, - child: HorarioMainScroller( - horario: horario, + body: SafeArea( + bottom: false, + child: Screenshot( + controller: _screenshotController, + child: HorarioMainScroller( + horario: horario, + ), ), ), - )); + ); }, ); } @@ -148,8 +136,7 @@ class _HorarioScreenState extends State { ); final image = await _screenshotController.captureFromWidget( horarioScroller.basicHorario, - targetSize: - Size(HorarioMainScroller.totalWidth, HorarioMainScroller.totalHeight), + targetSize: Size(HorarioMainScroller.totalWidth, HorarioMainScroller.totalHeight), ); final directory = await getApplicationDocumentsDirectory(); diff --git a/lib/screens/horario/widgets/horario_corner.dart b/lib/screens/horario/widgets/horario_corner.dart index 22823c4..c203f2e 100644 --- a/lib/screens/horario/widgets/horario_corner.dart +++ b/lib/screens/horario/widgets/horario_corner.dart @@ -13,30 +13,26 @@ class HorarioCorner extends StatelessWidget { this.backgroundColor = MainTheme.lightGrey, }); - List get _children => [ - TableRow( - children: [ - Container( - height: height, - width: width, - color: backgroundColor, - ), - ], - ), - ]; - @override - Widget build(BuildContext context) { - return Table( - defaultColumnWidth: FixedColumnWidth(width), - border: TableBorder( - right: BorderSide( - color: Color(0xFFBDBDBD), - style: BorderStyle.solid, - width: 2, - ), + Widget build(BuildContext context) => Table( + defaultColumnWidth: FixedColumnWidth(width), + border: TableBorder( + right: BorderSide( + color: Color(0xFFBDBDBD), + style: BorderStyle.solid, + width: 2, + ), + ), + children: [ + TableRow( + children: [ + Container( + height: height, + width: width, + color: backgroundColor, + ), + ], ), - children: _children, - ); - } + ], + ); } diff --git a/lib/screens/horario/widgets/horario_main_scroller.dart b/lib/screens/horario/widgets/horario_main_scroller.dart index cb2ae84..25acd43 100644 --- a/lib/screens/horario/widgets/horario_main_scroller.dart +++ b/lib/screens/horario/widgets/horario_main_scroller.dart @@ -23,9 +23,8 @@ class HorarioMainScroller extends StatefulWidget { final Horario horario; final bool showActive; - final controller = Get.find(); - HorarioMainScroller({ + const HorarioMainScroller({ super.key, required this.horario, this.showActive = true, @@ -63,15 +62,17 @@ class HorarioMainScroller extends StatefulWidget { class _HorarioMainScrollerState extends State { + final controller = Get.find(); + @override void initState() { - widget.controller.setOnUpdate(() => setState(() {})); + controller.setOnUpdate(() => setState(() {})); super.initState(); } @override void dispose() { - widget.controller.setOnUpdate(null); + controller.setOnUpdate(null); super.dispose(); } @@ -85,19 +86,21 @@ class _HorarioMainScrollerState extends State { height: HorarioMainScroller.periodsHeight, width: HorarioMainScroller.daysWidth, margin: EdgeInsets.only( - top: HorarioMainScroller.dayHeight * widget.controller.zoom.value, - left: HorarioMainScroller.periodWidth * widget.controller.zoom.value, + top: HorarioMainScroller.dayHeight * controller.zoom.value, + left: HorarioMainScroller.periodWidth * controller.zoom.value, ), - child: InteractiveViewer( - transformationController: widget.controller.blockContentController, - maxScale: HorarioMainScroller.defaultMaxScale, - minScale: HorarioMainScroller.defaultMinScale, - panAxis: PanAxis.free, - clipBehavior: Clip.none, - constrained: false, - onInteractionUpdate: (interaction) {}, - child: SafeArea( - child: widget._horarioBlocksContent, + child: ClipRect( + child: InteractiveViewer( + transformationController: controller.blockContentController, + maxScale: HorarioMainScroller.defaultMaxScale, + minScale: HorarioMainScroller.defaultMinScale, + panAxis: PanAxis.free, + clipBehavior: Clip.none, + constrained: false, + onInteractionUpdate: (interaction) {}, + child: SafeArea( + child: widget._horarioBlocksContent, + ), ), ), ), @@ -105,9 +108,9 @@ class _HorarioMainScrollerState extends State { Container( width: HorarioMainScroller.daysWidth, height: HorarioMainScroller.dayHeight, - margin: EdgeInsets.only(left: HorarioMainScroller.periodWidth * widget.controller.zoom.value), - child: InteractiveViewer( - transformationController: widget.controller.daysHeaderController, + margin: EdgeInsets.only(left: HorarioMainScroller.periodWidth * controller.zoom.value), + child: ClipRect(child: InteractiveViewer( + transformationController: controller.daysHeaderController, maxScale: HorarioMainScroller.defaultMaxScale, minScale: HorarioMainScroller.defaultMinScale, panAxis: PanAxis.free, @@ -118,14 +121,14 @@ class _HorarioMainScrollerState extends State { child: SafeArea( child: widget._horarioDaysHeader, ), - ), + )), ), /* Esquina del Horario */ Container( width: HorarioMainScroller.periodWidth, height: HorarioMainScroller.dayHeight, - child: InteractiveViewer( - transformationController: widget.controller.cornerController, + child: ClipRect(child: InteractiveViewer( + transformationController: controller.cornerController, maxScale: HorarioMainScroller.defaultMaxScale, minScale: HorarioMainScroller.defaultMinScale, panAxis: PanAxis.free, @@ -137,15 +140,15 @@ class _HorarioMainScrollerState extends State { child: SafeArea( child: widget._horarioCorner, ), - ), + )), ), /* Lista de Horas */ Container( width: HorarioMainScroller.periodWidth, height: HorarioMainScroller.periodsHeight, - margin: EdgeInsets.only(top: HorarioMainScroller.dayHeight * widget.controller.zoom.value), + margin: EdgeInsets.only(top: HorarioMainScroller.dayHeight * controller.zoom.value), child: InteractiveViewer( - transformationController: widget.controller.periodHeaderController, + transformationController: controller.periodHeaderController, maxScale: HorarioMainScroller.defaultMaxScale, minScale: HorarioMainScroller.defaultMinScale, panAxis: PanAxis.free, From 95c5740a4a6cdd6bf1f0e658f19bd3d012bd78ab Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:30:31 -0400 Subject: [PATCH 186/194] patch: se actualiza el CHANGELOG.md Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f7804..a6b6f12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,12 +33,14 @@ Tipos de cambios - Se agrega onboarding (con configuración de apodo, solicitud de permiso de notificaciones y bienvenida a la app). - Lista de estudiantes al resumen de asignatura. - Vista previa de estudiantes y profesores. -- Vista previa de los datos del profesor +- Vista previa de los datos del profesor. - Se agrega ventana de vista previa de la asignatura desde horario (al mantener presionado un bloque). - Se agrega navegación hacia la asignatura desde el horario al presionar un bloque. - Botón para limpiar las notas en la calculadora. - Sección de `Acerca de la App` en modo depuración. -- Se agrega métricas y toma de errores de Sentry (esta vez están bien configurados) +- Se agrega métricas y toma de errores de Sentry (esta vez están bien configurados). +- Se agrega modo fuera de línea para permitir el uso de la app sin conexión. +- Se permite cualquier orientación de pantalla (horizontal y vertical). ### Changed - Se actualizaron algunas dependencias. From 763b5b8998abcb39c5722cd8f31ce0233a575072 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:30:56 -0400 Subject: [PATCH 187/194] patch: bump version Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 1e9368d..9f0e7d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+121 +version: 3.0.0+124 environment: sdk: ">=2.17.0 <3.0.0" From 23a080f116d84dbf327f0efb04a40fb555f44141 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:23:09 +0000 Subject: [PATCH 188/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9f0e7d4..03b57a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+124 +version: 3.0.0+125 environment: sdk: ">=2.17.0 <3.0.0" From b470f75beb966dad44c83017e7deaf4956765d03 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:30:34 -0400 Subject: [PATCH 189/194] =?UTF-8?q?patch:=20se=20repara=20margen=20de=20?= =?UTF-8?q?=C3=A1rea=20segura=20*=20Se=20agrega=20SafeArea=20a=20varias=20?= =?UTF-8?q?vistas=20de=20la=20app=20para=20permitir=20el=20uso=20del=20dis?= =?UTF-8?q?positivo=20horizontal=20*=20Se=20repara=20y=20mejora=20el=20uso?= =?UTF-8?q?=20de=20buildCacheOptions=20en=20utils/http/functions.dart=20*?= =?UTF-8?q?=20Se=20repara=20OfflineModeInterceptor#onRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/{widgets/acerca => screens}/acerca_screen.dart | 4 ++-- lib/screens/asignatura/asignaturas_lista_screen.dart | 4 ++-- .../asignatura/detalle/asignatura_detalle_screen.dart | 11 +++-------- .../detalle/asignatura_estudiantes_tab.dart | 4 ++-- lib/screens/calculadora_notas_screen.dart | 4 ++-- lib/screens/credencial_screen.dart | 7 +++++++ lib/screens/perfil/perfil_screen.dart | 4 ++-- lib/screens/permiso_covid_screen.dart | 6 +++--- lib/utils/http/functions.dart | 11 ++++++----- .../http/interceptors/offline_mode_interceptor.dart | 6 +++--- .../acerca/dialog/acerca_dialog_action_button.dart | 2 +- lib/widgets/custom_drawer.dart | 2 +- lib/widgets/dialogs/not_ready_dialog.dart | 2 +- lib/widgets/dialogs/sad_dialog.dart | 2 +- lib/widgets/login_screen/creditos_app.dart | 2 +- 15 files changed, 37 insertions(+), 34 deletions(-) rename lib/{widgets/acerca => screens}/acerca_screen.dart (95%) diff --git a/lib/widgets/acerca/acerca_screen.dart b/lib/screens/acerca_screen.dart similarity index 95% rename from lib/widgets/acerca/acerca_screen.dart rename to lib/screens/acerca_screen.dart index 75b957b..b7ac294 100644 --- a/lib/widgets/acerca/acerca_screen.dart +++ b/lib/screens/acerca_screen.dart @@ -18,7 +18,7 @@ class AcercaScreen extends StatelessWidget { appBar: CustomAppBar( title: const Text("Acerca de Mi UTEM"), ), - body: SingleChildScrollView( + body: SafeArea(child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(10), child: Column( @@ -36,6 +36,6 @@ class AcercaScreen extends StatelessWidget { ], ), ), - ), + )), ); } diff --git a/lib/screens/asignatura/asignaturas_lista_screen.dart b/lib/screens/asignatura/asignaturas_lista_screen.dart index cbabd42..ecb9af9 100644 --- a/lib/screens/asignatura/asignaturas_lista_screen.dart +++ b/lib/screens/asignatura/asignaturas_lista_screen.dart @@ -46,7 +46,7 @@ class _AsignaturasListaScreenState extends State { ), ] : [], ), - body: PullToRefresh( + body: SafeArea(child: PullToRefresh( onRefresh: () async => setState(() => _forceRefresh = true), child: FutureBuilder?>>( future: () async { @@ -88,7 +88,7 @@ class _AsignaturasListaScreenState extends State { ); }, ), - ), + )), ); Widget _loadingWidget() => Padding( diff --git a/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart b/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart index 67f0bee..0557243 100644 --- a/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart +++ b/lib/screens/asignatura/detalle/asignatura_detalle_screen.dart @@ -4,7 +4,6 @@ import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/asignaturas/asignatura.dart'; import 'package:mi_utem/models/asignaturas/detalles/navigation_tab.dart'; import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/repositories/asignaturas_repository.dart'; import 'package:mi_utem/repositories/grades_repository.dart'; import 'package:mi_utem/screens/asignatura/detalle/asignatura_notas_tab.dart'; import 'package:mi_utem/screens/asignatura/detalle/asignatura_resumen_tab.dart'; @@ -49,12 +48,8 @@ class _AsignaturaDetalleScreenState extends State { child: AsignaturaNotasTab( asignatura: asignatura, onRefresh: () async { - final asignatura = (await Get.find().getAsignaturas(widget.carrera.id, forceRefresh: true))?.firstWhere((it) => it.id == this.asignatura.id).copyWith( - grades: await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: this.asignatura.id, forceRefresh: true), - ); - if(asignatura != null) { - setState(() => this.asignatura = asignatura); - } + final grades = await Get.find().getGrades(carreraId: widget.carrera.id, asignaturaId: this.asignatura.id, forceRefresh: true); + setState(() => this.asignatura = asignatura.copyWith(grades: grades)); }, ), initial: true, @@ -79,7 +74,7 @@ class _AsignaturaDetalleScreenState extends State { tabs: tabs.map((e) => Tab(text: e.label)).toList(), ), ), - body: TabBarView(children: tabs.map((e) => e.child).toList()), + body: SafeArea(child: TabBarView(children: tabs.map((e) => e.child).toList())), ), ); } diff --git a/lib/screens/asignatura/detalle/asignatura_estudiantes_tab.dart b/lib/screens/asignatura/detalle/asignatura_estudiantes_tab.dart index 8d6272d..d6701fc 100644 --- a/lib/screens/asignatura/detalle/asignatura_estudiantes_tab.dart +++ b/lib/screens/asignatura/detalle/asignatura_estudiantes_tab.dart @@ -34,7 +34,7 @@ class _AsignaturaEstudiantesTabState extends State { appBar: CustomAppBar( title: Text("Estudiantes"), ), - body: PullToRefresh( + body: SafeArea(child: PullToRefresh( onRefresh: () async => setState(() => _forceRefresh = true), child: FutureBuilder?>( future: () async { @@ -95,6 +95,6 @@ class _AsignaturaEstudiantesTabState extends State { ); }, ), - ), + )), ); } diff --git a/lib/screens/calculadora_notas_screen.dart b/lib/screens/calculadora_notas_screen.dart index c2f4d9e..a5cc0c7 100644 --- a/lib/screens/calculadora_notas_screen.dart +++ b/lib/screens/calculadora_notas_screen.dart @@ -46,13 +46,13 @@ class _CalculadoraNotasScreenState extends State { ), ], ), - body: ListView( + body: SafeArea(child: ListView( padding: const EdgeInsets.all(10), children: [ const DisplayNotasWidget(), const EditarNotasWidget(), ], - ), + )), ); } } diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index 0872c0a..0989670 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/carrera.dart'; @@ -30,6 +31,11 @@ class _CredencialScreenState extends State { @override void initState() { ReviewService.addScreen("CredencialScreen"); + // Set device orientation to portrait + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); ScreenProtector.preventScreenshotOn(); ScreenProtector.protectDataLeakageOn(); ScreenProtector.protectDataLeakageWithBlur(); @@ -40,6 +46,7 @@ class _CredencialScreenState extends State { void dispose() { ScreenProtector.preventScreenshotOff(); ScreenProtector.protectDataLeakageOff(); + SystemChrome.setPreferredOrientations(DeviceOrientation.values); super.dispose(); } diff --git a/lib/screens/perfil/perfil_screen.dart b/lib/screens/perfil/perfil_screen.dart index f20aadb..e3e5ea1 100644 --- a/lib/screens/perfil/perfil_screen.dart +++ b/lib/screens/perfil/perfil_screen.dart @@ -38,7 +38,7 @@ class _PerfilScreenState extends State { appBar: CustomAppBar( title: Text("Perfil"), ), - body: FutureBuilder( + body: SafeArea(child: FutureBuilder( future: Get.find().getUser(), builder: (ctx, snapshot) { if(snapshot.connectionState == ConnectionState.waiting) { @@ -169,6 +169,6 @@ class _PerfilScreenState extends State { ), ); }, - ), + )), ); } diff --git a/lib/screens/permiso_covid_screen.dart b/lib/screens/permiso_covid_screen.dart index 91d66cd..6634da5 100644 --- a/lib/screens/permiso_covid_screen.dart +++ b/lib/screens/permiso_covid_screen.dart @@ -24,12 +24,12 @@ class PermisoCovidScreen extends StatefulWidget { class _PermisoCovidScreenState extends State { - PermisoIngresoRepository _permisoIngresoRepository = Get.find(); + final PermisoIngresoRepository _permisoIngresoRepository = Get.find(); @override Widget build(BuildContext context) => Scaffold( appBar: CustomAppBar(title: Text("Permiso de ingreso")), - body: PullToRefresh( + body: SafeArea(child: PullToRefresh( onRefresh: () async { await _permisoIngresoRepository.getDetallesPermiso(widget.passId, forceRefresh: true); setState(() {}); @@ -62,6 +62,6 @@ class _PermisoCovidScreenState extends State { ); }, ), - ), + )), ); } diff --git a/lib/utils/http/functions.dart b/lib/utils/http/functions.dart index 51c9ad2..ff02eec 100644 --- a/lib/utils/http/functions.dart +++ b/lib/utils/http/functions.dart @@ -32,11 +32,12 @@ Future authClientRequest(String path, { primaryKey: 'miutem', subKey: path, maxStale: const Duration(days: 14), - ).copyWith( - method: method, - headers: headers, - contentType: contentType, - responseType: responseType, + options: Options( + method: method, + headers: headers, + contentType: contentType, + responseType: responseType, + ), ), ); diff --git a/lib/utils/http/interceptors/offline_mode_interceptor.dart b/lib/utils/http/interceptors/offline_mode_interceptor.dart index dfd7018..2b562b1 100644 --- a/lib/utils/http/interceptors/offline_mode_interceptor.dart +++ b/lib/utils/http/interceptors/offline_mode_interceptor.dart @@ -13,14 +13,14 @@ class OfflineModeInterceptor extends Interceptor { bool offlineMode = (await Preferencia.isOffline.getAsBool(defaultValue: false, guardar: true)); bool _forceRefresh = options.extra.containsKey(DIO_CACHE_KEY_FORCE_REFRESH) && options.extra[DIO_CACHE_KEY_FORCE_REFRESH] == true; if(!offlineMode || !_forceRefresh) { - return super.onRequest(options, handler); + return handler.next(options); } // Revisa si sigue offline realizando solicitud a la API (solo head) offlineMode = await isOffline(); if(!offlineMode) { // Si vuelve la conexión - return super.onRequest(options, handler); + return handler.next(options); } if(_forceRefresh) { @@ -32,7 +32,7 @@ class OfflineModeInterceptor extends Interceptor { options.extra[DIO_CACHE_KEY_FORCE_REFRESH] = offlineMode; - return super.onRequest(options, handler); + return handler.next(options); } @override diff --git a/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart b/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart index 41ea096..a8cbfaa 100644 --- a/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart +++ b/lib/widgets/acerca/dialog/acerca_dialog_action_button.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; +import 'package:mi_utem/screens/acerca_screen.dart'; class AcercaDialogActionButton extends StatefulWidget { diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index e12b67b..ad9a629 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -7,6 +7,7 @@ import 'package:mdi/mdi.dart'; import 'package:mi_utem/models/pair.dart'; import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/models/user/user.dart'; +import 'package:mi_utem/screens/acerca_screen.dart'; import 'package:mi_utem/screens/asignatura/asignaturas_lista_screen.dart'; import 'package:mi_utem/screens/credencial_screen.dart'; import 'package:mi_utem/screens/horario/horario_screen.dart'; @@ -17,7 +18,6 @@ import 'package:mi_utem/services/remote_config/remote_config.dart'; import 'package:mi_utem/services/review_service.dart'; import 'package:mi_utem/themes/theme.dart'; import 'package:mi_utem/utils/utils.dart'; -import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; import 'package:mi_utem/widgets/profile_photo.dart'; class CustomDrawer extends StatelessWidget { diff --git a/lib/widgets/dialogs/not_ready_dialog.dart b/lib/widgets/dialogs/not_ready_dialog.dart index 3fd893a..aa08c18 100644 --- a/lib/widgets/dialogs/not_ready_dialog.dart +++ b/lib/widgets/dialogs/not_ready_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:mi_utem/screens/acerca_screen.dart'; import 'package:mi_utem/themes/theme.dart'; -import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; import 'package:mi_utem/widgets/dialogs/error_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/widgets/dialogs/sad_dialog.dart b/lib/widgets/dialogs/sad_dialog.dart index c1296c0..db3305d 100644 --- a/lib/widgets/dialogs/sad_dialog.dart +++ b/lib/widgets/dialogs/sad_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; +import 'package:mi_utem/screens/acerca_screen.dart'; final _formKey = GlobalKey(); diff --git a/lib/widgets/login_screen/creditos_app.dart b/lib/widgets/login_screen/creditos_app.dart index 787a578..c5a38e3 100644 --- a/lib/widgets/login_screen/creditos_app.dart +++ b/lib/widgets/login_screen/creditos_app.dart @@ -3,8 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:mi_utem/screens/acerca_screen.dart'; import 'package:mi_utem/services/remote_config/remote_config.dart'; -import 'package:mi_utem/widgets/acerca/acerca_screen.dart'; class CreditosApp extends StatelessWidget { From 00cf7a0f204a10e0062f4cba68774babfc9bf5cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:07:28 +0000 Subject: [PATCH 190/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 03b57a6..1f1828b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+125 +version: 3.0.0+126 environment: sdk: ">=2.17.0 <3.0.0" From 5a2fcf9dd21d86b1ab2570672690b82dad6d3507 Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 25 Jun 2024 00:22:08 -0400 Subject: [PATCH 191/194] =?UTF-8?q?patch:=20arreglos=20*=20Se=20agrega=20m?= =?UTF-8?q?=C3=A9todo=20para=20enviar=20notificaciones=20de=20anuncios.=20?= =?UTF-8?q?*=20Se=20repara=20error=20en=20notificaciones.=20*=20Se=20repar?= =?UTF-8?q?a=20error=20al=20refrescar=20datos=20en=20segundo=20plano.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/credencial_screen.dart | 6 +- .../onboarding/notifications_screen.dart | 8 +- lib/services/auth_service.dart | 4 +- lib/services/background_service.dart | 82 ++++++++++++------- lib/services/notification_service.dart | 21 +++++ pubspec.yaml | 2 +- 6 files changed, 80 insertions(+), 43 deletions(-) diff --git a/lib/screens/credencial_screen.dart b/lib/screens/credencial_screen.dart index 0989670..19180ce 100644 --- a/lib/screens/credencial_screen.dart +++ b/lib/screens/credencial_screen.dart @@ -67,12 +67,8 @@ class _CredencialScreenState extends State { final authService = Get.find(); final carrerasService = Get.find(); - if(carrerasService.selectedCarrera == null) { - await carrerasService.getCarreras(); - } - final user = await authService.getUser(); - final carrera = carrerasService.selectedCarrera; + final carrera = await carrerasService.getCarreras(); return Pair(user, carrera); }(), diff --git a/lib/screens/onboarding/notifications_screen.dart b/lib/screens/onboarding/notifications_screen.dart index 63eaf18..19c7c53 100644 --- a/lib/screens/onboarding/notifications_screen.dart +++ b/lib/screens/onboarding/notifications_screen.dart @@ -1,4 +1,3 @@ -import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:flutter/material.dart'; import 'package:mi_utem/models/preferencia.dart'; import 'package:mi_utem/screens/main_screen.dart'; @@ -93,13 +92,10 @@ class _NotificationsScreenState extends State { await Preferencia.onboardingStep.set("complete"); Navigator.popUntil(context, (route) => route.isFirst); final alias = await Preferencia.apodo.get(); - AwesomeNotifications().createNotification(content: NotificationContent( - id: 1, - channelKey: NotificationService.announcementsChannelKey, - actionType: ActionType.Default, + NotificationService.showAnnouncementNotification( title: alias != null ? "¡Hola $alias! 🎉" : "¡Hola! 🎉", body: '¡Te damos la bienvenida a la aplicación Mi UTEM! 🚀', - )); + ); Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => MainScreen())); }, style: ElevatedButton.styleFrom( diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 3459202..1283d88 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -25,14 +25,14 @@ class AuthService { Future isLoggedIn({ bool forceRefresh = false }) async { final credentials = await _getCredential(); if(credentials == null) { - logger.d("[AuthService#isLoggedIn]: no credential"); + logger.d("[AuthService#isLoggedIn]: No se encontraron credenciales."); return false; } final user = await getUser(); final userToken = user?.token; if(user == null || userToken == null) { - logger.d("[AuthService#isLoggedIn]: Usuario o token nulo (user: ${user == null}, token: ${userToken == null})"); + logger.d("[AuthService#isLoggedIn]: Usuario o token nulo (user?: ${user == null}, token?: ${userToken == null})"); return false; } diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 715312d..ea3f9b2 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -41,14 +41,13 @@ class BackgroundController { } class BackgroundService { + static Future initAndStart() async { BackgroundFetch.registerHeadlessTask(BackgroundController.backgroundFetchHeadlessTask); - await BackgroundFetch.configure(_backgroundFetchConfig, (taskId) { - Sentry.metrics().timing("BackgroundFetch_$taskId", - function: _onFetch(taskId), - unit: DurationSentryMeasurementUnit.milliSecond, - ); - }, _onTimeout); + await BackgroundFetch.configure(_backgroundFetchConfig, (taskId) => Sentry.metrics().timing("BackgroundFetch_$taskId", + function: () async => await _onFetch(taskId), + unit: DurationSentryMeasurementUnit.milliSecond, + ), _onTimeout); BackgroundFetch.start().then((_) {}).catchError((e, stackTrace) { Sentry.captureException(e, stackTrace: stackTrace); @@ -71,31 +70,38 @@ class BackgroundService { now = DateTime.now(); // Refresca las carreras - await Get.find().getCarreras(forceRefresh: true); - logger.d("[BackgroundFetch]: Se refrescaron las carreras, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); - now = DateTime.now(); + now = await refrescarCarreras(now); + + // Actualiza el horario + now = await refrescarHorario(now); // Revisa si hubo un cambio en las notas - await Get.find().lookForGradeUpdates(); - logger.d("[BackgroundFetch]: Se revisaron las notas, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); - now = DateTime.now(); + now = await notificarCambiosNotas(now); // Actualiza los permisos de ingreso + now = await refrescarPermisos(now); + + // Actualiza los datos de las asignaturas + now = await refrescarAsignaturasYEstudiantes(now); + + logger.d("[BackgroundFetch]: Se terminó la tarea '$taskId', tomó ${DateTime.now().difference(init).inMilliseconds} ms"); + } + + static Future refrescarHorario(DateTime now) async { try { - PermisoIngresoRepository permisoIngresoRepository = Get.find(); - final permisos = await permisoIngresoRepository.getPermisos(forceRefresh: true); - for(final permiso in permisos) { - final id = permiso.id; - if(id == null) continue; - await permisoIngresoRepository.getDetallesPermiso(id, forceRefresh: true); + final carreraId = (await Get.find().getCarreras())?.id; + if(carreraId != null) { + await Get.find().getHorario(carreraId, forceRefresh: true); } - } catch (_){} - logger.d("[BackgroundFetch]: Se refrescaron los permisos de ingreso, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + } catch(_){} + logger.d("[BackgroundFetch]: Se refrescó el horario, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); now = DateTime.now(); + return now; + } - // Actualiza los datos de las carreras y asignaturas + static Future refrescarAsignaturasYEstudiantes(DateTime now) async { try { - final carreraId = Get.find().selectedCarrera?.id; + final carreraId = (await Get.find().getCarreras())?.id; if(carreraId != null) { AsignaturasRepository asignaturasRepository = Get.find(); final asignaturas = await asignaturasRepository.getAsignaturas(carreraId, forceRefresh: true) ?? []; @@ -106,18 +112,36 @@ class BackgroundService { } catch(_){} logger.d("[BackgroundFetch]: Se refrescaron los datos de las carreras y asignaturas, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); now = DateTime.now(); + return now; + } - // Actualiza el horario + static Future refrescarPermisos(DateTime now) async { try { - final carreraId = Get.find().selectedCarrera?.id; - if(carreraId != null) { - await Get.find().getHorario(carreraId, forceRefresh: true); + PermisoIngresoRepository permisoIngresoRepository = Get.find(); + final permisos = await permisoIngresoRepository.getPermisos(forceRefresh: true); + for(final permiso in permisos) { + final id = permiso.id; + if(id == null) continue; + await permisoIngresoRepository.getDetallesPermiso(id, forceRefresh: true); } - } catch(_){} - logger.d("[BackgroundFetch]: Se refrescó el horario, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + } catch (_){} + logger.d("[BackgroundFetch]: Se refrescaron los permisos de ingreso, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); now = DateTime.now(); + return now; + } - logger.d("[BackgroundFetch]: Se terminó la tarea '$taskId', tomó ${DateTime.now().difference(init).inMilliseconds} ms"); + static Future notificarCambiosNotas(DateTime now) async { + await Get.find().lookForGradeUpdates(); + logger.d("[BackgroundFetch]: Se revisaron las notas, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + now = DateTime.now(); + return now; + } + + static Future refrescarCarreras(DateTime now) async { + await Get.find().getCarreras(forceRefresh: true); + logger.d("[BackgroundFetch]: Se refrescaron las carreras, tomó ${DateTime.now().difference(now).inMilliseconds} ms"); + now = DateTime.now(); + return now; } static _onTimeout(String taskId) async { diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 12cb2de..fca5429 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -71,6 +71,27 @@ class NotificationService { return isAllowed; } + static void showAnnouncementNotification({ + required String title, + required String body, + Map payload = const {}, + }) async { + if(!await hasAllowedNotifications()) { + return; + } + + notifications.createNotification(content: NotificationContent( + id: payload.hashCode, + channelKey: announcementsChannelKey, + title: title, + body: body, + payload: { + 'type': 'announcement', + ...payload, + }, + )); + } + static void showGradeChangeNotification({ required String title, required String body, diff --git a/pubspec.yaml b/pubspec.yaml index 1f1828b..049d0bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+126 +version: 3.0.0+129 environment: sdk: ">=2.17.0 <3.0.0" From 7785449837021dd4628b6cafa65aaf9a969749d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 04:55:17 +0000 Subject: [PATCH 192/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 049d0bb..9b4a9f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+129 +version: 3.0.0+130 environment: sdk: ">=2.17.0 <3.0.0" From 4f83282560ac1cea230d1f3f54bc688e10902b0b Mon Sep 17 00:00:00 2001 From: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:45:19 -0400 Subject: [PATCH 193/194] =?UTF-8?q?patch:=20enfoque=20en=20privacidad=20y?= =?UTF-8?q?=20arreglos=20a=20firebase=20*=20Se=20remueven=20IIP=20(informa?= =?UTF-8?q?ci=C3=B3n=20de=20identificaci=C3=B3n=20personal)=20*=20Se=20rep?= =?UTF-8?q?ara=20configuraci=C3=B3n=20de=20firebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Francisco Solis <30329003+Im-Fran@users.noreply.github.com> --- lib/screens/splash_screen.dart | 3 -- lib/services/analytics_service.dart | 50 ------------------- lib/services/carreras_service.dart | 2 - lib/services/remote_config/remote_config.dart | 4 ++ lib/widgets/login_screen/login_button.dart | 1 - 5 files changed, 4 insertions(+), 56 deletions(-) diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index c5c4656..2e6a10f 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -89,9 +89,6 @@ class _SplashScreenState extends State { final isLoggedIn = await _authService.isLoggedIn(); AnalyticsService.removeUser(); - if(isLoggedIn && user != null) { - AnalyticsService.setUser(user); - } // Esto nos asegura de que el splash es la única ruta inicial, y resuelve el error de poder volver al login. Navigator.popUntil(context, (route) => route.isFirst); diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart index a3efc90..5cf7b3b 100644 --- a/lib/services/analytics_service.dart +++ b/lib/services/analytics_service.dart @@ -1,58 +1,8 @@ -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter_uxcam/flutter_uxcam.dart'; -import 'package:mi_utem/models/carrera.dart'; -import 'package:mi_utem/models/user/user.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class AnalyticsService { - static Future setUser(User user) async { - await FirebaseAnalytics.instance.setUserId(id: user.correoUtem); - await FlutterUxcam.setUserIdentity(user.correoUtem); - - await FirebaseAnalytics.instance.setUserProperty(name: "name", value: user.primerNombre); - await FlutterUxcam.setUserProperty("name", user.primerNombre); - - if (user.apellidos != null) { - await FirebaseAnalytics.instance.setUserProperty(name: "last_name", value: user.apellidos); - await FlutterUxcam.setUserProperty("last_name", user.apellidos!); - } - - final correoUtem = user.correoUtem; - - await Sentry.configureScope((scope) => scope.setUser(SentryUser( - id: correoUtem, - email: correoUtem, - name: user.nombreCompletoCapitalizado, - username: user.username, - data: { - "backend_uid": correoUtem != null ? md5.convert(utf8.encode(correoUtem)) : "desconocido", - }, - ipAddress: "{{auto}}", - ))); - } - - static Future setCarreraToUser(Carrera carrera) async { - if (carrera.nombre != null) { - await FirebaseAnalytics.instance.setUserProperty(name: "carreraActiva", value: carrera.nombre!); - await FlutterUxcam.setUserProperty("carreraActiva", carrera.nombre!); - } - - if (carrera.estado != null) { - await FirebaseAnalytics.instance.setUserProperty(name: "estadoCarreraActiva", value: carrera.estado!); - await FlutterUxcam.setUserProperty("estadoCarreraActiva", carrera.estado!); - } - - await Sentry.configureScope((scope) => scope.setUser(scope.user?.copyWith( - data: { - ...?scope.user?.data, - "carrera": carrera.nombre, - "estado_carrera": carrera.estado, - }, - ))); - } static Future removeUser() async { await FirebaseAnalytics.instance.setUserId(id: null); diff --git a/lib/services/carreras_service.dart b/lib/services/carreras_service.dart index 1cf33c3..35e999a 100644 --- a/lib/services/carreras_service.dart +++ b/lib/services/carreras_service.dart @@ -3,7 +3,6 @@ import 'package:get/get.dart'; import 'package:mi_utem/config/logger.dart'; import 'package:mi_utem/models/carrera.dart'; import 'package:mi_utem/repositories/carreras_repository.dart'; -import 'package:mi_utem/services/analytics_service.dart'; class CarrerasService { @@ -34,7 +33,6 @@ class CarrerasService { carreras.sort((a,b) => estados.indexOf(b.estado!.toLowerCase()).compareTo(estados.indexOf(a.estado!.toLowerCase()))); final carreraActiva = carreras.first; - AnalyticsService.setCarreraToUser(carreraActiva); changeSelectedCarrera(carreraActiva); } diff --git a/lib/services/remote_config/remote_config.dart b/lib/services/remote_config/remote_config.dart index d5124de..48580a7 100644 --- a/lib/services/remote_config/remote_config.dart +++ b/lib/services/remote_config/remote_config.dart @@ -54,6 +54,10 @@ class RemoteConfigService { try { await _firebaseRemoteConfigInstance.setDefaults(defaults); await _firebaseRemoteConfigInstance.fetchAndActivate(); + await _firebaseRemoteConfigInstance.setConfigSettings(RemoteConfigSettings( + minimumFetchInterval: Duration(hours: 12), + fetchTimeout: Duration(minutes: 1), + )); } catch (exception) { logger.e('Error al descargar la configuración remota. Se usarán los valores guardados en cache o los valores por defecto', exception); } diff --git a/lib/widgets/login_screen/login_button.dart b/lib/widgets/login_screen/login_button.dart index acd01b7..1e5bb3b 100644 --- a/lib/widgets/login_screen/login_button.dart +++ b/lib/widgets/login_screen/login_button.dart @@ -95,7 +95,6 @@ class _LoginButtonState extends State { } AnalyticsService.logEvent('login'); - AnalyticsService.setUser(user); Navigator.of(context).popUntil((route) => route.isFirst); // Esto elimina todas las pantallas anteriores // Y esto reemplaza la pantalla actual por la nueva, cosa de que no pueda "volver" al login a menos que cierre la sesión. From 63d9f3008c4bf0ae442c61d4cc7523542e65c4eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 03:25:49 +0000 Subject: [PATCH 194/194] ci(bump-version): github action bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9b4a9f5..e95d083 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mi_utem description: 'Plataforma académica para estudiantes de la Universidad Tecnológica Metropolitana (UTEM)' publish_to: none -version: 3.0.0+130 +version: 3.0.0+131 environment: sdk: ">=2.17.0 <3.0.0"