├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── run_run_run │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── run.png │ │ │ ├── mipmap-ldpi │ │ │ └── run.png │ │ │ ├── mipmap-mdpi │ │ │ └── run.png │ │ │ ├── mipmap-xhdpi │ │ │ └── run.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── run.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── run.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── banner.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── core │ ├── debouncer.dart │ ├── error.dart │ └── utils │ │ └── storage_utils.dart ├── data │ ├── api │ │ ├── activity_api.dart │ │ ├── friend_request_api.dart │ │ ├── helpers │ │ │ └── api_helper.dart │ │ └── user_api.dart │ ├── model │ │ ├── request │ │ │ ├── activity_request.dart │ │ │ ├── edit_password_request.dart │ │ │ ├── edit_profile_request.dart │ │ │ ├── location_request.dart │ │ │ ├── login_request.dart │ │ │ ├── registration_request.dart │ │ │ └── send_new_password_request.dart │ │ └── response │ │ │ ├── activity_comment_response.dart │ │ │ ├── activity_response.dart │ │ │ ├── friend_request_response.dart │ │ │ ├── location_response.dart │ │ │ ├── login_response.dart │ │ │ ├── page_response.dart │ │ │ └── user_response.dart │ └── repositories │ │ ├── activity_repository_impl.dart │ │ ├── friend_request_repository_impl.dart │ │ └── user_repository_impl.dart ├── domain │ ├── entities │ │ ├── activity.dart │ │ ├── activity_comment.dart │ │ ├── enum │ │ │ ├── activity_type.dart │ │ │ └── friend_request_status.dart │ │ ├── friend_request.dart │ │ ├── location.dart │ │ ├── page.dart │ │ └── user.dart │ └── repositories │ │ ├── activity_repository.dart │ │ ├── friend_request_repository.dart │ │ └── user_repository.dart ├── l10n.yaml ├── l10n │ ├── app_ar.arb │ ├── app_bn.arb │ ├── app_de.arb │ ├── app_en.arb │ ├── app_en_US.arb │ ├── app_es.arb │ ├── app_fr.arb │ ├── app_fr_FR.arb │ ├── app_hi.arb │ ├── app_it.arb │ ├── app_pt.arb │ ├── app_ru.arb │ ├── app_ur.arb │ ├── app_zh.arb │ └── support_locale.dart ├── main.dart └── presentation │ ├── common │ ├── activity │ │ ├── view_model │ │ │ ├── activity_item_comments_view_model.dart │ │ │ ├── activity_item_interaction_view_model.dart │ │ │ ├── activity_item_like_view_model.dart │ │ │ ├── activity_item_view_model.dart │ │ │ ├── activity_list_view_model.dart │ │ │ └── state │ │ │ │ ├── activity_item_comments_state.dart │ │ │ │ ├── activity_item_interaction_state.dart │ │ │ │ ├── activity_item_like_state.dart │ │ │ │ ├── activity_item_state.dart │ │ │ │ └── activity_list_state.dart │ │ └── widgets │ │ │ ├── activity_comments.dart │ │ │ ├── activity_item.dart │ │ │ ├── activity_item_details.dart │ │ │ ├── activity_item_interaction.dart │ │ │ ├── activity_item_user_informations.dart │ │ │ ├── activity_list.dart │ │ │ └── activty_like.dart │ ├── core │ │ ├── enums │ │ │ └── infinite_scroll_list.enum.dart │ │ ├── services │ │ │ └── text_to_speech_service.dart │ │ ├── utils │ │ │ ├── activity_utils.dart │ │ │ ├── color_utils.dart │ │ │ ├── form_utils.dart │ │ │ ├── image_utils.dart │ │ │ ├── map_utils.dart │ │ │ ├── share_utils.dart │ │ │ ├── type_utils.dart │ │ │ ├── ui_utils.dart │ │ │ └── user_utils.dart │ │ ├── validators │ │ │ └── login_validators.dart │ │ └── widgets │ │ │ ├── date.dart │ │ │ ├── infinite_scroll_list.dart │ │ │ ├── share_map_button.dart │ │ │ ├── upload_file.dart │ │ │ └── view_model │ │ │ ├── infinite_scroll_list_view_model.dart │ │ │ └── state │ │ │ └── infinite_scroll_list_state.dart │ ├── friendship │ │ └── widgets │ │ │ └── accept_refuse.dart │ ├── location │ │ ├── view_model │ │ │ ├── location_view_model.dart │ │ │ └── state │ │ │ │ └── location_state.dart │ │ └── widgets │ │ │ ├── current_location_map.dart │ │ │ └── location_map.dart │ ├── metrics │ │ ├── view_model │ │ │ ├── metrics_view_model.dart │ │ │ └── state │ │ │ │ └── metrics_state.dart │ │ └── widgets │ │ │ └── metrics.dart │ ├── timer │ │ ├── viewmodel │ │ │ ├── state │ │ │ │ └── timer_state.dart │ │ │ └── timer_view_model.dart │ │ └── widgets │ │ │ ├── timer_pause.dart │ │ │ ├── timer_sized.dart │ │ │ ├── timer_start.dart │ │ │ └── timer_text.dart │ └── user │ │ ├── screens │ │ └── profile_screen.dart │ │ ├── view_model │ │ ├── profile_picture_view_model.dart │ │ ├── profile_view_model.dart │ │ └── state │ │ │ ├── profile_picture_state.dart │ │ │ └── profile_state.dart │ │ └── widgets │ │ └── friend_request.dart │ ├── community │ ├── screens │ │ ├── community_screen.dart │ │ └── pending_requests_screen.dart │ ├── view_model │ │ ├── community_view_model.dart │ │ ├── pending_request_view_model.dart │ │ └── state │ │ │ ├── community_state.dart │ │ │ └── pending_requests_state.dart │ └── widgets │ │ ├── pending_request_list.dart │ │ └── search_widget.dart │ ├── home │ ├── screens │ │ └── home_screen.dart │ └── view_model │ │ ├── home_view_model.dart │ │ └── state │ │ └── home_state.dart │ ├── login │ ├── screens │ │ └── login_screen.dart │ └── view_model │ │ ├── login_view_model.dart │ │ └── state │ │ └── login_state.dart │ ├── my_activities │ ├── screens │ │ ├── activity_details_screen.dart │ │ └── activity_list_screen.dart │ ├── view_model │ │ ├── activity_details_view_model.dart │ │ ├── activity_list_view_model.dart │ │ └── state │ │ │ ├── activitie_details_state.dart │ │ │ └── activity_list_state.dart │ └── widgets │ │ ├── back_to_home_button.dart │ │ ├── details_tab.dart │ │ └── graph_tab.dart │ ├── new_activity │ ├── screens │ │ ├── new_activity_screen.dart │ │ └── sum_up_screen.dart │ ├── view_model │ │ ├── state │ │ │ └── sum_up_state.dart │ │ └── sum_up_view_model.dart │ └── widgets │ │ └── save_button.dart │ ├── registration │ ├── screens │ │ └── registration_screen.dart │ └── view_model │ │ ├── registration_view_model.dart │ │ └── state │ │ └── registration_state.dart │ ├── send_new_password │ ├── screens │ │ └── send_new_password_screen.dart │ └── view_model │ │ ├── send_new_password_view_model.dart │ │ └── state │ │ └── send_new_password_state.dart │ └── settings │ ├── screens │ ├── edit_password_screen.dart │ ├── edit_profile_screen.dart │ └── settings_screen.dart │ └── view_model │ ├── edit_password_view_model.dart │ ├── edit_profile_view_model.dart │ ├── settings_view_model.dart │ └── state │ ├── edit_password_state.dart │ ├── edit_profile_state.dart │ └── settings_state.dart ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── activity_list │ ├── activity_details.png │ ├── activity_graph.png │ └── activity_list.png ├── community │ ├── all_activities.png │ ├── pending_requests.png │ └── user_profile.png ├── login_registration │ ├── login.png │ └── registration.png ├── new_activity │ ├── current_activity.png │ └── home.png └── settings │ ├── edit_profile.png │ └── settings.png └── test └── presentation └── core └── utils ├── activity_utils_test.dart ├── color_utils_test.dart ├── form_utils_test.dart ├── map_utils_test.dart └── ui_utils_test.dart /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build mobile apps & check tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build-android: 11 | name: Build android 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-java@v2 17 | with: 18 | distribution: 'zulu' 19 | java-version: '17' 20 | - uses: subosito/flutter-action@v2 21 | with: 22 | flutter-version: '3.24.0' 23 | - name: Clean Gradle cache 24 | run: rm -rf ~/.gradle/caches/ 25 | - name: Flutter clean 26 | run: flutter clean 27 | - name: Install dependencies 28 | run: flutter pub get 29 | - name: Generate l10n 30 | run: flutter gen-l10n 31 | - name: Build apk 32 | run: flutter build apk 33 | - name: Build appbundle 34 | run: flutter build appbundle 35 | 36 | build-ios: 37 | name: Build ios 38 | runs-on: macos-latest 39 | 40 | steps: 41 | - uses: actions/checkout@v3 42 | - uses: subosito/flutter-action@v2 43 | with: 44 | channel: 'stable' 45 | architecture: x64 46 | - name: Install dependencies 47 | run: flutter pub get 48 | - name: Generate l10n 49 | run: flutter gen-l10n 50 | - name: Build ios 51 | run: flutter build ios --release --no-codesign 52 | 53 | check-tests: 54 | name: Check Tests 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - uses: actions/checkout@v3 59 | - uses: subosito/flutter-action@v2 60 | with: 61 | flutter-version: '3.24.0' 62 | channel: 'stable' 63 | - name: Flutter doctor 64 | run: flutter doctor 65 | - name: Get dependencies 66 | run: flutter pub get 67 | - name: Run l10n generation 68 | run: flutter gen-l10n 69 | - name: Run tests 70 | run: flutter test 71 | 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Flutter APK 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Flutter 16 | uses: subosito/flutter-action@v2 17 | with: 18 | flutter-version: '3.24.0' 19 | 20 | - name: Install dependencies 21 | run: flutter pub get 22 | - name: Generate l10n 23 | run: flutter gen-l10n 24 | - name: Build APK 25 | run: flutter build apk --release 26 | 27 | - name: Upload APK 28 | uses: actions/upload-artifact@v2 29 | with: 30 | name: run-flutter-run.apk 31 | path: build/app/outputs/flutter-apk/app-release.apk 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 17 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 18 | - platform: android 19 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 20 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 21 | - platform: ios 22 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 23 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 24 | - platform: linux 25 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 26 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 27 | - platform: macos 28 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 29 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 30 | - platform: web 31 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 32 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 33 | - platform: windows 34 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 35 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Benjamin Canape 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Run Flutter Run 4 | 5 | A clone of the Runtastic or Strava mobile app 6 | 7 | ## Fork it, add new functionnalities, please don't hesitate 8 | 9 | If you want to play with the project or add new functionnalities, you can fork this current repository and the backend one (https://github.com/BenjaminCanape/RunBackEndRun) and do what you want with it, and if it's awesome maybe propose it in a Pull request here. 10 | 11 | I would be happy to see all your ideas and improvments. 12 | 13 | And if the code helped you and you want to thank me for this, you can buy me a coffee:
14 | 15 | Buy Me A Coffee 16 | 17 | ## Functionalities 18 | 19 | Record your running, walking, and cycling sessions with session tracking through voice synthesis and a map updated with real-time location. 20 | 21 | Users can also view their previous sessions and follow other users to see, like and comment their activities. 22 | 23 | ## Getting Started 24 | 25 | ### Prerequisites 26 | 27 | You need flutter (The best is to use the version 3.22 and above) 28 | 29 | ### How to run 30 | 31 | ``` 32 | flutter pub get 33 | flutter gen-l10n 34 | flutter run 35 | ``` 36 | 37 | ## Some screenshots of the application 38 | 39 | ### New Activity 40 | 41 | Description de l'image Description de l'image 42 | 43 | ### Activity list 44 | 45 | Description de l'image Description de l'image Description de l'image 46 | 47 | ### Community 48 | 49 | Description de l'image Description de l'image Description de l'image 50 | 51 | ### Settings 52 | 53 | Description de l'image Description de l'image 54 | 55 | ### Login and registration 56 | 57 | Description de l'image Description de l'image 58 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.run_flutter_run" 27 | compileSdkVersion 34 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_17 32 | targetCompatibility JavaVersion.VERSION_17 33 | } 34 | 35 | kotlin { 36 | jvmToolchain(17) 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.example.run_flutter_run" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion 21 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 11 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 38 | 41 | 42 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/run_run_run/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.run_flutter_run 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/android/app/src/main/res/mipmap-hdpi/run.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/android/app/src/main/res/mipmap-ldpi/run.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/android/app/src/main/res/mipmap-mdpi/run.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/android/app/src/main/res/mipmap-xhdpi/run.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/android/app/src/main/res/mipmap-xxhdpi/run.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/android/app/src/main/res/mipmap-xxxhdpi/run.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | 20 | configurations.all { 21 | resolutionStrategy { 22 | force 'androidx.core:core:2.0.10' 23 | force 'androidx.core:core-ktx:2.0.10' 24 | } 25 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlin.jvm.target.validation.mode = IGNORE -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "7.3.0" apply false 23 | id "org.jetbrains.kotlin.android" version "2.0.10" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/banner.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Run Run Run 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | run_flutter_run 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | NSLocationWhenInUseUsageDescription 51 | This app needs access to location when open. 52 | NSPhotoLibraryUsageDescription 53 | Allow this app to access your photos 54 | 55 | 56 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/debouncer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | class Debouncer { 5 | final int milliseconds; 6 | VoidCallback? action; 7 | Timer? _timer; 8 | 9 | Debouncer({required this.milliseconds}); 10 | 11 | run(VoidCallback action) { 12 | if (_timer != null) { 13 | _timer!.cancel(); 14 | } 15 | _timer = Timer(Duration(milliseconds: milliseconds), action); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/core/error.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a failure object that can be used for error handling and reporting. 4 | class Failure extends Equatable { 5 | /// The error message associated with the failure. 6 | final String message; 7 | 8 | /// Constructs a Failure object with the given [message]. 9 | const Failure({required this.message}); 10 | 11 | @override 12 | List get props => [message]; 13 | 14 | @override 15 | bool get stringify => true; 16 | } 17 | -------------------------------------------------------------------------------- /lib/data/model/request/activity_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../../domain/entities/enum/activity_type.dart'; 4 | import 'location_request.dart'; 5 | 6 | /// Represents a request object for creating or updating an activity. 7 | class ActivityRequest extends Equatable { 8 | /// The id of the activity. 9 | final String? id; 10 | 11 | /// The type of the activity. 12 | final ActivityType type; 13 | 14 | /// The start datetime of the activity. 15 | final DateTime startDatetime; 16 | 17 | /// The end datetime of the activity. 18 | final DateTime endDatetime; 19 | 20 | /// The distance of the activity. 21 | final double distance; 22 | 23 | /// The list of locations associated with the activity. 24 | final List locations; 25 | 26 | /// Constructs an ActivityRequest object with the given parameters. 27 | const ActivityRequest({ 28 | this.id, 29 | required this.type, 30 | required this.startDatetime, 31 | required this.endDatetime, 32 | required this.distance, 33 | required this.locations, 34 | }); 35 | 36 | @override 37 | List get props => 38 | [id, type, startDatetime, endDatetime, distance, locations]; 39 | 40 | /// Converts the ActivityRequest object to a JSON map. 41 | Map toMap() { 42 | return { 43 | 'id': id, 44 | 'type': type.toString().split('.').last.toUpperCase(), 45 | 'startDatetime': startDatetime.toIso8601String(), 46 | 'endDatetime': endDatetime.toIso8601String(), 47 | 'distance': distance, 48 | 'locations': locations.map((location) => location.toMap()).toList(), 49 | }; 50 | } 51 | 52 | @override 53 | bool get stringify => true; 54 | } 55 | -------------------------------------------------------------------------------- /lib/data/model/request/edit_password_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a edit password request object. 4 | class EditPasswordRequest extends Equatable { 5 | /// The currentPassword for the request. 6 | final String currentPassword; 7 | 8 | /// The password for the request. 9 | final String password; 10 | 11 | /// Constructs a EditPasswordRequest object with the given parameters. 12 | const EditPasswordRequest({ 13 | required this.currentPassword, 14 | required this.password, 15 | }); 16 | 17 | @override 18 | List get props => [currentPassword, password]; 19 | 20 | /// Converts the EditPasswordRequest object to a JSON map. 21 | Map toMap() { 22 | return { 23 | 'currentPassword': currentPassword, 24 | 'password': password, 25 | }; 26 | } 27 | 28 | @override 29 | bool get stringify => true; 30 | } 31 | -------------------------------------------------------------------------------- /lib/data/model/request/edit_profile_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a edit profile request object. 4 | class EditProfileRequest extends Equatable { 5 | /// The firstname for the request. 6 | final String firstname; 7 | 8 | /// The lastname for the request. 9 | final String lastname; 10 | 11 | /// Constructs a EditProfileRequest object with the given parameters. 12 | const EditProfileRequest({ 13 | required this.firstname, 14 | required this.lastname, 15 | }); 16 | 17 | @override 18 | List get props => [firstname, lastname]; 19 | 20 | /// Converts the EditProfileRequest object to a JSON map. 21 | Map toMap() { 22 | return { 23 | 'firstname': firstname, 24 | 'lastname': lastname, 25 | }; 26 | } 27 | 28 | @override 29 | bool get stringify => true; 30 | } 31 | -------------------------------------------------------------------------------- /lib/data/model/request/location_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a request object for a location. 4 | class LocationRequest extends Equatable { 5 | /// The id of the location. 6 | final String? id; 7 | 8 | /// The datetime of the location. 9 | final DateTime datetime; 10 | 11 | /// The latitude of the location. 12 | final double latitude; 13 | 14 | /// The longitude of the location. 15 | final double longitude; 16 | 17 | /// Constructs a LocationRequest object with the given parameters. 18 | const LocationRequest({ 19 | this.id, 20 | required this.datetime, 21 | required this.latitude, 22 | required this.longitude, 23 | }); 24 | 25 | @override 26 | List get props => [datetime, latitude, longitude]; 27 | 28 | /// Converts the LocationRequest object to a JSON map. 29 | Map toMap() { 30 | return { 31 | 'id': id, 32 | 'datetime': datetime.toIso8601String(), 33 | 'latitude': latitude, 34 | 'longitude': longitude, 35 | }; 36 | } 37 | 38 | @override 39 | bool get stringify => true; 40 | } 41 | -------------------------------------------------------------------------------- /lib/data/model/request/login_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a login request object. 4 | class LoginRequest extends Equatable { 5 | /// The username for the login request. 6 | final String username; 7 | 8 | /// The password for the login request. 9 | final String password; 10 | 11 | /// Constructs a LoginRequest object with the given parameters. 12 | const LoginRequest({ 13 | required this.username, 14 | required this.password, 15 | }); 16 | 17 | @override 18 | List get props => [username, password]; 19 | 20 | /// Converts the LoginRequest object to a JSON map. 21 | Map toMap() { 22 | return { 23 | 'username': username, 24 | 'password': password, 25 | }; 26 | } 27 | 28 | @override 29 | bool get stringify => true; 30 | } 31 | -------------------------------------------------------------------------------- /lib/data/model/request/registration_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a registration request object. 4 | class RegistrationRequest extends Equatable { 5 | /// The firstname for the registration request. 6 | final String firstname; 7 | 8 | /// The lastname for the registration request. 9 | final String lastname; 10 | 11 | /// The username for the registration request. 12 | final String username; 13 | 14 | /// The password for the registration request. 15 | final String password; 16 | 17 | /// Constructs a RegistrationRequest object with the given parameters. 18 | const RegistrationRequest({ 19 | required this.firstname, 20 | required this.lastname, 21 | required this.username, 22 | required this.password, 23 | }); 24 | 25 | @override 26 | List get props => [username, password]; 27 | 28 | /// Converts the RegistrationRequest object to a JSON map. 29 | Map toMap() { 30 | return { 31 | 'firstname': firstname, 32 | 'lastname': lastname, 33 | 'username': username, 34 | 'password': password, 35 | }; 36 | } 37 | 38 | @override 39 | bool get stringify => true; 40 | } 41 | -------------------------------------------------------------------------------- /lib/data/model/request/send_new_password_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a send new password request object. 4 | class SendNewPasswordRequest extends Equatable { 5 | /// The email for the send new password request. 6 | final String email; 7 | 8 | /// Constructs a SendNewPasswordRequest object with the given parameters. 9 | const SendNewPasswordRequest({required this.email}); 10 | 11 | @override 12 | List get props => [email]; 13 | 14 | /// Converts the SendNewPasswordRequest object to a JSON map. 15 | Map toMap() { 16 | return {'email': email}; 17 | } 18 | 19 | @override 20 | bool get stringify => true; 21 | } 22 | -------------------------------------------------------------------------------- /lib/data/model/response/activity_comment_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../../domain/entities/activity_comment.dart'; 4 | import 'user_response.dart'; 5 | 6 | /// Represents a response object for an activity comment. 7 | class ActivityCommentResponse extends Equatable { 8 | /// The ID of the comment. 9 | final String id; 10 | 11 | /// The datetime of the comment 12 | final DateTime createdAt; 13 | 14 | /// The user 15 | final UserResponse user; 16 | 17 | /// The comment content 18 | final String content; 19 | 20 | /// Constructs an ActivityCommentResponse object with the given parameters. 21 | const ActivityCommentResponse( 22 | {required this.id, 23 | required this.createdAt, 24 | required this.user, 25 | required this.content}); 26 | 27 | @override 28 | List get props => [id, createdAt, user, content]; 29 | 30 | /// Creates a ActivityCommentResponse object from a JSON map. 31 | factory ActivityCommentResponse.fromMap(Map map) { 32 | return ActivityCommentResponse( 33 | id: map['id'].toString(), 34 | createdAt: DateTime.parse(map['createdAt']), 35 | user: UserResponse.fromMap(map['user']), 36 | content: map['content'].toString()); 37 | } 38 | 39 | /// Converts the ActivityCommentResponse object to a ActivityComment entity. 40 | ActivityComment toEntity() { 41 | return ActivityComment( 42 | id: id, 43 | createdAt: createdAt, 44 | user: user.toEntity(), 45 | content: content, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/data/model/response/friend_request_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../../domain/entities/enum/friend_request_status.dart'; 4 | import '../../../domain/entities/friend_request.dart'; 5 | 6 | /// Represents a response object for a friend request. 7 | class FriendRequestResponse extends Equatable { 8 | /// The username of the user 9 | final FriendRequestStatus? status; 10 | 11 | /// Constructs an FriendRequestResponse object with the given parameters. 12 | const FriendRequestResponse({required this.status}); 13 | 14 | @override 15 | List get props => [status]; 16 | 17 | /// Creates an FriendRequestResponse object from a JSON map. 18 | factory FriendRequestResponse.fromMap(Map map) { 19 | final status = FriendRequestStatus.values 20 | .firstWhere((type) => type.name.toUpperCase() == map['status']); 21 | 22 | return FriendRequestResponse(status: status); 23 | } 24 | 25 | /// Converts the FriendRequestResponse object to a FriendRequest entity. 26 | FriendRequest toEntity() { 27 | return FriendRequest(status: status); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/data/model/response/location_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../../domain/entities/location.dart'; 4 | 5 | /// Represents a response object for a location. 6 | class LocationResponse extends Equatable { 7 | /// The ID of the location. 8 | final String id; 9 | 10 | /// The datetime of the location. 11 | final DateTime datetime; 12 | 13 | /// The latitude of the location. 14 | final double latitude; 15 | 16 | /// The longitude of the location. 17 | final double longitude; 18 | 19 | /// Constructs a LocationResponse object with the given parameters. 20 | const LocationResponse({ 21 | required this.id, 22 | required this.datetime, 23 | required this.latitude, 24 | required this.longitude, 25 | }); 26 | 27 | @override 28 | List get props => [ 29 | id, 30 | datetime, 31 | latitude, 32 | longitude, 33 | ]; 34 | 35 | /// Creates a LocationResponse object from a JSON map. 36 | factory LocationResponse.fromMap(Map map) { 37 | return LocationResponse( 38 | id: map['id'].toString(), 39 | datetime: DateTime.parse(map['datetime']), 40 | latitude: (map['latitude'] as num?)?.toDouble() ?? 0.0, 41 | longitude: (map['longitude'] as num?)?.toDouble() ?? 0.0, 42 | ); 43 | } 44 | 45 | /// Converts the LocationResponse object to a Location entity. 46 | Location toEntity() { 47 | return Location( 48 | id: id, 49 | datetime: datetime, 50 | latitude: latitude, 51 | longitude: longitude, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/data/model/response/login_response.dart: -------------------------------------------------------------------------------- 1 | import 'user_response.dart'; 2 | 3 | /// Represents a response object for a login request. 4 | class LoginResponse { 5 | /// The refresh token received in the login response. 6 | final String refreshToken; 7 | 8 | /// The access token (JWT token) received in the login response. 9 | final String token; 10 | 11 | /// The user received in the login response. 12 | final UserResponse user; 13 | 14 | /// A message associated with the login response. 15 | final String message; 16 | 17 | /// Constructs a LoginResponse object with the given parameters. 18 | const LoginResponse({ 19 | required this.refreshToken, 20 | required this.token, 21 | required this.user, 22 | required this.message, 23 | }); 24 | 25 | /// Creates a LoginResponse object from a JSON map. 26 | factory LoginResponse.fromMap(Map map) { 27 | return LoginResponse( 28 | refreshToken: map['refreshToken']?.toString() ?? '', 29 | token: map['token']?.toString() ?? '', 30 | user: UserResponse.fromMap(map['user']), 31 | message: map['message']?.toString() ?? '', 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/data/model/response/page_response.dart: -------------------------------------------------------------------------------- 1 | class PageResponse { 2 | final List list; 3 | final int total; 4 | 5 | const PageResponse({ 6 | required this.list, 7 | required this.total, 8 | }); 9 | 10 | factory PageResponse.fromMap(Map map) { 11 | return PageResponse(list: map['content'], total: map['totalElements']); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/data/model/response/user_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../../domain/entities/user.dart'; 4 | 5 | /// Represents a response object for a user. 6 | class UserResponse extends Equatable { 7 | /// The ID of the user. 8 | final String id; 9 | 10 | /// The firstname of the user 11 | final String? firstname; 12 | 13 | /// The lastname of the user 14 | final String? lastname; 15 | 16 | /// The username of the user 17 | final String username; 18 | 19 | /// Constructs an UserResponse object with the given parameters. 20 | const UserResponse( 21 | {required this.id, 22 | required this.username, 23 | required this.firstname, 24 | required this.lastname}); 25 | 26 | @override 27 | List get props => [id, username]; 28 | 29 | /// Creates an UserResponse object from a JSON map. 30 | factory UserResponse.fromMap(Map map) { 31 | return UserResponse( 32 | id: map['id'].toString(), 33 | username: map['username'], 34 | firstname: map['firstname'], 35 | lastname: map['lastname']); 36 | } 37 | 38 | /// Converts the UserResponse object to a User entity. 39 | User toEntity() { 40 | return User( 41 | id: id, username: username, firstname: firstname, lastname: lastname); 42 | } 43 | 44 | Map toJson() { 45 | return { 46 | 'id': id, 47 | 'username': username, 48 | 'firstname': firstname, 49 | 'lastname': lastname, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/data/repositories/friend_request_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../../domain/entities/enum/friend_request_status.dart'; 4 | import '../../domain/entities/friend_request.dart'; 5 | import '../../domain/entities/page.dart'; 6 | import '../../domain/entities/user.dart'; 7 | import '../../domain/repositories/friend_request_repository.dart'; 8 | import '../api/friend_request_api.dart'; 9 | 10 | /// Provider for the FriendRequestRepository implementation. 11 | final friendRequestRepositoryProvider = 12 | Provider((ref) => FriendRequestRepositoryImpl()); 13 | 14 | /// Implementation of the FriendRequestRepository. 15 | class FriendRequestRepositoryImpl extends FriendRequestRepository { 16 | FriendRequestRepositoryImpl(); 17 | 18 | @override 19 | Future> getPendingRequestUsers({int pageNumber = 0}) async { 20 | final pendingUsersResponses = 21 | await FriendRequestApi.getPendindRequestUsers(pageNumber); 22 | 23 | List users = pendingUsersResponses.list 24 | .map((response) => response.toEntity()) 25 | .toList(); 26 | return EntityPage(list: users, total: pendingUsersResponses.total); 27 | } 28 | 29 | @override 30 | Future getStatus(String userId) async { 31 | return await FriendRequestApi.getStatus(userId); 32 | } 33 | 34 | @override 35 | Future sendRequest(String userId) async { 36 | return await FriendRequestApi.sendRequest(userId); 37 | } 38 | 39 | @override 40 | Future accept(String userId) async { 41 | final response = await FriendRequestApi.accept(userId); 42 | return response.toEntity(); 43 | } 44 | 45 | @override 46 | Future reject(String userId) async { 47 | final response = await FriendRequestApi.reject(userId); 48 | return response.toEntity(); 49 | } 50 | 51 | @override 52 | Future cancel(String userId) async { 53 | final response = await FriendRequestApi.cancel(userId); 54 | return response.toEntity(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/data/repositories/user_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../../core/utils/storage_utils.dart'; 6 | import '../../domain/entities/user.dart'; 7 | import '../../domain/repositories/user_repository.dart'; 8 | import '../api/user_api.dart'; 9 | import '../model/request/edit_password_request.dart'; 10 | import '../model/request/edit_profile_request.dart'; 11 | import '../model/request/login_request.dart'; 12 | import '../model/request/registration_request.dart'; 13 | import '../model/request/send_new_password_request.dart'; 14 | import '../model/response/login_response.dart'; 15 | 16 | /// Provider for the UserRepository implementation. 17 | final userRepositoryProvider = 18 | Provider((ref) => UserRepositoryImpl()); 19 | 20 | /// Implementation of the UserRepository. 21 | class UserRepositoryImpl extends UserRepository { 22 | UserRepositoryImpl(); 23 | 24 | @override 25 | Future register(RegistrationRequest request) async { 26 | return UserApi.createUser(request); 27 | } 28 | 29 | @override 30 | Future login(LoginRequest request) async { 31 | LoginResponse response = await UserApi.login(request); 32 | await StorageUtils.setJwt(response.token); 33 | await StorageUtils.setRefreshToken(response.refreshToken); 34 | await StorageUtils.setUser(response.user); 35 | return response; 36 | } 37 | 38 | @override 39 | Future logout() async { 40 | await UserApi.logout(); 41 | await StorageUtils.removeJwt(); 42 | await StorageUtils.removeRefreshToken(); 43 | return; 44 | } 45 | 46 | @override 47 | Future delete() async { 48 | return UserApi.delete(); 49 | } 50 | 51 | @override 52 | Future sendNewPasswordByMail(SendNewPasswordRequest request) async { 53 | await UserApi.sendNewPasswordByMail(request); 54 | } 55 | 56 | @override 57 | Future editPassword(EditPasswordRequest request) async { 58 | await UserApi.editPassword(request); 59 | } 60 | 61 | @override 62 | Future editProfile(EditProfileRequest request) async { 63 | await UserApi.editProfile(request); 64 | } 65 | 66 | @override 67 | Future> search(String text) async { 68 | final userResponses = await UserApi.search(text); 69 | return userResponses.map((response) => response.toEntity()).toList(); 70 | } 71 | 72 | @override 73 | Future downloadProfilePicture(String id) async { 74 | return await UserApi.downloadProfilePicture(id); 75 | } 76 | 77 | @override 78 | Future uploadProfilePicture(Uint8List file) async { 79 | return await UserApi.uploadProfilePicture(file); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/domain/entities/activity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'activity_comment.dart'; 4 | import 'enum/activity_type.dart'; 5 | import 'location.dart'; 6 | import 'user.dart'; 7 | 8 | /// Represents an activity. 9 | class Activity extends Equatable { 10 | /// The ID of the activity. 11 | final String id; 12 | 13 | /// The type of the activity. 14 | final ActivityType type; 15 | 16 | /// The start datetime of the activity. 17 | final DateTime startDatetime; 18 | 19 | /// The end datetime of the activity. 20 | final DateTime endDatetime; 21 | 22 | /// The distance covered in the activity. 23 | final double distance; 24 | 25 | /// The average speed in the activity. 26 | final double speed; 27 | 28 | /// The total time of the activity. 29 | final double time; 30 | 31 | /// The list of locations associated with the activity. 32 | final Iterable locations; 33 | 34 | // The user concerned by the activity 35 | final User user; 36 | 37 | /// The count of likes on the activity 38 | final double likesCount; 39 | 40 | /// has current user liked ? 41 | final bool hasCurrentUserLiked; 42 | 43 | /// The list of comments associated with the activity. 44 | final Iterable comments; 45 | 46 | /// Constructs an Activity object with the given parameters. 47 | const Activity( 48 | {required this.id, 49 | required this.type, 50 | required this.startDatetime, 51 | required this.endDatetime, 52 | required this.distance, 53 | required this.speed, 54 | required this.time, 55 | required this.locations, 56 | required this.user, 57 | required this.likesCount, 58 | required this.hasCurrentUserLiked, 59 | required this.comments}); 60 | 61 | Activity copy( 62 | {ActivityType? type, 63 | double? likesCount, 64 | bool? hasCurrentUserLiked, 65 | Iterable? comments}) { 66 | return Activity( 67 | id: id, 68 | type: type ?? this.type, 69 | startDatetime: startDatetime, 70 | endDatetime: endDatetime, 71 | distance: distance, 72 | speed: speed, 73 | time: time, 74 | locations: locations, 75 | user: user, 76 | likesCount: likesCount ?? this.likesCount, 77 | hasCurrentUserLiked: hasCurrentUserLiked ?? this.hasCurrentUserLiked, 78 | comments: comments ?? this.comments); 79 | } 80 | 81 | @override 82 | List get props => [ 83 | id, 84 | type, 85 | startDatetime, 86 | endDatetime, 87 | distance, 88 | speed, 89 | time, 90 | ...locations, 91 | user, 92 | likesCount, 93 | hasCurrentUserLiked, 94 | ...comments 95 | ]; 96 | } 97 | -------------------------------------------------------------------------------- /lib/domain/entities/activity_comment.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'user.dart'; 4 | 5 | /// Represents an activity comment. 6 | class ActivityComment extends Equatable { 7 | /// The ID of the activityComment. 8 | final String id; 9 | 10 | /// The datetime of the comment. 11 | final DateTime createdAt; 12 | 13 | /// The user 14 | final User user; 15 | 16 | /// The content 17 | final String content; 18 | 19 | /// Constructs a Location object with the given parameters. 20 | const ActivityComment({ 21 | required this.id, 22 | required this.createdAt, 23 | required this.user, 24 | required this.content, 25 | }); 26 | 27 | @override 28 | List get props => [id, createdAt, user, content]; 29 | } 30 | -------------------------------------------------------------------------------- /lib/domain/entities/enum/activity_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 2 | 3 | /// Enum representing different types of activities. 4 | enum ActivityType { running, walking, cycling } 5 | 6 | /// Extension on ActivityType to provide translated names based on the given localization. 7 | extension ActivityTypeExtension on ActivityType { 8 | /// Retrieves the translated name of the activity type based on the provided localization. 9 | String getTranslatedName(AppLocalizations localization) { 10 | switch (this) { 11 | case ActivityType.running: 12 | return localization.running; 13 | case ActivityType.walking: 14 | return localization.walking; 15 | case ActivityType.cycling: 16 | return localization.cycling; 17 | default: 18 | return ''; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/domain/entities/enum/friend_request_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 2 | 3 | /// Enum representing different status of friend requests. 4 | enum FriendRequestStatus { pending, accepted, rejected, canceled, noDisplay } 5 | 6 | /// Extension on FriendRequestStatus to provide translated names based on the given localization. 7 | extension FriendRequestStatusExtension on FriendRequestStatus { 8 | /// Retrieves the translated name of the friend request status based on the provided localization. 9 | String getTranslatedName(AppLocalizations localization) { 10 | switch (this) { 11 | case FriendRequestStatus.pending: 12 | return localization.pending; 13 | case FriendRequestStatus.accepted: 14 | return localization.accepted; 15 | case FriendRequestStatus.rejected: 16 | return localization.rejected; 17 | case FriendRequestStatus.canceled: 18 | return localization.canceled; 19 | case FriendRequestStatus.noDisplay: 20 | return ''; 21 | default: 22 | return ''; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/domain/entities/friend_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'enum/friend_request_status.dart'; 4 | 5 | /// Represents a friend request. 6 | class FriendRequest extends Equatable { 7 | /// The status of the friend request. 8 | final FriendRequestStatus? status; 9 | 10 | /// Constructs a FriendRequets object with the given parameters. 11 | const FriendRequest({required this.status}); 12 | 13 | @override 14 | List get props => [status]; 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/entities/location.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a location. 4 | class Location extends Equatable { 5 | /// The ID of the location. 6 | final String id; 7 | 8 | /// The datetime of the location. 9 | final DateTime datetime; 10 | 11 | /// The latitude of the location. 12 | final double latitude; 13 | 14 | /// The longitude of the location. 15 | final double longitude; 16 | 17 | /// Constructs a Location object with the given parameters. 18 | const Location({ 19 | required this.id, 20 | required this.datetime, 21 | required this.latitude, 22 | required this.longitude, 23 | }); 24 | 25 | @override 26 | List get props => [id, datetime, latitude, longitude]; 27 | } 28 | -------------------------------------------------------------------------------- /lib/domain/entities/page.dart: -------------------------------------------------------------------------------- 1 | class EntityPage { 2 | final List list; 3 | final int total; 4 | 5 | const EntityPage({ 6 | required this.list, 7 | required this.total, 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /lib/domain/entities/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// Represents a user. 4 | class User extends Equatable { 5 | /// The ID of the user. 6 | final String id; 7 | 8 | /// The firstname of the user. 9 | final String? firstname; 10 | 11 | /// The lastname of the user. 12 | final String? lastname; 13 | 14 | /// The username of the user. 15 | final String username; 16 | 17 | /// Constructs a User object with the given parameters. 18 | const User( 19 | {required this.id, 20 | required this.username, 21 | required this.firstname, 22 | required this.lastname}); 23 | 24 | @override 25 | List get props => [id, username, firstname, lastname]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/domain/repositories/activity_repository.dart: -------------------------------------------------------------------------------- 1 | import '../../data/model/request/activity_request.dart'; 2 | import '../entities/activity.dart'; 3 | import '../entities/activity_comment.dart'; 4 | import '../entities/page.dart'; 5 | 6 | /// Abstract class representing the activity repository. 7 | abstract class ActivityRepository { 8 | /// Retrieves a page of activities. 9 | Future> getActivities({int pageNumber}); 10 | 11 | /// Retrieves a page of my activities and my friends. 12 | Future> getMyAndMyFriendsActivities({int pageNumber}); 13 | 14 | /// Retrieves a page of a user activities. 15 | Future> getUserActivities(String userId, 16 | {int pageNumber}); 17 | 18 | /// Retrieves an activity by its ID. 19 | Future getActivityById({required String id}); 20 | 21 | /// Removes an activity by its ID. 22 | Future removeActivity({required String id}); 23 | 24 | /// Adds a new activity. 25 | Future addActivity(ActivityRequest request); 26 | 27 | /// Edits an existing activity. 28 | Future editActivity(ActivityRequest request); 29 | 30 | /// Like the activity 31 | Future like(String id); 32 | 33 | /// Dislike the activity 34 | Future dislike(String id); 35 | 36 | /// Removes a comment by its ID. 37 | Future removeComment({required String id}); 38 | 39 | /// Adds a new comment. 40 | Future createComment(String activityId, String comment); 41 | 42 | /// Edits an existing comment. 43 | Future editComment(String id, String comment); 44 | } 45 | -------------------------------------------------------------------------------- /lib/domain/repositories/friend_request_repository.dart: -------------------------------------------------------------------------------- 1 | import '../entities/enum/friend_request_status.dart'; 2 | import '../entities/friend_request.dart'; 3 | import '../entities/page.dart'; 4 | import '../entities/user.dart'; 5 | 6 | /// Abstract class representing the friend request repository. 7 | abstract class FriendRequestRepository { 8 | /// Get the users for whom I have a pending friend request 9 | Future> getPendingRequestUsers({int pageNumber}); 10 | 11 | /// Get the status of the friend request I have with the user 12 | Future getStatus(String userId); 13 | 14 | /// Send a friend request to the user 15 | Future sendRequest(String userId); 16 | 17 | /// Accept the friend request of the user 18 | Future accept(String userId); 19 | 20 | /// Reject the friend request of the user 21 | Future reject(String userId); 22 | 23 | /// Cancel the friend request of the user 24 | Future cancel(String userId); 25 | } 26 | -------------------------------------------------------------------------------- /lib/domain/repositories/user_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import '../../data/model/request/edit_password_request.dart'; 4 | import '../../data/model/request/edit_profile_request.dart'; 5 | import '../../data/model/request/login_request.dart'; 6 | import '../../data/model/request/registration_request.dart'; 7 | import '../../data/model/request/send_new_password_request.dart'; 8 | import '../../data/model/response/login_response.dart'; 9 | import '../entities/user.dart'; 10 | 11 | /// Abstract class representing the user repository. 12 | abstract class UserRepository { 13 | /// Registers a new user. 14 | Future register(RegistrationRequest request); 15 | 16 | /// Performs user login. 17 | Future login(LoginRequest request); 18 | 19 | /// Logs out the user. 20 | Future logout(); 21 | 22 | /// Deletes the user account. 23 | Future delete(); 24 | 25 | /// Send new password by mail 26 | Future sendNewPasswordByMail(SendNewPasswordRequest request); 27 | 28 | /// Edit the password 29 | Future editPassword(EditPasswordRequest request); 30 | 31 | /// Edit the profile 32 | Future editProfile(EditProfileRequest request); 33 | 34 | /// Search users based on a text value 35 | Future> search(String text); 36 | 37 | /// Download the profile picture of a user 38 | Future downloadProfilePicture(String id); 39 | 40 | /// Upload the profile picture of the current user 41 | Future uploadProfilePicture(Uint8List file); 42 | } 43 | -------------------------------------------------------------------------------- /lib/l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "accepted": "Accepted", 4 | "activity": "Activity", 5 | "activity_list": " Activity list", 6 | "activity_sumup": "Activity Sumup", 7 | "ask_account_removal": "Are you sure to delete your account", 8 | "ask_activity_removal": "Are you sure to delete this activity", 9 | "average_speed": "Average speed", 10 | "back": "Back", 11 | "cancel": "Cancel", 12 | "canceled": "Canceled", 13 | "close": "Close", 14 | "community": "Community", 15 | "congrats": "End of activity. Congratulations.", 16 | "current_password": "Current Password", 17 | "cycling": "Cycling", 18 | "date_pronoun": "On", 19 | "delete": "Delete", 20 | "delete_account": "Delete account", 21 | "details": "Details", 22 | "distance": "Distance", 23 | "duration": "Duration", 24 | "edit_password": "Edit the password", 25 | "edit_password_error": "Error: the password was not edited", 26 | "edit_profile": "Edit profile", 27 | "edit_profile_error": "Error: the profile was not saved", 28 | "email": "Email", 29 | "end": "End", 30 | "firstname": "Firstname", 31 | "follow": "Follow", 32 | "followed": "Followed", 33 | "form_description_email_empty": "Type your email", 34 | "form_description_email_not_valid": "Type a valid email", 35 | "form_description_name_empty": "Type your name", 36 | "form_description_password_empty": "Type your password", 37 | "good_luck": "Let start, good luck", 38 | "graph": "Graph", 39 | "hello": "Hello", 40 | "hours": "hours", 41 | "hours_pronoun": "at", 42 | "kilometers": "kilometers", 43 | "lastname": "Lastname", 44 | "list": "My activities", 45 | "load_more": "Load more", 46 | "login": "Log in", 47 | "login_page": "Login", 48 | "logout": "Log out", 49 | "minutes": "minutes", 50 | "new_password": "New password", 51 | "no_data": "No data", 52 | "password": "Password", 53 | "passwords_do_not_match": "Passwords do not match", 54 | "pause_activity": "Activity paused", 55 | "pending": "Pending", 56 | "pending_requests_title": "Pending requests", 57 | "per": "per", 58 | "profile_picture_select": "Choose a profile picture", 59 | "profile_picture_select_please": "Please choose a profile picture", 60 | "registration": "Registration", 61 | "rejected": "Rejected", 62 | "resume_activity": "Activity resumed", 63 | "running": "Running", 64 | "search": "Search", 65 | "seconds": "seconds", 66 | "see_pending_requests": "Pending requests", 67 | "send_mail": "Send the mail", 68 | "send_new_password": "Forgot your password ?", 69 | "settings": "Settings", 70 | "share_failed": "Activity sharing failed", 71 | "speed": "Speed", 72 | "start": "Start", 73 | "start_activity": "Start", 74 | "statistics": "Statistics", 75 | "unfollow": "Unfollow", 76 | "validate": "Validate", 77 | "verify": "Verify", 78 | "view_previous_comments": "View {previousCommentsCount} previous comments", 79 | "@view_previous_comments": { 80 | "placeholders": { 81 | "previousCommentsCount": { 82 | "type": "int" 83 | } 84 | } 85 | }, 86 | "walking": "Walking", 87 | "welcome": "Welcome" 88 | } -------------------------------------------------------------------------------- /lib/l10n/app_en_US.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en_US", 3 | "accepted": "Accepted", 4 | "activity": "Activity", 5 | "activity_list": " Activity list", 6 | "activity_sumup": "Activity Sumup", 7 | "average_speed": "Average speed", 8 | "ask_account_removal": "Are you sure to delete your account", 9 | "ask_activity_removal": "Are you sure to delete this activity", 10 | "back": "Back", 11 | "cancel": "Cancel", 12 | "canceled": "Canceled", 13 | "close": "Close", 14 | "community": "Community", 15 | "congrats": "End of activity. Congratulations.", 16 | "current_password": "Current Password", 17 | "cycling": "Cycling", 18 | "date_pronoun": "On", 19 | "delete": "Delete", 20 | "delete_account": "Delete account", 21 | "details": "Details", 22 | "distance": "Distance", 23 | "duration": "Duration", 24 | "edit_password": "Edit the password", 25 | "edit_password_error": "Error: the password was not edited", 26 | "edit_profile": "Edit profile", 27 | "edit_profile_error": "Error: the profile was not saved", 28 | "email": "Email", 29 | "end": "End", 30 | "firstname": "Firstname", 31 | "follow": "Follow", 32 | "followed": "Followed", 33 | "form_description_email_empty": "Type your email", 34 | "form_description_email_not_valid": "Type a valid email", 35 | "form_description_name_empty": "Type your name", 36 | "form_description_password_empty": "Type your password", 37 | "good_luck": "Let start, good luck", 38 | "graph": "Graph", 39 | "hello": "Hello", 40 | "hours": "hours", 41 | "hours_pronoun": "at", 42 | "kilometers": "kilometers", 43 | "lastname": "Lastname", 44 | "list": "My activities", 45 | "load_more": "Load more", 46 | "login": "Log in", 47 | "login_page": "Login", 48 | "logout": "Log out", 49 | "minutes": "minutes", 50 | "new_password": "New password", 51 | "no_data": "No data", 52 | "password": "Password", 53 | "passwords_do_not_match": "Passwords do not match", 54 | "pause_activity": "Activity paused", 55 | "pending": "Pending", 56 | "pending_requests_title": "Pending requests", 57 | "per": "per", 58 | "profile_picture_select": "Choose a profile picture", 59 | "profile_picture_select_please": "Please choose a profile picture", 60 | "registration": "Registration", 61 | "rejected": "Rejected", 62 | "resume_activity": "Activity resumed", 63 | "running": "Running", 64 | "search": "Search", 65 | "seconds": "seconds", 66 | "see_pending_requests": "Pending requests", 67 | "send_mail": "Send the mail", 68 | "send_new_password": "Forgot your password ?", 69 | "settings": "Settings", 70 | "share_failed": "Activity sharing failed", 71 | "speed": "Speed", 72 | "start": "Start", 73 | "start_activity": "Start", 74 | "statistics": "Statistics", 75 | "unfollow": "Unfollow", 76 | "validate": "Validate", 77 | "verify": "Verify", 78 | "view_previous_comments": "View {previousCommentsCount} previous comments", 79 | "@view_previous_comments": { 80 | "placeholders": { 81 | "previousCommentsCount": { 82 | "type": "int" 83 | } 84 | } 85 | }, 86 | "walking": "Walking", 87 | "welcome": "Welcome" 88 | } -------------------------------------------------------------------------------- /lib/l10n/app_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "zh", 3 | "accepted": "已接受", 4 | "activity": "活动", 5 | "activity_list": "活动列表", 6 | "activity_sumup": "活动总结", 7 | "ask_account_removal": "确认删除您的帐户", 8 | "ask_activity_removal": "确认删除活动", 9 | "average_speed": "平均速度", 10 | "back": "返回", 11 | "cancel": "取消", 12 | "canceled": "已取消", 13 | "close": "关闭", 14 | "community": "社区", 15 | "congrats": "活动结束。恭喜", 16 | "current_password": "当前密码", 17 | "cycling": "骑自行车", 18 | "date_pronoun": "于", 19 | "delete": "删除", 20 | "delete_account": "删除帐户", 21 | "details": "详情", 22 | "distance": "距离", 23 | "duration": "持续时间", 24 | "edit_password": "编辑密码", 25 | "edit_password_error": "错误:密码未更改", 26 | "edit_profile": "编辑个人资料", 27 | "edit_profile_error": "错误:个人资料未保存", 28 | "email": "电子邮件", 29 | "end": "结束", 30 | "firstname": "名字", 31 | "follow": "关注", 32 | "followed": "已关注", 33 | "form_description_email_empty": "请输入您的电子邮件", 34 | "form_description_email_not_valid": "请输入有效的电子邮件地址", 35 | "form_description_name_empty": "请输入姓名", 36 | "form_description_password_empty": "请输入您的密码", 37 | "good_luck": "开始吧,祝你好运", 38 | "graph": "图表", 39 | "hello": "你好", 40 | "hours": "小时", 41 | "hours_pronoun": "在", 42 | "kilometers": "公里", 43 | "lastname": "姓", 44 | "list": "我的活动", 45 | "load_more": "加载更多", 46 | "login": "登录", 47 | "login_page": "登录页面", 48 | "logout": "注销", 49 | "minutes": "分钟", 50 | "new_password": "新密码", 51 | "no_data": "没有数据", 52 | "password": "密码", 53 | "passwords_do_not_match": "密码不匹配", 54 | "pause_activity": "活动已暂停", 55 | "pending": "待处理", 56 | "pending_requests_title": "待处理请求", 57 | "per": "每", 58 | "profile_picture_select": "选择个人资料图片", 59 | "profile_picture_select_please": "请选择个人资料图片", 60 | "registration": "注册", 61 | "rejected": "已拒绝", 62 | "resume_activity": "恢复活动", 63 | "running": "跑步", 64 | "search": "搜索", 65 | "seconds": "秒", 66 | "see_pending_requests": "查看待处理请求", 67 | "send_mail": "发送密码", 68 | "send_new_password": "忘记密码?", 69 | "settings": "设置", 70 | "share_failed": "分享活动失败", 71 | "speed": "速度", 72 | "start": "开始", 73 | "start_activity": "开始活动", 74 | "statistics": "统计", 75 | "unfollow": "取消关注", 76 | "validate": "验证", 77 | "verify": "验证", 78 | "view_previous_comments": "查看前 {previousCommentsCount} 条评论", 79 | "@view_previous_comments": { 80 | "placeholders": { 81 | "previousCommentsCount": { 82 | "type": "int" 83 | } 84 | } 85 | }, 86 | "walking": "步行", 87 | "welcome": "欢迎" 88 | } 89 | -------------------------------------------------------------------------------- /lib/l10n/support_locale.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class L10n { 4 | static const List support = [ 5 | Locale("en"), 6 | Locale("en", "US"), 7 | Locale("fr"), 8 | Locale("fr", "FR"), 9 | Locale("de"), 10 | Locale("es"), 11 | Locale("pt"), 12 | Locale("ar"), 13 | Locale("ru"), 14 | Locale("zh"), 15 | Locale("bn"), 16 | Locale("hi"), 17 | Locale("it"), 18 | Locale("ur"), 19 | ]; 20 | } 21 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/activity_item_interaction_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import 'state/activity_item_interaction_state.dart'; 4 | 5 | /// Provider for the activity item interaction view model. 6 | final activityItemInteractionViewModelProvider = StateNotifierProvider.family< 7 | ActivityItemInteractionViewModel, ActivityItemInteractionState, String>( 8 | (ref, activityId) => ActivityItemInteractionViewModel(ref, activityId)); 9 | 10 | /// View model for the activity item interaction widget. 11 | class ActivityItemInteractionViewModel 12 | extends StateNotifier { 13 | final String activityId; 14 | final Ref ref; 15 | 16 | ActivityItemInteractionViewModel(this.ref, this.activityId) 17 | : super(ActivityItemInteractionState.initial()); 18 | 19 | /// Toggle the comments in the state 20 | void toggleComments() { 21 | state = state.copyWith(displayComments: !state.displayComments); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/activity_item_like_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../../../../data/repositories/activity_repository_impl.dart'; 4 | import '../../../../domain/entities/activity.dart'; 5 | import '../../core/enums/infinite_scroll_list.enum.dart'; 6 | import '../../core/utils/activity_utils.dart'; 7 | import '../../core/widgets/view_model/infinite_scroll_list_view_model.dart'; 8 | import 'state/activity_item_like_state.dart'; 9 | 10 | /// Provider for the activity item like view model. 11 | final activityItemLikeViewModelProvider = StateNotifierProvider.family< 12 | ActivityItemLikeViewModel, 13 | ActivityItemLikeState, 14 | String>((ref, activityId) => ActivityItemLikeViewModel(ref, activityId)); 15 | 16 | /// View model for the activity item interaction widget. 17 | class ActivityItemLikeViewModel extends StateNotifier { 18 | final String activityId; 19 | final Ref ref; 20 | 21 | ActivityItemLikeViewModel(this.ref, this.activityId) 22 | : super(ActivityItemLikeState.initial()); 23 | 24 | /// Set the likes count in the state 25 | void setLikesCount(double likes) { 26 | state = state.copyWith(likes: likes); 27 | } 28 | 29 | /// Set hasUserLiked in the state 30 | void setHasUserLiked(bool hasUserLiked) { 31 | state = state.copyWith(hasUserLiked: hasUserLiked); 32 | } 33 | 34 | /// Like the activity. 35 | Future like(Activity activity) async { 36 | await ref.read(activityRepositoryProvider).like(activity.id); 37 | state = state.copyWith(likes: state.likes + 1, hasUserLiked: true); 38 | 39 | List> activities = ref 40 | .read(infiniteScrollListViewModelProvider( 41 | InfiniteScrollListEnum.community.toString(), 42 | )) 43 | .data as List>; 44 | 45 | var updatedActivities = ActivityUtils.replaceActivity( 46 | activities, 47 | activity.copy( 48 | likesCount: activity.likesCount + 1, hasCurrentUserLiked: true)); 49 | 50 | ref 51 | .read(infiniteScrollListViewModelProvider( 52 | InfiniteScrollListEnum.community.toString(), 53 | ).notifier) 54 | .replaceData(updatedActivities); 55 | } 56 | 57 | /// Dislike the activity. 58 | Future dislike(Activity activity) async { 59 | await ref.read(activityRepositoryProvider).dislike(activity.id); 60 | state = state.copyWith(likes: state.likes - 1, hasUserLiked: false); 61 | 62 | List> activities = ref 63 | .read(infiniteScrollListViewModelProvider( 64 | InfiniteScrollListEnum.community.toString(), 65 | )) 66 | .data as List>; 67 | 68 | var updatedActivities = ActivityUtils.replaceActivity( 69 | activities, 70 | activity.copy( 71 | likesCount: activity.likesCount - 1, hasCurrentUserLiked: false)); 72 | 73 | ref 74 | .read(infiniteScrollListViewModelProvider( 75 | InfiniteScrollListEnum.community.toString(), 76 | ).notifier) 77 | .replaceData(updatedActivities); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/activity_item_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../../data/repositories/activity_repository_impl.dart'; 5 | import '../../../../domain/entities/activity.dart'; 6 | import '../../../../main.dart'; 7 | import '../../../my_activities/screens/activity_details_screen.dart'; 8 | import '../../../my_activities/view_model/activity_list_view_model.dart'; 9 | import '../../user/view_model/profile_picture_view_model.dart'; 10 | import 'state/activity_item_state.dart'; 11 | 12 | /// Provider for the activity item view model. 13 | final activityItemViewModelProvider = StateNotifierProvider.family< 14 | ActivityItemViewModel, 15 | ActivityItemState, 16 | String>((ref, activityId) => ActivityItemViewModel(ref, activityId)); 17 | 18 | /// View model for the activity item widget. 19 | class ActivityItemViewModel extends StateNotifier { 20 | final String activityId; 21 | final Ref ref; 22 | final TextEditingController commentController = TextEditingController(); 23 | 24 | ActivityItemViewModel(this.ref, this.activityId) 25 | : super(ActivityItemState.initial()); 26 | 27 | /// Sets the activity in the state 28 | void setActivity(Activity activity) { 29 | state = state.copyWith(activity: activity); 30 | } 31 | 32 | /// Get the profile picture of the user 33 | void getProfilePicture(String userId) async { 34 | ref 35 | .read(profilePictureViewModelProvider(userId).notifier) 36 | .getProfilePicture(userId); 37 | } 38 | 39 | /// Retrieves the details of an activity. 40 | Future getActivityDetails(Activity activity) async { 41 | try { 42 | ref.read(activityListViewModelProvider.notifier).setIsLoading(true); 43 | final activityDetails = await ref 44 | .read(activityRepositoryProvider) 45 | .getActivityById(id: activity.id); 46 | return activityDetails; 47 | } catch (error) { 48 | // Handle error 49 | rethrow; 50 | } 51 | } 52 | 53 | /// Navigates to the activity details screen. 54 | void goToActivity(Activity activityDetails) { 55 | Future.delayed(const Duration(milliseconds: 500), () { 56 | ref.read(activityListViewModelProvider.notifier).setIsLoading(false); 57 | }); 58 | 59 | navigatorKey.currentState?.push( 60 | PageRouteBuilder( 61 | transitionDuration: const Duration(milliseconds: 500), 62 | pageBuilder: (context, animation, secondaryAnimation) => 63 | SlideTransition( 64 | position: Tween( 65 | begin: const Offset(1.0, 0.0), 66 | end: Offset.zero, 67 | ).animate(animation), 68 | child: ActivityDetailsScreen(activity: activityDetails), 69 | ), 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/activity_list_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import 'state/activity_list_state.dart'; 5 | 6 | /// Provider for the activity list view model. 7 | final activityListWidgetViewModelProvider = StateNotifierProvider.family< 8 | ActivityListWidgetViewModel, 9 | ActivityListWidgetState, 10 | String>((ref, listId) => ActivityListWidgetViewModel(ref, listId)); 11 | 12 | /// View model for the activity item widget. 13 | class ActivityListWidgetViewModel 14 | extends StateNotifier { 15 | final Ref ref; 16 | final String listId; 17 | final ScrollController scrollController = ScrollController(); 18 | 19 | ActivityListWidgetViewModel(this.ref, this.listId) 20 | : super(ActivityListWidgetState.initial()); 21 | 22 | int calculateTotalElements(List listOfLists) { 23 | int totalElements = 24 | listOfLists.fold(0, (sum, list) => sum + (list.length as int)); 25 | 26 | return totalElements; 27 | } 28 | 29 | bool hasMoreData(List list, int total) { 30 | return calculateTotalElements(list) < total; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/state/activity_item_comments_state.dart: -------------------------------------------------------------------------------- 1 | import '../../../../../domain/entities/activity_comment.dart'; 2 | 3 | /// The state class for activity item comments. 4 | class ActivityItemCommentsState { 5 | final bool displayPreviousComments; 6 | final List comments; 7 | final bool isLoading; 8 | 9 | const ActivityItemCommentsState( 10 | {required this.displayPreviousComments, 11 | required this.comments, 12 | required this.isLoading}); 13 | 14 | /// Factory method to create the initial state. 15 | factory ActivityItemCommentsState.initial() { 16 | return const ActivityItemCommentsState( 17 | displayPreviousComments: false, comments: [], isLoading: false); 18 | } 19 | 20 | ActivityItemCommentsState copyWith( 21 | {bool? displayPreviousComments, 22 | List? comments, 23 | bool? isLoading}) { 24 | return ActivityItemCommentsState( 25 | displayPreviousComments: 26 | displayPreviousComments ?? this.displayPreviousComments, 27 | comments: comments ?? this.comments, 28 | isLoading: isLoading ?? this.isLoading); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/state/activity_item_interaction_state.dart: -------------------------------------------------------------------------------- 1 | /// The state class for activity item interaction. 2 | class ActivityItemInteractionState { 3 | final bool displayComments; 4 | 5 | const ActivityItemInteractionState({required this.displayComments}); 6 | 7 | /// Factory method to create the initial state. 8 | factory ActivityItemInteractionState.initial() { 9 | return const ActivityItemInteractionState(displayComments: false); 10 | } 11 | 12 | ActivityItemInteractionState copyWith({bool? displayComments}) { 13 | return ActivityItemInteractionState( 14 | displayComments: displayComments ?? this.displayComments); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/state/activity_item_like_state.dart: -------------------------------------------------------------------------------- 1 | /// The state class for activity item like. 2 | class ActivityItemLikeState { 3 | final double likes; 4 | final bool hasUserLiked; 5 | 6 | const ActivityItemLikeState( 7 | {required this.likes, required this.hasUserLiked}); 8 | 9 | /// Factory method to create the initial state. 10 | factory ActivityItemLikeState.initial() { 11 | return const ActivityItemLikeState(likes: 0, hasUserLiked: false); 12 | } 13 | 14 | ActivityItemLikeState copyWith({double? likes, bool? hasUserLiked}) { 15 | return ActivityItemLikeState( 16 | likes: likes ?? this.likes, 17 | hasUserLiked: hasUserLiked ?? this.hasUserLiked); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/state/activity_item_state.dart: -------------------------------------------------------------------------------- 1 | import '../../../../../domain/entities/activity.dart'; 2 | 3 | /// The state class for activity item. 4 | class ActivityItemState { 5 | final Activity? activity; 6 | 7 | const ActivityItemState({this.activity}); 8 | 9 | /// Factory method to create the initial state. 10 | factory ActivityItemState.initial() { 11 | return const ActivityItemState(activity: null); 12 | } 13 | 14 | ActivityItemState copyWith({Activity? activity}) { 15 | return ActivityItemState(activity: activity ?? this.activity); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/view_model/state/activity_list_state.dart: -------------------------------------------------------------------------------- 1 | import '../../../../../domain/entities/activity.dart'; 2 | 3 | /// The state class for activity list. 4 | class ActivityListWidgetState { 5 | final List> groupedActivities; 6 | 7 | const ActivityListWidgetState({required this.groupedActivities}); 8 | 9 | /// Factory method to create the initial state. 10 | factory ActivityListWidgetState.initial() { 11 | return const ActivityListWidgetState(groupedActivities: []); 12 | } 13 | 14 | ActivityListWidgetState copyWith({List>? groupedActivities}) { 15 | return ActivityListWidgetState( 16 | groupedActivities: groupedActivities ?? this.groupedActivities); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/widgets/activity_item_interaction.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../../domain/entities/activity.dart'; 5 | import '../../core/utils/color_utils.dart'; 6 | import '../view_model/activity_item_comments_view_model.dart'; 7 | import '../view_model/activity_item_interaction_view_model.dart'; 8 | import 'activity_comments.dart'; 9 | import 'activty_like.dart'; 10 | 11 | class ActivityItemInteraction extends HookConsumerWidget { 12 | final GlobalKey formKey = GlobalKey(); 13 | final Activity currentActivity; 14 | 15 | ActivityItemInteraction({ 16 | super.key, 17 | required this.currentActivity, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | final provider = ref.read( 23 | activityItemInteractionViewModelProvider(currentActivity.id).notifier); 24 | 25 | final state = 26 | ref.watch(activityItemInteractionViewModelProvider(currentActivity.id)); 27 | final commentsState = 28 | ref.watch(activityItemCommentsViewModelProvider(currentActivity.id)); 29 | return Padding( 30 | padding: const EdgeInsets.only(left: 16, right: 16), 31 | child: Column( 32 | crossAxisAlignment: CrossAxisAlignment.start, 33 | children: [ 34 | Row( 35 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 36 | children: [ 37 | Row( 38 | children: [ 39 | IconButton( 40 | icon: Icon(Icons.comment_outlined, 41 | color: ColorUtils.black, size: 24), 42 | onPressed: () => provider.toggleComments(), 43 | ), 44 | Text(commentsState.comments.length.toString()), 45 | ], 46 | ), 47 | ActivityLike(currentActivity: currentActivity), 48 | ], 49 | ), 50 | if (state.displayComments) 51 | ActivityComments( 52 | currentActivity: currentActivity, formKey: formKey), 53 | ], 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/widgets/activity_item_user_informations.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | import '../../../../domain/entities/activity.dart'; 7 | import '../../core/utils/color_utils.dart'; 8 | import '../../core/utils/ui_utils.dart'; 9 | import '../../core/utils/user_utils.dart'; 10 | import '../../user/view_model/profile_picture_view_model.dart'; 11 | import '../view_model/activity_item_view_model.dart'; 12 | 13 | class ActivityItemUserInformation extends HookConsumerWidget { 14 | final Activity activity; 15 | 16 | final futureDataProvider = 17 | FutureProvider.family((ref, activity) async { 18 | final provider = 19 | ref.read(activityItemViewModelProvider(activity.id).notifier); 20 | String userId = activity.user.id; 21 | provider.getProfilePicture(userId); 22 | }); 23 | 24 | ActivityItemUserInformation({super.key, required this.activity}); 25 | 26 | Widget buildProfilePicture( 27 | AsyncValue futureProvider, Uint8List? profilePicture) { 28 | return futureProvider.when( 29 | data: (_) { 30 | return ClipRRect( 31 | borderRadius: BorderRadius.circular(50), 32 | child: Container( 33 | alignment: Alignment.center, 34 | width: 50, 35 | height: 50, 36 | child: profilePicture != null 37 | ? Image.memory(profilePicture, fit: BoxFit.cover) 38 | : UserUtils.personIcon, 39 | ), 40 | ); 41 | }, 42 | loading: () => Center(child: UIUtils.loader), 43 | error: (_, __) => UserUtils.personIcon, 44 | ); 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context, WidgetRef ref) { 49 | final futureProvider = ref.watch(futureDataProvider(activity)); 50 | final profilePicture = ref 51 | .watch(profilePictureViewModelProvider(activity.user.id)) 52 | .profilePicture; 53 | return Padding( 54 | padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), 55 | child: Container( 56 | decoration: BoxDecoration( 57 | border: Border( 58 | bottom: BorderSide( 59 | color: ColorUtils.greyLight, 60 | width: 0.5, 61 | ), 62 | ), 63 | ), 64 | child: TextButton( 65 | onPressed: () => UserUtils.goToProfile(activity.user), 66 | child: Row( 67 | children: [ 68 | buildProfilePicture(futureProvider, profilePicture), 69 | const SizedBox(width: 20), 70 | Flexible( 71 | child: Text( 72 | UserUtils.getNameOrUsername(activity.user), 73 | style: TextStyle(color: ColorUtils.black), 74 | overflow: TextOverflow.ellipsis, 75 | maxLines: 1, 76 | ), 77 | ), 78 | ], 79 | ), 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/presentation/common/activity/widgets/activty_like.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../../domain/entities/activity.dart'; 5 | import '../../core/utils/color_utils.dart'; 6 | import '../view_model/activity_item_like_view_model.dart'; 7 | 8 | class ActivityLike extends HookConsumerWidget { 9 | final Activity currentActivity; 10 | 11 | const ActivityLike({super.key, required this.currentActivity}); 12 | 13 | @override 14 | Widget build(BuildContext buildContext, WidgetRef ref) { 15 | bool hasCurrentUserLiked = ref 16 | .watch(activityItemLikeViewModelProvider(currentActivity.id)) 17 | .hasUserLiked; 18 | 19 | double likesCount = 20 | ref.watch(activityItemLikeViewModelProvider(currentActivity.id)).likes; 21 | 22 | final provider = ref 23 | .read(activityItemLikeViewModelProvider(currentActivity.id).notifier); 24 | 25 | return Padding( 26 | padding: const EdgeInsets.only(left: 16), 27 | child: Row( 28 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 29 | children: [ 30 | Row( 31 | children: [ 32 | IconButton( 33 | icon: Icon( 34 | hasCurrentUserLiked ? Icons.favorite : Icons.favorite_border, 35 | color: 36 | hasCurrentUserLiked ? ColorUtils.red : ColorUtils.black, 37 | ), 38 | onPressed: () { 39 | if (hasCurrentUserLiked) { 40 | provider.dislike(currentActivity); 41 | } else { 42 | provider.like(currentActivity); 43 | } 44 | }, 45 | ), 46 | Text( 47 | '${likesCount.ceil()}', 48 | style: TextStyle( 49 | color: ColorUtils.grey, 50 | fontFamily: 'Avenir', 51 | ), 52 | ), 53 | ], 54 | ) 55 | ], 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/presentation/common/core/enums/infinite_scroll_list.enum.dart: -------------------------------------------------------------------------------- 1 | enum InfiniteScrollListEnum { community, myActivities, profile } 2 | -------------------------------------------------------------------------------- /lib/presentation/common/core/services/text_to_speech_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | import 'package:flutter_tts/flutter_tts.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import '../../../../main.dart'; 8 | 9 | /// A provider for the text-to-speech service. 10 | final textToSpeechService = Provider((ref) { 11 | return TextToSpeechService(ref); 12 | }); 13 | 14 | /// A service for text-to-speech functionality. 15 | class TextToSpeechService { 16 | late dynamic ref; 17 | late AppLocalizations translate; 18 | FlutterTts flutterTts = FlutterTts(); 19 | 20 | TextToSpeechService(this.ref); 21 | 22 | /// Initializes the text-to-speech service. 23 | Future init() async { 24 | var lang = ui.window.locale.languageCode; 25 | await flutterTts.setLanguage(lang); 26 | translate = await ref.read(myAppProvider).getLocalizedConf(); 27 | } 28 | 29 | /// Says "Good luck" using text-to-speech. 30 | Future sayGoodLuck() async { 31 | await flutterTts.speak(translate.good_luck); 32 | } 33 | 34 | /// Says the activity sum-up using text-to-speech. 35 | Future sayActivitySumUp() async { 36 | await flutterTts.speak(translate.activity_sumup); 37 | } 38 | 39 | /// Says "Pause" using text-to-speech. 40 | Future sayPause() async { 41 | await flutterTts.speak(translate.pause_activity); 42 | } 43 | 44 | /// Says "Resume" using text-to-speech. 45 | Future sayResume() async { 46 | await flutterTts.speak(translate.resume_activity); 47 | } 48 | 49 | /// Says the given text using text-to-speech. 50 | Future say(String text) async { 51 | await flutterTts.speak(text); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/presentation/common/core/utils/form_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'color_utils.dart'; 4 | 5 | /// Utility class for form-related operations. 6 | class FormUtils { 7 | /// Button style used for form buttons. 8 | static final ButtonStyle buttonStyle = createButtonStyle(ColorUtils.main); 9 | 10 | /// Default text style for form fields. 11 | static const TextStyle textFormFieldStyle = TextStyle(fontSize: 20); 12 | 13 | /// Dark text style for form fields. 14 | static TextStyle darkTextFormFieldStyle = TextStyle( 15 | fontSize: 20, 16 | color: ColorUtils.white, 17 | ); 18 | 19 | /// Creates a button style with the given [backgroundColor]. 20 | /// 21 | /// The [backgroundColor] determines the background color of the button. 22 | /// Returns the created button style. 23 | static ButtonStyle createButtonStyle(Color backgroundColor) { 24 | return ButtonStyle( 25 | textStyle: WidgetStateProperty.all(TextStyle( 26 | fontSize: 20, 27 | color: ColorUtils.white, 28 | )), 29 | minimumSize: WidgetStateProperty.all(const Size(150, 50)), 30 | backgroundColor: WidgetStateProperty.all(backgroundColor), 31 | shape: WidgetStateProperty.all( 32 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), 33 | ), 34 | ); 35 | } 36 | 37 | /// Creates an input decoration for form fields. 38 | /// 39 | /// The [text] is the label text for the form field. 40 | /// The [dark] flag determines if the form field should use dark colors. 41 | /// The [icon] is an optional icon for the form field. 42 | /// Returns the created input decoration. 43 | static InputDecoration createInputDecorative(String text, 44 | {bool? dark, IconData? icon}) { 45 | dark ??= false; 46 | final color = dark ? ColorUtils.mainLight : ColorUtils.main; 47 | final errorColor = dark ? ColorUtils.errorLight : ColorUtils.error; 48 | 49 | return InputDecoration( 50 | icon: icon != null ? Icon(icon) : null, 51 | iconColor: color, 52 | errorStyle: TextStyle(color: errorColor), 53 | errorBorder: 54 | UnderlineInputBorder(borderSide: BorderSide(color: errorColor)), 55 | focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: color)), 56 | border: UnderlineInputBorder(borderSide: BorderSide(color: color)), 57 | enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: color)), 58 | focusedErrorBorder: 59 | UnderlineInputBorder(borderSide: BorderSide(color: errorColor)), 60 | focusColor: color, 61 | labelStyle: TextStyle(color: color), 62 | labelText: text, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/presentation/common/core/utils/map_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:latlong2/latlong.dart'; 4 | 5 | /// Utility class for map-related operations. 6 | class MapUtils { 7 | /// Returns the center coordinates of a collection of [points]. 8 | /// 9 | /// The [points] is a list of [LatLng] coordinates. 10 | /// Returns the center [LatLng] coordinates of the collection. 11 | static LatLng getCenterOfMap(List points) { 12 | double sumLat = 0.0; 13 | double sumLng = 0.0; 14 | 15 | for (LatLng coordinate in points) { 16 | sumLat += coordinate.latitude; 17 | sumLng += coordinate.longitude; 18 | } 19 | 20 | double centerLat = points.isNotEmpty ? sumLat / points.length : 0; 21 | double centerLng = points.isNotEmpty ? sumLng / points.length : 0; 22 | 23 | return LatLng(centerLat, centerLng); 24 | } 25 | 26 | /// Calculates the distance between two [LatLng] coordinates. 27 | /// 28 | /// The [point1] and [point2] are the coordinates to calculate the distance between. 29 | /// Returns the distance between the coordinates in meters. 30 | static double getDistance(LatLng point1, LatLng point2) { 31 | return const Distance().as(LengthUnit.Meter, point1, point2); 32 | } 33 | 34 | /// Calculates the radius of a collection of [points] around a given [center] coordinate. 35 | /// 36 | /// The [points] is a list of [LatLng] coordinates. 37 | /// The [center] is the coordinate around which to calculate the radius. 38 | /// Returns the maximum distance from the center to any point in the collection. 39 | static double getRadius(List points, LatLng center) { 40 | double maxDistance = 0.0; 41 | 42 | for (LatLng coordinate in points) { 43 | final distance = getDistance(center, coordinate); 44 | if (distance > maxDistance) { 45 | maxDistance = distance; 46 | } 47 | } 48 | 49 | return maxDistance; 50 | } 51 | 52 | /// Calculates the zoom level based on a collection of [points] and a [center] coordinate. 53 | /// 54 | /// The [points] is a list of [LatLng] coordinates. 55 | /// The [center] is the coordinate around which to calculate the zoom level. 56 | /// Returns the calculated zoom level. 57 | static double getZoomLevel(List points, LatLng center) { 58 | final radius = getRadius(points, center); 59 | 60 | double zoomLevel = 11; 61 | if (radius > 0) { 62 | final radiusElevated = radius + radius / 2; 63 | final scale = radiusElevated / 500; 64 | zoomLevel = 16 - (log(scale) / log(2)); 65 | } 66 | zoomLevel = double.parse(zoomLevel.toStringAsFixed(2)) - 0.25; 67 | return zoomLevel; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/presentation/common/core/utils/share_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | import 'package:share_plus/share_plus.dart'; 6 | 7 | /// Utility class for sharing operations. 8 | /// 9 | class ShareUtils { 10 | /// Open the sharing dialog 11 | static Future shareImage(BuildContext context, Uint8List image) async { 12 | await Share.shareXFiles([ 13 | XFile.fromData( 14 | image, 15 | name: 'run_flutter_run', 16 | mimeType: 'image/png', 17 | ) 18 | ]); 19 | } 20 | 21 | /// Display a snackbar when share image failed 22 | static void showShareFailureSnackBar(BuildContext context) { 23 | ScaffoldMessenger.of(context).showSnackBar( 24 | SnackBar( 25 | content: Text(AppLocalizations.of(context)!.share_failed), 26 | duration: const Duration(seconds: 3), 27 | action: SnackBarAction( 28 | label: AppLocalizations.of(context)!.close, 29 | onPressed: () { 30 | ScaffoldMessenger.of(context).hideCurrentSnackBar(); 31 | }, 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/presentation/common/core/utils/type_utils.dart: -------------------------------------------------------------------------------- 1 | extension StringExtension on String { 2 | String capitalize() { 3 | return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; 4 | } 5 | } 6 | 7 | extension DoubleFormatting on double { 8 | String formatAsFixed(int fractionDigits) => toStringAsFixed(fractionDigits); 9 | } 10 | -------------------------------------------------------------------------------- /lib/presentation/common/core/utils/ui_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 3 | 4 | import 'color_utils.dart'; 5 | 6 | class UIUtils { 7 | /// A loader widget that displays a spinning animation with three bouncing balls. 8 | /// It is a part of the `flutter_spinkit` package. 9 | static final loader = SpinKitThreeBounce( 10 | color: ColorUtils.blueGrey, // The color of the bouncing balls 11 | size: 50.0, // The size of the loader widget 12 | ); 13 | 14 | /// A function that create the header for a specific title 15 | static Column createHeader(title) { 16 | return Column(children: [ 17 | Container( 18 | padding: const EdgeInsets.only(left: 0, top: 12), 19 | child: Text( 20 | title, 21 | style: TextStyle( 22 | color: ColorUtils.blueGrey, 23 | fontSize: 28, 24 | fontWeight: FontWeight.bold), 25 | ), 26 | ), 27 | const Divider(), 28 | ]); 29 | } 30 | 31 | /// A function that create the back button 32 | static FloatingActionButton createBackButton(BuildContext context) { 33 | return FloatingActionButton( 34 | heroTag: 'back_button', 35 | backgroundColor: ColorUtils.main, 36 | elevation: 4.0, 37 | child: Icon( 38 | Icons.arrow_back, 39 | color: ColorUtils.white, 40 | ), 41 | onPressed: () { 42 | Navigator.pop(context); 43 | }, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/presentation/common/core/utils/user_utils.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../../../domain/entities/user.dart'; 5 | import '../../../../main.dart'; 6 | import '../../user/screens/profile_screen.dart'; 7 | import 'color_utils.dart'; 8 | 9 | /// Utility class for user-related operations. 10 | class UserUtils { 11 | static final personIcon = Icon( 12 | Icons.person, 13 | size: 50, 14 | color: ColorUtils.black, 15 | ); 16 | 17 | static String getNameOrUsername(User user) { 18 | return user.firstname != null && user.lastname != null 19 | ? '${user.firstname} ${user.lastname}' 20 | : user.username; 21 | } 22 | 23 | /// Go to user profile 24 | static void goToProfile(User user) { 25 | navigatorKey.currentState?.push( 26 | PageRouteBuilder( 27 | transitionDuration: const Duration(milliseconds: 500), 28 | pageBuilder: (context, animation, secondaryAnimation) => 29 | SlideTransition( 30 | position: Tween( 31 | begin: const Offset(1.0, 0.0), 32 | end: Offset.zero, 33 | ).animate(animation), 34 | child: ProfileScreen(user: user), 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/presentation/common/core/validators/login_validators.dart: -------------------------------------------------------------------------------- 1 | import 'package:email_validator/email_validator.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | /// Validators for login form fields. 6 | class LoginValidators { 7 | /// Validates the names [value]. 8 | /// 9 | /// The [context] is the build context for localization. 10 | /// The [value] is the name input value to validate. 11 | /// Returns an error message if the name is empty, or null if the name is valid. 12 | static String? name(BuildContext context, String? value) { 13 | if (value == null || value.isEmpty) { 14 | return AppLocalizations.of(context)!.form_description_name_empty; 15 | } 16 | return null; 17 | } 18 | 19 | /// Validates the email [value]. 20 | /// 21 | /// The [context] is the build context for localization. 22 | /// The [value] is the email input value to validate. 23 | /// Returns an error message if the email is empty or not valid, or null if the email is valid. 24 | static String? email(BuildContext context, String? value) { 25 | if (value == null || value.isEmpty) { 26 | return AppLocalizations.of(context)!.form_description_email_empty; 27 | } 28 | if (!EmailValidator.validate(value)) { 29 | return AppLocalizations.of(context)!.form_description_email_not_valid; 30 | } 31 | return null; 32 | } 33 | 34 | /// Validates the password [value]. 35 | /// 36 | /// The [context] is the build context for localization. 37 | /// The [value] is the password input value to validate. 38 | /// Returns an error message if the password is empty, or null if the password is valid. 39 | static String? password(BuildContext context, String? value) { 40 | if (value == null || value.isEmpty) { 41 | return AppLocalizations.of(context)!.form_description_password_empty; 42 | } 43 | return null; 44 | } 45 | 46 | /// Validates the confirm password [value] against the [password]. 47 | /// 48 | /// The [context] is the build context for localization. 49 | /// The [value] is the confirm password input value to validate. 50 | /// The [password] is the password input value to match against. 51 | /// Returns an error message if the confirm password is empty or does not match the password, 52 | /// or null if the confirm password is valid. 53 | static String? confirmPassword( 54 | BuildContext context, String? value, String? password) { 55 | if (value == null || value.isEmpty) { 56 | return AppLocalizations.of(context)!.form_description_password_empty; 57 | } 58 | if (value != password) { 59 | return AppLocalizations.of(context)!.passwords_do_not_match; 60 | } 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/presentation/common/core/widgets/date.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | /// A widget that displays a formatted date. 7 | class Date extends HookConsumerWidget { 8 | /// The date to display. 9 | final DateTime date; 10 | 11 | /// Creates a [Date] widget. 12 | /// 13 | /// The [date] is the date to display. 14 | const Date({super.key, required this.date}); 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | final appLocalizations = AppLocalizations.of(context)!; 19 | final formattedDate = DateFormat('dd/MM/yyyy').format(date); 20 | 21 | final formattedTime = DateFormat('HH:mm').format(date); 22 | 23 | return Text( 24 | '${appLocalizations.date_pronoun} $formattedDate ${appLocalizations.hours_pronoun} $formattedTime', 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/presentation/common/core/widgets/share_map_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import '../../../../domain/entities/activity.dart'; 8 | import '../../../../main.dart'; 9 | import '../../timer/viewmodel/timer_view_model.dart'; 10 | import '../utils/activity_utils.dart'; 11 | import '../utils/color_utils.dart'; 12 | import '../utils/image_utils.dart'; 13 | import '../utils/share_utils.dart'; 14 | 15 | /// A widget that displays the share map button 16 | class ShareMapButton extends HookConsumerWidget { 17 | final GlobalKey boundaryKey; 18 | final Activity activity; 19 | 20 | /// Creates a [ShareMapButton] widget. 21 | /// 22 | /// The [boundaryKey] is the key of the widget to capture and share 23 | const ShareMapButton( 24 | {super.key, required this.boundaryKey, required this.activity}); 25 | 26 | @override 27 | Widget build(BuildContext context, WidgetRef ref) { 28 | final appLocalizations = AppLocalizations.of(context)!; 29 | final timerViewModel = ref.read(timerViewModelProvider.notifier); 30 | 31 | Future shareImageWithText(Uint8List image) async { 32 | String duration = 33 | "${appLocalizations.duration}: ${timerViewModel.getFormattedTime(activity.time.toInt())}"; 34 | String distance = 35 | "${appLocalizations.distance}: ${activity.distance.toStringAsFixed(2)} km"; 36 | String speed = 37 | "${appLocalizations.speed}: ${activity.speed.toStringAsFixed(2)} km/h"; 38 | 39 | Uint8List? imageEdited = await ImageUtils.addTextToImage( 40 | image, 41 | ActivityUtils.translateActivityTypeValue( 42 | appLocalizations, activity.type), 43 | "$duration - $distance - $speed", 44 | ); 45 | 46 | if (imageEdited != null) { 47 | await ShareUtils.shareImage(navigatorKey.currentContext!, imageEdited); 48 | } else { 49 | throw Exception(); 50 | } 51 | } 52 | 53 | return FloatingActionButton( 54 | heroTag: 'share_button', 55 | onPressed: () async { 56 | try { 57 | Uint8List? image = await ImageUtils.captureWidgetToImage(boundaryKey); 58 | if (image == null) throw Exception(); 59 | 60 | await shareImageWithText(image); 61 | } catch (e) { 62 | ShareUtils.showShareFailureSnackBar(context); 63 | } 64 | }, 65 | backgroundColor: ColorUtils.main, 66 | elevation: 4.0, 67 | child: Icon( 68 | Icons.share, 69 | color: ColorUtils.white, 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/presentation/common/core/widgets/view_model/infinite_scroll_list_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import 'state/infinite_scroll_list_state.dart'; 4 | 5 | /// Provider for the infinite scroll list view model. 6 | final infiniteScrollListViewModelProvider = StateNotifierProvider.family< 7 | InfiniteScrollListViewModel, 8 | InfiniteScrollListState, 9 | String>((ref, listId) { 10 | return InfiniteScrollListViewModel(ref, listId); 11 | }); 12 | 13 | /// View model for the infinite scroll list interaction widget. 14 | class InfiniteScrollListViewModel 15 | extends StateNotifier { 16 | final String listId; 17 | final Ref ref; 18 | 19 | InfiniteScrollListViewModel(this.ref, this.listId) 20 | : super(InfiniteScrollListState.initial()); 21 | 22 | /// Set isLoading in the state 23 | void setIsLoading(bool isLoading) { 24 | state = state.copyWith(isLoading: isLoading); 25 | } 26 | 27 | /// Set data in the state 28 | void setData(List data) { 29 | state = state.copyWith(data: data, pageNumber: state.pageNumber + 1); 30 | } 31 | 32 | /// Replace data in the state 33 | void replaceData(List data) { 34 | state = state.copyWith(data: data); 35 | } 36 | 37 | /// Add data in the state 38 | void addData(List data) { 39 | var currentData = state.data; 40 | currentData.addAll(data); 41 | state = state.copyWith(data: currentData, pageNumber: state.pageNumber + 1); 42 | } 43 | 44 | /// Set pageNumber in the state 45 | void setPageNumber(int pageNumber) { 46 | state = state.copyWith(pageNumber: pageNumber); 47 | } 48 | 49 | /// reset state 50 | void reset() { 51 | state = InfiniteScrollListState.initial(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/presentation/common/core/widgets/view_model/state/infinite_scroll_list_state.dart: -------------------------------------------------------------------------------- 1 | /// The state class for infinite scroll list 2 | class InfiniteScrollListState { 3 | final List data; 4 | final bool isLoading; 5 | final int pageNumber; 6 | 7 | const InfiniteScrollListState( 8 | {required this.data, required this.isLoading, required this.pageNumber}); 9 | 10 | /// Factory method to create the initial state. 11 | factory InfiniteScrollListState.initial() { 12 | return const InfiniteScrollListState( 13 | data: [], isLoading: false, pageNumber: 0); 14 | } 15 | 16 | InfiniteScrollListState copyWith( 17 | {List? data, bool? isLoading, int? pageNumber}) { 18 | return InfiniteScrollListState( 19 | data: data ?? this.data, 20 | isLoading: isLoading ?? this.isLoading, 21 | pageNumber: pageNumber ?? this.pageNumber); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/presentation/common/friendship/widgets/accept_refuse.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../core/utils/color_utils.dart'; 4 | 5 | /// A widget that displays a accept and refuse buttons 6 | class AcceptRefuseWidget extends StatelessWidget { 7 | final String userId; 8 | final Function(String) onAccept; 9 | final Function(String) onReject; 10 | 11 | const AcceptRefuseWidget({ 12 | super.key, 13 | required this.userId, 14 | required this.onAccept, 15 | required this.onReject, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Row( 21 | mainAxisSize: MainAxisSize.min, 22 | children: [ 23 | IconButton( 24 | icon: const Icon(Icons.check), 25 | color: ColorUtils.green, 26 | onPressed: () { 27 | onAccept(userId); 28 | }, 29 | ), 30 | IconButton( 31 | icon: const Icon(Icons.close), 32 | color: ColorUtils.red, 33 | onPressed: () { 34 | onReject(userId); 35 | }, 36 | ), 37 | ], 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/presentation/common/location/view_model/state/location_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:geolocator/geolocator.dart'; 2 | 3 | import '../../../../../../data/model/request/location_request.dart'; 4 | 5 | /// Represents the state of the location. 6 | class LocationState { 7 | /// The current position. 8 | final Position? currentPosition; 9 | 10 | /// The last recorded position. 11 | final Position? lastPosition; 12 | 13 | /// The list of saved positions. 14 | final List savedPositions; 15 | 16 | /// Creates a [LocationState] instance. 17 | /// 18 | /// The [currentPosition] is the current position. 19 | /// The [lastPosition] is the last recorded position. 20 | /// The [savedPositions] is the list of saved positions. 21 | const LocationState({ 22 | this.currentPosition, 23 | this.lastPosition, 24 | required this.savedPositions, 25 | }); 26 | 27 | /// Creates an initial [LocationState] instance. 28 | factory LocationState.initial() { 29 | return const LocationState(savedPositions: []); 30 | } 31 | 32 | /// Creates a copy of this [LocationState] instance with the given fields replaced with the new values. 33 | LocationState copyWith({ 34 | Position? currentPosition, 35 | Position? lastPosition, 36 | List? savedPositions, 37 | }) { 38 | return LocationState( 39 | currentPosition: currentPosition ?? this.currentPosition, 40 | lastPosition: lastPosition ?? this.lastPosition, 41 | savedPositions: savedPositions ?? this.savedPositions, 42 | ); 43 | } 44 | 45 | @override 46 | bool operator ==(Object other) => 47 | identical(this, other) || 48 | other is LocationState && 49 | runtimeType == other.runtimeType && 50 | currentPosition == other.currentPosition && 51 | lastPosition == other.lastPosition && 52 | savedPositions == other.savedPositions; 53 | 54 | @override 55 | int get hashCode => 56 | currentPosition.hashCode ^ 57 | lastPosition.hashCode ^ 58 | savedPositions.hashCode; 59 | } 60 | -------------------------------------------------------------------------------- /lib/presentation/common/location/widgets/location_map.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_map/flutter_map.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:latlong2/latlong.dart'; 5 | 6 | import '../../core/utils/color_utils.dart'; 7 | import '../../core/utils/map_utils.dart'; 8 | import '../../core/utils/ui_utils.dart'; 9 | 10 | /// Widget that displays a map with markers and polylines representing locations. 11 | class LocationMap extends HookConsumerWidget { 12 | final List points; 13 | final List markers; 14 | final MapController? mapController; 15 | final LatLng? currentPosition; 16 | 17 | const LocationMap( 18 | {super.key, 19 | required this.points, 20 | required this.markers, 21 | required this.mapController, 22 | this.currentPosition}); 23 | 24 | @override 25 | Widget build(BuildContext context, WidgetRef ref) { 26 | final center = MapUtils.getCenterOfMap(points); 27 | final zoomLevel = MapUtils.getZoomLevel(points, center); 28 | 29 | return points.isNotEmpty || currentPosition != null 30 | ? FlutterMap( 31 | key: ValueKey(MediaQuery.of(context).orientation), 32 | mapController: mapController, 33 | options: MapOptions( 34 | initialCenter: points.isNotEmpty 35 | ? center 36 | : currentPosition ?? const LatLng(0, 0), 37 | initialZoom: zoomLevel, 38 | ), 39 | children: [ 40 | TileLayer( 41 | urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', 42 | ), 43 | PolylineLayer( 44 | polylines: [ 45 | Polyline( 46 | points: points, 47 | strokeWidth: 4, 48 | color: ColorUtils.blueGrey), 49 | ], 50 | ), 51 | MarkerLayer(markers: markers), 52 | ], 53 | ) 54 | : Center(child: UIUtils.loader); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/presentation/common/metrics/view_model/state/metrics_state.dart: -------------------------------------------------------------------------------- 1 | /// Represents the state of metrics. 2 | class MetricsState { 3 | /// The distance covered. 4 | final double distance; 5 | 6 | /// The global speed. 7 | final double globalSpeed; 8 | 9 | /// Creates a new instance of [MetricsState]. 10 | const MetricsState({required this.distance, required this.globalSpeed}); 11 | 12 | /// Creates an initial instance of [MetricsState] with default values. 13 | factory MetricsState.initial() { 14 | return const MetricsState(distance: 0, globalSpeed: 0); 15 | } 16 | 17 | /// Creates a copy of [MetricsState] with optional updates. 18 | MetricsState copyWith({double? distance, double? globalSpeed}) { 19 | return MetricsState( 20 | distance: distance ?? this.distance, 21 | globalSpeed: globalSpeed ?? this.globalSpeed, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/presentation/common/metrics/widgets/metrics.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../view_model/metrics_view_model.dart'; 5 | 6 | /// A widget that displays the metrics information such as speed and distance. 7 | class Metrics extends HookConsumerWidget { 8 | final double? speed; 9 | final double? distance; 10 | 11 | /// Creates a Metrics widget. 12 | const Metrics({super.key, this.speed, this.distance}); 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | final state = ref.watch(metricsViewModelProvider); 17 | const textStyle = TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold); 18 | 19 | double speedToDisplay = state.globalSpeed; 20 | double distanceToDisplay = state.distance; 21 | 22 | if (speed != null) { 23 | speedToDisplay = speed!; 24 | } 25 | if (distance != null) { 26 | distanceToDisplay = distance!; 27 | } 28 | 29 | return Center( 30 | child: Row( 31 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 32 | children: [ 33 | Row(children: [ 34 | const Icon( 35 | Icons.location_on, 36 | size: 45, 37 | ), 38 | const SizedBox(width: 8), 39 | Column(children: [ 40 | Text( 41 | distanceToDisplay.toStringAsFixed(2), 42 | style: textStyle, 43 | ), 44 | const Text('km'), 45 | ]) 46 | ]), 47 | Row(children: [ 48 | Column(mainAxisAlignment: MainAxisAlignment.start, children: [ 49 | Text( 50 | speedToDisplay.toStringAsFixed(2), 51 | style: textStyle, 52 | ), 53 | const Text('km/h'), 54 | ]), 55 | const SizedBox(width: 8), 56 | const Icon( 57 | Icons.speed, 58 | size: 45, 59 | ), 60 | ]) 61 | ], 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/presentation/common/timer/viewmodel/state/timer_state.dart: -------------------------------------------------------------------------------- 1 | class TimerState { 2 | final DateTime startDatetime; 3 | final int hours; 4 | final int minutes; 5 | final int seconds; 6 | final bool isRunning; 7 | 8 | /// Represents the state of a timer. 9 | const TimerState({ 10 | required this.startDatetime, 11 | required this.hours, 12 | required this.minutes, 13 | required this.seconds, 14 | required this.isRunning, 15 | }); 16 | 17 | /// Creates the initial state of a timer. 18 | factory TimerState.initial() { 19 | return TimerState( 20 | startDatetime: DateTime.now(), 21 | hours: 0, 22 | minutes: 0, 23 | seconds: 0, 24 | isRunning: false, 25 | ); 26 | } 27 | 28 | /// Creates a copy of the current state with optional changes. 29 | TimerState copyWith({ 30 | DateTime? startDatetime, 31 | int? hours, 32 | int? minutes, 33 | int? seconds, 34 | bool? isRunning, 35 | }) { 36 | return TimerState( 37 | startDatetime: startDatetime ?? this.startDatetime, 38 | hours: hours ?? this.hours, 39 | minutes: minutes ?? this.minutes, 40 | seconds: seconds ?? this.seconds, 41 | isRunning: isRunning ?? this.isRunning, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/presentation/common/timer/widgets/timer_pause.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../core/utils/color_utils.dart'; 5 | import '../viewmodel/timer_view_model.dart'; 6 | 7 | /// A floating action button used to pause or resume the timer. 8 | class TimerPause extends HookConsumerWidget { 9 | const TimerPause({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | final isRunning = ref.watch(timerViewModelProvider).isRunning; 14 | final timerViewModel = ref.watch(timerViewModelProvider.notifier); 15 | 16 | if (timerViewModel.hasTimerStarted()) { 17 | return AnimatedSwitcher( 18 | duration: const Duration(milliseconds: 300), 19 | transitionBuilder: (child, animation) { 20 | return ScaleTransition( 21 | scale: animation, 22 | child: child, 23 | ); 24 | }, 25 | child: FloatingActionButton( 26 | heroTag: 'pause_resume_button', 27 | backgroundColor: ColorUtils.main, 28 | key: ValueKey(isRunning), 29 | tooltip: timerViewModel.isTimerRunning() ? 'Pause' : 'Resume', 30 | child: Icon( 31 | isRunning ? Icons.pause : Icons.play_arrow, 32 | color: ColorUtils.white, 33 | ), 34 | onPressed: () { 35 | if (timerViewModel.isTimerRunning()) { 36 | timerViewModel.pauseTimer(); 37 | } else { 38 | timerViewModel.startTimer(); 39 | } 40 | }, 41 | ), 42 | ); 43 | } 44 | return const SizedBox.shrink(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/presentation/common/timer/widgets/timer_sized.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../viewmodel/timer_view_model.dart'; 5 | import 'timer_text.dart'; 6 | 7 | /// A widget that displays the timer text with a fixed size. 8 | class TimerTextSized extends HookConsumerWidget { 9 | const TimerTextSized({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | // ignore: unused_local_variable 14 | final state = ref.watch(timerViewModelProvider); 15 | // ignore: unused_local_variable 16 | final timerViewModel = ref.watch(timerViewModelProvider.notifier); 17 | 18 | return const Column( 19 | children: [ 20 | SizedBox( 21 | height: 125, 22 | child: Center( 23 | child: TimerText(), 24 | ), 25 | ) 26 | ], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/presentation/common/timer/widgets/timer_start.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../core/utils/color_utils.dart'; 5 | import '../viewmodel/timer_view_model.dart'; 6 | 7 | /// A widget that displays the timer start button. 8 | class TimerStart extends HookConsumerWidget { 9 | const TimerStart({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | // ignore: unused_local_variable 14 | final state = ref.watch(timerViewModelProvider); 15 | final timerViewModel = ref.watch(timerViewModelProvider.notifier); 16 | 17 | return FloatingActionButton( 18 | heroTag: 'start_button', 19 | backgroundColor: timerViewModel.hasTimerStarted() 20 | ? ColorUtils.errorDarker 21 | : ColorUtils.main, 22 | elevation: 4.0, 23 | child: AnimatedSwitcher( 24 | duration: const Duration(milliseconds: 300), 25 | transitionBuilder: (child, animation) { 26 | return ScaleTransition( 27 | scale: animation, 28 | child: FadeTransition( 29 | opacity: animation, 30 | child: child, 31 | ), 32 | ); 33 | }, 34 | child: Icon( 35 | timerViewModel.hasTimerStarted() ? Icons.stop : Icons.play_arrow, 36 | key: ValueKey(timerViewModel.hasTimerStarted()), 37 | color: ColorUtils.white, 38 | ), 39 | ), 40 | onPressed: () { 41 | if (timerViewModel.hasTimerStarted()) { 42 | timerViewModel.stopTimer(); 43 | } else { 44 | timerViewModel.startTimer(); 45 | } 46 | }, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/presentation/common/timer/widgets/timer_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../viewmodel/timer_view_model.dart'; 5 | 6 | /// A widget that displays the timer text. 7 | class TimerText extends HookConsumerWidget { 8 | final int? timeInMs; 9 | 10 | const TimerText({super.key, this.timeInMs}); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | // ignore: unused_local_variable 15 | final state = ref.watch(timerViewModelProvider); 16 | final timerViewModel = ref.watch(timerViewModelProvider.notifier); 17 | 18 | const TextStyle timerTextStyle = 19 | TextStyle(fontSize: 60.0, fontFamily: "Open Sans"); 20 | 21 | return Text( 22 | timeInMs != null 23 | ? timerViewModel.getFormattedTime(timeInMs) 24 | : timerViewModel.getFormattedTime(), 25 | style: timerTextStyle, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/presentation/common/user/view_model/profile_picture_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../../../../data/repositories/user_repository_impl.dart'; 6 | import 'state/profile_picture_state.dart'; 7 | 8 | /// Provider for the profile picture view model. 9 | final profilePictureViewModelProvider = StateNotifierProvider.family< 10 | ProfilePictureViewModel, 11 | ProfilePictureState, 12 | String>((ref, userId) => ProfilePictureViewModel(ref, userId)); 13 | 14 | class ProfilePictureViewModel extends StateNotifier { 15 | late final Ref ref; 16 | final String userId; 17 | 18 | ProfilePictureViewModel(this.ref, this.userId) 19 | : super(ProfilePictureState.initial()); 20 | 21 | Future getProfilePicture(String userId) async { 22 | if (state.loaded == false) { 23 | ref.read(userRepositoryProvider).downloadProfilePicture(userId).then( 24 | (value) => 25 | state = state.copyWith(profilePicture: value, loaded: true)); 26 | } 27 | } 28 | 29 | void editProfilePicture(Uint8List? image) { 30 | state = state.copyWith(profilePicture: image); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/common/user/view_model/profile_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../../../../core/utils/storage_utils.dart'; 4 | import '../../../../data/repositories/activity_repository_impl.dart'; 5 | import '../../../../data/repositories/friend_request_repository_impl.dart'; 6 | import '../../../../domain/entities/activity.dart'; 7 | import '../../../../domain/entities/enum/friend_request_status.dart'; 8 | import '../../../../domain/entities/page.dart'; 9 | import '../../core/enums/infinite_scroll_list.enum.dart'; 10 | import '../../core/widgets/view_model/infinite_scroll_list_view_model.dart'; 11 | import 'profile_picture_view_model.dart'; 12 | import 'state/profile_state.dart'; 13 | 14 | /// Provider for the profile view model. 15 | final profileViewModelProvider = 16 | StateNotifierProvider.family( 17 | (ref, userId) => ProfileViewModel(ref, userId)); 18 | 19 | /// View model for the community screen. 20 | class ProfileViewModel extends StateNotifier { 21 | late final Ref ref; 22 | final String userId; 23 | 24 | ProfileViewModel(this.ref, this.userId) : super(ProfileState.initial()); 25 | 26 | /// Retrieves the friendship status. 27 | Future getFriendshipStatus(String userId) async { 28 | final friendRequestRepository = ref.read(friendRequestRepositoryProvider); 29 | final currentUser = await StorageUtils.getUser(); 30 | 31 | if (userId != currentUser?.id) { 32 | final status = await friendRequestRepository.getStatus(userId); 33 | state = state.copyWith(status: status); 34 | } else { 35 | state = state.copyWith(status: FriendRequestStatus.noDisplay); 36 | } 37 | } 38 | 39 | Future> fetchActivities({int pageNumber = 0}) async { 40 | try { 41 | final activityRepository = ref.read(activityRepositoryProvider); 42 | 43 | final currentUser = await StorageUtils.getUser(); 44 | 45 | if (userId != currentUser?.id) { 46 | final activities = await activityRepository.getUserActivities(userId, 47 | pageNumber: pageNumber); 48 | return activities; 49 | } 50 | return await activityRepository.getActivities(pageNumber: pageNumber); 51 | } catch (error) { 52 | return EntityPage(list: List.empty(), total: 0); 53 | } 54 | } 55 | 56 | /// Send the friend request 57 | void sendFriendRequest(String userId) async { 58 | await ref.read(friendRequestRepositoryProvider).sendRequest(userId); 59 | state = state.copyWith(status: FriendRequestStatus.pending); 60 | } 61 | 62 | /// unfollow 63 | void unfollow(String userId) async { 64 | await ref.read(friendRequestRepositoryProvider).reject(userId); 65 | state = state.copyWith(status: FriendRequestStatus.rejected); 66 | } 67 | 68 | void getProfilePicture(String userId) { 69 | ref 70 | .read(profilePictureViewModelProvider(userId).notifier) 71 | .getProfilePicture(userId); 72 | } 73 | 74 | void refreshList() { 75 | ref 76 | .read(infiniteScrollListViewModelProvider( 77 | '${InfiniteScrollListEnum.profile}_$userId', 78 | ).notifier) 79 | .reset(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/presentation/common/user/view_model/state/profile_picture_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | /// The state class for profile picture. 4 | class ProfilePictureState { 5 | final bool loaded; 6 | final Uint8List? profilePicture; // the profile picture 7 | 8 | const ProfilePictureState( 9 | {required this.loaded, required this.profilePicture}); 10 | 11 | /// Factory method to create the initial state. 12 | factory ProfilePictureState.initial() { 13 | return const ProfilePictureState(loaded: false, profilePicture: null); 14 | } 15 | 16 | /// Method to create a copy of the state with updated values. 17 | ProfilePictureState copyWith({bool? loaded, Uint8List? profilePicture}) { 18 | return ProfilePictureState( 19 | loaded: loaded ?? this.loaded, 20 | profilePicture: profilePicture ?? this.profilePicture); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/presentation/common/user/view_model/state/profile_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import '../../../../../domain/entities/enum/friend_request_status.dart'; 4 | 5 | /// The state class for profile. 6 | class ProfileState { 7 | final bool isLoading; // Indicates if the list is currently loading 8 | final FriendRequestStatus? friendshipStatus; //the friend request status 9 | final Uint8List? profilePicture; // the profile picture 10 | 11 | const ProfileState( 12 | {required this.isLoading, 13 | required this.friendshipStatus, 14 | required this.profilePicture}); 15 | 16 | /// Factory method to create the initial state. 17 | factory ProfileState.initial() { 18 | return const ProfileState( 19 | isLoading: false, friendshipStatus: null, profilePicture: null); 20 | } 21 | 22 | /// Method to create a copy of the state with updated values. 23 | ProfileState copyWith( 24 | {bool? isLoading, // Updated loading state 25 | FriendRequestStatus? status, // Updated friend request status 26 | Uint8List? profilePicture}) { 27 | return ProfileState( 28 | isLoading: isLoading ?? this.isLoading, 29 | friendshipStatus: status ?? friendshipStatus, 30 | profilePicture: profilePicture ?? this.profilePicture); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/common/user/widgets/friend_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../../../../domain/entities/enum/friend_request_status.dart'; 6 | import '../../core/utils/color_utils.dart'; 7 | import '../view_model/profile_view_model.dart'; 8 | 9 | class FriendRequestWidget extends HookConsumerWidget { 10 | final String userId; 11 | 12 | const FriendRequestWidget({super.key, required this.userId}); 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | final provider = ref.read(profileViewModelProvider(userId).notifier); 17 | final state = ref.watch(profileViewModelProvider(userId)); 18 | 19 | if (state.friendshipStatus == FriendRequestStatus.pending) { 20 | return _buildStatusWidget(context, Icons.access_time, ColorUtils.warning, 21 | AppLocalizations.of(context)!.pending, ColorUtils.warning); 22 | } else if (state.friendshipStatus == FriendRequestStatus.accepted) { 23 | return ElevatedButton( 24 | onPressed: () { 25 | provider.unfollow(userId); 26 | }, 27 | style: ElevatedButton.styleFrom( 28 | backgroundColor: ColorUtils.red, 29 | shape: 30 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), 31 | ), 32 | child: _buildStatusWidget( 33 | context, 34 | Icons.person_remove, 35 | ColorUtils.white, 36 | AppLocalizations.of(context)!.unfollow, 37 | ColorUtils.white, 38 | ), 39 | ); 40 | } else { 41 | return ElevatedButton( 42 | onPressed: () { 43 | provider.sendFriendRequest(userId); 44 | }, 45 | style: ElevatedButton.styleFrom( 46 | backgroundColor: ColorUtils.main, 47 | shape: 48 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), 49 | ), 50 | child: _buildStatusWidget( 51 | context, 52 | Icons.person_add, 53 | ColorUtils.white, 54 | AppLocalizations.of(context)!.follow, 55 | ColorUtils.white, 56 | ), 57 | ); 58 | } 59 | } 60 | 61 | Widget _buildStatusWidget( 62 | BuildContext context, 63 | IconData icon, 64 | Color iconColor, 65 | String text, 66 | Color textColor, 67 | ) { 68 | return Container( 69 | padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), 70 | child: Row( 71 | mainAxisSize: MainAxisSize.min, 72 | children: [ 73 | Icon(icon, color: iconColor), 74 | const SizedBox(width: 8), 75 | Text( 76 | text, 77 | style: TextStyle(color: textColor), 78 | ), 79 | ], 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/presentation/community/screens/pending_requests_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../../../domain/entities/page.dart'; 6 | import '../../../domain/entities/user.dart'; 7 | import '../../common/core/utils/ui_utils.dart'; 8 | import '../view_model/pending_request_view_model.dart'; 9 | import '../widgets/pending_request_list.dart'; 10 | 11 | /// The screen that displays pending requests 12 | class PendingRequestsScreen extends HookConsumerWidget { 13 | PendingRequestsScreen({super.key}); 14 | 15 | final pendingRequestsDataFutureProvider = 16 | FutureProvider>((ref) async { 17 | final provider = ref.read(pendingRequestsViewModelProvider.notifier); 18 | return await provider.fetchPendingRequests(); 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context, WidgetRef ref) { 23 | var state = ref.watch(pendingRequestsViewModelProvider); 24 | var provider = ref.read(pendingRequestsViewModelProvider.notifier); 25 | 26 | var pendingRequestsStateProvider = 27 | ref.watch(pendingRequestsDataFutureProvider); 28 | 29 | return state.isLoading 30 | ? Expanded(child: Center(child: UIUtils.loader)) 31 | : Scaffold( 32 | body: SafeArea( 33 | child: Column(children: [ 34 | UIUtils.createHeader( 35 | AppLocalizations.of(context)!.pending_requests_title), 36 | const SizedBox(height: 40), 37 | pendingRequestsStateProvider.when( 38 | data: (initialData) { 39 | return PendingRequestsListWidget( 40 | users: initialData.list, 41 | total: initialData.total, 42 | onAccept: (userId) => provider.acceptRequest(userId), 43 | onReject: (userId) => provider.rejectRequest(userId), 44 | bottomListScrollFct: provider.fetchPendingRequests, 45 | ); 46 | }, 47 | loading: () { 48 | return Expanded(child: Center(child: UIUtils.loader)); 49 | }, 50 | error: (error, stackTrace) { 51 | return Text('$error'); 52 | }, 53 | ) 54 | ])), 55 | floatingActionButtonLocation: 56 | FloatingActionButtonLocation.centerFloat, 57 | floatingActionButton: UIUtils.createBackButton(context), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/presentation/community/view_model/community_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../../../data/repositories/activity_repository_impl.dart'; 4 | import '../../../data/repositories/user_repository_impl.dart'; 5 | import '../../../domain/entities/activity.dart'; 6 | import '../../../domain/entities/page.dart'; 7 | import '../../../domain/entities/user.dart'; 8 | import '../../common/core/enums/infinite_scroll_list.enum.dart'; 9 | import '../../common/core/widgets/view_model/infinite_scroll_list_view_model.dart'; 10 | import 'state/community_state.dart'; 11 | 12 | /// Provider for the community view model. 13 | final communityViewModelProvider = 14 | StateNotifierProvider.autoDispose( 15 | (ref) => CommunityViewModel(ref)); 16 | 17 | /// View model for the community screen. 18 | class CommunityViewModel extends StateNotifier { 19 | late final Ref ref; 20 | 21 | CommunityViewModel(this.ref) : super(CommunityState.initial()); 22 | 23 | Future> search(String text) { 24 | return ref.read(userRepositoryProvider).search(text); 25 | } 26 | 27 | Future> getInitialMyAndMyFriendsActivities( 28 | {int pageNumber = 0}) async { 29 | EntityPage newActivities = await ref 30 | .read(activityRepositoryProvider) 31 | .getMyAndMyFriendsActivities(pageNumber: pageNumber); 32 | return newActivities; 33 | } 34 | 35 | void refreshList() { 36 | ref 37 | .read(infiniteScrollListViewModelProvider( 38 | InfiniteScrollListEnum.community.toString()) 39 | .notifier) 40 | .reset(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/presentation/community/view_model/pending_request_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../../../data/repositories/friend_request_repository_impl.dart'; 4 | import '../../../domain/entities/page.dart'; 5 | import '../../../domain/entities/user.dart'; 6 | import 'state/pending_requests_state.dart'; 7 | 8 | /// Provider for the pending request view model. 9 | final pendingRequestsViewModelProvider = 10 | StateNotifierProvider( 11 | (ref) => PendingRequestsViewModel(ref)); 12 | 13 | /// View model for the pending requests screen. 14 | class PendingRequestsViewModel extends StateNotifier { 15 | late final Ref ref; 16 | 17 | PendingRequestsViewModel(this.ref) : super(PendingRequestsState.initial()); 18 | 19 | /// Fetches the list of activities. 20 | Future> fetchPendingRequests({int pageNumber = 0}) async { 21 | try { 22 | final newPendingRequests = await ref 23 | .read(friendRequestRepositoryProvider) 24 | .getPendingRequestUsers(pageNumber: pageNumber); 25 | return newPendingRequests; 26 | } catch (error) { 27 | return EntityPage(list: List.empty(), total: 0); 28 | } 29 | } 30 | 31 | void setPendingRequest(List users) { 32 | state = state.copyWith(pendingRequests: users); 33 | } 34 | 35 | acceptRequest(String userId) { 36 | state = state.copyWith(isLoading: true); 37 | return ref 38 | .read(friendRequestRepositoryProvider) 39 | .accept((userId)) 40 | .then((value) { 41 | var requests = state.pendingRequests; 42 | requests.removeWhere((user) => user.id == userId); 43 | state = state.copyWith(pendingRequests: requests, isLoading: false); 44 | }); 45 | } 46 | 47 | rejectRequest(String userId) { 48 | state = state.copyWith(isLoading: true); 49 | return ref 50 | .read(friendRequestRepositoryProvider) 51 | .reject((userId)) 52 | .then((value) { 53 | var requests = state.pendingRequests; 54 | requests.removeWhere((user) => user.id == userId); 55 | state = state.copyWith(pendingRequests: requests, isLoading: false); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/presentation/community/view_model/state/community_state.dart: -------------------------------------------------------------------------------- 1 | /// The state class for community screen. 2 | class CommunityState { 3 | const CommunityState(); 4 | 5 | /// Factory method to create the initial state. 6 | factory CommunityState.initial() { 7 | return const CommunityState(); 8 | } 9 | 10 | /// Method to create a copy of the state with updated values. 11 | CommunityState copyWith() { 12 | return const CommunityState(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/presentation/community/view_model/state/pending_requests_state.dart: -------------------------------------------------------------------------------- 1 | import '../../../../domain/entities/user.dart'; 2 | 3 | /// The state class for pending requests screen. 4 | class PendingRequestsState { 5 | final bool isLoading; 6 | final List pendingRequests; 7 | final int total; 8 | 9 | const PendingRequestsState( 10 | {required this.isLoading, 11 | required this.pendingRequests, 12 | required this.total}); 13 | 14 | /// Factory method to create the initial state. 15 | factory PendingRequestsState.initial() { 16 | return const PendingRequestsState( 17 | isLoading: false, pendingRequests: [], total: 0); 18 | } 19 | 20 | /// Method to create a copy of the state with updated values. 21 | PendingRequestsState copyWith( 22 | {bool? isLoading, // Updated loading state 23 | List? pendingRequests, 24 | int? total}) { 25 | return PendingRequestsState( 26 | isLoading: isLoading ?? this.isLoading, 27 | pendingRequests: pendingRequests ?? this.pendingRequests, 28 | total: total ?? this.total); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/presentation/community/widgets/pending_request_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../domain/entities/page.dart'; 5 | import '../../../domain/entities/user.dart'; 6 | import '../../common/core/utils/user_utils.dart'; 7 | import '../../common/core/widgets/infinite_scroll_list.dart'; 8 | import '../../common/friendship/widgets/accept_refuse.dart'; 9 | 10 | class PendingRequestsListWidget extends HookConsumerWidget { 11 | final List users; 12 | final int total; 13 | final Function(String) onAccept; 14 | final Function(String) onReject; 15 | final Future> Function({int pageNumber}) bottomListScrollFct; 16 | 17 | const PendingRequestsListWidget( 18 | {super.key, 19 | required this.users, 20 | required this.total, 21 | required this.onAccept, 22 | required this.onReject, 23 | required this.bottomListScrollFct}); 24 | 25 | @override 26 | Widget build(BuildContext context, WidgetRef ref) { 27 | return InfiniteScrollList( 28 | listId: 'PENDING_REQUESTS', 29 | initialData: users, 30 | total: total, 31 | loadData: (int pageNumber) async { 32 | return await bottomListScrollFct(pageNumber: pageNumber); 33 | }, 34 | hasMoreData: (data, total) { 35 | return data.length < total; 36 | }, 37 | itemBuildFunction: (context, users, index) { 38 | return ListTile( 39 | title: Text( 40 | UserUtils.getNameOrUsername(users[index]), 41 | ), 42 | trailing: AcceptRefuseWidget( 43 | userId: users[index].id, 44 | onAccept: onAccept, 45 | onReject: onReject, 46 | ), 47 | ); 48 | }, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/presentation/community/widgets/search_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:flutter_typeahead/flutter_typeahead.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | import '../../../domain/entities/user.dart'; 7 | import '../../common/core/utils/color_utils.dart'; 8 | import '../../common/core/utils/user_utils.dart'; 9 | 10 | class SearchWidget extends HookConsumerWidget implements PreferredSizeWidget { 11 | final TextEditingController searchController; 12 | final Future> Function(String) onSearchChanged; 13 | 14 | const SearchWidget({ 15 | super.key, 16 | required this.searchController, 17 | required this.onSearchChanged, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | return AppBar( 23 | backgroundColor: ColorUtils.white, 24 | title: TypeAheadField( 25 | textFieldConfiguration: TextFieldConfiguration( 26 | controller: searchController, 27 | decoration: InputDecoration( 28 | hintText: '${AppLocalizations.of(context)!.search}...', 29 | border: InputBorder.none, 30 | suffixIconColor: ColorUtils.main, 31 | suffixIcon: const Icon(Icons.search), 32 | ), 33 | ), 34 | suggestionsCallback: (String query) async { 35 | if (query.isNotEmpty) { 36 | return await onSearchChanged(query); 37 | } 38 | return []; 39 | }, 40 | itemBuilder: (BuildContext context, User suggestion) { 41 | return ListTile( 42 | title: Text( 43 | UserUtils.getNameOrUsername(suggestion), 44 | )); 45 | }, 46 | onSuggestionSelected: (User suggestion) => 47 | UserUtils.goToProfile(suggestion), 48 | noItemsFoundBuilder: (context) => 49 | Text(AppLocalizations.of(context)!.no_data), 50 | ), 51 | ); 52 | } 53 | 54 | @override 55 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 56 | } 57 | -------------------------------------------------------------------------------- /lib/presentation/home/screens/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:google_nav_bar/google_nav_bar.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | import '../../common/core/utils/color_utils.dart'; 7 | import '../../community/screens/community_screen.dart'; 8 | import '../../my_activities/screens/activity_list_screen.dart'; 9 | import '../../new_activity/screens/new_activity_screen.dart'; 10 | import '../../settings/screens/settings_screen.dart'; 11 | import '../view_model/home_view_model.dart'; 12 | 13 | /// An enumeration representing the available tabs in the home screen. 14 | enum Tabs { home, list, community, settings } 15 | 16 | /// The home screen widget. 17 | class HomeScreen extends HookConsumerWidget { 18 | const HomeScreen({super.key}); 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | final state = ref.watch(homeViewModelProvider); 23 | final homeViewModel = ref.watch(homeViewModelProvider.notifier); 24 | final currentIndex = state.currentIndex; 25 | 26 | final tabs = [ 27 | const NewActivityScreen(), 28 | ActivityListScreen(), 29 | CommunityScreen(), 30 | const SettingsScreen(), 31 | ]; 32 | 33 | return Scaffold( 34 | body: SafeArea(child: tabs[currentIndex]), 35 | bottomNavigationBar: Container( 36 | color: ColorUtils.mainMedium, 37 | child: Padding( 38 | padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), 39 | child: GNav( 40 | backgroundColor: ColorUtils.mainMedium, 41 | color: ColorUtils.white, 42 | activeColor: ColorUtils.white, 43 | tabBackgroundColor: ColorUtils.mainDarker, 44 | padding: const EdgeInsets.all(16), 45 | selectedIndex: currentIndex, 46 | onTabChange: (value) { 47 | homeViewModel.setCurrentIndex(value); 48 | }, 49 | gap: 8, 50 | tabs: [ 51 | GButton( 52 | icon: Icons.flash_on, 53 | text: AppLocalizations.of(context)!.start_activity, 54 | ), 55 | GButton( 56 | icon: Icons.list, 57 | text: AppLocalizations.of(context)!.list, 58 | ), 59 | GButton( 60 | icon: Icons.people, 61 | text: AppLocalizations.of(context)!.community, 62 | ), 63 | GButton( 64 | icon: Icons.settings, 65 | text: AppLocalizations.of(context)!.settings, 66 | ), 67 | ], 68 | ), 69 | ))); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/presentation/home/view_model/home_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import 'state/home_state.dart'; 4 | 5 | final homeViewModelProvider = 6 | StateNotifierProvider.autoDispose( 7 | (ref) => HomeViewModel(ref), 8 | ); 9 | 10 | class HomeViewModel extends StateNotifier { 11 | final Ref ref; 12 | 13 | /// Constructs a `HomeViewModel` with the provided [ref] and an initial [HomeState]. 14 | HomeViewModel(this.ref) : super(HomeState.initial()); 15 | 16 | /// Returns the current index value from the state. 17 | int getCurrentIndex() { 18 | return state.currentIndex; 19 | } 20 | 21 | /// Sets the current index value to the specified [index]. 22 | void setCurrentIndex(int index) { 23 | state = state.copyWith(currentIndex: index); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/presentation/home/view_model/state/home_state.dart: -------------------------------------------------------------------------------- 1 | class HomeState { 2 | final int currentIndex; 3 | 4 | /// Constructs a `HomeState` object with the provided [currentIndex]. 5 | const HomeState({required this.currentIndex}); 6 | 7 | /// Constructs an initial `HomeState` object with the default [currentIndex]. 8 | factory HomeState.initial() { 9 | return const HomeState(currentIndex: 0); 10 | } 11 | 12 | /// Creates a copy of this `HomeState` object with the specified attributes overridden. 13 | HomeState copyWith({ 14 | int? currentIndex, 15 | }) { 16 | return HomeState( 17 | currentIndex: currentIndex ?? this.currentIndex, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/presentation/login/view_model/login_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../data/model/request/login_request.dart'; 5 | import '../../../data/repositories/user_repository_impl.dart'; 6 | import '../../../main.dart'; 7 | import '../../home/screens/home_screen.dart'; 8 | import 'state/login_state.dart'; 9 | 10 | /// Provides the view model for the login screen. 11 | final loginViewModelProvider = 12 | StateNotifierProvider.autoDispose( 13 | (ref) => LoginViewModel(ref), 14 | ); 15 | 16 | /// The view model class for the login screen. 17 | class LoginViewModel extends StateNotifier { 18 | final Ref ref; 19 | 20 | LoginViewModel(this.ref) : super(LoginState.initial()); 21 | 22 | /// Sets the username in the state. 23 | void setUsername(String? username) { 24 | state = state.copyWith(username: username ?? ''); 25 | } 26 | 27 | /// Sets the password in the state. 28 | void setPassword(String? password) { 29 | state = state.copyWith(password: password ?? ''); 30 | } 31 | 32 | /// Submits the login form. 33 | Future submitForm( 34 | BuildContext context, GlobalKey formKey) async { 35 | if (formKey.currentState!.validate()) { 36 | formKey.currentState!.save(); 37 | 38 | state = state.copyWith(isLogging: true); 39 | 40 | final userRepository = ref.read(userRepositoryProvider); 41 | final loginRequest = LoginRequest( 42 | username: state.username, 43 | password: state.password, 44 | ); 45 | 46 | try { 47 | await userRepository.login(loginRequest); 48 | 49 | state = state.copyWith(isLogging: false); 50 | 51 | navigatorKey.currentState?.pushReplacement( 52 | MaterialPageRoute(builder: (context) => const HomeScreen()), 53 | ); 54 | } catch (error) { 55 | // Handle login error 56 | state = state.copyWith(isLogging: false); 57 | // Show error message to the user 58 | showDialog( 59 | context: context, 60 | builder: (context) { 61 | return AlertDialog( 62 | title: const Text('Error'), 63 | content: Text(error.toString()), 64 | actions: [ 65 | TextButton( 66 | child: const Text('OK'), 67 | onPressed: () { 68 | Navigator.pop(context); 69 | }, 70 | ), 71 | ], 72 | ); 73 | }, 74 | ); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/presentation/login/view_model/state/login_state.dart: -------------------------------------------------------------------------------- 1 | /// Represents the state of the login screen. 2 | class LoginState { 3 | /// The username entered by the user. 4 | final String username; 5 | 6 | /// The password entered by the user. 7 | final String password; 8 | 9 | /// Indicates whether the user is currently logging in. 10 | final bool isLogging; 11 | 12 | const LoginState({ 13 | required this.username, 14 | required this.password, 15 | required this.isLogging, 16 | }); 17 | 18 | /// Creates an initial instance of [LoginState]. 19 | factory LoginState.initial() { 20 | return const LoginState( 21 | username: '', 22 | password: '', 23 | isLogging: false, 24 | ); 25 | } 26 | 27 | /// Creates a copy of [LoginState] with the specified fields replaced with new values. 28 | LoginState copyWith({ 29 | String? username, 30 | String? password, 31 | bool? isLogging, 32 | }) { 33 | return LoginState( 34 | username: username ?? this.username, 35 | password: password ?? this.password, 36 | isLogging: isLogging ?? this.isLogging, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/presentation/my_activities/screens/activity_list_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../domain/entities/activity.dart'; 5 | import '../../../domain/entities/page.dart'; 6 | import '../../common/activity/widgets/activity_list.dart'; 7 | import '../../common/core/enums/infinite_scroll_list.enum.dart'; 8 | import '../../common/core/utils/ui_utils.dart'; 9 | import '../view_model/activity_list_view_model.dart'; 10 | 11 | /// The screen that displays a list of activities. 12 | class ActivityListScreen extends HookConsumerWidget { 13 | final activityDataFutureProvider = 14 | FutureProvider>((ref) async { 15 | final provider = ref.read(activityListViewModelProvider.notifier); 16 | return await provider.fetchActivities(); 17 | }); 18 | 19 | ActivityListScreen({super.key}); 20 | 21 | @override 22 | Widget build(BuildContext context, WidgetRef ref) { 23 | final isLoading = ref.watch(activityListViewModelProvider).isLoading; 24 | final provider = ref.watch(activityListViewModelProvider.notifier); 25 | 26 | var activityStateProvider = ref.watch(activityDataFutureProvider); 27 | 28 | return Scaffold( 29 | body: isLoading 30 | ? Center(child: UIUtils.loader) 31 | : SafeArea( 32 | child: Column( 33 | children: [ 34 | activityStateProvider.when( 35 | data: (initialData) { 36 | return ActivityList( 37 | id: InfiniteScrollListEnum.myActivities.toString(), 38 | activities: initialData.list, 39 | total: initialData.total, 40 | bottomListScrollFct: provider.fetchActivities, 41 | ); 42 | }, 43 | loading: () { 44 | return Expanded(child: Center(child: UIUtils.loader)); 45 | }, 46 | error: (error, stackTrace) { 47 | return Text('$error'); 48 | }, 49 | ) 50 | ], 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/presentation/my_activities/view_model/activity_list_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../data/repositories/activity_repository_impl.dart'; 5 | import '../../../domain/entities/activity.dart'; 6 | import '../../../domain/entities/page.dart'; 7 | import '../../../main.dart'; 8 | import '../screens/activity_details_screen.dart'; 9 | import 'state/activity_list_state.dart'; 10 | 11 | /// The provider for the activity list view model. 12 | final activityListViewModelProvider = 13 | StateNotifierProvider.autoDispose( 14 | (ref) => ActivityListViewModel(ref)); 15 | 16 | /// The view model for the activity list screen. 17 | class ActivityListViewModel extends StateNotifier { 18 | late final Ref ref; 19 | 20 | ActivityListViewModel(this.ref) : super(ActivityListState.initial()); 21 | 22 | /// Fetches the list of activities. 23 | Future> fetchActivities({int pageNumber = 0}) async { 24 | try { 25 | final newActivities = await ref 26 | .read(activityRepositoryProvider) 27 | .getActivities(pageNumber: pageNumber); 28 | return newActivities; 29 | } catch (error) { 30 | return EntityPage(list: List.empty(), total: 0); 31 | } 32 | } 33 | 34 | /// Retrieves the details of an activity. 35 | Future getActivityDetails(Activity activity) async { 36 | state = state.copyWith(isLoading: true); 37 | 38 | try { 39 | final activityDetails = await ref 40 | .read(activityRepositoryProvider) 41 | .getActivityById(id: activity.id); 42 | state = state.copyWith(isLoading: false); 43 | return activityDetails; 44 | } catch (error) { 45 | // Handle error 46 | state = state.copyWith(isLoading: false); 47 | rethrow; 48 | } 49 | } 50 | 51 | /// Navigates back to the home screen. 52 | void backToHome() { 53 | navigatorKey.currentState?.pop(); 54 | } 55 | 56 | /// Navigates to the activity details screen. 57 | void goToActivity(Activity activityDetails) { 58 | navigatorKey.currentState?.push( 59 | PageRouteBuilder( 60 | transitionDuration: const Duration(milliseconds: 500), 61 | pageBuilder: (context, animation, secondaryAnimation) => 62 | SlideTransition( 63 | position: Tween( 64 | begin: const Offset(1.0, 0.0), 65 | end: Offset.zero, 66 | ).animate(animation), 67 | child: ActivityDetailsScreen(activity: activityDetails), 68 | ), 69 | ), 70 | ); 71 | } 72 | 73 | void setIsLoading(bool isLoading) { 74 | state = state.copyWith(isLoading: isLoading); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/presentation/my_activities/view_model/state/activitie_details_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import '../../../../domain/entities/activity.dart'; 4 | import '../../../../domain/entities/enum/activity_type.dart'; 5 | 6 | /// Represents the state of the activity details screen. 7 | class ActivityDetailsState { 8 | final Activity? activity; 9 | final ActivityType? type; 10 | final bool isLoading; 11 | final bool isEditing; 12 | final GlobalKey boundaryKey; 13 | 14 | const ActivityDetailsState( 15 | {this.activity, 16 | this.type, 17 | required this.isLoading, 18 | required this.isEditing, 19 | required this.boundaryKey}); 20 | 21 | /// Creates an initial state with no activity. 22 | factory ActivityDetailsState.initial() { 23 | return ActivityDetailsState( 24 | isLoading: false, isEditing: false, boundaryKey: GlobalKey()); 25 | } 26 | 27 | /// Creates a new state with the provided activity, or retains the existing activity if not provided. 28 | ActivityDetailsState copyWith( 29 | {Activity? activity, 30 | bool? isLoading, 31 | ActivityType? type, 32 | bool? isEditing}) { 33 | return ActivityDetailsState( 34 | activity: activity ?? this.activity, 35 | isLoading: isLoading ?? this.isLoading, 36 | type: type ?? this.type, 37 | isEditing: isEditing ?? this.isEditing, 38 | boundaryKey: boundaryKey); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/presentation/my_activities/view_model/state/activity_list_state.dart: -------------------------------------------------------------------------------- 1 | /// The state class for the activity list. 2 | class ActivityListState { 3 | final bool isLoading; // Indicates if the list is currently loading 4 | 5 | const ActivityListState({required this.isLoading}); 6 | 7 | /// Factory method to create the initial state. 8 | factory ActivityListState.initial() { 9 | return const ActivityListState(isLoading: false); 10 | } 11 | 12 | /// Method to create a copy of the state with updated values. 13 | ActivityListState copyWith({ 14 | bool? isLoading, // Updated loading state 15 | }) { 16 | return ActivityListState(isLoading: isLoading ?? this.isLoading); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/presentation/my_activities/widgets/back_to_home_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../common/core/utils/color_utils.dart'; 5 | import '../view_model/activity_list_view_model.dart'; 6 | 7 | /// A floating action button widget that allows the user to navigate back to the home screen. 8 | class BackToHomeButton extends HookConsumerWidget { 9 | const BackToHomeButton({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | final provider = ref.watch(activityListViewModelProvider.notifier); 14 | 15 | return FloatingActionButton( 16 | heroTag: 'back_button', 17 | backgroundColor: ColorUtils.main, 18 | elevation: 4.0, 19 | child: Icon( 20 | Icons.arrow_back, 21 | color: ColorUtils.white, 22 | ), 23 | onPressed: () { 24 | provider.backToHome(); 25 | }, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/presentation/new_activity/screens/new_activity_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../common/location/widgets/current_location_map.dart'; 5 | import '../../common/metrics/widgets/metrics.dart'; 6 | import '../../common/timer/viewmodel/timer_view_model.dart'; 7 | import '../../common/timer/widgets/timer_pause.dart'; 8 | import '../../common/timer/widgets/timer_sized.dart'; 9 | import '../../common/timer/widgets/timer_start.dart'; 10 | 11 | /// The screen for creating a new activity. 12 | class NewActivityScreen extends HookConsumerWidget { 13 | const NewActivityScreen({super.key}); 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | final timerViewModel = ref.watch(timerViewModelProvider.notifier); 18 | // ignore: unused_local_variable 19 | final isRunning = 20 | ref.watch(timerViewModelProvider.select((value) => value.isRunning)); 21 | 22 | return Scaffold( 23 | body: SafeArea( 24 | child: Column( 25 | children: [ 26 | const TimerTextSized(), 27 | const Metrics(), 28 | const SizedBox(height: 10), 29 | CurrentLocationMap(), 30 | ], 31 | ), 32 | ), 33 | floatingActionButton: timerViewModel.hasTimerStarted() 34 | ? const Stack( 35 | children: [ 36 | Positioned( 37 | bottom: 16, 38 | right: 80, 39 | child: TimerPause(), 40 | ), 41 | Positioned( 42 | bottom: 16, 43 | left: 80, 44 | child: TimerStart(), 45 | ), 46 | ], 47 | ) 48 | : const TimerStart(), 49 | floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/presentation/new_activity/view_model/state/sum_up_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import '../../../../domain/entities/enum/activity_type.dart'; 4 | 5 | /// Represents the state of the SumUpScreen. 6 | class SumUpState { 7 | final bool isSaving; 8 | final ActivityType type; 9 | final GlobalKey boundaryKey; 10 | 11 | /// Creates a new instance of SumUpState. 12 | const SumUpState( 13 | {required this.type, required this.isSaving, required this.boundaryKey}); 14 | 15 | /// Creates an initial state with default values. 16 | factory SumUpState.initial() { 17 | return SumUpState( 18 | isSaving: false, type: ActivityType.running, boundaryKey: GlobalKey()); 19 | } 20 | 21 | /// Creates a copy of the state with optional updates. 22 | SumUpState copyWith({ 23 | bool? isSaving, 24 | ActivityType? type, 25 | }) { 26 | return SumUpState( 27 | isSaving: isSaving ?? this.isSaving, 28 | type: type ?? this.type, 29 | boundaryKey: boundaryKey); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/presentation/new_activity/widgets/save_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../common/core/utils/color_utils.dart'; 5 | import '../view_model/sum_up_view_model.dart'; 6 | 7 | /// Represents the Save button widget. 8 | class SaveButton extends HookConsumerWidget { 9 | final bool disabled; 10 | 11 | /// Creates a new instance of [SaveButton] with the given [disabled] state. 12 | const SaveButton({super.key, required this.disabled}); 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | final provider = ref.read(sumUpViewModelProvider.notifier); 17 | const animationDuration = Duration(milliseconds: 300); 18 | 19 | return AnimatedOpacity( 20 | opacity: disabled ? 0.5 : 1.0, 21 | duration: animationDuration, 22 | child: FloatingActionButton( 23 | heroTag: 'save_button', 24 | backgroundColor: ColorUtils.main, 25 | elevation: 4.0, 26 | onPressed: disabled 27 | ? null 28 | : () { 29 | provider.save(); 30 | Future.delayed(animationDuration, () { 31 | // Callback function to handle post-animation logic 32 | }); 33 | }, 34 | child: Icon( 35 | Icons.save, 36 | color: ColorUtils.white, 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/presentation/registration/view_model/registration_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../data/model/request/registration_request.dart'; 5 | import '../../../data/repositories/user_repository_impl.dart'; 6 | import '../../../main.dart'; 7 | import 'state/registration_state.dart'; 8 | 9 | final registrationViewModelProvider = 10 | StateNotifierProvider.autoDispose( 11 | (ref) => RegistrationViewModel(ref), 12 | ); 13 | 14 | class RegistrationViewModel extends StateNotifier { 15 | Ref ref; 16 | 17 | /// Creates a new instance of [RegistrationViewModel]. 18 | RegistrationViewModel(this.ref) : super(RegistrationState.initial()); 19 | 20 | /// Sets the firstname in the state. 21 | void setFirstname(String? firstname) { 22 | state = state.copyWith(firstname: firstname); 23 | } 24 | 25 | /// Sets the lastname in the state. 26 | void setLastname(String? lastname) { 27 | state = state.copyWith(lastname: lastname); 28 | } 29 | 30 | /// Sets the username in the state. 31 | void setUsername(String? username) { 32 | state = state.copyWith(username: username); 33 | } 34 | 35 | /// Sets the password in the state. 36 | void setPassword(String? password) { 37 | state = state.copyWith(password: password); 38 | } 39 | 40 | /// Sets the check password in the state. 41 | void setCheckPassword(String? checkPassword) { 42 | state = state.copyWith(checkPassword: checkPassword); 43 | } 44 | 45 | /// Submits the registration form. 46 | Future submitForm( 47 | BuildContext context, GlobalKey formKey) async { 48 | if (formKey.currentState!.validate()) { 49 | formKey.currentState!.save(); 50 | 51 | state = state.copyWith(isLogging: true); 52 | 53 | final userRepository = ref.read(userRepositoryProvider); 54 | final registrationRequest = RegistrationRequest( 55 | firstname: state.firstname, 56 | lastname: state.lastname, 57 | username: state.username, 58 | password: state.password, 59 | ); 60 | 61 | try { 62 | await userRepository.register(registrationRequest); 63 | navigatorKey.currentState?.pop(); 64 | } catch (error) { 65 | // Show error message to the user 66 | showDialog( 67 | context: context, 68 | builder: (context) { 69 | return AlertDialog( 70 | title: const Text('Error'), 71 | content: Text(error.toString()), 72 | actions: [ 73 | TextButton( 74 | child: const Text('OK'), 75 | onPressed: () { 76 | Navigator.pop(context); 77 | }, 78 | ), 79 | ], 80 | ); 81 | }, 82 | ); 83 | } finally { 84 | state = state.copyWith(isLogging: false); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/presentation/registration/view_model/state/registration_state.dart: -------------------------------------------------------------------------------- 1 | /// Represents the state of the registration screen. 2 | class RegistrationState { 3 | final String firstname; 4 | final String lastname; 5 | final String username; 6 | final String password; 7 | final String checkPassword; 8 | final bool isLogging; 9 | 10 | /// Creates a new instance of [RegistrationState]. 11 | const RegistrationState({ 12 | required this.firstname, 13 | required this.lastname, 14 | required this.username, 15 | required this.password, 16 | required this.checkPassword, 17 | required this.isLogging, 18 | }); 19 | 20 | /// Creates the initial state for the registration screen. 21 | factory RegistrationState.initial() { 22 | return const RegistrationState( 23 | firstname: '', 24 | lastname: '', 25 | username: '', 26 | password: '', 27 | checkPassword: '', 28 | isLogging: false, 29 | ); 30 | } 31 | 32 | /// Creates a copy of this state object with the specified changes. 33 | RegistrationState copyWith({ 34 | String? firstname, 35 | String? lastname, 36 | String? username, 37 | String? password, 38 | String? checkPassword, 39 | bool? isLogging, 40 | }) { 41 | return RegistrationState( 42 | firstname: firstname ?? this.firstname, 43 | lastname: lastname ?? this.lastname, 44 | username: username ?? this.username, 45 | password: password ?? this.password, 46 | checkPassword: checkPassword ?? this.checkPassword, 47 | isLogging: isLogging ?? this.isLogging, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/presentation/send_new_password/view_model/send_new_password_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../data/model/request/send_new_password_request.dart'; 5 | import '../../../data/repositories/user_repository_impl.dart'; 6 | import '../../../main.dart'; 7 | import 'state/send_new_password_state.dart'; 8 | 9 | /// Provides the view model for the send new password screen. 10 | final sendNewPasswordViewModelProvider = StateNotifierProvider.autoDispose< 11 | SendNewPasswordViewModel, SendNewPasswordState>( 12 | (ref) => SendNewPasswordViewModel(ref), 13 | ); 14 | 15 | /// The view model class for the send new password screen. 16 | class SendNewPasswordViewModel extends StateNotifier { 17 | final Ref ref; 18 | 19 | SendNewPasswordViewModel(this.ref) : super(SendNewPasswordState.initial()); 20 | 21 | /// Sets the email in the state. 22 | void setEmail(String? email) { 23 | state = state.copyWith(email: email ?? ''); 24 | } 25 | 26 | /// Submits the send mail form. 27 | Future submitForm( 28 | BuildContext context, GlobalKey formKey) async { 29 | if (formKey.currentState!.validate()) { 30 | formKey.currentState!.save(); 31 | 32 | state = state.copyWith(isSending: true); 33 | 34 | final userRepository = ref.read(userRepositoryProvider); 35 | final sendNewPasswordRequest = SendNewPasswordRequest(email: state.email); 36 | 37 | try { 38 | await userRepository.sendNewPasswordByMail(sendNewPasswordRequest); 39 | 40 | state = state.copyWith(isSending: false); 41 | 42 | navigatorKey.currentState?.pop(); 43 | } catch (error) { 44 | // Handle send mail error 45 | state = state.copyWith(isSending: false); 46 | navigatorKey.currentState?.pop(); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/presentation/send_new_password/view_model/state/send_new_password_state.dart: -------------------------------------------------------------------------------- 1 | /// Represents the state of the send new password screen. 2 | class SendNewPasswordState { 3 | /// The email entered by the user. 4 | final String email; 5 | 6 | /// Indicates whether the mail is sending 7 | final bool isSending; 8 | 9 | const SendNewPasswordState({ 10 | required this.email, 11 | required this.isSending, 12 | }); 13 | 14 | /// Creates an initial instance of [SendNewPasswordState]. 15 | factory SendNewPasswordState.initial() { 16 | return const SendNewPasswordState( 17 | email: '', 18 | isSending: false, 19 | ); 20 | } 21 | 22 | /// Creates a copy of [SendNewPasswordState] with the specified fields replaced with new values. 23 | SendNewPasswordState copyWith({ 24 | String? email, 25 | bool? isSending, 26 | }) { 27 | return SendNewPasswordState( 28 | email: email ?? this.email, 29 | isSending: isSending ?? this.isSending, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/settings/view_model/edit_password_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../../data/model/request/edit_password_request.dart'; 5 | import '../../../data/repositories/user_repository_impl.dart'; 6 | import '../../../main.dart'; 7 | import 'state/edit_password_state.dart'; 8 | 9 | final editPasswordViewModelProvider = 10 | StateNotifierProvider.autoDispose( 11 | (ref) => EditPasswordViewModel(ref), 12 | ); 13 | 14 | class EditPasswordViewModel extends StateNotifier { 15 | Ref ref; 16 | 17 | /// Creates a new instance of [EditPasswordViewModel]. 18 | EditPasswordViewModel(this.ref) : super(EditPasswordState.initial()); 19 | 20 | /// Sets the currentPassword in the state. 21 | void setCurrentPassword(String? currentPassword) { 22 | state = state.copyWith(currentPassword: currentPassword); 23 | } 24 | 25 | /// Sets the password in the state. 26 | void setPassword(String? password) { 27 | state = state.copyWith(password: password); 28 | } 29 | 30 | /// Sets the check password in the state. 31 | void setCheckPassword(String? checkPassword) { 32 | state = state.copyWith(checkPassword: checkPassword); 33 | } 34 | 35 | /// Submits the edit password form. 36 | Future submitForm( 37 | BuildContext context, GlobalKey formKey) async { 38 | state = state.copyWith(errorOnRequest: false); 39 | if (formKey.currentState!.validate()) { 40 | formKey.currentState!.save(); 41 | 42 | state = state.copyWith(isEditing: true); 43 | 44 | final userRepository = ref.read(userRepositoryProvider); 45 | final editPasswordRequest = EditPasswordRequest( 46 | currentPassword: state.currentPassword, 47 | password: state.password, 48 | ); 49 | 50 | try { 51 | await userRepository.editPassword(editPasswordRequest); 52 | navigatorKey.currentState?.pop(); 53 | } catch (e) { 54 | state = state.copyWith(errorOnRequest: true); 55 | } finally { 56 | state = state.copyWith(isEditing: false); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/presentation/settings/view_model/state/edit_password_state.dart: -------------------------------------------------------------------------------- 1 | /// Represents the state of the edit password screen. 2 | class EditPasswordState { 3 | final String currentPassword; 4 | final String password; 5 | final String checkPassword; 6 | final bool isEditing; 7 | final bool errorOnRequest; 8 | 9 | /// Creates a new instance of [EditPasswordState]. 10 | const EditPasswordState( 11 | {required this.currentPassword, 12 | required this.password, 13 | required this.checkPassword, 14 | required this.isEditing, 15 | required this.errorOnRequest}); 16 | 17 | /// Creates the initial state for the edit password screen. 18 | factory EditPasswordState.initial() { 19 | return const EditPasswordState( 20 | currentPassword: '', 21 | password: '', 22 | checkPassword: '', 23 | isEditing: false, 24 | errorOnRequest: false); 25 | } 26 | 27 | /// Creates a copy of this state object with the specified changes. 28 | EditPasswordState copyWith( 29 | {String? currentPassword, 30 | String? password, 31 | String? checkPassword, 32 | bool? isEditing, 33 | bool? errorOnRequest}) { 34 | return EditPasswordState( 35 | currentPassword: currentPassword ?? this.currentPassword, 36 | password: password ?? this.password, 37 | checkPassword: checkPassword ?? this.checkPassword, 38 | isEditing: isEditing ?? this.isEditing, 39 | errorOnRequest: errorOnRequest ?? this.errorOnRequest); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/presentation/settings/view_model/state/edit_profile_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | /// Represents the state of the edit profile screen. 4 | class EditProfileState { 5 | final String firstname; 6 | final String lastname; 7 | final Uint8List? profilePicture; 8 | final bool isEditing; 9 | final bool errorOnRequest; 10 | final bool isUploading; 11 | 12 | /// Creates a new instance of [EditProfileState]. 13 | const EditProfileState( 14 | {required this.firstname, 15 | required this.lastname, 16 | required this.profilePicture, 17 | required this.isEditing, 18 | required this.errorOnRequest, 19 | required this.isUploading}); 20 | 21 | /// Creates the initial state for the edit profile screen. 22 | factory EditProfileState.initial() { 23 | return const EditProfileState( 24 | firstname: '', 25 | lastname: '', 26 | profilePicture: null, 27 | isEditing: false, 28 | errorOnRequest: false, 29 | isUploading: false); 30 | } 31 | 32 | /// Creates a copy of this state object with the specified changes. 33 | EditProfileState copyWith( 34 | {String? firstname, 35 | String? lastname, 36 | Uint8List? profilePicture, 37 | bool? isEditing, 38 | bool? errorOnRequest, 39 | bool? isUploading}) { 40 | return EditProfileState( 41 | firstname: firstname ?? this.firstname, 42 | lastname: lastname ?? this.lastname, 43 | profilePicture: profilePicture ?? this.profilePicture, 44 | isEditing: isEditing ?? this.isEditing, 45 | errorOnRequest: errorOnRequest ?? this.errorOnRequest, 46 | isUploading: isUploading ?? this.isUploading); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/presentation/settings/view_model/state/settings_state.dart: -------------------------------------------------------------------------------- 1 | class SettingsState { 2 | final bool isLoading; 3 | 4 | /// Represents the state of the settings screen. 5 | /// 6 | /// [isLoading] indicates whether the screen is in a loading state. 7 | const SettingsState({ 8 | required this.isLoading, 9 | }); 10 | 11 | /// Creates an initial state for the settings screen. 12 | factory SettingsState.initial() { 13 | return const SettingsState( 14 | isLoading: false, 15 | ); 16 | } 17 | 18 | /// Creates a copy of this state object with the provided changes. 19 | SettingsState copyWith({ 20 | bool? isLoading, 21 | }) { 22 | return SettingsState( 23 | isLoading: isLoading ?? this.isLoading, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /screenshots/activity_list/activity_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/activity_list/activity_details.png -------------------------------------------------------------------------------- /screenshots/activity_list/activity_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/activity_list/activity_graph.png -------------------------------------------------------------------------------- /screenshots/activity_list/activity_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/activity_list/activity_list.png -------------------------------------------------------------------------------- /screenshots/community/all_activities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/community/all_activities.png -------------------------------------------------------------------------------- /screenshots/community/pending_requests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/community/pending_requests.png -------------------------------------------------------------------------------- /screenshots/community/user_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/community/user_profile.png -------------------------------------------------------------------------------- /screenshots/login_registration/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/login_registration/login.png -------------------------------------------------------------------------------- /screenshots/login_registration/registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/login_registration/registration.png -------------------------------------------------------------------------------- /screenshots/new_activity/current_activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/new_activity/current_activity.png -------------------------------------------------------------------------------- /screenshots/new_activity/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/new_activity/home.png -------------------------------------------------------------------------------- /screenshots/settings/edit_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/settings/edit_profile.png -------------------------------------------------------------------------------- /screenshots/settings/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenjaminCanape/RunFlutterRun/5b06027a75292e19b66c0b39a171ff2f1016c2e4/screenshots/settings/settings.png -------------------------------------------------------------------------------- /test/presentation/core/utils/activity_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:run_flutter_run/domain/entities/enum/activity_type.dart'; 5 | import 'package:run_flutter_run/presentation/common/core/utils/activity_utils.dart'; 6 | 7 | void main() { 8 | group('ActivityUtils', () { 9 | test('getActivityTypeIcon should return correct icons', () { 10 | expect(ActivityUtils.getActivityTypeIcon(ActivityType.running), 11 | Icons.directions_run); 12 | expect(ActivityUtils.getActivityTypeIcon(ActivityType.walking), 13 | Icons.directions_walk); 14 | expect(ActivityUtils.getActivityTypeIcon(ActivityType.cycling), 15 | Icons.directions_bike); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test/presentation/core/utils/color_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:run_flutter_run/presentation/common/core/utils/color_utils.dart'; 5 | 6 | void main() { 7 | test('generateDarkColor should return a darker color', () { 8 | const baseColor = Colors.teal; 9 | final darkColor = ColorUtils.generateDarkColor(baseColor); 10 | expect(darkColor, isNot(baseColor)); 11 | expect(darkColor, isInstanceOf()); 12 | }); 13 | 14 | test('generateLightColor should return a lighter color', () { 15 | const baseColor = Colors.teal; 16 | final lightColor = ColorUtils.generateLightColor(baseColor); 17 | expect(lightColor, isNot(baseColor)); 18 | expect(lightColor, isInstanceOf()); 19 | }); 20 | 21 | test('generateColorTupleFromIndex should return a tuple of colors', () { 22 | const index = 0; 23 | final colorTuple = ColorUtils.generateColorTupleFromIndex(index); 24 | expect(colorTuple, isList); 25 | expect(colorTuple, hasLength(2)); 26 | expect(colorTuple[0], isInstanceOf()); 27 | expect(colorTuple[1], isInstanceOf()); 28 | }); 29 | 30 | test('darker should return a darker shade of the color', () { 31 | const color = Colors.teal; 32 | final darkerColor = color.darker(); 33 | expect(darkerColor, isNot(color)); 34 | expect(darkerColor, isInstanceOf()); 35 | }); 36 | 37 | test('lighter should return a lighter shade of the color', () { 38 | const color = Colors.teal; 39 | final lighterColor = color.lighter(); 40 | expect(lighterColor, isNot(color)); 41 | expect(lighterColor, isInstanceOf()); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/presentation/core/utils/form_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:run_flutter_run/presentation/common/core/utils/color_utils.dart'; 4 | 5 | import 'package:run_flutter_run/presentation/common/core/utils/form_utils.dart'; 6 | 7 | void main() { 8 | test( 9 | 'createButtonStyle should return a ButtonStyle with the given background color', 10 | () { 11 | final backgroundColor = ColorUtils.main; 12 | final buttonStyle = FormUtils.createButtonStyle(backgroundColor); 13 | expect(buttonStyle, isInstanceOf()); 14 | expect(buttonStyle.backgroundColor?.resolve({}), 15 | equals(backgroundColor)); 16 | }); 17 | 18 | testWidgets( 19 | 'createInputDecorative should return an InputDecoration with the given parameters', 20 | (WidgetTester tester) async { 21 | const labelText = 'Label'; 22 | const dark = true; 23 | const icon = Icons.ac_unit; 24 | 25 | await tester.pumpWidget( 26 | MaterialApp( 27 | home: Scaffold( 28 | body: Builder( 29 | builder: (BuildContext context) { 30 | final inputDecoration = FormUtils.createInputDecorative(labelText, 31 | dark: dark, icon: icon); 32 | return TextField( 33 | decoration: inputDecoration, 34 | ); 35 | }, 36 | ), 37 | ), 38 | ), 39 | ); 40 | 41 | await tester.pumpAndSettle(); 42 | 43 | final textFieldFinder = find.byType(TextField); 44 | final textField = tester.widget(textFieldFinder); 45 | final inputDecoration = textField.decoration as InputDecoration; 46 | 47 | expect(inputDecoration.labelText, equals(labelText)); 48 | expect(inputDecoration.icon, isNotNull); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/presentation/core/utils/map_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:latlong2/latlong.dart'; 3 | import 'package:run_flutter_run/presentation/common/core/utils/map_utils.dart'; 4 | 5 | void main() { 6 | group('MapUtils', () { 7 | test('getCenterOfMap should return the center coordinates', () { 8 | final points = [ 9 | const LatLng(48.8566, 2.3522), // Paris 10 | const LatLng(45.75, 4.85), // Lyon 11 | const LatLng(43.2965, 5.3698), // Marseille 12 | ]; 13 | 14 | final center = MapUtils.getCenterOfMap(points); 15 | 16 | expect(center.latitude, closeTo(45.9677, 0.0001)); 17 | expect(center.longitude, closeTo(4.190666666666666, 0.0001)); 18 | }); 19 | 20 | test('getDistance should return the distance between two coordinates', () { 21 | const point1 = LatLng(48.8566, 2.3522); // Paris 22 | const point2 = LatLng(45.7579, 4.8357); // Lyon 23 | 24 | final distance = MapUtils.getDistance(point1, point2); 25 | 26 | expect(distance, closeTo(392313.0, 10)); 27 | }); 28 | 29 | test( 30 | 'getRadius should return the maximum distance from the center to any point', 31 | () { 32 | final points = [ 33 | const LatLng(48.8566, 2.3522), // Paris 34 | const LatLng(45.75, 4.85), // Lyon 35 | const LatLng(43.2965, 5.3698), // Marseille 36 | ]; 37 | const center = LatLng(45.9677, 38 | 4.190666666666666); // Center between Paris, Lyon, and Marseille 39 | 40 | final radius = MapUtils.getRadius(points, center); 41 | 42 | expect(radius, closeTo(349844.0, 0.001)); 43 | }); 44 | 45 | test('getZoomLevel should return the calculated zoom level', () { 46 | final points = [ 47 | const LatLng(48.8566, 2.3522), // Paris 48 | const LatLng(45.75, 4.85), // Lyon 49 | const LatLng(43.2965, 5.3698), // Marseille 50 | ]; 51 | const center = LatLng(45.9677, 52 | 4.190666666666666); // Center between Paris, Lyon, and Marseille 53 | 54 | final zoomLevel = MapUtils.getZoomLevel(points, center); 55 | 56 | expect(zoomLevel, closeTo(5.71, 0.01)); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/presentation/core/utils/ui_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 4 | import 'package:run_flutter_run/presentation/common/core/utils/ui_utils.dart'; 5 | 6 | void main() { 7 | testWidgets('Loader widget should render correctly', 8 | (WidgetTester tester) async { 9 | await tester.pumpWidget( 10 | MaterialApp( 11 | home: Scaffold( 12 | body: UIUtils.loader, 13 | ), 14 | ), 15 | ); 16 | 17 | final finder = find.byType(SpinKitThreeBounce); 18 | expect(finder, findsOneWidget); 19 | 20 | final spinner = tester.widget(finder); 21 | expect(spinner.color, equals(Colors.blueGrey)); 22 | expect(spinner.size, equals(50.0)); 23 | }); 24 | } 25 | --------------------------------------------------------------------------------