diff --git a/.github/composite/ci-build/action.yaml b/.github/composite/ci-build/action.yaml new file mode 100644 index 000000000..49c42f4d0 --- /dev/null +++ b/.github/composite/ci-build/action.yaml @@ -0,0 +1,58 @@ +name: "CI Build" +description: "Build the application on the CI" +inputs: + target: + description: The target OS {iOS, Android} + required: true + build-target: + description: The build target {ios or apk} + required: true + build-args: + description: The arguments for flutter build + required: false + build-path: + required: true + asset-extension: + required: true + default: zip + asset-content-type: + required: true + default: application/zip + github-api-token: + description: the Github API token + required: true + app-name: + required: true + +runs: + using: "composite" + steps: + - name: Build the application + shell: bash + run: flutter build -v ${{ inputs.build-target }} ${{ inputs.build-args }} --release --dart-define=GH_API_TOKEN=${{ inputs.github-api-token }} + + - name: Set environment + shell: bash + run: | + echo "APP_PATH=${{ github.workspace }}/${{ inputs.app-name }}_${{ inputs.target }}.${{ inputs.asset-extension }}" >> $GITHUB_ENV + echo $APP_PATH + + - name: Rename Android build + if: inputs.target == 'Android' + shell: bash + run: mv app-release.${{ inputs.asset-extension }} $APP_PATH + working-directory: ${{ inputs.build-path }} + + - name: Compress iOS build + if: inputs.target == 'iOS' + shell: bash + run: | + mv Runner.app ${{ inputs.app-name }}.app + ditto -c -k --sequesterRsrc --keepParent ${{ inputs.app-name }}.app $APP_PATH + working-directory: ${{ inputs.build-path }} + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.target }} + path: $APP_PATH \ No newline at end of file diff --git a/.github/composite/flutter-setup/action.yaml b/.github/composite/flutter-setup/action.yaml new file mode 100644 index 000000000..8149bb0b7 --- /dev/null +++ b/.github/composite/flutter-setup/action.yaml @@ -0,0 +1,86 @@ +name: "Flutter setup" +description: "Setup flutter" +inputs: + encrypted-signets-api-cert-password: + required: true + encrypted-google-service-password: + required: true + encrypted-etsmobile-keystore-password: + required: true + encrypted-keystore-properties-password: + required: true + encrypted-android-service-account-credentials-password: + required: false + encrypted-ios-service-account-credentials-password: + required: false + encrypted-ios-matchfile-password: + required: false + target: + description: "Build target: {Android, iOS}" + required: false + default: 'Android' + +runs: + using: "composite" + steps: + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.19.x' + channel: 'stable' + cache: true + + # Get flutter dependencies. + - name: Get flutter dependencies + shell: bash + run: flutter pub get + + - name: Install Android dependencies + if: ${{ inputs.target == 'Android' }} + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install iOS dependencies + if: ${{ inputs.target == 'iOS' }} + shell: bash + run: | + cd ios + pod update + + - name: Commit pod updates + if: ${{ inputs.target == 'iOS' }} + id: commit_pod_versions + uses: stefanzweifel/git-auto-commit-action@v5 + with: + file_pattern: "ios/*" + commit_user_name: github-actions[bot] + commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com + commit_message: "[BOT] Applying pod update." + add_options: '-u' + + # Fail workflow, because new commit will execute workflow + - if: ${{ inputs.target == 'iOS' && steps.commit_pod_versions.outputs.changes_detected == 'true' }} + name: Fail workflow if pod version change + shell: bash + run: | + echo 'Pod update applied, running bot commit workflow' + exit 1 + + - name: Run flutter doctor + shell: bash + run: flutter doctor + + - name: Decrypt SignETS certificate and Google Services files + shell: bash + run: | + chmod +x ./scripts/decrypt.sh + ./scripts/decrypt.sh + env: + ENCRYPTED_SIGNETS_API_CERT_PASSWORD: ${{ inputs.encrypted-signets-api-cert-password }} + ENCRYPTED_GOOGLE_SERVICE_PASSWORD: ${{ inputs.encrypted-google-service-password }} + ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD: ${{ inputs.encrypted-etsmobile-keystore-password }} + ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD: ${{ inputs.encrypted-keystore-properties-password }} + ENCRYPTED_ANDROID_SERVICE_ACCOUNT_CREDENTIALS_PASSWORD: ${{ inputs.encrypted-android-service-account-credentials-password }} + ENCRYPTED_IOS_SERVICE_ACCOUNT_CREDENTIALS_PASSWORD: ${{ inputs.encrypted-ios-service-account-credentials-password }} + ENCRYPTED_IOS_MATCHFILE_PASSWORD: ${{ inputs.encrypted-ios-matchfile-password }} diff --git a/.github/composite/tag-validation/action.yaml b/.github/composite/tag-validation/action.yaml new file mode 100644 index 000000000..ce9c05c71 --- /dev/null +++ b/.github/composite/tag-validation/action.yaml @@ -0,0 +1,31 @@ +name: "Tag validation" +description: "Check if the tag already exists." +outputs: + description: "The version in the pubspec.yaml file." + version: ${{ steps.split.outputs._0 }} + +runs: + using: "composite" + steps: + - name: Get the version from the pubspec + id: pubspecVersion + uses: CumulusDS/get-yaml-paths-action@v1.0.2 + with: + file: pubspec.yaml + version: version + - uses: winterjung/split@v2.1.0 + id: split + with: + msg: ${{ steps.pubspecVersion.outputs.version }} + separator: '+' + - name: Validate that version doesn't exists + uses: mukunku/tag-exists-action@v1.6.0 + id: checkTag + with: + tag: ${{ steps.split.outputs._0 }} + - if: ${{ steps.checkTag.outputs.exists == 'true' }} + name: Post comment on PR and fail. + shell: bash + run: | + echo '${{ steps.split.outputs._0 }} already exists, please update the pubspec version.' + exit 1 diff --git a/.github/composite/tests/action.yaml b/.github/composite/tests/action.yaml new file mode 100644 index 000000000..663ac0576 --- /dev/null +++ b/.github/composite/tests/action.yaml @@ -0,0 +1,61 @@ +name: "Tests and checks" +inputs: + format: + description: "If the project needs formatting" + required: false + default: true + +runs: + using: "composite" + steps: + - name: Generate mocking files + shell: bash + run: dart run build_runner build + + # Check the format of the code and commit the formatted files. + - name: Format files in lib and test directories + if: ${{ inputs.format == true }} + shell: bash + run: dart format lib test + + - name: Commit formatted files + if: ${{ inputs.format == true }} + id: commit_formatted + uses: stefanzweifel/git-auto-commit-action@v5 + with: + file_pattern: "*.dart" + commit_user_name: github-actions[bot] + commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com + commit_message: "[BOT] Applying format." + add_options: '-u' + + # Fail workflow, because new commit will execute workflow + - if: ${{ inputs.format == true && steps.commit_formatted.outputs.changes_detected == 'true' }} + name: Fail workflow if linting commit + shell: bash + run: | + echo 'Linting applied, running bot commit workflow' + exit 1 + + # Check if the code has any errors/warnings + - name: Analyze code + shell: bash + run: flutter analyze + + - name: Run tests + shell: bash + run: flutter test --coverage + + - name: Upload coverage file + uses: actions/upload-artifact@v4 + with: + name: lcov.info + path: ${{ github.workspace }}/coverage/lcov.info + + - name: Get code coverage + shell: bash + run: | + chmod +x ./scripts/determine_code_coverage.sh + export COV="$(./scripts/determine_code_coverage.sh coverage/lcov.info)" + echo "Coverage detected is: $COV" + echo "percentage=$COV" >> $GITHUB_OUTPUT diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 84ebc8f7c..2289c92a1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,7 +16,6 @@ - [ ] If it is a core feature, I have added thorough tests. - [ ] If needed, I added analytics. - [ ] Make sure to add either one of the following labels: `version: Major`,`version: Minor` or `version: Patch`. -- [ ] Make sure golden files changes were reviewed and approved. ### 🖼️ Screenshots (if useful): diff --git a/.github/workflows/dev-workflow.yaml b/.github/workflows/dev-workflow.yaml index af34a7af6..784bd0389 100644 --- a/.github/workflows/dev-workflow.yaml +++ b/.github/workflows/dev-workflow.yaml @@ -15,104 +15,25 @@ concurrency: group: ${{ github.ref }} cancel-in-progress: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} APP_NAME: 'notre_dame' jobs: - ############################################################## - # Testing - ############################################################## testing: name: Tests and checks runs-on: ubuntu-latest - outputs: - coverage: ${{ steps.coverage.outputs.percentage }} steps: - uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} token: ${{ secrets.PTA }} - - uses: actions/setup-java@v4 + - uses: ./.github/composite/flutter-setup with: - distribution: 'temurin' - java-version: '17' - - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.19.x' - channel: 'stable' - cache: true - - run: flutter doctor - - name: Decrypt SignETS certificate and Google Services files - run: | - chmod +x ./scripts/decrypt.sh - ./scripts/decrypt.sh - env: - ENCRYPTED_SIGNETS_API_CERT_PASSWORD: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} - ENCRYPTED_GOOGLE_SERVICE_PASSWORD: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} - - # Get flutter dependencies. - - run: | - flutter pub get - dart run build_runner build --delete-conflicting-outputs - - # Check the format of the code and commit the formatted files. - - name: Format files in lib and test directories - run: dart format lib test - - name: Commit formatted files - id: commit_formatted - uses: stefanzweifel/git-auto-commit-action@v5 - with: - file_pattern: "*.dart" - commit_user_name: github-actions[bot] - commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com - commit_message: "[BOT] Applying format." - add_options: '-u' - - # Fail workflow, because new commit will execute workflow - - if: ${{ steps.commit_formatted.outputs.changes_detected == 'true' }} - name: Fail workflow if linting commit - run: | - echo 'Linting applied, running bot commit workflow' - exit 1 - - # Check if the code has any errors/warnings - - name: Analyze code - run: flutter analyze + encrypted-signets-api-cert-password: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} + encrypted-google-service-password: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} + encrypted-etsmobile-keystore-password: ${{ secrets.ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD }} + encrypted-keystore-properties-password: ${{ secrets.ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD }} + - uses: ./.github/composite/tests - # Run the tests with --update-goldens. - - name: Run tests - run: flutter test --coverage --update-goldens - - # Commit and push the goldens files updated. - - name: Commit golden files - id: commit_golden - uses: stefanzweifel/git-auto-commit-action@v5 - with: - file_pattern: test/* - commit_user_name: github-actions[bot] - commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com - commit_message: '[BOT] Update golden files' - - # Fail workflow, because new commit will execute workflow - - if: ${{ steps.commit_golden.outputs.changes_detected == 'true' }} - name: Fail workflow if golden commit - run: | - echo 'Golden files changes commit, running bot commit workflow' - exit 1 - - - name: Upload coverage file - uses: actions/upload-artifact@v4 - with: - name: lcov.info - path: ${{ github.workspace }}/coverage/lcov.info - - name: Get code coverage - id: coverage - run: | - chmod +x ./scripts/determine_code_coverage.sh - export COV="$(./scripts/determine_code_coverage.sh coverage/lcov.info)" - echo "Coverage detected is: $COV" - echo "percentage=$COV" >> $GITHUB_OUTPUT coverage: name: Update coverage needs: @@ -123,16 +44,12 @@ jobs: uses: actions/download-artifact@v4 with: name: lcov.info - # Comment coverage report - name: Comment the coverage of the PR uses: romeovs/lcov-reporter-action@v0.4.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} lcov-file: ./lcov.info - ############################################################## - # Build - ############################################################## build: name: Create ${{ matrix.target }} build runs-on: ${{ matrix.os }} @@ -160,78 +77,25 @@ jobs: - testing steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.19.x' - channel: 'stable' - cache: true - - name: Install Android dependencies - if: matrix.target == 'Android' - uses: actions/setup-java@v4 with: - java-version: '17' - distribution: 'temurin' - - run: flutter doctor -v - - name: Install iOS dependencies - if: matrix.target == 'iOS' - run: | - flutter pub get - dart run build_runner build --delete-conflicting-outputs - cd ios - pod update - flutter clean - - - name: Commit pod updates - if: matrix.target == 'iOS' - id: commit_pod_versions - uses: stefanzweifel/git-auto-commit-action@v5 + ref: ${{ github.head_ref }} + token: ${{ secrets.PTA }} + - uses: ./.github/composite/flutter-setup with: - file_pattern: "ios/*" - commit_user_name: github-actions[bot] - commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com - commit_message: "[BOT] Applying pod update." - add_options: '-u' - - # Fail workflow, because new commit will execute workflow - - if: ${{ matrix.target == 'iOS' && steps.commit_pod_versions.outputs.changes_detected == 'true' }} - name: Fail workflow if pod version change - run: | - echo 'Pod update applied, running bot commit workflow' - exit 1 - - # Get dependencies and decrypt needed files. - - run: | - flutter pub get - dart run build_runner build --delete-conflicting-outputs + encrypted-signets-api-cert-password: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} + encrypted-google-service-password: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} + encrypted-etsmobile-keystore-password: ${{ secrets.ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD }} + encrypted-keystore-properties-password: ${{ secrets.ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD }} + target: ${{ matrix.target }} - - name: Decrypt SignETS certificate and Google Services files - run: | - chmod +x ./scripts/decrypt.sh - ./scripts/decrypt.sh - env: - ENCRYPTED_SIGNETS_API_CERT_PASSWORD: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} - ENCRYPTED_GOOGLE_SERVICE_PASSWORD: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} - ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD: ${{ secrets.ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD }} - ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD: ${{ secrets.ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD }} - - # Build the application. - - name: Build the application - run: flutter build -v ${{ matrix.build_target }} ${{ matrix.build_args }} --release --dart-define=GH_API_TOKEN=${{ secrets.GH_API_TOKEN }} - - - name: Rename Android build - if: matrix.target == 'Android' - run: mv app-release.${{ matrix.asset_extension }} ${{ github.workspace }}/${{ env.APP_NAME }}_${{ matrix.target }}.${{ matrix.asset_extension }} - working-directory: ${{ matrix.build_path }} - - - name: Compress iOS build - if: matrix.target == 'iOS' - run: | - mv Runner.app ${{ env.APP_NAME }}.app - ditto -c -k --sequesterRsrc --keepParent ${{ env.APP_NAME }}.app ${{ github.workspace }}/${{ env.APP_NAME }}_${{ matrix.target }}.${{ matrix.asset_extension }} - working-directory: ${{ matrix.build_path }} - - - name: Upload build artifact - uses: actions/upload-artifact@v4 + - uses: ./.github/composite/ci-build with: - name: ${{ matrix.target }} - path: ${{ github.workspace }}/${{ env.APP_NAME }}_${{ matrix.target }}.${{ matrix.asset_extension }} + target: ${{ matrix.target }} + build-target: ${{ matrix.build_target }} + build-args: ${{ matrix.build_args }} + build-path: ${{ matrix.build_path }} + asset-extension: ${{ matrix.asset_extension }} + asset-content-type: ${{ matrix.asset_content_type }} + github-api-token: ${{ secrets.GH_API_TOKEN }} + app-name: ${{ env.APP_NAME }} + diff --git a/.github/workflows/master-workflow.yaml b/.github/workflows/master-workflow.yaml index 4eb6e5aad..b08ef65a6 100644 --- a/.github/workflows/master-workflow.yaml +++ b/.github/workflows/master-workflow.yaml @@ -17,8 +17,6 @@ concurrency: group: ${{ github.ref }} cancel-in-progress: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} APP_NAME: 'notre_dame' jobs: @@ -26,37 +24,15 @@ jobs: # Setup ############################################################## tag_validation: - name: Tag validation runs-on: ubuntu-latest outputs: - version: ${{ steps.split.outputs._0 }} + description: "The version in the pubspec.yaml file." + version: ${{ steps.tag_validation.outputs.version }} steps: - uses: actions/checkout@v4 - - name: Get the version from the pubspec - id: pubspecVersion - uses: CumulusDS/get-yaml-paths-action@v1.0.2 - with: - file: pubspec.yaml - version: version - - uses: winterjung/split@v2.1.0 - id: split - with: - msg: ${{ steps.pubspecVersion.outputs.version }} - separator: '+' - - name: Validate that version doesn't exists - uses: mukunku/tag-exists-action@v1.6.0 - id: checkTag - with: - tag: ${{ steps.split.outputs._0 }} - - if: ${{ steps.checkTag.outputs.exists == 'true' }} - name: Post comment on PR and fail. - run: | - echo '${{ steps.split.outputs._0 }} already exists, please update the pubspec version.' - exit 1 + - uses: ./.github/composite/tag-validation + id: tag_validation - ############################################################## - # Tests - ############################################################## testing: name: Tests and checks runs-on: ubuntu-latest @@ -64,53 +40,15 @@ jobs: coverage: ${{ steps.coverage.outputs.percentage }} steps: - uses: actions/checkout@v4 + - uses: ./.github/composite/flutter-setup with: - ref: ${{ github.head_ref }} - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.19.x' - channel: 'stable' - cache: true - - run: flutter doctor - - name: Decrypt SignETS certificate and Google Services files - run: | - chmod +x ./scripts/decrypt.sh - ./scripts/decrypt.sh - env: - ENCRYPTED_SIGNETS_API_CERT_PASSWORD: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} - ENCRYPTED_GOOGLE_SERVICE_PASSWORD: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} - - # Get flutter dependencies. - - run : | - flutter pub get - dart run build_runner build --delete-conflicting-outputs - - # Check if the code has any errors/warnings - - name: Analyze code - run: flutter analyze - - # Run the tests. - - name: Run tests - run: flutter test --coverage - - - name: Upload coverage file - uses: actions/upload-artifact@v4 + encrypted-signets-api-cert-password: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} + encrypted-google-service-password: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} + encrypted-etsmobile-keystore-password: ${{ secrets.ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD }} + encrypted-keystore-properties-password: ${{ secrets.ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD }} + - uses: ./.github/composite/tests with: - name: lcov.info - path: ${{ github.workspace }}/coverage/lcov.info - - - name: Get code coverage - id: coverage - run: | - chmod +x ./scripts/determine_code_coverage.sh - export COV="$(./scripts/determine_code_coverage.sh coverage/lcov.info)" - echo "Coverage detected is: $COV" - echo "percentage=$COV" >> $GITHUB_OUTPUT - + format: false coverage: name: Update coverage needs: @@ -162,63 +100,24 @@ jobs: - testing steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 + - uses: ./.github/composite/flutter-setup with: - flutter-version: '3.19.x' - channel: 'stable' - cache: true - - name: Install Android dependencies - if: matrix.target == 'Android' - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - run: flutter doctor -v - - name: Install iOS dependencies - if: matrix.target == 'iOS' - run: | - flutter pub get - dart run build_runner build --delete-conflicting-outputs - cd ios - pod install - flutter clean - - # Get dependencies and decrypt needed files. - - run: | - flutter pub get - dart run build_runner build --delete-conflicting-outputs - - - name: Decrypt SignETS certificate and Google Services files - run: | - chmod +x ./scripts/decrypt.sh - ./scripts/decrypt.sh - env: - ENCRYPTED_SIGNETS_API_CERT_PASSWORD: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} - ENCRYPTED_GOOGLE_SERVICE_PASSWORD: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} - ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD: ${{ secrets.ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD }} - ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD: ${{ secrets.ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD }} - - # Build the application. - - name: Build the application - run: flutter build -v ${{ matrix.build_target }} ${{ matrix.build_args }} --release --dart-define=GH_API_TOKEN=${{ secrets.GH_API_TOKEN }} - - - name: Rename Android build - if: matrix.target == 'Android' - run: mv app-release.${{ matrix.asset_extension }} ${{ github.workspace }}/${{ env.APP_NAME }}_${{ matrix.target }}.${{ matrix.asset_extension }} - working-directory: ${{ matrix.build_path }} - - - name: Compress iOS build - if: matrix.target == 'iOS' - run: | - mv Runner.app ${{ env.APP_NAME }}.app - ditto -c -k --sequesterRsrc --keepParent ${{ env.APP_NAME }}.app ${{ github.workspace }}/${{ env.APP_NAME }}_${{ matrix.target }}.${{ matrix.asset_extension }} - working-directory: ${{ matrix.build_path }} + encrypted-signets-api-cert-password: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} + encrypted-google-service-password: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} + encrypted-etsmobile-keystore-password: ${{ secrets.ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD }} + encrypted-keystore-properties-password: ${{ secrets.ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD }} + target: ${{ matrix.target }} - - name: Upload build artifact - uses: actions/upload-artifact@v4 + - uses: ./.github/composite/ci-build with: - name: ${{ matrix.target }} - path: ${{ github.workspace }}/${{ env.APP_NAME }}_${{ matrix.target }}.${{ matrix.asset_extension }} + target: ${{ matrix.target }} + build-target: ${{ matrix.build_target }} + build-args: ${{ matrix.build_args }} + build-path: ${{ matrix.build_path }} + asset-extension: ${{ matrix.asset_extension }} + asset-content-type: ${{ matrix.asset_content_type }} + github-api-token: ${{ secrets.GH_API_TOKEN }} + app-name: ${{ env.APP_NAME }} ############################################################## # Post-build diff --git a/.github/workflows/release-workflow.yaml b/.github/workflows/release-workflow.yaml index 4388b6e0b..0608a7e23 100644 --- a/.github/workflows/release-workflow.yaml +++ b/.github/workflows/release-workflow.yaml @@ -28,50 +28,16 @@ jobs: fail-fast: false steps: - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.19.x' - channel: 'stable' - cache: true - - name: Setup Fastlane - uses: ruby/setup-ruby@v1 - with: - ruby-version: '2.7' - bundler-cache: true - working-directory: ${{ matrix.working_directory }} - - name: Install Android dependencies - if: matrix.target == 'Android' - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'adopt' - - name: Install iOS dependencies - if: matrix.target == 'iOS' - run: | - flutter pub get - dart run build_runner build --delete-conflicting-outputs - cd ios - pod install - flutter clean - - run: flutter doctor -v - - # Get dependencies and decrypt needed files. - - run: | - flutter pub get - dart run build_runner build --delete-conflicting-outputs - - - name: Decrypt certificates files - run: | - chmod +x ./scripts/decrypt.sh - ./scripts/decrypt.sh + - uses: ./.github/composite/flutter-setup env: - ENCRYPTED_SIGNETS_API_CERT_PASSWORD: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} - ENCRYPTED_GOOGLE_SERVICE_PASSWORD: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} - ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD: ${{ secrets.ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD }} - ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD: ${{ secrets.ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD }} - ENCRYPTED_ANDROID_SERVICE_ACCOUNT_CREDENTIALS_PASSWORD: ${{ secrets.ENCRYPTED_ANDROID_SERVICE_ACCOUNT_CREDENTIALS_PASSWORD }} - ENCRYPTED_IOS_SERVICE_ACCOUNT_CREDENTIALS_PASSWORD: ${{ secrets.ENCRYPTED_IOS_SERVICE_ACCOUNT_CREDENTIALS_PASSWORD }} - ENCRYPTED_IOS_MATCHFILE_PASSWORD: ${{ secrets.ENCRYPTED_IOS_MATCHFILE_PASSWORD }} + encrypted-signets-api-cert-password: ${{ secrets.ENCRYPTED_SIGNETS_API_CERT_PASSWORD }} + encrypted-google-service-password: ${{ secrets.ENCRYPTED_GOOGLE_SERVICE_PASSWORD }} + encrypted-etsmobile-keystore-password: ${{ secrets.ENCRYPTED_ETSMOBILE_KEYSTORE_PASSWORD }} + encrypted-keystore-properties-password: ${{ secrets.ENCRYPTED_KEYSTORE_PROPERTIES_PASSWORD }} + encrypted-android-service-account-credentials-password: ${{ secrets.ENCRYPTED_ANDROID_SERVICE_ACCOUNT_CREDENTIALS_PASSWORD }} + encrypted-ios-service-account-credentials-password: ${{ secrets.ENCRYPTED_IOS_SERVICE_ACCOUNT_CREDENTIALS_PASSWORD }} + encrypted-ios-matchfile-password: ${{ secrets.ENCRYPTED_IOS_MATCHFILE_PASSWORD }} + target: ${{ matrix.target }} - name: Build the application run: flutter build -v ${{ matrix.build_target }} ${{ matrix.build_args }} --build-number=$(date '+%s') --release --dart-define=GH_API_TOKEN=${{ secrets.GH_API_TOKEN }} @@ -81,22 +47,20 @@ jobs: - name: Set changelog for each platform run: | echo "${{ github.event.release.body }}" > releaseBody.txt - enChangelog=$(cat releaseBody.txt | sed -n '/## English version$/,/## End english version/p' | sed '1d;$d'); - frChangelog=$(cat releaseBody.txt | sed -n '/## French version$/,/## End french version/p' | sed '1d;$d'); + enChangelog=$(sed -n '/## English version$/,/## End english version/p' releaseBody.txt | sed '1d;$d') + frChangelog=$(sed -n '/## French version$/,/## End french version/p' releaseBody.txt | sed '1d;$d') - if [[ ! -z "$enChangelog" ]]; then + if [[ -n "$enChangelog" ]]; then echo "Changing english changelog" - echo $enChangelog > ${{ matrix.metadata_path }}/en-CA/${{ matrix.changelog_path }} + echo "$enChangelog" > ${{ matrix.metadata_path }}/en-CA/${{ matrix.changelog_path }} echo "en-CA Changelog file cat:" cat ${{ matrix.metadata_path }}/en-CA/${{ matrix.changelog_path }} - echo ${{ matrix.metadata_path }}/en-CA/${{ matrix.changelog_path }} fi - if [[ ! -z "$frChangelog" ]]; then + if [[ -n "$frChangelog" ]]; then echo "Changing english changelog" - echo $frChangelog > ${{ matrix.metadata_path }}/fr-CA/${{ matrix.changelog_path }} + echo "$frChangelog" > ${{ matrix.metadata_path }}/fr-CA/${{ matrix.changelog_path }} echo "fr-CA Changelog file cat:" cat ${{ matrix.metadata_path }}/fr-CA/${{ matrix.changelog_path }} - echo ${{ matrix.metadata_path }}/en-CA/${{ matrix.changelog_path }} fi working-directory: ${{ matrix.working_directory }} @@ -121,6 +85,13 @@ jobs: env: PRIVATE_KEY: ${{ secrets.MATCH_GIT_SSH_KEY }} + - name: Setup Fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: '2.7' + bundler-cache: true + working-directory: ${{ matrix.working_directory }} + - name: Deploy to store run: bundle exec fastlane deploy working-directory: ${{ matrix.working_directory }} diff --git a/.gitignore b/.gitignore index a12547a23..98ca2c50a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ app_untranslated_messages.yaml # Flutter test test/**/failures/ -test/mock/**/*.mocks.dart +*.mocks.dart # Certificates and secrets assets/certificates/ diff --git a/README.fr.md b/README.fr.md index 54a3d414a..64871d137 100644 --- a/README.fr.md +++ b/README.fr.md @@ -19,8 +19,8 @@ Ce projet concrétise la quatrième version de l'application mobile ÉTSMobile pour Android et iOS. Il s'agit de portail principal entre l'utilisateur et l'École de technologie supérieure (ÉTS) sur appareils mobiles. ÉTSMobile est un projet open-source développé par les membres du club étudiant ApplETS. L'application offre notamment : * L'accès aux notes d'évaluations -* L'accès aux horaires de cours -* Et bien plus... +* L'accès aux horaires de cours +* Et bien plus... _Note: Ce guide est aussi disponible en: [English](https://github.com/ApplETS/Notre-Dame/blob/master/README.md)_ @@ -52,7 +52,7 @@ chmod +x ./env_variables.sh flutter pub get ``` -- Pour généré les mocks: +- Pour générer les mocks: ```bash dart run build_runner build ``` diff --git a/analysis_options.yaml b/analysis_options.yaml index 54fa1f2db..a5c4862f8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,4 +9,4 @@ linter: analyzer: exclude: - "lib/generated" - - "test/mock/services/*.mocks.dart" + - "*.mocks.dart" diff --git a/assets/html/armed_person_detail_en.html b/assets/html/armed_person_detail_en.html deleted file mode 100644 index 5a6330e7d..000000000 --- a/assets/html/armed_person_detail_en.html +++ /dev/null @@ -1,68 +0,0 @@ - - - -

If you can leave the premises without putting yourself in danger, evacuate the building

- - - -

If you cannot safely leave the premises without putting yourself in danger, hide

- - - -

In the event of an evacuation

- - - -

Remember…

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/armed_person_detail_fr.html b/assets/html/armed_person_detail_fr.html deleted file mode 100644 index 03b86d68a..000000000 --- a/assets/html/armed_person_detail_fr.html +++ /dev/null @@ -1,71 +0,0 @@ - - - -

Si vous pouvez quitter les lieux sans ĂŞtre menacĂ©, Ă©vacuez l’Ă©difice

- - - -

Si vous ne pouvez pas quitter les lieux sans ĂŞtre menacĂ©, confinez-vous  

- - - -

Dans l’Ă©ventualitĂ© d’une Ă©vacuation

- - - -

Rappelez-vous

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/bomb_threat_detail_en.html b/assets/html/bomb_threat_detail_en.html deleted file mode 100644 index 385f59e73..000000000 --- a/assets/html/bomb_threat_detail_en.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - -

If the bomb threat is made by phone:

- - - -

Evacuation

- - - -

Returning to the building

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/bomb_threat_detail_fr.html b/assets/html/bomb_threat_detail_fr.html deleted file mode 100644 index 5c13cfd1d..000000000 --- a/assets/html/bomb_threat_detail_fr.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - -

Si la menace est transmise par téléphone

- - - -

Évacuation

- - - -

Retour dans l’immeuble

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/broken_elevator_detail_en.html b/assets/html/broken_elevator_detail_en.html deleted file mode 100644 index 5dc2deef9..000000000 --- a/assets/html/broken_elevator_detail_en.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - -

If people are stuck inside

- - - -

If a person’s condition worsens, inform Security.
- If you can’t reach Security, dial 911.

- - - \ No newline at end of file diff --git a/assets/html/broken_elevator_detail_fr.html b/assets/html/broken_elevator_detail_fr.html deleted file mode 100644 index c2d897346..000000000 --- a/assets/html/broken_elevator_detail_fr.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - -

Si des personnes sont coincĂ©es Ă  l’intĂ©rieur

- - - -

Si l’Ă©tat d’une personne s’aggrave, informez-en la SĂ©curitĂ©.
- Si vous n’arrivez pas Ă  joindre la SĂ©curitĂ©, composez le 911.

- - - \ No newline at end of file diff --git a/assets/html/earthquake_detail_en.html b/assets/html/earthquake_detail_en.html deleted file mode 100644 index 3be005638..000000000 --- a/assets/html/earthquake_detail_en.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - -

When should you evacuate?

- - - -

How should you evacuate the building?

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/earthquake_detail_fr.html b/assets/html/earthquake_detail_fr.html deleted file mode 100644 index c79261a6e..000000000 --- a/assets/html/earthquake_detail_fr.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - -

Quand Ă©vacuer?

- - - -

Comment évacuer le bâtiment ?

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/electrical_outage_detail_en.html b/assets/html/electrical_outage_detail_en.html deleted file mode 100644 index ef026c58d..000000000 --- a/assets/html/electrical_outage_detail_en.html +++ /dev/null @@ -1,50 +0,0 @@ - - - -

When an electrical outage occurs. 

- - - -

There may be a delay between the beginning of the outage and emergency systems being - activated.

- -

General outage

- - - -

End of the outage

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/electrical_outage_detail_fr.html b/assets/html/electrical_outage_detail_fr.html deleted file mode 100644 index 4eb83fc08..000000000 --- a/assets/html/electrical_outage_detail_fr.html +++ /dev/null @@ -1,53 +0,0 @@ - - - -

Dès que survient la panne électrique

- - - -

Il peut y avoir un dĂ©lai entre le dĂ©but de la panne et l’activation des systèmes d’urgence. -

- -

Panne généralisée

- - - -

Fin de la panne

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/evacuation_detail_en.html b/assets/html/evacuation_detail_en.html deleted file mode 100644 index 5fdfc0019..000000000 --- a/assets/html/evacuation_detail_en.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - -

Returning to the building

- - - -

Meeting points

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/evacuation_detail_fr.html b/assets/html/evacuation_detail_fr.html deleted file mode 100644 index 6421b1091..000000000 --- a/assets/html/evacuation_detail_fr.html +++ /dev/null @@ -1,44 +0,0 @@ - - - -

Attendez que la SĂ©curitĂ© ordonne l’Ă©vacuation. Celui-ci doit inspecter les voies d’Ă©vacuation - pour s’assurer qu’il n’y a pas de danger (colis suspect)

- - - -

Retour dans l’immeuble

- - - -

Lieux de rassemblement

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/fire_detail_en.html b/assets/html/fire_detail_en.html deleted file mode 100644 index 5e00681cd..000000000 --- a/assets/html/fire_detail_en.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - -

 If you hear a fire alarm go off

- - - -

Meeting points

- - - -

Returning to the building

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/fire_detail_fr.html b/assets/html/fire_detail_fr.html deleted file mode 100644 index 7e5de97cf..000000000 --- a/assets/html/fire_detail_fr.html +++ /dev/null @@ -1,55 +0,0 @@ - - - -

Si vous voyez du feu ou de la fumée

- - - -

Lieux de rassemblement

- - - -

Retour dans l’immeuble

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/gas_leak_detail_en.html b/assets/html/gas_leak_detail_en.html deleted file mode 100644 index 4fe3d151a..000000000 --- a/assets/html/gas_leak_detail_en.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - -

Gas odour or leak

- - - -

Evacuation order

- -

Under order of evacuation by Security or a member of the evacuation team:

- - - -

Don’t touch anything

- - - \ No newline at end of file diff --git a/assets/html/gas_leak_detail_fr.html b/assets/html/gas_leak_detail_fr.html deleted file mode 100644 index 49d786fc9..000000000 --- a/assets/html/gas_leak_detail_fr.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - -

Odeur ou fuite de gaz

- - - -

Ordre d’Ă©vacuation

- -

Sur ordre d’Ă©vacuation de la SĂ©curitĂ© ou d’un membre de l’Ă©quipe d’Ă©vacuation - :

- - - -

Ne touchez Ă  rien

- - - - \ No newline at end of file diff --git a/assets/html/medical_emergency_detail_en.html b/assets/html/medical_emergency_detail_en.html deleted file mode 100644 index 98b0607a3..000000000 --- a/assets/html/medical_emergency_detail_en.html +++ /dev/null @@ -1,36 +0,0 @@ - - - -

If someone’s life is in danger, call 911

- -

If the person is conscious, ask him or her:

- -
    -
  1. His or her name
  2. -
  3. Details about the incident
  4. -
  5. Details about his or her wounds
  6. -
  7. Questions about his or her allergies and health problems
  8. -
- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/medical_emergency_detail_fr.html b/assets/html/medical_emergency_detail_fr.html deleted file mode 100644 index fccdc7c8c..000000000 --- a/assets/html/medical_emergency_detail_fr.html +++ /dev/null @@ -1,36 +0,0 @@ - - - -

Si la vie d’une personne est en danger, composez le 911

- -

Si la personne est consciente, demandez-lui :

- - - - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/suspicious_packages_detail_en.html b/assets/html/suspicious_packages_detail_en.html deleted file mode 100644 index 045db795b..000000000 --- a/assets/html/suspicious_packages_detail_en.html +++ /dev/null @@ -1,73 +0,0 @@ - - - -

A package is considered suspicious if it presents the following characteristics: 

- - - -

Note

- -

A package may contain nuclear, radiological, biological or chemical material if it presents one - of the following characteristics: 

- - - -

What to do

- - - -

Evacuation

- - - -

Returning to the building

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/html/suspicious_packages_detail_fr.html b/assets/html/suspicious_packages_detail_fr.html deleted file mode 100644 index b65c38048..000000000 --- a/assets/html/suspicious_packages_detail_fr.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -

Un colis est considĂ©rĂ© comme suspect s’il prĂ©sente les caractĂ©ristiques suivantes :

- - - -

Note

- -

Un colis peut ĂŞtre considĂ©rĂ© comme contenant des matières nuclĂ©aires, radiologiques, biologiques - ou chimiques s’il prĂ©sente l’une des caractĂ©ristiques suivantes :

- - - -

Marche Ă  suivre

- - - -

Évacuation

- - - -

Retour dans l’immeuble

- - -
-
-
-
- \ No newline at end of file diff --git a/assets/images/capra.png b/assets/images/capra.png new file mode 100644 index 000000000..a0a53661b Binary files /dev/null and b/assets/images/capra.png differ diff --git a/assets/images/capra_long.png b/assets/images/capra_long.png deleted file mode 100644 index 10ddc543a..000000000 Binary files a/assets/images/capra_long.png and /dev/null differ diff --git a/assets/markdown/armed_person_en.md b/assets/markdown/armed_person_en.md new file mode 100644 index 000000000..de9611e4b --- /dev/null +++ b/assets/markdown/armed_person_en.md @@ -0,0 +1,39 @@ +## If you can leave the premises without putting yourself in danger, evacuate the building + +- Remain calm. +- Never try to stop an armed person. +- Do not activate the fire alarm. +- Take the nearest emergency exit. +- Use the stairs to exit the building. Don’t take the elevator. +- Show your hands so that police officers don’t see you as a threat. +- Inform anyone you meet that they should leave the premises immediately. +- Once you leave the building, don’t stay in close range. Go to the meeting point that is a little further away, at the corner of Peel and St. Jacques Streets (the space in front of the old Planetarium). +- Once you’ve reached safety, call 911. + +## If you cannot safely leave the premises without putting yourself in danger, hide + +- Stay calm and silent. +- Hide and tell other people to do the same. +- Lock the door of the room where you’re hiding. +- Barricade the door with furniture. +- Close the blinds on the windows. +- Stay away from doors and windows (hallways and outdoor walls). +- If there is a window that looks out on an external wall of the building, let police know you are there by putting a note in the window. +- Hide under a desk. +- Stay quiet. +- Put your cell phone in silent mode. +- If someone knocks at the door, don’t open it. The person who is knocking may pose a threat to you. +- In the event that police officers need to check the premises, they will receive access cards and sets of keys. +- Never jump out of your hiding place, and keep your hands in plain sight. +- Wait for directions from police officers or the Security and Prevention Office before leaving the premises. + +### In the event of an evacuation + +- Go to the meeting point that is a little further away, at the corner of Peel and St. Jacques Streets (the space in front of the old Planetarium). +- Never stay close to the building. +- Keep quiet so you can hear directions given by police or security officers. + +### Remember… + +- Never jump out of your hiding place. +- Keep your hands in plain sight. \ No newline at end of file diff --git a/assets/markdown/armed_person_fr.md b/assets/markdown/armed_person_fr.md new file mode 100644 index 000000000..67ba153f9 --- /dev/null +++ b/assets/markdown/armed_person_fr.md @@ -0,0 +1,39 @@ +## Si vous pouvez quitter les lieux sans être menacé, évacuez l’édifice + +- Restez calme; +- Ne tentez jamais d’arrêter une personne armée; +- N’activez pas l’alarme-incendie; +- Empruntez la sortie de secours la plus près; +- Utilisez les escaliers pour sortir du bâtiment (n’utilisez jamais les ascenseurs); +- Montrez vos mains afin de ne pas être perçu comme une menace par les policiers; +- Informez les gens que vous croisez de quitter les lieux immédiatement; +- Une fois que vous avez quitté l’édifice, ne restez pas à proximité de l’immeuble; rendez-vous au point de rassemblement éloigné, qui se trouve au coin des rue Peel et St-Jacques (l’espace devant l’ancien planétarium); +- Une fois en sécurité, composez le 911. + +## Si vous ne pouvez pas quitter les lieux sans être menacé, confinez-vous + +- Restez calme et silencieux; +- Cachez-vous et dites aux gens de faire comme vous; +- Verrouillez la porte du local où vous êtes caché; +- Barricadez la porte avec des meubles; +- Fermez les stores des fenêtres; +- Éloignez-vous de la porte et des fenêtres (corridors et murs extérieurs); +- Si une fenêtre donne sur un mur extérieur du bâtiment, signalez votre présence aux policiers en mettant un papier avec une note; +- Cachez-vous sous un bureau; +- Gardez le silence; +- Mettez votre cellulaire en mode silencieux; +- Si une personne frappe à la porte, ne l’ouvrez pas. La personne qui frappe à la porte représente peut-être une menace; +- Dans l’éventualité où les policiers auront à vérifier tous les locaux, ces derniers recevront des cartes d’accès et des trousseaux de clefs; +- Ne bondissez jamais de votre cachette et gardez les mains bien en vue; +- Attendez les directives des policiers ou du Bureau de la prévention et de la sécurité avant de quitter les lieux. + +### Dans l’éventualité d’une évacuation + +- Rendez-vous au lieu de rassemblement éloigné (l’espace devant l’ancien Planétarium) situé à l’angle des rues Peel et St-Jacques; +- Ne demeurez jamais à proximité de l’immeuble; +- Gardez le silence pour entendre les directives données par les policiers ou par la sécurité. + +### Rappelez-vous + +- Ne bondissez jamais de votre cachette; +- Gardez vos mains bien en vue. \ No newline at end of file diff --git a/assets/markdown/bomb_threat_en.md b/assets/markdown/bomb_threat_en.md new file mode 100644 index 000000000..9b19fc7bc --- /dev/null +++ b/assets/markdown/bomb_threat_en.md @@ -0,0 +1,38 @@ +### If the bomb threat is made by phone: + +- Don’t hang up. +- Let the caller hang up first. +- Try to get as much information as possible. +- Get the person to repeat him- or herself, or pretend you can’t hear to buy time. +- Pay special attention to the following elements: + - Gender + - Age + - Accent, language spoken + - Tone of voice (high or low) + - Speech rhythm and emotions (angry, calm, agitated) + - Background noises + +- Depending on the situation and what is possible, try to obtain the following information: + - The time at which the bomb will go off + - The type of bomb + - The people being targeted and the reason + - The location of the bomb + - The name and number on the phone’s Caller ID + +- Contact Security at extension 55 (internal phone system) to alert them to the situation and share relevant information. +- Security may then ask you to come to the Security Operations Centre, located at the entrance of Pavilion A. + +### Evacuation + +- Wait until Security orders evacuation. Security officers must inspect evacuation routes to ensure that there is no danger (suspicious packages). +- Stay calm. +- Dress according to the outdoor temperature, but don’t make a detour to get clothing (for example, don’t go to your locker). +- Evacuate the area via the closest emergency staircase. Don’t take the elevator. +- Keep a safe distance from the building. +- Go to the designated meeting point. +- Do not go back inside for any reason. + +### Returning to the building + +- You can return to the building as soon as Security announces that it is safe to do so. +- This authorization will be announced over a megaphone at the meeting point \ No newline at end of file diff --git a/assets/markdown/bomb_threat_fr.md b/assets/markdown/bomb_threat_fr.md new file mode 100644 index 000000000..4ab580620 --- /dev/null +++ b/assets/markdown/bomb_threat_fr.md @@ -0,0 +1,38 @@ +### Si la menace est transmise par téléphone + +- Ne mettez pas fin à la communication; +- Laissez l’interlocuteur raccrocher en premier; +- Tentez d’obtenir le maximum de renseignements; +- Faites répéter la personne ou prétextez avoir de la difficulté à l’entendre afin de gagner du temps; +- Portez une attention particulière aux éléments suivants : + - Sexe + - Âge + - Accent, langue parlée + - Tonalité de la voix (aiguë ou grave) + - Débit et émotions (fâché, calme, agité) + - Bruits dans l’environnement + +- Dans la mesure du possible et en fonction de la situation, tentez d’obtenir les renseignements suivants : + - L’heure à laquelle la bombe explosera + - Le type de bombe + - Les personnes visées et le motif + - L’emplacement de la bombe + - Le nom et le numéro qui apparaissent sur l’afficheur du téléphone + +- Communiquez avec la Sécurité au poste 55 (système téléphonique interne) pour signaler la situation et transmettre les renseignements pertinents; +- La Sécurité pourrait ensuite vous demander de vous rendre au centre des opérations de la sécurité, située à l’entrée du Pavillon A. + +### Évacuation + +- Attendez que la Sécurité ordonne l’évacuation. Celle-ci doit inspecter les voies d’évacuation pour s’assurer qu’il n’y a pas de danger (colis suspect); +- Restez calme; +- Habillez-vous en fonction de la température extérieure, mais ne faites pas de détour pour récupérer vos vêtements (par exemple : n’allez pas à votre casier); +- Évacuez l’endroit par les escaliers de secours les plus près. N’utilisez pas les ascenseurs; +- Éloignez-vous de l’immeuble; +- Dirigez-vous vers le lieu de rassemblement; +- Ne retournez sous aucun prétexte à l’intérieur. + +### Retour dans l’immeuble + +- Le retour dans l’immeuble se fera dès que la Sécurité l’autorisera; +- Cette autorisation sera communiquée au moyen d’un porte-voix au lieu de rassemblement. \ No newline at end of file diff --git a/assets/markdown/broken_elevator_en.md b/assets/markdown/broken_elevator_en.md new file mode 100644 index 000000000..0a32ed984 --- /dev/null +++ b/assets/markdown/broken_elevator_en.md @@ -0,0 +1,10 @@ +### If people are stuck inside + +- Don’t try to save them by opening the door. +- Reassure them. Let them know that help is on the way. +- Ask them if anyone is injured or if anyone needs special assistance (for example, people with claustrophobia) and find out how many people are stuck in the elevator. +- Contact Security at extension 55 (internal phone system). +- Stay on site until help arrives. Continue to reassure people. + +**If a person’s condition worsens, inform Security. +If you can’t reach Security, dial 911.** \ No newline at end of file diff --git a/assets/markdown/broken_elevator_fr.md b/assets/markdown/broken_elevator_fr.md new file mode 100644 index 000000000..25cbbdd81 --- /dev/null +++ b/assets/markdown/broken_elevator_fr.md @@ -0,0 +1,10 @@ +### Si des personnes sont coincées à l’intérieur + +- Ne tentez pas de les secourir en ouvrant la porte; +- Rassurez-les. Dites-leur que les secours s’en viennent; +- Demandez-leur s’il y a des blessés ou des personnes nécessitant une assistance particulière (par exemple : claustrophobes) et le nombre de personnes prises dans l’ascenseur; +- Communiquez avec la Sécurité, au poste 55 (à partir du système téléphonique interne); +- Restez sur les lieux jusqu’à ce que les secours arrivent. Continuez de rassurer les gens. + +**Si l’état d’une personne s’aggrave, informez-en la Sécurité. +Si vous n’arrivez pas à joindre la Sécurité, composez le 911.** \ No newline at end of file diff --git a/assets/markdown/earthquake_en.md b/assets/markdown/earthquake_en.md new file mode 100644 index 000000000..ec47089f6 --- /dev/null +++ b/assets/markdown/earthquake_en.md @@ -0,0 +1,18 @@ +- **Get down**: As soon as you feel the first tremors, your first reflex should be to crouch down on the floor to avoid falling. +- **Seek shelter**: As soon as you have crouched down, locate and move to a place where you can shelter yourself, such as underneath your desk or work table. +- **Hang on**: As soon as you are under your shelter, hang on tightly to the desk or table and remain there until the earthquake is over. +- Once the tremors have ended, remain under your shelter. +- Security will make an announcement by voice message that is appropriate for your area. + +### When should you evacuate? + +- When a fire has been declared near you. +- When Security has ordered you to evacuate. + +### How should you evacuate the building? + +- Do not use elevators. +- Follow the directions given by Security through the voice messaging system. +- Security may indicate emergency exits for you to use. +- Once you have exited the building, look around carefully to avoid objects that could fall on you. +- Go to the meeting point in the parking lot of the old Planetarium, at the corner of Peel and Notre Dame Streets. \ No newline at end of file diff --git a/assets/markdown/earthquake_fr.md b/assets/markdown/earthquake_fr.md new file mode 100644 index 000000000..6fc48ad9e --- /dev/null +++ b/assets/markdown/earthquake_fr.md @@ -0,0 +1,18 @@ +- **Baissez-vous** : Dès les premières secousses, votre premier réflexe doit être de vous accroupir au sol pour éviter une chute. +- **Abritez-vous** : Aussitôt que vous êtes accroupi, localisez et dirigez-vous vers un endroit où vous pouvez vous abriter. Les dessous de votre bureau ou de votre table de travail, par exemple. +- **Agrippez-vous** : Dès que vous êtes à l’abri, agrippez-vous solidement au meuble et demeurez ainsi jusqu’à la fin du tremblement de terre. +- Une fois les secousses terminées, restez à l’abri. +- La Sécurité diffusera un message phonique approprié à votre secteur. + +### Quand évacuer? + +- Lorsqu’un incendie s’est déclaré près de vous. +- Lorsque la Sécurité vous donne l’ordre d’évacuer. + +### Comment évacuer le bâtiment ? + +- N’utilisez pas les ascenseurs. +- Suivez les indications transmises par la Sécurité au moyen du système de messagerie phonique. +- La Sécurité pourra alors vous indiquer les issues de secours à utiliser. +- Une fois à l’extérieur du bâtiment, surveillez votre environnement pour éviter les objets qui pourraient vous heurter. +- Dirigez-vous au lieu de rassemblement situé dans le stationnement de l’ancien planétarium, à l’angle des rues Peel et Notre-Dame. \ No newline at end of file diff --git a/assets/markdown/electrical_outage_en.md b/assets/markdown/electrical_outage_en.md new file mode 100644 index 000000000..089bdb128 --- /dev/null +++ b/assets/markdown/electrical_outage_en.md @@ -0,0 +1,25 @@ +### When an electrical outage occurs + +- Remain calm. +- Stay at your work station. +- If you are in a dark area, slowly make your way to a place that is well-lit until the situation returns to normal. +- Find people who may be in spaces without windows: washrooms, storage areas, etc. +- Make sure that the outage does not affect your area. +- Ask the person in charge of your group to contact Security at extension 55 (internal phone system). +- Stop any dangerous activity (for example, handling chemical products). +- Put away dangerous products. +- Turn off all sources of heat and burners. +- Listen to instructions announced over the loudspeakers. + +#### There may be a delay between the beginning of the outage and emergency systems being activated. + +### General outage + +- Wait for instructions from Security. +- Following an evacuation order, take the stairs. Elevators should only be used for emergencies and for people with reduced mobility. +- When an evacuation occurs following an electricity outage, people leave school for the rest of the day. They should not go to the meeting point. +- If the outage is of a longer duration, you can obtain information on the ÉTS website or by listening to its greeting message at 514-396-8800. + +### End of the outage + +- Wait for instructions before entering the building. diff --git a/assets/markdown/electrical_outage_fr.md b/assets/markdown/electrical_outage_fr.md new file mode 100644 index 000000000..2fd311d8e --- /dev/null +++ b/assets/markdown/electrical_outage_fr.md @@ -0,0 +1,25 @@ +### Dès que survient la panne électrique + +- Restez calme. +- Demeurez à votre poste de travail. +- Si vous êtes dans un local sans lumière, dirigez-vous lentement vers un endroit éclairé en attendant que la situation se rétablisse. +- Allez chercher les personnes qui pourraient se trouver dans des locaux sans fenêtres : toilettes, rangements, etc. +- Vérifiez que la panne ne touche que votre secteur. +- Demandez à la personne responsable de votre groupe de communiquer avec la sécurité au poste 55 (à partir du système téléphonique interne). +- Cessez toute activité dangereuse (par exemple : manipulation de produits chimiques). +- Entreposez les produits dangereux. +- Éteignez les sources de chaleur ou les brûleurs. +- Soyez à l’écoute des instructions diffusées dans les haut-parleurs. + +#### Il peut y avoir un délai entre le début de la panne et l’activation des systèmes d’urgence. + +### Panne généralisée + +- Attendez les instructions de la Sécurité. +- À la suite d’un ordre d’évacuation, prenez les escaliers. Les ascenseurs ne doivent servir que pour les cas d’urgence et pour les personnes à mobilité réduite. +- Lorsqu’une évacuation survient à la suite d’une panne d’électricité, les personnes quittent l’école pour le reste de la journée. Elles ne doivent pas se rendre au point de rassemblement. +- Si la panne est de longue durée, vous pouvez vous informer sur le site Web de l’ÉTS ou en écoutant son message d’accueil, au 514-396-8800. + +### Fin de la panne + +- Attendez les instructions avant d’entrer dans l’établissement. \ No newline at end of file diff --git a/assets/markdown/evacuation_en.md b/assets/markdown/evacuation_en.md new file mode 100644 index 000000000..b0f8ac3f9 --- /dev/null +++ b/assets/markdown/evacuation_en.md @@ -0,0 +1,18 @@ +Wait until Security orders evacuation. Security officers must inspect evacuation routes to ensure that there is no danger (suspicious packages). + +- Stay calm. +- Dress according to the outdoor temperature, but don’t make a detour to get clothing (for example, don’t go to your locker). +- Evacuate the area via the closest emergency stairwell. Don’t take the elevator. +- Keep a safe distance from the building. +- Go to the meeting point. +- Do not go back inside for any reason. + +### Returning to the building + +- You can return to the building as soon as Security announces that it is safe to do so. +- This authorization will be announced over a megaphone at the meeting point. + +### Meeting points + +- The Dow Planetarium parking lot, at the corner of Peel and Notre Dame streets, at the northeastern corner of ÉTS. +- In the wintertime, people who are evacuated will be escorted, as soon as possible, to the hall in Pavilion B and the common areas of the dormitories located at 301 Peel Street and 1045 Ottawa Street. \ No newline at end of file diff --git a/assets/markdown/evacuation_fr.md b/assets/markdown/evacuation_fr.md new file mode 100644 index 000000000..f39fc9669 --- /dev/null +++ b/assets/markdown/evacuation_fr.md @@ -0,0 +1,18 @@ +Attendez que la Sécurité ordonne l’évacuation. Celui-ci doit inspecter les voies d’évacuation pour s’assurer qu’il n’y a pas de danger (colis suspect). + +- Restez calme +- Habillez-vous en fonction de la température extérieure, mais ne faites pas de détour pour récupérer vos vêtements (par exemple : n’allez pas à votre casier) +- Évacuez l’endroit par les escaliers de secours les plus proches. N’utilisez pas les ascenseurs. +- Éloignez-vous de l’immeuble +- Dirigez-vous vers le lieu de rassemblement +- Ne retournez sous aucun prétexte à l’intérieur + +### Retour dans l’immeuble + +- Le retour dans l’immeuble se fera dès que le responsable de la Sécurité l’autorisera +- Cette autorisation sera communiquée au moyen d’un porte-voix au point de rassemblement + +### Lieux de rassemblement + +- Le stationnement du Planétarium Dow, situé à l’angle des rues Peel et Notre-Dame côté nord-est de l’ÉTS. +- En période hivernale, tous les évacués seront escortés le plus tôt possible dans le hall du pavillon B et dans les salles communes des résidences universitaires situées au 301, rue Peel et au 1045, rue Ottawa. \ No newline at end of file diff --git a/assets/markdown/fire_en.md b/assets/markdown/fire_en.md new file mode 100644 index 000000000..b3544b2b3 --- /dev/null +++ b/assets/markdown/fire_en.md @@ -0,0 +1,28 @@ +## If you see fire or smoke + +- Help anyone who is in immediate danger, as long as there is no risk to your own life. +- Leave the premises. +- Set off the fire alarm (stations are very close to emergency exits). +- Use the stairs to leave the building (never use the elevators). +- Close all doors behind you to make sure that the fire doesn’t spread. +- Dial 911 as soon as you are in a safe area. +- Do not go back inside for any reason. + +### If you hear a fire alarm go off + +- Help anyone who is in immediate danger, as long as there is no risk to your own life. +- Leave the premises. +- Use the stairs to leave the building (never use the elevators). +- Close all doors behind you to make sure that the fire doesn’t spread. +- Keep your distance from the premises (minimum of 100 metres) so that those who are following behind you can exit easily, and to make firefighters’ jobs easier. +- Do not go back inside for any reason. + +### Meeting points + +- The Dow Planetarium parking lot, at the corner of Peel and Notre Dame Streets, at the northeastern corner of ÉTS. +- In the wintertime, people who are evacuated will be escorted, as soon as possible, to the hall in Pavilion B and the common areas of the dormitories located at 301 Peel Street and 1045 Ottawa Street. + +### Returning to the building + +- You can return to the building as soon as Security announces that it is safe to do so. +- This authorization will be announced over a megaphone at the meeting point. \ No newline at end of file diff --git a/assets/markdown/fire_fr.md b/assets/markdown/fire_fr.md new file mode 100644 index 000000000..08c0960f5 --- /dev/null +++ b/assets/markdown/fire_fr.md @@ -0,0 +1,28 @@ +## Si vous voyez du feu ou de la fumée + +- Aidez quiconque se trouve en danger immédiat, si cela ne comporte pas de risque pour votre vie. +- Quittez les lieux. +- Déclenchez l’alarme-incendie (les postes se trouvent près des issues de secours). +- Utilisez les escaliers pour sortir du bâtiment (n’utilisez jamais les ascenseurs). +- Fermez toutes les portes derrière vous pour éviter que l’incendie ne se propage. +- Composez le 911 dès que vous vous trouvez dans un endroit sécuritaire. +- Ne retournez sous aucun prétexte à l’intérieur. + +### Si vous entendez le signal d’alarme-incendie + +- Aidez quiconque se trouve en danger immédiat, si cela ne comporte pas de risque pour votre vie. +- Quittez les lieux. +- Utilisez les escaliers pour sortir du bâtiment (n’utilisez jamais les ascenseurs). +- Fermez toutes les portes derrière vous pour éviter que l’incendie ne se propage. +- Éloignez-vous des lieux (distance minimale de 100 mètres) afin de permettre à ceux qui suivent de sortir et de faciliter le travail des pompiers. +- Ne retournez sous aucun prétexte à l’intérieur. + +### Lieux de rassemblement + +- Le stationnement du Planétarium Dow, situé à l’angle des rues Peel et Notre-Dame, du côté nord-est de l’ÉTS. +- En période hivernale, tous les évacués seront escortés le plus tôt possible dans le hall du pavillon B et dans les salles communes des résidences universitaires situées au 301, rue Peel et au 1045, rue Ottawa. + +### Retour dans l’immeuble + +- Le retour dans l’immeuble se fera dès que la Sécurité l’autorisera. +- Cette autorisation sera communiquée au moyen d’un porte-voix au lieu de rassemblement. \ No newline at end of file diff --git a/assets/markdown/gas_leak_en.md b/assets/markdown/gas_leak_en.md new file mode 100644 index 000000000..861d51bc0 --- /dev/null +++ b/assets/markdown/gas_leak_en.md @@ -0,0 +1,18 @@ +### Gas odour or leak + +- Leave the area where you are (i.e., lab, cafeteria kitchen, etc.). +- Contact Security at extension 55 (internal phone system) in an emergency, using a phone in your area. +- Don’t use a cell phone near the leak (be sure that there is no perceptible odour before using a cell phone, to prevent any danger of explosion). +- Never activate a fire alarm (red station). + +### Evacuation order + +Under order of evacuation by Security or a member of the evacuation team: + +- Stay calm. +- Stop whatever you are doing safely. + +### Don’t touch anything + +- Anything that is on must stay on. +- Anything that is off must stay off. \ No newline at end of file diff --git a/assets/markdown/gas_leak_fr.md b/assets/markdown/gas_leak_fr.md new file mode 100644 index 000000000..ecabffeef --- /dev/null +++ b/assets/markdown/gas_leak_fr.md @@ -0,0 +1,18 @@ +### Odeur ou fuite de gaz + +- Quittez le secteur où vous vous trouvez (par ex. : laboratoire, cuisine de la cafétéria, etc.). +- Communiquez avec la Sécurité au poste 55 (système téléphonique interne) pour les urgences en utilisant un téléphone situé dans un autre secteur. +- N’utilisez pas un cellulaire à proximité de la fuite (s’assurer que l’odeur n’est plus perceptible avant d’utiliser un cellulaire, pour prévenir tout danger d’explosion). +- N’activez jamais une alarme-incendie (station rouge). + +### Ordre d’évacuation + +Sur ordre d’évacuation de la Sécurité ou d’un membre de l’équipe d’évacuation : + +- Restez calme. +- Cessez toute activité de façon sécuritaire. + +### Ne touchez à rien + +- Ce qui est allumé doit rester allumé. +- Ce qui est éteint doit rester éteint. \ No newline at end of file diff --git a/assets/markdown/medical_emergency_en.md b/assets/markdown/medical_emergency_en.md new file mode 100644 index 000000000..b2cfc8307 --- /dev/null +++ b/assets/markdown/medical_emergency_en.md @@ -0,0 +1,20 @@ +#### If someone’s life is in danger, call 911 + +### If the person is conscious, ask him or her: + +1. His or her name +2. Details about the incident +3. Details about his or her wounds +4. Questions about his or her allergies and health problems + +- Contact Security at extension 55 (internal telephone system). +- Do not hang up until the dispatcher tells you to do so. +- If you have first aid training, give first aid. +- Ask those who are present to find a first aid worker on the floor. +- Stay on site to assist first aid workers and give them information about the victim. +- Ask people on site to help you as you wait for help. +- Keep people away from the victim to keep the area clear and make first aid workers’ job easier. +- Don’t move the victim unless the situation presents a danger to him or her. +- Don’t give the victim anything to eat or drink, even if he or she asks for it. +- If possible, stay in contact with Security. +- If the victim’s state deteriorates, inform Security. \ No newline at end of file diff --git a/assets/markdown/medical_emergency_fr.md b/assets/markdown/medical_emergency_fr.md new file mode 100644 index 000000000..688e3c52a --- /dev/null +++ b/assets/markdown/medical_emergency_fr.md @@ -0,0 +1,20 @@ +#### Si la vie de quelqu'un est en danger, composez le 911 + +### Si la personne est consciente, demandez-lui : + +1. Son nom +2. Les détails de l’incident +3. Les détails sur ses blessures +4. Ses allergies et problèmes de santé + +- Contactez la Sécurité au poste 55 (système téléphonique interne). +- Ne raccrochez pas avant que le répartiteur vous le demande. +- Si vous avez une formation en premiers secours, administrez les premiers secours. +- Demandez aux personnes présentes de trouver un secouriste sur le site. +- Restez sur place pour assister les secouristes et leur fournir des informations sur la victime. +- Demandez aux personnes présentes de vous aider en attendant l’arrivée de l’aide. +- Gardez les personnes éloignées de la victime pour maintenir la zone dégagée et faciliter le travail des secouristes. +- Ne déplacez pas la victime à moins que la situation ne présente un danger pour elle. +- Ne donnez rien à manger ou à boire à la victime, même si elle en fait la demande. +- Si possible, restez en contact avec la Sécurité. +- Si l’état de la victime se détériore, informez la Sécurité. \ No newline at end of file diff --git a/assets/markdown/suspicious_packages_en.md b/assets/markdown/suspicious_packages_en.md new file mode 100644 index 000000000..959f707f5 --- /dev/null +++ b/assets/markdown/suspicious_packages_en.md @@ -0,0 +1,46 @@ +A package is considered suspicious if it presents the following characteristics: + +- Excessive stamps +- Suspicious odour +- Suspicious noise +- No return address +- Several misspelled words +- Addressed to a person’s title or no name +- Excessive amount of tape +- Unexpected delivery +- Labelled “private” or “only to be opened by” + +**Note** + +A package may contain nuclear, radiological, biological, or chemical material if it presents one of the following characteristics: + +- It appears damaged +- It’s leaking +- It contains liquid, dust, or granules +- It smells unusual +- It is emitting gas or vapour +- It is seeping or boiling +- It contains aerosols +- It shows signs of corrosion + +## What to do + +- Keep a safe distance from the package. +- Don’t use your cell phone near the package. Turn it off to prevent any interference with the package. +- Contact Security at extension 55 (internal telephone system). +- Don’t hang up until the dispatcher tells you to do so. + +## Evacuation + +- Wait until Security orders evacuation. Security officers must inspect evacuation routes to ensure that there is no danger (suspicious packages). +- Stay calm. +- Dress according to the outdoor temperature, but don’t make a detour to get clothing (for example, don’t go to your locker). +- Evacuate the area via the closest emergency stairwell. Don’t take the elevator. +- Keep a safe distance from the building. +- Go to the meeting point. +- Do not go back inside for any reason. + +## Returning to the building + +- You can return to the building as soon as Security announces that it is safe to do so. +- This authorization will be announced over a megaphone at the meeting point. \ No newline at end of file diff --git a/assets/markdown/suspicious_packages_fr.md b/assets/markdown/suspicious_packages_fr.md new file mode 100644 index 000000000..77e71f8ea --- /dev/null +++ b/assets/markdown/suspicious_packages_fr.md @@ -0,0 +1,46 @@ +Un colis est considéré comme suspect s’il présente les caractéristiques suivantes : + +- Timbrage exagéré; +- Odeur suspecte; +- Bruit suspect; +- Aucune adresse de retour; +- Plusieurs mots mal orthographiés; +- Titre de la personne ou absence du nom du destinataire; +- Présence excessive de ruban; +- Livraison non prévue; +- Mention « privé », « à ouvrir seulement par ». + +**Note** + +Un colis peut être considéré comme contenant des matières nucléaires, radiologiques, biologiques ou chimiques s’il présente l’une des caractéristiques suivantes : + +- Il est détérioré; +- Il fuit; +- Il contient du liquide, de la poussière ou des granules; +- Il dégage une odeur anormale; +- Il émet des gaz ou de la vapeur; +- Il suinte ou bouillonne; +- Il contient des aérosols; +- Il démontre des signes de corrosion. + +## Marche à suivre + +- Éloignez-vous du colis; +- N’utilisez pas votre téléphone cellulaire à proximité du colis. Éteignez-le pour prévenir toute interférence avec le colis; +- Communiquez avec la Sécurité, au poste 55 (système téléphonique interne); +- Ne raccrochez pas tant que le répartiteur vous dit de le faire. + +## Évacuation + +- Attendez que la Sécurité ordonne l’évacuation. La Sécurité doit inspecter les voies d’évacuation pour s’assurer qu’il n’y a pas de danger (colis suspect); +- Restez calme; +- Habillez-vous en fonction de la température extérieure, mais ne faites pas de détour pour récupérer vos vêtements (par exemple : n’allez pas à votre casier); +- Évacuez l’endroit par les escaliers de secours les plus près. N’utilisez pas les ascenseurs; +- Éloignez-vous de l’immeuble; +- Dirigez-vous vers le lieu de rassemblement; +- Ne retournez sous aucun prétexte à l’intérieur. + +## Retour dans l’immeuble + +- Le retour dans l’immeuble se fera dès que la Sécurité l’autorisera; +- Cette autorisation sera communiquée au moyen d’un porte-voix au lieu de rassemblement. \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cfcaa4e1d..b6120c4d1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -329,14 +329,14 @@ SPEC CHECKSUMS: path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - rive_common: c537b4eed761e903a9403d93c347b69bd7a4762f - share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + rive_common: cbbac3192af00d7341f19dae2f26298e9e37d99e + share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyXMLParser: 027d9e6fb54a38d95dccec025bcea9693f699c47 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 + webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 PODFILE CHECKSUM: 18f1615a0bcd417392c9107b3e8dc59c76a68dac diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 053cbba47..7caba3e14 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -376,6 +376,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; @@ -783,6 +784,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -793,6 +795,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -864,6 +867,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -874,6 +878,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -919,6 +924,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -929,6 +935,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/l10n/intl_en.arb b/l10n/intl_en.arb index 538834452..6cc0bdbbe 100644 --- a/l10n/intl_en.arb +++ b/l10n/intl_en.arb @@ -258,25 +258,25 @@ "more_report_bug_step5": "mode on the right side. \n\nWhen in navigation mode, you can freely navigate in the app.\n\nTo switch to the drawing mode just press the 'Draw' button. Now you can draw on the screen.\n\nTo finish your feedback just write a description below and send it by pressing the 'Submit' button.", "security_bomb_threat_title": "Bomb threat", - "security_bomb_threat_detail": "bomb_threat_detail_en.html", + "security_bomb_threat_detail": "bomb_threat_en.md", "security_suspicious_packages_title": "Suspicious packages", - "security_suspicious_packages_detail": "suspicious_packages_detail_en.html", + "security_suspicious_packages_detail": "suspicious_packages_en.md", "security_evacuation_title": "Evacuation", - "security_evacuation_detail": "evacuation_detail_en.html", + "security_evacuation_detail": "evacuation_en.md", "security_gas_leak_title": "Gas leak", - "security_gas_leak_detail": "gas_leak_detail_en.html", + "security_gas_leak_detail": "gas_leak_en.md", "security_fire_title": "Fire", - "security_fire_detail": "fire_detail_en.html", + "security_fire_detail": "fire_en.md", "security_broken_elevator_title": "Broken elevator", - "security_broken_elevator_detail": "broken_elevator_detail_en.html", + "security_broken_elevator_detail": "broken_elevator_en.md", "security_electrical_outage_title": "Electrical outage", - "security_electrical_outage_detail": "electrical_outage_detail_en.html", + "security_electrical_outage_detail": "electrical_outage_en.md", "security_armed_person_title": "Armed person", - "security_armed_person_detail": "armed_person_detail_en.html", + "security_armed_person_detail": "armed_person_en.md", "security_earthquake_title": "Earthquake", - "security_earthquake_detail": "earthquake_detail_en.html", + "security_earthquake_detail": "earthquake_en.md", "security_medical_emergency_title": "Medical emergency", - "security_medical_emergency_detail": "medical_emergency_detail_en.html", + "security_medical_emergency_detail": "medical_emergency_en.md", "security_emergency_call": "Emergency call", "security_reach_security": "Reach security", diff --git a/l10n/intl_fr.arb b/l10n/intl_fr.arb index ce5d2fe1c..afcd66269 100644 --- a/l10n/intl_fr.arb +++ b/l10n/intl_fr.arb @@ -258,25 +258,25 @@ "more_report_bug_step5": "sur le côté droit. \n\nEn mode navigation, vous pouvez naviguer librement dans l'application.\n\nPour passer en mode dessin, appuyez simplement sur le bouton 'Dessiner'. Vous pouvez maintenant dessiner sur l'écran.\n\nPour terminer votre commentaire, écrivez simplement une description ci-dessous et envoyez-la en appuyant sur le bouton 'Soumettre'.", "security_bomb_threat_title": "Alerte à la bombe", - "security_bomb_threat_detail": "bomb_threat_detail_fr.html", + "security_bomb_threat_detail": "bomb_threat_fr.md", "security_suspicious_packages_title": "Colis suspect", - "security_suspicious_packages_detail": "suspicious_packages_detail_fr.html", + "security_suspicious_packages_detail": "suspicious_packages_fr.md", "security_evacuation_title": "Évacuation", - "security_evacuation_detail": "evacuation_detail_fr.html", + "security_evacuation_detail": "evacuation_fr.md", "security_gas_leak_title": "Fuite de gaz", - "security_gas_leak_detail": "gas_leak_detail_fr.html", + "security_gas_leak_detail": "gas_leak_fr.md", "security_fire_title": "Incendie", - "security_fire_detail": "fire_detail_fr.html", + "security_fire_detail": "fire_fr.md", "security_broken_elevator_title": "Panne d'ascenseur", - "security_broken_elevator_detail": "broken_elevator_detail_fr.html", + "security_broken_elevator_detail": "broken_elevator_fr.md", "security_electrical_outage_title": "Panne électrique", - "security_electrical_outage_detail": "electrical_outage_detail_fr.html", + "security_electrical_outage_detail": "electrical_outage_fr.md", "security_armed_person_title": "Personne armée", - "security_armed_person_detail": "armed_person_detail_fr.html", + "security_armed_person_detail": "armed_person_fr.md", "security_earthquake_title": "Tremblement de terre", - "security_earthquake_detail": "earthquake_detail_fr.html", + "security_earthquake_detail": "earthquake_fr.md", "security_medical_emergency_title": "Urgence médicale", - "security_medical_emergency_detail": "medical_emergency_detail_fr.html", + "security_medical_emergency_detail": "medical_emergency_fr.md", "security_emergency_call": "Appel d'urgence", "security_reach_security": "Joindre la sécurité", diff --git a/lib/constants/preferences_flags.dart b/lib/constants/preferences_flags.dart index 67ef95b64..5d5cb0110 100644 --- a/lib/constants/preferences_flags.dart +++ b/lib/constants/preferences_flags.dart @@ -35,13 +35,11 @@ enum PreferencesFlag { discoveryMore, // Dashboard flags - broadcastCard, aboutUsCard, scheduleCard, progressBarCard, gradesCard, progressBarText, - broadcastChange, // Rating flag ratingTimer, diff --git a/lib/features/app/error/outage/outage_view.dart b/lib/features/app/error/outage/outage_view.dart index af5230b1f..4f9e84195 100644 --- a/lib/features/app/error/outage/outage_view.dart +++ b/lib/features/app/error/outage/outage_view.dart @@ -3,131 +3,70 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:notredame/features/app/error/outage/widgets/outage_build_image.dart'; +import 'package:notredame/features/app/error/outage/widgets/outage_build_social_media.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/constants/urls.dart'; +import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/app/error/outage/outage_viewmodel.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/utils/utils.dart'; class OutageView extends StatelessWidget { @override - Widget build(BuildContext context) => ViewModelBuilder< - OutageViewModel>.nonReactive( - viewModelBuilder: () => OutageViewModel(), - builder: (context, model, child) => Scaffold( - backgroundColor: Utils.getColorByBrightness( - context, AppTheme.etsLightRed, AppTheme.primaryDark), - body: Stack( - children: [ - SafeArea( - minimum: const EdgeInsets.all(20), - child: Column( - children: [ - SizedBox( - height: model.getImagePlacement(context), - ), - Hero( - tag: 'ets_logo', - child: Image.asset( - "assets/animations/outage.gif", - excludeFromSemantics: true, - width: 500, - color: - Theme.of(context).brightness == Brightness.light - ? Colors.white - : AppTheme.etsLightRed, - )), - const SizedBox( - height: 15, - ), - SizedBox(height: model.getTextPlacement(context)), - Text( - AppIntl.of(context)!.service_outage, - textAlign: TextAlign.center, - style: - const TextStyle(fontSize: 18, color: Colors.white), - ), - SizedBox(height: model.getButtonPlacement(context)), - ElevatedButton( - onPressed: () { - model.tapRefreshButton(context); - }, - child: Text( - AppIntl.of(context)!.service_outage_refresh, - style: const TextStyle(fontSize: 17), - ), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - height: model.getContactTextPlacement(context), - child: Text( - AppIntl.of(context)!.service_outage_contact, - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - IconButton( - icon: const FaIcon( - FontAwesomeIcons.earthAmericas, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubWebsite, - AppIntl.of(context)!)), - IconButton( - icon: const FaIcon( - FontAwesomeIcons.github, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubGithub, AppIntl.of(context)!)), - IconButton( - icon: const FaIcon( - Icons.mail_outline, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubEmail, AppIntl.of(context)!)), - IconButton( - icon: const FaIcon( - FontAwesomeIcons.discord, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubDiscord, - AppIntl.of(context)!)), - ], + Widget build(BuildContext context) => + ViewModelBuilder.nonReactive( + viewModelBuilder: () => OutageViewModel(), + builder: (context, model, child) => Scaffold( + backgroundColor: Utils.getColorByBrightness( + context, AppTheme.etsLightRed, AppTheme.primaryDark), + body: Stack( + children: [ + SafeArea( + minimum: const EdgeInsets.all(20), + child: Column( + children: [ + SizedBox( + height: model.getImagePlacement(context), + ), + outageImageSection(context), + SizedBox(height: model.getTextPlacement(context)), + Text( + AppIntl.of(context)!.service_outage, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, color: Colors.white), + ), + SizedBox(height: model.getButtonPlacement(context)), + ElevatedButton( + onPressed: () { + model.tapRefreshButton(context); + }, + child: Text( + AppIntl.of(context)!.service_outage_refresh, + style: const TextStyle(fontSize: 17), ), - ], + ), + Expanded( + child: outageSocialSection(model, context), + ), + ], + ), + ), + SafeArea( + child: Align( + alignment: Alignment.topRight, + child: GestureDetector( + onTap: () => model.triggerTap(context), + child: Container( + width: 60, + height: 60, + color: Colors.transparent, + ), ), ), - ], - ), + ) + ], ), - SafeArea( - child: Align( - alignment: Alignment.topRight, - child: GestureDetector( - onTap: () => model.triggerTap(context), - child: Container( - width: 60, - height: 60, - color: Colors.transparent, - ), - ), - ), - ) - ], - ), - )); + )); } diff --git a/lib/features/app/error/outage/outage_viewmodel.dart b/lib/features/app/error/outage/outage_viewmodel.dart index d5e108892..3edac6aff 100644 --- a/lib/features/app/error/outage/outage_viewmodel.dart +++ b/lib/features/app/error/outage/outage_viewmodel.dart @@ -20,7 +20,7 @@ class OutageViewModel extends BaseViewModel { } double getTextPlacement(BuildContext context) { - return MediaQuery.of(context).size.height * 0.20; + return MediaQuery.of(context).size.height * 0.15; } double getButtonPlacement(BuildContext context) { @@ -28,7 +28,7 @@ class OutageViewModel extends BaseViewModel { } double getContactTextPlacement(BuildContext context) { - return MediaQuery.of(context).size.height * 0.04; + return MediaQuery.of(context).size.height * 0.10; } void tapRefreshButton(BuildContext context) { diff --git a/lib/features/app/error/outage/widgets/outage_build_image.dart b/lib/features/app/error/outage/widgets/outage_build_image.dart new file mode 100644 index 000000000..df19fc4b3 --- /dev/null +++ b/lib/features/app/error/outage/widgets/outage_build_image.dart @@ -0,0 +1,18 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/utils/app_theme.dart'; + +Widget outageImageSection(BuildContext context) { + return Hero( + tag: 'ets_logo', + child: Image.asset( + "assets/animations/outage.gif", + excludeFromSemantics: true, + width: 500, + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : AppTheme.etsLightRed, + )); +} diff --git a/lib/features/app/error/outage/widgets/outage_build_social_media.dart b/lib/features/app/error/outage/widgets/outage_build_social_media.dart new file mode 100644 index 000000000..2e3b22f4b --- /dev/null +++ b/lib/features/app/error/outage/widgets/outage_build_social_media.dart @@ -0,0 +1,61 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/error/outage/outage_viewmodel.dart'; +import 'package:notredame/utils/utils.dart'; + +Widget outageSocialSection(OutageViewModel model, BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + height: model.getContactTextPlacement(context), + child: Text( + AppIntl.of(context)!.service_outage_contact, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + IconButton( + icon: const FaIcon( + FontAwesomeIcons.earthAmericas, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubWebsite, AppIntl.of(context)!)), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.github, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubGithub, AppIntl.of(context)!)), + IconButton( + icon: const FaIcon( + Icons.mail_outline, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubEmail, AppIntl.of(context)!)), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.discord, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubDiscord, AppIntl.of(context)!)), + ], + ), + ], + ); +} diff --git a/lib/features/app/navigation/router.dart b/lib/features/app/navigation/router.dart index 2bebe4fc2..fa4473472 100644 --- a/lib/features/app/navigation/router.dart +++ b/lib/features/app/navigation/router.dart @@ -55,20 +55,26 @@ Route generateRoute(RouteSettings routeSettings) { return PageRouteBuilder( settings: RouteSettings( name: routeSettings.name, arguments: routeSettings.arguments), + transitionsBuilder: (_, animation, ___, child) => + rootPagesAnimation(animation, child), pageBuilder: (_, __, ___) => DashboardView(updateCode: code)); case RouterPaths.schedule: return PageRouteBuilder( settings: RouteSettings(name: routeSettings.name), + transitionsBuilder: (_, animation, ___, child) => + rootPagesAnimation(animation, child), pageBuilder: (_, __, ___) => const ScheduleView()); case RouterPaths.defaultSchedule: - return PageRouteBuilder( + return MaterialPageRoute( settings: RouteSettings( name: routeSettings.name, arguments: routeSettings.arguments), - pageBuilder: (_, __, ___) => ScheduleDefaultView( + builder: (_) => ScheduleDefaultView( sessionCode: routeSettings.arguments as String?)); case RouterPaths.student: return PageRouteBuilder( settings: RouteSettings(name: routeSettings.name), + transitionsBuilder: (_, animation, ___, child) => + rootPagesAnimation(animation, child), pageBuilder: (_, __, ___) => StudentView()); case RouterPaths.gradeDetails: return MaterialPageRoute( @@ -78,6 +84,8 @@ Route generateRoute(RouteSettings routeSettings) { case RouterPaths.ets: return PageRouteBuilder( settings: RouteSettings(name: routeSettings.name), + transitionsBuilder: (_, animation, ___, child) => + rootPagesAnimation(animation, child), pageBuilder: (_, __, ___) => ETSView()); case RouterPaths.usefulLinks: return PageRouteBuilder( @@ -111,6 +119,8 @@ Route generateRoute(RouteSettings routeSettings) { case RouterPaths.more: return PageRouteBuilder( settings: RouteSettings(name: routeSettings.name), + transitionsBuilder: (_, animation, ___, child) => + rootPagesAnimation(animation, child), pageBuilder: (_, __, ___) => MoreView()); case RouterPaths.settings: return MaterialPageRoute( @@ -139,3 +149,17 @@ Route generateRoute(RouteSettings routeSettings) { NotFoundView(pageName: routeSettings.name)); } } + +Widget rootPagesAnimation(Animation animation, Widget child) { + return Align( + child: FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: CurvedAnimation( + curve: Curves.easeIn, + parent: animation, + ), + child: child, + ), + )); +} diff --git a/lib/features/app/presentation/webview_controller_extension.dart b/lib/features/app/presentation/webview_controller_extension.dart deleted file mode 100644 index 6b98725f8..000000000 --- a/lib/features/app/presentation/webview_controller_extension.dart +++ /dev/null @@ -1,40 +0,0 @@ -// Flutter imports: -import 'package:flutter/services.dart'; - -// Package imports: -import 'package:webview_flutter/webview_flutter.dart'; - -extension type WebViewControllerExtension(WebViewController controller) - implements WebViewController { - /// used to load the emergency procedures html files inside the webView - Future loadHtmlFromAssets(String filename, Brightness brightness) async { - final String fileText = await rootBundle.loadString(filename); - final String data = darkMode(scaleText(fileText), brightness); - - await loadHtmlString(data); - } - - /// used to add dark theme to emergency procedures html files - String darkMode(String fileText, Brightness brightness) { - String colorFileText = fileText; - if (brightness == Brightness.dark) { - colorFileText = colorFileText.replaceAll('', - ''); - } - - return colorFileText; - } - - String scaleText(String fileText) { - return fileText.replaceAll( - '', - // ignore: missing_whitespace_between_adjacent_strings - ""); - } -} diff --git a/lib/features/app/repository/author_repository.dart b/lib/features/app/repository/author_repository.dart index 6c0f06782..b2b8dae8b 100644 --- a/lib/features/app/repository/author_repository.dart +++ b/lib/features/app/repository/author_repository.dart @@ -3,6 +3,8 @@ import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; import 'package:notredame/utils/locator.dart'; +// Project imports: + /// Repository to access authors class AuthorRepository { static const String tag = "AuthorRepository"; diff --git a/lib/features/app/widgets/base_scaffold.dart b/lib/features/app/widgets/base_scaffold.dart index 7a31e400b..cfbccee3c 100644 --- a/lib/features/app/widgets/base_scaffold.dart +++ b/lib/features/app/widgets/base_scaffold.dart @@ -10,8 +10,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: import 'package:notredame/features/app/integration/networking_service.dart'; -import 'package:notredame/features/app/widgets/navigation_rail.dart'; import 'package:notredame/features/app/widgets/bottom_bar.dart'; +import 'package:notredame/features/app/widgets/navigation_rail.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/loading.dart'; import 'package:notredame/utils/locator.dart'; diff --git a/lib/features/dashboard/dashboard_view.dart b/lib/features/dashboard/dashboard_view.dart index 71617d41c..136d8cf5f 100644 --- a/lib/features/dashboard/dashboard_view.dart +++ b/lib/features/dashboard/dashboard_view.dart @@ -7,7 +7,6 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:stacked/stacked.dart'; @@ -18,6 +17,7 @@ import 'package:notredame/constants/urls.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:notredame/features/app/signets-api/models/course_activity.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/app/widgets/dismissible_card.dart'; @@ -80,6 +80,10 @@ class _DashboardViewState extends State data: Theme.of(context) .copyWith(canvasColor: Colors.transparent), child: ReorderableListView( + header: + model.remoteConfigService.dashboardMessageActive + ? _buildMessageBroadcastCard(model) + : null, onReorder: (oldIndex, newIndex) => onReorder(model, oldIndex, newIndex), padding: const EdgeInsets.fromLTRB(0, 4, 0, 24), @@ -96,16 +100,11 @@ class _DashboardViewState extends State List _buildCards(DashboardViewModel model) { final List cards = List.empty(growable: true); - // always try to build broadcast cart so the user doesn't miss out on // important info if they dismissed it previously for (final PreferencesFlag element in model.cardsToDisplay ?? []) { switch (element) { - case PreferencesFlag.broadcastCard: - if (model.remoteConfigService.dashboardMessageActive) { - cards.add(_buildMessageBroadcastCard(model, element)); - } case PreferencesFlag.aboutUsCard: cards.add(_buildAboutUsCard(model, element)); case PreferencesFlag.scheduleCard: @@ -114,7 +113,6 @@ class _DashboardViewState extends State cards.add(_buildProgressBarCard(model, element)); case PreferencesFlag.gradesCard: cards.add(_buildGradesCards(model, element)); - default: } @@ -459,43 +457,48 @@ class _DashboardViewState extends State ); } - Widget _buildMessageBroadcastCard( - DashboardViewModel model, PreferencesFlag flag) { + Widget _buildMessageBroadcastCard(DashboardViewModel model) { + if (model.broadcastMessage == "" || + model.broadcastColor == "" || + model.broadcastTitle == "") { + return const SizedBox.shrink(); + } final broadcastMsgColor = Color(int.parse(model.broadcastColor)); final broadcastMsgType = model.broadcastType; final broadcastMsgUrl = model.broadcastUrl; - return DismissibleCard( + return Card( key: UniqueKey(), - onDismissed: (DismissDirection direction) { - dismissCard(model, flag); - }, - isBusy: model.busy(model.broadcastMessage), - cardColor: broadcastMsgColor, + color: broadcastMsgColor, child: Padding( padding: const EdgeInsets.fromLTRB(17, 10, 15, 20), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - // title row - Row( - children: [ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Text(model.broadcastTitle, - style: Theme.of(context).primaryTextTheme.titleLarge), - ), - ), - Align( - alignment: Alignment.centerRight, - child: InkWell( - child: getBroadcastIcon(broadcastMsgType, broadcastMsgUrl), + child: model.busy(model.broadcastMessage) + ? const Center(child: CircularProgressIndicator()) + : Column(mainAxisSize: MainAxisSize.min, children: [ + // title row + Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Text(model.broadcastTitle, + style: Theme.of(context) + .primaryTextTheme + .titleLarge), + ), + ), + Align( + alignment: Alignment.centerRight, + child: InkWell( + child: getBroadcastIcon( + broadcastMsgType, broadcastMsgUrl), + ), + ), + ], ), - ), - ], - ), - // main text - AutoSizeText(model.broadcastMessage, - style: Theme.of(context).primaryTextTheme.bodyMedium) - ]), + // main text + AutoSizeText(model.broadcastMessage, + style: Theme.of(context).primaryTextTheme.bodyMedium) + ]), )); } diff --git a/lib/features/dashboard/dashboard_viewmodel.dart b/lib/features/dashboard/dashboard_viewmodel.dart index 92c00020b..804c31fac 100644 --- a/lib/features/dashboard/dashboard_viewmodel.dart +++ b/lib/features/dashboard/dashboard_viewmodel.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:stacked/stacked.dart'; // Project imports: @@ -36,7 +37,6 @@ class DashboardViewModel extends FutureViewModel> { static const String tag = "DashboardViewModel"; final SettingsManager _settingsManager = locator(); - final PreferencesService _preferencesService = locator(); final CourseRepository _courseRepository = locator(); final AnalyticsService _analyticsService = locator(); final RemoteConfigService remoteConfigService = @@ -197,9 +197,27 @@ class DashboardViewModel extends FutureViewModel> { Future> futureToRun() async { final dashboard = await _settingsManager.getDashboard(); - _cards = dashboard; + //TODO: remove when all users are on 4.50.1 or more + final sharedPreferences = await SharedPreferences.getInstance(); + if (sharedPreferences.containsKey("PreferencesFlag.broadcastChange")) { + sharedPreferences.remove("PreferencesFlag.broadcastChange"); + } + if (sharedPreferences.containsKey("PreferencesFlag.broadcastCard")) { + sharedPreferences.remove("PreferencesFlag.broadcastCard"); + } + final sortedList = dashboard.entries.toList() + ..sort((a, b) => a.value.compareTo(b.value)); + final sortedDashboard = LinkedHashMap.fromEntries(sortedList); + int index = 0; + for (final element in sortedDashboard.entries) { + if (element.value >= 0) { + sortedDashboard.update(element.key, (value) => index); + index++; + } + } + //TODO: end remove when all users are on 4.50.1 or more - await checkForBroadcastChange(); + _cards = sortedDashboard; getCardsToDisplay(); @@ -207,7 +225,7 @@ class DashboardViewModel extends FutureViewModel> { // (moved from getCardsToDisplay()) await loadDataAndUpdateWidget(); - return dashboard; + return sortedDashboard; } Future loadDataAndUpdateWidget() async { @@ -254,13 +272,13 @@ class DashboardViewModel extends FutureViewModel> { void setAllCardsVisible() { _cards?.updateAll((key, value) { _settingsManager - .setInt(key, key.index - PreferencesFlag.broadcastCard.index) + .setInt(key, key.index - PreferencesFlag.aboutUsCard.index) .then((value) { if (!value) { Fluttertoast.showToast(msg: _appIntl.error); } }); - return key.index - PreferencesFlag.broadcastCard.index; + return key.index - PreferencesFlag.aboutUsCard.index; }); getCardsToDisplay(); @@ -288,23 +306,6 @@ class DashboardViewModel extends FutureViewModel> { _analyticsService.logEvent(tag, "Restoring cards"); } - Future checkForBroadcastChange() async { - final broadcastChange = - await _preferencesService.getString(PreferencesFlag.broadcastChange) ?? - ""; - if (broadcastChange != remoteConfigService.dashboardMessageEn) { - // Update pref - _preferencesService.setString(PreferencesFlag.broadcastChange, - remoteConfigService.dashboardMessageEn); - if (_cards != null && _cards![PreferencesFlag.broadcastCard]! < 0) { - _cards?.updateAll((key, value) { - return value >= 0 ? value + 1 : value; - }); - _cards![PreferencesFlag.broadcastCard] = 0; - } - } - } - Future> futureToRunSessionProgressBar() async { try { final progressBarText = diff --git a/lib/features/dashboard/widgets-dashboard/about_us_card.dart b/lib/features/dashboard/widgets-dashboard/about_us_card.dart new file mode 100644 index 000000000..1fdf5a38f --- /dev/null +++ b/lib/features/dashboard/widgets-dashboard/about_us_card.dart @@ -0,0 +1,117 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +// Project imports: +import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/widgets/dismissible_card.dart'; +import 'package:notredame/features/dashboard/dashboard_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; + +class AboutUsCard extends StatelessWidget { + final DashboardViewModel model; + final PreferencesFlag flag; + final AnalyticsService analyticsService; + static const String tag = "DashboardView"; + + const AboutUsCard({ + super.key, + required this.model, + required this.flag, + required this.analyticsService, + }); + + @override + Widget build(BuildContext context) { + return DismissibleCard( + key: UniqueKey(), + onDismissed: (DismissDirection direction) { + model.hideCard(flag); + }, + cardColor: AppTheme.appletsPurple, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align( + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.fromLTRB(17, 15, 0, 0), + child: Text(AppIntl.of(context)!.card_applets_title, + style: Theme.of(context).primaryTextTheme.titleLarge), + )), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(17, 10, 15, 10), + child: Text(AppIntl.of(context)!.card_applets_text, + style: Theme.of(context).primaryTextTheme.bodyMedium), + ), + Container( + padding: const EdgeInsets.fromLTRB(10, 0, 0, 0), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Wrap(spacing: 15.0, children: [ + IconButton( + onPressed: () { + analyticsService.logEvent(tag, "Facebook clicked"); + Utils.launchURL(Urls.clubFacebook, AppIntl.of(context)!); + }, + icon: const FaIcon( + FontAwesomeIcons.facebook, + color: Colors.white, + ), + ), + IconButton( + onPressed: () { + analyticsService.logEvent(tag, "Instagram clicked"); + Utils.launchURL(Urls.clubInstagram, AppIntl.of(context)!); + }, + icon: const FaIcon( + FontAwesomeIcons.instagram, + color: Colors.white, + ), + ), + IconButton( + onPressed: () { + analyticsService.logEvent(tag, "Github clicked"); + Utils.launchURL(Urls.clubGithub, AppIntl.of(context)!); + }, + icon: const FaIcon( + FontAwesomeIcons.github, + color: Colors.white, + ), + ), + IconButton( + onPressed: () { + analyticsService.logEvent(tag, "Email clicked"); + Utils.launchURL(Urls.clubEmail, AppIntl.of(context)!); + }, + icon: const FaIcon( + FontAwesomeIcons.envelope, + color: Colors.white, + ), + ), + IconButton( + onPressed: () { + analyticsService.logEvent(tag, "Discord clicked"); + Utils.launchURL(Urls.clubDiscord, AppIntl.of(context)!); + }, + icon: const FaIcon( + FontAwesomeIcons.discord, + color: Colors.white, + ), + ), + ]), + ), + ), + ], + ), + ]), + ); + } +} diff --git a/lib/features/dashboard/widgets-dashboard/grades_card.dart b/lib/features/dashboard/widgets-dashboard/grades_card.dart new file mode 100644 index 000000000..ffc27ca2c --- /dev/null +++ b/lib/features/dashboard/widgets-dashboard/grades_card.dart @@ -0,0 +1,79 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/widgets/dismissible_card.dart'; +import 'package:notredame/features/dashboard/dashboard_viewmodel.dart'; +import 'package:notredame/features/student/grades/widgets/grade_button.dart'; +import 'package:notredame/utils/app_theme.dart'; + +class GradesCard extends StatelessWidget { + final DashboardViewModel model; + final PreferencesFlag flag; + final VoidCallback dismissCard; + + const GradesCard({ + required this.model, + required this.flag, + required this.dismissCard, + super.key, + }); + + @override + Widget build(BuildContext context) { + final NavigationService navigationService = NavigationService(); + + return DismissibleCard( + key: UniqueKey(), + onDismissed: (DismissDirection direction) { + dismissCard(); + }, + isBusy: model.busy(model.courses), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.fromLTRB(17, 15, 0, 0), + child: GestureDetector( + onTap: () => navigationService + .pushNamedAndRemoveUntil(RouterPaths.student), + child: Text(AppIntl.of(context)!.grades_title, + style: Theme.of(context).textTheme.titleLarge), + ), + ), + ), + if (model.courses.isEmpty) + SizedBox( + height: 100, + child: Center( + child: Text(AppIntl.of(context)! + .grades_msg_no_grades + .split("\n") + .first)), + ) + else + Container( + padding: const EdgeInsets.fromLTRB(17, 10, 15, 10), + child: Wrap( + children: model.courses + .map((course) => GradeButton(course, + color: + Theme.of(context).brightness == Brightness.light + ? AppTheme.lightThemeBackground + : AppTheme.darkThemeBackground)) + .toList(), + ), + ) + ]), + ); + } +} diff --git a/lib/features/dashboard/widgets-dashboard/message_broadcast_card.dart b/lib/features/dashboard/widgets-dashboard/message_broadcast_card.dart new file mode 100644 index 000000000..f418cdd62 --- /dev/null +++ b/lib/features/dashboard/widgets-dashboard/message_broadcast_card.dart @@ -0,0 +1,46 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/features/app/widgets/dismissible_card.dart'; + +class MessageBroadcastCard extends StatelessWidget { + final String title; + final String content; + final VoidCallback onDismissed; + final String broadcastColor; + + const MessageBroadcastCard({ + required this.title, + required this.content, + required this.onDismissed, + required this.broadcastColor, + super.key, + }); + + @override + Widget build(BuildContext context) { + final broadcastMsgColor = Color(int.parse(broadcastColor)); + return DismissibleCard( + key: UniqueKey(), + onDismissed: (DismissDirection direction) { + onDismissed(); + }, + cardColor: broadcastMsgColor, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align( + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.fromLTRB(17, 15, 0, 0), + child: Text(title, + style: Theme.of(context).primaryTextTheme.titleLarge), + )), + Container( + padding: const EdgeInsets.fromLTRB(17, 0, 17, 8), + child: Text(content, + style: Theme.of(context).primaryTextTheme.bodyMedium), + ), + ]), + ); + } +} diff --git a/lib/features/dashboard/widgets-dashboard/progress_bar_card.dart b/lib/features/dashboard/widgets-dashboard/progress_bar_card.dart new file mode 100644 index 000000000..152a36d9b --- /dev/null +++ b/lib/features/dashboard/widgets-dashboard/progress_bar_card.dart @@ -0,0 +1,102 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/features/app/widgets/dismissible_card.dart'; +import 'package:notredame/features/dashboard/dashboard_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; + +class ProgressBarCard extends StatefulWidget { + final DashboardViewModel model; + final PreferencesFlag flag; + final Text? progressBarText; + final VoidCallback dismissCard; + final VoidCallback changeProgressBarText; + final VoidCallback setText; + + const ProgressBarCard({ + required this.model, + required this.flag, + required this.progressBarText, + required this.dismissCard, + required this.changeProgressBarText, + required this.setText, + super.key, + }); + + @override + _ProgressBarCardState createState() => _ProgressBarCardState(); +} + +class _ProgressBarCardState extends State { + @override + Widget build(BuildContext context) { + return DismissibleCard( + isBusy: widget.model.busy(widget.model.progress), + key: UniqueKey(), + onDismissed: (DismissDirection direction) { + widget.dismissCard(); + }, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align( + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.fromLTRB(17, 15, 0, 0), + child: Text(AppIntl.of(context)!.progress_bar_title, + style: Theme.of(context).textTheme.titleLarge), + )), + if (widget.model.progress >= 0.0) + Stack(children: [ + Container( + padding: const EdgeInsets.fromLTRB(17, 10, 15, 20), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: GestureDetector( + onTap: () => setState(() { + widget.changeProgressBarText(); + widget.setText(); + }), + child: LinearProgressIndicator( + value: widget.model.progress, + minHeight: 30, + valueColor: const AlwaysStoppedAnimation( + AppTheme.gradeGoodMax), + backgroundColor: AppTheme.etsDarkGrey, + ), + ), + ), + ), + GestureDetector( + onTap: () => setState(() { + widget.changeProgressBarText(); + widget.setText(); + }), + child: Container( + padding: const EdgeInsets.only(top: 16), + child: Center( + child: widget.progressBarText ?? + Text( + AppIntl.of(context)!.progress_bar_message( + widget.model.sessionDays[0], + widget.model.sessionDays[1]), + style: const TextStyle(color: Colors.white), + ), + ), + ), + ), + ]) + else + Container( + padding: const EdgeInsets.all(16), + child: Center( + child: Text(AppIntl.of(context)!.session_without), + ), + ), + ]), + ); + } +} diff --git a/lib/features/dashboard/widgets-dashboard/schedule_card.dart b/lib/features/dashboard/widgets-dashboard/schedule_card.dart new file mode 100644 index 000000000..cf87ce8c6 --- /dev/null +++ b/lib/features/dashboard/widgets-dashboard/schedule_card.dart @@ -0,0 +1,84 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/widgets/dismissible_card.dart'; +import 'package:notredame/features/dashboard/dashboard_viewmodel.dart'; +import 'package:notredame/features/dashboard/widgets/course_activity_tile.dart'; + +class ScheduleCard extends StatelessWidget { + final DashboardViewModel model; + final PreferencesFlag flag; + final VoidCallback dismissCard; + final NavigationService navigationService; + + const ScheduleCard({ + required this.model, + required this.flag, + required this.dismissCard, + required this.navigationService, + super.key, + }); + + @override + Widget build(BuildContext context) { + var title = AppIntl.of(context)!.title_schedule; + if (model.todayDateEvents.isEmpty && model.tomorrowDateEvents.isNotEmpty) { + title = title + AppIntl.of(context)!.card_schedule_tomorrow; + } + return DismissibleCard( + isBusy: model.busy(model.todayDateEvents) || + model.busy(model.tomorrowDateEvents), + onDismissed: (DismissDirection direction) { + dismissCard(); + }, + key: UniqueKey(), + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align( + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.fromLTRB(17, 15, 0, 0), + child: GestureDetector( + onTap: () => navigationService + .pushNamedAndRemoveUntil(RouterPaths.schedule), + child: Text(title, + style: Theme.of(context).textTheme.titleLarge), + ), + )), + if (model.todayDateEvents.isEmpty) + if (model.tomorrowDateEvents.isEmpty) + SizedBox( + height: 100, + child: Center( + child: Text(AppIntl.of(context)!.schedule_no_event))) + else + _buildEventList(model.tomorrowDateEvents) + else + _buildEventList(model.todayDateEvents) + ]), + ), + ); + } + + Widget _buildEventList(List events) { + return ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + itemBuilder: (_, index) => + CourseActivityTile(events[index] as CourseActivity), + separatorBuilder: (_, index) => (index < events.length) + ? const Divider(thickness: 1, indent: 30, endIndent: 30) + : const SizedBox(), + itemCount: events.length); + } +} diff --git a/lib/features/dashboard/widgets/course_activity_tile.dart b/lib/features/dashboard/widgets/course_activity_tile.dart index a2ce1ca24..e3c6f639c 100644 --- a/lib/features/dashboard/widgets/course_activity_tile.dart +++ b/lib/features/dashboard/widgets/course_activity_tile.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:intl/intl.dart'; +import 'package:skeletonizer/skeletonizer.dart'; // Project imports: import 'package:notredame/features/app/signets-api/models/course_activity.dart'; -import 'package:skeletonizer/skeletonizer.dart'; class CourseActivityTile extends StatelessWidget { /// Course to display diff --git a/lib/features/ets/events/author/author_view.dart b/lib/features/ets/events/author/author_view.dart index cf86d6ca9..f4a0de293 100644 --- a/lib/features/ets/events/author/author_view.dart +++ b/lib/features/ets/events/author/author_view.dart @@ -3,356 +3,27 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/ets/events/api-client/models/news.dart'; -import 'package:notredame/features/ets/events/author/author_info_skeleton.dart'; import 'package:notredame/features/ets/events/author/author_viewmodel.dart'; +import 'package:notredame/features/ets/events/author/widget/author_info_widget.dart'; +import 'package:notredame/features/ets/events/author/widget/avatar_widget.dart'; +import 'package:notredame/features/ets/events/author/widget/back_button_widget.dart'; +import 'package:notredame/features/ets/events/author/widget/no_more_news_card_widget.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card_skeleton.dart'; -import 'package:notredame/features/ets/events/social/models/social_link.dart'; -import 'package:notredame/features/ets/events/social/social_links_card.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/utils/utils.dart'; -class AuthorView extends StatefulWidget { +class AuthorView extends StatelessWidget { final String authorId; - const AuthorView({required this.authorId}); + const AuthorView({required this.authorId, super.key}); - @override - State createState() => _AuthorViewState(); -} - -class _AuthorViewState extends State { - static const int _nbSkeletons = 3; - late String notifyBtnText; - - @override - Widget build(BuildContext context) => - ViewModelBuilder.reactive( - viewModelBuilder: () => AuthorViewModel( - authorId: widget.authorId, appIntl: AppIntl.of(context)!), - onViewModelReady: (model) { - model.fetchAuthorData(); - model.pagingController.addStatusListener((status) { - if (status == PagingStatus.subsequentPageError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - AppIntl.of(context)!.news_error_not_found, - ), - action: SnackBarAction( - label: AppIntl.of(context)!.retry, - onPressed: () => - model.pagingController.retryLastFailedRequest(), - ), - ), - ); - } - }); - }, - builder: (context, model, child) => BaseScaffold( - showBottomBar: false, - body: RefreshIndicator( - onRefresh: () => Future.sync( - () => model.pagingController.refresh(), - ), - child: Theme( - data: Theme.of(context) - .copyWith(canvasColor: Colors.transparent), - child: Padding( - padding: const EdgeInsets.only(top: 32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - _buildBackButton(), - _buildAuthorInfo(model), - _buildAvatar(model, widget.authorId), - ], - ), - Expanded( - child: PagedListView( - key: const Key("pagedListView"), - pagingController: model.pagingController, - padding: - const EdgeInsets.fromLTRB(0, 4, 0, 8), - builderDelegate: - PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => - NewsCard(item), - firstPageProgressIndicatorBuilder: - (context) => _buildSkeletonLoader(), - newPageProgressIndicatorBuilder: - (context) => NewsCardSkeleton(), - noMoreItemsIndicatorBuilder: (context) => - _buildNoMoreNewsCard(), - firstPageErrorIndicatorBuilder: (context) => - _buildError(model.pagingController), - ), - ), - ), - ], - )), - )), - )); - - Widget _buildBackButton() { - return IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.of(context).pop(), - ); - } - - Widget _buildAuthorInfo(AuthorViewModel model) { - notifyBtnText = getNotifyMeBtnText(model); - - final author = model.author; - - List socialLinks = []; - if (author != null) { - socialLinks = [ - if (author.email != null) - SocialLink(id: 0, name: 'Email', link: author.email!), - if (author.facebookLink != null) - SocialLink(id: 1, name: 'Facebook', link: author.facebookLink!), - if (author.instagramLink != null) - SocialLink(id: 2, name: 'Instagram', link: author.instagramLink!), - if (author.tikTokLink != null) - SocialLink(id: 3, name: 'TikTok', link: author.tikTokLink!), - if (author.xLink != null) - SocialLink(id: 4, name: 'X', link: author.xLink!), - if (author.redditLink != null) - SocialLink(id: 5, name: 'Reddit', link: author.redditLink!), - if (author.discordLink != null) - SocialLink(id: 6, name: 'Discord', link: author.discordLink!), - if (author.linkedInLink != null) - SocialLink(id: 7, name: 'LinkedIn', link: author.linkedInLink!), - ]; - } - - return Padding( - padding: const EdgeInsets.only(top: 76), - child: model.isBusy - ? AuthorInfoSkeleton() - : SizedBox( - width: double.infinity, - child: Card( - color: Utils.getColorByBrightnessNullable( - context, AppTheme.newsSecondaryColor, null), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16)), - key: UniqueKey(), - child: Container( - padding: const EdgeInsets.fromLTRB(32, 64, 32, 16), - child: Column( - children: [ - if (author?.organization != null || - author?.organization != "") - Text( - author?.organization ?? "", - style: const TextStyle(fontSize: 26), - ), - if (author?.organization != null && - author?.organization != "") - const SizedBox(height: 8), - if (author?.profileDescription != null && - author?.profileDescription != "") - Text( - author?.profileDescription ?? "", - style: TextStyle( - color: Utils.getColorByBrightness( - context, - AppTheme.etsDarkGrey, - AppTheme.newsSecondaryColor, - ), - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - if (author?.profileDescription != null && - author?.profileDescription != "") - const SizedBox(height: 8), - IconButton( - onPressed: () async { - await showModalBottomSheet( - isDismissible: true, - enableDrag: true, - isScrollControlled: true, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10), - topRight: Radius.circular(10), - ), - ), - builder: (context) => SocialLinks( - socialLinks: socialLinks, - ), - ); - }, - icon: FaIcon( - FontAwesomeIcons.link, - color: Utils.getColorByBrightness( - context, - AppTheme.newsAccentColorLight, - AppTheme.newsAccentColorDark, - ), - ), - style: ButtonStyle( - shape: - MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - backgroundColor: MaterialStateProperty.all( - Utils.getColorByBrightness( - context, - AppTheme.lightThemeBackground, - AppTheme.darkThemeBackground, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - } - - String getNotifyMeBtnText(AuthorViewModel model) { - return model.isNotified - ? AppIntl.of(context)!.news_author_dont_notify_me - : AppIntl.of(context)!.news_author_notify_me; - } - - Widget _buildAvatar(AuthorViewModel model, String authorId) { - return model.isBusy - ? AvatarSkeleton() - : Align( - alignment: Alignment.topCenter, - child: Padding( - padding: const EdgeInsets.only(top: 16), - child: SizedBox( - width: 120, - height: 120, - child: Hero( - tag: 'news_author_avatar', - child: CircleAvatar( - backgroundColor: Utils.getColorByBrightness(context, - AppTheme.lightThemeAccent, AppTheme.darkThemeAccent), - child: Stack( - fit: StackFit.expand, - children: [ - if (model.author?.avatarUrl != null && - model.author!.avatarUrl != "") - ClipRRect( - borderRadius: BorderRadius.circular(120), - child: Image.network( - model.author!.avatarUrl!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Center( - child: Text( - model.author?.organization - ?.substring(0, 1) ?? - '', - style: TextStyle( - fontSize: 56, - color: Utils.getColorByBrightness( - context, - Colors.black, - Colors.white)), - ), - ); - }, - )), - if (model.author?.avatarUrl == null || - model.author!.avatarUrl == "") - Center( - child: Text( - model.author?.organization?.substring(0, 1) ?? '', - style: TextStyle( - fontSize: 56, - color: Utils.getColorByBrightness( - context, Colors.black, Colors.white)), - ), - ), - ], - ), - ), - ), - ), - ), - ); - } - - Widget _buildSkeletonLoader() { - final Widget skeleton = NewsCardSkeleton(); - return Column(children: [ - for (var i = 0; i < _nbSkeletons; i++) skeleton, - ]); - } - - Widget _buildNoMoreNewsCard() { - return Column( - children: [ - const SizedBox(height: 16), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Divider(), - ), - const SizedBox(height: 16), - Card( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), - child: Row( - children: [ - const Icon(Icons.check, color: Colors.blue, size: 40), - const SizedBox(width: 16), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(AppIntl.of(context)!.news_no_more_card_title, - style: const TextStyle(fontSize: 24)), - const SizedBox(height: 16), - Text( - AppIntl.of(context)!.news_no_more_card, - textAlign: TextAlign.justify, - ), - ], - ), - ), - ], - ), - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildError(PagingController pagingController) { + Widget _buildErrorWidget( + PagingController pagingController, BuildContext context) { return Scaffold( body: SafeArea( minimum: const EdgeInsets.all(20), @@ -396,4 +67,77 @@ class _AuthorViewState extends State { ), ); } + + @override + Widget build(BuildContext context) { + return ViewModelBuilder.reactive( + viewModelBuilder: () => AuthorViewModel( + authorId: authorId, + appIntl: AppIntl.of(context)!, + ), + onViewModelReady: (model) { + model.fetchAuthorData(); + model.pagingController.addStatusListener((status) { + if (status == PagingStatus.subsequentPageError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppIntl.of(context)!.news_error_not_found), + action: SnackBarAction( + label: AppIntl.of(context)!.retry, + onPressed: () => + model.pagingController.retryLastFailedRequest(), + ), + ), + ); + } + }); + }, + builder: (context, model, child) { + return BaseScaffold( + showBottomBar: false, + body: RefreshIndicator( + onRefresh: () => + Future.sync(() => model.pagingController.refresh()), + child: Theme( + data: Theme.of(context).copyWith(canvasColor: Colors.transparent), + child: Padding( + padding: const EdgeInsets.only(top: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Stack( + children: [ + BackButtonWidget(), + AuthorInfoWidget(), + AvatarWidget(), + ], + ), + Expanded( + child: PagedListView( + key: const Key("pagedListView"), + pagingController: model.pagingController, + padding: const EdgeInsets.fromLTRB(0, 4, 0, 8), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => NewsCard(item), + firstPageProgressIndicatorBuilder: (context) => + NewsCardSkeleton(), + newPageProgressIndicatorBuilder: (context) => + NewsCardSkeleton(), + noMoreItemsIndicatorBuilder: (context) => + const NoMoreNewsCardWidget(), + firstPageErrorIndicatorBuilder: (context) => + _buildErrorWidget( + model.pagingController, context), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } } diff --git a/lib/features/ets/events/author/author_viewmodel.dart b/lib/features/ets/events/author/author_viewmodel.dart index 8858a1c42..8d7c513ed 100644 --- a/lib/features/ets/events/author/author_viewmodel.dart +++ b/lib/features/ets/events/author/author_viewmodel.dart @@ -15,15 +15,10 @@ class AuthorViewModel extends BaseViewModel implements Initialisable { final AuthorRepository _authorRepository = locator(); final NewsRepository _newsRepository = locator(); - /// Localization class of the application. final AppIntl appIntl; - final String authorId; - /// Author Organizer? _author; - - /// Return the author Organizer? get author => _author; final PagingController pagingController = @@ -36,7 +31,6 @@ class AuthorViewModel extends BaseViewModel implements Initialisable { @override Future initialise() async { - // This will be called when init state cycle runs pagingController.addPageRequestListener((pageKey) { fetchPage(pageKey); }); @@ -59,16 +53,17 @@ class AuthorViewModel extends BaseViewModel implements Initialisable { } void notifyMe() { - // TODO activate/deactivate notifications isNotified = !isNotified; if (isNotified) { Fluttertoast.showToast( - msg: appIntl.news_author_notified_for(author?.organization ?? ""), - toastLength: Toast.LENGTH_LONG); + msg: appIntl.news_author_notified_for(author?.organization ?? ""), + toastLength: Toast.LENGTH_LONG, + ); } else { Fluttertoast.showToast( - msg: appIntl.news_author_not_notified_for(author?.organization ?? ""), - toastLength: Toast.LENGTH_LONG); + msg: appIntl.news_author_not_notified_for(author?.organization ?? ""), + toastLength: Toast.LENGTH_LONG, + ); } } diff --git a/lib/features/ets/events/author/widget/author_info_widget.dart b/lib/features/ets/events/author/widget/author_info_widget.dart new file mode 100644 index 000000000..501ebc2a3 --- /dev/null +++ b/lib/features/ets/events/author/widget/author_info_widget.dart @@ -0,0 +1,116 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:stacked/stacked.dart'; + +// Project imports: +import 'package:notredame/features/ets/events/author/author_info_skeleton.dart'; +import 'package:notredame/features/ets/events/author/author_viewmodel.dart'; +import 'package:notredame/features/ets/events/social/models/social_link.dart'; +import 'package:notredame/features/ets/events/social/social_links_card.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; + +class AuthorInfoWidget extends ViewModelWidget { + const AuthorInfoWidget({super.key}); + + @override + Widget build(BuildContext context, AuthorViewModel model) { + final author = model.author; + + List socialLinks = []; + if (author != null) { + socialLinks = [ + if (author.email != null) + SocialLink(id: 0, name: 'Email', link: author.email!), + if (author.facebookLink != null) + SocialLink(id: 1, name: 'Facebook', link: author.facebookLink!), + if (author.instagramLink != null) + SocialLink(id: 2, name: 'Instagram', link: author.instagramLink!), + if (author.tikTokLink != null) + SocialLink(id: 3, name: 'TikTok', link: author.tikTokLink!), + if (author.xLink != null) + SocialLink(id: 4, name: 'X', link: author.xLink!), + if (author.redditLink != null) + SocialLink(id: 5, name: 'Reddit', link: author.redditLink!), + if (author.discordLink != null) + SocialLink(id: 6, name: 'Discord', link: author.discordLink!), + if (author.linkedInLink != null) + SocialLink(id: 7, name: 'LinkedIn', link: author.linkedInLink!), + ]; + } + + return Padding( + padding: const EdgeInsets.only(top: 76), + child: model.isBusy + ? AuthorInfoSkeleton() + : SizedBox( + width: double.infinity, + child: Card( + color: Utils.getColorByBrightnessNullable( + context, AppTheme.newsSecondaryColor, null), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + child: Container( + padding: const EdgeInsets.fromLTRB(32, 64, 32, 16), + child: Column( + children: [ + if (author?.organization != null || + author?.organization != "") + Text( + author?.organization ?? "", + style: const TextStyle(fontSize: 26), + ), + if (author?.organization != null && + author?.organization != "") + const SizedBox(height: 8), + if (author?.profileDescription != null && + author?.profileDescription != "") + Text( + author?.profileDescription ?? "", + style: TextStyle( + color: Utils.getColorByBrightness( + context, + AppTheme.etsDarkGrey, + AppTheme.newsSecondaryColor), + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + if (author?.profileDescription != null && + author?.profileDescription != "") + const SizedBox(height: 8), + IconButton( + onPressed: () async { + await showModalBottomSheet( + isDismissible: true, + enableDrag: true, + isScrollControlled: true, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10)), + ), + builder: (context) => + SocialLinks(socialLinks: socialLinks), + ); + }, + icon: FaIcon( + FontAwesomeIcons.link, + color: Utils.getColorByBrightness( + context, + AppTheme.newsAccentColorLight, + AppTheme.newsAccentColorDark), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/ets/events/author/widget/avatar_widget.dart b/lib/features/ets/events/author/widget/avatar_widget.dart new file mode 100644 index 000000000..5e7afb1a5 --- /dev/null +++ b/lib/features/ets/events/author/widget/avatar_widget.dart @@ -0,0 +1,78 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:stacked/stacked.dart'; + +// Project imports: +import 'package:notredame/features/ets/events/author/author_info_skeleton.dart'; +import 'package:notredame/features/ets/events/author/author_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; + +class AvatarWidget extends ViewModelWidget { + const AvatarWidget({super.key}); + + @override + Widget build(BuildContext context, AuthorViewModel model) { + return model.isBusy + ? AvatarSkeleton() + : Align( + alignment: Alignment.topCenter, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: SizedBox( + width: 120, + height: 120, + child: Hero( + tag: 'news_author_avatar', + child: CircleAvatar( + backgroundColor: Utils.getColorByBrightness(context, + AppTheme.lightThemeAccent, AppTheme.darkThemeAccent), + child: Stack( + fit: StackFit.expand, + children: [ + if (model.author?.avatarUrl != null && + model.author!.avatarUrl != "") + ClipRRect( + borderRadius: BorderRadius.circular(120), + child: Image.network( + model.author!.avatarUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Text( + model.author?.organization + ?.substring(0, 1) ?? + '', + style: TextStyle( + fontSize: 56, + color: Utils.getColorByBrightness( + context, + Colors.black, + Colors.white)), + ), + ); + }, + ), + ), + if (model.author?.avatarUrl == null || + model.author!.avatarUrl == "") + Center( + child: Text( + model.author?.organization?.substring(0, 1) ?? '', + style: TextStyle( + fontSize: 56, + color: Utils.getColorByBrightness( + context, Colors.black, Colors.white)), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/ets/events/author/widget/back_button_widget.dart b/lib/features/ets/events/author/widget/back_button_widget.dart new file mode 100644 index 000000000..74fb0d1be --- /dev/null +++ b/lib/features/ets/events/author/widget/back_button_widget.dart @@ -0,0 +1,14 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class BackButtonWidget extends StatelessWidget { + const BackButtonWidget({super.key}); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ); + } +} diff --git a/lib/features/ets/events/author/widget/no_more_news_card_widget.dart b/lib/features/ets/events/author/widget/no_more_news_card_widget.dart new file mode 100644 index 000000000..fd1f089d0 --- /dev/null +++ b/lib/features/ets/events/author/widget/no_more_news_card_widget.dart @@ -0,0 +1,59 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class NoMoreNewsCardWidget extends StatelessWidget { + const NoMoreNewsCardWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 16), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Divider(), + ), + const SizedBox(height: 16), + Card( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), + child: Row( + children: [ + const Icon(Icons.check, color: Colors.blue, size: 40), + const SizedBox(width: 16), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppIntl.of(context)!.news_no_more_card_title, + style: const TextStyle(fontSize: 24)), + const SizedBox(height: 16), + Text( + AppIntl.of(context)!.news_no_more_card, + textAlign: TextAlign.justify, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/ets/events/news/news-details/news_details_view.dart b/lib/features/ets/events/news/news-details/news_details_view.dart index d2530c8ea..9d48e554e 100644 --- a/lib/features/ets/events/news/news-details/news_details_view.dart +++ b/lib/features/ets/events/news/news-details/news_details_view.dart @@ -3,22 +3,22 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:intl/intl.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:shimmer/shimmer.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; -import 'package:notredame/features/ets/events/api-client/models/activity_area.dart'; import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/features/ets/events/news/news-details/news_details_viewmodel.dart'; +import 'package:notredame/features/ets/events/news/news-details/widgets/news_details_build_author.dart'; +import 'package:notredame/features/ets/events/news/news-details/widgets/news_details_build_content.dart'; +import 'package:notredame/features/ets/events/news/news-details/widgets/news_details_build_date.dart'; +import 'package:notredame/features/ets/events/news/news-details/widgets/news_details_build_image.dart'; +import 'package:notredame/features/ets/events/news/news-details/widgets/news_details_build_tags.dart'; +import 'package:notredame/features/ets/events/news/news-details/widgets/news_details_build_title.dart'; import 'package:notredame/features/ets/events/report-news/report_news_widget.dart'; import 'package:notredame/features/schedule/calendar_selection_viewmodel.dart'; import 'package:notredame/features/schedule/widgets/calendar_selector.dart'; @@ -38,7 +38,6 @@ class NewsDetailsView extends StatefulWidget { enum Menu { share, export, report } class _NewsDetailsViewState extends State { - final NavigationService _navigationService = locator(); final AnalyticsService _analyticsService = locator(); final RemoteConfigService _remoteConfigService = locator(); @@ -138,26 +137,27 @@ class _NewsDetailsViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildTitle(widget.news.title), - _buildDate( + newsTitleSection(widget.news.title, context), + newsDateSection( context, widget.news.publicationDate, widget.news.eventStartDate, widget.news.eventEndDate), - _buildImage(widget.news), - _buildAuthor( + newsImageSection(widget.news), + newsAuthorSection( widget.news.organizer.avatarUrl ?? "", widget.news.organizer.organization ?? "", widget.news.organizer.activityArea, - widget.news.organizer.id), - _buildContent(widget.news.content), + widget.news.organizer.id, + context), + newsContentSection(widget.news.content), ], ), ), ], ), ), - _buildTags(model), + newsTagSection(model, widget.news), ], ), ), @@ -180,8 +180,6 @@ class _NewsDetailsViewState extends State { ); case Menu.report: showModalBottomSheet( - isDismissible: true, - enableDrag: true, isScrollControlled: true, context: context, shape: const RoundedRectangleBorder( @@ -193,269 +191,4 @@ class _NewsDetailsViewState extends State { )); } } - - Widget _buildContent(String content) { - // TODO : Support underline - String modifiedContent = content.replaceAll('', ""); - modifiedContent = modifiedContent.replaceAll('', ""); - - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: MarkdownBody(data: modifiedContent), - ), - ); - } - - Widget _buildTitle(String title) { - return Padding( - padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0), - child: Text( - title, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: - Utils.getColorByBrightness(context, Colors.black, Colors.white), - fontSize: 25, - fontWeight: FontWeight.bold), - ), - ); - } - - Widget _buildImage(News news) { - var isLoaded = false; - return Hero( - tag: 'news_image_id_${news.id}', - child: (news.imageUrl == null || news.imageUrl == "") - ? const SizedBox.shrink() - : Image.network( - news.imageUrl!, - fit: BoxFit.cover, - frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { - isLoaded = frame != null; - return child; - }, - loadingBuilder: (context, child, loadingProgress) { - if (isLoaded && loadingProgress == null) { - return child; - } else { - return const ShimmerEffect(); - } - }, - ), - ); - } - - Widget _buildAuthor( - String avatar, String author, ActivityArea? activity, String authorId) { - return ColoredBox( - color: Utils.getColorByBrightness( - context, AppTheme.etsLightRed, AppTheme.darkThemeBackgroundAccent), - child: ListTile( - leading: GestureDetector( - onTap: () => _navigationService.pushNamed(RouterPaths.newsAuthor, - arguments: authorId), - child: Hero( - tag: 'news_author_avatar', - child: CircleAvatar( - radius: 26, - backgroundColor: Utils.getColorByBrightness(context, - AppTheme.lightThemeAccent, AppTheme.darkThemeAccent), - child: (avatar != "") - ? ClipRRect( - borderRadius: BorderRadius.circular(26), - child: Image.network( - avatar, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Center( - child: Text( - author.substring(0, 1), - style: TextStyle( - fontSize: 24, - color: Utils.getColorByBrightness( - context, Colors.black, Colors.white)), - ), - ); - }, - )) - : Stack( - fit: StackFit.expand, - children: [ - Center( - child: Text( - author.substring(0, 1), - style: TextStyle( - fontSize: 24, - color: Utils.getColorByBrightness( - context, Colors.black, Colors.white)), - ), - ), - ], - ), - ))), - title: Text( - author, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - fontSize: 20, - ), - ), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - activity != null - ? Utils.getMessageByLocale( - context, activity.nameFr, activity.nameEn) - : "", - style: TextStyle( - color: Utils.getColorByBrightness( - context, Colors.white, const Color(0xffbababa)), - fontSize: 16, - ), - ), - ), - ), - ); - } - - Widget _buildDate(BuildContext context, DateTime publishedDate, - DateTime eventStartDate, DateTime? eventEndDate) { - final String locale = Localizations.localeOf(context).toString(); - final String formattedPublishedDate = - DateFormat('d MMMM yyyy', locale).format(publishedDate); - - late String formattedEventDate; - - final bool sameMonthAndYear = eventEndDate?.month == eventStartDate.month && - eventEndDate?.year == eventStartDate.year; - final bool sameDayMonthAndYear = - eventEndDate?.day == eventStartDate.day && sameMonthAndYear; - - if (eventEndDate == null || sameDayMonthAndYear) { - formattedEventDate = - DateFormat('d MMMM yyyy', locale).format(eventStartDate); - } else { - if (sameMonthAndYear) { - formattedEventDate = - '${DateFormat('d', locale).format(eventStartDate)} - ${DateFormat('d MMMM yyyy', locale).format(eventEndDate)}'; - } else { - formattedEventDate = - '${DateFormat('d MMMM yyyy', locale).format(eventStartDate)} -\n${DateFormat('d MMMM yyyy', locale).format(eventEndDate)}'; - } - } - - return Padding( - padding: const EdgeInsets.fromLTRB(16.0, 5.0, 16.0, 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - formattedPublishedDate, - style: TextStyle( - color: Utils.getColorByBrightness( - context, Colors.black, Colors.white)), - ), - const SizedBox(height: 12.0), - ], - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Utils.getColorByBrightness(context, - AppTheme.darkThemeAccent, AppTheme.etsDarkGrey), - shape: BoxShape.circle, - ), - child: - const Icon(Icons.event, size: 20.0, color: Colors.white), - ), - Flexible( - child: Padding( - padding: const EdgeInsets.only(left: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppIntl.of(context)!.news_event_date, - style: TextStyle( - color: Utils.getColorByBrightness( - context, Colors.black, AppTheme.etsLightGrey)), - textAlign: TextAlign.right, - ), - Text( - formattedEventDate, - style: TextStyle( - color: Utils.getColorByBrightness(context, - AppTheme.darkThemeAccent, Colors.white)), - textAlign: TextAlign.right, - ), - ], - ), - )), - ], - ), - ), - ], - ), - ); - } - - Widget _buildTags(NewsDetailsViewModel model) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: List.generate( - widget.news.tags.length, - (index) => Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: model.getTagColor(widget.news.tags[index].name), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - widget.news.tags[index].name, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ], - ), - ); - } -} - -class ShimmerEffect extends StatelessWidget { - const ShimmerEffect({super.key}); - - @override - Widget build(BuildContext context) { - return Shimmer.fromColors( - baseColor: Theme.of(context).brightness == Brightness.light - ? AppTheme.lightThemeBackground - : AppTheme.darkThemeBackground, - highlightColor: Theme.of(context).brightness == Brightness.light - ? AppTheme.lightThemeAccent - : AppTheme.darkThemeAccent, - child: Container( - height: 200, - color: Colors.grey, - ), - ); - } } diff --git a/lib/features/ets/events/news/news-details/widgets/news_details_build_author.dart b/lib/features/ets/events/news/news-details/widgets/news_details_build_author.dart new file mode 100644 index 000000000..eef096cf8 --- /dev/null +++ b/lib/features/ets/events/news/news-details/widgets/news_details_build_author.dart @@ -0,0 +1,85 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/ets/events/api-client/models/activity_area.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; + +Widget newsAuthorSection(String avatar, String author, ActivityArea? activity, + String authorId, BuildContext context) { + final NavigationService navigationService = locator(); + return ColoredBox( + color: Utils.getColorByBrightness( + context, AppTheme.etsLightRed, AppTheme.darkThemeBackgroundAccent), + child: ListTile( + leading: GestureDetector( + onTap: () => navigationService.pushNamed(RouterPaths.newsAuthor, + arguments: authorId), + child: Hero( + tag: 'news_author_avatar', + child: CircleAvatar( + radius: 26, + backgroundColor: Utils.getColorByBrightness(context, + AppTheme.lightThemeAccent, AppTheme.darkThemeAccent), + child: (avatar != "") + ? ClipRRect( + borderRadius: BorderRadius.circular(26), + child: Image.network( + avatar, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Text( + author.substring(0, 1), + style: TextStyle( + fontSize: 24, + color: Utils.getColorByBrightness( + context, Colors.black, Colors.white)), + ), + ); + }, + )) + : Stack( + fit: StackFit.expand, + children: [ + Center( + child: Text( + author.substring(0, 1), + style: TextStyle( + fontSize: 24, + color: Utils.getColorByBrightness( + context, Colors.black, Colors.white)), + ), + ), + ], + ), + ))), + title: Text( + author, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 20, + ), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + activity != null + ? Utils.getMessageByLocale( + context, activity.nameFr, activity.nameEn) + : "", + style: TextStyle( + color: Utils.getColorByBrightness( + context, Colors.white, const Color(0xffbababa)), + fontSize: 16, + ), + ), + ), + ), + ); +} diff --git a/lib/features/ets/events/news/news-details/widgets/news_details_build_content.dart b/lib/features/ets/events/news/news-details/widgets/news_details_build_content.dart new file mode 100644 index 000000000..53ff2368c --- /dev/null +++ b/lib/features/ets/events/news/news-details/widgets/news_details_build_content.dart @@ -0,0 +1,22 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_markdown/flutter_markdown.dart'; + +// Package imports: + +// Project imports: + +Widget newsContentSection(String content) { + // TODO : Support underline + String modifiedContent = content.replaceAll('', ""); + modifiedContent = modifiedContent.replaceAll('', ""); + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: MarkdownBody(data: modifiedContent), + ), + ); +} diff --git a/lib/features/ets/events/news/news-details/widgets/news_details_build_date.dart b/lib/features/ets/events/news/news-details/widgets/news_details_build_date.dart new file mode 100644 index 000000000..33035a1ac --- /dev/null +++ b/lib/features/ets/events/news/news-details/widgets/news_details_build_date.dart @@ -0,0 +1,99 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; + +// Project imports: +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; + +// Project imports: + +Widget newsDateSection(BuildContext context, DateTime publishedDate, + DateTime eventStartDate, DateTime? eventEndDate) { + final String locale = Localizations.localeOf(context).toString(); + final String formattedPublishedDate = + DateFormat('d MMMM yyyy', locale).format(publishedDate); + + late String formattedEventDate; + + final bool sameMonthAndYear = eventEndDate?.month == eventStartDate.month && + eventEndDate?.year == eventStartDate.year; + final bool sameDayMonthAndYear = + eventEndDate?.day == eventStartDate.day && sameMonthAndYear; + + if (eventEndDate == null || sameDayMonthAndYear) { + formattedEventDate = + DateFormat('d MMMM yyyy', locale).format(eventStartDate); + } else { + if (sameMonthAndYear) { + formattedEventDate = + '${DateFormat('d', locale).format(eventStartDate)} - ${DateFormat('d MMMM yyyy', locale).format(eventEndDate)}'; + } else { + formattedEventDate = + '${DateFormat('d MMMM yyyy', locale).format(eventStartDate)} -\n${DateFormat('d MMMM yyyy', locale).format(eventEndDate)}'; + } + } + + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 5.0, 16.0, 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + formattedPublishedDate, + style: TextStyle( + color: Utils.getColorByBrightness( + context, Colors.black, Colors.white)), + ), + const SizedBox(height: 12.0), + ], + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Utils.getColorByBrightness( + context, AppTheme.darkThemeAccent, AppTheme.etsDarkGrey), + shape: BoxShape.circle, + ), + child: const Icon(Icons.event, size: 20.0, color: Colors.white), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppIntl.of(context)!.news_event_date, + style: TextStyle( + color: Utils.getColorByBrightness( + context, Colors.black, AppTheme.etsLightGrey)), + textAlign: TextAlign.right, + ), + Text( + formattedEventDate, + style: TextStyle( + color: Utils.getColorByBrightness( + context, AppTheme.darkThemeAccent, Colors.white)), + textAlign: TextAlign.right, + ), + ], + ), + )), + ], + ), + ), + ], + ), + ); +} diff --git a/lib/features/ets/events/news/news-details/widgets/news_details_build_image.dart b/lib/features/ets/events/news/news-details/widgets/news_details_build_image.dart new file mode 100644 index 000000000..d3d769cf8 --- /dev/null +++ b/lib/features/ets/events/news/news-details/widgets/news_details_build_image.dart @@ -0,0 +1,55 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:shimmer/shimmer.dart'; + +// Project imports: +import 'package:notredame/features/ets/events/api-client/models/news.dart'; +import 'package:notredame/utils/app_theme.dart'; + +// Project imports: + +Widget newsImageSection(News news) { + var isLoaded = false; + return Hero( + tag: 'news_image_id_${news.id}', + child: (news.imageUrl == null || news.imageUrl == "") + ? const SizedBox.shrink() + : Image.network( + news.imageUrl!, + fit: BoxFit.cover, + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + isLoaded = frame != null; + return child; + }, + loadingBuilder: (context, child, loadingProgress) { + if (isLoaded && loadingProgress == null) { + return child; + } else { + return const ShimmerEffect(); + } + }, + ), + ); +} + +class ShimmerEffect extends StatelessWidget { + const ShimmerEffect({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Theme.of(context).brightness == Brightness.light + ? AppTheme.lightThemeBackground + : AppTheme.darkThemeBackground, + highlightColor: Theme.of(context).brightness == Brightness.light + ? AppTheme.lightThemeAccent + : AppTheme.darkThemeAccent, + child: Container( + height: 200, + color: Colors.grey, + ), + ); + } +} diff --git a/lib/features/ets/events/news/news-details/widgets/news_details_build_tags.dart b/lib/features/ets/events/news/news-details/widgets/news_details_build_tags.dart new file mode 100644 index 000000000..5aa9bb962 --- /dev/null +++ b/lib/features/ets/events/news/news-details/widgets/news_details_build_tags.dart @@ -0,0 +1,42 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/features/ets/events/api-client/models/news.dart'; +import 'package:notredame/features/ets/events/news/news-details/news_details_viewmodel.dart'; + +// Package imports: + +// Project imports: + +Widget newsTagSection(NewsDetailsViewModel model, News news) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate( + news.tags.length, + (index) => Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: model.getTagColor(news.tags[index].name), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + news.tags[index].name, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ); +} diff --git a/lib/features/ets/events/news/news-details/widgets/news_details_build_title.dart b/lib/features/ets/events/news/news-details/widgets/news_details_build_title.dart new file mode 100644 index 000000000..48d0c0ca0 --- /dev/null +++ b/lib/features/ets/events/news/news-details/widgets/news_details_build_title.dart @@ -0,0 +1,21 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/utils/utils.dart'; + +// Project imports: + +Widget newsTitleSection(String title, BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0), + child: Text( + title, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: + Utils.getColorByBrightness(context, Colors.black, Colors.white), + fontSize: 25, + fontWeight: FontWeight.bold), + ), + ); +} diff --git a/lib/features/ets/events/news/news_view.dart b/lib/features/ets/events/news/news_view.dart index 839ed5549..8ef78dd7c 100644 --- a/lib/features/ets/events/news/news_view.dart +++ b/lib/features/ets/events/news/news_view.dart @@ -11,8 +11,8 @@ import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/features/ets/events/news/news_viewmodel.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card_skeleton.dart'; +import 'package:notredame/features/ets/events/news/widgets/news_search_bar.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/utils/utils.dart'; class NewsView extends StatefulWidget { @override @@ -23,7 +23,6 @@ class _NewsViewState extends State { static const int _nbSkeletons = 3; final ScrollController _scrollController = ScrollController(); bool _showBackToTopButton = false; - String _query = ""; @override void initState() { @@ -99,34 +98,7 @@ class _NewsViewState extends State { child: Row( children: [ Expanded( - child: SizedBox( - height: 52, - child: TextField( - decoration: InputDecoration( - hintText: - AppIntl.of(context)!.search, - filled: true, - fillColor: - Utils.getColorByBrightness( - context, - AppTheme.lightThemeAccent, - Theme.of(context) - .cardColor), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: - BorderRadius.circular(8.0), - ), - contentPadding: - const EdgeInsets.fromLTRB( - 16, 8, 16, 0)), - style: const TextStyle(fontSize: 18), - onEditingComplete: () => - {model.searchNews(_query)}, - onChanged: (query) { - _query = query; - }, - )), + child: NewsSearchBar(model: model), ), ], )), diff --git a/lib/features/ets/events/news/news_viewmodel.dart b/lib/features/ets/events/news/news_viewmodel.dart index d4e26fb30..7650fc4f6 100644 --- a/lib/features/ets/events/news/news_viewmodel.dart +++ b/lib/features/ets/events/news/news_viewmodel.dart @@ -1,3 +1,5 @@ +// Package imports: + // Package imports: import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:stacked/stacked.dart'; @@ -16,6 +18,7 @@ class NewsViewModel extends BaseViewModel implements Initialisable { bool isLoadingEvents = false; String title = ""; + String query = ""; @override void initialise() { diff --git a/lib/features/ets/events/news/widgets/news_card.dart b/lib/features/ets/events/news/widgets/news_card.dart index 677810449..1f69a4c97 100644 --- a/lib/features/ets/events/news/widgets/news_card.dart +++ b/lib/features/ets/events/news/widgets/news_card.dart @@ -13,6 +13,8 @@ import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/locator.dart'; +// Package imports: + class NewsCard extends StatefulWidget { final News news; diff --git a/lib/features/ets/events/news/widgets/news_search_bar.dart b/lib/features/ets/events/news/widgets/news_search_bar.dart new file mode 100644 index 000000000..8d6e1d4fe --- /dev/null +++ b/lib/features/ets/events/news/widgets/news_search_bar.dart @@ -0,0 +1,42 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/ets/events/news/news_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; + +class NewsSearchBar extends StatelessWidget { + const NewsSearchBar({ + super.key, + required this.model, + }); + + final NewsViewModel model; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 52, + child: TextField( + decoration: InputDecoration( + hintText: AppIntl.of(context)!.search, + filled: true, + fillColor: Utils.getColorByBrightness(context, + AppTheme.lightThemeAccent, Theme.of(context).cardColor), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8.0), + ), + contentPadding: const EdgeInsets.fromLTRB(16, 8, 16, 0)), + style: const TextStyle(fontSize: 18), + onEditingComplete: () => {model.searchNews(model.query)}, + onChanged: (query) { + model.query = query; + }, + )); + } +} diff --git a/lib/features/ets/events/report-news/report_news_viewmodel.dart b/lib/features/ets/events/report-news/report_news_viewmodel.dart index 694e4206c..26abad4c3 100644 --- a/lib/features/ets/events/report-news/report_news_viewmodel.dart +++ b/lib/features/ets/events/report-news/report_news_viewmodel.dart @@ -1,3 +1,5 @@ +// Package imports: + // Package imports: import 'package:stacked/stacked.dart'; diff --git a/lib/features/ets/quick-link/quick_links_view.dart b/lib/features/ets/quick-link/quick_links_view.dart index f691a531d..2a46a28ab 100644 --- a/lib/features/ets/quick-link/quick_links_view.dart +++ b/lib/features/ets/quick-link/quick_links_view.dart @@ -3,14 +3,11 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:reorderable_grid_view/reorderable_grid_view.dart'; +import 'package:notredame/features/ets/quick-link/widgets/quick_links_reorderable.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/quick_links_viewmodel.dart'; -import 'package:notredame/features/ets/quick-link/widgets/web_link_card.dart'; -import 'package:notredame/utils/app_theme.dart'; class QuickLinksView extends StatefulWidget { @override @@ -19,13 +16,16 @@ class QuickLinksView extends StatefulWidget { class _QuickLinksViewState extends State with SingleTickerProviderStateMixin { - // Enable/Disable the edit state - bool _editMode = false; - // Animation Controller for Shake Animation late AnimationController _controller; late Animation _animation; + void refresh(Function() function) { + setState(() { + function(); + }); + } + @override void initState() { super.initState(); @@ -50,28 +50,28 @@ class _QuickLinksViewState extends State Widget _buildBody(BuildContext context, QuickLinksViewModel model) { return GestureDetector( onTap: () { - if (_editMode) { + if (model.editMode) { _controller.reset(); setState(() { - _editMode = false; + model.editMode = false; }); } }, child: Column( children: [ Expanded( - child: _buildReorderableGridView( - model, model.quickLinkList, _buildDeleteButton), + child: quickLinksReorderableGridView(model, model.quickLinkList, + context, refresh, _controller, _animation, deleteButton: true, blockReorder: false), ), - if (_editMode && model.deletedQuickLinks.isNotEmpty) ...[ + if (model.editMode && model.deletedQuickLinks.isNotEmpty) ...[ const Divider( thickness: 2, indent: 10, endIndent: 10, ), Expanded( - child: _buildReorderableGridView( - model, model.deletedQuickLinks, _buildAddButton), + child: quickLinksReorderableGridView(model, model.deletedQuickLinks, + context, refresh, _controller, _animation, deleteButton: false, blockReorder: true), ), ], ], @@ -79,122 +79,6 @@ class _QuickLinksViewState extends State ); } - ReorderableGridView _buildReorderableGridView( - QuickLinksViewModel model, - List quickLinks, - Widget Function(QuickLinksViewModel, int) buildButtonFunction) { - final double screenWidth = MediaQuery.of(context).size.width; - int crossAxisCount; - - if (screenWidth > 310 && screenWidth < 440) { - crossAxisCount = 3; - } else { - crossAxisCount = - (screenWidth / 110).floor().clamp(1, double.infinity).toInt(); - } - - return ReorderableGridView.count( - padding: const EdgeInsets.all(8.0), - mainAxisSpacing: 2.0, - crossAxisSpacing: 2.0, - crossAxisCount: crossAxisCount, - children: List.generate( - quickLinks.length, - (index) { - return KeyedSubtree( - key: ValueKey(quickLinks[index].id), - child: - _buildGridChild(model, index, quickLinks, buildButtonFunction), - ); - }, - ), - onReorder: (oldIndex, newIndex) { - setState(() { - model.reorderQuickLinks(oldIndex, newIndex); - }); - }, - ); - } - - Widget _buildGridChild( - QuickLinksViewModel model, - int index, - List quickLinks, - Widget Function(QuickLinksViewModel, int) buildButtonFunction) { - return GestureDetector( - onLongPress: _editMode - ? null - : () { - _controller.repeat(reverse: true); - setState(() { - _editMode = true; - }); - }, - child: AnimatedBuilder( - animation: _animation, - builder: (BuildContext context, Widget? child) { - return Transform.rotate( - angle: _editMode ? _animation.value : 0, - child: child, - ); - }, - child: Stack( - children: [ - WebLinkCard(quickLinks[index]), - if (_editMode && - quickLinks[index].id != - 1) // Don't show delete button for Security QuickLink - Positioned( - top: 0, - left: 0, - child: buildButtonFunction(model, index), - ), - ], - ), - ), - ); - } - - Container _buildDeleteButton(QuickLinksViewModel model, int index) { - return Container( - width: 32, - height: 32, - decoration: const BoxDecoration( - color: AppTheme.etsDarkGrey, - shape: BoxShape.circle, - ), - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.close, color: Colors.white, size: 16), - onPressed: () { - setState(() { - model.deleteQuickLink(index); - }); - }, - ), - ); - } - - Container _buildAddButton(QuickLinksViewModel model, int index) { - return Container( - width: 32, - height: 32, - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.add, color: Colors.white, size: 20), - onPressed: () { - setState(() { - model.restoreQuickLink(index); - }); - }, - ), - ); - } - @override void dispose() { _controller.dispose(); diff --git a/lib/features/ets/quick-link/quick_links_viewmodel.dart b/lib/features/ets/quick-link/quick_links_viewmodel.dart index 607177c27..5b0460e1d 100644 --- a/lib/features/ets/quick-link/quick_links_viewmodel.dart +++ b/lib/features/ets/quick-link/quick_links_viewmodel.dart @@ -12,6 +12,9 @@ class QuickLinksViewModel extends FutureViewModel> { /// Localization class of the application. final AppIntl _appIntl; + // Enable/Disable the edit state + bool editMode = false; + /// used to get all links for ETS page List quickLinkList = []; diff --git a/lib/features/ets/quick-link/widgets/quick_links_reorderable.dart b/lib/features/ets/quick-link/widgets/quick_links_reorderable.dart new file mode 100644 index 000000000..7a65016dc --- /dev/null +++ b/lib/features/ets/quick-link/widgets/quick_links_reorderable.dart @@ -0,0 +1,146 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:reorderable_grid_view/reorderable_grid_view.dart'; + +// Project imports: +import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; +import 'package:notredame/features/ets/quick-link/quick_links_viewmodel.dart'; +import 'package:notredame/features/ets/quick-link/widgets/web_link_card.dart'; +import 'package:notredame/utils/app_theme.dart'; + +ReorderableGridView quickLinksReorderableGridView( + QuickLinksViewModel model, + List quickLinks, + BuildContext context, + Function(Function()) setState, + AnimationController controller, + Animation animation, + {required bool deleteButton, required bool blockReorder}) { + final double screenWidth = MediaQuery.of(context).size.width; + int crossAxisCount; + + if (screenWidth > 310 && screenWidth < 440) { + crossAxisCount = 3; + } else { + crossAxisCount = + (screenWidth / 110).floor().clamp(1, double.infinity).toInt(); + } + + return ReorderableGridView.count( + dragEnabled: !blockReorder, + padding: const EdgeInsets.all(8.0), + mainAxisSpacing: 2.0, + crossAxisSpacing: 2.0, + crossAxisCount: crossAxisCount, + dragWidgetBuilder: (index, widget) => + _buildGridChild(model, index, quickLinks, controller, animation, setState, deleteButton ? _buildDeleteButton : _buildAddButton), + children: List.generate( + quickLinks.length, + (index) { + return KeyedSubtree( + key: ValueKey(quickLinks[index].id), + child: _buildGridChild( + model, + index, + quickLinks, + controller, + animation, + setState, + deleteButton ? _buildDeleteButton : _buildAddButton), + ); + }, + ), + onReorder: (oldIndex, newIndex) { + setState(() { + model.reorderQuickLinks(oldIndex, newIndex); + }); + }, + ); +} + +Widget _buildGridChild( + QuickLinksViewModel model, + int index, + List quickLinks, + AnimationController controller, + Animation animation, + Function(Function()) setState, + Widget Function(QuickLinksViewModel, int, Function(Function())) + buildButtonFunction) { + return GestureDetector( + onLongPress: model.editMode + ? null + : () { + controller.repeat(reverse: true); + setState(() { + model.editMode = true; + }); + }, + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return Transform.rotate( + angle: model.editMode ? animation.value : 0, + child: child, + ); + }, + child: Stack( + children: [ + WebLinkCard(quickLinks[index]), + if (model.editMode && + quickLinks[index].id != + 1) // Don't show delete button for Security QuickLink + Positioned( + top: 0, + left: 0, + child: buildButtonFunction(model, index, setState), + ), + ], + ), + ), + ); +} + +Container _buildDeleteButton( + QuickLinksViewModel model, int index, Function(Function()) setState) { + return Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: AppTheme.etsDarkGrey, + shape: BoxShape.circle, + ), + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.close, color: Colors.white, size: 16), + onPressed: () { + setState(() { + model.deleteQuickLink(index); + }); + }, + ), + ); +} + +Container _buildAddButton( + QuickLinksViewModel model, int index, Function(Function()) setState) { + return Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.add, color: Colors.white, size: 20), + onPressed: () { + setState(() { + model.restoreQuickLink(index); + }); + }, + ), + ); +} diff --git a/lib/features/ets/quick-link/widgets/security-info/emergency_view.dart b/lib/features/ets/quick-link/widgets/security-info/emergency_view.dart index 8f10febb6..651861a01 100644 --- a/lib/features/ets/quick-link/widgets/security-info/emergency_view.dart +++ b/lib/features/ets/quick-link/widgets/security-info/emergency_view.dart @@ -1,14 +1,11 @@ // Flutter imports: import 'package:flutter/material.dart'; - -// Package imports: -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:webview_flutter/webview_flutter.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:notredame/features/app/widgets/base_scaffold.dart'; // Project imports: -import 'package:notredame/features/app/presentation/webview_controller_extension.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/utils/utils.dart'; +import 'package:notredame/features/ets/quick-link/widgets/security-info/widget/emergency_floating_button.dart'; class EmergencyView extends StatefulWidget { final String title; @@ -22,29 +19,28 @@ class EmergencyView extends StatefulWidget { class _EmergencyViewState extends State { @override - Widget build(BuildContext context) => Scaffold( + Widget build(BuildContext context) => BaseScaffold( appBar: AppBar(title: Text(widget.title)), - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: FloatingActionButton.extended( - onPressed: () { - Utils.launchURL( - 'tel:${AppIntl.of(context)!.security_emergency_number}', - AppIntl.of(context)!) - .catchError((error) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(error.toString()))); - }); - }, - label: Text( - AppIntl.of(context)!.security_reach_security, - style: const TextStyle(color: Colors.white, fontSize: 20), - ), - icon: const Icon(Icons.phone, size: 30, color: Colors.white), - backgroundColor: AppTheme.etsLightRed, - ), - body: WebViewWidget( - controller: WebViewControllerExtension(WebViewController()) - ..loadHtmlFromAssets( - widget.description, Theme.of(context).brightness)), - ); + showBottomBar: false, + safeArea: false, + fabPosition: FloatingActionButtonLocation.centerFloat, + fab: emergencyFloatingButton(context), + body: FutureBuilder( + future: rootBundle.loadString(widget.description), + builder: (context, AsyncSnapshot fileContent) { + if (fileContent.hasData) { + return SafeArea( + top: false, + bottom: false, + child: Scrollbar( + child: Markdown( + padding: const EdgeInsets.only(bottom: 120, top: 12, left: 12, right: 12), + data: fileContent.data!), + ) + ); + } + // Loading a file is so fast showing a spinner would make the user experience worse + return const SizedBox.shrink(); + } + )); } diff --git a/lib/features/ets/quick-link/widgets/security-info/models/emergency_procedures.dart b/lib/features/ets/quick-link/widgets/security-info/models/emergency_procedures.dart index e9169c84e..0a39dd0e1 100644 --- a/lib/features/ets/quick-link/widgets/security-info/models/emergency_procedures.dart +++ b/lib/features/ets/quick-link/widgets/security-info/models/emergency_procedures.dart @@ -7,32 +7,32 @@ import 'package:notredame/features/ets/quick-link/widgets/security-info/models/e List emergencyProcedures(AppIntl intl) => [ EmergencyProcedure( title: intl.security_bomb_threat_title, - detail: 'assets/html/${intl.security_bomb_threat_detail}'), + detail: 'assets/markdown/${intl.security_bomb_threat_detail}'), EmergencyProcedure( title: intl.security_suspicious_packages_title, - detail: 'assets/html/${intl.security_suspicious_packages_detail}'), + detail: 'assets/markdown/${intl.security_suspicious_packages_detail}'), EmergencyProcedure( title: intl.security_evacuation_title, - detail: 'assets/html/${intl.security_evacuation_detail}'), + detail: 'assets/markdown/${intl.security_evacuation_detail}'), EmergencyProcedure( title: intl.security_gas_leak_title, - detail: 'assets/html/${intl.security_gas_leak_detail}'), + detail: 'assets/markdown/${intl.security_gas_leak_detail}'), EmergencyProcedure( title: intl.security_fire_title, - detail: 'assets/html/${intl.security_fire_detail}'), + detail: 'assets/markdown/${intl.security_fire_detail}'), EmergencyProcedure( title: intl.security_broken_elevator_title, - detail: 'assets/html/${intl.security_broken_elevator_detail}'), + detail: 'assets/markdown/${intl.security_broken_elevator_detail}'), EmergencyProcedure( title: intl.security_electrical_outage_title, - detail: 'assets/html/${intl.security_electrical_outage_detail}'), + detail: 'assets/markdown/${intl.security_electrical_outage_detail}'), EmergencyProcedure( title: intl.security_armed_person_title, - detail: 'assets/html/${intl.security_armed_person_detail}'), + detail: 'assets/markdown/${intl.security_armed_person_detail}'), EmergencyProcedure( title: intl.security_earthquake_title, - detail: 'assets/html/${intl.security_earthquake_detail}'), + detail: 'assets/markdown/${intl.security_earthquake_detail}'), EmergencyProcedure( title: intl.security_medical_emergency_title, - detail: 'assets/html/${intl.security_medical_emergency_detail}'), + detail: 'assets/markdown/${intl.security_medical_emergency_detail}'), ]; diff --git a/lib/features/ets/quick-link/widgets/security-info/security_view.dart b/lib/features/ets/quick-link/widgets/security-info/security_view.dart index 3565cf87d..d3039d005 100644 --- a/lib/features/ets/quick-link/widgets/security-info/security_view.dart +++ b/lib/features/ets/quick-link/widgets/security-info/security_view.dart @@ -1,18 +1,17 @@ // Flutter imports: -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:notredame/features/ets/quick-link/widgets/security-info/widgets/security_emergency_procedures.dart'; +import 'package:notredame/features/ets/quick-link/widgets/security-info/widgets/security_map.dart'; +import 'package:notredame/features/ets/quick-link/widgets/security-info/widgets/security_phone_card.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/ets/quick-link/widgets/security-info/emergency_view.dart'; import 'package:notredame/features/ets/quick-link/widgets/security-info/security_viewmodel.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/utils/utils.dart'; + +import 'package:notredame/features/app/widgets/base_scaffold.dart'; class SecurityView extends StatefulWidget { @override @@ -20,126 +19,29 @@ class SecurityView extends StatefulWidget { } class _SecurityViewState extends State { - static const CameraPosition _etsLocation = CameraPosition( - target: LatLng(45.49449875, -73.56246144109338), zoom: 17.0); - @override Widget build(BuildContext context) => ViewModelBuilder.reactive( viewModelBuilder: () => SecurityViewModel(intl: AppIntl.of(context)!), - builder: (context, model, child) => Scaffold( + builder: (context, model, child) => BaseScaffold( appBar: AppBar( title: Text(AppIntl.of(context)!.ets_security_title), ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 250, - child: GoogleMap( - initialCameraPosition: _etsLocation, - zoomControlsEnabled: false, - markers: - model.getSecurityMarkersForMaps(model.markersList), - onMapCreated: (GoogleMapController controller) { - model.controller = controller; - model.changeMapMode(context); - }, - gestureRecognizers: >{ - Factory( - () => EagerGestureRecognizer()), - }), - ), - joinSecurity(), - emergencyProcedures(model), - ], - ), - ), - ), - ); - - Widget joinSecurity() => Padding( - padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppIntl.of(context)!.security_reach_security, - style: const TextStyle(color: AppTheme.etsLightRed, fontSize: 24), - ), - Card( - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(10)), - splashColor: Colors.red.withAlpha(50), - onTap: () => Utils.launchURL( - 'tel:${AppIntl.of(context)!.security_emergency_number}', - AppIntl.of(context)!) - .catchError((error) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(error.toString()))); - }), - child: ListTile( - leading: const Icon(Icons.phone, size: 30), - title: Text(AppIntl.of(context)!.security_emergency_call), - subtitle: - Text(AppIntl.of(context)!.security_emergency_number), - ), + showBottomBar: false, + body: SafeArea( + top: false, + bottom: false, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + securityMap(context, model), + securityPhoneCard(context), + securityEmergencyProcedures(context, model), + ], ), ), - Card( - elevation: 0, - color: Colors.transparent, - child: ListTile( - leading: const Icon(Icons.phone, size: 30), - title: - Text(AppIntl.of(context)!.security_emergency_intern_call), - subtitle: - Text(AppIntl.of(context)!.security_emergency_intern_number), - ), - ), - ], - ), - ); - - Widget emergencyProcedures(SecurityViewModel model) => SingleChildScrollView( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 24.0), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - AppIntl.of(context)!.security_emergency_procedures, - style: const TextStyle(color: AppTheme.etsLightRed, fontSize: 24), ), - for (int i = 0; i < model.emergencyProcedureList.length; i++) - Card( - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(10)), - splashColor: Colors.red.withAlpha(50), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmergencyView( - model.emergencyProcedureList[i].title, - model.emergencyProcedureList[i].detail))), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 16.0, bottom: 16.0, left: 16.0), - child: Text( - model.emergencyProcedureList[i].title, - style: const TextStyle(fontSize: 18), - ), - ), - const Padding( - padding: EdgeInsets.only(right: 16.0), - child: Icon(Icons.arrow_forward_ios), - ), - ], - ), - ), - ), - ]), + ), ); } diff --git a/lib/features/ets/quick-link/widgets/security-info/widget/emergency_floating_button.dart b/lib/features/ets/quick-link/widgets/security-info/widget/emergency_floating_button.dart new file mode 100644 index 000000000..85ac8512d --- /dev/null +++ b/lib/features/ets/quick-link/widgets/security-info/widget/emergency_floating_button.dart @@ -0,0 +1,28 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; + +FloatingActionButton emergencyFloatingButton(BuildContext context) { + return FloatingActionButton.extended( + onPressed: () { + Utils.launchURL('tel:${AppIntl.of(context)!.security_emergency_number}', + AppIntl.of(context)!) + .catchError((error) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(error.toString()))); + }); + }, + label: Text( + AppIntl.of(context)!.security_reach_security, + style: const TextStyle(color: Colors.white, fontSize: 20), + ), + icon: const Icon(Icons.phone, size: 30, color: Colors.white), + backgroundColor: AppTheme.etsLightRed, + ); +} diff --git a/lib/features/ets/quick-link/widgets/security-info/widgets/security_emergency_procedures.dart b/lib/features/ets/quick-link/widgets/security-info/widgets/security_emergency_procedures.dart new file mode 100644 index 000000000..68cb0441a --- /dev/null +++ b/lib/features/ets/quick-link/widgets/security-info/widgets/security_emergency_procedures.dart @@ -0,0 +1,58 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/ets/quick-link/widgets/security-info/emergency_view.dart'; +import 'package:notredame/features/ets/quick-link/widgets/security-info/security_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; + +Widget securityEmergencyProcedures( + BuildContext context, SecurityViewModel model) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + AppIntl.of(context)!.security_emergency_procedures, + style: const TextStyle(color: AppTheme.etsLightRed, fontSize: 24), + ), + ), + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + model.emergencyProcedureList.length, + (index) => Card( + child: InkWell( + splashColor: Colors.red.withAlpha(50), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EmergencyView( + model.emergencyProcedureList[index].title, + model.emergencyProcedureList[index].detail))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 16.0, bottom: 16.0, left: 16.0), + child: Text( + model.emergencyProcedureList[index].title, + style: const TextStyle(fontSize: 18), + ), + ), + const Icon(Icons.arrow_forward_ios), + ], + ), + ), + ), + ), + ), + ), + ], + ); +} diff --git a/lib/features/ets/quick-link/widgets/security-info/widgets/security_map.dart b/lib/features/ets/quick-link/widgets/security-info/widgets/security_map.dart new file mode 100644 index 000000000..2cb76c2c8 --- /dev/null +++ b/lib/features/ets/quick-link/widgets/security-info/widgets/security_map.dart @@ -0,0 +1,32 @@ +// Flutter imports: +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +// Project imports: +import 'package:notredame/features/ets/quick-link/widgets/security-info/security_viewmodel.dart'; + +Widget securityMap(BuildContext context, SecurityViewModel model) { + const CameraPosition etsLocation = CameraPosition( + target: LatLng(45.49449875, -73.56246144109338), zoom: 17.0); + return SizedBox( + height: 250, + child: GoogleMap( + initialCameraPosition: etsLocation, + zoomControlsEnabled: false, + markers: + model.getSecurityMarkersForMaps(model.markersList), + onMapCreated: (GoogleMapController controller) { + model.controller = controller; + model.changeMapMode(context); + }, + gestureRecognizers: >{ + Factory( + () => EagerGestureRecognizer()), + }) + ); +} diff --git a/lib/features/ets/quick-link/widgets/security-info/widgets/security_phone_card.dart b/lib/features/ets/quick-link/widgets/security-info/widgets/security_phone_card.dart new file mode 100644 index 000000000..f3b0d6e64 --- /dev/null +++ b/lib/features/ets/quick-link/widgets/security-info/widgets/security_phone_card.dart @@ -0,0 +1,41 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/utils/utils.dart'; + +Widget securityPhoneCard(BuildContext context) { + return Column( + children: [ + Card( + child: InkWell( + splashColor: Colors.red.withAlpha(50), + onTap: () => Utils.launchURL( + 'tel:${AppIntl.of(context)!.security_emergency_number}', + AppIntl.of(context)!) + .catchError((error) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(error.toString()))); + }), + child: ListTile( + leading: const Icon(Icons.phone, size: 30), + title: Text(AppIntl.of(context)!.security_emergency_call), + subtitle: Text(AppIntl.of(context)!.security_emergency_number), + ), + ), + ), + Card( + elevation: 0, + color: Colors.transparent, + child: ListTile( + leading: const Icon(Icons.phone, size: 30), + title: Text(AppIntl.of(context)!.security_emergency_intern_call), + subtitle: Text(AppIntl.of(context)!.security_emergency_intern_number), + ), + ), + ], + ); +} diff --git a/lib/features/more/about/about_view.dart b/lib/features/more/about/about_view.dart index 54183a454..fa2e8a1a3 100644 --- a/lib/features/more/about/about_view.dart +++ b/lib/features/more/about/about_view.dart @@ -2,13 +2,10 @@ import 'package:flutter/material.dart'; // Package imports: -import 'package:easter_egg_trigger/easter_egg_trigger.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -// Project imports: -import 'package:notredame/constants/urls.dart'; -import 'package:notredame/utils/utils.dart'; +import 'package:notredame/features/more/about/widget/easter_egg_icon.dart'; // Importez le widget extrait +import 'package:notredame/features/more/about/widget/social_icons_row.dart'; // Importez le widget extrait class AboutView extends StatefulWidget { @override @@ -49,6 +46,10 @@ class _AboutViewState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { + return newMethod(context); + } + + Scaffold newMethod(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( @@ -88,26 +89,9 @@ class _AboutViewState extends State with TickerProviderStateMixin { left: 0, child: Column( children: [ - EasterEggTrigger( - action: () => toggleTrigger(), - codes: const [ - EasterEggTriggers.SwipeUp, - EasterEggTriggers.SwipeRight, - EasterEggTriggers.SwipeDown, - EasterEggTriggers.SwipeLeft, - EasterEggTriggers.Tap - ], - child: SizedBox( - width: 100, - height: 100, - child: Hero( - tag: 'about', - child: Image.asset( - "assets/images/favicon_applets.png", - scale: 2.0, - ), - ), - ), + EasterEggIcon( + toggleTrigger: toggleTrigger, + easterEggTrigger: _easterEggTrigger, ), Padding( padding: const EdgeInsets.all(16.0), @@ -116,68 +100,7 @@ class _AboutViewState extends State with TickerProviderStateMixin { style: const TextStyle(color: Colors.white), ), ), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - icon: const FaIcon( - FontAwesomeIcons.earthAmericas, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubWebsite, AppIntl.of(context)!)), - IconButton( - icon: const FaIcon( - FontAwesomeIcons.github, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubGithub, AppIntl.of(context)!)), - IconButton( - icon: const FaIcon( - FontAwesomeIcons.facebook, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubFacebook, AppIntl.of(context)!)), - IconButton( - icon: const FaIcon( - FontAwesomeIcons.twitter, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubTwitter, AppIntl.of(context)!)), - IconButton( - icon: const FaIcon( - FontAwesomeIcons.youtube, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubYoutube, AppIntl.of(context)!)), - IconButton( - icon: const FaIcon( - FontAwesomeIcons.discord, - color: Colors.white, - ), - onPressed: () => Utils.launchURL( - Urls.clubDiscord, AppIntl.of(context)!)), - ], - ), - ), - if (_easterEggTrigger) - SizedBox( - width: 200, - height: 200, - child: Hero( - tag: 'capra', - child: Image.asset( - "assets/images/capra_long.png", - scale: 1.0, - ), - ), - ), + const SocialIconsRow(), ], ), ), diff --git a/lib/features/more/about/widget/easter_egg_icon.dart b/lib/features/more/about/widget/easter_egg_icon.dart new file mode 100644 index 000000000..7b9f56b1e --- /dev/null +++ b/lib/features/more/about/widget/easter_egg_icon.dart @@ -0,0 +1,55 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:easter_egg_trigger/easter_egg_trigger.dart'; + +class EasterEggIcon extends StatelessWidget { + final Function toggleTrigger; + final bool easterEggTrigger; + + const EasterEggIcon( + {required this.toggleTrigger, required this.easterEggTrigger}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + EasterEggTrigger( + // ignore: avoid_dynamic_calls + action: () => toggleTrigger(), + codes: const [ + EasterEggTriggers.SwipeUp, + EasterEggTriggers.SwipeRight, + EasterEggTriggers.SwipeDown, + EasterEggTriggers.SwipeLeft, + EasterEggTriggers.Tap + ], + child: SizedBox( + width: 100, + height: 100, + child: Hero( + tag: 'about', + child: Image.asset( + "assets/images/favicon_applets.png", + scale: 2.0, + ), + ), + ), + ), + if (easterEggTrigger) + SizedBox( + width: 128, + height: 128, + child: Hero( + tag: 'capra', + child: Image.asset( + "assets/images/capra.png", + scale: 1.0, + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/more/about/widget/social_icons_row.dart b/lib/features/more/about/widget/social_icons_row.dart new file mode 100644 index 000000000..913537b78 --- /dev/null +++ b/lib/features/more/about/widget/social_icons_row.dart @@ -0,0 +1,74 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/utils/utils.dart'; + +class SocialIconsRow extends StatelessWidget { + const SocialIconsRow(); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const FaIcon( + FontAwesomeIcons.earthAmericas, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubWebsite, AppIntl.of(context)!), + ), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.github, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubGithub, AppIntl.of(context)!), + ), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.facebook, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubFacebook, AppIntl.of(context)!), + ), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.twitter, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubTwitter, AppIntl.of(context)!), + ), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.youtube, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubYoutube, AppIntl.of(context)!), + ), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.discord, + color: Colors.white, + ), + onPressed: () => + Utils.launchURL(Urls.clubDiscord, AppIntl.of(context)!), + ), + ], + ), + ); + } +} diff --git a/lib/features/more/contributors/contributors_view.dart b/lib/features/more/contributors/contributors_view.dart index 2bba2531b..f45a4169b 100644 --- a/lib/features/more/contributors/contributors_view.dart +++ b/lib/features/more/contributors/contributors_view.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:github/github.dart'; -import 'package:skeletonizer/skeletonizer.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/more/contributors/contributors_viewmodel.dart'; +import 'package:notredame/features/more/contributors/widgets/contributor_title_widget.dart'; import 'package:notredame/utils/utils.dart'; class ContributorsView extends StatelessWidget { @@ -28,15 +28,15 @@ class ContributorsView extends StatelessWidget { future: model.contributors, builder: (context, snapshot) { if (!snapshot.hasData) { - // Populate the skeleton - final fakeContributors = - List.filled(30, Contributor(login: "Username")); - return Skeletonizer( - child: contributorsList(fakeContributors), - ); - } else { - return contributorsList(snapshot.data!); + return const Center(child: CircularProgressIndicator()); } + return ListView.builder( + padding: EdgeInsets.zero, + itemCount: snapshot.data!.length, + itemBuilder: (context, index) => ContributorTileWidget( + contributor: snapshot.data![index], + ), + ); }, ), ); diff --git a/lib/features/more/contributors/widgets/contributor_title_widget.dart b/lib/features/more/contributors/widgets/contributor_title_widget.dart new file mode 100644 index 000000000..a99317fcc --- /dev/null +++ b/lib/features/more/contributors/widgets/contributor_title_widget.dart @@ -0,0 +1,31 @@ +// widgets/ContributorTileWidget.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:github/github.dart'; + +// Project imports: +import 'package:notredame/utils/utils.dart'; + +class ContributorTileWidget extends StatelessWidget { + final Contributor contributor; + + const ContributorTileWidget({super.key, required this.contributor}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(contributor.login ?? ''), + leading: CircleAvatar( + backgroundImage: NetworkImage(contributor.avatarUrl ?? ''), + ), + onTap: () => Utils.launchURL( + contributor.htmlUrl ?? '', + AppIntl.of(context)!, + ), + ); + } +} diff --git a/lib/features/more/faq/faq_view.dart b/lib/features/more/faq/faq_view.dart index 0252f6fa7..855f347e4 100644 --- a/lib/features/more/faq/faq_view.dart +++ b/lib/features/more/faq/faq_view.dart @@ -9,8 +9,10 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/more/faq/faq_viewmodel.dart'; import 'package:notredame/features/more/faq/models/faq.dart'; -import 'package:notredame/features/more/faq/models/faq_actions.dart'; -import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/features/more/faq/widget/action_card.dart'; +import 'package:notredame/features/more/faq/widget/faq_subtitle.dart'; +import 'package:notredame/features/more/faq/widget/faq_title.dart'; +import 'package:notredame/features/more/faq/widget/question_card.dart'; class FaqView extends StatefulWidget { final Color? backgroundColor; @@ -28,238 +30,84 @@ class _FaqViewState extends State { Widget build(BuildContext context) => ViewModelBuilder.reactive( viewModelBuilder: () => FaqViewModel(), builder: (context, model, child) { - return BaseScaffold( - appBar: AppBar( - title: Text(AppIntl.of(context)!.need_help), - ), - showBottomBar: false, - body: (MediaQuery.of(context).orientation == Orientation.portrait) - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - getSubtitle(AppIntl.of(context)!.questions_and_answers), - getCaroussel(model), - getSubtitle(AppIntl.of(context)!.actions), - getActions(model) - ], - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Column( - children: [ - getSubtitle( - AppIntl.of(context)!.questions_and_answers), - Expanded(child: getCaroussel(model)), - ], - ), - ), - Flexible( - child: Column( - children: [ - getSubtitle(AppIntl.of(context)!.actions), - Container(child: getActions(model)), - ], - ), - ) - ], - ), - ); - }, - ); - - Padding getSubtitle(String subtitle) { - return Padding( - padding: const EdgeInsets.only(left: 18.0, top: 18.0, bottom: 10.0), - child: Text( - subtitle, - style: Theme.of(context).textTheme.headlineSmall!.copyWith( - color: widget.backgroundColor == Colors.white - ? Colors.black - : Colors.white, - ), - ), - ); - } - - CarouselSlider getCaroussel(FaqViewModel model) { - return CarouselSlider( - options: CarouselOptions( - height: 260.0, - ), - items: faq.questions.asMap().entries.map((entry) { - final int index = entry.key; - final question = faq.questions[index]; - - return Builder( - builder: (BuildContext context) { - return Container( - width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.symmetric(horizontal: 5.0), - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light - ? const Color.fromARGB(255, 240, 238, 238) - : const Color.fromARGB(255, 40, 40, 40), - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - child: getQuestionCard( - question.title[model.locale?.languageCode] ?? '', - question.description[model.locale?.languageCode] ?? '', - ), - ); - }, - ); - }).toList(), - ); - } - - Padding getQuestionCard(String title, String description) { - return Padding( - padding: const EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0), - child: SingleChildScrollView( - child: Column( - children: [ - Text( - title, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 20, - color: Theme.of(context).brightness == Brightness.light - ? Colors.black - : Colors.white, + return Scaffold( + backgroundColor: widget.backgroundColor, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FaqTitle(backgroundColor: widget.backgroundColor), + FaqSubtitle( + subtitle: AppIntl.of(context)!.questions_and_answers, + backgroundColor: widget.backgroundColor, + ), + Padding( + padding: const EdgeInsets.only(left: 15.0, right: 15.0), + child: CarouselSlider( + options: CarouselOptions( + height: 250.0, + ), + items: faq.questions.asMap().entries.map((entry) { + final int index = entry.key; + final question = faq.questions[index]; + + return Builder( + builder: (BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.symmetric(horizontal: 5.0), + decoration: BoxDecoration( + color: Theme.of(context).brightness == + Brightness.light + ? const Color.fromARGB(255, 240, 238, 238) + : const Color.fromARGB(255, 40, 40, 40), + borderRadius: + const BorderRadius.all(Radius.circular(8.0)), + ), + child: QuestionCard( + title: + question.title[model.locale?.languageCode] ?? + '', + description: question.description[ + model.locale?.languageCode] ?? + '', + ), + ); + }, + ); + }).toList(), ), - ), - const SizedBox(height: 12), - Text( - description, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 16, - color: Theme.of(context).brightness == Brightness.light - ? Colors.black - : Colors.white, + ), + FaqSubtitle( + subtitle: AppIntl.of(context)!.actions, + backgroundColor: widget.backgroundColor, + ), + Expanded( + child: ListView.builder( + key: const Key("action_listview_key"), + padding: const EdgeInsets.only(top: 1.0), + itemCount: faq.actions.length, + itemBuilder: (context, index) { + final action = faq.actions[index]; + + return ActionCard( + title: action.title[model.locale?.languageCode] ?? '', + description: + action.description[model.locale?.languageCode] ?? + '', + type: action.type, + link: action.link, + iconName: action.iconName, + iconColor: action.iconColor, + circleColor: action.circleColor, + context: context, + model: model, + ); + }, ), + ) + ], ), - ], - ), - ), - ); - } - - Expanded getActions(FaqViewModel model) { - return Expanded( - child: ListView.builder( - key: const Key("action_listview_key"), - padding: const EdgeInsets.only(top: 1.0), - itemCount: faq.actions.length, - itemBuilder: (context, index) { - final action = faq.actions[index]; - - return getActionCard( - action.title[model.locale?.languageCode] ?? '', - action.description[model.locale?.languageCode] ?? '', - action.type, - action.link, - action.iconName, - action.iconColor, - action.circleColor, - context, - model); - }, - ), - ); - } - - Padding getActionCard( - String title, - String description, - ActionType type, - String link, - IconData iconName, - Color iconColor, - Color circleColor, - BuildContext context, - FaqViewModel model) { - return Padding( - padding: const EdgeInsets.fromLTRB(15.0, 0.0, 15.0, 15.0), - child: ElevatedButton( - onPressed: () { - if (type.name == ActionType.webview.name) { - openWebview(model, link); - } else if (type.name == ActionType.email.name) { - openMail(model, context, link); - } + ); }, - style: ButtonStyle( - elevation: MaterialStateProperty.all(8.0), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - )), - child: getActionCardInfo( - context, - title, - description, - iconName, - iconColor, - circleColor, - ), - ), - ); - } - - Padding getActionCardInfo( - BuildContext context, - String title, - String description, - IconData iconName, - Color iconColor, - Color circleColor) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - title, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 18, - color: Theme.of(context).brightness == Brightness.light - ? Colors.black - : Colors.white, - ), - ), - ), - const SizedBox(width: 16), - CircleAvatar( - backgroundColor: circleColor, - radius: 25, - child: Icon(iconName, color: iconColor), - ), - ], - ), - const SizedBox(height: 12.0), - Text(description, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 16, - color: Theme.of(context).brightness == Brightness.light - ? Colors.black - : Colors.white, - )) - ], - ), - ); - } - - Future openWebview(FaqViewModel model, String link) async { - model.launchWebsite(link, Theme.of(context).brightness); - } - - Future openMail( - FaqViewModel model, BuildContext context, String addressEmail) async { - model.openMail(addressEmail, context); - } + ); } diff --git a/lib/features/more/faq/widget/action_card.dart b/lib/features/more/faq/widget/action_card.dart new file mode 100644 index 000000000..6693f56da --- /dev/null +++ b/lib/features/more/faq/widget/action_card.dart @@ -0,0 +1,119 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/features/more/faq/faq_viewmodel.dart'; +import 'package:notredame/features/more/faq/models/faq_actions.dart'; + +class ActionCard extends StatelessWidget { + final String title; + final String description; + final ActionType type; + final String link; + final IconData iconName; + final Color iconColor; + final Color circleColor; + final BuildContext context; + final FaqViewModel model; + + const ActionCard({ + required this.title, + required this.description, + required this.type, + required this.link, + required this.iconName, + required this.iconColor, + required this.circleColor, + required this.context, + required this.model, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(15.0, 0.0, 15.0, 15.0), + child: ElevatedButton( + onPressed: () { + if (type.name == ActionType.webview.name) { + openWebview(model, link); + } else if (type.name == ActionType.email.name) { + openMail(model, context, link); + } + }, + style: ButtonStyle( + elevation: MaterialStateProperty.all(8.0), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + )), + child: getActionCardInfo( + context, + title, + description, + iconName, + iconColor, + circleColor, + ), + ), + ); + } + + Row getActionCardInfo(BuildContext context, String title, String description, + IconData iconName, Color iconColor, Color circleColor) { + return Row( + children: [ + Column( + children: [ + CircleAvatar( + backgroundColor: circleColor, + radius: 25, + child: Icon(iconName, color: iconColor), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 15, 0, 15), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 18, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10.0), + Text( + description, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.justify, + ) + ], + ), + ), + ) + ], + ); + } + + Future openWebview(FaqViewModel model, String link) async { + model.launchWebsite(link, Theme.of(context).brightness); + } + + Future openMail( + FaqViewModel model, BuildContext context, String addressEmail) async { + model.openMail(addressEmail, context); + } +} diff --git a/lib/features/more/faq/widget/faq_subtitle.dart b/lib/features/more/faq/widget/faq_subtitle.dart new file mode 100644 index 000000000..149391c73 --- /dev/null +++ b/lib/features/more/faq/widget/faq_subtitle.dart @@ -0,0 +1,23 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class FaqSubtitle extends StatelessWidget { + final String subtitle; + final Color? backgroundColor; + + const FaqSubtitle({required this.subtitle, this.backgroundColor}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 18.0, top: 18.0, bottom: 10.0), + child: Text( + subtitle, + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: + backgroundColor == Colors.white ? Colors.black : Colors.white, + ), + ), + ); + } +} diff --git a/lib/features/more/faq/widget/faq_title.dart b/lib/features/more/faq/widget/faq_title.dart new file mode 100644 index 000000000..8ab78f134 --- /dev/null +++ b/lib/features/more/faq/widget/faq_title.dart @@ -0,0 +1,51 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class FaqTitle extends StatelessWidget { + final Color? backgroundColor; + + const FaqTitle({this.backgroundColor}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 60.0), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 5.0), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Icon( + Icons.arrow_back, + color: backgroundColor == Colors.white + ? Colors.black + : Colors.white, + ), + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: Text( + AppIntl.of(context)!.need_help, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: backgroundColor == Colors.white + ? Colors.black + : Colors.white, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/more/faq/widget/question_card.dart b/lib/features/more/faq/widget/question_card.dart new file mode 100644 index 000000000..91a552d24 --- /dev/null +++ b/lib/features/more/faq/widget/question_card.dart @@ -0,0 +1,49 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class QuestionCard extends StatelessWidget { + final String title; + final String description; + + const QuestionCard({required this.title, required this.description}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0), + child: Align( + alignment: Alignment.topLeft, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + textScaler: TextScaler.noScaling, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 20, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.justify, + ), + Text( + description, + textScaler: TextScaler.noScaling, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.justify, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/more/feedback/feedback_view.dart b/lib/features/more/feedback/feedback_view.dart index 31e997acb..fa2c6e057 100644 --- a/lib/features/more/feedback/feedback_view.dart +++ b/lib/features/more/feedback/feedback_view.dart @@ -9,6 +9,8 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/more/feedback/feedback_type.dart'; import 'package:notredame/features/more/feedback/feedback_viewmodel.dart'; +import 'package:notredame/features/more/feedback/widgets-feedback/card_info.dart'; +import 'package:notredame/features/more/feedback/widgets-feedback/list_tag.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/loading.dart'; import 'package:notredame/utils/utils.dart'; @@ -50,13 +52,13 @@ class _FeedbackViewState extends State { borderRadius: BorderRadius.circular(8.0), ), )), - child: getCardInfo( - context, - AppIntl.of(context)!.more_report_bug_bug, - AppIntl.of(context)!.more_report_bug_bug_subtitle, - Icons.bug_report_outlined, - const Color.fromRGBO(252, 196, 238, 1), - const Color.fromRGBO(153, 78, 174, 1), + child: CardInfo( + title: AppIntl.of(context)!.more_report_bug_bug, + subtitle: + AppIntl.of(context)!.more_report_bug_bug_subtitle, + icon: Icons.bug_report, + iconColor: const Color.fromRGBO(252, 196, 238, 1), + circleColor: const Color.fromRGBO(153, 78, 174, 1), ), ), ), @@ -78,13 +80,13 @@ class _FeedbackViewState extends State { borderRadius: BorderRadius.circular(8.0), ), )), - child: getCardInfo( - context, - AppIntl.of(context)!.more_report_bug_feature, - AppIntl.of(context)!.more_report_bug_feature_subtitle, - Icons.design_services_outlined, - const Color.fromRGBO(63, 219, 251, 1), - const Color.fromRGBO(14, 127, 188, 1), + child: CardInfo( + title: AppIntl.of(context)!.more_report_bug_feature, + subtitle: + AppIntl.of(context)!.more_report_bug_feature_subtitle, + icon: Icons.design_services, + iconColor: const Color.fromRGBO(63, 219, 251, 1), + circleColor: const Color.fromRGBO(14, 127, 188, 1), ), ), ), @@ -149,16 +151,16 @@ class _FeedbackViewState extends State { ), ), Row(children: [ - createListTag( - model.myIssues[index].createdAt, + ListTag( + text: model.myIssues[index].createdAt, color: Colors.transparent, textColor: isLightMode ? const Color.fromARGB(168, 0, 0, 0) : Colors.white, ), const SizedBox(width: 4), - createListTag( - model.myIssues[index].isOpen + ListTag( + text: model.myIssues[index].isOpen ? AppIntl.of(context)! .ticket_status_open : AppIntl.of(context)! @@ -285,64 +287,4 @@ class _FeedbackViewState extends State { ? AppTheme.lightThemeBackground : AppTheme.darkThemeAccent; } - - Row getCardInfo(BuildContext context, String title, String subtitle, - IconData icon, Color iconColor, Color circleColor) { - return Row( - children: [ - Column( - children: [ - CircleAvatar( - backgroundColor: circleColor, - radius: 25, - child: Icon(icon, color: iconColor), - ), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(15, 15, 0, 15), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 19), - textAlign: TextAlign.left, - ), - Text( - subtitle, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 16), - textAlign: TextAlign.left, - ) - ], - ), - ), - ) - ], - ); - } - - Widget createListTag(String text, {Color? textColor, Color? color}) { - return Container( - decoration: BoxDecoration( - // border radius - borderRadius: BorderRadius.circular(6), - color: color), - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - text, - style: TextStyle(color: textColor), - ), - ), - ); - } } diff --git a/lib/features/more/feedback/widgets-feedback/card_info.dart b/lib/features/more/feedback/widgets-feedback/card_info.dart new file mode 100644 index 000000000..1d5b828d8 --- /dev/null +++ b/lib/features/more/feedback/widgets-feedback/card_info.dart @@ -0,0 +1,63 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class CardInfo extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final Color iconColor; + final Color circleColor; + + const CardInfo({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + required this.iconColor, + required this.circleColor, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Column( + children: [ + CircleAvatar( + backgroundColor: circleColor, + radius: 25, + child: Icon(icon, color: iconColor), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 15, 0, 15), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(fontSize: 19), + textAlign: TextAlign.left, + ), + Text( + subtitle, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(fontSize: 16), + textAlign: TextAlign.left, + ) + ], + ), + ), + ) + ], + ); + } +} diff --git a/lib/features/more/feedback/widgets-feedback/list_tag.dart b/lib/features/more/feedback/widgets-feedback/list_tag.dart new file mode 100644 index 000000000..838763fe3 --- /dev/null +++ b/lib/features/more/feedback/widgets-feedback/list_tag.dart @@ -0,0 +1,32 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class ListTag extends StatelessWidget { + final String text; + final Color? textColor; + final Color? color; + + const ListTag({ + super.key, + required this.text, + this.textColor, + this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: color, + ), + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + text, + style: TextStyle(color: textColor), + ), + ), + ); + } +} diff --git a/lib/features/more/more_view.dart b/lib/features/more/more_view.dart index 5ed6bbc89..81213ed44 100644 --- a/lib/features/more/more_view.dart +++ b/lib/features/more/more_view.dart @@ -1,10 +1,10 @@ +// more_view.dart + // Flutter imports: -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; // Package imports: -import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; @@ -13,8 +13,11 @@ import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/more/more_viewmodel.dart'; -import 'package:notredame/features/welcome/discovery/discovery_components.dart'; -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/features/more/widget/about_applets_tile.dart'; +import 'package:notredame/features/more/widget/contributors_list_tile.dart'; +import 'package:notredame/features/more/widget/in_app_review_tile.dart'; +import 'package:notredame/features/more/widget/open_source_licenses_list_tile.dart'; +import 'package:notredame/features/more/widget/report_bug_tile.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/locator.dart'; import 'package:notredame/utils/utils.dart'; @@ -38,38 +41,6 @@ class _MoreViewState extends State { }); } - /// Returns right icon color for discovery depending on theme. - Widget getProperIconAccordingToTheme(IconData icon) { - return (Theme.of(context).brightness == Brightness.dark && - isDiscoveryOverlayActive) - ? Icon(icon, color: Colors.black) - : Icon(icon); - } - - /// License text box - List aboutBoxChildren(BuildContext context) { - final textStyle = Theme.of(context).textTheme.bodyMedium!; - return [ - const SizedBox(height: 24), - RichText( - text: TextSpan( - children: [ - TextSpan( - style: textStyle, text: AppIntl.of(context)!.flutter_license), - TextSpan( - style: textStyle.copyWith(color: Colors.blue), - text: AppIntl.of(context)!.flutter_website, - recognizer: TapGestureRecognizer() - ..onTap = () => Utils.launchURL( - AppIntl.of(context)!.flutter_website, - AppIntl.of(context)!)), - TextSpan(style: textStyle, text: '.'), - ], - ), - ), - ]; - } - @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( @@ -83,79 +54,13 @@ class _MoreViewState extends State { body: ListView( padding: EdgeInsets.zero, children: [ - ListTile( - title: Text(AppIntl.of(context)!.more_about_applets_title), - leading: _buildDiscoveryFeatureDescriptionWidget( - context, - Hero( - tag: 'about', - child: Image.asset( - "assets/images/favicon_applets.png", - height: 24, - width: 24, - ), - ), - DiscoveryIds.detailsMoreThankYou, - model), - onTap: () { - _analyticsService.logEvent(tag, "About App|ETS clicked"); - model.navigationService.pushNamed(RouterPaths.about); - }), - ListTile( - title: Text(AppIntl.of(context)!.more_report_bug), - leading: _buildDiscoveryFeatureDescriptionWidget( - context, - getProperIconAccordingToTheme( - Icons.bug_report_outlined), - DiscoveryIds.detailsMoreBugReport, - model), - onTap: () { - _analyticsService.logEvent(tag, "Report a bug clicked"); - model.navigationService.pushNamed(RouterPaths.feedback); - }), - ListTile( - title: Text(AppIntl.of(context)!.in_app_review_title), - leading: const Icon(Icons.rate_review_outlined), - onTap: () { - _analyticsService.logEvent(tag, "Rate us clicked"); - MoreViewModel.launchInAppReview(); - }), - ListTile( - title: Text(AppIntl.of(context)!.more_contributors), - leading: _buildDiscoveryFeatureDescriptionWidget( - context, - getProperIconAccordingToTheme(Icons.people_outline), - DiscoveryIds.detailsMoreContributors, - model), - onTap: () { - _analyticsService.logEvent(tag, "Contributors clicked"); - model.navigationService - .pushNamed(RouterPaths.contributors); - }), - ListTile( - title: Text(AppIntl.of(context)!.more_open_source_licenses), - leading: const Icon(Icons.code_outlined), - onTap: () { - _analyticsService.logEvent(tag, "Rate us clicked"); - Navigator.of(context).push(PageRouteBuilder( - pageBuilder: (context, _, __) => AboutDialog( - applicationIcon: Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - width: 75, - child: Image.asset( - 'assets/images/favicon_applets.png')), - ), - applicationName: - AppIntl.of(context)!.more_open_source_licenses, - applicationVersion: model.appVersion, - applicationLegalese: - '\u{a9} ${DateTime.now().year} App|ETS', - children: aboutBoxChildren(context), - ), - opaque: false, - )); - }), + AboutAppletsTile(), + ReportBugTile( + isDiscoveryOverlayActive: isDiscoveryOverlayActive), + const InAppReviewTile(), + ContributorsTile( + isDiscoveryOverlayActive: isDiscoveryOverlayActive), + const OpenSourceLicensesTile(), if (model.privacyPolicyToggle) ListTile( title: Text(AppIntl.of(context)!.privacy_policy), @@ -167,12 +72,8 @@ class _MoreViewState extends State { }), ListTile( title: Text(AppIntl.of(context)!.need_help), - leading: _buildDiscoveryFeatureDescriptionWidget( - context, - getProperIconAccordingToTheme( - Icons.question_answer_outlined), - DiscoveryIds.detailsMoreFaq, - model), + leading: + getProperIconAccordingToTheme(Icons.question_answer), onTap: () { _analyticsService.logEvent(tag, "FAQ clicked"); model.navigationService.pushNamed(RouterPaths.faq, @@ -181,11 +82,7 @@ class _MoreViewState extends State { }), ListTile( title: Text(AppIntl.of(context)!.settings_title), - leading: _buildDiscoveryFeatureDescriptionWidget( - context, - getProperIconAccordingToTheme(Icons.settings_outlined), - DiscoveryIds.detailsMoreSettings, - model), + leading: getProperIconAccordingToTheme(Icons.settings), onTap: () { _analyticsService.logEvent(tag, "Settings clicked"); model.navigationService.pushNamed(RouterPaths.settings); @@ -225,35 +122,10 @@ class _MoreViewState extends State { }); } - DescribedFeatureOverlay _buildDiscoveryFeatureDescriptionWidget( - BuildContext context, - Widget icon, - String featuredId, - MoreViewModel model) { - final discovery = getDiscoveryByFeatureId( - context, DiscoveryGroupIds.pageMore, featuredId); - - return DescribedFeatureOverlay( - overflowMode: OverflowMode.wrapBackground, - contentLocation: ContentLocation.below, - featureId: discovery.featureId, - title: Text(discovery.title, textAlign: TextAlign.justify), - description: discovery.details, - backgroundColor: AppTheme.appletsDarkPurple, - tapTarget: icon, - pulseDuration: const Duration(seconds: 5), - child: icon, - onComplete: () { - setState(() { - isDiscoveryOverlayActive = false; - }); - return model.discoveryCompleted(); - }, - onOpen: () async { - setState(() { - isDiscoveryOverlayActive = true; - }); - return true; - }); + Widget getProperIconAccordingToTheme(IconData icon) { + return (Theme.of(context).brightness == Brightness.dark && + isDiscoveryOverlayActive) + ? Icon(icon, color: Colors.black) + : Icon(icon); } } diff --git a/lib/features/more/more_viewmodel.dart b/lib/features/more/more_viewmodel.dart index 6937be17a..70a6f7781 100644 --- a/lib/features/more/more_viewmodel.dart +++ b/lib/features/more/more_viewmodel.dart @@ -10,6 +10,7 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/analytics/remote_config_service.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; @@ -47,6 +48,9 @@ class MoreViewModel extends FutureViewModel { /// Used to redirect on the dashboard. final NavigationService navigationService = locator(); + /// Analytics service + final AnalyticsService _analyticsService = locator(); + String? _appVersion; final AppIntl _appIntl; @@ -158,4 +162,7 @@ class MoreViewModel extends FutureViewModel { /// Get the privacy policy toggle bool get privacyPolicyToggle => _remoteConfigService.privacyPolicyToggle; + + /// Getter for analyticsService + AnalyticsService get analyticsService => _analyticsService; } diff --git a/lib/features/more/settings/choose_language_view.dart b/lib/features/more/settings/choose_language_view.dart index d883ba8a3..b254bd09e 100644 --- a/lib/features/more/settings/choose_language_view.dart +++ b/lib/features/more/settings/choose_language_view.dart @@ -7,6 +7,7 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/more/settings/choose_language_viewmodel.dart'; +import 'package:notredame/features/more/settings/widget/language_list_widget.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/utils.dart'; @@ -16,80 +17,59 @@ class ChooseLanguageView extends StatefulWidget { } class _ChooseLanguageViewState extends State { - ListView languagesListView(ChooseLanguageViewModel model) { - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.all(8), - itemCount: model.languages.length, - itemBuilder: (BuildContext context, int index) { - return Card( - color: Utils.getColorByBrightness( - context, Colors.white, Colors.grey[900]!), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - ListTile( - title: Text(model.languages[index]), - trailing: Icon( - model.languageSelectedIndex == index ? Icons.check : null), - onTap: () { - model.changeLanguage(index); - }, - ), - ])); - }, - ); - } - @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( - viewModelBuilder: () => - ChooseLanguageViewModel(intl: AppIntl.of(context)!), - builder: (context, model, child) => Scaffold( - backgroundColor: Utils.getColorByBrightness( - context, AppTheme.etsLightRed, AppTheme.primaryDark), - body: Center( - child: ListView( - shrinkWrap: true, - children: [ - Icon( - Icons.language, - size: 80, - color: Utils.getColorByBrightness( - context, Colors.white, AppTheme.etsLightRed), - ), - Padding( - padding: const EdgeInsets.only(left: 20, top: 60), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - AppIntl.of(context)!.choose_language_title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white), - ), - ), + viewModelBuilder: () => + ChooseLanguageViewModel(intl: AppIntl.of(context)!), + builder: (context, model, child) => Scaffold( + backgroundColor: Utils.getColorByBrightness( + context, AppTheme.etsLightRed, AppTheme.primaryDark), + body: Center( + child: ListView( + shrinkWrap: true, + children: [ + Icon( + Icons.language, + size: 80, + color: Utils.getColorByBrightness( + context, Colors.white, AppTheme.etsLightRed), + ), + Padding( + padding: const EdgeInsets.only(left: 20, top: 60), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + AppIntl.of(context)!.choose_language_title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, ), - Padding( - padding: - const EdgeInsets.only(left: 20, top: 10, bottom: 30), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - AppIntl.of(context)!.choose_language_subtitle, - style: const TextStyle( - fontSize: 16, color: Colors.white), - ), - ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 20, top: 10, bottom: 30), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + AppIntl.of(context)!.choose_language_subtitle, + style: const TextStyle( + fontSize: 16, + color: Colors.white, ), - Padding( - padding: const EdgeInsets.only(bottom: 80), - child: languagesListView(model), - ) - ], + ), ), ), - )); + Padding( + padding: const EdgeInsets.only(bottom: 80), + child: LanguageListViewWidget(model: model), + ), + ], + ), + ), + ), + ); } } diff --git a/lib/features/more/settings/settings_manager.dart b/lib/features/more/settings/settings_manager.dart index 9cd1fb68c..0acb0830a 100644 --- a/lib/features/more/settings/settings_manager.dart +++ b/lib/features/more/settings/settings_manager.dart @@ -94,14 +94,6 @@ class SettingsManager with ChangeNotifier { /// Get Dashboard Future> getDashboard() async { final Map dashboard = {}; - - final broadcastCardIndex = - await _preferencesService.getInt(PreferencesFlag.broadcastCard) ?? - getDefaultCardIndex(PreferencesFlag.broadcastCard); - - dashboard.putIfAbsent( - PreferencesFlag.broadcastCard, () => broadcastCardIndex); - final aboutUsIndex = await _preferencesService.getInt(PreferencesFlag.aboutUsCard) ?? getDefaultCardIndex(PreferencesFlag.aboutUsCard); @@ -277,9 +269,8 @@ class SettingsManager with ChangeNotifier { } /// Get the default index of each card - int getDefaultCardIndex(PreferencesFlag flag) { - return flag.index - PreferencesFlag.broadcastCard.index; - } + int getDefaultCardIndex(PreferencesFlag flag) => + flag.index - PreferencesFlag.aboutUsCard.index; bool get calendarViewSetting => _remoteConfigService.scheduleListViewDefault; } diff --git a/lib/features/more/settings/settings_view.dart b/lib/features/more/settings/settings_view.dart index e5a178f39..243e1fb6c 100644 --- a/lib/features/more/settings/settings_view.dart +++ b/lib/features/more/settings/settings_view.dart @@ -3,12 +3,13 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:notredame/features/more/settings/widgets/settings_language_selector.dart'; +import 'package:notredame/features/more/settings/widgets/settings_theme_selector.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/more/settings/settings_viewmodel.dart'; -import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/features/app/widgets/base_scaffold.dart'; class SettingsView extends StatefulWidget { @override @@ -21,103 +22,32 @@ class _SettingsViewState extends State { ViewModelBuilder.reactive( viewModelBuilder: () => SettingsViewModel(intl: AppIntl.of(context)!), builder: (context, model, child) => BaseScaffold( + showBottomBar: false, appBar: AppBar( title: Text(AppIntl.of(context)!.settings_title), ), body: ListView( padding: EdgeInsets.zero, children: [ - ListTile( - title: Text( - AppIntl.of(context)!.settings_display_pref_category, - style: const TextStyle(color: AppTheme.etsLightRed), - ), - ), - PopupMenuButton( - offset: Offset(MediaQuery.of(context).size.width, 20), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - onSelected: (ThemeMode value) { - setState(() { - model.selectedTheme = value; - }); - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: ThemeMode.light, - child: ListTile( - title: Text(AppIntl.of(context)!.light_theme), - leading: const Icon(Icons.wb_sunny), - ), - ), - PopupMenuItem( - value: ThemeMode.dark, - child: ListTile( - title: Text(AppIntl.of(context)!.dark_theme), - leading: const Icon(Icons.nightlight_round), - ), - ), - PopupMenuItem( - value: ThemeMode.system, - child: ListTile( - title: Text(AppIntl.of(context)!.system_theme), - leading: const Icon(Icons.brightness_auto), - ), - ), - ], - child: ListTile( - leading: Icon(model.selectedTheme == ThemeMode.light - ? Icons.wb_sunny - : model.selectedTheme == ThemeMode.dark - ? Icons.nightlight_round - : Icons.brightness_auto), - title: Text(AppIntl.of(context)!.settings_dark_theme_pref), - subtitle: Text(model.selectedTheme == ThemeMode.light - ? AppIntl.of(context)!.light_theme - : model.selectedTheme == ThemeMode.dark - ? AppIntl.of(context)!.dark_theme - : AppIntl.of(context)!.system_theme), - trailing: const Icon(Icons.arrow_drop_down), - ), - ), + settingsThemeSelector(context, model, (ThemeMode value) { + setState(() { + model.selectedTheme = value; + }); + }), const Divider( thickness: 2, indent: 10, endIndent: 10, ), - ListTile( - title: Text( - AppIntl.of(context)!.settings_miscellaneous_category, - style: const TextStyle(color: AppTheme.etsLightRed), - ), - ), - PopupMenuButton( - offset: Offset(MediaQuery.of(context).size.width, 20), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - onSelected: (String value) { + settingsLanguageSelector( + context, + model, + (String value) { setState(() { model.currentLocale = value; }); }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: AppIntl.supportedLocales.first.languageCode, - child: Text(AppIntl.of(context)!.settings_english), - ), - PopupMenuItem( - value: AppIntl.supportedLocales.last.languageCode, - child: Text(AppIntl.of(context)!.settings_french), - ), - ], - child: ListTile( - leading: const Icon(Icons.language), - title: Text(AppIntl.of(context)!.settings_language_pref), - subtitle: Text(model.currentLocale), - trailing: const Icon(Icons.arrow_drop_down), - ), - ), + ) ], ), ), diff --git a/lib/features/more/settings/widget/language_card_widget.dart b/lib/features/more/settings/widget/language_card_widget.dart new file mode 100644 index 000000000..53e8278ae --- /dev/null +++ b/lib/features/more/settings/widget/language_card_widget.dart @@ -0,0 +1,36 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/utils/utils.dart'; + +class LanguageCardWidget extends StatelessWidget { + final String language; + final bool isSelected; + final VoidCallback onTap; + + const LanguageCardWidget({ + super.key, + required this.language, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Card( + color: + Utils.getColorByBrightness(context, Colors.white, Colors.grey[900]!), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text(language), + trailing: Icon(isSelected ? Icons.check : null), + onTap: onTap, + ), + ], + ), + ); + } +} diff --git a/lib/features/more/settings/widget/language_list_widget.dart b/lib/features/more/settings/widget/language_list_widget.dart new file mode 100644 index 000000000..7200cafb2 --- /dev/null +++ b/lib/features/more/settings/widget/language_list_widget.dart @@ -0,0 +1,31 @@ +// widgets/LanguageListViewWidget.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/features/more/settings/choose_language_viewmodel.dart'; +import 'package:notredame/features/more/settings/widget/language_card_widget.dart'; + +class LanguageListViewWidget extends StatelessWidget { + final ChooseLanguageViewModel model; + + const LanguageListViewWidget({super.key, required this.model}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(8), + itemCount: model.languages.length, + itemBuilder: (BuildContext context, int index) { + return LanguageCardWidget( + language: model.languages[index], + isSelected: model.languageSelectedIndex == index, + onTap: () => model.changeLanguage(index), + ); + }, + ); + } +} diff --git a/lib/features/more/settings/widgets/settings_language_selector.dart b/lib/features/more/settings/widgets/settings_language_selector.dart new file mode 100644 index 000000000..fe158abab --- /dev/null +++ b/lib/features/more/settings/widgets/settings_language_selector.dart @@ -0,0 +1,41 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/more/settings/settings_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; + +Widget settingsLanguageSelector(BuildContext context, SettingsViewModel model, + Function(String)? onSelected) => + Column(children: [ + ListTile( + title: Text( + AppIntl.of(context)!.settings_miscellaneous_category, + style: const TextStyle(color: AppTheme.etsLightRed), + ), + ), + PopupMenuButton( + offset: Offset(MediaQuery.of(context).size.width, 20), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + onSelected: onSelected, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: AppIntl.supportedLocales.first.languageCode, + child: Text(AppIntl.of(context)!.settings_english), + ), + PopupMenuItem( + value: AppIntl.supportedLocales.last.languageCode, + child: Text(AppIntl.of(context)!.settings_french), + ), + ], + child: ListTile( + leading: const Icon(Icons.language), + title: Text(AppIntl.of(context)!.settings_language_pref), + subtitle: Text(model.currentLocale), + trailing: const Icon(Icons.arrow_drop_down), + ), + ), + ]); diff --git a/lib/features/more/settings/widgets/settings_theme_selector.dart b/lib/features/more/settings/widgets/settings_theme_selector.dart new file mode 100644 index 000000000..242bb55ec --- /dev/null +++ b/lib/features/more/settings/widgets/settings_theme_selector.dart @@ -0,0 +1,65 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/more/settings/settings_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; + +Widget settingsThemeSelector(BuildContext context, SettingsViewModel model, + Function(ThemeMode) onSelected) { + return Column( + children: [ + ListTile( + title: Text( + AppIntl.of(context)!.settings_display_pref_category, + style: const TextStyle(color: AppTheme.etsLightRed), + ), + ), + PopupMenuButton( + offset: Offset(MediaQuery.of(context).size.width, 20), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + onSelected: onSelected, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: ThemeMode.light, + child: ListTile( + title: Text(AppIntl.of(context)!.light_theme), + leading: const Icon(Icons.wb_sunny), + ), + ), + PopupMenuItem( + value: ThemeMode.dark, + child: ListTile( + title: Text(AppIntl.of(context)!.dark_theme), + leading: const Icon(Icons.nightlight_round), + ), + ), + PopupMenuItem( + value: ThemeMode.system, + child: ListTile( + title: Text(AppIntl.of(context)!.system_theme), + leading: const Icon(Icons.brightness_auto), + ), + ), + ], + child: ListTile( + leading: Icon(model.selectedTheme == ThemeMode.light + ? Icons.wb_sunny + : model.selectedTheme == ThemeMode.dark + ? Icons.nightlight_round + : Icons.brightness_auto), + title: Text(AppIntl.of(context)!.settings_dark_theme_pref), + subtitle: Text(model.selectedTheme == ThemeMode.light + ? AppIntl.of(context)!.light_theme + : model.selectedTheme == ThemeMode.dark + ? AppIntl.of(context)!.dark_theme + : AppIntl.of(context)!.system_theme), + trailing: const Icon(Icons.arrow_drop_down), + ), + ), + ], + ); +} diff --git a/lib/features/more/widget/about_applets_tile.dart b/lib/features/more/widget/about_applets_tile.dart new file mode 100644 index 000000000..e6eb48bc9 --- /dev/null +++ b/lib/features/more/widget/about_applets_tile.dart @@ -0,0 +1,35 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:stacked/stacked.dart'; + +// Project imports: +import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/more/more_viewmodel.dart'; +import 'package:notredame/utils/locator.dart'; + +class AboutAppletsTile extends ViewModelWidget { + final AnalyticsService _analyticsService = locator(); + + @override + Widget build(BuildContext context, MoreViewModel model) { + return ListTile( + title: Text(AppIntl.of(context)!.more_about_applets_title), + leading: Hero( + tag: 'about', + child: Image.asset( + "assets/images/favicon_applets.png", + height: 24, + width: 24, + ), + ), + onTap: () { + _analyticsService.logEvent("MoreView", "About App|ETS clicked"); + model.navigationService.pushNamed(RouterPaths.about); + }, + ); + } +} diff --git a/lib/features/more/widget/contributors_list_tile.dart b/lib/features/more/widget/contributors_list_tile.dart new file mode 100644 index 000000000..f2d1308fb --- /dev/null +++ b/lib/features/more/widget/contributors_list_tile.dart @@ -0,0 +1,37 @@ +// widgets/contributors_tile.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:stacked/stacked.dart'; + +// Project imports: +import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/more/more_viewmodel.dart'; +import 'package:notredame/utils/locator.dart'; + +class ContributorsTile extends ViewModelWidget { + final AnalyticsService _analyticsService = locator(); + static const String tag = "MoreView"; + final bool isDiscoveryOverlayActive; + + ContributorsTile({super.key, required this.isDiscoveryOverlayActive}); + + @override + Widget build(BuildContext context, MoreViewModel model) { + return ListTile( + title: Text(AppIntl.of(context)!.more_contributors), + leading: (Theme.of(context).brightness == Brightness.dark && + isDiscoveryOverlayActive) + ? const Icon(Icons.people_outline, color: Colors.black) + : const Icon(Icons.people_outline), + onTap: () { + _analyticsService.logEvent(tag, "Contributors clicked"); + model.navigationService.pushNamed(RouterPaths.contributors); + }, + ); + } +} diff --git a/lib/features/more/widget/in_app_review_tile.dart b/lib/features/more/widget/in_app_review_tile.dart new file mode 100644 index 000000000..e535932f0 --- /dev/null +++ b/lib/features/more/widget/in_app_review_tile.dart @@ -0,0 +1,27 @@ +// widgets/in_app_review_tile.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:stacked/stacked.dart'; + +// Project imports: +import 'package:notredame/features/more/more_viewmodel.dart'; + +class InAppReviewTile extends ViewModelWidget { + const InAppReviewTile({super.key}); + + @override + Widget build(BuildContext context, MoreViewModel model) { + return ListTile( + title: Text(AppIntl.of(context)!.in_app_review_title), + leading: const Icon(Icons.rate_review), + onTap: () { + model.analyticsService.logEvent("MoreView", "Rate us clicked"); + MoreViewModel.launchInAppReview(); + }, + ); + } +} diff --git a/lib/features/more/widget/open_source_licenses_list_tile.dart b/lib/features/more/widget/open_source_licenses_list_tile.dart new file mode 100644 index 000000000..02404bbbf --- /dev/null +++ b/lib/features/more/widget/open_source_licenses_list_tile.dart @@ -0,0 +1,66 @@ +// widgets/open_source_licenses_tile.dart + +// Flutter imports: +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:stacked/stacked.dart'; + +// Project imports: +import 'package:notredame/features/more/more_viewmodel.dart'; +import 'package:notredame/utils/utils.dart'; + +class OpenSourceLicensesTile extends ViewModelWidget { + const OpenSourceLicensesTile({super.key}); + + @override + Widget build(BuildContext context, MoreViewModel model) { + return ListTile( + title: Text(AppIntl.of(context)!.more_open_source_licenses), + leading: const Icon(Icons.code), + onTap: () { + model.analyticsService.logEvent("MoreView", "Rate us clicked"); + Navigator.of(context).push(PageRouteBuilder( + pageBuilder: (context, _, __) => AboutDialog( + applicationIcon: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 75, + child: Image.asset('assets/images/favicon_applets.png')), + ), + applicationName: AppIntl.of(context)!.more_open_source_licenses, + applicationVersion: model.appVersion, + applicationLegalese: '\u{a9} ${DateTime.now().year} App|ETS', + children: aboutBoxChildren(context, model), + ), + opaque: false, + )); + }, + ); + } + + List aboutBoxChildren(BuildContext context, MoreViewModel model) { + final textStyle = Theme.of(context).textTheme.bodyMedium!; + return [ + const SizedBox(height: 24), + RichText( + text: TextSpan( + children: [ + TextSpan( + style: textStyle, text: AppIntl.of(context)!.flutter_license), + TextSpan( + style: textStyle.copyWith(color: Colors.blue), + text: AppIntl.of(context)!.flutter_website, + recognizer: TapGestureRecognizer() + ..onTap = () => Utils.launchURL( + AppIntl.of(context)!.flutter_website, + AppIntl.of(context)!)), + TextSpan(style: textStyle, text: '.'), + ], + ), + ), + ]; + } +} diff --git a/lib/features/more/widget/report_bug_tile.dart b/lib/features/more/widget/report_bug_tile.dart new file mode 100644 index 000000000..16faa5af8 --- /dev/null +++ b/lib/features/more/widget/report_bug_tile.dart @@ -0,0 +1,37 @@ +// widgets/report_bug_tile.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:stacked/stacked.dart'; + +// Project imports: +import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/more/more_viewmodel.dart'; +import 'package:notredame/utils/locator.dart'; + +class ReportBugTile extends ViewModelWidget { + final AnalyticsService _analyticsService = locator(); + static const String tag = "MoreView"; + final bool isDiscoveryOverlayActive; + + ReportBugTile({super.key, required this.isDiscoveryOverlayActive}); + + @override + Widget build(BuildContext context, MoreViewModel model) { + return ListTile( + title: Text(AppIntl.of(context)!.more_report_bug), + leading: (Theme.of(context).brightness == Brightness.dark && + isDiscoveryOverlayActive) + ? const Icon(Icons.bug_report, color: Colors.black) + : const Icon(Icons.bug_report), + onTap: () { + _analyticsService.logEvent(tag, "Report a bug clicked"); + model.navigationService.pushNamed(RouterPaths.feedback); + }, + ); + } +} diff --git a/lib/features/schedule/schedule_default/schedule_default.dart b/lib/features/schedule/schedule_default/schedule_default.dart index a3524762e..082d2e705 100644 --- a/lib/features/schedule/schedule_default/schedule_default.dart +++ b/lib/features/schedule/schedule_default/schedule_default.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:calendar_view/calendar_view.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; // Project imports: import 'package:notredame/features/schedule/widgets/schedule_calendar_tile.dart'; @@ -11,8 +12,10 @@ import 'package:notredame/utils/app_theme.dart'; class ScheduleDefault extends StatefulWidget { final List> calendarEvents; + final bool loaded; - const ScheduleDefault({super.key, required this.calendarEvents}); + const ScheduleDefault( + {super.key, required this.calendarEvents, required this.loaded}); @override _ScheduleDefaultState createState() => _ScheduleDefaultState(); @@ -27,7 +30,7 @@ class _ScheduleDefaultState extends State { @override Widget build(BuildContext context) { // Check if there are no events - if (widget.calendarEvents.isEmpty) { + if (widget.calendarEvents.isEmpty && widget.loaded) { return Scaffold( backgroundColor: Colors.transparent, body: Center( @@ -37,38 +40,47 @@ class _ScheduleDefaultState extends State { ), ); } - + final double heightPerMinute = + (MediaQuery.of(context).size.height / 1200).clamp(0.45, 1.0); // If there are events, display the calendar return Scaffold( - body: AbsorbPointer( - child: WeekView( - key: weekViewKey, - controller: eventController..addAll(widget.calendarEvents), - backgroundColor: Theme.of(context).brightness == Brightness.light - ? AppTheme.lightThemeBackground - : AppTheme.darkThemeBackground, - weekDays: const [ - WeekDays.monday, - WeekDays.tuesday, - WeekDays.wednesday, - WeekDays.thursday, - WeekDays.friday, - WeekDays.saturday - ], - scrollOffset: 340, - liveTimeIndicatorSettings: LiveTimeIndicatorSettings.none(), - headerStyle: const HeaderStyle( - headerTextStyle: TextStyle(fontSize: 0), - leftIconVisible: false, - rightIconVisible: false, - decoration: BoxDecoration(color: Colors.transparent)), - heightPerMinute: 0.72, - eventTileBuilder: (date, events, boundary, startDuration, - endDuration) => - _buildEventTile( - date, events, boundary, startDuration, endDuration, context), - weekDayBuilder: (DateTime date) => _buildWeekDay(date), - ), + body: WeekView( + maxDay: DateTime.now(), + minDay: DateTime.now(), + key: weekViewKey, + safeAreaOption: const SafeAreaOption(bottom: false), + controller: eventController..addAll(widget.calendarEvents), + backgroundColor: Theme.of(context).brightness == Brightness.light + ? AppTheme.lightThemeBackground + : AppTheme.primaryDark, + weekDays: const [ + WeekDays.monday, + WeekDays.tuesday, + WeekDays.wednesday, + WeekDays.thursday, + WeekDays.friday, + WeekDays.saturday + ], + hourIndicatorSettings: HourIndicatorSettings( + color: Theme.of(context).brightness == Brightness.light + ? AppTheme.scheduleLineColorLight + : AppTheme.scheduleLineColorDark), + scrollOffset: heightPerMinute * 60 * 7.5, + timeLineStringBuilder: (date, {secondaryDate}) { + return DateFormat('H:mm').format(date); + }, + liveTimeIndicatorSettings: LiveTimeIndicatorSettings.none(), + weekNumberBuilder: (date) => null, + headerStyle: const HeaderStyle( + headerTextStyle: TextStyle(fontSize: 0), + leftIconVisible: false, + rightIconVisible: false, + decoration: BoxDecoration(color: Colors.transparent)), + heightPerMinute: heightPerMinute, + eventTileBuilder: (date, events, boundary, startDuration, endDuration) => + _buildEventTile( + date, events, boundary, startDuration, endDuration, context), + weekDayBuilder: (DateTime date) => _buildWeekDay(date), )); } diff --git a/lib/features/schedule/schedule_default/schedule_default_view.dart b/lib/features/schedule/schedule_default/schedule_default_view.dart index 75e1d6119..5383b7eb8 100644 --- a/lib/features/schedule/schedule_default/schedule_default_view.dart +++ b/lib/features/schedule/schedule_default/schedule_default_view.dart @@ -31,6 +31,7 @@ class _ScheduleDefaultViewState extends State { ScheduleDefaultViewModel(sessionCode: widget.sessionCode), builder: (context, model, child) => BaseScaffold( showBottomBar: false, + safeArea: false, isLoading: model.busy(model.isLoadingEvents), appBar: AppBar( title: Text(_sessionName(widget.sessionCode!, AppIntl.of(context)!)), @@ -45,7 +46,9 @@ class _ScheduleDefaultViewState extends State { body: RefreshIndicator( child: model.isBusy ? const Center(child: CircularProgressIndicator()) - : ScheduleDefault(calendarEvents: model.calendarEvents), + : ScheduleDefault( + calendarEvents: model.calendarEvents, + loaded: !model.busy(model.isLoadingEvents)), onRefresh: () => model.refresh(), ), ), diff --git a/lib/features/schedule/schedule_default/schedule_default_viewmodel.dart b/lib/features/schedule/schedule_default/schedule_default_viewmodel.dart index 27c8cffcc..3d1c70ff5 100644 --- a/lib/features/schedule/schedule_default/schedule_default_viewmodel.dart +++ b/lib/features/schedule/schedule_default/schedule_default_viewmodel.dart @@ -61,7 +61,7 @@ class ScheduleDefaultViewModel return CalendarEventData( title: title, description: - "${eventData.courseAcronym};$courseLocation;${eventData.courseTitle};Je suis un nom de prof", + "${eventData.courseAcronym};$courseLocation;${eventData.courseTitle};null", date: targetDate, startTime: newStartTime, endTime: newEndTime, diff --git a/lib/features/schedule/schedule_view.dart b/lib/features/schedule/schedule_view.dart index 7fe9a7e03..1a375c0d4 100644 --- a/lib/features/schedule/schedule_view.dart +++ b/lib/features/schedule/schedule_view.dart @@ -174,6 +174,9 @@ class _ScheduleViewState extends State final chevronColor = Theme.of(context).brightness == Brightness.light ? AppTheme.primaryDark : AppTheme.lightThemeBackground; + final textColor = Theme.of(context).brightness == Brightness.light + ? AppTheme.primaryDark + : AppTheme.lightThemeAccent; final scheduleCardsPalette = Theme.of(context).brightness == Brightness.light ? AppTheme.schedulePaletteLight.toList() @@ -183,8 +186,15 @@ class _ScheduleViewState extends State DateTime.now(), eventController, scheduleCardsPalette); if (model.calendarFormat == CalendarFormat.month) { - return _buildCalendarViewMonthly(model, context, eventController, - backgroundColor, chevronColor, scheduleCardsPalette); + return _buildCalendarViewMonthly( + model, + context, + eventController, + backgroundColor, + chevronColor, + scheduleLineColor, + textColor, + scheduleCardsPalette); } return _buildCalendarViewWeekly(model, context, eventController, backgroundColor, chevronColor, scheduleLineColor, scheduleCardsPalette); @@ -198,8 +208,11 @@ class _ScheduleViewState extends State Color chevronColor, Color scheduleLineColor, List scheduleCardsPalette) { + final double heightPerMinute = + (MediaQuery.of(context).size.height / 1200).clamp(0.45, 1.0); return calendar_view.WeekView( key: weekViewKey, + weekNumberBuilder: (date) => null, controller: eventController ..addAll(model.selectedWeekCalendarEvents(scheduleCardsPalette)), onPageChange: (date, page) => @@ -209,7 +222,8 @@ class _ScheduleViewState extends State (MediaQuery.of(context).orientation == Orientation.portrait) ? 60 : 35, - safeAreaOption: const calendar_view.SafeAreaOption(top: false), + safeAreaOption: + const calendar_view.SafeAreaOption(top: false, bottom: false), headerStyle: calendar_view.HeaderStyle( decoration: BoxDecoration( color: backgroundColor, @@ -238,22 +252,17 @@ class _ScheduleViewState extends State calendar_view.WeekDays.sunday, ], initialDay: DateTime.now(), - heightPerMinute: - (MediaQuery.of(context).orientation == Orientation.portrait) - ? 0.65 - : 0.45, - // height occupied by 1 minute time span. + heightPerMinute: heightPerMinute, + scrollOffset: heightPerMinute * 60 * 7.5, hourIndicatorSettings: calendar_view.HourIndicatorSettings( color: scheduleLineColor, ), liveTimeIndicatorSettings: calendar_view.LiveTimeIndicatorSettings( color: chevronColor, ), - scrollOffset: (MediaQuery.of(context).orientation == Orientation.portrait) - ? 305 - : 220, + keepScrollOffset: true, timeLineStringBuilder: (date, {secondaryDate}) { - return DateFormat('HH:mm').format(date); + return DateFormat('H:mm').format(date); }, weekDayStringBuilder: (p0) { return weekTitles[p0]; @@ -277,19 +286,30 @@ class _ScheduleViewState extends State calendar_view.EventController eventController, Color backgroundColor, Color chevronColor, + Color scheduleLineColor, + Color textColor, List scheduleCardsPalette) { return calendar_view.MonthView( key: monthViewKey, + borderColor: scheduleLineColor, controller: eventController ..addAll(model.selectedMonthCalendarEvents(scheduleCardsPalette)), - // to provide custom UI for month cells. - cellAspectRatio: - (MediaQuery.of(context).orientation == Orientation.portrait) - ? 0.78 - : 1.2, - safeAreaOption: const calendar_view.SafeAreaOption(top: false), + cellAspectRatio: 0.8, + safeAreaOption: + const calendar_view.SafeAreaOption(top: false, bottom: false), + useAvailableVerticalSpace: MediaQuery.of(context).size.height >= 500, onPageChange: (date, page) => model.handleViewChanged(date, eventController, []), + weekDayBuilder: (int value) => calendar_view.WeekDayTile( + dayIndex: value, + displayBorder: false, + textStyle: TextStyle(color: textColor), + backgroundColor: backgroundColor, + weekDayStringBuilder: (p0) => weekTitles[p0]), + headerStringBuilder: (date, {secondaryDate}) { + final locale = AppIntl.of(context)!.localeName; + return '${DateFormat.MMMM(locale).format(date).characters.first.toUpperCase()}${DateFormat.MMMM(locale).format(date).substring(1)} ${date.year}'; + }, headerStyle: calendar_view.HeaderStyle( decoration: BoxDecoration( color: backgroundColor, @@ -304,15 +324,20 @@ class _ScheduleViewState extends State size: 30, color: chevronColor, )), - weekDayStringBuilder: (p0) { - return weekTitles[p0]; - }, - headerStringBuilder: (date, {secondaryDate}) { - final locale = AppIntl.of(context)!.localeName; - return '${DateFormat.MMMM(locale).format(date).characters.first.toUpperCase()}${DateFormat.MMMM(locale).format(date).substring(1)} ${date.year}'; - }, startDay: calendar_view.WeekDays.sunday, initialMonth: DateTime(DateTime.now().year, DateTime.now().month), + cellBuilder: (date, events, _, __, ___) => calendar_view.FilledCell( + hideDaysNotInMonth: false, + titleColor: textColor, + highlightColor: AppTheme.accent, + shouldHighlight: date.getDayDifference(DateTime.now()) == 0, + date: date, + isInMonth: date.month == DateTime.now().month, + events: events, + backgroundColor: (date.month == DateTime.now().month) + ? backgroundColor.withAlpha(128) + : Colors.grey.withOpacity(0.1), + ), ); } diff --git a/lib/features/schedule/schedule_viewmodel.dart b/lib/features/schedule/schedule_viewmodel.dart index 1377ca3e5..d9e7599e9 100644 --- a/lib/features/schedule/schedule_viewmodel.dart +++ b/lib/features/schedule/schedule_viewmodel.dart @@ -396,7 +396,8 @@ class ScheduleViewModel extends FutureViewModel> { /// return false otherwise (today was already selected, show toast for /// visual feedback). bool selectToday() { - if (compareDates(selectedDate, DateTime.now())) { + if (compareDates(selectedDate, DateTime.now()) && + compareDates(focusedDate.value, DateTime.now())) { Fluttertoast.showToast(msg: _appIntl.schedule_already_today_toast); return false; } else { diff --git a/lib/features/schedule/widgets/schedule_calendar_tile.dart b/lib/features/schedule/widgets/schedule_calendar_tile.dart index 08f70f077..f32736b37 100644 --- a/lib/features/schedule/widgets/schedule_calendar_tile.dart +++ b/lib/features/schedule/widgets/schedule_calendar_tile.dart @@ -74,13 +74,14 @@ class _ScheduleCalendarTileState extends State { fontWeight: FontWeight.w500, ), ), - Text( - "${AppIntl.of(widget.buildContext)!.schedule_calendar_by} $teacherName", - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, + if (teacherName != "null") + Text( + "${AppIntl.of(widget.buildContext)!.schedule_calendar_by} $teacherName", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), ), - ), Text( "${AppIntl.of(widget.buildContext)!.schedule_calendar_from_time} $startTime ${AppIntl.of(widget.buildContext)!.schedule_calendar_to_time} $endTime", style: const TextStyle( diff --git a/lib/features/student/grades/grade_details/grade_details_view.dart b/lib/features/student/grades/grade_details/grade_details_view.dart index bd583bd77..4f6279a30 100644 --- a/lib/features/student/grades/grade_details/grade_details_view.dart +++ b/lib/features/student/grades/grade_details/grade_details_view.dart @@ -4,18 +4,18 @@ import 'package:flutter/scheduler.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:notredame/features/app/signets-api/models/course_evaluation.dart'; +import 'package:notredame/features/student/grades/widgets/grade_circular_progress.dart'; +import 'package:notredame/features/student/grades/widgets/grade_evaluation_tile.dart'; +import 'package:notredame/features/student/grades/widgets/grade_not_available.dart'; +import 'package:notredame/utils/utils.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/signets-api/models/course.dart'; -import 'package:notredame/features/app/signets-api/models/course_evaluation.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/student/grades/grade_details/grades_details_viewmodel.dart'; -import 'package:notredame/features/student/grades/widgets/grade_circular_progress.dart'; -import 'package:notredame/features/student/grades/widgets/grade_evaluation_tile.dart'; -import 'package:notredame/features/student/grades/widgets/grade_not_available.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/utils/utils.dart'; class GradesDetailsView extends StatefulWidget { final Course course; @@ -58,78 +58,75 @@ class _GradesDetailsViewState extends State builder: (context, model, child) => BaseScaffold( safeArea: false, showBottomBar: false, - body: Material( - child: NestedScrollView( - headerSliverBuilder: (context, innerBoxScrolled) => [ - SliverAppBar( - backgroundColor: - Theme.of(context).brightness == Brightness.light - ? AppTheme.etsLightRed - : BottomAppBarTheme.of(context).color, - pinned: true, - onStretchTrigger: () { - return Future.value(); - }, - titleSpacing: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), - ), - title: Hero( - tag: - 'course_acronym_${model.course.acronym}_${model.course.session}', - child: Text( - model.course.acronym, - softWrap: false, - overflow: TextOverflow.visible, - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: Colors.white, - fontSize: 25, - fontWeight: FontWeight.bold), - ), + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxScrolled) => [ + SliverAppBar( + backgroundColor: + Theme.of(context).brightness == Brightness.light + ? AppTheme.etsLightRed + : BottomAppBarTheme.of(context).color, + pinned: true, + onStretchTrigger: () { + return Future.value(); + }, + titleSpacing: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + title: Hero( + tag: + 'course_acronym_${model.course.acronym}_${model.course.session}', + child: Text( + model.course.acronym, + softWrap: false, + overflow: TextOverflow.visible, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Colors.white, + fontSize: 25, + fontWeight: FontWeight.bold), ), ), - SliverToBoxAdapter( - child: Center( - child: Container( - constraints: BoxConstraints( - minWidth: MediaQuery.of(context).size.width, - ), - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light - ? AppTheme.etsLightRed - : AppTheme.darkTheme().cardColor, - ), - child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: SafeArea( - top: false, - bottom: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildClassInfo(model.course.title), - if (model.course.teacherName != null) - _buildClassInfo(AppIntl.of(context)! - .grades_teacher(model.course.teacherName!)), - _buildClassInfo(AppIntl.of(context)! - .grades_group_number(model.course.group)), + ), + SliverToBoxAdapter( + child: Center( + child: Container( + constraints: BoxConstraints( + minWidth: MediaQuery.of(context).size.width, + ), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.light + ? AppTheme.etsLightRed + : AppTheme.darkTheme().cardColor, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: SafeArea( + top: false, + bottom: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildClassInfo(model.course.title), + if (model.course.teacherName != null) _buildClassInfo(AppIntl.of(context)! - .credits_number( - model.course.numberOfCredits)), - ], - ), + .grades_teacher(model.course.teacherName!)), + _buildClassInfo(AppIntl.of(context)! + .grades_group_number(model.course.group)), + _buildClassInfo(AppIntl.of(context)! + .credits_number(model.course.numberOfCredits)), + ], ), ), ), ), ), - ], - body: SafeArea( - top: false, - bottom: false, - child: _buildGradeEvaluations(model), ), + ], + body: SafeArea( + top: false, + bottom: false, + child: _buildGradeEvaluations(model), ), ), ), @@ -206,46 +203,53 @@ class _GradesDetailsViewState extends State ), ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - flex: 3, - child: _buildCourseGradeSummary( - AppIntl.of(context)!.grades_median, - validateGrade( - context, - model.course.summary?.median.toString(), - AppIntl.of(context)!.grades_grade_in_percentage( - Utils.getGradeInPercentage( - model.course.summary?.median, - model.course.summary?.markOutOf)), + IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + flex: 3, + child: _buildCourseGradeSummary( + AppIntl.of(context)!.grades_median, + validateGrade( + context, + model.course.summary?.median.toString(), + AppIntl.of(context)!.grades_grade_in_percentage( + Utils.getGradeInPercentage( + model.course.summary?.median, + model.course.summary?.markOutOf)), + ), ), ), - ), - Expanded( - flex: 3, - child: _buildCourseGradeSummary( - AppIntl.of(context)!.grades_standard_deviation, - validateGrade( - context, - model.course.summary?.standardDeviation.toString(), - model.course.summary?.standardDeviation.toString(), + Expanded( + flex: 3, + child: _buildCourseGradeSummary( + AppIntl.of(context)!.grades_standard_deviation, + validateGrade( + context, + model.course.summary?.standardDeviation + .toString(), + model.course.summary?.standardDeviation + .toString(), + ), ), ), - ), - Expanded( - flex: 3, - child: _buildCourseGradeSummary( - AppIntl.of(context)!.grades_percentile_rank, - validateGrade( - context, - model.course.summary?.percentileRank.toString(), - model.course.summary?.percentileRank.toString(), + Expanded( + flex: 3, + child: _buildCourseGradeSummary( + AppIntl.of(context)!.grades_percentile_rank, + validateGrade( + context, + model.course.summary?.percentileRank.toString(), + model.course.summary?.percentileRank.toString(), + ), ), ), - ), - ]), + ]), + ), + const SizedBox( + height: 8.0, + ), Column(children: [ for (final CourseEvaluation evaluation in model.course.summary?.evaluations ?? []) @@ -324,36 +328,24 @@ class _GradesDetailsViewState extends State } /// Build the card of the Medidian, Standard deviation or Percentile Rank - SizedBox _buildCourseGradeSummary(String? title, String number) { - return SizedBox( - height: 110, - width: MediaQuery.of(context).size.width / 3.1, - child: Card( + Widget _buildCourseGradeSummary(String? title, String number) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - flex: 5, - child: Padding( - padding: const EdgeInsets.only(top: 14.0), - child: Text( - title ?? "", - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - ), - ), + Text( + title ?? "", + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, ), ), - Expanded( - flex: 5, - child: Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text( - number, - style: const TextStyle(fontSize: 19), - ), - ), + const SizedBox(height: 4), + Text( + number, + style: const TextStyle(fontSize: 19), ), ], ), diff --git a/lib/features/student/grades/grade_details/widget/class_info.dart b/lib/features/student/grades/grade_details/widget/class_info.dart new file mode 100644 index 000000000..e776c750a --- /dev/null +++ b/lib/features/student/grades/grade_details/widget/class_info.dart @@ -0,0 +1,24 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class ClassInfo extends StatelessWidget { + final String info; + + const ClassInfo({required this.info}); + + @override + Widget build(BuildContext context) => Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: Text( + info, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Colors.white, fontSize: 16), + overflow: TextOverflow.ellipsis, + ), + ), + ); +} diff --git a/lib/features/student/grades/grade_details/widget/course_grade_summary.dart b/lib/features/student/grades/grade_details/widget/course_grade_summary.dart new file mode 100644 index 000000000..f29f4e44b --- /dev/null +++ b/lib/features/student/grades/grade_details/widget/course_grade_summary.dart @@ -0,0 +1,47 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class CourseGradeSummary extends StatelessWidget { + final String? title; + final String number; + + const CourseGradeSummary({required this.title, required this.number}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 110, + width: MediaQuery.of(context).size.width / 3.1, + child: Card( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: Padding( + padding: const EdgeInsets.only(top: 14.0), + child: Text( + title ?? "", + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ), + Expanded( + flex: 5, + child: Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text( + number, + style: const TextStyle(fontSize: 19), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/student/grades/grade_details/widget/grade_evaluations.dart b/lib/features/student/grades/grade_details/widget/grade_evaluations.dart new file mode 100644 index 000000000..aa64c8833 --- /dev/null +++ b/lib/features/student/grades/grade_details/widget/grade_evaluations.dart @@ -0,0 +1,184 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/app/signets-api/models/course_evaluation.dart'; +import 'package:notredame/features/student/grades/grade_details/grades_details_viewmodel.dart'; +import 'package:notredame/features/student/grades/grade_details/widget/course_grade_summary.dart'; +import 'package:notredame/features/student/grades/grade_details/widget/grade_summary.dart'; +import 'package:notredame/features/student/grades/widgets/grade_circular_progress.dart'; +import 'package:notredame/features/student/grades/widgets/grade_evaluation_tile.dart'; +import 'package:notredame/features/student/grades/widgets/grade_not_available.dart'; +import 'package:notredame/utils/utils.dart'; + +class GradeEvaluations extends StatelessWidget { + final GradesDetailsViewModel model; + final bool completed; + + const GradeEvaluations({required this.model, required this.completed}); + + @override + Widget build(BuildContext context) { + if (model.isBusy) { + return _buildLoadingIndicator(); + } else if (model.course.inReviewPeriod && + !(model.course.allReviewsCompleted ?? true)) { + return _buildEvaluationNotCompleted(); + } else if (model.course.summary != null) { + return _buildGradeDetails(context); + } else { + return _buildGradeNotAvailable(); + } + } + + Widget _buildLoadingIndicator() { + return const Center( + child: CircularProgressIndicator(), + ); + } + + Widget _buildEvaluationNotCompleted() { + return Center( + child: GradeNotAvailable( + key: const Key("EvaluationNotCompleted"), + onPressed: model.refresh, + isEvaluationPeriod: true, + ), + ); + } + + Widget _buildGradeNotAvailable() { + return Center( + child: GradeNotAvailable( + key: const Key("GradeNotAvailable"), + onPressed: model.refresh, + ), + ); + } + + Widget _buildGradeDetails(BuildContext context) { + return RefreshIndicator( + onRefresh: () => model.refresh(), + child: ListView( + padding: const EdgeInsets.all(5.0), + children: [ + _buildSummaryCard(context), + _buildAdditionalDetails(context), + ], + ), + ); + } + + Widget _buildSummaryCard(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 50, + child: GradeCircularProgress( + 1.0, + completed: completed, + key: const Key("GradeCircularProgress_summary"), + finalGrade: model.course.grade, + studentGrade: Utils.getGradeInPercentage( + model.course.summary?.currentMark, + model.course.summary?.markOutOf, + ), + averageGrade: Utils.getGradeInPercentage( + model.course.summary?.passMark, + model.course.summary?.markOutOf, + ), + ), + ), + Expanded( + flex: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GradesSummary( + currentGrade: model.course.summary?.currentMark, + maxGrade: model.course.summary?.markOutOf, + recipient: AppIntl.of(context)!.grades_current_rating, + color: Colors.green, + ), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: GradesSummary( + currentGrade: model.course.summary?.passMark, + maxGrade: model.course.summary?.markOutOf, + recipient: AppIntl.of(context)!.grades_average, + color: Colors.red, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildAdditionalDetails(BuildContext context) { + return Column( + children: [ + _buildAdditionalSummary(context), + _buildEvaluationTiles(), + ], + ); + } + + Widget _buildAdditionalSummary(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + flex: 3, + child: CourseGradeSummary( + title: AppIntl.of(context)!.grades_median, + number: model.course.summary?.median.toString() ?? + AppIntl.of(context)!.grades_not_available, + ), + ), + Expanded( + flex: 3, + child: CourseGradeSummary( + title: AppIntl.of(context)!.grades_standard_deviation, + number: model.course.summary?.standardDeviation.toString() ?? + AppIntl.of(context)!.grades_not_available, + ), + ), + Expanded( + flex: 3, + child: CourseGradeSummary( + title: AppIntl.of(context)!.grades_percentile_rank, + number: model.course.summary?.percentileRank.toString() ?? + AppIntl.of(context)!.grades_not_available, + ), + ), + ], + ); + } + + Widget _buildEvaluationTiles() { + return Column( + children: [ + for (final CourseEvaluation evaluation + in model.course.summary?.evaluations ?? []) + GradeEvaluationTile( + evaluation, + completed: completed, + key: Key("GradeEvaluationTile_${evaluation.title}"), + isFirstEvaluation: + evaluation == model.course.summary?.evaluations.first, + ), + ], + ); + } +} diff --git a/lib/features/student/grades/grade_details/widget/grade_summary.dart b/lib/features/student/grades/grade_details/widget/grade_summary.dart new file mode 100644 index 000000000..244491b5d --- /dev/null +++ b/lib/features/student/grades/grade_details/widget/grade_summary.dart @@ -0,0 +1,50 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/utils/utils.dart'; + +class GradesSummary extends StatelessWidget { + final double? currentGrade; + final double? maxGrade; + final String recipient; + final Color color; + + const GradesSummary({ + required this.currentGrade, + required this.maxGrade, + required this.recipient, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.fitWidth, + child: Text( + AppIntl.of(context)!.grades_grade_with_percentage( + currentGrade ?? 0.0, + maxGrade ?? 0.0, + Utils.getGradeInPercentage( + currentGrade, + maxGrade, + ), + ), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: color)), + ), + Text(recipient, + style: + Theme.of(context).textTheme.bodyLarge!.copyWith(color: color)), + ], + ); + } +} diff --git a/lib/features/student/grades/grades_view.dart b/lib/features/student/grades/grades_view.dart index 91b7cba1d..68336f79c 100644 --- a/lib/features/student/grades/grades_view.dart +++ b/lib/features/student/grades/grades_view.dart @@ -1,20 +1,18 @@ +// GradesView.dart + // Flutter imports: import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; -import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:notredame/features/student/grades/grades_viewmodel.dart'; -import 'package:notredame/features/student/grades/widgets/grade_button.dart'; -import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/features/student/grades/widget-grade-view/empty_grades_message.dart'; +import 'package:notredame/features/student/grades/widget-grade-view/grade_list.dart'; import 'package:notredame/utils/loading.dart'; import 'package:notredame/utils/locator.dart'; @@ -26,9 +24,6 @@ class GradesView extends StatefulWidget { class _GradesViewState extends State { final AnalyticsService _analyticsService = locator(); - /// Used to redirect on the dashboard. - final NavigationService _navigationService = locator(); - @override void initState() { super.initState(); @@ -49,40 +44,11 @@ class _GradesViewState extends State { onRefresh: () => model.refresh(), child: Stack( children: [ - // This widget is here to make this widget a Scrollable. Needed - // by the RefreshIndicator ListView(), if (model.coursesBySession.isEmpty) - Center( - child: Text(AppIntl.of(context)!.grades_msg_no_grades, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge)) + EmptyGradesMessage() else - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: AnimationLimiter( - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: model.coursesBySession.length, - itemBuilder: (BuildContext context, int index) => - AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 750), - child: SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: _buildSessionCourses( - index, - _sessionName(model.sessionOrder[index], - AppIntl.of(context)!), - model.coursesBySession[ - model.sessionOrder[index]]!, - model), - ), - ), - )), - ), - ), + GradeList(model: model), if (model.isBusy) buildLoading(isInteractionLimitedWhileLoading: false) else @@ -92,56 +58,4 @@ class _GradesViewState extends State { ); }); } - - /// Build a session which is the name of the session and one [GradeButton] for - /// each [Course] in [courses] - Widget _buildSessionCourses(int index, String sessionName, - List courses, GradesViewModel model) => - Padding( - padding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - sessionName, - style: const TextStyle( - fontSize: 25, - color: AppTheme.etsLightRed, - ), - ), - IconButton( - icon: const Icon(Icons.today, color: AppTheme.etsDarkGrey), - onPressed: () => _navigationService.pushNamed( - RouterPaths.defaultSchedule, - arguments: model.sessionOrder[index]), - ), - ], - ), - const SizedBox(height: 16.0), - Wrap( - children: courses - .map((course) => - GradeButton(course, showDiscovery: index == 0)) - .toList(), - ), - ], - ), - ); - - /// Build the complete name of the session for the user local. - String _sessionName(String shortName, AppIntl intl) { - switch (shortName[0]) { - case 'H': - return "${intl.session_winter} ${shortName.substring(1)}"; - case 'A': - return "${intl.session_fall} ${shortName.substring(1)}"; - case 'É': - return "${intl.session_summer} ${shortName.substring(1)}"; - default: - return intl.session_without; - } - } } diff --git a/lib/features/student/grades/widget-grade-view/empty_grades_message.dart b/lib/features/student/grades/widget-grade-view/empty_grades_message.dart new file mode 100644 index 000000000..d7f61e286 --- /dev/null +++ b/lib/features/student/grades/widget-grade-view/empty_grades_message.dart @@ -0,0 +1,16 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class EmptyGradesMessage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Text(AppIntl.of(context)!.grades_msg_no_grades, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge), + ); + } +} diff --git a/lib/features/student/grades/widget-grade-view/grade_list.dart b/lib/features/student/grades/widget-grade-view/grade_list.dart new file mode 100644 index 000000000..1ac81526c --- /dev/null +++ b/lib/features/student/grades/widget-grade-view/grade_list.dart @@ -0,0 +1,62 @@ +// widgets/GradeList.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; + +// Project imports: +import 'package:notredame/features/student/grades/grades_viewmodel.dart'; +import 'package:notredame/features/student/grades/widget-grade-view/grade_session_courses.dart'; + +class GradeList extends StatelessWidget { + final GradesViewModel model; + + const GradeList({required this.model}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: AnimationLimiter( + child: ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: model.coursesBySession.length, + itemBuilder: (BuildContext context, int index) => + AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 750), + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: GradeSessionCourses( + index: index, + sessionName: _sessionName( + model.sessionOrder[index], AppIntl.of(context)!), + courses: + model.coursesBySession[model.sessionOrder[index]]!, + sessionOrder: model.sessionOrder, + ), + ), + ), + )), + ), + ); + } + + /// Build the complete name of the session for the user local. + String _sessionName(String shortName, AppIntl intl) { + switch (shortName[0]) { + case 'H': + return "${intl.session_winter} ${shortName.substring(1)}"; + case 'A': + return "${intl.session_fall} ${shortName.substring(1)}"; + case 'É': + return "${intl.session_summer} ${shortName.substring(1)}"; + default: + return intl.session_without; + } + } +} diff --git a/lib/features/student/grades/widget-grade-view/grade_session_courses.dart b/lib/features/student/grades/widget-grade-view/grade_session_courses.dart new file mode 100644 index 000000000..c4ee77d17 --- /dev/null +++ b/lib/features/student/grades/widget-grade-view/grade_session_courses.dart @@ -0,0 +1,62 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/student/grades/widgets/grade_button.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; + +class GradeSessionCourses extends StatelessWidget { + final int index; + final String sessionName; + final List courses; + final List sessionOrder; + + GradeSessionCourses({ + required this.index, + required this.sessionName, + required this.courses, + required this.sessionOrder, + }); + + final NavigationService _navigationService = locator(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + sessionName, + style: const TextStyle( + fontSize: 25, + color: AppTheme.etsLightRed, + ), + ), + IconButton( + icon: const Icon(Icons.today, color: AppTheme.etsDarkGrey), + onPressed: () => _navigationService.pushNamed( + RouterPaths.defaultSchedule, + arguments: sessionOrder[index]), + ), + ], + ), + const SizedBox(height: 16.0), + Wrap( + children: courses + .map((course) => GradeButton(course, showDiscovery: index == 0)) + .toList(), + ), + ], + ), + ); + } +} diff --git a/lib/features/student/grades/widgets/grade_evaluation_tile.dart b/lib/features/student/grades/widgets/grade_evaluation_tile.dart index bbeed31a6..521c54fc5 100644 --- a/lib/features/student/grades/widgets/grade_evaluation_tile.dart +++ b/lib/features/student/grades/widgets/grade_evaluation_tile.dart @@ -63,61 +63,61 @@ class _GradeEvaluationTileState extends State data: Theme.of(context).copyWith( dividerColor: Colors.transparent, unselectedWidgetColor: Colors.red, - colorScheme: - ColorScheme.fromSwatch().copyWith(secondary: Colors.red), ), - child: ExpansionTile( - onExpansionChanged: (value) { - setState(() { - showEvaluationDetails = !showEvaluationDetails; + child: Card( + color: Theme.of(context).brightness == Brightness.light + ? AppTheme.lightTheme().cardTheme.color + : AppTheme.darkTheme().cardColor, + clipBehavior: Clip.antiAlias, + child: ExpansionTile( + onExpansionChanged: (value) { + setState(() { + showEvaluationDetails = !showEvaluationDetails; - if (showEvaluationDetails) { - controller.reverse(from: pi); - } else { - controller.forward(from: 0.0); - } - }); - }, - leading: FractionallySizedBox( - heightFactor: 1.3, - alignment: Alignment.topCenter, - child: LayoutBuilder( - builder: (context, constraints) { - final GradeCircularProgress circularProgress = - GradeCircularProgress( - constraints.maxHeight / 100, - completed: widget.completed, - key: Key( - "GradeCircularProgress_${widget.evaluation.title}"), - studentGrade: Utils.getGradeInPercentage( - widget.evaluation.mark, - widget.evaluation.correctedEvaluationOutOfFormatted, - ), - averageGrade: Utils.getGradeInPercentage( - widget.evaluation.passMark, - widget.evaluation.correctedEvaluationOutOfFormatted, - ), - ); - - if (widget.isFirstEvaluation) { - return _buildDiscoveryFeatureDescriptionWidget( - context, - circularProgress, - DiscoveryIds.detailsGradeDetailsEvaluations); + if (showEvaluationDetails) { + controller.reverse(from: pi); + } else { + controller.forward(from: 0.0); } + }); + }, + leading: FractionallySizedBox( + heightFactor: 1.3, + child: LayoutBuilder( + builder: (context, constraints) { + final GradeCircularProgress circularProgress = + GradeCircularProgress( + constraints.maxHeight / 100, + completed: widget.completed, + key: Key( + "GradeCircularProgress_${widget.evaluation.title}"), + studentGrade: Utils.getGradeInPercentage( + widget.evaluation.mark, + widget.evaluation.correctedEvaluationOutOfFormatted, + ), + averageGrade: Utils.getGradeInPercentage( + widget.evaluation.passMark, + widget.evaluation.correctedEvaluationOutOfFormatted, + ), + ); - return circularProgress; - }, + if (widget.isFirstEvaluation) { + return _buildDiscoveryFeatureDescriptionWidget( + context, + circularProgress, + DiscoveryIds.detailsGradeDetailsEvaluations); + } + + return circularProgress; + }, + ), ), - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 30.0), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( + title: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( widget.evaluation.title, style: TextStyle( fontSize: 16, @@ -125,25 +125,19 @@ class _GradeEvaluationTileState extends State context, Colors.black, Colors.white), ), ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 10.0, bottom: 20.0), - child: Text( - AppIntl.of(context)! - .grades_weight(widget.evaluation.weight), - style: TextStyle( - fontSize: 14, - color: Utils.getColorByBrightness( - context, Colors.black, Colors.white), + Text( + AppIntl.of(context)! + .grades_weight(widget.evaluation.weight), + style: TextStyle( + fontSize: 14, + color: Utils.getColorByBrightness( + context, Colors.black, Colors.white), + ), ), - ), + ], ), - ], - ), - trailing: Padding( - padding: const EdgeInsets.only(top: 25.0, right: 10.0), - child: AnimatedBuilder( + ), + trailing: AnimatedBuilder( animation: rotateAnimation, builder: (BuildContext context, Widget? child) { return Transform.rotate( @@ -159,10 +153,10 @@ class _GradeEvaluationTileState extends State color: AppTheme.etsLightRed, ), ), + children: [ + _buildEvaluationSummary(context, widget.evaluation), + ], ), - children: [ - _buildEvaluationSummary(context, widget.evaluation), - ], ), ), ], @@ -171,56 +165,51 @@ class _GradeEvaluationTileState extends State Widget _buildEvaluationSummary( BuildContext context, CourseEvaluation evaluation) { return Padding( - padding: const EdgeInsets.only(left: 25.0, right: 15.0), - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.91, - child: Column( - children: [ - _buildSummary( - AppIntl.of(context)!.grades_grade, - AppIntl.of(context)!.grades_grade_with_percentage( - evaluation.mark ?? 0.0, - evaluation.correctedEvaluationOutOf, - Utils.getGradeInPercentage(evaluation.mark, - evaluation.correctedEvaluationOutOfFormatted), - ), + padding: const EdgeInsets.only(left: 20.0, right: 15.0, bottom: 20.0), + child: Column( + children: [ + _buildSummary( + AppIntl.of(context)!.grades_grade, + AppIntl.of(context)!.grades_grade_with_percentage( + evaluation.mark ?? 0.0, + evaluation.correctedEvaluationOutOf, + Utils.getGradeInPercentage(evaluation.mark, + evaluation.correctedEvaluationOutOfFormatted), ), - _buildSummary( - AppIntl.of(context)!.grades_average, - AppIntl.of(context)!.grades_grade_with_percentage( - evaluation.passMark ?? 0.0, - evaluation.correctedEvaluationOutOf, - Utils.getGradeInPercentage(evaluation.passMark, - evaluation.correctedEvaluationOutOfFormatted), - ), + ), + _buildSummary( + AppIntl.of(context)!.grades_average, + AppIntl.of(context)!.grades_grade_with_percentage( + evaluation.passMark ?? 0.0, + evaluation.correctedEvaluationOutOf, + Utils.getGradeInPercentage(evaluation.passMark, + evaluation.correctedEvaluationOutOfFormatted), ), - _buildSummary( - AppIntl.of(context)!.grades_median, - AppIntl.of(context)!.grades_grade_with_percentage( - evaluation.median ?? 0.0, - evaluation.correctedEvaluationOutOf, - Utils.getGradeInPercentage(evaluation.median, - evaluation.correctedEvaluationOutOfFormatted), - ), + ), + _buildSummary( + AppIntl.of(context)!.grades_median, + AppIntl.of(context)!.grades_grade_with_percentage( + evaluation.median ?? 0.0, + evaluation.correctedEvaluationOutOf, + Utils.getGradeInPercentage(evaluation.median, + evaluation.correctedEvaluationOutOfFormatted), ), - _buildSummary( - AppIntl.of(context)!.grades_weighted, - validateResultWithPercentage( - context, - evaluation.weightedGrade, - evaluation.weight, - Utils.getGradeInPercentage(evaluation.mark, - evaluation.correctedEvaluationOutOfFormatted))), - _buildSummary( - AppIntl.of(context)!.grades_standard_deviation, - validateResult( - context, evaluation.standardDeviation.toString())), - _buildSummary(AppIntl.of(context)!.grades_percentile_rank, - validateResult(context, evaluation.percentileRank.toString())), - _buildSummary(AppIntl.of(context)!.grades_target_date, - getDate(evaluation.targetDate, context)), - ], - ), + ), + _buildSummary( + AppIntl.of(context)!.grades_weighted, + validateResultWithPercentage( + context, + evaluation.weightedGrade, + evaluation.weight, + Utils.getGradeInPercentage(evaluation.mark, + evaluation.correctedEvaluationOutOfFormatted))), + _buildSummary(AppIntl.of(context)!.grades_standard_deviation, + validateResult(context, evaluation.standardDeviation.toString())), + _buildSummary(AppIntl.of(context)!.grades_percentile_rank, + validateResult(context, evaluation.percentileRank.toString())), + _buildSummary(AppIntl.of(context)!.grades_target_date, + getDate(evaluation.targetDate, context)), + ], ), ); } diff --git a/lib/features/student/grades/widgets/grade_not_available.dart b/lib/features/student/grades/widgets/grade_not_available.dart index b3fd17d87..006e7e3a9 100644 --- a/lib/features/student/grades/widgets/grade_not_available.dart +++ b/lib/features/student/grades/widgets/grade_not_available.dart @@ -18,6 +18,7 @@ class GradeNotAvailable extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( + padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.min, diff --git a/lib/features/student/profile/profile_view.dart b/lib/features/student/profile/profile_view.dart index 980ee632f..56a069a81 100644 --- a/lib/features/student/profile/profile_view.dart +++ b/lib/features/student/profile/profile_view.dart @@ -1,20 +1,22 @@ // Flutter imports: import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:percent_indicator/percent_indicator.dart'; +import 'package:notredame/features/student/profile/widgets/profile_balance_card.dart'; +import 'package:notredame/features/student/profile/widgets/profile_infos_card.dart'; +import 'package:notredame/features/student/profile/widgets/profile_main_infos_card.dart'; +import 'package:notredame/features/student/profile/widgets/profile_current_program_tile.dart'; +import 'package:notredame/features/student/profile/widgets/profile_program_completion_card.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/signets-api/models/program.dart'; import 'package:notredame/features/student/profile/profile_viewmodel.dart'; -import 'package:notredame/features/student/widgets/student_program.dart'; +import 'package:notredame/utils/locator.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/loading.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/student/widgets/student_program.dart'; class ProfileView extends StatefulWidget { @override @@ -86,7 +88,7 @@ Widget buildPage(BuildContext context, ProfileViewModel model) => Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 4.0), - child: getProgramCompletion(model, context), + child: getProgramCompletionCard(model, context), ), ], ), @@ -127,250 +129,3 @@ Widget buildPage(BuildContext context, ProfileViewModel model) => Column( const SizedBox(height: 10.0), ], ); - -Card getMainInfoCard(ProfileViewModel model) { - var programName = ""; - if (model.programList.isNotEmpty) { - programName = model.programList.last.name; - } - - return Card( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(5.0), - child: Text( - '${model.profileStudent.firstName} ${model.profileStudent.lastName}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: Text( - programName, - style: const TextStyle( - fontSize: 16, - ), - ), - ), - ], - ), - ), - ); -} - -Card getMyInfosCard(ProfileViewModel model, BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - Clipboard.setData( - ClipboardData(text: model.profileStudent.permanentCode)); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppIntl.of(context)! - .profile_permanent_code_copied_to_clipboard), - )); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 3.0), - child: Text( - AppIntl.of(context)!.profile_permanent_code, - style: const TextStyle( - fontSize: 16, - ), - ), - ), - Center( - child: Text( - model.profileStudent.permanentCode, - style: const TextStyle(fontSize: 14), - ), - ), - ], - ), - ), - GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: model.universalAccessCode)); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppIntl.of(context)! - .profile_universal_code_copied_to_clipboard), - )); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0, bottom: 3.0), - child: Text( - AppIntl.of(context)!.login_prompt_universal_code, - style: const TextStyle(fontSize: 16), - ), - ), - Center( - child: Text( - model.universalAccessCode, - style: const TextStyle(fontSize: 14), - ), - ), - ], - ), - ), - ], - ), - ), - ); -} - -Card getMyBalanceCard(ProfileViewModel model, BuildContext context) { - final stringBalance = model.profileStudent.balance; - var balance = 0.0; - - if (stringBalance.isNotEmpty) { - balance = double.parse(stringBalance - .substring(0, stringBalance.length - 1) - .replaceAll(",", ".")); - } - - return Card( - color: balance > 0 ? Colors.red : Colors.green, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0, left: 16.0, bottom: 3.0), - child: Text( - AppIntl.of(context)!.profile_balance, - style: const TextStyle( - fontSize: 16, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Center( - child: Text( - stringBalance, - style: const TextStyle(fontSize: 18), - ), - ), - ), - ], - ), - ); -} - -Card getProgramCompletion(ProfileViewModel model, BuildContext context) { - return Card( - child: SizedBox( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - AppIntl.of(context)!.profile_program_completion, - style: const TextStyle( - fontSize: 16, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 30.0, bottom: 37.0), - child: Center(child: getLoadingIndicator(model, context))), - ], - ), - ), - ); -} - -CircularPercentIndicator getLoadingIndicator( - ProfileViewModel model, BuildContext context) { - final double percentage = model.programProgression; - - return CircularPercentIndicator( - animation: true, - animationDuration: 1100, - radius: 40, - lineWidth: 10, - percent: percentage / 100, - circularStrokeCap: CircularStrokeCap.round, - center: Text( - percentage != 0 - ? '$percentage%' - : AppIntl.of(context)!.profile_program_completion_not_available, - style: const TextStyle(fontSize: 20), - ), - progressColor: Colors.green, - ); -} - -Column getCurrentProgramTile(List programList, BuildContext context) { - if (programList.isNotEmpty) { - final program = programList.last; - - final List dataTitles = [ - AppIntl.of(context)!.profile_code_program, - AppIntl.of(context)!.profile_average_program, - AppIntl.of(context)!.profile_number_accumulated_credits_program, - AppIntl.of(context)!.profile_number_registered_credits_program, - AppIntl.of(context)!.profile_number_completed_courses_program, - AppIntl.of(context)!.profile_number_failed_courses_program, - AppIntl.of(context)!.profile_number_equivalent_courses_program, - AppIntl.of(context)!.profile_status_program - ]; - - final List dataFetched = [ - program.code, - program.average, - program.accumulatedCredits, - program.registeredCredits, - program.completedCourses, - program.failedCourses, - program.equivalentCourses, - program.status - ]; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), - child: Text( - program.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.etsLightRed), - ), - ), - ...List.generate(dataTitles.length, (index) { - return Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(dataTitles[index]), - Text(dataFetched[index]), - ], - ), - ); - }), - ], - ); - } else { - return const Column(); - } -} diff --git a/lib/features/student/profile/widgets/profile_balance_card.dart b/lib/features/student/profile/widgets/profile_balance_card.dart new file mode 100644 index 000000000..52ecad18d --- /dev/null +++ b/lib/features/student/profile/widgets/profile_balance_card.dart @@ -0,0 +1,46 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/student/profile/profile_viewmodel.dart'; + +Card getMyBalanceCard(ProfileViewModel model, BuildContext context) { + final stringBalance = model.profileStudent.balance; + var balance = 0.0; + + if (stringBalance.isNotEmpty) { + balance = double.parse(stringBalance + .substring(0, stringBalance.length - 1) + .replaceAll(",", ".")); + } + + return Card( + color: balance > 0 ? Colors.red : Colors.green, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0, left: 16.0, bottom: 3.0), + child: Text( + AppIntl.of(context)!.profile_balance, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Center( + child: Text( + stringBalance, + style: const TextStyle(fontSize: 18), + ), + ), + ), + ], + ), + ); +} diff --git a/lib/features/student/profile/widgets/profile_current_program_tile.dart b/lib/features/student/profile/widgets/profile_current_program_tile.dart new file mode 100644 index 000000000..468731aad --- /dev/null +++ b/lib/features/student/profile/widgets/profile_current_program_tile.dart @@ -0,0 +1,67 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/utils/app_theme.dart'; + +Column getCurrentProgramTile(List programList, BuildContext context) { + if (programList.isNotEmpty) { + final program = programList.last; + + final List dataTitles = [ + AppIntl.of(context)!.profile_code_program, + AppIntl.of(context)!.profile_average_program, + AppIntl.of(context)!.profile_number_accumulated_credits_program, + AppIntl.of(context)!.profile_number_registered_credits_program, + AppIntl.of(context)!.profile_number_completed_courses_program, + AppIntl.of(context)!.profile_number_failed_courses_program, + AppIntl.of(context)!.profile_number_equivalent_courses_program, + AppIntl.of(context)!.profile_status_program + ]; + + final List dataFetched = [ + program.code, + program.average, + program.accumulatedCredits, + program.registeredCredits, + program.completedCourses, + program.failedCourses, + program.equivalentCourses, + program.status + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), + child: Text( + program.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.etsLightRed), + ), + ), + ...List.generate(dataTitles.length, (index) { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(dataTitles[index]), + Text(dataFetched[index]), + ], + ), + ); + }), + ], + ); + } else { + return const Column(); + } +} diff --git a/lib/features/student/profile/widgets/profile_infos_card.dart b/lib/features/student/profile/widgets/profile_infos_card.dart new file mode 100644 index 000000000..7b3593454 --- /dev/null +++ b/lib/features/student/profile/widgets/profile_infos_card.dart @@ -0,0 +1,79 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/student/profile/profile_viewmodel.dart'; + +Card getMyInfosCard(ProfileViewModel model, BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Clipboard.setData( + ClipboardData(text: model.profileStudent.permanentCode)); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppIntl.of(context)! + .profile_permanent_code_copied_to_clipboard), + )); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 3.0), + child: Text( + AppIntl.of(context)!.profile_permanent_code, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + Center( + child: Text( + model.profileStudent.permanentCode, + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + ), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: model.universalAccessCode)); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppIntl.of(context)! + .profile_universal_code_copied_to_clipboard), + )); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 3.0), + child: Text( + AppIntl.of(context)!.login_prompt_universal_code, + style: const TextStyle(fontSize: 16), + ), + ), + Center( + child: Text( + model.universalAccessCode, + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + ), + ], + ), + ), + ); +} diff --git a/lib/features/student/profile/widgets/profile_main_infos_card.dart b/lib/features/student/profile/widgets/profile_main_infos_card.dart new file mode 100644 index 000000000..e9648f744 --- /dev/null +++ b/lib/features/student/profile/widgets/profile_main_infos_card.dart @@ -0,0 +1,41 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:notredame/features/student/profile/profile_viewmodel.dart'; + +Card getMainInfoCard(ProfileViewModel model) { + var programName = ""; + if (model.programList.isNotEmpty) { + programName = model.programList.last.name; + } + + return Card( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Text( + '${model.profileStudent.firstName} ${model.profileStudent.lastName}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + programName, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ], + ), + ), + ); +} diff --git a/lib/features/student/profile/widgets/profile_program_completion_card.dart b/lib/features/student/profile/widgets/profile_program_completion_card.dart new file mode 100644 index 000000000..8106f8abd --- /dev/null +++ b/lib/features/student/profile/widgets/profile_program_completion_card.dart @@ -0,0 +1,54 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:percent_indicator/circular_percent_indicator.dart'; + +// Project imports: +import 'package:notredame/features/student/profile/profile_viewmodel.dart'; + +Card getProgramCompletionCard(ProfileViewModel model, BuildContext context) { + return Card( + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + AppIntl.of(context)!.profile_program_completion, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 30.0, bottom: 37.0), + child: Center(child: getLoadingIndicator(model, context))), + ], + ), + ), + ); +} + +CircularPercentIndicator getLoadingIndicator( + ProfileViewModel model, BuildContext context) { + final double percentage = model.programProgression; + + return CircularPercentIndicator( + animation: true, + animationDuration: 1100, + radius: 40, + lineWidth: 10, + percent: percentage / 100, + circularStrokeCap: CircularStrokeCap.round, + center: Text( + percentage != 0 + ? '$percentage%' + : AppIntl.of(context)!.profile_program_completion_not_available, + style: const TextStyle(fontSize: 20), + ), + progressColor: Colors.green, + ); +} diff --git a/lib/features/student/student_view.dart b/lib/features/student/student_view.dart index 8c8d1b185..cca2b2a59 100644 --- a/lib/features/student/student_view.dart +++ b/lib/features/student/student_view.dart @@ -2,16 +2,13 @@ import 'package:flutter/material.dart'; // Package imports: -import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:notredame/features/student/widgets/student_tutorial.dart'; // Project imports: -import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/student/grades/grades_view.dart'; import 'package:notredame/features/student/profile/profile_view.dart'; -import 'package:notredame/features/welcome/discovery/discovery_components.dart'; -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; -import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/features/app/widgets/base_scaffold.dart'; class StudentView extends StatefulWidget { @override @@ -55,7 +52,7 @@ class _StudentViewState extends State { tabs: List.generate( tabs.length, (index) => index == 1 - ? _buildDiscoveryFeatureDescriptionWidget( + ? studentDiscoveryFeatureDescriptionWidget( context, tabs, index) : Tab( text: tabs[index], @@ -66,8 +63,6 @@ class _StudentViewState extends State { ]; }, body: SafeArea( - top: false, - bottom: false, child: TabBarView( children: tabsView, ), @@ -76,31 +71,4 @@ class _StudentViewState extends State { ), ); } - - DescribedFeatureOverlay _buildDiscoveryFeatureDescriptionWidget( - BuildContext context, List tabs, int index) { - final discovery = getDiscoveryByFeatureId(context, - DiscoveryGroupIds.pageStudent, DiscoveryIds.detailsStudentProfile); - - return DescribedFeatureOverlay( - overflowMode: OverflowMode.wrapBackground, - contentLocation: ContentLocation.below, - featureId: discovery.featureId, - title: Text(discovery.title, textAlign: TextAlign.justify), - description: discovery.details, - backgroundColor: AppTheme.appletsDarkPurple, - tapTarget: Tab( - child: Text( - tabs[index], - style: (Theme.of(context).brightness == Brightness.dark) - ? const TextStyle(color: Colors.black) - : null, - ), - ), - pulseDuration: const Duration(seconds: 5), - child: Tab( - text: tabs[index], - ), - ); - } } diff --git a/lib/features/student/widgets/student_tutorial.dart b/lib/features/student/widgets/student_tutorial.dart new file mode 100644 index 000000000..5e2daae98 --- /dev/null +++ b/lib/features/student/widgets/student_tutorial.dart @@ -0,0 +1,37 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:feature_discovery/feature_discovery.dart'; + +// Project imports: +import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/app_theme.dart'; + +DescribedFeatureOverlay studentDiscoveryFeatureDescriptionWidget( + BuildContext context, List tabs, int index) { + final discovery = getDiscoveryByFeatureId(context, + DiscoveryGroupIds.pageStudent, DiscoveryIds.detailsStudentProfile); + + return DescribedFeatureOverlay( + overflowMode: OverflowMode.wrapBackground, + contentLocation: ContentLocation.below, + featureId: discovery.featureId, + title: Text(discovery.title, textAlign: TextAlign.justify), + description: discovery.details, + backgroundColor: AppTheme.appletsDarkPurple, + tapTarget: Tab( + child: Text( + tabs[index], + style: (Theme.of(context).brightness == Brightness.dark) + ? const TextStyle(color: Colors.black) + : null, + ), + ), + pulseDuration: const Duration(seconds: 5), + child: Tab( + text: tabs[index], + ), + ); +} diff --git a/lib/features/welcome/login/login_view.dart b/lib/features/welcome/login/login_view.dart index ac30e3e51..cf0dd8cec 100644 --- a/lib/features/welcome/login/login_view.dart +++ b/lib/features/welcome/login/login_view.dart @@ -1,22 +1,22 @@ +// LoginView.dart + // Flutter imports: import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/features/app/integration/launch_url_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; -import 'package:notredame/features/welcome/login/login_mask.dart'; import 'package:notredame/features/welcome/login/login_viewmodel.dart'; +import 'package:notredame/features/welcome/login/widget/app_ets_logo.dart'; +import 'package:notredame/features/welcome/login/widget/forgot_password_link.dart'; +import 'package:notredame/features/welcome/login/widget/login_button.dart'; +import 'package:notredame/features/welcome/login/widget/login_widget.dart'; +import 'package:notredame/features/welcome/login/widget/need_help_link.dart'; +import 'package:notredame/features/welcome/login/widget/universal_code_field.dart'; import 'package:notredame/features/welcome/widgets/password_text_field.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/utils/locator.dart'; import 'package:notredame/utils/utils.dart'; class LoginView extends StatefulWidget { @@ -29,13 +29,6 @@ class _LoginViewState extends State { final FocusScopeNode _focusNode = FocusScopeNode(); - final NavigationService _navigationService = locator(); - - final LaunchUrlService _launchUrlService = locator(); - - final RemoteConfigService _remoteConfigService = - locator(); - /// Unique key of the login form form final GlobalKey formKey = GlobalKey(); @@ -51,227 +44,51 @@ class _LoginViewState extends State { context, AppTheme.etsLightRed, AppTheme.primaryDark), resizeToAvoidBottomInset: false, body: Builder( - builder: (BuildContext context) => SafeArea( - minimum: const EdgeInsets.all(20.0), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Form( - key: formKey, - onChanged: () { - setState(() { - formKey.currentState?.validate(); - }); - }, - child: AutofillGroup( - child: FocusScope( - node: _focusNode, - child: Column( - children: [ - const SizedBox( - height: 48, - ), - Hero( - tag: 'ets_logo', - child: SvgPicture.asset( - "assets/images/ets_white_logo.svg", - excludeFromSemantics: true, - width: 90, - height: 90, - colorFilter: ColorFilter.mode( - Theme.of(context).brightness == - Brightness.light - ? Colors.white - : AppTheme.etsLightRed, - BlendMode.srcIn), - )), - const SizedBox( - height: 48, - ), - TextFormField( - autofillHints: const [ - AutofillHints.username - ], - cursorColor: Colors.white, - keyboardType: - TextInputType.visiblePassword, - decoration: InputDecoration( - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.white70)), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.white, - width: borderRadiusOnFocus)), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: errorTextColor, - width: borderRadiusOnFocus)), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: errorTextColor, - width: borderRadiusOnFocus)), - labelText: AppIntl.of(context)! - .login_prompt_universal_code, - labelStyle: const TextStyle( - color: Colors.white54), - errorStyle: - TextStyle(color: errorTextColor), - suffixIcon: Tooltip( - key: tooltipkey, - triggerMode: - TooltipTriggerMode.manual, - message: AppIntl.of(context)! - .universal_code_example, - preferBelow: true, - child: IconButton( - icon: const Icon(Icons.help, - color: Colors.white), - onPressed: () { - tooltipkey.currentState - ?.ensureTooltipVisible(); - }, - )), - ), - autofocus: true, - style: - const TextStyle(color: Colors.white), - onEditingComplete: _focusNode.nextFocus, - validator: model.validateUniversalCode, - initialValue: model.universalCode, - inputFormatters: [ - LoginMask(), - ], - ), - const SizedBox( - height: 16, - ), - PasswordFormField( - validator: model.validatePassword, - onEditionComplete: - _focusNode.nextFocus), - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: InkWell( - child: Text( - AppIntl.of(context)! - .forgot_password, - style: const TextStyle( - decoration: - TextDecoration.underline, - color: Colors.white), - ), - onTap: () { - final signetsPasswordResetUrl = - _remoteConfigService - .signetsPasswordResetUrl; - if (signetsPasswordResetUrl != "") { - _launchUrlService.launchInBrowser( - _remoteConfigService - .signetsPasswordResetUrl, - Theme.of(context).brightness); - } else { - Fluttertoast.showToast( - msg: AppIntl.of(context)! - .error); - } - }, - ), - ), - ), - const SizedBox( - height: 24, - ), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: !model.canSubmit - ? null - : () async { - final String error = - await model.authenticate(); - - setState(() { - if (error.isNotEmpty) { - Fluttertoast.showToast( - msg: error); - } - formKey.currentState?.reset(); - }); - }, - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all( - model.canSubmit - ? colorButton - : Colors.white38), - padding: MaterialStateProperty.all( - const EdgeInsets.symmetric( - vertical: 16)), - ), - child: Text( - AppIntl.of(context)! - .login_action_sign_in, - style: TextStyle( - color: model.canSubmit - ? submitTextColor - : Colors.white60, - fontSize: 18), - ), - ), - ), - Center( - child: Padding( - padding: const EdgeInsets.only(top: 24), - child: InkWell( - child: Text( - AppIntl.of(context)!.need_help, - style: const TextStyle( - decoration: - TextDecoration.underline, - color: Colors.white), - ), - onTap: () async { - _navigationService.pushNamed( - RouterPaths.faq, - arguments: - Utils.getColorByBrightness( - context, - AppTheme.etsLightRed, - AppTheme.primaryDark)); - }, - ), - ), - ), - ], - ), - ), - ), - ), - Wrap( - direction: Axis.vertical, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: -20, - children: [ - Text( - AppIntl.of(context)!.login_applets_logo, - style: const TextStyle(color: Colors.white), - ), - Image.asset( - 'assets/images/applets_white_logo.png', - excludeFromSemantics: true, - width: 100, - height: 100, + builder: (BuildContext context) => SafeArea( + minimum: const EdgeInsets.all(20.0), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Form( + key: formKey, + onChanged: () { + setState(() { + formKey.currentState?.validate(); + }); + }, + child: AutofillGroup( + child: FocusScope( + node: _focusNode, + child: Column( + children: [ + const SizedBox(height: 48), + const LogoWidget(), + const SizedBox(height: 48), + UniversalCodeField( + borderRadiusOnFocus: borderRadiusOnFocus, + tooltipKey: tooltipkey, + model: model, ), + const SizedBox(height: 16), + PasswordFormField( + validator: model.validatePassword, + onEditionComplete: _focusNode.nextFocus), + ForgotPasswordLink(), + const SizedBox(height: 24), + LoginButton(formKey: formKey, model: model), + NeedHelpLink(), ], ), - ], + ), ), ), - )), + const AppEtsLogo(), + ], + ), + ), + ), + ), ), ); @@ -280,13 +97,4 @@ class _LoginViewState extends State { _focusNode.dispose(); super.dispose(); } - - Color get errorTextColor => - Utils.getColorByBrightness(context, Colors.amberAccent, Colors.redAccent); - - Color get colorButton => - Utils.getColorByBrightness(context, Colors.white, AppTheme.etsLightRed); - - Color get submitTextColor => - Utils.getColorByBrightness(context, AppTheme.etsLightRed, Colors.white); } diff --git a/lib/features/welcome/login/widget/app_ets_logo.dart b/lib/features/welcome/login/widget/app_ets_logo.dart new file mode 100644 index 000000000..0d72d870a --- /dev/null +++ b/lib/features/welcome/login/widget/app_ets_logo.dart @@ -0,0 +1,32 @@ +// widgets/AppletsLogo.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AppEtsLogo extends StatelessWidget { + const AppEtsLogo({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + direction: Axis.vertical, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: -20, + children: [ + Text( + AppIntl.of(context)!.login_applets_logo, + style: const TextStyle(color: Colors.white), + ), + Image.asset( + 'assets/images/applets_white_logo.png', + excludeFromSemantics: true, + width: 100, + height: 100, + ), + ], + ); + } +} diff --git a/lib/features/welcome/login/widget/forgot_password_link.dart b/lib/features/welcome/login/widget/forgot_password_link.dart new file mode 100644 index 000000000..34654e78b --- /dev/null +++ b/lib/features/welcome/login/widget/forgot_password_link.dart @@ -0,0 +1,49 @@ +// widgets/ForgotPasswordLink.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +// Project imports: +import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/integration/launch_url_service.dart'; +import 'package:notredame/utils/locator.dart'; + +class ForgotPasswordLink extends StatelessWidget { + final LaunchUrlService _launchUrlService = locator(); + final RemoteConfigService _remoteConfigService = + locator(); + + ForgotPasswordLink({super.key}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(top: 4), + child: InkWell( + child: Text( + AppIntl.of(context)!.forgot_password, + style: const TextStyle( + decoration: TextDecoration.underline, color: Colors.white), + ), + onTap: () { + final signetsPasswordResetUrl = + _remoteConfigService.signetsPasswordResetUrl; + if (signetsPasswordResetUrl != "") { + _launchUrlService.launchInBrowser( + _remoteConfigService.signetsPasswordResetUrl, + Theme.of(context).brightness); + } else { + Fluttertoast.showToast(msg: AppIntl.of(context)!.error); + } + }, + ), + ), + ); + } +} diff --git a/lib/features/welcome/login/widget/login_button.dart b/lib/features/welcome/login/widget/login_button.dart new file mode 100644 index 000000000..e1c3a86d5 --- /dev/null +++ b/lib/features/welcome/login/widget/login_button.dart @@ -0,0 +1,58 @@ +// widgets/LoginButton.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +// Project imports: +import 'package:notredame/features/welcome/login/login_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; + +class LoginButton extends StatelessWidget { + final GlobalKey formKey; + final LoginViewModel model; + + const LoginButton({super.key, required this.formKey, required this.model}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: !model.canSubmit + ? null + : () async { + final String error = await model.authenticate(); + + if (error.isNotEmpty) { + Fluttertoast.showToast(msg: error); + } + formKey.currentState?.reset(); + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + model.canSubmit ? colorButton(context) : Colors.white38), + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(vertical: 16)), + ), + child: Text( + AppIntl.of(context)!.login_action_sign_in, + style: TextStyle( + color: + model.canSubmit ? submitTextColor(context) : Colors.white60, + fontSize: 18), + ), + ), + ); + } + + Color colorButton(BuildContext context) => + Utils.getColorByBrightness(context, Colors.white, AppTheme.etsLightRed); + + Color submitTextColor(BuildContext context) => + Utils.getColorByBrightness(context, AppTheme.etsLightRed, Colors.white); +} diff --git a/lib/features/welcome/login/widget/login_widget.dart b/lib/features/welcome/login/widget/login_widget.dart new file mode 100644 index 000000000..75f94bab3 --- /dev/null +++ b/lib/features/welcome/login/widget/login_widget.dart @@ -0,0 +1,30 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_svg/flutter_svg.dart'; + +// Project imports: +import 'package:notredame/utils/app_theme.dart'; + +class LogoWidget extends StatelessWidget { + const LogoWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Hero( + tag: 'ets_logo', + child: SvgPicture.asset( + "assets/images/ets_white_logo.svg", + excludeFromSemantics: true, + width: 90, + height: 90, + colorFilter: ColorFilter.mode( + Theme.of(context).brightness == Brightness.light + ? Colors.white + : AppTheme.etsLightRed, + BlendMode.srcIn), + ), + ); + } +} diff --git a/lib/features/welcome/login/widget/need_help_link.dart b/lib/features/welcome/login/widget/need_help_link.dart new file mode 100644 index 000000000..54f4777ef --- /dev/null +++ b/lib/features/welcome/login/widget/need_help_link.dart @@ -0,0 +1,41 @@ +// widgets/NeedHelpLink.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; + +class NeedHelpLink extends StatelessWidget { + final NavigationService _navigationService = locator(); + + NeedHelpLink({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 24), + child: InkWell( + child: Text( + AppIntl.of(context)!.need_help, + style: const TextStyle( + decoration: TextDecoration.underline, color: Colors.white), + ), + onTap: () async { + _navigationService.pushNamed(RouterPaths.faq, + arguments: Utils.getColorByBrightness( + context, AppTheme.etsLightRed, AppTheme.primaryDark)); + }, + ), + ), + ); + } +} diff --git a/lib/features/welcome/login/widget/universal_code_field.dart b/lib/features/welcome/login/widget/universal_code_field.dart new file mode 100644 index 000000000..5e226d002 --- /dev/null +++ b/lib/features/welcome/login/widget/universal_code_field.dart @@ -0,0 +1,70 @@ +// widgets/UniversalCodeField.dart + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Project imports: +import 'package:notredame/features/welcome/login/login_mask.dart'; +import 'package:notredame/features/welcome/login/login_viewmodel.dart'; +import 'package:notredame/utils/utils.dart'; + +class UniversalCodeField extends StatelessWidget { + final double borderRadiusOnFocus; + final GlobalKey tooltipKey; + final LoginViewModel model; + + const UniversalCodeField({ + super.key, + required this.borderRadiusOnFocus, + required this.tooltipKey, + required this.model, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + autofillHints: const [AutofillHints.username], + cursorColor: Colors.white, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white70)), + focusedBorder: OutlineInputBorder( + borderSide: + BorderSide(color: Colors.white, width: borderRadiusOnFocus)), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: errorTextColor(context), width: borderRadiusOnFocus)), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: errorTextColor(context), width: borderRadiusOnFocus)), + labelText: AppIntl.of(context)!.login_prompt_universal_code, + labelStyle: const TextStyle(color: Colors.white54), + errorStyle: TextStyle(color: errorTextColor(context)), + suffixIcon: Tooltip( + key: tooltipKey, + triggerMode: TooltipTriggerMode.manual, + message: AppIntl.of(context)!.universal_code_example, + preferBelow: true, + child: IconButton( + icon: const Icon(Icons.help, color: Colors.white), + onPressed: () { + tooltipKey.currentState?.ensureTooltipVisible(); + }, + )), + ), + autofocus: true, + style: const TextStyle(color: Colors.white), + onEditingComplete: () => FocusScope.of(context).nextFocus(), + validator: model.validateUniversalCode, + initialValue: model.universalCode, + inputFormatters: [LoginMask()], + ); + } + + Color errorTextColor(BuildContext context) => + Utils.getColorByBrightness(context, Colors.amberAccent, Colors.redAccent); +} diff --git a/lib/utils/calendar_utils.dart b/lib/utils/calendar_utils.dart index 2789f8534..596e5cac0 100644 --- a/lib/utils/calendar_utils.dart +++ b/lib/utils/calendar_utils.dart @@ -1,3 +1,5 @@ +// Dart imports: + // Dart imports: import 'dart:collection'; @@ -8,6 +10,8 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:notredame/features/app/signets-api/models/course_activity.dart'; import 'package:notredame/features/ets/events/api-client/models/news.dart'; +// Package imports: + mixin CalendarUtils { static Future checkPermissions() async { final deviceCalendarPluginPermissionsResponse = diff --git a/lib/utils/loading.dart b/lib/utils/loading.dart index a88c07882..c84c7c258 100644 --- a/lib/utils/loading.dart +++ b/lib/utils/loading.dart @@ -5,7 +5,7 @@ Widget buildLoading({bool isInteractionLimitedWhileLoading = true}) => Stack( children: [ if (isInteractionLimitedWhileLoading) const Opacity( - opacity: 0.5, + opacity: 0.3, child: ModalBarrier(dismissible: false, color: Colors.grey), ), const Center(child: CircularProgressIndicator()) diff --git a/pubspec.lock b/pubspec.lock index 0b1690541..e76b28974 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" convert: dependency: transitive description: @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" easter_egg_trigger: dependency: "direct main" description: @@ -333,10 +333,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" firebase_analytics: dependency: "direct main" description: @@ -373,10 +373,10 @@ packages: dependency: transitive description: name: firebase_core_platform_interface - sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb" + sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.2.0" firebase_core_web: dependency: transitive description: @@ -442,10 +442,10 @@ packages: dependency: "direct main" description: name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" flutter_config: dependency: "direct dev" description: @@ -587,6 +587,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + flutter_staggered_grid_view: + dependency: transitive + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_svg: dependency: "direct main" description: @@ -681,10 +689,10 @@ packages: dependency: transitive description: name: google_maps_flutter_ios - sha256: "1043d0a4ad52612444b24edbd2d61f6de40e01e842fe2a9248be53fa70bc7047" + sha256: a6e3c6ecdda6c985053f944be13a0645ebb919da2ef0f5bc579c5e1670a5b2a8 url: "https://pub.dev" source: hosted - version: "2.8.1" + version: "2.10.0" google_maps_flutter_platform_interface: dependency: transitive description: @@ -777,10 +785,10 @@ packages: dependency: "direct main" description: name: infinite_scroll_pagination - sha256: "9517328f4e373f08f57dbb11c5aac5b05554142024d6b60c903f3b73476d52db" + sha256: b68bce20752fcf36c7739e60de4175494f74e99e9a69b4dd2fe3a1dd07a7f16a url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" intl: dependency: transitive description: @@ -857,10 +865,10 @@ packages: dependency: "direct main" description: name: logger - sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" logging: dependency: transitive description: @@ -977,10 +985,10 @@ packages: dependency: transitive description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: @@ -1017,10 +1025,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" percent_indicator: dependency: "direct main" description: @@ -1097,26 +1105,26 @@ packages: dependency: "direct main" description: name: rive - sha256: ec44b6cf7341e21727c4b0e762f4ac82f9a45f7e52df3ebad2d1289a726fbaaf + sha256: "255ab7892a77494458846cecee1376a017e64fd6b4130b51ec21424d12ffa6fe" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.13.4" rive_common: dependency: transitive description: name: rive_common - sha256: "0f070bc0e764c570abd8b34d744ef30d1292bd4051f2e0951bbda755875fce6a" + sha256: "3a0d95f529d52caef535d8ff32d75629ca37f7ab4707b13c83e9552a322557bc" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.4.8" rxdart: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" sanitize_html: dependency: transitive description: @@ -1129,26 +1137,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: f582d5741930f3ad1bf0211d358eddc0508cc346e5b4b248bd1e569c995ebb7a - url: "https://pub.dev" - source: hosted - version: "4.5.3" - share_plus_linux: - dependency: transitive - description: - name: share_plus_linux - sha256: dc32bf9f1151b9864bb86a997c61a487967a08f2e0b4feaa9a10538712224da4 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - share_plus_macos: - dependency: transitive - description: - name: share_plus_macos - sha256: "44daa946f2845045ecd7abb3569b61cd9a55ae9cc4cbec9895b2067b270697ae" + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: @@ -1157,22 +1149,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.0" - share_plus_web: - dependency: transitive - description: - name: share_plus_web - sha256: eaef05fa8548b372253e772837dd1fbe4ce3aca30ea330765c945d7d4f7c9935 - url: "https://pub.dev" - source: hosted - version: "3.1.0" - share_plus_windows: - dependency: transitive - description: - name: share_plus_windows - sha256: "3a21515ae7d46988d42130cd53294849e280a5de6ace24bae6912a1bffd757d4" - url: "https://pub.dev" - source: hosted - version: "3.0.1" shared_preferences: dependency: "direct main" description: @@ -1193,26 +1169,26 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: @@ -1225,10 +1201,10 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shelf: dependency: transitive description: @@ -1261,6 +1237,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + skeletonizer: + dependency: "direct main" + description: + name: skeletonizer + sha256: "3b202e4fa9c49b017d368fb0e570d4952bcd19972b67b2face071bdd68abbfae" + url: "https://pub.dev" + source: hosted + version: "1.4.2" sky_engine: dependency: transitive description: flutter @@ -1462,10 +1446,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -1502,18 +1486,18 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: transitive description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.4.2" vector_graphics: dependency: transitive description: @@ -1614,10 +1598,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "7affdf9d680c015b11587181171d3cad8093e449db1f7d9f0f08f4f33d24f9a0" + sha256: "9c62cc46fa4f2d41e10ab81014c1de470a6c6f26051a2de32111b2ee55287feb" url: "https://pub.dev" source: hosted - version: "3.13.1" + version: "3.14.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0ec67fcaa..06943e378 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 4.48.0+1 +version: 4.52.0+1 environment: sdk: '>=3.3.0 <4.0.0' @@ -31,7 +31,7 @@ dependencies: logger: ^2.2.0 url_launcher: ^6.2.5 enum_to_string: ^2.0.1 - fluttertoast: ^8.2.4 + fluttertoast: ^8.2.5 # Widgets table_calendar: ^3.0.9 @@ -64,7 +64,7 @@ dependencies: carousel_slider: ^4.2.1 reorderable_grid_view: ^2.2.8 shimmer: ^3.0.0 - infinite_scroll_pagination: ^3.2.0 + infinite_scroll_pagination: ^4.0.0 device_calendar: ^4.3.1 share_plus: ^4.5.3 flutter_markdown: ^0.7.1 @@ -93,7 +93,7 @@ flutter: assets: - assets/images/ - - assets/html/ + - assets/markdown/ - assets/icons/ - assets/animations/ - assets/animations/discovery/en/ diff --git a/test/helpers.dart b/test/common/helpers.dart similarity index 86% rename from test/helpers.dart rename to test/common/helpers.dart index 7ea41bb88..c3f4226e0 100644 --- a/test/helpers.dart +++ b/test/common/helpers.dart @@ -31,27 +31,27 @@ import 'package:notredame/features/app/storage/siren_flutter_service.dart'; import 'package:notredame/features/more/feedback/in_app_review_service.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/utils/locator.dart'; -import 'mock/managers/author_repository_mock.dart'; -import 'mock/managers/cache_manager_mock.dart'; -import 'mock/managers/course_repository_mock.dart'; -import 'mock/managers/news_repository_mock.dart'; -import 'mock/managers/quick_links_repository_mock.dart'; -import 'mock/managers/settings_manager_mock.dart'; -import 'mock/managers/user_repository_mock.dart'; -import 'mock/services/analytics_service_mock.dart'; -import 'mock/services/flutter_secure_storage_mock.dart'; -import 'mock/services/github_api_mock.dart'; -import 'mock/services/in_app_review_service_mock.dart'; -import 'mock/services/internal_info_service_mock.dart'; -import 'mock/services/launch_url_service_mock.dart'; -import 'mock/services/mon_ets_api_mock.dart'; -import 'mock/services/navigation_service_mock.dart'; -import 'mock/services/networking_service_mock.dart'; -import 'mock/services/preferences_service_mock.dart'; -import 'mock/services/remote_config_service_mock.dart'; -import 'mock/services/rive_animation_service_mock.dart'; -import 'mock/services/signets_api_mock.dart'; -import 'mock/services/siren_flutter_service_mock.dart'; +import '../features/app/analytics/mocks/analytics_service_mock.dart'; +import '../features/app/analytics/mocks/remote_config_service_mock.dart'; +import '../features/app/error/internal_info/mocks/internal_info_service_mock.dart'; +import '../features/app/integration/mocks/github_api_mock.dart'; +import '../features/app/integration/mocks/launch_url_service_mock.dart'; +import '../features/app/integration/mocks/networking_service_mock.dart'; +import '../features/app/monets_api/mocks/mon_ets_api_mock.dart'; +import '../features/app/navigation/mocks/navigation_service_mock.dart'; +import '../features/app/presentation/mocks/rive_animation_service_mock.dart'; +import '../features/app/repository/mocks/author_repository_mock.dart'; +import '../features/app/repository/mocks/course_repository_mock.dart'; +import '../features/app/repository/mocks/news_repository_mock.dart'; +import '../features/app/repository/mocks/quick_links_repository_mock.dart'; +import '../features/app/repository/mocks/user_repository_mock.dart'; +import '../features/app/signets_api/mocks/signets_api_mock.dart'; +import '../features/app/storage/mocks/cache_manager_mock.dart'; +import '../features/app/storage/mocks/flutter_secure_storage_mock.dart'; +import '../features/app/storage/mocks/preferences_service_mock.dart'; +import '../features/app/storage/mocks/siren_flutter_service_mock.dart'; +import '../features/more/feedback/mocks/in_app_review_service_mock.dart'; +import '../features/more/settings/mocks/settings_manager_mock.dart'; /// Return the path of the [goldenName] file. String goldenFilePath(String goldenName) => "./goldenFiles/$goldenName.png"; diff --git a/test/mock/services/analytics_service_mock.dart b/test/features/app/analytics/mocks/analytics_service_mock.dart similarity index 100% rename from test/mock/services/analytics_service_mock.dart rename to test/features/app/analytics/mocks/analytics_service_mock.dart diff --git a/test/mock/services/remote_config_service_mock.dart b/test/features/app/analytics/mocks/remote_config_service_mock.dart similarity index 100% rename from test/mock/services/remote_config_service_mock.dart rename to test/features/app/analytics/mocks/remote_config_service_mock.dart diff --git a/test/mock/services/internal_info_service_mock.dart b/test/features/app/error/internal_info/mocks/internal_info_service_mock.dart similarity index 100% rename from test/mock/services/internal_info_service_mock.dart rename to test/features/app/error/internal_info/mocks/internal_info_service_mock.dart diff --git a/test/ui/views/not_found_view_test.dart b/test/features/app/error/not_found/mocks/not_found_view_test.dart similarity index 67% rename from test/ui/views/not_found_view_test.dart rename to test/features/app/error/not_found/mocks/not_found_view_test.dart index c11713ebd..056981081 100644 --- a/test/ui/views/not_found_view_test.dart +++ b/test/features/app/error/not_found/mocks/not_found_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -10,8 +7,8 @@ import 'package:rive/rive.dart'; // Project imports: import 'package:notredame/features/app/error/not_found/not_found_view.dart'; -import '../../helpers.dart'; -import '../../mock/services/rive_animation_service_mock.dart'; +import '../../../../../common/helpers.dart'; +import '../../../presentation/mocks/rive_animation_service_mock.dart'; void main() { group('NotFoundView - ', () { @@ -40,18 +37,6 @@ void main() { // Make sure to find the page name somewhere expect(find.textContaining(pageNotFoundPassed), findsOneWidget); }); - group("golden - ", () { - testWidgets("default view (no events)", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget( - child: const NotFoundView(pageName: pageNotFoundPassed))); - await tester.pumpAndSettle(); - - await expectLater(find.byType(NotFoundView), - matchesGoldenFile(goldenFilePath("notFoundView_1"))); - }); - }, skip: !Platform.isLinux); }); }); } diff --git a/test/viewmodels/not_found_viewmodel_test.dart b/test/features/app/error/not_found/mocks/not_found_viewmodel_test.dart similarity index 94% rename from test/viewmodels/not_found_viewmodel_test.dart rename to test/features/app/error/not_found/mocks/not_found_viewmodel_test.dart index 37e2cf1e4..ab063a686 100644 --- a/test/viewmodels/not_found_viewmodel_test.dart +++ b/test/features/app/error/not_found/mocks/not_found_viewmodel_test.dart @@ -9,10 +9,10 @@ import 'package:notredame/features/app/error/not_found/not_found_viewmodel.dart' import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/presentation/rive_animation_service.dart'; -import '../helpers.dart'; -import '../mock/services/analytics_service_mock.dart'; -import '../mock/services/navigation_service_mock.dart'; -import '../mock/services/rive_animation_service_mock.dart'; +import '../../../../../common/helpers.dart'; +import '../../../analytics/mocks/analytics_service_mock.dart'; +import '../../../navigation/mocks/navigation_service_mock.dart'; +import '../../../presentation/mocks/rive_animation_service_mock.dart'; void main() { late NavigationServiceMock navigationServiceMock; diff --git a/test/mock/services/github_api_mock.dart b/test/features/app/integration/mocks/github_api_mock.dart similarity index 100% rename from test/mock/services/github_api_mock.dart rename to test/features/app/integration/mocks/github_api_mock.dart diff --git a/test/mock/services/launch_url_service_mock.dart b/test/features/app/integration/mocks/launch_url_service_mock.dart similarity index 100% rename from test/mock/services/launch_url_service_mock.dart rename to test/features/app/integration/mocks/launch_url_service_mock.dart diff --git a/test/mock/services/networking_service_mock.dart b/test/features/app/integration/mocks/networking_service_mock.dart similarity index 100% rename from test/mock/services/networking_service_mock.dart rename to test/features/app/integration/mocks/networking_service_mock.dart diff --git a/test/mock/services/mon_ets_api_mock.dart b/test/features/app/monets_api/mocks/mon_ets_api_mock.dart similarity index 100% rename from test/mock/services/mon_ets_api_mock.dart rename to test/features/app/monets_api/mocks/mon_ets_api_mock.dart diff --git a/test/services/monets_api_client_test.dart b/test/features/app/monets_api/monets_api_client_test.dart similarity index 97% rename from test/services/monets_api_client_test.dart rename to test/features/app/monets_api/monets_api_client_test.dart index 46afafdaa..fea21f9f5 100644 --- a/test/services/monets_api_client_test.dart +++ b/test/features/app/monets_api/monets_api_client_test.dart @@ -11,7 +11,7 @@ import 'package:notredame/constants/urls.dart'; import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart'; import 'package:notredame/features/app/monets_api/monets_api_client.dart'; import 'package:notredame/utils/http_exception.dart'; -import '../mock/signets-api-client/http_client_mock_helper.dart'; +import '../signets_api/http_client_mock_helper.dart'; void main() { late MonETSAPIClient service; diff --git a/test/mock/services/navigation_service_mock.dart b/test/features/app/navigation/mocks/navigation_service_mock.dart similarity index 100% rename from test/mock/services/navigation_service_mock.dart rename to test/features/app/navigation/mocks/navigation_service_mock.dart diff --git a/test/mock/services/rive_animation_service_mock.dart b/test/features/app/presentation/mocks/rive_animation_service_mock.dart similarity index 100% rename from test/mock/services/rive_animation_service_mock.dart rename to test/features/app/presentation/mocks/rive_animation_service_mock.dart diff --git a/test/managers/course_repository_test.dart b/test/features/app/repository/course_repository_test.dart similarity index 99% rename from test/managers/course_repository_test.dart rename to test/features/app/repository/course_repository_test.dart index 8b744a53a..285b12310 100644 --- a/test/managers/course_repository_test.dart +++ b/test/features/app/repository/course_repository_test.dart @@ -22,12 +22,12 @@ import 'package:notredame/features/app/signets-api/signets_api_client.dart'; import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/utils/activity_code.dart'; import 'package:notredame/utils/api_exception.dart'; -import '../helpers.dart'; -import '../mock/managers/cache_manager_mock.dart'; -import '../mock/managers/user_repository_mock.dart'; -import '../mock/services/analytics_service_mock.dart'; -import '../mock/services/networking_service_mock.dart'; -import '../mock/services/signets_api_mock.dart'; +import '../../../common/helpers.dart'; +import '../analytics/mocks/analytics_service_mock.dart'; +import '../integration/mocks/networking_service_mock.dart'; +import '../signets_api/mocks/signets_api_mock.dart'; +import '../storage/mocks/cache_manager_mock.dart'; +import 'mocks/user_repository_mock.dart'; void main() { late AnalyticsServiceMock analyticsServiceMock; diff --git a/test/mock/managers/author_repository_mock.dart b/test/features/app/repository/mocks/author_repository_mock.dart similarity index 100% rename from test/mock/managers/author_repository_mock.dart rename to test/features/app/repository/mocks/author_repository_mock.dart diff --git a/test/mock/managers/course_repository_mock.dart b/test/features/app/repository/mocks/course_repository_mock.dart similarity index 100% rename from test/mock/managers/course_repository_mock.dart rename to test/features/app/repository/mocks/course_repository_mock.dart diff --git a/test/mock/managers/news_repository_mock.dart b/test/features/app/repository/mocks/news_repository_mock.dart similarity index 100% rename from test/mock/managers/news_repository_mock.dart rename to test/features/app/repository/mocks/news_repository_mock.dart diff --git a/test/mock/managers/quick_links_repository_mock.dart b/test/features/app/repository/mocks/quick_links_repository_mock.dart similarity index 100% rename from test/mock/managers/quick_links_repository_mock.dart rename to test/features/app/repository/mocks/quick_links_repository_mock.dart diff --git a/test/mock/managers/user_repository_mock.dart b/test/features/app/repository/mocks/user_repository_mock.dart similarity index 100% rename from test/mock/managers/user_repository_mock.dart rename to test/features/app/repository/mocks/user_repository_mock.dart diff --git a/test/managers/quick_link_repository_test.dart b/test/features/app/repository/quick_link_repository_test.dart similarity index 97% rename from test/managers/quick_link_repository_test.dart rename to test/features/app/repository/quick_link_repository_test.dart index 7f27380ce..da6576efa 100644 --- a/test/managers/quick_link_repository_test.dart +++ b/test/features/app/repository/quick_link_repository_test.dart @@ -13,8 +13,8 @@ import 'package:notredame/features/app/repository/quick_link_repository.dart'; import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link_data.dart'; -import '../helpers.dart'; -import '../mock/managers/cache_manager_mock.dart'; +import '../../../common/helpers.dart'; +import '../storage/mocks/cache_manager_mock.dart'; void main() { late CacheManagerMock cacheManagerMock; diff --git a/test/managers/user_repository_test.dart b/test/features/app/repository/user_repository_test.dart similarity index 98% rename from test/managers/user_repository_test.dart rename to test/features/app/repository/user_repository_test.dart index 1bcf3b840..c4153344c 100644 --- a/test/managers/user_repository_test.dart +++ b/test/features/app/repository/user_repository_test.dart @@ -21,13 +21,13 @@ import 'package:notredame/features/app/signets-api/signets_api_client.dart'; import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/utils/api_exception.dart'; import 'package:notredame/utils/http_exception.dart'; -import '../helpers.dart'; -import '../mock/managers/cache_manager_mock.dart'; -import '../mock/services/analytics_service_mock.dart'; -import '../mock/services/flutter_secure_storage_mock.dart'; -import '../mock/services/mon_ets_api_mock.dart'; -import '../mock/services/networking_service_mock.dart'; -import '../mock/services/signets_api_mock.dart'; +import '../../../common/helpers.dart'; +import '../analytics/mocks/analytics_service_mock.dart'; +import '../integration/mocks/networking_service_mock.dart'; +import '../monets_api/mocks/mon_ets_api_mock.dart'; +import '../signets_api/mocks/signets_api_mock.dart'; +import '../storage/mocks/cache_manager_mock.dart'; +import '../storage/mocks/flutter_secure_storage_mock.dart'; void main() { late AnalyticsServiceMock analyticsServiceMock; diff --git a/test/mock/signets-api-client/http_client_mock_helper.dart b/test/features/app/signets_api/http_client_mock_helper.dart similarity index 100% rename from test/mock/signets-api-client/http_client_mock_helper.dart rename to test/features/app/signets_api/http_client_mock_helper.dart diff --git a/test/mock/services/signets_api_mock.dart b/test/features/app/signets_api/mocks/signets_api_mock.dart similarity index 100% rename from test/mock/services/signets_api_mock.dart rename to test/features/app/signets_api/mocks/signets_api_mock.dart diff --git a/test/services/signets_api_client_test.dart b/test/features/app/signets_api/signets_api_client_test.dart similarity index 99% rename from test/services/signets_api_client_test.dart rename to test/features/app/signets_api/signets_api_client_test.dart index b419043d2..4400e5f57 100644 --- a/test/services/signets_api_client_test.dart +++ b/test/features/app/signets_api/signets_api_client_test.dart @@ -21,7 +21,7 @@ import 'package:notredame/features/app/signets-api/models/signets_errors.dart'; import 'package:notredame/features/app/signets-api/signets_api_client.dart'; import 'package:notredame/utils/activity_code.dart'; import 'package:notredame/utils/api_exception.dart'; -import '../mock/signets-api-client/http_client_mock_helper.dart'; +import 'http_client_mock_helper.dart'; void main() { late MockClient clientMock; diff --git a/test/services/soap_service_test.dart b/test/features/app/signets_api/soap_service_test.dart similarity index 100% rename from test/services/soap_service_test.dart rename to test/features/app/signets_api/soap_service_test.dart diff --git a/test/viewmodels/startup_viewmodel_test.dart b/test/features/app/startup/startup_viewmodel_test.dart similarity index 96% rename from test/viewmodels/startup_viewmodel_test.dart rename to test/features/app/startup/startup_viewmodel_test.dart index 428efaf67..b5d1c005b 100644 --- a/test/viewmodels/startup_viewmodel_test.dart +++ b/test/features/app/startup/startup_viewmodel_test.dart @@ -13,14 +13,14 @@ import 'package:notredame/features/app/startup/startup_viewmodel.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/features/app/storage/siren_flutter_service.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../helpers.dart'; -import '../mock/managers/settings_manager_mock.dart'; -import '../mock/managers/user_repository_mock.dart'; -import '../mock/services/internal_info_service_mock.dart'; -import '../mock/services/navigation_service_mock.dart'; -import '../mock/services/networking_service_mock.dart'; -import '../mock/services/preferences_service_mock.dart'; -import '../mock/services/siren_flutter_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../more/settings/mocks/settings_manager_mock.dart'; +import '../error/internal_info/mocks/internal_info_service_mock.dart'; +import '../integration/mocks/networking_service_mock.dart'; +import '../navigation/mocks/navigation_service_mock.dart'; +import '../repository/mocks/user_repository_mock.dart'; +import '../storage/mocks/preferences_service_mock.dart'; +import '../storage/mocks/siren_flutter_service_mock.dart'; void main() { late NavigationServiceMock navigationServiceMock; diff --git a/test/mock/managers/cache_manager_mock.dart b/test/features/app/storage/mocks/cache_manager_mock.dart similarity index 100% rename from test/mock/managers/cache_manager_mock.dart rename to test/features/app/storage/mocks/cache_manager_mock.dart diff --git a/test/mock/services/flutter_secure_storage_mock.dart b/test/features/app/storage/mocks/flutter_secure_storage_mock.dart similarity index 94% rename from test/mock/services/flutter_secure_storage_mock.dart rename to test/features/app/storage/mocks/flutter_secure_storage_mock.dart index f51d7ee45..a5d5f206d 100644 --- a/test/mock/services/flutter_secure_storage_mock.dart +++ b/test/features/app/storage/mocks/flutter_secure_storage_mock.dart @@ -4,7 +4,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; // Project imports: -import 'flutter_secure_storage_mock.mocks.dart'; +import '../../../../features/app/storage/mocks/flutter_secure_storage_mock.mocks.dart'; /// Mock for the [FlutterSecureStorage] @GenerateNiceMocks([MockSpec()]) diff --git a/test/mock/services/preferences_service_mock.dart b/test/features/app/storage/mocks/preferences_service_mock.dart similarity index 100% rename from test/mock/services/preferences_service_mock.dart rename to test/features/app/storage/mocks/preferences_service_mock.dart diff --git a/test/mock/services/siren_flutter_service_mock.dart b/test/features/app/storage/mocks/siren_flutter_service_mock.dart similarity index 100% rename from test/mock/services/siren_flutter_service_mock.dart rename to test/features/app/storage/mocks/siren_flutter_service_mock.dart diff --git a/test/services/preferences_service_test.dart b/test/features/app/storage/preferences_service_test.dart similarity index 100% rename from test/services/preferences_service_test.dart rename to test/features/app/storage/preferences_service_test.dart diff --git a/test/ui/widgets/base_scaffold_test.dart b/test/features/app/widgets/base_scaffold_test.dart similarity index 96% rename from test/ui/widgets/base_scaffold_test.dart rename to test/features/app/widgets/base_scaffold_test.dart index 23528b676..05af85112 100644 --- a/test/ui/widgets/base_scaffold_test.dart +++ b/test/features/app/widgets/base_scaffold_test.dart @@ -9,8 +9,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:notredame/features/app/integration/networking_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; -import '../../helpers.dart'; -import '../../mock/services/analytics_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../analytics/mocks/analytics_service_mock.dart'; void main() { group('BaseScaffold - ', () { diff --git a/test/ui/widgets/bottom_bar_test.dart b/test/features/app/widgets/bottom_bar_test.dart similarity index 96% rename from test/ui/widgets/bottom_bar_test.dart rename to test/features/app/widgets/bottom_bar_test.dart index 5dedfb8fa..8b467f2c0 100644 --- a/test/ui/widgets/bottom_bar_test.dart +++ b/test/features/app/widgets/bottom_bar_test.dart @@ -11,9 +11,9 @@ import 'package:notredame/features/app/integration/networking_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/widgets/bottom_bar.dart'; -import '../../helpers.dart'; -import '../../mock/services/analytics_service_mock.dart'; -import '../../mock/services/navigation_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../analytics/mocks/analytics_service_mock.dart'; +import '../navigation/mocks/navigation_service_mock.dart'; late NavigationServiceMock navigationServiceMock; diff --git a/test/ui/widgets/dismissible_card_test.dart b/test/features/app/widgets/dismissible_card_test.dart similarity index 96% rename from test/ui/widgets/dismissible_card_test.dart rename to test/features/app/widgets/dismissible_card_test.dart index fe57d60a8..5bbf268c8 100644 --- a/test/ui/widgets/dismissible_card_test.dart +++ b/test/features/app/widgets/dismissible_card_test.dart @@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/app/widgets/dismissible_card.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; void main() { const String cardText = "I'm a dismissible card !"; diff --git a/test/ui/widgets/link_web_view_test.dart b/test/features/app/widgets/link_web_view_test.dart similarity index 96% rename from test/ui/widgets/link_web_view_test.dart rename to test/features/app/widgets/link_web_view_test.dart index 213a42a4d..a2447d963 100644 --- a/test/ui/widgets/link_web_view_test.dart +++ b/test/features/app/widgets/link_web_view_test.dart @@ -9,7 +9,7 @@ import 'package:webview_flutter_android/webview_flutter_android.dart'; // Project imports: import 'package:notredame/features/app/widgets/link_web_view.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; final _quickLink = QuickLink( id: 1, diff --git a/test/ui/views/dashboard_view_test.dart b/test/features/dashboard/dashboard_view_test.dart similarity index 85% rename from test/ui/views/dashboard_view_test.dart rename to test/features/dashboard/dashboard_view_test.dart index 22d168135..e51b2b00f 100644 --- a/test/ui/views/dashboard_view_test.dart +++ b/test/features/dashboard/dashboard_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -19,11 +16,12 @@ import 'package:notredame/features/app/widgets/dismissible_card.dart'; import 'package:notredame/features/dashboard/dashboard_view.dart'; import 'package:notredame/features/dashboard/widgets/course_activity_tile.dart'; import 'package:notredame/features/student/grades/widgets/grade_button.dart'; -import '../../helpers.dart'; -import '../../mock/managers/course_repository_mock.dart'; -import '../../mock/managers/settings_manager_mock.dart'; -import '../../mock/services/in_app_review_service_mock.dart'; -import '../../mock/services/remote_config_service_mock.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../common/helpers.dart'; +import '../app/analytics/mocks/remote_config_service_mock.dart'; +import '../app/repository/mocks/course_repository_mock.dart'; +import '../more/feedback/mocks/in_app_review_service_mock.dart'; +import '../more/settings/mocks/settings_manager_mock.dart'; void main() { late SettingsManagerMock settingsManagerMock; @@ -70,12 +68,11 @@ void main() { final List activities = [gen101, gen102, gen103]; // Cards - Map dashboard = { - PreferencesFlag.broadcastCard: 0, - PreferencesFlag.aboutUsCard: 1, - PreferencesFlag.scheduleCard: 2, - PreferencesFlag.progressBarCard: 3, - PreferencesFlag.gradesCard: 4 + final Map dashboard = { + PreferencesFlag.aboutUsCard: 0, + PreferencesFlag.scheduleCard: 1, + PreferencesFlag.progressBarCard: 2, + PreferencesFlag.gradesCard: 3 }; final numberOfCards = dashboard.entries.length; @@ -164,7 +161,7 @@ void main() { // Find schedule card in second position by its title return tester.firstWidget(find.descendant( - of: find.byType(Dismissible, skipOffstage: false).at(2), + of: find.byType(Dismissible, skipOffstage: false).at(1), matching: find.byType(Text), )); } @@ -180,6 +177,9 @@ void main() { setupNetworkingServiceMock(); setupAnalyticsServiceMock(); setupPreferencesServiceMock(); + // TODO: Remove when 4.50.1 is released + SharedPreferences.setMockInitialValues({}); + // End TODO: Remove when 4.50.1 is released inAppReviewServiceMock = setupInAppReviewServiceMock() as InAppReviewServiceMock; @@ -199,24 +199,14 @@ void main() { CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock); CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, fromCacheOnly: true); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - RemoteConfigServiceMock.stubGetBroadcastEnabled(remoteConfigServiceMock); - RemoteConfigServiceMock.stubGetBroadcastColor(remoteConfigServiceMock); - RemoteConfigServiceMock.stubGetBroadcastEn(remoteConfigServiceMock); - RemoteConfigServiceMock.stubGetBroadcastFr(remoteConfigServiceMock); - RemoteConfigServiceMock.stubGetBroadcastTitleEn(remoteConfigServiceMock); - RemoteConfigServiceMock.stubGetBroadcastTitleFr(remoteConfigServiceMock); - RemoteConfigServiceMock.stubGetBroadcastType(remoteConfigServiceMock); - RemoteConfigServiceMock.stubGetBroadcastUrl(remoteConfigServiceMock); + RemoteConfigServiceMock.stubGetBroadcastEnabled(remoteConfigServiceMock, + toReturn: false); SettingsManagerMock.stubGetBool( settingsManagerMock, PreferencesFlag.discoveryDashboard, toReturn: true); - SettingsManagerMock.stubSetInt( - settingsManagerMock, PreferencesFlag.broadcastCard); - SettingsManagerMock.stubSetInt( settingsManagerMock, PreferencesFlag.aboutUsCard); @@ -353,7 +343,7 @@ void main() { of: find.byType(SizedBox, skipOffstage: false), matching: find.byType(Text), ), - findsNWidgets(1)); + findsNWidgets(2)); }); }); @@ -369,9 +359,6 @@ void main() { SettingsManagerMock.stubGetDashboard(settingsManagerMock, toReturn: dashboard); - SettingsManagerMock.stubSetInt( - settingsManagerMock, PreferencesFlag.broadcastCard); - SettingsManagerMock.stubSetInt( settingsManagerMock, PreferencesFlag.aboutUsCard); @@ -395,7 +382,7 @@ void main() { expect(find.text(intl.card_applets_title), findsOneWidget); // Swipe Dismissible aboutUs Card horizontally - await tester.drag(find.byType(Dismissible, skipOffstage: false).at(1), + await tester.drag(find.byType(Dismissible, skipOffstage: false).at(0), const Offset(1000.0, 0.0)); // Check that the card is now absent from the view @@ -425,9 +412,6 @@ void main() { fromCacheOnly: true); CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - SettingsManagerMock.stubSetInt( - settingsManagerMock, PreferencesFlag.broadcastCard); - SettingsManagerMock.stubSetInt( settingsManagerMock, PreferencesFlag.aboutUsCard); @@ -454,7 +438,7 @@ void main() { // Check that the aboutUs card is in the first position var text = tester.firstWidget(find.descendant( - of: find.byType(Dismissible, skipOffstage: false).at(1), + of: find.byType(Dismissible, skipOffstage: false).at(0), matching: find.byType(Text), )); @@ -485,7 +469,7 @@ void main() { await tester.pumpAndSettle(); text = tester.firstWidget(find.descendant( - of: find.byType(Dismissible, skipOffstage: false).at(1), + of: find.byType(Dismissible, skipOffstage: false).at(0), matching: find.byType(Text), )); @@ -513,7 +497,7 @@ void main() { findsOneWidget); // Swipe Dismissible schedule Card horizontally - await tester.drag(find.byType(Dismissible, skipOffstage: false).at(2), + await tester.drag(find.byType(Dismissible, skipOffstage: false).at(1), const Offset(1000.0, 0.0)); // Check that the card is now absent from the view @@ -596,9 +580,6 @@ void main() { testWidgets('gradesCard is dismissible and can be restored', (WidgetTester tester) async { - SettingsManagerMock.stubSetInt( - settingsManagerMock, PreferencesFlag.broadcastCard); - SettingsManagerMock.stubSetInt( settingsManagerMock, PreferencesFlag.aboutUsCard); @@ -625,10 +606,11 @@ void main() { findsOneWidget); // Swipe Dismissible grades Card horizontally - await tester.drag( - find.widgetWithText(Dismissible, intl.grades_title, - skipOffstage: false), - const Offset(1000.0, 0.0)); + final finder = find.widgetWithText(Dismissible, intl.grades_title, + skipOffstage: false); + await tester.scrollUntilVisible(finder, 100); + await tester.pumpAndSettle(); + await tester.drag(finder, const Offset(1000.0, 0.0)); // Check that the card is now absent from the view await tester.pumpAndSettle(); @@ -780,83 +762,5 @@ void main() { expect(text.data, intl.progress_bar_title); }); }); - - group("golden - ", () { - setUp(() async { - setupInAppReviewMock(); - }); - - testWidgets("Applets Card", (WidgetTester tester) async { - RemoteConfigServiceMock.stubGetBroadcastEnabled(remoteConfigServiceMock, - toReturn: false); - tester.view.physicalSize = const Size(800, 1410); - - final Map dashboard = { - PreferencesFlag.broadcastCard: 0, - PreferencesFlag.aboutUsCard: 1, - }; - - SettingsManagerMock.stubGetDashboard(settingsManagerMock, - toReturn: dashboard); - - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: const DashboardView(updateCode: UpdateCode.none)))); - await tester.pumpAndSettle(); - - await expectLater(find.byType(DashboardView), - matchesGoldenFile(goldenFilePath("dashboardView_appletsCard_1"))); - }); - - testWidgets("Schedule card", (WidgetTester tester) async { - RemoteConfigServiceMock.stubGetBroadcastEnabled(remoteConfigServiceMock, - toReturn: false); - tester.view.physicalSize = const Size(800, 1410); - - CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - - dashboard = { - PreferencesFlag.broadcastCard: 0, - PreferencesFlag.scheduleCard: 1, - }; - - SettingsManagerMock.stubGetDashboard(settingsManagerMock, - toReturn: dashboard); - - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: const DashboardView(updateCode: UpdateCode.none)))); - await tester.pumpAndSettle(); - - await expectLater(find.byType(DashboardView), - matchesGoldenFile(goldenFilePath("dashboardView_scheduleCard_1"))); - }); - testWidgets("progressBar Card", (WidgetTester tester) async { - RemoteConfigServiceMock.stubGetBroadcastEnabled(remoteConfigServiceMock, - toReturn: false); - tester.view.physicalSize = const Size(800, 1410); - - dashboard = { - PreferencesFlag.broadcastCard: 0, - PreferencesFlag.progressBarCard: 1, - }; - - SettingsManagerMock.stubGetDashboard(settingsManagerMock, - toReturn: dashboard); - - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: const DashboardView(updateCode: UpdateCode.none)))); - await tester.pumpAndSettle(); - - await expectLater( - find.byType(DashboardView), - matchesGoldenFile( - goldenFilePath("dashboardView_progressBarCard_1"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/dashboard_viewmodel_test.dart b/test/features/dashboard/dashboard_viewmodel_test.dart similarity index 95% rename from test/viewmodels/dashboard_viewmodel_test.dart rename to test/features/dashboard/dashboard_viewmodel_test.dart index fd5886c8f..ac34622e4 100644 --- a/test/viewmodels/dashboard_viewmodel_test.dart +++ b/test/features/dashboard/dashboard_viewmodel_test.dart @@ -11,13 +11,14 @@ import 'package:notredame/features/dashboard/dashboard_viewmodel.dart'; import 'package:notredame/features/dashboard/progress_bar_text_options.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/utils/activity_code.dart'; -import '../helpers.dart'; -import '../mock/managers/course_repository_mock.dart'; -import '../mock/managers/settings_manager_mock.dart'; -import '../mock/services/analytics_service_mock.dart'; -import '../mock/services/in_app_review_service_mock.dart'; -import '../mock/services/preferences_service_mock.dart'; -import '../mock/services/remote_config_service_mock.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../common/helpers.dart'; +import '../app/analytics/mocks/analytics_service_mock.dart'; +import '../app/analytics/mocks/remote_config_service_mock.dart'; +import '../app/repository/mocks/course_repository_mock.dart'; +import '../app/storage/mocks/preferences_service_mock.dart'; +import '../more/feedback/mocks/in_app_review_service_mock.dart'; +import '../more/settings/mocks/settings_manager_mock.dart'; void main() { late PreferencesServiceMock preferenceServiceMock; @@ -155,26 +156,23 @@ void main() { // Cards final Map dashboard = { - PreferencesFlag.broadcastCard: 0, - PreferencesFlag.aboutUsCard: 1, - PreferencesFlag.scheduleCard: 2, - PreferencesFlag.progressBarCard: 3, + PreferencesFlag.aboutUsCard: 0, + PreferencesFlag.scheduleCard: 1, + PreferencesFlag.progressBarCard: 2, }; // Reorderered Cards final Map reorderedDashboard = { - PreferencesFlag.broadcastCard: 1, - PreferencesFlag.aboutUsCard: 2, - PreferencesFlag.scheduleCard: 3, + PreferencesFlag.aboutUsCard: 1, + PreferencesFlag.scheduleCard: 2, PreferencesFlag.progressBarCard: 0, }; // Reorderered Cards with hidden scheduleCard final Map hiddenCardDashboard = { - PreferencesFlag.broadcastCard: 0, - PreferencesFlag.aboutUsCard: 1, + PreferencesFlag.aboutUsCard: 0, PreferencesFlag.scheduleCard: -1, - PreferencesFlag.progressBarCard: 2, + PreferencesFlag.progressBarCard: 1, }; // Session @@ -202,6 +200,9 @@ void main() { preferenceServiceMock = setupPreferencesServiceMock(); analyticsServiceMock = setupAnalyticsServiceMock(); preferencesServiceMock = setupPreferencesServiceMock(); + // TODO: Remove when 4.50.1 is released + SharedPreferences.setMockInitialValues({}); + // TODO: End remove when 4.50.1 is released viewModel = DashboardViewModel(intl: await setupAppIntl()); CourseRepositoryMock.stubGetSessions(courseRepositoryMock, @@ -342,7 +343,6 @@ void main() { await viewModel.futureToRun(); expect(viewModel.cards, dashboard); expect(viewModel.cardsToDisplay, [ - PreferencesFlag.broadcastCard, PreferencesFlag.aboutUsCard, PreferencesFlag.scheduleCard, PreferencesFlag.progressBarCard @@ -549,8 +549,6 @@ void main() { CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock); CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - PreferencesServiceMock.stubException( - preferenceServiceMock, PreferencesFlag.broadcastCard); PreferencesServiceMock.stubException( preferenceServiceMock, PreferencesFlag.aboutUsCard); PreferencesServiceMock.stubException( @@ -650,8 +648,6 @@ void main() { group("interact with cards - ", () { test("can hide a card and reset cards to default layout", () async { - SettingsManagerMock.stubSetInt( - settingsManagerMock, PreferencesFlag.broadcastCard); SettingsManagerMock.stubSetInt( settingsManagerMock, PreferencesFlag.aboutUsCard); SettingsManagerMock.stubSetInt( @@ -672,32 +668,26 @@ void main() { settingsManagerMock.setInt(PreferencesFlag.scheduleCard, -1)); expect(viewModel.cards, hiddenCardDashboard); - expect(viewModel.cardsToDisplay, [ - PreferencesFlag.broadcastCard, - PreferencesFlag.aboutUsCard, - PreferencesFlag.progressBarCard - ]); + expect(viewModel.cardsToDisplay, + [PreferencesFlag.aboutUsCard, PreferencesFlag.progressBarCard]); verify(analyticsServiceMock.logEvent( "DashboardViewModel", "Deleting scheduleCard")); verify(settingsManagerMock.setInt(PreferencesFlag.scheduleCard, -1)) .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.broadcastCard, 0)) + verify(settingsManagerMock.setInt(PreferencesFlag.aboutUsCard, 0)) .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.aboutUsCard, 1)) - .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.progressBarCard, 2)) + verify(settingsManagerMock.setInt(PreferencesFlag.progressBarCard, 1)) .called(1); // Call the setter. viewModel.setAllCardsVisible(); await untilCalled( - settingsManagerMock.setInt(PreferencesFlag.progressBarCard, 3)); + settingsManagerMock.setInt(PreferencesFlag.progressBarCard, 2)); expect(viewModel.cards, dashboard); expect(viewModel.cardsToDisplay, [ - PreferencesFlag.broadcastCard, PreferencesFlag.aboutUsCard, PreferencesFlag.scheduleCard, PreferencesFlag.progressBarCard @@ -706,13 +696,11 @@ void main() { verify(analyticsServiceMock.logEvent( "DashboardViewModel", "Restoring cards")); verify(settingsManagerMock.getDashboard()).called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.broadcastCard, 0)) + verify(settingsManagerMock.setInt(PreferencesFlag.aboutUsCard, 0)) .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.aboutUsCard, 1)) - .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.scheduleCard, 2)) + verify(settingsManagerMock.setInt(PreferencesFlag.scheduleCard, 1)) .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.progressBarCard, 3)) + verify(settingsManagerMock.setInt(PreferencesFlag.progressBarCard, 2)) .called(1); verify(settingsManagerMock.getString(PreferencesFlag.progressBarText)) .called(2); @@ -727,9 +715,6 @@ void main() { SettingsManagerMock.stubGetDashboard(settingsManagerMock, toReturn: dashboard); - - SettingsManagerMock.stubSetInt( - settingsManagerMock, PreferencesFlag.broadcastCard); SettingsManagerMock.stubSetInt( settingsManagerMock, PreferencesFlag.aboutUsCard); SettingsManagerMock.stubSetInt( @@ -741,7 +726,6 @@ void main() { expect(viewModel.cards, dashboard); expect(viewModel.cardsToDisplay, [ - PreferencesFlag.broadcastCard, PreferencesFlag.aboutUsCard, PreferencesFlag.scheduleCard, PreferencesFlag.progressBarCard, @@ -756,7 +740,6 @@ void main() { expect(viewModel.cards, reorderedDashboard); expect(viewModel.cardsToDisplay, [ PreferencesFlag.progressBarCard, - PreferencesFlag.broadcastCard, PreferencesFlag.aboutUsCard, PreferencesFlag.scheduleCard ]); @@ -766,11 +749,9 @@ void main() { verify(settingsManagerMock.getDashboard()).called(1); verify(settingsManagerMock.setInt(PreferencesFlag.progressBarCard, 0)) .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.broadcastCard, 1)) - .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.aboutUsCard, 2)) + verify(settingsManagerMock.setInt(PreferencesFlag.aboutUsCard, 1)) .called(1); - verify(settingsManagerMock.setInt(PreferencesFlag.scheduleCard, 3)) + verify(settingsManagerMock.setInt(PreferencesFlag.scheduleCard, 2)) .called(1); verify(settingsManagerMock.getString(PreferencesFlag.progressBarText)) .called(1); diff --git a/test/ui/widgets/course_activity_tile_test.dart b/test/features/dashboard/widgets/course_activity_tile_test.dart similarity index 97% rename from test/ui/widgets/course_activity_tile_test.dart rename to test/features/dashboard/widgets/course_activity_tile_test.dart index f761bd8d1..fb97e7e5a 100644 --- a/test/ui/widgets/course_activity_tile_test.dart +++ b/test/features/dashboard/widgets/course_activity_tile_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/app/signets-api/models/course_activity.dart'; import 'package:notredame/features/dashboard/widgets/course_activity_tile.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; final CourseActivity course = CourseActivity( courseGroup: 'GEN101-01', diff --git a/test/ui/views/ets_view_test.dart b/test/features/ets/ets_view_test.dart similarity index 91% rename from test/ui/views/ets_view_test.dart rename to test/features/ets/ets_view_test.dart index 140ec5860..2558e523a 100644 --- a/test/ui/views/ets_view_test.dart +++ b/test/features/ets/ets_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -22,9 +19,11 @@ import 'package:notredame/features/ets/events/api-client/models/news_tags.dart'; import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../../helpers.dart'; -import '../../mock/managers/news_repository_mock.dart'; -import '../../mock/services/remote_config_service_mock.dart'; +import '../../common/helpers.dart'; +import '../app/analytics/mocks/remote_config_service_mock.dart'; +import '../app/repository/mocks/news_repository_mock.dart'; + +// Flutter imports: void main() { late NewsRepositoryMock newsRepository; @@ -162,18 +161,5 @@ void main() { expect(find.byType(BaseScaffold), findsOneWidget); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget( - localizedWidget(child: FeatureDiscovery(child: ETSView()))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater(find.byType(ETSView), - matchesGoldenFile(goldenFilePath("etsView_1"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/services/hello_api_client_test.dart b/test/features/ets/events/api-client/hello_api_client_test.dart similarity index 99% rename from test/services/hello_api_client_test.dart rename to test/features/ets/events/api-client/hello_api_client_test.dart index 1d721107b..fd98c4d6f 100644 --- a/test/services/hello_api_client_test.dart +++ b/test/features/ets/events/api-client/hello_api_client_test.dart @@ -12,7 +12,7 @@ import 'package:notredame/features/ets/events/api-client/models/paginated_news.d import 'package:notredame/features/ets/events/api-client/models/report.dart'; import 'package:notredame/utils/api_response.dart'; import 'package:notredame/utils/http_exception.dart'; -import '../mock/signets-api-client/http_client_mock_helper.dart'; +import '../../../app/signets_api/http_client_mock_helper.dart'; void main() { const String helloNewsAPI = "api.hello.ca"; diff --git a/test/ui/widgets/author_info_skeleton_test.dart b/test/features/ets/events/author/author_info_skeleton_test.dart similarity index 93% rename from test/ui/widgets/author_info_skeleton_test.dart rename to test/features/ets/events/author/author_info_skeleton_test.dart index f60b1f116..e70ec16c9 100644 --- a/test/ui/widgets/author_info_skeleton_test.dart +++ b/test/features/ets/events/author/author_info_skeleton_test.dart @@ -4,7 +4,7 @@ import 'package:shimmer/shimmer.dart'; // Project imports: import 'package:notredame/features/ets/events/author/author_info_skeleton.dart'; -import '../../helpers.dart'; +import '../../../../common/helpers.dart'; void main() { group('AuthorInfoSkeleton Tests', () { diff --git a/test/ui/views/author_view_test.dart b/test/features/ets/events/author/author_view_test.dart similarity index 85% rename from test/ui/views/author_view_test.dart rename to test/features/ets/events/author/author_view_test.dart index d8a5af854..90e7c7530 100644 --- a/test/ui/views/author_view_test.dart +++ b/test/features/ets/events/author/author_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -24,9 +21,11 @@ import 'package:notredame/features/ets/events/author/author_view.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card.dart'; import 'package:notredame/features/ets/events/social/social_links_card.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../../helpers.dart'; -import '../../mock/managers/author_repository_mock.dart'; -import '../../mock/managers/news_repository_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/repository/mocks/author_repository_mock.dart'; +import '../../../app/repository/mocks/news_repository_mock.dart'; + +// Package imports: void main() { late AuthorRepositoryMock authorRepository; @@ -208,33 +207,5 @@ void main() { expect(find.text(newsItem.title), findsOneWidget); } }); - - group("golden - ", () { - testWidgets("author view news empty", (WidgetTester tester) async { - NewsRepositoryMock.stubGetNewsOrganizer(newsRepository, organizerId, - toReturn: paginatedNewsEmpty); - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget( - localizedWidget(child: const AuthorView(authorId: organizerId))); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - await expectLater(find.byType(AuthorView), - matchesGoldenFile(goldenFilePath("authorView_1"))); - }); - - testWidgets("author view", (WidgetTester tester) async { - NewsRepositoryMock.stubGetNewsOrganizer(newsRepository, organizerId, - toReturn: paginatedNews); - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget( - localizedWidget(child: const AuthorView(authorId: organizerId))); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - await expectLater(find.byType(AuthorView), - matchesGoldenFile(goldenFilePath("authorView_2"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/author_viewmodel_test.dart b/test/features/ets/events/author/author_viewmodel_test.dart similarity index 96% rename from test/viewmodels/author_viewmodel_test.dart rename to test/features/ets/events/author/author_viewmodel_test.dart index b6b88a2c8..f359968a4 100644 --- a/test/viewmodels/author_viewmodel_test.dart +++ b/test/features/ets/events/author/author_viewmodel_test.dart @@ -13,9 +13,9 @@ import 'package:notredame/features/ets/events/api-client/models/news_tags.dart'; import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; import 'package:notredame/features/ets/events/author/author_viewmodel.dart'; -import '../helpers.dart'; -import '../mock/managers/author_repository_mock.dart'; -import '../mock/managers/news_repository_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/repository/mocks/author_repository_mock.dart'; +import '../../../app/repository/mocks/news_repository_mock.dart'; void main() { late AuthorViewModel viewModel; diff --git a/test/features/ets/events/author/goldenFiles/authorView_1.png b/test/features/ets/events/author/goldenFiles/authorView_1.png new file mode 100644 index 000000000..948071e01 Binary files /dev/null and b/test/features/ets/events/author/goldenFiles/authorView_1.png differ diff --git a/test/features/ets/events/author/goldenFiles/authorView_2.png b/test/features/ets/events/author/goldenFiles/authorView_2.png new file mode 100644 index 000000000..0f6ed6d25 Binary files /dev/null and b/test/features/ets/events/author/goldenFiles/authorView_2.png differ diff --git a/test/ui/views/news_details_view_test.dart b/test/features/ets/events/news/news-details/news_details_view_test.dart similarity index 84% rename from test/ui/views/news_details_view_test.dart rename to test/features/ets/events/news/news-details/news_details_view_test.dart index b801354f5..4f355a6cf 100644 --- a/test/ui/views/news_details_view_test.dart +++ b/test/features/ets/events/news/news-details/news_details_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -16,7 +13,9 @@ import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/features/ets/events/api-client/models/news_tags.dart'; import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; import 'package:notredame/features/ets/events/news/news-details/news_details_view.dart'; -import '../../helpers.dart'; +import '../../../../../common/helpers.dart'; + +// Package imports: void main() { late News sampleNews; @@ -79,20 +78,5 @@ void main() { expect(find.text(sampleNews.organizer.organization!), findsOneWidget); expect(find.byType(IconButton), findsWidgets); }); - - group("golden - ", () { - testWidgets("news details view", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget( - child: NewsDetailsView( - news: sampleNews, - ))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater(find.byType(NewsDetailsView), - matchesGoldenFile(goldenFilePath("newsDetailsView_1"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/news_details_viewmodel_test.dart b/test/features/ets/events/news/news-details/news_details_viewmodel_test.dart similarity index 100% rename from test/viewmodels/news_details_viewmodel_test.dart rename to test/features/ets/events/news/news-details/news_details_viewmodel_test.dart diff --git a/test/ui/views/news_view_test.dart b/test/features/ets/events/news/news_view_test.dart similarity index 79% rename from test/ui/views/news_view_test.dart rename to test/features/ets/events/news/news_view_test.dart index 7164fcc7a..150477478 100644 --- a/test/ui/views/news_view_test.dart +++ b/test/features/ets/events/news/news_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -19,8 +16,10 @@ import 'package:notredame/features/ets/events/api-client/models/paginated_news.d import 'package:notredame/features/ets/events/news/news_view.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../../helpers.dart'; -import '../../mock/managers/news_repository_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/repository/mocks/news_repository_mock.dart'; + +// Flutter imports: void main() { late NewsRepositoryMock newsRepository; @@ -115,32 +114,5 @@ void main() { expect(find.byType(NewsCard), findsNWidgets(1)); }); - - group("golden - ", () { - testWidgets("news view empty", (WidgetTester tester) async { - NewsRepositoryMock.stubGetNews(newsRepository, - toReturn: paginatedEmptyNews); - - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget(child: NewsView())); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater(find.byType(NewsView), - matchesGoldenFile(goldenFilePath("newsView_1"))); - }); - - testWidgets("news view", (WidgetTester tester) async { - NewsRepositoryMock.stubGetNews(newsRepository, toReturn: paginatedNews); - - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget(child: NewsView())); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater(find.byType(NewsView), - matchesGoldenFile(goldenFilePath("newsView_2"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/news_viewmodel_test.dart b/test/features/ets/events/news/news_viewmodel_test.dart similarity index 96% rename from test/viewmodels/news_viewmodel_test.dart rename to test/features/ets/events/news/news_viewmodel_test.dart index 7b85e6a52..54cdbd8f4 100644 --- a/test/viewmodels/news_viewmodel_test.dart +++ b/test/features/ets/events/news/news_viewmodel_test.dart @@ -1,3 +1,5 @@ +// Package imports: + // Package imports: import 'package:flutter_test/flutter_test.dart'; import 'package:logger/logger.dart'; @@ -11,8 +13,8 @@ import 'package:notredame/features/ets/events/api-client/models/paginated_news.d import 'package:notredame/features/ets/events/news/news_viewmodel.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/utils/locator.dart'; -import '../helpers.dart'; -import '../mock/managers/news_repository_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/repository/mocks/news_repository_mock.dart'; void main() { late NewsViewModel viewModel; diff --git a/test/ui/widgets/news_card_test.dart b/test/features/ets/events/news/widgets/news_card_test.dart similarity index 97% rename from test/ui/widgets/news_card_test.dart rename to test/features/ets/events/news/widgets/news_card_test.dart index 5e98ff729..2cf8fdf91 100644 --- a/test/ui/widgets/news_card_test.dart +++ b/test/features/ets/events/news/widgets/news_card_test.dart @@ -9,7 +9,9 @@ import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/features/ets/events/api-client/models/news_tags.dart'; import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card.dart'; -import '../../helpers.dart'; +import '../../../../../common/helpers.dart'; + +// Package imports: void main() { final news = News( diff --git a/test/ui/widgets/news_skeleton_test.dart b/test/features/ets/events/news/widgets/news_skeleton_test.dart similarity index 92% rename from test/ui/widgets/news_skeleton_test.dart rename to test/features/ets/events/news/widgets/news_skeleton_test.dart index d574e3920..75689bff8 100644 --- a/test/ui/widgets/news_skeleton_test.dart +++ b/test/features/ets/events/news/widgets/news_skeleton_test.dart @@ -4,7 +4,7 @@ import 'package:shimmer/shimmer.dart'; // Project imports: import 'package:notredame/features/ets/events/news/widgets/news_card_skeleton.dart'; -import '../../helpers.dart'; +import '../../../../../common/helpers.dart'; void main() { group('News card skeleton Tests', () { diff --git a/test/models/report_news_test.dart b/test/features/ets/events/report-news/models/report_news_test.dart similarity index 100% rename from test/models/report_news_test.dart rename to test/features/ets/events/report-news/models/report_news_test.dart diff --git a/test/ui/views/quick_links_view_test.dart b/test/features/ets/quick-link/quick_links_view_test.dart similarity index 67% rename from test/ui/views/quick_links_view_test.dart rename to test/features/ets/quick-link/quick_links_view_test.dart index 5b2211b06..7c0ebbfd4 100644 --- a/test/ui/views/quick_links_view_test.dart +++ b/test/features/ets/quick-link/quick_links_view_test.dart @@ -1,9 +1,3 @@ -// Dart imports: -import 'dart:io'; - -// Flutter imports: -import 'package:flutter/material.dart'; - // Package imports: import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -16,11 +10,11 @@ import 'package:notredame/features/app/repository/quick_link_repository.dart'; import 'package:notredame/features/ets/quick-link/models/quick_links.dart'; import 'package:notredame/features/ets/quick-link/quick_links_view.dart'; import 'package:notredame/features/ets/quick-link/widgets/web_link_card.dart'; -import '../../helpers.dart'; -import '../../mock/managers/quick_links_repository_mock.dart'; -import '../../mock/services/analytics_service_mock.dart'; -import '../../mock/services/internal_info_service_mock.dart'; -import '../../mock/services/navigation_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/analytics/mocks/analytics_service_mock.dart'; +import '../../app/error/internal_info/mocks/internal_info_service_mock.dart'; +import '../../app/navigation/mocks/navigation_service_mock.dart'; +import '../../app/repository/mocks/quick_links_repository_mock.dart'; void main() { late AppIntl intl; @@ -62,21 +56,6 @@ void main() { expect(find.byType(WebLinkCard, skipOffstage: false), findsNWidgets(quickLinks(intl).length)); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery(child: QuickLinksView()), - useScaffold: false)); - await tester.pumpAndSettle(); - await tester.pump(const Duration(milliseconds: 500)); - - await expectLater(find.byType(QuickLinksView), - matchesGoldenFile(goldenFilePath("quicksLinksView_1"))); - }); - }, skip: !Platform.isLinux); }); }); } diff --git a/test/viewmodels/quick_links_viewmodel_test.dart b/test/features/ets/quick-link/quick_links_viewmodel_test.dart similarity index 97% rename from test/viewmodels/quick_links_viewmodel_test.dart rename to test/features/ets/quick-link/quick_links_viewmodel_test.dart index 119ea96a5..6e0d1bd35 100644 --- a/test/viewmodels/quick_links_viewmodel_test.dart +++ b/test/features/ets/quick-link/quick_links_viewmodel_test.dart @@ -8,8 +8,8 @@ import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link_data.dart'; import 'package:notredame/features/ets/quick-link/models/quick_links.dart'; import 'package:notredame/features/ets/quick-link/quick_links_viewmodel.dart'; -import '../helpers.dart'; -import '../mock/managers/quick_links_repository_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/repository/mocks/quick_links_repository_mock.dart'; void main() { late QuickLinkRepositoryMock quickLinkRepositoryMock; diff --git a/test/ui/views/emergency_view_test.dart b/test/features/ets/quick-link/widgets/security-info/emergency_view_test.dart similarity index 68% rename from test/ui/views/emergency_view_test.dart rename to test/features/ets/quick-link/widgets/security-info/emergency_view_test.dart index cff82deea..e329c82b2 100644 --- a/test/ui/views/emergency_view_test.dart +++ b/test/features/ets/quick-link/widgets/security-info/emergency_view_test.dart @@ -1,5 +1,6 @@ // Flutter imports: import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; // Package imports: import 'package:flutter_test/flutter_test.dart'; @@ -8,25 +9,26 @@ import 'package:webview_flutter_android/webview_flutter_android.dart'; // Project imports: import 'package:notredame/features/ets/quick-link/widgets/security-info/emergency_view.dart'; -import '../../helpers.dart'; +import '../../../../../common/helpers.dart'; void main() { group('EmergencyView - ', () { setUp(() async { WebViewPlatform.instance = AndroidWebViewPlatform(); + setupNetworkingServiceMock(); }); tearDown(() {}); group('UI - ', () { - testWidgets('has call button and webview', (WidgetTester tester) async { + testWidgets('has call button and markdown view', (WidgetTester tester) async { await tester.pumpWidget(localizedWidget( child: const EmergencyView( - 'testEmergency', 'assets/html/armed_person_detail_en.html'))); + 'testEmergency', 'assets/markdown/armed_person_en.md'))); await tester.pumpAndSettle(); - final webView = find.byType(WebViewWidget); - expect(webView, findsOneWidget); + final markdown = find.byType(Markdown); + expect(markdown, findsOneWidget); final Finder phoneButton = find.byType(FloatingActionButton); expect(phoneButton, findsOneWidget); diff --git a/test/ui/views/security_view_test.dart b/test/features/ets/quick-link/widgets/security-info/security_view_test.dart similarity index 94% rename from test/ui/views/security_view_test.dart rename to test/features/ets/quick-link/widgets/security-info/security_view_test.dart index ada787023..6ff3db4eb 100644 --- a/test/ui/views/security_view_test.dart +++ b/test/features/ets/quick-link/widgets/security-info/security_view_test.dart @@ -9,7 +9,7 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; // Project imports: import 'package:notredame/features/ets/quick-link/widgets/security-info/models/emergency_procedures.dart'; import 'package:notredame/features/ets/quick-link/widgets/security-info/security_view.dart'; -import '../../helpers.dart'; +import '../../../../../common/helpers.dart'; void main() { late AppIntl intl; @@ -17,6 +17,7 @@ void main() { group('SecurityView - ', () { setUp(() async { intl = await setupAppIntl(); + setupNetworkingServiceMock(); }); tearDown(() {}); diff --git a/test/ui/widgets/web_link_card_test.dart b/test/features/ets/quick-link/widgets/web_link_card_test.dart similarity index 90% rename from test/ui/widgets/web_link_card_test.dart rename to test/features/ets/quick-link/widgets/web_link_card_test.dart index a42186711..ae2e4d62a 100644 --- a/test/ui/widgets/web_link_card_test.dart +++ b/test/features/ets/quick-link/widgets/web_link_card_test.dart @@ -11,9 +11,9 @@ import 'package:notredame/features/app/error/internal_info_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/widgets/web_link_card.dart'; -import '../../helpers.dart'; -import '../../mock/services/analytics_service_mock.dart'; -import '../../mock/services/launch_url_service_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/analytics/mocks/analytics_service_mock.dart'; +import '../../../app/integration/mocks/launch_url_service_mock.dart'; final _quickLink = QuickLink( id: 1, image: const Icon(Icons.ac_unit), name: 'test', link: 'testlink'); diff --git a/test/viewmodels/web_link_card_viewmodel_test.dart b/test/features/ets/quick-link/widgets/web_link_card_viewmodel_test.dart similarity index 89% rename from test/viewmodels/web_link_card_viewmodel_test.dart rename to test/features/ets/quick-link/widgets/web_link_card_viewmodel_test.dart index db135c78b..38074e16a 100644 --- a/test/viewmodels/web_link_card_viewmodel_test.dart +++ b/test/features/ets/quick-link/widgets/web_link_card_viewmodel_test.dart @@ -13,11 +13,11 @@ import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/widgets/web_link_card_viewmodel.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../helpers.dart'; -import '../mock/services/analytics_service_mock.dart'; -import '../mock/services/internal_info_service_mock.dart'; -import '../mock/services/launch_url_service_mock.dart'; -import '../mock/services/navigation_service_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/analytics/mocks/analytics_service_mock.dart'; +import '../../../app/error/internal_info/mocks/internal_info_service_mock.dart'; +import '../../../app/integration/mocks/launch_url_service_mock.dart'; +import '../../../app/navigation/mocks/navigation_service_mock.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); diff --git a/test/ui/views/about_view_test.dart b/test/features/more/about/about_view_test.dart similarity index 52% rename from test/ui/views/about_view_test.dart rename to test/features/more/about/about_view_test.dart index 418be4909..23d376d1a 100644 --- a/test/ui/views/about_view_test.dart +++ b/test/features/more/about/about_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -9,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/more/about/about_view.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -36,25 +33,6 @@ void main() { final row = find.byType(Row); expect(row, findsOneWidget); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.runAsync(() async { - await tester.pumpWidget( - localizedWidget(useScaffold: false, child: AboutView())); - final Element element = tester.element(find.byType(Hero)); - final Hero widget = element.widget as Hero; - final Image image = widget.child as Image; - await precacheImage(image.image, element); - await tester.pumpAndSettle(); - }); - await tester.pumpAndSettle(); - await expectLater(find.byType(AboutView), - matchesGoldenFile(goldenFilePath("aboutView_1"))); - }); - }, skip: !Platform.isLinux); }); }); } diff --git a/test/ui/views/faq_view_test.dart b/test/features/more/faq/faq_view_test.dart similarity index 79% rename from test/ui/views/faq_view_test.dart rename to test/features/more/faq/faq_view_test.dart index e65e48df8..5a8cf53fb 100644 --- a/test/ui/views/faq_view_test.dart +++ b/test/features/more/faq/faq_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -12,8 +9,8 @@ import 'package:shared_preferences/shared_preferences.dart'; // Project imports: import 'package:notredame/features/more/faq/faq_view.dart'; import 'package:notredame/features/more/faq/models/faq.dart'; -import '../../helpers.dart'; -import '../../mock/managers/settings_manager_mock.dart'; +import '../../../common/helpers.dart'; +import '../settings/mocks/settings_manager_mock.dart'; void main() { SharedPreferences.setMockInitialValues({}); @@ -72,18 +69,5 @@ void main() { expect(subtitle2, findsNWidgets(1)); }); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - SettingsManagerMock.stubLocale(settingsManagerMock); - tester.view.physicalSize = const Size(1800, 2410); - - await tester.pumpWidget(localizedWidget(child: const FaqView())); - await tester.pumpAndSettle(); - - await expectLater(find.byType(FaqView), - matchesGoldenFile(goldenFilePath("FaqView_1"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/faq_viewmodel_test.dart b/test/features/more/faq/faq_viewmodel_test.dart similarity index 92% rename from test/viewmodels/faq_viewmodel_test.dart rename to test/features/more/faq/faq_viewmodel_test.dart index da0730fbf..80284e733 100644 --- a/test/viewmodels/faq_viewmodel_test.dart +++ b/test/features/more/faq/faq_viewmodel_test.dart @@ -9,8 +9,8 @@ import 'package:mockito/mockito.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; import 'package:notredame/features/more/faq/faq_viewmodel.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../helpers.dart'; -import '../mock/services/launch_url_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/integration/mocks/launch_url_service_mock.dart'; void main() { late LaunchUrlServiceMock launchUrlServiceMock; diff --git a/test/viewmodels/feedback_viewmodel_test.dart b/test/features/more/feedback/feedback_viewmodel_test.dart similarity index 97% rename from test/viewmodels/feedback_viewmodel_test.dart rename to test/features/more/feedback/feedback_viewmodel_test.dart index 9f296e627..48985cf20 100644 --- a/test/viewmodels/feedback_viewmodel_test.dart +++ b/test/features/more/feedback/feedback_viewmodel_test.dart @@ -19,9 +19,9 @@ import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/more/feedback/feedback_type.dart'; import 'package:notredame/features/more/feedback/feedback_viewmodel.dart'; import 'package:notredame/features/more/feedback/models/feedback_issue.dart'; -import '../helpers.dart'; -import '../mock/services/github_api_mock.dart'; -import '../mock/services/preferences_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/integration/mocks/github_api_mock.dart'; +import '../../app/storage/mocks/preferences_service_mock.dart'; void main() { // Needed to support FlutterToast. diff --git a/test/mock/services/in_app_review_service_mock.dart b/test/features/more/feedback/mocks/in_app_review_service_mock.dart similarity index 89% rename from test/mock/services/in_app_review_service_mock.dart rename to test/features/more/feedback/mocks/in_app_review_service_mock.dart index 314962191..194613190 100644 --- a/test/mock/services/in_app_review_service_mock.dart +++ b/test/features/more/feedback/mocks/in_app_review_service_mock.dart @@ -4,7 +4,7 @@ import 'package:mockito/mockito.dart'; // Project imports: import 'package:notredame/features/more/feedback/in_app_review_service.dart'; -import 'in_app_review_service_mock.mocks.dart'; +import '../../../../features/more/feedback/mocks/in_app_review_service_mock.mocks.dart'; /// Mock for the [AnalyticsService] @GenerateNiceMocks([MockSpec()]) diff --git a/test/models/feedback_issue_test.dart b/test/features/more/feedback/models/feedback_issue_test.dart similarity index 100% rename from test/models/feedback_issue_test.dart rename to test/features/more/feedback/models/feedback_issue_test.dart diff --git a/test/ui/views/feedback_view_test.dart b/test/features/more/feedback/models/feedback_view_test.dart similarity index 61% rename from test/ui/views/feedback_view_test.dart rename to test/features/more/feedback/models/feedback_view_test.dart index cc4911755..fa7b4821d 100644 --- a/test/ui/views/feedback_view_test.dart +++ b/test/features/more/feedback/models/feedback_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -9,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/more/feedback/feedback_view.dart'; -import '../../helpers.dart'; +import '../../../../common/helpers.dart'; void main() { group('FeedbackView - ', () { @@ -31,16 +28,5 @@ void main() { expect(elevatedButton, findsNWidgets(2)); }); }); - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget(child: FeedbackView())); - await tester.pumpAndSettle(); - - await expectLater(find.byType(FeedbackView), - matchesGoldenFile(goldenFilePath("feedbackView_1"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/ui/views/more_view_test.dart b/test/features/more/more_view_test.dart similarity index 87% rename from test/ui/views/more_view_test.dart rename to test/features/more/more_view_test.dart index 5c52621ce..e6fe1203e 100644 --- a/test/ui/views/more_view_test.dart +++ b/test/features/more/more_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -15,11 +12,11 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/more/more_view.dart'; -import '../../helpers.dart'; -import '../../mock/managers/settings_manager_mock.dart'; -import '../../mock/services/in_app_review_service_mock.dart'; -import '../../mock/services/navigation_service_mock.dart'; -import '../../mock/services/remote_config_service_mock.dart'; +import '../../common/helpers.dart'; +import '../app/analytics/mocks/remote_config_service_mock.dart'; +import '../app/navigation/mocks/navigation_service_mock.dart'; +import 'feedback/mocks/in_app_review_service_mock.dart'; +import 'settings/mocks/settings_manager_mock.dart'; void main() { SharedPreferences.setMockInitialValues({}); @@ -242,29 +239,6 @@ void main() { expect(find.byType(AlertDialog), findsOneWidget); }); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - RemoteConfigServiceMock.stubGetPrivacyPolicyEnabled( - remoteConfigServiceMock, - toReturn: false); - tester.view.physicalSize = const Size(800, 1410); - - await tester.runAsync(() async { - await tester.pumpWidget( - localizedWidget(child: FeatureDiscovery(child: MoreView()))); - final Element element = tester.element(find.byType(Hero)); - final Hero widget = element.widget as Hero; - final Image image = widget.child as Image; - await precacheImage(image.image, element); - await tester.pumpAndSettle(); - }); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater(find.byType(MoreView), - matchesGoldenFile(goldenFilePath("moreView_1"))); - }); - }, skip: !Platform.isLinux); }); }); } diff --git a/test/viewmodels/more_viewmodel_test.dart b/test/features/more/more_viewmodel_test.dart similarity index 88% rename from test/viewmodels/more_viewmodel_test.dart rename to test/features/more/more_viewmodel_test.dart index d4c0028de..a3090d3c2 100644 --- a/test/viewmodels/more_viewmodel_test.dart +++ b/test/features/more/more_viewmodel_test.dart @@ -1,9 +1,12 @@ // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart'; import 'package:mockito/mockito.dart'; // Project imports: +import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; @@ -15,14 +18,14 @@ import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/features/more/more_viewmodel.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../helpers.dart'; -import '../mock/managers/cache_manager_mock.dart'; -import '../mock/managers/course_repository_mock.dart'; -import '../mock/managers/settings_manager_mock.dart'; -import '../mock/managers/user_repository_mock.dart'; -import '../mock/services/navigation_service_mock.dart'; -import '../mock/services/preferences_service_mock.dart'; -import '../mock/services/remote_config_service_mock.dart'; +import '../../common/helpers.dart'; +import '../app/analytics/mocks/remote_config_service_mock.dart'; +import '../app/navigation/mocks/navigation_service_mock.dart'; +import '../app/repository/mocks/course_repository_mock.dart'; +import '../app/repository/mocks/user_repository_mock.dart'; +import '../app/storage/mocks/cache_manager_mock.dart'; +import '../app/storage/mocks/preferences_service_mock.dart'; +import 'settings/mocks/settings_manager_mock.dart'; void main() { // Needed to support FlutterToast. @@ -115,6 +118,7 @@ void main() { group('MoreViewModel - ', () { setUp(() async { + setupAnalyticsServiceMock(); cacheManagerMock = setupCacheManagerMock(); settingsManagerMock = setupSettingsManagerMock(); courseRepositoryMock = setupCourseRepositoryMock(); @@ -137,12 +141,16 @@ void main() { }); tearDown(() { + unregister(); unregister(); unregister(); unregister(); + unregister(); unregister(); unregister(); unregister(); + unregister(); + unregister(); }); group('logout - ', () { diff --git a/test/ui/views/choose_language_view_test.dart b/test/features/more/settings/choose_language_view_test.dart similarity index 74% rename from test/ui/views/choose_language_view_test.dart rename to test/features/more/settings/choose_language_view_test.dart index 2f4711b41..640ccf627 100644 --- a/test/ui/views/choose_language_view_test.dart +++ b/test/features/more/settings/choose_language_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -12,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/more/settings/choose_language_view.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; void main() { late AppIntl intl; @@ -47,17 +44,5 @@ void main() { expect(listview, findsWidgets); }); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget(child: ChooseLanguageView())); - await tester.pumpAndSettle(); - - await expectLater(find.byType(ChooseLanguageView), - matchesGoldenFile(goldenFilePath("chooseLanguageView_1"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/choose_language_viewmodel_test.dart b/test/features/more/settings/choose_language_viewmodel_test.dart similarity index 94% rename from test/viewmodels/choose_language_viewmodel_test.dart rename to test/features/more/settings/choose_language_viewmodel_test.dart index 28ac30d52..b03f346c1 100644 --- a/test/viewmodels/choose_language_viewmodel_test.dart +++ b/test/features/more/settings/choose_language_viewmodel_test.dart @@ -9,9 +9,9 @@ import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/more/settings/choose_language_viewmodel.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../helpers.dart'; -import '../mock/managers/settings_manager_mock.dart'; -import '../mock/services/navigation_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/navigation/mocks/navigation_service_mock.dart'; +import 'mocks/settings_manager_mock.dart'; late ChooseLanguageViewModel viewModel; diff --git a/test/mock/managers/settings_manager_mock.dart b/test/features/more/settings/mocks/settings_manager_mock.dart similarity index 100% rename from test/mock/managers/settings_manager_mock.dart rename to test/features/more/settings/mocks/settings_manager_mock.dart diff --git a/test/managers/settings_manager_test.dart b/test/features/more/settings/settings_manager_test.dart similarity index 94% rename from test/managers/settings_manager_test.dart rename to test/features/more/settings/settings_manager_test.dart index 1dce2fd4f..727099232 100644 --- a/test/managers/settings_manager_test.dart +++ b/test/features/more/settings/settings_manager_test.dart @@ -13,10 +13,10 @@ import 'package:table_calendar/table_calendar.dart'; import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import '../helpers.dart'; -import '../mock/services/analytics_service_mock.dart'; -import '../mock/services/preferences_service_mock.dart'; -import '../mock/services/remote_config_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/analytics/mocks/analytics_service_mock.dart'; +import '../../app/analytics/mocks/remote_config_service_mock.dart'; +import '../../app/storage/mocks/preferences_service_mock.dart'; void main() { late AnalyticsServiceMock analyticsServiceMock; @@ -418,11 +418,10 @@ void main() { // Cards final Map expected = { - PreferencesFlag.broadcastCard: 0, - PreferencesFlag.aboutUsCard: 1, - PreferencesFlag.scheduleCard: 2, - PreferencesFlag.progressBarCard: 3, - PreferencesFlag.gradesCard: 4 + PreferencesFlag.aboutUsCard: 0, + PreferencesFlag.scheduleCard: 1, + PreferencesFlag.progressBarCard: 2, + PreferencesFlag.gradesCard: 3 }; expect( @@ -430,8 +429,6 @@ void main() { expected, ); - verify(preferencesServiceMock.getInt(PreferencesFlag.broadcastCard)) - .called(1); verify(preferencesServiceMock.getInt(PreferencesFlag.aboutUsCard)) .called(1); verify(preferencesServiceMock.getInt(PreferencesFlag.scheduleCard)) @@ -447,29 +444,24 @@ void main() { test("validate the loading of the cards", () async { PreferencesServiceMock.stubGetInt( - preferencesServiceMock, PreferencesFlag.broadcastCard, - toReturn: 0); - PreferencesServiceMock.stubGetInt( - preferencesServiceMock, PreferencesFlag.aboutUsCard, - toReturn: 2); + preferencesServiceMock, PreferencesFlag.aboutUsCard); PreferencesServiceMock.stubGetInt( preferencesServiceMock, PreferencesFlag.scheduleCard, - toReturn: 3); + toReturn: 2); PreferencesServiceMock.stubGetInt( preferencesServiceMock, PreferencesFlag.progressBarCard, // ignore: avoid_redundant_argument_values - toReturn: 1); + toReturn: 0); PreferencesServiceMock.stubGetInt( preferencesServiceMock, PreferencesFlag.gradesCard, - toReturn: 4); + toReturn: 3); // Cards final Map expected = { - PreferencesFlag.broadcastCard: 0, - PreferencesFlag.aboutUsCard: 2, - PreferencesFlag.scheduleCard: 3, - PreferencesFlag.progressBarCard: 1, - PreferencesFlag.gradesCard: 4 + PreferencesFlag.aboutUsCard: 1, + PreferencesFlag.scheduleCard: 2, + PreferencesFlag.progressBarCard: 0, + PreferencesFlag.gradesCard: 3 }; expect( @@ -477,8 +469,6 @@ void main() { expected, ); - verify(preferencesServiceMock.getInt(PreferencesFlag.broadcastCard)) - .called(1); verify(preferencesServiceMock.getInt(PreferencesFlag.aboutUsCard)) .called(1); verify(preferencesServiceMock.getInt(PreferencesFlag.scheduleCard)) diff --git a/test/ui/views/settings_view_test.dart b/test/features/more/settings/settings_view_test.dart similarity index 90% rename from test/ui/views/settings_view_test.dart rename to test/features/more/settings/settings_view_test.dart index b6ef8fdf9..7d823736f 100644 --- a/test/ui/views/settings_view_test.dart +++ b/test/features/more/settings/settings_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -12,8 +9,8 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/app/integration/networking_service.dart'; import 'package:notredame/features/more/settings/settings_view.dart'; -import '../../helpers.dart'; -import '../../mock/services/analytics_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/analytics/mocks/analytics_service_mock.dart'; void main() { late AppIntl intl; @@ -168,19 +165,6 @@ void main() { expect(find.text(intl.settings_english), findsOneWidget); }); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget( - localizedWidget(child: FeatureDiscovery(child: SettingsView()))); - await tester.pumpAndSettle(); - - await expectLater(find.byType(SettingsView), - matchesGoldenFile(goldenFilePath("settingsView_1"))); - }); - }, skip: !Platform.isLinux); }); }); } diff --git a/test/viewmodels/settings_viewmodel_test.dart b/test/features/more/settings/settings_viewmodel_test.dart similarity index 97% rename from test/viewmodels/settings_viewmodel_test.dart rename to test/features/more/settings/settings_viewmodel_test.dart index 8d0138a25..5c6ac256e 100644 --- a/test/viewmodels/settings_viewmodel_test.dart +++ b/test/features/more/settings/settings_viewmodel_test.dart @@ -10,8 +10,8 @@ import 'package:mockito/mockito.dart'; import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/more/settings/settings_viewmodel.dart'; -import '../helpers.dart'; -import '../mock/managers/settings_manager_mock.dart'; +import '../../../common/helpers.dart'; +import 'mocks/settings_manager_mock.dart'; late SettingsViewModel viewModel; diff --git a/test/ui/widgets/schedule_default_test.dart b/test/features/schedule/schedule_defaut/schedule_default_test.dart similarity index 54% rename from test/ui/widgets/schedule_default_test.dart rename to test/features/schedule/schedule_defaut/schedule_default_test.dart index eb41959b3..18571c3d9 100644 --- a/test/ui/widgets/schedule_default_test.dart +++ b/test/features/schedule/schedule_defaut/schedule_default_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/schedule/schedule_default/schedule_default.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; void main() { late AppIntl intl; @@ -16,10 +16,18 @@ void main() { testWidgets('Displays no schedule message when there are no events', (WidgetTester tester) async { - await tester.pumpWidget( - localizedWidget(child: const ScheduleDefault(calendarEvents: []))); + await tester.pumpWidget(localizedWidget( + child: const ScheduleDefault(calendarEvents: [], loaded: true))); await tester.pumpAndSettle(); expect(find.text(intl.no_schedule_available), findsOneWidget); }); + + testWidgets('Displays no empty schedule message when loading', + (WidgetTester tester) async { + await tester.pumpWidget(localizedWidget( + child: const ScheduleDefault(calendarEvents: [], loaded: false))); + await tester.pumpAndSettle(); + expect(find.text(intl.no_schedule_available), findsNothing); + }); }); } diff --git a/test/ui/views/schedule_default_view_test.dart b/test/features/schedule/schedule_defaut/schedule_default_view_test.dart similarity index 56% rename from test/ui/views/schedule_default_view_test.dart rename to test/features/schedule/schedule_defaut/schedule_default_view_test.dart index f077f61b0..943ecfac4 100644 --- a/test/ui/views/schedule_default_view_test.dart +++ b/test/features/schedule/schedule_defaut/schedule_default_view_test.dart @@ -1,9 +1,3 @@ -// Dart imports: -import 'dart:io'; - -// Flutter imports: -import 'package:flutter/material.dart'; - // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -15,8 +9,8 @@ import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; import 'package:notredame/features/schedule/schedule_default/schedule_default.dart'; import 'package:notredame/features/schedule/schedule_default/schedule_default_view.dart'; -import '../../helpers.dart'; -import '../../mock/managers/course_repository_mock.mocks.dart'; +import '../../../common/helpers.dart'; +import '../../app/repository/mocks/course_repository_mock.mocks.dart'; void main() { late CourseRepository mockCourseRepository; @@ -64,42 +58,5 @@ void main() { expect(fallSessionText, findsOneWidget); expect(find.byType(ScheduleDefault), findsOneWidget); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - when(mockCourseRepository.getDefaultScheduleActivities( - session: "valid_session")) - .thenAnswer((_) async => []); - await tester.pumpWidget(localizedWidget( - child: const ScheduleDefaultView(sessionCode: "A2024"), - useScaffold: false)); - await tester.pumpAndSettle(); - await tester.pump(const Duration(milliseconds: 500)); - - await expectLater(find.byType(ScheduleDefaultView), - matchesGoldenFile(goldenFilePath("scheduleDefaultView_1"))); - }); - - /// TODO: add when https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/pull/343 - /// is merged - // testWidgets("calendar view", (WidgetTester tester) async { - // tester.view.physicalSize = const Size(800, 1410); - // - // when(mockCourseRepository.getDefaultScheduleActivities( - // session: "H2024")) - // .thenAnswer((_) async => [lectureActivity]); - // - // await tester.pumpWidget(localizedWidget( - // child: const ScheduleDefaultView(sessionCode: "H2024"), - // useScaffold: false)); - // await tester.pumpAndSettle(); - // await tester.pump(const Duration(seconds: 2)); - // - // await expectLater(find.byType(ScheduleDefaultView), - // matchesGoldenFile(goldenFilePath("scheduleDefaultView_2"))); - // }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/schedule_default_viewmodel_test.dart b/test/features/schedule/schedule_defaut/schedule_default_viewmodel_test.dart similarity index 98% rename from test/viewmodels/schedule_default_viewmodel_test.dart rename to test/features/schedule/schedule_defaut/schedule_default_viewmodel_test.dart index 5b5c422da..010a1a4e4 100644 --- a/test/viewmodels/schedule_default_viewmodel_test.dart +++ b/test/features/schedule/schedule_defaut/schedule_default_viewmodel_test.dart @@ -9,7 +9,7 @@ import 'package:mockito/mockito.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; import 'package:notredame/features/schedule/schedule_default/schedule_default_viewmodel.dart'; -import '../helpers.dart'; +import '../../../common/helpers.dart'; void main() { late CourseRepository mockCourseRepository; diff --git a/test/viewmodels/schedule_settings_viewmodel_test.dart b/test/features/schedule/schedule_settings_viewmodel_test.dart similarity index 98% rename from test/viewmodels/schedule_settings_viewmodel_test.dart rename to test/features/schedule/schedule_settings_viewmodel_test.dart index 318baf65a..ffea3b7b6 100644 --- a/test/viewmodels/schedule_settings_viewmodel_test.dart +++ b/test/features/schedule/schedule_settings_viewmodel_test.dart @@ -10,9 +10,9 @@ import 'package:notredame/features/app/signets-api/models/schedule_activity.dart import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/schedule/schedule_settings_viewmodel.dart'; import 'package:notredame/utils/activity_code.dart'; -import '../helpers.dart'; -import '../mock/managers/course_repository_mock.dart'; -import '../mock/managers/settings_manager_mock.dart'; +import '../../common/helpers.dart'; +import '../app/repository/mocks/course_repository_mock.dart'; +import '../more/settings/mocks/settings_manager_mock.dart'; late SettingsManagerMock settingsManagerMock; late CourseRepositoryMock courseRepositoryMock; diff --git a/test/features/schedule/schedule_view_test.dart b/test/features/schedule/schedule_view_test.dart new file mode 100644 index 000000000..87bc8b9b5 --- /dev/null +++ b/test/features/schedule/schedule_view_test.dart @@ -0,0 +1,128 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:feature_discovery/feature_discovery.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:table_calendar/table_calendar.dart'; + +// Project imports: +import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/integration/networking_service.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; +import 'package:notredame/features/schedule/schedule_view.dart'; +import 'package:notredame/features/schedule/widgets/schedule_settings.dart'; +import '../../common/helpers.dart'; +import '../app/analytics/mocks/remote_config_service_mock.dart'; +import '../app/repository/mocks/course_repository_mock.dart'; +import '../more/settings/mocks/settings_manager_mock.dart'; + +void main() { + SharedPreferences.setMockInitialValues({}); + late SettingsManagerMock settingsManagerMock; + late CourseRepositoryMock courseRepositoryMock; + late RemoteConfigServiceMock remoteConfigServiceMock; + + // Some activities + late CourseActivity activityToday; + + // Some settings + Map settings = { + PreferencesFlag.scheduleCalendarFormat: CalendarFormat.week, + PreferencesFlag.scheduleStartWeekday: StartingDayOfWeek.monday, + PreferencesFlag.scheduleShowTodayBtn: true + }; + + group("ScheduleView - ", () { + setUpAll(() { + DateTime today = DateTime(2020); + today = today.subtract(Duration( + hours: today.hour, + minutes: today.minute, + seconds: today.second, + milliseconds: today.millisecond, + microseconds: today.microsecond)); + + activityToday = CourseActivity( + courseGroup: "GEN101", + courseName: "Generic course", + activityName: "TD", + activityDescription: "Activity description", + activityLocation: "location", + startDateTime: today, + endDateTime: today.add(const Duration(hours: 4))); + }); + + setUp(() async { + setupNavigationServiceMock(); + settingsManagerMock = setupSettingsManagerMock(); + courseRepositoryMock = setupCourseRepositoryMock(); + remoteConfigServiceMock = setupRemoteConfigServiceMock(); + setupNetworkingServiceMock(); + setupAnalyticsServiceMock(); + + SettingsManagerMock.stubLocale(settingsManagerMock); + + settings = { + PreferencesFlag.scheduleCalendarFormat: CalendarFormat.week, + PreferencesFlag.scheduleStartWeekday: StartingDayOfWeek.monday, + PreferencesFlag.scheduleShowTodayBtn: true, + PreferencesFlag.scheduleShowWeekEvents: false, + PreferencesFlag.scheduleListView: true, + }; + + CourseRepositoryMock.stubGetScheduleActivities(courseRepositoryMock); + RemoteConfigServiceMock.stubGetCalendarViewEnabled( + remoteConfigServiceMock); + }); + + tearDown(() => { + unregister(), + unregister(), + unregister(), + unregister(), + unregister(), + unregister(), + }); + group("interactions - ", () { + testWidgets("tap on settings button to open the schedule settings", + (WidgetTester tester) async { + tester.view.physicalSize = const Size(800, 1410); + + CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock, + toReturn: [activityToday]); + CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, + fromCacheOnly: true); + CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); + CourseRepositoryMock.stubGetCourses(courseRepositoryMock, + fromCacheOnly: true); + CourseRepositoryMock.stubGetCourses(courseRepositoryMock); + SettingsManagerMock.stubGetScheduleSettings(settingsManagerMock, + toReturn: settings); + + await tester.runAsync(() async { + await tester.pumpWidget(localizedWidget( + child: FeatureDiscovery(child: const ScheduleView()))); + await tester.pumpAndSettle(); + }).then((value) async { + expect(find.byType(ScheduleSettings), findsNothing, + reason: "The settings page should not be open"); + + // Tap on the settings button + await tester.tap(find.byIcon(Icons.settings_outlined)); + // Reload view + await tester.pumpAndSettle(); + + expect(find.byType(ScheduleSettings), findsOneWidget, + reason: "The settings view should be open"); + }); + }); + }); + }); +} diff --git a/test/viewmodels/schedule_viewmodel_test.dart b/test/features/schedule/schedule_viewmodel_test.dart similarity index 98% rename from test/viewmodels/schedule_viewmodel_test.dart rename to test/features/schedule/schedule_viewmodel_test.dart index 82575790f..c2c74c7b6 100644 --- a/test/viewmodels/schedule_viewmodel_test.dart +++ b/test/features/schedule/schedule_viewmodel_test.dart @@ -13,9 +13,9 @@ import 'package:notredame/features/app/signets-api/models/schedule_activity.dart import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/schedule/schedule_viewmodel.dart'; import 'package:notredame/utils/activity_code.dart'; -import '../helpers.dart'; -import '../mock/managers/course_repository_mock.dart'; -import '../mock/managers/settings_manager_mock.dart'; +import '../../common/helpers.dart'; +import '../app/repository/mocks/course_repository_mock.dart'; +import '../more/settings/mocks/settings_manager_mock.dart'; late CourseRepositoryMock courseRepositoryMock; late SettingsManagerMock settingsManagerMock; @@ -824,6 +824,18 @@ void main() { expect(res, true, reason: "Today was not selected before"); }); + test('today selected, but focused date different', () async { + final today = DateTime.now(); + final oldSelectedDate = DateTime(2022, 1, 2); + + viewModel.selectedDate = today; + viewModel.focusedDate.value = oldSelectedDate; + + final res = viewModel.selectToday(); + + expect(res, true, reason: "Today was not focused before"); + }); + test('show toast if today already selected', () async { final today = DateTime.now(); diff --git a/test/ui/widgets/schedule_settings_test.dart b/test/features/schedule/widgets/schedule_settings_test.dart similarity index 92% rename from test/ui/widgets/schedule_settings_test.dart rename to test/features/schedule/widgets/schedule_settings_test.dart index b1c9e20a3..4758c903c 100644 --- a/test/ui/widgets/schedule_settings_test.dart +++ b/test/features/schedule/widgets/schedule_settings_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -16,10 +13,10 @@ import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; import 'package:notredame/features/schedule/widgets/schedule_settings.dart'; import 'package:notredame/utils/activity_code.dart'; -import '../../helpers.dart'; -import '../../mock/managers/course_repository_mock.dart'; -import '../../mock/managers/settings_manager_mock.dart'; -import '../../mock/services/remote_config_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/analytics/mocks/remote_config_service_mock.dart'; +import '../../app/repository/mocks/course_repository_mock.dart'; +import '../../more/settings/mocks/settings_manager_mock.dart'; void main() { late SettingsManagerMock settingsManagerMock; @@ -450,36 +447,5 @@ void main() { }); }); }); - - group("golden - ", () { - testWidgets( - "Should display activity selection section when a course has activities", - (WidgetTester tester) async { - SettingsManagerMock.stubGetScheduleSettings(settingsManagerMock, - toReturn: settings); - CourseRepositoryMock.stubGetScheduleActivities(courseRepositoryMock, - toReturn: classOneWithLaboratoryABscheduleActivities); - - const scheduleSettings = ScheduleSettings(showHandle: false); - - await tester.pumpWidget(localizedWidget(child: scheduleSettings)); - await tester.pumpAndSettle(); - - final laboB = find.textContaining(intl.course_activity_group_b); - await tester.dragUntilVisible( - laboB, - find.byKey(const ValueKey("SettingsScrollingArea")), - const Offset(0, -250)); - expect(laboB, findsOneWidget); - - // generate a golden file - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpAndSettle(); - - await expectLater(find.byType(ScheduleSettings), - matchesGoldenFile(goldenFilePath("scheduleSettingsView_1"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_1.png b/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_1.png new file mode 100644 index 000000000..16b9206a5 Binary files /dev/null and b/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_1.png differ diff --git a/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_2.png b/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_2.png new file mode 100644 index 000000000..7e96ce9c8 Binary files /dev/null and b/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_2.png differ diff --git a/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_evaluation_not_completed.png b/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_evaluation_not_completed.png new file mode 100644 index 000000000..83df07b19 Binary files /dev/null and b/test/features/student/grades/grade_details/goldenFiles/gradesDetailsView_evaluation_not_completed.png differ diff --git a/test/ui/views/grades_details_view_test.dart b/test/features/student/grades/grade_details/grades_details_view_test.dart similarity index 67% rename from test/ui/views/grades_details_view_test.dart rename to test/features/student/grades/grade_details/grades_details_view_test.dart index b4c9ba203..097d12f0e 100644 --- a/test/ui/views/grades_details_view_test.dart +++ b/test/features/student/grades/grade_details/grades_details_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -18,8 +15,8 @@ import 'package:notredame/features/app/signets-api/models/course_review.dart'; import 'package:notredame/features/app/signets-api/models/course_summary.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/student/grades/grade_details/grade_details_view.dart'; -import '../../helpers.dart'; -import '../../mock/managers/course_repository_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/repository/mocks/course_repository_mock.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -74,22 +71,8 @@ void main() { isCompleted: false, ); - final altNonCompletedCourseReview = CourseReview( - acronym: 'GEN101', - group: '02', - teacherName: 'TEST ALT', - startAt: DateTime.now().subtract(const Duration(days: 1)), - endAt: DateTime.now().add(const Duration(days: 1)), - type: 'Cours', - isCompleted: false, - ); - final completedReviewList = [completedCourseReview]; final nonCompletedReviewList = [nonCompletedCourseReview]; - final partiallyCompletedReviewList = [ - completedCourseReview, - altNonCompletedCourseReview - ]; final Course course = Course( acronym: 'GEN101', @@ -120,16 +103,6 @@ void main() { summary: courseSummary, reviews: nonCompletedReviewList); - final Course courseWithPartialEvaluationCompleted = Course( - acronym: 'GEN101', - group: '02', - session: 'H2020', - programCode: '999', - numberOfCredits: 3, - title: 'Cours générique', - summary: courseSummary, - reviews: partiallyCompletedReviewList); - group('GradesDetailsView - ', () { setUp(() async { setupNavigationServiceMock(); @@ -168,7 +141,7 @@ void main() { findsOneWidget); } - expect(find.byType(Card), findsNWidgets(4)); + expect(find.byType(Card), findsNWidgets(5)); for (final eval in courseSummary.evaluations) { expect(find.byKey(Key("GradeEvaluationTile_${eval.title}")), @@ -258,78 +231,5 @@ void main() { expect(find.byKey(const Key("EvaluationNotCompleted")), findsOneWidget); }); }); - - group("golden - ", () { - testWidgets("default view", (WidgetTester tester) async { - setupFlutterToastMock(tester); - CourseRepositoryMock.stubGetCourseSummary( - courseRepositoryMock, courseWithoutSummary, - toReturn: course); - - tester.view.physicalSize = const Size(800, 1410); - await tester.runAsync(() async { - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: GradesDetailsView(course: courseWithoutSummary)))); - await tester.pumpAndSettle(); - }).then((value) async { - await expectLater(find.byType(GradesDetailsView), - matchesGoldenFile(goldenFilePath("gradesDetailsView_1"))); - }); - }); - - testWidgets("if there is no grades available", - (WidgetTester tester) async { - setupFlutterToastMock(tester); - CourseRepositoryMock.stubGetCourseSummary(courseRepositoryMock, course, - toReturn: courseWithoutSummary); - - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery(child: GradesDetailsView(course: course)))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater(find.byType(GradesDetailsView), - matchesGoldenFile(goldenFilePath("gradesDetailsView_2"))); - }); - - testWidgets("if in the evaluation period and evaluation not completed", - (WidgetTester tester) async { - setupFlutterToastMock(tester); - CourseRepositoryMock.stubGetCourseSummary(courseRepositoryMock, course, - toReturn: courseWithEvaluationNotCompleted); - - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery(child: GradesDetailsView(course: course)))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater( - find.byType(GradesDetailsView), - matchesGoldenFile( - goldenFilePath("gradesDetailsView_evaluation_not_completed"))); - }); - - testWidgets( - "if in the evaluation period and partially completed evaluation", - (WidgetTester tester) async { - setupFlutterToastMock(tester); - CourseRepositoryMock.stubGetCourseSummary(courseRepositoryMock, course, - toReturn: courseWithPartialEvaluationCompleted); - - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery(child: GradesDetailsView(course: course)))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater( - find.byType(GradesDetailsView), - matchesGoldenFile( - goldenFilePath("gradesDetailsView_evaluation_not_completed"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/grades_details_viewmodel_test.dart b/test/features/student/grades/grade_details/grades_details_viewmodel_test.dart similarity index 97% rename from test/viewmodels/grades_details_viewmodel_test.dart rename to test/features/student/grades/grade_details/grades_details_viewmodel_test.dart index 60d170dbc..f82f4f248 100644 --- a/test/viewmodels/grades_details_viewmodel_test.dart +++ b/test/features/student/grades/grade_details/grades_details_viewmodel_test.dart @@ -10,8 +10,8 @@ import 'package:notredame/features/app/signets-api/models/course_evaluation.dart import 'package:notredame/features/app/signets-api/models/course_summary.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/student/grades/grade_details/grades_details_viewmodel.dart'; -import '../helpers.dart'; -import '../mock/managers/course_repository_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/repository/mocks/course_repository_mock.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/test/ui/views/grades_view_test.dart b/test/features/student/grades/grades_view_test.dart similarity index 75% rename from test/ui/views/grades_view_test.dart rename to test/features/student/grades/grades_view_test.dart index e3846c36a..c1e71483b 100644 --- a/test/ui/views/grades_view_test.dart +++ b/test/features/student/grades/grades_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -18,8 +15,8 @@ import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/student/grades/grades_view.dart'; import 'package:notredame/features/student/grades/widgets/grade_button.dart'; -import '../../helpers.dart'; -import '../../mock/managers/course_repository_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/repository/mocks/course_repository_mock.dart'; void main() { SharedPreferences.setMockInitialValues({}); @@ -82,47 +79,6 @@ void main() { unregister(); }); - group("golden -", () { - testWidgets("No grades available", (WidgetTester tester) async { - // Mock the repository to have 0 courses available - CourseRepositoryMock.stubCourses(courseRepositoryMock); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock, - fromCacheOnly: true); - - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget( - localizedWidget(child: FeatureDiscovery(child: GradesView()))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater(find.byType(GradesView), - matchesGoldenFile(goldenFilePath("gradesView_1"))); - }); - - testWidgets("Multiples sessions and grades loaded", - (WidgetTester tester) async { - // Mock the repository to have 0 courses available - CourseRepositoryMock.stubCourses(courseRepositoryMock, - toReturn: courses); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock, - toReturn: courses, fromCacheOnly: true); - - tester.view.physicalSize = const Size(800, 1410); - await tester.runAsync(() async { - await tester.pumpWidget( - localizedWidget(child: FeatureDiscovery(child: GradesView()))); - await tester.pumpAndSettle(const Duration(seconds: 2)); - }).then( - (value) async { - await expectLater(find.byType(GradesView), - matchesGoldenFile(goldenFilePath("gradesView_2"))); - }, - ); - }); - }, skip: !Platform.isLinux); - group("UI -", () { testWidgets( "Right message is displayed when there is no grades available", diff --git a/test/viewmodels/grades_viewmodel_test.dart b/test/features/student/grades/grades_viewmodel_test.dart similarity index 98% rename from test/viewmodels/grades_viewmodel_test.dart rename to test/features/student/grades/grades_viewmodel_test.dart index 0f92800bd..3085027e6 100644 --- a/test/viewmodels/grades_viewmodel_test.dart +++ b/test/features/student/grades/grades_viewmodel_test.dart @@ -9,8 +9,8 @@ import 'package:notredame/features/app/repository/course_repository.dart'; import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/student/grades/grades_viewmodel.dart'; -import '../helpers.dart'; -import '../mock/managers/course_repository_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/repository/mocks/course_repository_mock.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/test/ui/widgets/grade_button_test.dart b/test/features/student/grades/widgets/grade_button_test.dart similarity index 96% rename from test/ui/widgets/grade_button_test.dart rename to test/features/student/grades/widgets/grade_button_test.dart index 6b0d27f56..85bae2e9f 100644 --- a/test/ui/widgets/grade_button_test.dart +++ b/test/features/student/grades/widgets/grade_button_test.dart @@ -11,9 +11,9 @@ import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:notredame/features/app/signets-api/models/course_summary.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/student/grades/widgets/grade_button.dart'; -import '../../helpers.dart'; -import '../../mock/managers/settings_manager_mock.dart'; -import '../../mock/services/navigation_service_mock.dart'; +import '../../../../common/helpers.dart'; +import '../../../app/navigation/mocks/navigation_service_mock.dart'; +import '../../../more/settings/mocks/settings_manager_mock.dart'; void main() { late AppIntl intl; diff --git a/test/ui/widgets/grade_circular_progress_test.dart b/test/features/student/grades/widgets/grade_circular_progress_test.dart similarity index 98% rename from test/ui/widgets/grade_circular_progress_test.dart rename to test/features/student/grades/widgets/grade_circular_progress_test.dart index 9768b63cc..af1df29b1 100644 --- a/test/ui/widgets/grade_circular_progress_test.dart +++ b/test/features/student/grades/widgets/grade_circular_progress_test.dart @@ -5,7 +5,7 @@ import 'package:percent_indicator/percent_indicator.dart'; // Project imports: import 'package:notredame/features/student/grades/widgets/grade_circular_progress.dart'; -import '../../helpers.dart'; +import '../../../../common/helpers.dart'; void main() { late AppIntl intl; diff --git a/test/ui/widgets/grade_evaluation_tile_test.dart b/test/features/student/grades/widgets/grade_evaluation_tile_test.dart similarity index 98% rename from test/ui/widgets/grade_evaluation_tile_test.dart rename to test/features/student/grades/widgets/grade_evaluation_tile_test.dart index 14b6760c7..2f9ea2cc3 100644 --- a/test/ui/widgets/grade_evaluation_tile_test.dart +++ b/test/features/student/grades/widgets/grade_evaluation_tile_test.dart @@ -13,7 +13,7 @@ import 'package:notredame/features/app/signets-api/models/course_evaluation.dart import 'package:notredame/features/app/signets-api/models/course_summary.dart'; import 'package:notredame/features/student/grades/widgets/grade_circular_progress.dart'; import 'package:notredame/features/student/grades/widgets/grade_evaluation_tile.dart'; -import '../../helpers.dart'; +import '../../../../common/helpers.dart'; void main() { late AppIntl intl; diff --git a/test/ui/widgets/grade_not_available_test.dart b/test/features/student/grades/widgets/grade_not_available_test.dart similarity index 95% rename from test/ui/widgets/grade_not_available_test.dart rename to test/features/student/grades/widgets/grade_not_available_test.dart index eba10fe6e..2aa92036d 100644 --- a/test/ui/widgets/grade_not_available_test.dart +++ b/test/features/student/grades/widgets/grade_not_available_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/student/grades/widgets/grade_not_available.dart'; -import '../../helpers.dart'; +import '../../../../common/helpers.dart'; void main() { late AppIntl intl; diff --git a/test/ui/views/profile_view_test.dart b/test/features/student/profile/profile_view_test.dart similarity index 84% rename from test/ui/views/profile_view_test.dart rename to test/features/student/profile/profile_view_test.dart index b14c8d52e..f7f1eb07e 100644 --- a/test/ui/views/profile_view_test.dart +++ b/test/features/student/profile/profile_view_test.dart @@ -1,9 +1,3 @@ -// Dart imports: -import 'dart:io'; - -// Flutter imports: -import 'package:flutter/services.dart'; - // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,9 +7,9 @@ import 'package:notredame/features/app/integration/networking_service.dart'; import 'package:notredame/features/app/signets-api/models/profile_student.dart'; import 'package:notredame/features/app/signets-api/models/program.dart'; import 'package:notredame/features/student/profile/profile_view.dart'; -import '../../helpers.dart'; -import '../../mock/managers/user_repository_mock.dart'; -import '../../mock/services/analytics_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/analytics/mocks/analytics_service_mock.dart'; +import '../../app/repository/mocks/user_repository_mock.dart'; void main() { late AppIntl intl; @@ -116,17 +110,5 @@ void main() { expect(find.text(intl.profile_program_completion), findsOneWidget); }); - - group("golden - ", () { - testWidgets("default view (no events)", (WidgetTester tester) async { - tester.view.physicalSize = const Size(1080, 1920); - - await tester.pumpWidget(localizedWidget(child: ProfileView())); - await tester.pumpAndSettle(); - - await expectLater(find.byType(ProfileView), - matchesGoldenFile(goldenFilePath("profileView_1"))); - }); - }, skip: !Platform.isLinux); }); } diff --git a/test/viewmodels/profile_viewmodel_test.dart b/test/features/student/profile/profile_viewmodel_test.dart similarity index 98% rename from test/viewmodels/profile_viewmodel_test.dart rename to test/features/student/profile/profile_viewmodel_test.dart index ccc93075d..c9b7eef44 100644 --- a/test/viewmodels/profile_viewmodel_test.dart +++ b/test/features/student/profile/profile_viewmodel_test.dart @@ -8,8 +8,8 @@ import 'package:notredame/features/app/signets-api/models/profile_student.dart'; import 'package:notredame/features/app/signets-api/models/program.dart'; import 'package:notredame/features/student/profile/profile_viewmodel.dart'; import 'package:notredame/features/student/profile/programs_credits.dart'; -import '../helpers.dart'; -import '../mock/managers/user_repository_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/repository/mocks/user_repository_mock.dart'; void main() { late UserRepositoryMock userRepositoryMock; diff --git a/test/ui/views/student_view_test.dart b/test/features/student/student_view_test.dart similarity index 71% rename from test/ui/views/student_view_test.dart rename to test/features/student/student_view_test.dart index 33b5fa5b0..58ff93f4d 100644 --- a/test/ui/views/student_view_test.dart +++ b/test/features/student/student_view_test.dart @@ -1,6 +1,3 @@ -// Dart imports: -import 'dart:io'; - // Flutter imports: import 'package:flutter/material.dart'; @@ -14,9 +11,9 @@ import 'package:notredame/features/app/repository/course_repository.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/student/student_view.dart'; -import '../../helpers.dart'; -import '../../mock/managers/course_repository_mock.dart'; -import '../../mock/services/analytics_service_mock.dart'; +import '../../common/helpers.dart'; +import '../app/analytics/mocks/analytics_service_mock.dart'; +import '../app/repository/mocks/course_repository_mock.dart'; void main() { CourseRepositoryMock courseRepositoryMock; @@ -55,19 +52,6 @@ void main() { expect(find.byType(BaseScaffold), findsOneWidget); }); - - group("golden - ", () { - testWidgets("default view (no events)", (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - await tester.pumpWidget( - localizedWidget(child: FeatureDiscovery(child: StudentView()))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - await expectLater(find.byType(StudentView), - matchesGoldenFile(goldenFilePath("studentView_1"))); - }); - }, skip: !Platform.isLinux); }); }); } diff --git a/test/ui/widgets/student_program_test.dart b/test/features/student/widgets/student_program_test.dart similarity index 98% rename from test/ui/widgets/student_program_test.dart rename to test/features/student/widgets/student_program_test.dart index f64173e9b..e84af62d6 100644 --- a/test/ui/widgets/student_program_test.dart +++ b/test/features/student/widgets/student_program_test.dart @@ -8,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/app/signets-api/models/program.dart'; import 'package:notredame/features/student/widgets/student_program.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; final _program = Program( name: 'Bac', diff --git a/test/ui/views/login_view_test.dart b/test/features/welcome/login/login_view_test.dart similarity index 98% rename from test/ui/views/login_view_test.dart rename to test/features/welcome/login/login_view_test.dart index f549c7c6a..bc9a4879f 100644 --- a/test/ui/views/login_view_test.dart +++ b/test/features/welcome/login/login_view_test.dart @@ -15,7 +15,7 @@ import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/welcome/login/login_view.dart'; import 'package:notredame/features/welcome/widgets/password_text_field.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; void main() { late AppIntl intl; diff --git a/test/viewmodels/login_viewmodel_test.dart b/test/features/welcome/login/login_viewmodel_test.dart similarity index 96% rename from test/viewmodels/login_viewmodel_test.dart rename to test/features/welcome/login/login_viewmodel_test.dart index dd25b67db..ee4a956de 100644 --- a/test/viewmodels/login_viewmodel_test.dart +++ b/test/features/welcome/login/login_viewmodel_test.dart @@ -8,9 +8,9 @@ import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/repository/user_repository.dart'; import 'package:notredame/features/welcome/login/login_viewmodel.dart'; -import '../helpers.dart'; -import '../mock/managers/user_repository_mock.dart'; -import '../mock/services/navigation_service_mock.dart'; +import '../../../common/helpers.dart'; +import '../../app/navigation/mocks/navigation_service_mock.dart'; +import '../../app/repository/mocks/user_repository_mock.dart'; void main() { // Needed to support FlutterToast. diff --git a/test/ui/widgets/password_text_field_test.dart b/test/features/welcome/widgets/password_text_field_test.dart similarity index 98% rename from test/ui/widgets/password_text_field_test.dart rename to test/features/welcome/widgets/password_text_field_test.dart index 5c98478b2..a2e081314 100644 --- a/test/ui/widgets/password_text_field_test.dart +++ b/test/features/welcome/widgets/password_text_field_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/features/welcome/widgets/password_text_field.dart'; -import '../../helpers.dart'; +import '../../../common/helpers.dart'; void main() { late AppIntl intl; diff --git a/test/ui/views/goldenFiles/FaqView_1.png b/test/ui/views/goldenFiles/FaqView_1.png deleted file mode 100644 index c5ac4dfac..000000000 Binary files a/test/ui/views/goldenFiles/FaqView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/aboutView_1.png b/test/ui/views/goldenFiles/aboutView_1.png deleted file mode 100644 index c9f24e65f..000000000 Binary files a/test/ui/views/goldenFiles/aboutView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/authorView_1.png b/test/ui/views/goldenFiles/authorView_1.png deleted file mode 100644 index 164e40b8a..000000000 Binary files a/test/ui/views/goldenFiles/authorView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/authorView_2.png b/test/ui/views/goldenFiles/authorView_2.png deleted file mode 100644 index e6c42a77d..000000000 Binary files a/test/ui/views/goldenFiles/authorView_2.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/chooseLanguageView_1.png b/test/ui/views/goldenFiles/chooseLanguageView_1.png deleted file mode 100644 index bd4528b4a..000000000 Binary files a/test/ui/views/goldenFiles/chooseLanguageView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/dashboardView_appletsCard_1.png b/test/ui/views/goldenFiles/dashboardView_appletsCard_1.png deleted file mode 100644 index 19a16be9d..000000000 Binary files a/test/ui/views/goldenFiles/dashboardView_appletsCard_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/dashboardView_progressBarCard_1.png b/test/ui/views/goldenFiles/dashboardView_progressBarCard_1.png deleted file mode 100644 index 7d9c7822b..000000000 Binary files a/test/ui/views/goldenFiles/dashboardView_progressBarCard_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/dashboardView_scheduleCard_1.png b/test/ui/views/goldenFiles/dashboardView_scheduleCard_1.png deleted file mode 100644 index dfea879e1..000000000 Binary files a/test/ui/views/goldenFiles/dashboardView_scheduleCard_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/etsView_1.png b/test/ui/views/goldenFiles/etsView_1.png deleted file mode 100644 index c6ed5130d..000000000 Binary files a/test/ui/views/goldenFiles/etsView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/feedbackView_1.png b/test/ui/views/goldenFiles/feedbackView_1.png deleted file mode 100644 index 6e0f298fc..000000000 Binary files a/test/ui/views/goldenFiles/feedbackView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/gradesDetailsView_1.png b/test/ui/views/goldenFiles/gradesDetailsView_1.png deleted file mode 100644 index ee554320b..000000000 Binary files a/test/ui/views/goldenFiles/gradesDetailsView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/gradesDetailsView_2.png b/test/ui/views/goldenFiles/gradesDetailsView_2.png deleted file mode 100644 index 6691db661..000000000 Binary files a/test/ui/views/goldenFiles/gradesDetailsView_2.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/gradesDetailsView_evaluation_not_completed.png b/test/ui/views/goldenFiles/gradesDetailsView_evaluation_not_completed.png deleted file mode 100644 index 282a20b53..000000000 Binary files a/test/ui/views/goldenFiles/gradesDetailsView_evaluation_not_completed.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/gradesView_1.png b/test/ui/views/goldenFiles/gradesView_1.png deleted file mode 100644 index dea4acfbc..000000000 Binary files a/test/ui/views/goldenFiles/gradesView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/gradesView_2.png b/test/ui/views/goldenFiles/gradesView_2.png deleted file mode 100644 index 5eebfa243..000000000 Binary files a/test/ui/views/goldenFiles/gradesView_2.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/moreView_1.png b/test/ui/views/goldenFiles/moreView_1.png deleted file mode 100644 index c50fb0e29..000000000 Binary files a/test/ui/views/goldenFiles/moreView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/newsDetailsView_1.png b/test/ui/views/goldenFiles/newsDetailsView_1.png deleted file mode 100644 index ed356455a..000000000 Binary files a/test/ui/views/goldenFiles/newsDetailsView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/newsView_1.png b/test/ui/views/goldenFiles/newsView_1.png deleted file mode 100644 index 7c61212fc..000000000 Binary files a/test/ui/views/goldenFiles/newsView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/newsView_2.png b/test/ui/views/goldenFiles/newsView_2.png deleted file mode 100644 index 3c3ff4312..000000000 Binary files a/test/ui/views/goldenFiles/newsView_2.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/notFoundView_1.png b/test/ui/views/goldenFiles/notFoundView_1.png deleted file mode 100644 index 2cec40431..000000000 Binary files a/test/ui/views/goldenFiles/notFoundView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/profileView_1.png b/test/ui/views/goldenFiles/profileView_1.png deleted file mode 100644 index 559dd6f87..000000000 Binary files a/test/ui/views/goldenFiles/profileView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/quicksLinksView_1.png b/test/ui/views/goldenFiles/quicksLinksView_1.png deleted file mode 100644 index 75283b5dd..000000000 Binary files a/test/ui/views/goldenFiles/quicksLinksView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/scheduleDefaultView_1.png b/test/ui/views/goldenFiles/scheduleDefaultView_1.png deleted file mode 100644 index 60f3ce4d0..000000000 Binary files a/test/ui/views/goldenFiles/scheduleDefaultView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/scheduleDefaultView_2.png b/test/ui/views/goldenFiles/scheduleDefaultView_2.png deleted file mode 100644 index fd313cde0..000000000 Binary files a/test/ui/views/goldenFiles/scheduleDefaultView_2.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/scheduleView_1.png b/test/ui/views/goldenFiles/scheduleView_1.png deleted file mode 100644 index 399295dcb..000000000 Binary files a/test/ui/views/goldenFiles/scheduleView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/scheduleView_2.png b/test/ui/views/goldenFiles/scheduleView_2.png deleted file mode 100644 index 0a0163f2b..000000000 Binary files a/test/ui/views/goldenFiles/scheduleView_2.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/scheduleView_3.png b/test/ui/views/goldenFiles/scheduleView_3.png deleted file mode 100644 index e401dfa08..000000000 Binary files a/test/ui/views/goldenFiles/scheduleView_3.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/scheduleView_4.png b/test/ui/views/goldenFiles/scheduleView_4.png deleted file mode 100644 index dd582dd49..000000000 Binary files a/test/ui/views/goldenFiles/scheduleView_4.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/scheduleView_5.png b/test/ui/views/goldenFiles/scheduleView_5.png deleted file mode 100644 index 6cd80d11e..000000000 Binary files a/test/ui/views/goldenFiles/scheduleView_5.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/settingsView_1.png b/test/ui/views/goldenFiles/settingsView_1.png deleted file mode 100644 index 95446509d..000000000 Binary files a/test/ui/views/goldenFiles/settingsView_1.png and /dev/null differ diff --git a/test/ui/views/goldenFiles/studentView_1.png b/test/ui/views/goldenFiles/studentView_1.png deleted file mode 100644 index 6209ded69..000000000 Binary files a/test/ui/views/goldenFiles/studentView_1.png and /dev/null differ diff --git a/test/ui/views/schedule_view_test.dart b/test/ui/views/schedule_view_test.dart deleted file mode 100644 index 0fe4d62b1..000000000 --- a/test/ui/views/schedule_view_test.dart +++ /dev/null @@ -1,308 +0,0 @@ -// Dart imports: -import 'dart:io'; - -// Flutter imports: -import 'package:flutter/material.dart'; - -// Package imports: -import 'package:feature_discovery/feature_discovery.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:table_calendar/table_calendar.dart'; - -// Project imports: -import 'package:notredame/constants/preferences_flags.dart'; -import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/features/app/integration/networking_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/features/app/repository/course_repository.dart'; -import 'package:notredame/features/app/signets-api/models/course_activity.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/features/schedule/schedule_view.dart'; -import 'package:notredame/features/schedule/widgets/schedule_settings.dart'; -import '../../helpers.dart'; -import '../../mock/managers/course_repository_mock.dart'; -import '../../mock/managers/settings_manager_mock.dart'; -import '../../mock/services/remote_config_service_mock.dart'; - -void main() { - SharedPreferences.setMockInitialValues({}); - late SettingsManagerMock settingsManagerMock; - late CourseRepositoryMock courseRepositoryMock; - late RemoteConfigServiceMock remoteConfigServiceMock; - - // Some activities - late CourseActivity activityYesterday; - late CourseActivity activityToday; - late CourseActivity activityTomorrow; - - // Some settings - Map settings = { - PreferencesFlag.scheduleCalendarFormat: CalendarFormat.week, - PreferencesFlag.scheduleStartWeekday: StartingDayOfWeek.monday, - PreferencesFlag.scheduleShowTodayBtn: true - }; - - group("ScheduleView - ", () { - setUpAll(() { - DateTime today = DateTime(2020); - today = today.subtract(Duration( - hours: today.hour, - minutes: today.minute, - seconds: today.second, - milliseconds: today.millisecond, - microseconds: today.microsecond)); - final DateTime yesterday = today.subtract(const Duration(days: 1)); - final DateTime tomorrow = today.add(const Duration(days: 1)); - - activityYesterday = CourseActivity( - courseGroup: "GEN102", - courseName: "Generic course", - activityName: "TD", - activityDescription: "Activity description", - activityLocation: "location", - startDateTime: tomorrow, - endDateTime: tomorrow.add(const Duration(hours: 4))); - activityToday = CourseActivity( - courseGroup: "GEN101", - courseName: "Generic course", - activityName: "TD", - activityDescription: "Activity description", - activityLocation: "location", - startDateTime: today, - endDateTime: today.add(const Duration(hours: 4))); - activityTomorrow = CourseActivity( - courseGroup: "GEN103", - courseName: "Generic course", - activityName: "TD", - activityDescription: "Activity description", - activityLocation: "location", - startDateTime: yesterday, - endDateTime: yesterday.add(const Duration(hours: 4))); - }); - - setUp(() async { - setupNavigationServiceMock(); - settingsManagerMock = setupSettingsManagerMock(); - courseRepositoryMock = setupCourseRepositoryMock(); - remoteConfigServiceMock = setupRemoteConfigServiceMock(); - setupNetworkingServiceMock(); - setupAnalyticsServiceMock(); - - SettingsManagerMock.stubLocale(settingsManagerMock); - - settings = { - PreferencesFlag.scheduleCalendarFormat: CalendarFormat.week, - PreferencesFlag.scheduleStartWeekday: StartingDayOfWeek.monday, - PreferencesFlag.scheduleShowTodayBtn: true, - PreferencesFlag.scheduleShowWeekEvents: false, - PreferencesFlag.scheduleListView: true, - }; - - CourseRepositoryMock.stubGetScheduleActivities(courseRepositoryMock); - RemoteConfigServiceMock.stubGetCalendarViewEnabled( - remoteConfigServiceMock); - }); - - tearDown(() => { - unregister(), - unregister(), - unregister(), - unregister(), - unregister(), - unregister(), - }); - - group("golden - ", () { - const tableCalendarKey = Key("TableCalendar"); - testWidgets("default view (no events), showTodayButton enabled", - (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - SettingsManagerMock.stubGetScheduleSettings(settingsManagerMock, - toReturn: settings); - await tester.runAsync(() async { - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: ScheduleView(initialDay: DateTime(2020))))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - }).then((value) async { - await expectLater(find.byType(ScheduleView), - matchesGoldenFile(goldenFilePath("scheduleView_1"))); - }); - }); - - testWidgets("default view (no events), showTodayButton disabled", - (WidgetTester tester) async { - SettingsManagerMock.stubGetBool( - settingsManagerMock, PreferencesFlag.discoverySchedule); - tester.view.physicalSize = const Size(800, 1410); - - settings[PreferencesFlag.scheduleShowTodayBtn] = false; - - CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - SettingsManagerMock.stubGetScheduleSettings(settingsManagerMock, - toReturn: settings); - - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: ScheduleView(initialDay: DateTime(2020))))); - await tester.pumpAndSettle(); - - await expectLater(find.byType(ScheduleView), - matchesGoldenFile(goldenFilePath("scheduleView_2"))); - }); - - testWidgets("view with events, day with events selected", - (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock, - toReturn: [activityYesterday, activityToday, activityTomorrow]); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - SettingsManagerMock.stubGetScheduleSettings(settingsManagerMock, - toReturn: settings); - await tester.runAsync(() async { - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: MediaQuery( - data: const MediaQueryData( - textScaler: TextScaler.linear(0.5)), - child: ScheduleView(initialDay: DateTime(2020)))))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - }).then((value) async { - await expectLater(find.byType(ScheduleView), - matchesGoldenFile(goldenFilePath("scheduleView_3"))); - }); - }); - - testWidgets("view with events, day without events selected", - (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock, - toReturn: [activityYesterday, activityTomorrow]); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - SettingsManagerMock.stubGetScheduleSettings(settingsManagerMock, - toReturn: settings); - await tester.runAsync(() async { - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: ScheduleView(initialDay: DateTime(2020))))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - }).then((value) async { - await expectLater(find.byType(ScheduleView), - matchesGoldenFile(goldenFilePath("scheduleView_4"))); - }); - }); - - testWidgets("other day is selected, current day still has a square.", - (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock, - toReturn: [activityYesterday, activityTomorrow]); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - SettingsManagerMock.stubGetScheduleSettings(settingsManagerMock, - toReturn: settings); - - final testingDate = DateTime(2020); - await tester.runAsync(() async { - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery( - child: MediaQuery( - data: const MediaQueryData( - textScaler: TextScaler.linear(0.5)), - child: ScheduleView(initialDay: testingDate))))); - await tester.pumpAndSettle(const Duration(seconds: 1)); - }).then((value) async { - expect(find.byKey(tableCalendarKey, skipOffstage: false), - findsOneWidget); - expect( - find.descendant( - of: find.byKey(tableCalendarKey, skipOffstage: false), - matching: find.text( - "${testingDate.add(const Duration(days: 1)).day}", - skipOffstage: false)), - findsOneWidget); - - // Tap on the day after selected day - await tester.tap(find.descendant( - of: find.byKey(tableCalendarKey, skipOffstage: false), - matching: find.text( - "${testingDate.add(const Duration(days: 1)).day}", - skipOffstage: false))); - - // Reload the view - await tester.pump(); - - await expectLater(find.byType(ScheduleView), - matchesGoldenFile(goldenFilePath("scheduleView_5"))); - }); - }); - }, skip: !Platform.isLinux); - - group("interactions - ", () { - testWidgets("tap on settings button to open the schedule settings", - (WidgetTester tester) async { - tester.view.physicalSize = const Size(800, 1410); - - CourseRepositoryMock.stubCoursesActivities(courseRepositoryMock, - toReturn: [activityToday]); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCoursesActivities(courseRepositoryMock); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock, - fromCacheOnly: true); - CourseRepositoryMock.stubGetCourses(courseRepositoryMock); - SettingsManagerMock.stubGetScheduleSettings(settingsManagerMock, - toReturn: settings); - - await tester.runAsync(() async { - await tester.pumpWidget(localizedWidget( - child: FeatureDiscovery(child: const ScheduleView()))); - await tester.pumpAndSettle(); - }).then((value) async { - expect(find.byType(ScheduleSettings), findsNothing, - reason: "The settings page should not be open"); - - // Tap on the settings button - await tester.tap(find.byIcon(Icons.settings_outlined)); - // Reload view - await tester.pumpAndSettle(); - - expect(find.byType(ScheduleSettings), findsOneWidget, - reason: "The settings view should be open"); - }); - }); - }); - }); -} diff --git a/test/ui/widgets/goldenFiles/scheduleSettingsView_1.png b/test/ui/widgets/goldenFiles/scheduleSettingsView_1.png deleted file mode 100644 index dc435de07..000000000 Binary files a/test/ui/widgets/goldenFiles/scheduleSettingsView_1.png and /dev/null differ