diff --git a/.env.example b/.env.example index 982c4bd..21d2e5c 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ -COURSES_URL= -GRADS_URL= -GRADE_OPTIMIZER_URL= +API_COURSES= +API_SUBJECTS= +API_GENETIC_ALGORITHM= +API_SUPPORT_URL= +API_FINAL_SCORE_URL= +API_NOTIFICATIONS= diff --git a/.github/workflows/build_IOS.yml b/.github/workflows/build_IOS.yml index bbdbf77..72f6d31 100644 --- a/.github/workflows/build_IOS.yml +++ b/.github/workflows/build_IOS.yml @@ -28,7 +28,6 @@ # run: | # echo "GRADS_URL=${{ vars.API_COURSES }}" >> .env # The courses API # echo "COURSES_URL=${{ vars.API_SUBJECTS }}" >> .env # The subjects API - # echo "GRADE_OPTIMIZER_URL=${{ vars.API_GRADE_OPTMIZER }}" >> .env # The grade optimizer API # - name: Setup Fastlane # uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 732a13a..7dc359c 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -6,20 +6,16 @@ on: - homolog - prod - workflow_call: - jobs: build_android: environment: ${{ github.ref_name }} runs-on: ubuntu-latest permissions: id-token: write - contents: write + contents: read steps: - name: Checkout uses: actions/checkout@v4 - with: - repository: ${{ inputs.repository }} - name: Get Version run: | @@ -29,27 +25,38 @@ jobs: echo "app_build_name=$app_build_name" >> $GITHUB_ENV - name: Setup Java Version - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: "12.x" + java-version: "17" + distribution: 'temurin' - name: Setup Flutter Version - uses: subosito/flutter-action@v1 + uses: subosito/flutter-action@v2 with: flutter-version: "3.24.3" - name: Get flutter dependencies run: flutter pub get - - name: Setup Fastlane - uses: ruby/setup-ruby@v1 - with: - ruby-version: "2.6" - bundler-cache: true - working-directory: android - - - name: Set envs + - name: Set AWS Account ID run: | + if [[ "${{ github.ref_name }}" == "dev" ]]; then + echo "AWS_ACCOUNT_ID=${{ secrets.AWS_ACCOUNT_ID_DEV }}" >> $GITHUB_ENV + elif [[ "${{ github.ref_name }}" == "homolog" ]]; then + echo "AWS_ACCOUNT_ID=${{ secrets.AWS_ACCOUNT_ID_HOML }}" >> $GITHUB_ENV + elif [[ "${{ github.ref_name }}" == "prod" ]]; then + echo "AWS_ACCOUNT_ID=${{ secrets.AWS_ACCOUNT_ID_PROD }}" >> $GITHUB_ENV + else + echo "Invalid branch name!" && exit 1 + fi + echo "STACK_NAME=DevMediasStack${{github.ref_name}}" >> $GITHUB_ENV + + - name: Setup AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/GithubActionsRole + role-session-name: github-action - name: Configure Keystore run: | @@ -65,33 +72,51 @@ jobs: ANDROID_KEYSTORE_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_STORE_PASSWORD }} working-directory: android + # Busca o endpoint do APIGW no SSM (configurado no back). + - name: Fetch config from SSM + run: | + STAGE=$(echo "${{ github.ref_name }}" | tr '[:upper:]' '[:lower:]') + + API_BASE=$(aws ssm get-parameter \ + --name "/devmedias/$STAGE/api/url" \ + --query "Parameter.Value" --output text) + + API_BASE="${API_BASE%/}/" + echo "API_BASE=$API_BASE" >> $GITHUB_ENV + - name: Create .env file run: | - echo "API_COURSES=${{vars.API_COURSES}}" >> .env # The courses API - echo "API_SUBJECTS=${{vars.API_SUBJECTS}}" >> .env # The subjects API - echo "API_GRADE_OPTMIZER=${{vars.API_GRADE_OPTMIZER}}" >> .env # The grade optimizer API - echo "API_SUPPORT_URL=${{vars.API_SUPPORT_URL}}" >> .env # The support API - echo "API_FINAL_SCORE_URL=${{vars.API_FINAL_SCORE_URL}}" >> .env # The final score API + { + echo "API_COURSES=${API_BASE}get-all-cursos" + echo "API_SUBJECTS=${API_BASE}get-all-disciplinas" + echo "API_GENETIC_ALGORITHM=${API_BASE}genetic-algorithm" + echo "API_SUPPORT_URL=${API_BASE}public/contact-us" + echo "API_FINAL_SCORE_URL=${API_BASE}calculate-mean" + echo "API_NOTIFICATIONS=" + } >> .env - name: Build Android run: | - flutter build appbundle --release --build-name=$app_build_name --build-number=$app_build_number + flutter build appbundle --release \ + --build-name=$app_build_name \ + --build-number=$app_build_number - name: Upload to artifacts uses: actions/upload-artifact@v4 with: name: app-release.aab path: build/app/outputs/bundle/release/ + retention-days: 1 deploy_to_play_store: needs: build_android environment: ${{ github.ref_name }} runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v4 - with: - repository: ${{ inputs.repository }} - name: Download artifact uses: actions/download-artifact@v4 @@ -102,12 +127,12 @@ jobs: - name: Setup Fastlane uses: ruby/setup-ruby@v1 with: - ruby-version: "2.6" + ruby-version: "3.2" bundler-cache: true working-directory: android - name: Exec fastlane - run: bundle exec fastlane ${{github.ref_name}} + run: bundle exec fastlane ${{ github.ref_name }} env: PLAY_STORE_CONFIG_JSON: ${{ secrets.PLAY_STORE_CONFIG_JSON }} working-directory: android diff --git a/.gitignore b/.gitignore index d40954c..9be5be1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ # Miscellaneous *.class *.log +# Lock files (do not ignore Dart/Flutter lockfile — commit pubspec.lock) *.lock +!pubspec.lock *.pyc *.swp .DS_Store @@ -35,6 +37,11 @@ migrate_working_dir/ .pub/ /build/ +# Platform build outputs (Gradle, CMake, etc.) +/android/build/ +/android/app/build/ +/ios/build/ + # Symbolication related app.*.symbols diff --git a/android/.gitignore b/android/.gitignore index 6f56801..e67dffd 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -1,5 +1,7 @@ gradle-wrapper.jar /.gradle +/build/ +app/build/ /captures/ /gradlew /gradlew.bat diff --git a/android/Gemfile.lock b/android/Gemfile.lock index 29f7dcf..da7f77e 100644 --- a/android/Gemfile.lock +++ b/android/Gemfile.lock @@ -215,4 +215,4 @@ DEPENDENCIES fastlane BUNDLED WITH - 1.17.2 \ No newline at end of file + 1.17.2 diff --git a/android/app/build.gradle b/android/app/build.gradle index 8f4efc8..d53381e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -43,12 +43,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -57,7 +57,7 @@ android { defaultConfig { applicationId "com.dev_medias_front_rn_v2" - minSdkVersion 21 + minSdkVersion flutter.minSdkVersion targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/kotlin/com/example/dev_medias_front_flutter/MainActivity.kt b/android/app/src/main/kotlin/com/dev_medias_front_rn_v2/MainActivity.kt similarity index 100% rename from android/app/src/main/kotlin/com/example/dev_medias_front_flutter/MainActivity.kt rename to android/app/src/main/kotlin/com/dev_medias_front_rn_v2/MainActivity.kt diff --git a/android/build.gradle b/android/build.gradle index bc157bd..690d416 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,14 +5,15 @@ allprojects { } } -rootProject.buildDir = '../build' +rootProject.layout.buildDirectory.value(rootProject.layout.buildDirectory.dir("../../build").get()) + subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" + project.layout.buildDirectory.value(rootProject.layout.buildDirectory.dir(project.name).get()) } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { - delete rootProject.buildDir + delete rootProject.layout.buildDirectory } diff --git a/android/fastlane/Appfile b/android/fastlane/Appfile index 3ee7e12..a507123 100644 --- a/android/fastlane/Appfile +++ b/android/fastlane/Appfile @@ -1 +1 @@ -package_name("com.dev_medias_front_rn") \ No newline at end of file +package_name("com.dev_medias_front_rn_v2") \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e1ca574..afa1e8e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 1d6d19b..bd247b0 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,8 +19,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "com.android.application" version "8.7.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 310f180..3d62243 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Podfile b/ios/Podfile index d97f17e..e51a31d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2d4708e..5702378 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -21,10 +21,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 +PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3703d84..08dcd71 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -454,7 +454,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -585,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -636,7 +636,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5d..e3773d4 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/lib/app/controller/add_page_controller.dart b/lib/app/controller/add_page_controller.dart index 22eeec4..d0d6386 100644 --- a/lib/app/controller/add_page_controller.dart +++ b/lib/app/controller/add_page_controller.dart @@ -20,7 +20,8 @@ abstract class AddPageControllerBase with Store { @action Future loadCourses() async { setCoursesLoaded(false); - final result = await coursesController.fetchCourses(); + await coursesController.refreshAllSubjects(); + final result = Map.from(coursesController.allCourses!); result.removeWhere( (key, value) => userController.currentCourses.contains(value.code)); availableCourses = ObservableMap.of(result); diff --git a/lib/app/controller/common/courses_controller.dart b/lib/app/controller/common/courses_controller.dart index 17a85f9..f58f8d2 100644 --- a/lib/app/controller/common/courses_controller.dart +++ b/lib/app/controller/common/courses_controller.dart @@ -57,7 +57,21 @@ abstract class CoursesControllerBase with Store { setLoadedCourses(true); return response; } catch (e) { - throw Exception('Erro de rede: $e'); + setLoadedCourses(allCourses != null && allCourses!.isNotEmpty); + rethrow; + } + } + + /// GET de todas as disciplinas + atualização do catálogo e lista da home. + @action + Future refreshAllSubjects() async { + try { + final courses = await fetchCourses(); + setAllCourses(courses); + await userController.pruneCurrentCoursesWithCatalog(); + return true; + } catch (_) { + return false; } } } diff --git a/lib/app/controller/common/courses_controller.g.dart b/lib/app/controller/common/courses_controller.g.dart index a060a7e..23e4750 100644 --- a/lib/app/controller/common/courses_controller.g.dart +++ b/lib/app/controller/common/courses_controller.g.dart @@ -64,6 +64,15 @@ mixin _$CoursesController on CoursesControllerBase, Store { return _$fetchCoursesAsyncAction.run(() => super.fetchCourses()); } + late final _$refreshAllSubjectsAsyncAction = + AsyncAction('CoursesControllerBase.refreshAllSubjects', context: context); + + @override + Future refreshAllSubjects() { + return _$refreshAllSubjectsAsyncAction + .run(() => super.refreshAllSubjects()); + } + late final _$CoursesControllerBaseActionController = ActionController(name: 'CoursesControllerBase', context: context); @@ -89,6 +98,17 @@ mixin _$CoursesController on CoursesControllerBase, Store { } } + @override + void deleteAllCurrentCourses() { + final _$actionInfo = _$CoursesControllerBaseActionController.startAction( + name: 'CoursesControllerBase.deleteAllCurrentCourses'); + try { + return super.deleteAllCurrentCourses(); + } finally { + _$CoursesControllerBaseActionController.endAction(_$actionInfo); + } + } + @override String toString() { return ''' diff --git a/lib/app/controller/common/notifications_controller.dart b/lib/app/controller/common/notifications_controller.dart new file mode 100644 index 0000000..3dc27fd --- /dev/null +++ b/lib/app/controller/common/notifications_controller.dart @@ -0,0 +1,26 @@ +import 'package:dev_medias_front_flutter/app/model/notice.dart'; +import 'package:dev_medias_front_flutter/app/service/notification_service.dart'; +import 'package:mobx/mobx.dart'; + +part 'notifications_controller.g.dart'; + +class NotificationsController = NotificationsControllerBase + with _$NotificationsController; + +abstract class NotificationsControllerBase with Store { + NotificationsControllerBase(); + + final NotificationService _service = NotificationService(); + + @observable + ObservableList notices = ObservableList(); + + @action + Future fetchNotifications() async { + final list = await _service.getNotifications(); + notices = ObservableList.of(list); + } +} + +final NotificationsController notificationsController = + NotificationsController(); diff --git a/lib/app/controller/common/notifications_controller.g.dart b/lib/app/controller/common/notifications_controller.g.dart new file mode 100644 index 0000000..bf434e9 --- /dev/null +++ b/lib/app/controller/common/notifications_controller.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'notifications_controller.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$NotificationsController on NotificationsControllerBase, Store { + late final _$noticesAtom = + Atom(name: 'NotificationsControllerBase.notices', context: context); + + @override + ObservableList get notices { + _$noticesAtom.reportRead(); + return super.notices; + } + + @override + set notices(ObservableList value) { + _$noticesAtom.reportWrite(value, super.notices, () { + super.notices = value; + }); + } + + late final _$fetchNotificationsAsyncAction = AsyncAction( + 'NotificationsControllerBase.fetchNotifications', + context: context); + + @override + Future fetchNotifications() { + return _$fetchNotificationsAsyncAction + .run(() => super.fetchNotifications()); + } + + @override + String toString() { + return ''' +notices: ${notices} + '''; + } +} diff --git a/lib/app/controller/common/support_controller.dart b/lib/app/controller/common/support_controller.dart index 7fbad83..6b9cbd2 100644 --- a/lib/app/controller/common/support_controller.dart +++ b/lib/app/controller/common/support_controller.dart @@ -23,17 +23,17 @@ abstract class _SupportControllerBase with Store { TextEditingController get getMessageController => messageController; @action - Future sendSupport() async { + Future> sendSupport() async { final message = { - "email": emailController.text, - "message": messageController.text, + "email": emailController.text.trim(), + "message": messageController.text.trim(), }; - try { - final response = await supportService.postMessage(message); - return response; - } catch (e) { - throw Exception('Erro de rede: $e'); + final response = await supportService.postMessage(message); + if (response['success'] == true) { + emailController.clear(); + messageController.clear(); } + return response; } } diff --git a/lib/app/controller/common/support_controller.g.dart b/lib/app/controller/common/support_controller.g.dart index c09e21e..b70d3e0 100644 --- a/lib/app/controller/common/support_controller.g.dart +++ b/lib/app/controller/common/support_controller.g.dart @@ -62,7 +62,7 @@ mixin _$SupportController on _SupportControllerBase, Store { AsyncAction('_SupportControllerBase.sendSupport', context: context); @override - Future> sendSupport() { + Future> sendSupport() { return _$sendSupportAsyncAction.run(() => super.sendSupport()); } diff --git a/lib/app/controller/common/user_controller.dart b/lib/app/controller/common/user_controller.dart index a958543..2d3fb19 100644 --- a/lib/app/controller/common/user_controller.dart +++ b/lib/app/controller/common/user_controller.dart @@ -141,6 +141,22 @@ abstract class UserControllerBase with Store { currentCourses.clear(); } + /// Remove da home códigos que não existem mais no catálogo da API. + @action + Future pruneCurrentCoursesWithCatalog() async { + final catalog = coursesController.allCourses; + if (catalog == null || catalog.isEmpty) return; + + final valid = currentCourses + .where((code) => catalog.containsKey(code)) + .toList(); + if (valid.length == currentCourses.length) return; + + final response = await service.replaceCurrentCourses(valid); + setCurrentCourses( + ObservableList.of(response['currentCourses']!.toList())); + } + @action Future loadInitialCourses(String code, int year) async { code = code.split(" ")[0]; diff --git a/lib/app/controller/common/user_controller.g.dart b/lib/app/controller/common/user_controller.g.dart index 645db6f..b07ef47 100644 --- a/lib/app/controller/common/user_controller.g.dart +++ b/lib/app/controller/common/user_controller.g.dart @@ -207,6 +207,26 @@ mixin _$UserController on UserControllerBase, Store { .run(() => super.removeCurrentCourse(code)); } + late final _$deleteAllCurrentCoursesAsyncAction = AsyncAction( + 'UserControllerBase.deleteAllCurrentCourses', + context: context); + + @override + Future deleteAllCurrentCourses() { + return _$deleteAllCurrentCoursesAsyncAction + .run(() => super.deleteAllCurrentCourses()); + } + + late final _$pruneCurrentCoursesWithCatalogAsyncAction = AsyncAction( + 'UserControllerBase.pruneCurrentCoursesWithCatalog', + context: context); + + @override + Future pruneCurrentCoursesWithCatalog() { + return _$pruneCurrentCoursesWithCatalogAsyncAction + .run(() => super.pruneCurrentCoursesWithCatalog()); + } + late final _$loadInitialCoursesAsyncAction = AsyncAction('UserControllerBase.loadInitialCourses', context: context); diff --git a/lib/app/controller/edit_page_controller.dart b/lib/app/controller/edit_page_controller.dart index 6d99bf8..8c6456c 100644 --- a/lib/app/controller/edit_page_controller.dart +++ b/lib/app/controller/edit_page_controller.dart @@ -1,4 +1,6 @@ +import 'package:dev_medias_front_flutter/app/controller/common/courses_controller.dart'; import 'package:dev_medias_front_flutter/app/controller/grade_controller.dart'; +import 'package:dev_medias_front_flutter/app/model/course.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; part 'edit_page_controller.g.dart'; @@ -29,6 +31,9 @@ abstract class EditPageControllerBase with Store { @observable bool targetCalcError = false; + @observable + bool finalScoreCalcError = false; + @observable TextEditingController finalScoreController = TextEditingController(text: ""); @@ -82,34 +87,120 @@ abstract class EditPageControllerBase with Store { targetCalcError = value; } + @action + void setFinalScoreCalcError(bool value) { + finalScoreCalcError = value; + } + + bool _needsTargetFill(String gradeName) { + return gradeControllers[gradeName]!.text.isEmpty || + gradeTypes[gradeName] != 'normal'; + } + + int _filledExamsCount(CourseModel course) { + var count = 0; + for (final exam in course.exams ?? []) { + if (!_needsTargetFill(exam.name as String)) { + count++; + } + } + return count; + } + + int _filledAssignmentsCount(CourseModel course) { + var count = 0; + for (final assignment in course.assignments ?? []) { + if (!_needsTargetFill(assignment.name as String)) { + count++; + } + } + return count; + } + // Desenha as notas meta na tela @action - void renderTargetGrades(Map grades) { + bool renderTargetGrades(Map grades) { gradeRendered = false; - if (grades.containsKey("erro")) { + targetCalcError = false; + + if (grades.containsKey('erro') || + grades['notas'] == null || + grades['notas']['provas'] == null || + grades['notas']['trabalhos'] == null) { targetCalcError = true; gradeRendered = true; targetCalcInProgress = false; - return; + return false; } - final allGrades = grades["notas"]["provas"] + grades["notas"]["trabalhos"]; - int index = 0; - // Define os valores das notas restantes como as metas recebidas - editController.grades.forEach((key, value) { - if (editController.gradeControllers[key]!.text.isEmpty || editController.gradeTypes[key] != "normal") { - editController.grades[key] = allGrades[index]["valor"]; - editController.gradeControllers[key]!.text = "${allGrades[index]["valor"]}"; - editController.gradeTypes[key] = "targetcalc"; - index++; - } - } + + final course = coursesController.allCourses?[courseCode] as CourseModel?; + if (course == null) { + targetCalcError = true; + gradeRendered = true; + targetCalcInProgress = false; + return false; + } + + final provasMeta = List>.from( + (grades['notas']['provas'] as List).map( + (item) => Map.from(item as Map), + ), ); - // Faz o mesmo para a nota final - editController.finalScoreController.text = "$targetGrade"; - editController.finalScoreGrade = targetGrade; - editController.finalScoreType = "targetcalc"; + final trabalhosMeta = List>.from( + (grades['notas']['trabalhos'] as List).map( + (item) => Map.from(item as Map), + ), + ); + + var provaQueroIndex = 0; + var trabalhoQueroIndex = 0; + final provasTenho = _filledExamsCount(course); + final trabalhosTenho = _filledAssignmentsCount(course); + + for (final exam in course.exams ?? []) { + final name = exam.name as String; + if (!_needsTargetFill(name)) { + continue; + } + final responseIndex = provasTenho + provaQueroIndex; + if (responseIndex >= provasMeta.length) { + targetCalcError = true; + gradeRendered = true; + targetCalcInProgress = false; + return false; + } + final valor = (provasMeta[responseIndex]['valor'] as num).toDouble(); + grades[name] = valor; + gradeControllers[name]!.text = '$valor'; + gradeTypes[name] = 'targetcalc'; + provaQueroIndex++; + } + + for (final assignment in course.assignments ?? []) { + final name = assignment.name as String; + if (!_needsTargetFill(name)) { + continue; + } + final responseIndex = trabalhosTenho + trabalhoQueroIndex; + if (responseIndex >= trabalhosMeta.length) { + targetCalcError = true; + gradeRendered = true; + targetCalcInProgress = false; + return false; + } + final valor = (trabalhosMeta[responseIndex]['valor'] as num).toDouble(); + grades[name] = valor; + gradeControllers[name]!.text = '$valor'; + gradeTypes[name] = 'targetcalc'; + trabalhoQueroIndex++; + } + + finalScoreController.text = '$targetGrade'; + finalScoreGrade = targetGrade; + finalScoreType = 'targetcalc'; gradeRendered = true; targetCalcInProgress = false; + return true; } @action @@ -157,26 +248,25 @@ abstract class EditPageControllerBase with Store { // Calcula as metas de nota para cada avaliação de acordo com a meta inserida com o usuário @action - Future calcTargetGrade( + Future calcTargetGrade( Map grades, Map weights) async { + setTargetCalcError(false); - // Se o tipo de uma das notas for diferente de normal, ele é considerado como zero - final filteredGrades = grades; + final filteredGrades = Map.from(grades); filteredGrades.forEach((key, value) { if (gradeTypes[key] != "normal") { filteredGrades[key] = null; } }); - // Obtém as notas meta e devolve um mapa - Map targetGrades = {}; - try { - targetGrades = await gradeController.getTargetGrades(filteredGrades, weights, targetGrade.toDouble(), courseCode); - } catch (e) { - targetGrades = {"erro": "Erro ao calcular as notas meta."}; - } - // Pega o mapa e utiliza ele para alterar os valores da tela - renderTargetGrades(targetGrades); + final targetGrades = await gradeController.getTargetGrades( + filteredGrades, + weights, + targetGrade.toDouble(), + courseCode, + ); + + return renderTargetGrades(targetGrades); } @action @@ -199,20 +289,25 @@ abstract class EditPageControllerBase with Store { // Calcula a nota final de acordo com as notas inseridas pelo usuário @action - Future calcFinalScore(Map weights, Map grades) async { + Future calcFinalScore( + Map weights, Map grades) async { + setFinalScoreCalcError(false); + final result = + await gradeController.getFinalScore(grades, weights, courseCode); - try { - Map result = await gradeController.getFinalScore(grades, weights, courseCode); - finalScoreGrade = result["media"]; - } catch (e) { + if (result.containsKey('erro')) { + finalScoreCalcError = true; finalScoreGrade = null; + finalScoreController.text = ''; + return false; } - // Atualiza o resultado final na tela - finalScoreController.text = finalScoreGrade != null ? "$finalScoreGrade" : ""; - finalScoreGrade = finalScoreGrade; - finalScoreType = "normal"; + finalScoreGrade = (result['media'] as num?)?.toDouble(); + finalScoreController.text = + finalScoreGrade != null ? '$finalScoreGrade' : ''; + finalScoreType = 'normal'; + return true; } // Formata as notas para serem enviadas ao salvamento local no Hive diff --git a/lib/app/controller/edit_page_controller.g.dart b/lib/app/controller/edit_page_controller.g.dart index 32a8279..66a04e0 100644 --- a/lib/app/controller/edit_page_controller.g.dart +++ b/lib/app/controller/edit_page_controller.g.dart @@ -122,6 +122,22 @@ mixin _$EditPageController on EditPageControllerBase, Store { }); } + late final _$finalScoreCalcErrorAtom = Atom( + name: 'EditPageControllerBase.finalScoreCalcError', context: context); + + @override + bool get finalScoreCalcError { + _$finalScoreCalcErrorAtom.reportRead(); + return super.finalScoreCalcError; + } + + @override + set finalScoreCalcError(bool value) { + _$finalScoreCalcErrorAtom.reportWrite(value, super.finalScoreCalcError, () { + super.finalScoreCalcError = value; + }); + } + late final _$finalScoreControllerAtom = Atom( name: 'EditPageControllerBase.finalScoreController', context: context); @@ -191,7 +207,7 @@ mixin _$EditPageController on EditPageControllerBase, Store { AsyncAction('EditPageControllerBase.calcTargetGrade', context: context); @override - Future calcTargetGrade( + Future calcTargetGrade( Map grades, Map weights) { return _$calcTargetGradeAsyncAction .run(() => super.calcTargetGrade(grades, weights)); @@ -201,7 +217,7 @@ mixin _$EditPageController on EditPageControllerBase, Store { AsyncAction('EditPageControllerBase.calcFinalScore', context: context); @override - Future calcFinalScore( + Future calcFinalScore( Map weights, Map grades) { return _$calcFinalScoreAsyncAction .run(() => super.calcFinalScore(weights, grades)); @@ -299,7 +315,18 @@ mixin _$EditPageController on EditPageControllerBase, Store { } @override - void renderTargetGrades(Map grades) { + void setFinalScoreCalcError(bool value) { + final _$actionInfo = _$EditPageControllerBaseActionController.startAction( + name: 'EditPageControllerBase.setFinalScoreCalcError'); + try { + return super.setFinalScoreCalcError(value); + } finally { + _$EditPageControllerBaseActionController.endAction(_$actionInfo); + } + } + + @override + bool renderTargetGrades(Map grades) { final _$actionInfo = _$EditPageControllerBaseActionController.startAction( name: 'EditPageControllerBase.renderTargetGrades'); try { @@ -374,6 +401,7 @@ courseCode: ${courseCode}, finalScoreType: ${finalScoreType}, finalScoreGrade: ${finalScoreGrade}, targetCalcError: ${targetCalcError}, +finalScoreCalcError: ${finalScoreCalcError}, finalScoreController: ${finalScoreController}, gradeTypes: ${gradeTypes}, grades: ${grades}, diff --git a/lib/app/controller/grade_controller.dart b/lib/app/controller/grade_controller.dart index ffb36d0..727b185 100644 --- a/lib/app/controller/grade_controller.dart +++ b/lib/app/controller/grade_controller.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'package:dev_medias_front_flutter/app/controller/common/courses_controller.dart'; +import 'package:dev_medias_front_flutter/app/model/course.dart'; +import 'package:dev_medias_front_flutter/app/service/grade_api_payload.dart'; import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hive_flutter/adapters.dart'; @@ -13,6 +15,14 @@ abstract class GradeControllerBase with Store { final dio = Dio(); + CourseModel _course(String courseCode) { + final course = coursesController.allCourses?[courseCode]; + if (course == null) { + throw Exception('Matéria $courseCode não encontrada'); + } + return course as CourseModel; + } + @action Future getGrades(String code) async { await Hive.initFlutter(); @@ -32,81 +42,123 @@ abstract class GradeControllerBase with Store { box.put('grades', newGrades); } - @action - Future> getTargetGrades( - Map grades, - Map weights, - double targetGrade, - String courseCode) async { - Map gradeMap = { - "provas_que_tenho": [], - "trabalhos_que_tenho": [], - "provas_que_quero": [], - "trabalhos_que_quero": [], - "media_desejada": 0, - "peso_prova": coursesController.allCourses![courseCode].examWeight / 100, - "peso_trabalho": - coursesController.allCourses![courseCode].assignmentWeight / 100 + Map _buildTargetGradePayload({ + required Map grades, + required Map weights, + required double targetGrade, + required String courseCode, + }) { + final course = _course(courseCode); + final gradeMap = { + 'provas_que_tenho': >[], + 'trabalhos_que_tenho': >[], + 'provas_que_quero': >[], + 'trabalhos_que_quero': >[], + 'media_desejada': targetGrade, + 'peso_prova': normalizeCourseComponentWeight(course.examWeight), + 'peso_trabalho': normalizeCourseComponentWeight(course.assignmentWeight), }; - for (var item in grades.entries) { - if (item.key[0] == "P") { - item.value == null - ? gradeMap["provas_que_quero"].add({"peso": weights[item.key]}) - : gradeMap["provas_que_tenho"] - .add({"valor": item.value, "peso": weights[item.key]}); - } else { - item.value == null - ? gradeMap["trabalhos_que_quero"].add({"peso": weights[item.key]}) - : gradeMap["trabalhos_que_tenho"] - .add({"valor": item.value, "peso": weights[item.key]}); - } - } - gradeMap["media_desejada"] = targetGrade; - try { - final response = - await dio.post(dotenv.env['API_GRADE_OPTMIZER']!, data: gradeMap); - if (response.statusCode == 200) { - return response.data; + + for (final item in grades.entries) { + final peso = normalizeGradeWeight(weights[item.key]); + final isExam = isExamGrade(item.key, course); + if (item.value == null) { + final bucket = + isExam ? 'provas_que_quero' : 'trabalhos_que_quero'; + (gradeMap[bucket] as List).add({'peso': peso}); } else { - throw Exception('Erro na solicitação POST'); + final bucket = + isExam ? 'provas_que_tenho' : 'trabalhos_que_tenho'; + (gradeMap[bucket] as List).add({ + 'valor': (item.value as num).toDouble(), + 'peso': peso, + }); } - } catch (e) { - throw Exception('Erro de rede: $e'); } + + return gradeMap; } - @action - Future> getFinalScore(Map grades, - Map weights, String courseCode) async { - Map gradeMap = { - "provas_que_tenho": [], - "trabalhos_que_tenho": [], - "peso_prova": coursesController.allCourses![courseCode].examWeight / 100, - "peso_trabalho": - coursesController.allCourses![courseCode].assignmentWeight / 100 + Map _buildFinalScorePayload({ + required Map grades, + required Map weights, + required String courseCode, + }) { + final course = _course(courseCode); + final gradeMap = { + 'provas_que_tenho': >[], + 'trabalhos_que_tenho': >[], + 'peso_prova': normalizeCourseComponentWeight(course.examWeight), + 'peso_trabalho': normalizeCourseComponentWeight(course.assignmentWeight), }; - for (var item in grades.entries) { - if (item.key[0] == "P") { - gradeMap["provas_que_tenho"] - .add({"valor": item.value ?? 0.0, "peso": weights[item.key]}); - } else { - gradeMap["trabalhos_que_tenho"] - .add({"valor": item.value ?? 0.0, "peso": weights[item.key]}); - } + for (final item in grades.entries) { + final peso = normalizeGradeWeight(weights[item.key]); + final bucket = isExamGrade(item.key, course) + ? 'provas_que_tenho' + : 'trabalhos_que_tenho'; + (gradeMap[bucket] as List).add({ + 'valor': item.value == null ? 0.0 : (item.value as num).toDouble(), + 'peso': peso, + }); } + + return gradeMap; + } + + Future> _postJson( + String url, + Map body, + ) async { try { - final response = - await dio.post(dotenv.env['API_FINAL_SCORE_URL']!, data: gradeMap); + final response = await dio.post(url, data: body); if (response.statusCode == 200) { - return response.data; - } else { - throw Exception('Erro na solicitação POST'); + if (response.data is Map) { + return Map.from(response.data as Map); + } + final message = errorMessageFromApiBody(response.data); + return {'erro': message ?? 'Resposta inválida da API'}; } + return {'erro': 'Erro na solicitação POST (${response.statusCode})'}; + } on DioException catch (e) { + final message = errorMessageFromApiBody(e.response?.data) ?? + e.message ?? + 'Erro de rede'; + return {'erro': message}; } catch (e) { - throw Exception('Erro de rede: $e'); + return {'erro': 'Erro de rede: $e'}; } } + + @action + Future> getTargetGrades( + Map grades, + Map weights, + double targetGrade, + String courseCode, + ) async { + final gradeMap = _buildTargetGradePayload( + grades: grades, + weights: weights, + targetGrade: targetGrade, + courseCode: courseCode, + ); + return _postJson(dotenv.env['API_GENETIC_ALGORITHM']!, gradeMap); + } + + @action + Future> getFinalScore( + Map grades, + Map weights, + String courseCode, + ) async { + final gradeMap = _buildFinalScorePayload( + grades: grades, + weights: weights, + courseCode: courseCode, + ); + return _postJson(dotenv.env['API_FINAL_SCORE_URL']!, gradeMap); + } } GradeController gradeController = GradeController(); diff --git a/lib/app/model/notice.dart b/lib/app/model/notice.dart new file mode 100644 index 0000000..ddee8ca --- /dev/null +++ b/lib/app/model/notice.dart @@ -0,0 +1,25 @@ +class NoticeModel { + NoticeModel({ + required this.title, + required this.body, + }); + + final String title; + final String body; + + factory NoticeModel.fromJson(Map json) { + final title = (json['title'] ?? + json['subject'] ?? + json['heading'] ?? + 'Aviso') + .toString(); + final body = (json['description'] ?? + json['body'] ?? + json['message'] ?? + json['text'] ?? + json['content'] ?? + '') + .toString(); + return NoticeModel(title: title, body: body); + } +} diff --git a/lib/app/service/course_service.dart b/lib/app/service/course_service.dart index 98adcec..a87ca76 100644 --- a/lib/app/service/course_service.dart +++ b/lib/app/service/course_service.dart @@ -11,30 +11,15 @@ class CourseService { try { final response = await dio.get(dotenv.env['API_SUBJECTS']!); if (response.statusCode == 200) { - Map data = response.data; - Map aux = {}; - - data.forEach((code, course) { - // converte as provas no formato GradeModel - final prevExams = course["exams"]; - final newExamList = []; - for (Map exam in prevExams) { - newExamList.add(GradeModel.fromJson(exam)); - } - course["exams"] = newExamList; - // converte os trabalhos no formato GradeModel - final prevAssignments = course["assignments"]; - final newAssignmentList = []; - for (Map assignment in prevAssignments) { - newAssignmentList.add(GradeModel.fromJson(assignment)); - } - course["assignments"] = newAssignmentList; - course = CourseModel.fromJson(course); - aux[code] = course; - }); - - Map courses = aux; - return courses; + final subjects = _parseSubjectsPayload(response.data); + final Map aux = {}; + + for (final raw in subjects) { + final course = _toCourseModel(raw); + aux[course.code] = course; + } + + return aux; } else { throw Exception('Erro na solicitação GET'); } @@ -42,6 +27,55 @@ class CourseService { throw Exception('Erro de rede: $e'); } } + + /// API nova: lista JSON; CDN antigo: mapa indexado por código. + List> _parseSubjectsPayload(dynamic data) { + if (data is List) { + return data + .map((item) => Map.from(item as Map)) + .toList(); + } + if (data is Map) { + return data.entries.map((entry) { + final subject = Map.from(entry.value as Map); + subject.putIfAbsent('code', () => entry.key.toString()); + return subject; + }).toList(); + } + throw Exception('Formato de disciplinas não reconhecido'); + } + + Map _normalizeSubjectFields(Map raw) { + return { + 'code': raw['code'], + 'name': raw['name'], + 'period': raw['period'], + 'examWeight': raw['examWeight'] ?? raw['exam_weight'], + 'assignmentWeight': raw['assignmentWeight'] ?? raw['assignment_weight'], + 'exams': raw['exams'] ?? [], + 'assignments': raw['assignments'] ?? [], + 'courses': raw['courses'], + }; + } + + CourseModel _toCourseModel(Map raw) { + final course = _normalizeSubjectFields(raw); + + final exams = []; + for (final exam in course['exams'] as List) { + exams.add(GradeModel.fromJson(Map.from(exam as Map))); + } + course['exams'] = exams; + + final assignments = []; + for (final assignment in course['assignments'] as List) { + assignments.add( + GradeModel.fromJson(Map.from(assignment as Map))); + } + course['assignments'] = assignments; + + return CourseModel.fromJson(course); + } } CourseService courseService = CourseService(); diff --git a/lib/app/service/grade_api_payload.dart b/lib/app/service/grade_api_payload.dart new file mode 100644 index 0000000..504abb5 --- /dev/null +++ b/lib/app/service/grade_api_payload.dart @@ -0,0 +1,38 @@ +import 'package:dev_medias_front_flutter/app/model/course.dart'; + +/// Converte peso da matéria (0.6 na API nova ou 60 no JSON antigo) para fração 0–1. +double normalizeCourseComponentWeight(dynamic weight) { + final value = (weight as num).toDouble(); + return value > 1 ? value / 100 : value; +} + +double normalizeGradeWeight(dynamic weight) => (weight as num).toDouble(); + +Set examNamesFor(CourseModel course) { + return (course.exams ?? []) + .map((exam) => exam.name as String) + .toSet(); +} + +Set assignmentNamesFor(CourseModel course) { + return (course.assignments ?? []) + .map((assignment) => assignment.name as String) + .toSet(); +} + +bool isExamGrade(String gradeName, CourseModel course) { + return examNamesFor(course).contains(gradeName); +} + +String? errorMessageFromApiBody(dynamic body) { + if (body is Map && body['erro'] != null) { + return body['erro'].toString(); + } + if (body is Map && body['message'] != null) { + return body['message'].toString(); + } + if (body is String) { + return body; + } + return null; +} diff --git a/lib/app/service/graduation_service.dart b/lib/app/service/graduation_service.dart index 55a7d6b..5cee58e 100644 --- a/lib/app/service/graduation_service.dart +++ b/lib/app/service/graduation_service.dart @@ -1,6 +1,35 @@ import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +/// API nova: lista `[{ "código": "CIC", "nome": "..." }]`. +/// CDN antigo: mapa `{ "CIC": "Ciência da Computação", ... }`. +Map parseGraduationsPayload(dynamic data) { + if (data is List) { + final result = {}; + for (final item in data) { + final course = Map.from(item as Map); + final code = course['código'] ?? course['codigo'] ?? course['code']; + final name = course['nome'] ?? course['name']; + if (code != null && name != null) { + result[code.toString()] = name.toString(); + } + } + if (result.isEmpty) { + throw Exception('Lista de cursos vazia'); + } + return result; + } + + if (data is Map) { + if (data.containsKey('message')) { + throw Exception(data['message'].toString()); + } + return Map.from(data); + } + + throw Exception('Formato de cursos não reconhecido'); +} + class GraduationService { final Dio dio = Dio(); @@ -8,8 +37,7 @@ class GraduationService { try { final response = await dio.get(dotenv.env['API_COURSES']!); if (response.statusCode == 200) { - Map data = response.data; - return data; + return parseGraduationsPayload(response.data); } else { throw Exception('Erro na solicitação GET'); } diff --git a/lib/app/service/notification_service.dart b/lib/app/service/notification_service.dart new file mode 100644 index 0000000..4167510 --- /dev/null +++ b/lib/app/service/notification_service.dart @@ -0,0 +1,50 @@ +import 'package:dev_medias_front_flutter/app/model/notice.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class NotificationService { + final Dio dio = Dio(); + + /// GET [API_NOTIFICATIONS]. Sem URL no `.env`, retorna lista vazia (sem requisição). + Future> getNotifications() async { + final url = dotenv.env['API_NOTIFICATIONS']; + if (url == null || url.trim().isEmpty) { + return []; + } + try { + final response = await dio.get(url); + if (response.statusCode != 200) { + return []; + } + return _parseList(response.data); + } catch (_) { + return []; + } + } + + List _parseList(dynamic data) { + if (data is List) { + return data + .whereType() + .map((e) => NoticeModel.fromJson(Map.from(e))) + .where((n) => n.body.trim().isNotEmpty || n.title.trim().isNotEmpty) + .toList(); + } + if (data is Map) { + final nested = data['notifications'] ?? data['data'] ?? data['items']; + if (nested is List) { + return _parseList(nested); + } + try { + final single = NoticeModel.fromJson(Map.from(data)); + if (single.body.trim().isEmpty && single.title == 'Aviso') { + return []; + } + return [single]; + } catch (_) { + return []; + } + } + return []; + } +} diff --git a/lib/app/service/support_service.dart b/lib/app/service/support_service.dart index f806ce8..2bb4d2b 100644 --- a/lib/app/service/support_service.dart +++ b/lib/app/service/support_service.dart @@ -1,20 +1,29 @@ +import 'package:dev_medias_front_flutter/app/service/grade_api_payload.dart'; import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; class SupportService { final Dio dio = Dio(); - Future postMessage(Map message) async { + Future> postMessage(Map message) async { try { final response = await dio.post(dotenv.env['API_SUPPORT_URL']!, data: message); if (response.statusCode == 200) { - return message; - } else { - throw Exception('Erro na solicitação POST'); + return {'success': true}; } + return { + 'success': false, + 'error': errorMessageFromApiBody(response.data) ?? + 'Erro na solicitação POST (${response.statusCode})', + }; + } on DioException catch (e) { + final error = errorMessageFromApiBody(e.response?.data) ?? + e.message ?? + 'Erro de rede'; + return {'success': false, 'error': error}; } catch (e) { - throw Exception('Erro de rede: $e'); + return {'success': false, 'error': 'Erro de rede: $e'}; } } } diff --git a/lib/app/service/user_service.dart b/lib/app/service/user_service.dart index a93e412..7271398 100644 --- a/lib/app/service/user_service.dart +++ b/lib/app/service/user_service.dart @@ -85,4 +85,12 @@ class UserService { userBox.delete('currentCourses'); userBox.delete('grades'); } + + Future>> replaceCurrentCourses( + List courseList) async { + await Hive.initFlutter(); + final userBox = await Hive.openBox('user'); + userBox.put('currentCourses', courseList); + return {'currentCourses': courseList}; + } } diff --git a/lib/app/view/edit_page_view.dart b/lib/app/view/edit_page_view.dart index d3de5e2..1cb9c38 100644 --- a/lib/app/view/edit_page_view.dart +++ b/lib/app/view/edit_page_view.dart @@ -269,8 +269,10 @@ class _EditPageState extends State { MainAxisAlignment .spaceBetween, children: [ - widget.course.assignments! - .isEmpty + ((widget.course.assignments! + .isNotEmpty) && + (widget.course.exams! + .isEmpty)) ? IconButton( icon: const Icon( LucideIcons.bookOpen, @@ -402,71 +404,119 @@ class _EditPageState extends State { ? () {} : () { FocusScope.of(context).unfocus(); + editController.setFinalScoreCalcError(false); showDialog( context: context, - builder: (context) { + builder: (dialogContext) { return Observer( builder: (_) => AlertDialog( content: SizedBox( height: 250, - child: Column( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment - .start, - mainAxisAlignment: - MainAxisAlignment - .spaceAround, - children: [ - const Text( - "Deseja calcular suas notas?", - style: TextStyle( - fontWeight: - FontWeight - .bold, - fontSize: 16), + child: editController + .targetCalcInProgress + ? const Center( + child: SizedBox( + height: 50, + width: 50, + child: + CircularProgressIndicator( + color: AppColors.red, ), - RichText( - text: - const TextSpan( + ), + ) + : editController + .finalScoreCalcError + ? const Center( + child: Text( + "Erro ao calcular a média :(", style: TextStyle( - color: - AppColors - .black, - fontFamily: - 'Poppins', - fontSize: 16), - children: [ - TextSpan( - text: - "É importante dizer que as notas ", - ), - TextSpan( - text: - "calculadas por meta ", - style: TextStyle( - color: AppColors - .red, - fontWeight: - FontWeight - .bold), - ), - TextSpan( - text: - "serão contadas como 0 para a média final.", - ), - ], + color: + AppColors.red, + fontSize: 16, + ), ), + ) + : Column( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + mainAxisAlignment: + MainAxisAlignment + .spaceAround, + children: [ + const Text( + "Deseja calcular suas notas?", + style: TextStyle( + fontWeight: + FontWeight.bold, + fontSize: 16), + ), + RichText( + text: + const TextSpan( + style: TextStyle( + color: + AppColors.black, + fontFamily: + 'Poppins', + fontSize: 16), + children: [ + TextSpan( + text: + "É importante dizer que as notas ", + ), + TextSpan( + text: + "calculadas por meta ", + style: TextStyle( + color: AppColors.red, + fontWeight: FontWeight.bold), + ), + TextSpan( + text: + "serão contadas como 0 para a média final.", + ), + ], + ), + ), + ]), + ], ), - ]), - ], - ), ), actions: [ editController - .targetCalcInProgress - ? Container() + .targetCalcInProgress || + editController + .finalScoreCalcError + ? editController + .finalScoreCalcError + ? TextButton( + style: TextButton + .styleFrom( + backgroundColor: + AppColors.red, + foregroundColor: + AppColors.white, + shape: RoundedRectangleBorder( + borderRadius: + Round.primary), + minimumSize: + const Size + .fromHeight( + 50), + ), + onPressed: () { + Navigator.pop( + dialogContext); + editController + .setFinalScoreCalcError( + false); + }, + child: const Text( + "Fechar")) + : Container() : TextButton( style: TextButton.styleFrom( backgroundColor: @@ -488,40 +538,48 @@ class _EditPageState extends State { horizontal: 7)), onPressed: () async { + editController + .setFinalScoreCalcError( + false); editController .setTargetCalcProgress( true); - Map - weights = {}; - for (var grade in widget - .course - .exams! + - widget.course - .assignments!) { - weights[grade - .name] = + final weights = + {}; + for (var grade + in widget.course + .exams! + + widget.course + .assignments!) { + weights[grade.name] = grade.weight; } - Navigator.pop( - context); try { - await editController - .calcFinalScore( - weights, - editController - .grades); - final gradesToSave = + final success = + await editController + .calcFinalScore( + weights, + editController + .grades, + ); + if (success) { + final gradesToSave = + editController + .formatGradesForSaving(); + await gradeController + .insertGrades( editController - .formatGradesForSaving(); - gradeController - .insertGrades( - editController - .getCourseCode(), - gradesToSave); - editController - .setTargetCalcProgress( - false); - } catch (e) { + .getCourseCode(), + gradesToSave, + ); + if (dialogContext + .mounted) { + Navigator.pop( + dialogContext); + } + } + } finally { editController .setTargetCalcProgress( false); @@ -538,7 +596,7 @@ class _EditPageState extends State { backgroundColor: widget.course.assignments!.isEmpty && widget.course.exams!.isEmpty - ? AppColors.red.withOpacity(0.5) + ? AppColors.red.withValues(alpha: 0.5) : AppColors.red, shape: RoundedRectangleBorder( borderRadius: Round.primary), @@ -556,7 +614,7 @@ class _EditPageState extends State { color: widget.course.assignments! .isEmpty && widget.course.exams!.isEmpty - ? AppColors.white.withOpacity(0.5) + ? AppColors.white.withValues(alpha: 0.5) : AppColors.white, fontSize: 16.sp), ), @@ -575,9 +633,10 @@ class _EditPageState extends State { ? () {} : () { FocusScope.of(context).unfocus(); + editController.setTargetCalcError(false); showDialog( context: context, - builder: (context) { + builder: (dialogContext) { final targetController = TextEditingController(); final ValueNotifier @@ -590,97 +649,124 @@ class _EditPageState extends State { }); return Observer( builder: (_) => AlertDialog( - content: SizedBox( - height: 275, - child: editController - .targetCalcInProgress - ? const Center( - child: SizedBox( - height: 50, - width: 50, - child: - CircularProgressIndicator( - color: AppColors - .red, + content: SingleChildScrollView( + child: SizedBox( + width: double.maxFinite, + child: editController + .targetCalcInProgress + ? const Padding( + padding: EdgeInsets + .symmetric( + vertical: + 24), + child: Center( + child: SizedBox( + height: 50, + width: 50, + child: + CircularProgressIndicator( + color: AppColors + .red, + ), + ), ), - ), - ) - : editController - .targetCalcError - ? const Center( - child: Text( - "Erro ao calcular as notas :(", - style: TextStyle( - color: - AppColors - .red, - fontSize: - 16), - )) - : Column( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment - .start, - mainAxisAlignment: - MainAxisAlignment - .spaceAround, - children: [ - const Text( - "Como funciona?", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16), - ), - const Text( - "1. Digite a nota que você deseja alcançar na matéria.", - style: TextStyle( - color: AppColors.black, - fontFamily: 'Poppins', - fontSize: 16), - ), - const Text( - "2. Calcularemos as notas necessárias para alcançar essa meta.", - style: TextStyle( - color: AppColors.black, - fontFamily: 'Poppins', - fontSize: 16), - ), - RichText( - text: - const TextSpan( + ) + : editController + .targetCalcError + ? const Padding( + padding: EdgeInsets + .symmetric( + vertical: + 16), + child: Text( + "Erro ao calcular as notas :(", + style: + TextStyle( + color: AppColors + .red, + fontSize: + 16, + ), + ), + ) + : Column( + mainAxisSize: + MainAxisSize + .min, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + const Text( + "Como funciona?", + style: TextStyle( + fontWeight: + FontWeight.bold, + fontSize: 16), + ), + const SizedBox( + height: 8), + const Text( + "1. Digite a nota que você deseja alcançar na matéria.", + style: TextStyle( + color: AppColors + .black, + fontFamily: + 'Poppins', + fontSize: 16), + ), + const SizedBox( + height: 4), + const Text( + "2. Calcularemos as notas necessárias para alcançar essa meta.", + style: TextStyle( + color: AppColors + .black, + fontFamily: + 'Poppins', + fontSize: 16), + ), + const SizedBox( + height: 4), + RichText( + text: + const TextSpan( + style: TextStyle( + color: AppColors + .black, + fontFamily: + 'Poppins', + fontSize: 16), + children: [ + TextSpan( + text: + "3. As notas calculadas serão exibidas em ", + ), + TextSpan( + text: + "vermelho.", style: TextStyle( - color: AppColors.black, - fontFamily: 'Poppins', - fontSize: 16), - children: [ - TextSpan( - text: "3. As notas calculadas serão exibidas em ", - ), - TextSpan( - text: "vermelho.", - style: TextStyle(color: AppColors.red, fontWeight: FontWeight.bold), - ), - ], + color: AppColors.red, + fontWeight: FontWeight.bold), ), - ), - ]), - Expanded( - child: - Container()), - GradeInput( - changes: - false, - name: - "Sua meta", - controller: - targetController, - labelled: - true, - ), - ], - ), + ], + ), + ), + const SizedBox( + height: 16), + GradeInput( + changes: + false, + name: + "Sua meta", + controller: + targetController, + labelled: + true, + ), + ], + ), + ), ), actions: [ editController @@ -708,7 +794,7 @@ class _EditPageState extends State { 7)), onPressed: () { Navigator.pop( - context); + dialogContext); editController .setTargetCalcError( false); @@ -726,7 +812,7 @@ class _EditPageState extends State { bool>( valueListenable: isButtonDisabled, - builder: (context, + builder: (_, isDisabled, child) => Observer( @@ -734,8 +820,8 @@ class _EditPageState extends State { TextButton( style: TextButton.styleFrom( backgroundColor: isDisabled - ? AppColors.red.withOpacity( - 0.5) + ? AppColors.red.withValues( + alpha: 0.5) : AppColors .red, shape: RoundedRectangleBorder( @@ -751,20 +837,24 @@ class _EditPageState extends State { onPressed: isDisabled ? null : () async { - Navigator.pop(context); + editController.setTargetCalcError(false); editController.setTargetCalcProgress(true); editController.setTargetGrade(double.parse(targetController.text)); - Map weights = {}; + final weights = {}; for (var grade in widget.course.exams! + widget.course.assignments!) { weights[grade.name] = grade.weight; } try { - await editController.calcTargetGrade(editController.grades, weights); - final gradesToSave = editController.formatGradesForSaving(); - gradeController.insertGrades(editController.getCourseCode(), gradesToSave); - editController.setTargetCalcProgress(false); - } catch (e) { + final success = await editController.calcTargetGrade(editController.grades, weights); + if (success) { + final gradesToSave = editController.formatGradesForSaving(); + await gradeController.insertGrades(editController.getCourseCode(), gradesToSave); + if (dialogContext.mounted) { + Navigator.pop(dialogContext); + } + } + } finally { editController.setTargetCalcProgress(false); } }, @@ -772,7 +862,7 @@ class _EditPageState extends State { "Confirmar", style: TextStyle( color: isDisabled - ? AppColors.white.withOpacity(0.5) + ? AppColors.white.withValues(alpha: 0.5) : AppColors.white), )), ), @@ -786,7 +876,7 @@ class _EditPageState extends State { backgroundColor: widget.course.assignments!.isEmpty && widget.course.exams!.isEmpty - ? AppColors.red.withOpacity(0.5) + ? AppColors.red.withValues(alpha: 0.5) : AppColors.red, shape: RoundedRectangleBorder( borderRadius: Round.primary), @@ -800,7 +890,7 @@ class _EditPageState extends State { style: TextStyle( color: widget.course.assignments!.isEmpty && widget.course.exams!.isEmpty - ? AppColors.white.withOpacity(0.5) + ? AppColors.white.withValues(alpha: 0.5) : AppColors.white, fontSize: 16.sp), ), diff --git a/lib/app/view/home_page_view.dart b/lib/app/view/home_page_view.dart index 8909020..9de7235 100644 --- a/lib/app/view/home_page_view.dart +++ b/lib/app/view/home_page_view.dart @@ -4,13 +4,16 @@ import 'package:dev_medias_front_flutter/app/controller/common/courses_controlle import 'package:dev_medias_front_flutter/app/controller/edit_page_controller.dart'; import 'package:dev_medias_front_flutter/app/controller/home_page_controller.dart'; import 'package:dev_medias_front_flutter/app/controller/common/user_controller.dart'; +import 'package:dev_medias_front_flutter/app/controller/common/notifications_controller.dart'; import 'package:dev_medias_front_flutter/app/model/course.dart'; import 'package:dev_medias_front_flutter/app/utils/theme/measurements.dart'; import 'package:dev_medias_front_flutter/app/widgets/add_course_navigation_button.dart'; import 'package:dev_medias_front_flutter/app/widgets/current_course_card.dart'; import 'package:dev_medias_front_flutter/app/widgets/common/navigation_top_bar.dart'; import 'package:dev_medias_front_flutter/app/widgets/common/app_drawer.dart'; +import 'package:dev_medias_front_flutter/app/widgets/notice_card.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:dev_medias_front_flutter/app/utils/theme/app_colors.dart'; @@ -41,60 +44,51 @@ class _HomePageState extends State { return AnnotatedRegion( value: const SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.light, // Ícones brancos - statusBarBrightness: Brightness.dark, // Para iOS - systemNavigationBarColor: Colors.white, - systemNavigationBarIconBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.light, + systemNavigationBarContrastEnforced: false, ), child: Scaffold( resizeToAvoidBottomInset: false, backgroundColor: AppColors.background, drawer: const AppDrawer(), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), - child: Center( - child: FractionallySizedBox( - widthFactor: 1, - child: Padding( - padding: const EdgeInsets.only(top: 56.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //Top Barra de Navegação sem botão de voltar - NavigationTopBar( - prevPage: commonController.getPreviousPage, - isHomePage: true, - ), - // Botão Adicionar Matérias - const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: AddCourseNavigationButton(), - ), - // Lista de Matérias - Observer( - builder: (_) => coursesController.loadedCourses - ? userController.currentCourses.isNotEmpty - ? SizedBox( - height: MediaQuery.of(context).size.height * - 0.7 - - 13, - child: Column( - children: [ - Expanded( - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: userController + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NavigationTopBar( + prevPage: commonController.getPreviousPage, + isHomePage: true, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: AddCourseNavigationButton(), + ), + Expanded( + child: Observer( + builder: (_) => coursesController.loadedCourses + ? userController.currentCourses.isNotEmpty + ? ListView.builder( + padding: EdgeInsets.zero, + itemCount: userController .currentCourses.length, itemBuilder: (context, index) { - CourseModel? course = - coursesController.allCourses?[ - userController - .currentCourses[ - index]]; + final courseCode = userController + .currentCourses[index]; + final CourseModel? course = + coursesController + .allCourses?[courseCode]; + if (course == null) { + return const SizedBox.shrink(); + } return FutureBuilder( future: updateFinalScore( - course!.code), + course.code), builder: (context, snapshot) { if (snapshot .connectionState == @@ -247,57 +241,29 @@ class _HomePageState extends State { }, ); }, - ), - ), - ], - ), - ) - : SizedBox( - height: MediaQuery.of(context).size.height * - 0.7, - child: const Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Expanded( - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Center( - child: Text( - "Você não tem matérias cadastradas", - style: TextStyle( - fontSize: 16.0, - fontWeight: - FontWeight.w600, - color: AppColors.white), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ], - ), - ) - : SizedBox( - height: - MediaQuery.of(context).size.height * 0.7, - width: double.maxFinite, - child: const Center( - child: SizedBox( - width: 50, - height: 50, - child: CircularProgressIndicator( - color: AppColors.red, + ) + : const Center( + child: Text( + "Você não tem matérias cadastradas", + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + color: AppColors.white, ), + textAlign: TextAlign.center, ), + ) + : const Center( + child: SizedBox( + width: 50, + height: 50, + child: CircularProgressIndicator( + color: AppColors.red, ), - )), - ], + ), + )), ), - ), + ], ), ), ), @@ -311,14 +277,59 @@ class _HomePageState extends State { if (!hasAcceptedTerms) { _showTermsOfServiceDialog(); + } else { + _scheduleNotificationsDialog(); } } + void _scheduleNotificationsDialog() { + if (noticeDismissedThisAppSession) return; + + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted || noticeDismissedThisAppSession) return; + await _loadAndShowNotificationsIfAny(context); + }); + } + + Future _loadAndShowNotificationsIfAny(BuildContext pageContext) async { + if (noticeDismissedThisAppSession) return; + final notificationsUrl = dotenv.env['API_NOTIFICATIONS']; + final notificationsConfigured = notificationsUrl != null && + notificationsUrl.trim().isNotEmpty; + + if (notificationsConfigured) { + await notificationsController.fetchNotifications(); + } + + if (!pageContext.mounted) return; + + // Sem URL no .env: mostra o aviso estático (até você apontar API_NOTIFICATIONS). + if (!notificationsConfigured) { + showBlockingNoticeDialog(pageContext); + return; + } + + final items = notificationsController.notices; + if (items.isEmpty) return; + final title = items.length == 1 ? items.first.title : 'Avisos'; + final description = items.length == 1 + ? items.first.body + : items + .map((n) => '${n.title}\n\n${n.body}') + .join('\n\n────────────\n\n'); + showBlockingNoticeDialog( + pageContext, + title: title, + description: description, + ); + } + void _showTermsOfServiceDialog() { + final pageContext = context; showDialog( context: context, barrierDismissible: false, - builder: (BuildContext context) { + builder: (BuildContext dialogContext) { return AlertDialog( title: const Text( 'Termos de Serviço', @@ -345,10 +356,16 @@ class _HomePageState extends State { fixedSize: const Size(150, 50), ), onPressed: () async { - SharedPreferences prefs = - await SharedPreferences.getInstance(); + final nav = Navigator.of(dialogContext); + final prefs = await SharedPreferences.getInstance(); + if (!mounted) return; await prefs.setBool('hasAcceptedTerms', true); - Navigator.of(context).pop(); + if (!dialogContext.mounted) return; + nav.pop(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + await _loadAndShowNotificationsIfAny(pageContext); + }); }, child: const Text( "Aceitar", diff --git a/lib/app/view/loading_page_view.dart b/lib/app/view/loading_page_view.dart index d02e6f3..100fadd 100644 --- a/lib/app/view/loading_page_view.dart +++ b/lib/app/view/loading_page_view.dart @@ -26,7 +26,8 @@ class _LoadingPageState extends State { loadingPageController.setLoading(true); final userDataMissing = await userController.checkUserDataExists(); try { - coursesController.setAllCourses(await coursesController.fetchCourses()); + final refreshed = await coursesController.refreshAllSubjects(); + if (!refreshed) throw Exception('Falha ao atualizar disciplinas'); } catch (e) { Navigator.pushReplacementNamed(context, '/disconnected'); } diff --git a/lib/app/widgets/common/app_lifecycle_refresh.dart b/lib/app/widgets/common/app_lifecycle_refresh.dart new file mode 100644 index 0000000..abc8d32 --- /dev/null +++ b/lib/app/widgets/common/app_lifecycle_refresh.dart @@ -0,0 +1,38 @@ +import 'package:dev_medias_front_flutter/app/controller/common/courses_controller.dart'; +import 'package:dev_medias_front_flutter/app/controller/intro_page_controller.dart'; +import 'package:flutter/material.dart'; + +/// Refaz o GET de disciplinas quando o app volta do background. +class AppLifecycleRefresh extends StatefulWidget { + const AppLifecycleRefresh({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _AppLifecycleRefreshState(); +} + +class _AppLifecycleRefreshState extends State + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state != AppLifecycleState.resumed) return; + if (!introPageController.loggedIn) return; + coursesController.refreshAllSubjects(); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/lib/app/widgets/notice_card.dart b/lib/app/widgets/notice_card.dart new file mode 100644 index 0000000..6c02c52 --- /dev/null +++ b/lib/app/widgets/notice_card.dart @@ -0,0 +1,183 @@ +import 'package:dev_medias_front_flutter/app/utils/theme/app_colors.dart'; +import 'package:flutter/material.dart'; + +const String _kDefaultNoticeTitle = 'Aviso'; + +const String _kDefaultNoticeBody = ''' + +Lembre-se de conferir suas notas oficiais no sistema da instituição. Este aplicativo é uma ferramenta de apoio e pode conter erros ou interpretações diferentes do regulamento vigente. + +Se encontrar inconsistências, utilize o canal de suporte para reportar o problema com o máximo de detalhes possível. + +Obrigado por utilizar o DevMédias. Role até o final deste texto para habilitar o botão de confirmação. +'''; + +/// Aviso já fechado nesta execução do app; reinicia ao fechar e reabrir o app. +bool noticeDismissedThisAppSession = false; + +/// Mostra o card de aviso em tela quase cheia; só fecha após rolar até o fim e tocar em ENTENDI. +Future showBlockingNoticeDialog( + BuildContext context, { + String? title, + String? description, +}) async { + if (noticeDismissedThisAppSession) return; + + await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + final size = MediaQuery.sizeOf(dialogContext); + final horizontalInset = 20.0; + final verticalInset = 32.0; + return Dialog( + insetPadding: EdgeInsets.symmetric( + horizontal: horizontalInset, + vertical: verticalInset, + ), + backgroundColor: Colors.transparent, + elevation: 0, + child: SizedBox( + width: size.width - horizontalInset * 2, + height: size.height - verticalInset * 2, + child: NoticeCard( + title: title ?? _kDefaultNoticeTitle, + description: description ?? _kDefaultNoticeBody, + ), + ), + ); + }, + ); +} + +class NoticeCard extends StatefulWidget { + const NoticeCard({ + super.key, + required this.title, + required this.description, + }); + + final String title; + final String description; + + @override + State createState() => _NoticeCardState(); +} + +class _NoticeCardState extends State { + final ScrollController _scrollController = ScrollController(); + bool _reachedBottom = false; + + static const double _scrollEndTolerance = 12; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + WidgetsBinding.instance.addPostFrameCallback((_) => _syncScrollState()); + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() => _syncScrollState(); + + void _syncScrollState() { + if (!mounted) return; + final c = _scrollController; + if (!c.hasClients) return; + final maxExtent = c.position.maxScrollExtent; + final pixels = c.position.pixels; + final atEnd = maxExtent <= _scrollEndTolerance || + pixels >= maxExtent - _scrollEndTolerance; + if (atEnd != _reachedBottom) { + setState(() => _reachedBottom = atEnd); + } + } + + void _onEntendi() { + noticeDismissedThisAppSession = true; + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: AppColors.backgroundLight, + borderRadius: BorderRadius.circular(16), + clipBehavior: Clip.antiAlias, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + widget.title, + style: const TextStyle( + color: AppColors.white, + fontSize: 22, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 16), + Expanded( + child: Scrollbar( + controller: _scrollController, + thumbVisibility: true, + child: SingleChildScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + child: Text( + widget.description.trim(), + style: const TextStyle( + color: AppColors.white, + fontSize: 16, + height: 1.45, + ), + ), + ), + ), + ), + const SizedBox(height: 12), + if (!_reachedBottom) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + 'Role até o final para continuar', + textAlign: TextAlign.center, + style: TextStyle( + color: AppColors.white.withValues(alpha: 0.55), + fontSize: 13, + ), + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.red, + disabledBackgroundColor: AppColors.textFaded, + disabledForegroundColor: AppColors.white.withValues(alpha: 0.5), + foregroundColor: AppColors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric(vertical: 14), + ), + onPressed: _reachedBottom ? _onEntendi : null, + child: const Text( + 'ENTENDI', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/popup_report_error.dart b/lib/app/widgets/popup_report_error.dart index 9baaae8..f390625 100644 --- a/lib/app/widgets/popup_report_error.dart +++ b/lib/app/widgets/popup_report_error.dart @@ -13,6 +13,64 @@ class PopupReportError extends StatefulWidget { class _PopupReportErrorState extends State { final GlobalKey _formKey = GlobalKey(); + bool _isSending = false; + + Future _submitSupport() async { + final email = supportController.emailController.text.trim(); + final message = supportController.messageController.text.trim(); + + if (email.isEmpty || message.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Preencha o e-mail e a descrição antes de enviar.'), + backgroundColor: AppColors.red, + ), + ); + return; + } + + setState(() => _isSending = true); + + try { + final result = await supportController.sendSupport(); + + if (!mounted) return; + + if (result['success'] == true) { + Navigator.of(context).pop(); + await showDialog( + context: context, + builder: (BuildContext context) { + return const PopupErrorSent(); + }, + ); + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + result['error']?.toString() ?? 'Não foi possível enviar a mensagem.', + ), + backgroundColor: AppColors.red, + ), + ); + } catch (e) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro inesperado ao enviar: $e'), + backgroundColor: AppColors.red, + ), + ); + } finally { + if (mounted) { + setState(() => _isSending = false); + } + } + } + @override Widget build(BuildContext context) { return Observer( @@ -143,23 +201,24 @@ class _PopupReportErrorState extends State { ), fixedSize: const Size(150, 50), ), - onPressed: () { - supportController.sendSupport(); - showDialog( - context: context, - builder: (BuildContext context) { - return const PopupErrorSent(); - }, - ); - }, - child: const Text( - "Enviar", - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - color: AppColors.white, - ), - ), + onPressed: _isSending ? null : _submitSupport, + child: _isSending + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.white, + ), + ) + : const Text( + "Enviar", + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + color: AppColors.white, + ), + ), ), ], ), diff --git a/lib/app/widgets/popup_sent_error.dart b/lib/app/widgets/popup_sent_error.dart index 18d04cc..2bd507c 100644 --- a/lib/app/widgets/popup_sent_error.dart +++ b/lib/app/widgets/popup_sent_error.dart @@ -65,8 +65,7 @@ class PopupErrorSent extends StatelessWidget { fixedSize: const Size(150, 50), ), onPressed: () { - // Fecha ambos os popups e retorna à página anterior - Navigator.of(context).pop; + Navigator.of(context).pop(); }, child: const Text( "Concluir", diff --git a/lib/main.dart b/lib/main.dart index 23972bc..409885a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:dev_medias_front_flutter/app/widgets/common/app_lifecycle_refresh.dart'; import 'package:dev_medias_front_flutter/app/view/add_page_view.dart'; import 'package:dev_medias_front_flutter/app/view/edit_page_view.dart'; import 'package:dev_medias_front_flutter/app/view/home_page_view.dart'; @@ -11,7 +12,12 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; Future main() async { + WidgetsFlutterBinding.ensureInitialized(); await dotenv.load(fileName: ".env"); + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); runApp(DevicePreview(enabled: false, builder: (context) => const MyApp())); } @@ -24,7 +30,8 @@ class MyApp extends StatelessWidget { designSize: const Size(375, 812), minTextAdapt: true, splitScreenMode: true, - builder: (context, child) => MaterialApp( + builder: (context, child) => AppLifecycleRefresh( + child: MaterialApp( title: 'DevMédias', theme: ThemeData( useMaterial3: true, @@ -52,6 +59,7 @@ class MyApp extends StatelessWidget { }, '/disconnected': (_) => const NoConnectionPage(), }, + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index d9bf688..ad22679 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,98 +5,82 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "10.0.1" archive: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.9" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "4.0.5" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.0.2" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" - url: "https://pub.dev" - source: hosted - version: "2.4.2" + version: "4.1.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" - url: "https://pub.dev" - source: hosted - version: "2.4.11" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "2.13.1" built_collection: dependency: transitive description: @@ -109,154 +93,154 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.12.5" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" cli_util: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.11.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "3.1.5" device_frame: dependency: transitive description: name: device_frame - sha256: d031a06f5d6f4750009672db98a5aa1536aa4a231713852469ce394779a23d75 + sha256: "7b2ebb2a09d6cc0f086b51bd1412d7be83e0170056a7290349169be41164c86a" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.0" device_preview: dependency: "direct main" description: name: device_preview - sha256: a694acdd3894b4c7d600f4ee413afc4ff917f76026b97ab06575fe886429ef19 + sha256: "88aa1cc73ee9a8ec771b309dcbc4000cc66b5d8456b825980997640ab1195bf5" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.1" dio: dependency: "direct main" description: name: dio - sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c url: "https://pub.dev" source: hosted - version: "5.6.0" + version: "5.9.2" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.2" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -266,26 +250,26 @@ packages: dependency: "direct main" description: name: flutter_dotenv - sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + sha256: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.0.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77" + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.14.1" + version: "0.14.4" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "6.0.0" flutter_localizations: dependency: transitive description: flutter @@ -295,10 +279,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.3.0" flutter_screenutil: dependency: "direct main" description: @@ -311,10 +295,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.2.4" flutter_test: dependency: "direct dev" description: flutter @@ -329,26 +313,18 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "2.4.4" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" + version: "3.1.0" glob: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -377,106 +353,98 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.6.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: transitive description: name: image - sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.8.0" intl: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "1.0.5" json_annotation: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.11.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "6.1.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" lucide_icons: dependency: "direct main" description: @@ -489,10 +457,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -505,34 +473,34 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mobx: dependency: "direct main" description: name: mobx - sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" + sha256: "25ea9a3ee69243d578fb6288721d8eeb18550d0391c32f896d377d994154bff2" url: "https://pub.dev" source: hosted - version: "2.3.3+2" + version: "2.6.0" mobx_codegen: dependency: "direct main" description: name: mobx_codegen - sha256: "8e0d8653a0c720ad933cd8358f6f89f740ce89203657c13f25bea772ef1fff7c" + sha256: "6529bcae074838004b58ad01fbe4966be182a176aacfb723686985986ebac19f" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.7.7" nested: dependency: transitive description: @@ -545,50 +513,50 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" path: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: transitive description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.23" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -617,18 +585,18 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.2" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -641,58 +609,66 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" provider: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5+1" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: @@ -705,18 +681,18 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -729,199 +705,191 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "4.2.2" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.2" - timing: - dependency: transitive - description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" - url: "https://pub.dev" - source: hosted - version: "1.0.1" + version: "0.7.6" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.21" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.2.0" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" web: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" web_socket: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index ae342ab..38f4634 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 3.0.2 +version: 3.0.3 environment: sdk: ">=3.3.4 <4.0.0" @@ -33,19 +33,19 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.6 - mobx: ^2.3.3+2 - mobx_codegen: ^2.6.1 - flutter_mobx: ^2.2.1+1 - flutter_svg: ^2.0.10+1 + cupertino_icons: ^1.0.8 + mobx: ^2.6.0 + mobx_codegen: ^2.7.6 + flutter_mobx: ^2.3.0 + flutter_svg: ^2.2.4 path: ^1.9.0 - shared_preferences: ^2.3.2 - dio: ^5.6.0 + shared_preferences: ^2.5.5 + dio: ^5.9.2 hive_flutter: ^1.1.0 - flutter_dotenv: ^5.1.0 + flutter_dotenv: ^6.0.0 lucide_icons: ^0.257.0 flutter_screenutil: ^5.9.3 - device_preview: ^1.2.0 + device_preview: ^1.3.1 dev_dependencies: flutter_test: @@ -56,9 +56,9 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^4.0.0 - build_runner: ^2.4.9 - flutter_launcher_icons: ^0.14.1 + flutter_lints: ^6.0.0 + build_runner: ^2.4.13 + flutter_launcher_icons: ^0.14.4 flutter_icons: android: true diff --git a/test/grade_api_payload_test.dart b/test/grade_api_payload_test.dart new file mode 100644 index 0000000..3b27712 --- /dev/null +++ b/test/grade_api_payload_test.dart @@ -0,0 +1,23 @@ +import 'package:dev_medias_front_flutter/app/model/course.dart'; +import 'package:dev_medias_front_flutter/app/model/grade.dart'; +import 'package:dev_medias_front_flutter/app/service/grade_api_payload.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('normalizeCourseComponentWeight aceita fração e percentual', () { + expect(normalizeCourseComponentWeight(0.6), 0.6); + expect(normalizeCourseComponentWeight(60), 0.6); + }); + + test('isExamGrade usa nomes da matéria, não a primeira letra', () { + final course = CourseModel( + code: 'CIC401', + exams: [GradeModel(name: 'Primeira Prova Bimestral', weight: 0.4)], + assignments: [GradeModel(name: 'Trabalho 1', weight: 0.5)], + ); + + expect(isExamGrade('Primeira Prova Bimestral', course), isTrue); + expect(isExamGrade('Trabalho 1', course), isFalse); + expect(isExamGrade('Prova P1', course), isFalse); + }); +} diff --git a/test/graduation_service_test.dart b/test/graduation_service_test.dart new file mode 100644 index 0000000..42231f4 --- /dev/null +++ b/test/graduation_service_test.dart @@ -0,0 +1,34 @@ +import 'package:dev_medias_front_flutter/app/service/graduation_service.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('parseGraduationsPayload', () { + test('lista com código/nome vira mapa por sigla', () { + final parsed = parseGraduationsPayload([ + {'código': 'CIC', 'nome': 'Ciência da Computação'}, + {'código': 'EAL', 'nome': 'Engenharia de Alimentos'}, + ]); + + expect(parsed['CIC'], 'Ciência da Computação'); + expect(parsed['EAL'], 'Engenharia de Alimentos'); + }); + + test('aceita codigo sem acento', () { + final parsed = parseGraduationsPayload([ + {'codigo': 'ADM', 'name': 'Administração'}, + ]); + + expect(parsed['ADM'], 'Administração'); + }); + + test('mapa antigo do CDN continua válido', () { + final parsed = parseGraduationsPayload({ + 'CIC': 'Ciência da Computação', + 'ADM': 'Administração', + }); + + expect(parsed['CIC'], 'Ciência da Computação'); + expect(parsed.length, 2); + }); + }); +}