├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── build.md │ ├── chore.md │ ├── ci.md │ ├── config.yml │ ├── documentation.md │ ├── feature_request.md │ ├── performance.md │ ├── refactor.md │ ├── revert.md │ ├── style.md │ └── test.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yaml └── workflows │ ├── main.yaml │ └── version.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── coverage-total.svg ├── coverage.svg ├── coverage └── lcov.info ├── demo.gif ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── lib │ └── main.dart └── pubspec.yaml ├── lib ├── body_part_selector.dart ├── m_back.svg ├── m_front.svg ├── m_left.svg ├── m_right.svg └── src │ ├── body_part_selector.dart │ ├── body_part_selector_turnable.dart │ ├── model │ ├── body_parts.dart │ ├── body_parts.freezed.dart │ ├── body_parts.g.dart │ └── body_side.dart │ └── service │ └── svg_service.dart ├── melos.yaml ├── packages └── rotation_stage │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── coverage.svg │ ├── coverage │ └── lcov.info │ ├── demo.gif │ ├── example │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── lib │ │ └── main.dart │ └── pubspec.yaml │ ├── lib │ ├── rotation_stage.dart │ └── src │ │ ├── model │ │ └── rotation_stage_side.dart │ │ ├── rotation_stage_bar.dart │ │ ├── rotation_stage_content.dart │ │ ├── rotation_stage_controller.dart │ │ ├── rotation_stage_handle.dart │ │ └── rotation_stage_labels.dart │ ├── pubspec.yaml │ └── test │ ├── full_coverage_test.dart │ ├── rotation_stage_test.dart │ └── src │ └── rotation_stage_controller_test.dart ├── pubspec.yaml └── test ├── full_coverage_test.dart └── src └── model └── body_parts_test.dart /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "fix: " 5 | labels: bug 6 | --- 7 | 8 | **Description** 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | **Steps To Reproduce** 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected Behavior** 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Additional Context** 28 | 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build System 3 | about: Changes that affect the build system or external dependencies 4 | title: "build: " 5 | labels: build 6 | --- 7 | 8 | **Description** 9 | 10 | Describe what changes need to be done to the build system and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] The build system is passing 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Chore 3 | about: Other changes that don't modify src or test files 4 | title: "chore: " 5 | labels: chore 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what change is needed and why. If this changes code then please use another issue type. 11 | 12 | **Requirements** 13 | 14 | - [ ] No functional changes to the code 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous Integration 3 | about: Changes to the CI configuration files and scripts 4 | title: "ci: " 5 | labels: ci 6 | --- 7 | 8 | **Description** 9 | 10 | Describe what changes need to be done to the ci/cd system and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] The ci system is passing 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Improve the documentation so all collaborators have a common understanding 4 | title: "docs: " 5 | labels: documentation 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what documentation you are looking to add or improve. 11 | 12 | **Requirements** 13 | 14 | - [ ] Requirements go here 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: A new feature to be added to the project 4 | title: "feat: " 5 | labels: feature 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to add. The more context the better. 11 | 12 | **Requirements** 13 | 14 | - [ ] Checklist of requirements to be fulfilled 15 | 16 | **Additional Context** 17 | 18 | Add any other context or screenshots about the feature request go here. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Performance Update 3 | about: A code change that improves performance 4 | title: "perf: " 5 | labels: performance 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Refactor 3 | about: A code change that neither fixes a bug nor adds a feature 4 | title: "refactor: " 5 | labels: refactor 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/revert.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Revert Commit 3 | about: Reverts a previous commit 4 | title: "revert: " 5 | labels: revert 6 | --- 7 | 8 | **Description** 9 | 10 | Provide a link to a PR/Commit that you are looking to revert and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] Change has been reverted 15 | - [ ] No change in test coverage has happened 16 | - [ ] A new ticket is created for any follow on work that needs to happen 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Style Changes 3 | about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc) 4 | title: "style: " 5 | labels: style 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to change and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | about: Adding missing tests or correcting existing tests 4 | title: "test: " 5 | labels: test 6 | --- 7 | 8 | **Description** 9 | 10 | List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Description 10 | 11 | 12 | 13 | ## Checklist 14 | 15 | 16 | - [ ] My PR title is in the style of [conventional commits](https://www.conventionalcommits.org/) 17 | - [ ] All public facing APIs are documented with [dartdoc](https://dart.dev/guides/language/effective-dart/documentation) 18 | - [ ] I have added tests to cover my changes 19 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "pub" 9 | directory: "/" 10 | labels: 11 | - dependabot 12 | schedule: 13 | interval: "daily" 14 | commit-message: 15 | prefix: chore 16 | prefix-development: chore 17 | include: scope 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | semantic_pull_request: 17 | name: Check PR Title 18 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 19 | 20 | flutter-check: 21 | name: Build Check 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 10 24 | permissions: 25 | pull-requests: write 26 | contents: write 27 | steps: 28 | - name: 📚 Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: 🐦 Setup Flutter 32 | uses: subosito/flutter-action@v2 33 | with: 34 | channel: 'stable' 35 | cache: true 36 | 37 | - name: Ⓜ️ Set up Melos 38 | uses: bluefireteam/melos-action@v2 39 | 40 | - name: 🧪 Run Analyze 41 | run: melos run analyze 42 | 43 | - name: 📝 Run Test 44 | run: melos run coverage 45 | 46 | - name: 📊 Generate Coverage 47 | id: coverage-report 48 | uses: whynotmake-it/dart-coverage-assistant@v1.1 49 | with: 50 | generate_badges: pr 51 | 52 | check_generation: 53 | name: Check Code Generation 54 | timeout-minutes: 10 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: 📚 Checkout 58 | uses: actions/checkout@v4 59 | 60 | - name: 🐦 Setup Flutter 61 | uses: subosito/flutter-action@v2 62 | with: 63 | channel: 'stable' 64 | cache: true 65 | 66 | - name: Ⓜ️ Set up Melos 67 | uses: bluefireteam/melos-action@v2 68 | 69 | - name: 🔨 Generate 70 | run: melos run generate 71 | 72 | - name: 🔎 Check there are no uncommitted changes 73 | run: git add . && git diff --exit-code 74 | -------------------------------------------------------------------------------- /.github/workflows/version.yaml: -------------------------------------------------------------------------------- 1 | name: Version 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | version: 8 | name: Version 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | steps: 14 | - name: 📚 Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: 🐦 Setup Flutter 18 | uses: subosito/flutter-action@v2 19 | with: 20 | channel: 'stable' 21 | cache: true 22 | 23 | - name: Ⓜ️ Set up Melos 24 | uses: bluefireteam/melos-action@5a8367ec4b9942d712528c398ff3f996e03bc230 25 | with: 26 | run-versioning: true 27 | publish-dry-run: true 28 | tag: true 29 | 30 | - name: 🎋 Create Pull Request 31 | uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e 32 | with: 33 | title: "chore(release): Publish packages" 34 | body: "Prepared all packages to be released to pub.dev" 35 | branch: chore/release 36 | delete-branch: true 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .mason/ 12 | migrate_working_dir/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # See https://www.dartlang.org/guides/libraries/private-files 21 | 22 | # Files and directories created by pub 23 | .dart_tool/ 24 | .packages 25 | build/ 26 | pubspec.lock 27 | pubspec_overrides.yaml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | 3 | > Note: This release has breaking changes. 4 | 5 | - **FEAT**: added `animateToSide` method to `RotationStageController`. 6 | - **BREAKING** **FEAT**: rotation stage handle are not uppercase by default anymore. 7 | 8 | ## 0.1.0 9 | 10 | > Note: This release has breaking changes. 11 | 12 | - **FIX**: fixed broken toJson(). 13 | - **DOCS**(rotation_stage): documented all public classes. 14 | - **BREAKING** **FIX**: removed `labels` parameter in `RotationStage`. 15 | - **BREAKING** **BUILD**: bump flutter version. 16 | 17 | ## 0.0.3 18 | * bump rotation_stage version 19 | * allow for custom labels 20 | 21 | ## 0.0.2 22 | * added demo GIF to README 23 | 24 | ## 0.0.1 25 | * Initial Release 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jesper Bellenbaum, Tim Lehmann, Johann Schramm 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Body Part Selector 2 | A simple and beautiful selector for body parts. 3 | 4 | [![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) 5 | [![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) 6 | 7 | ![Demo GIF](./demo.gif) 8 | 9 | 10 | ## Installation 💻 11 | 12 | **❗ In order to start using Body Part Selector you must have the [Dart SDK][dart_install_link] installed on your machine.** 13 | 14 | Install via `dart pub add`: 15 | 16 | ```sh 17 | dart pub add body_part_selector 18 | ``` 19 | 20 | ## Usage 21 | There are two widgets: `BodyPartSelector` and `BodyPartSelectorTurnable`, the latter can be seen in the GIF. 22 | 23 | Check out the example file for a simple usage pattern. 24 | 25 | ## Example 26 | To run the example open the ``example`` folder and run ``flutter create .`` 27 | 28 | --- 29 | 30 | [dart_install_link]: https://dart.dev/get-dart 31 | [github_actions_link]: https://docs.github.com/en/actions/learn-github-actions 32 | [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg 33 | [license_link]: https://opensource.org/licenses/MIT 34 | [mason_link]: https://github.com/felangel/mason 35 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lintervention/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /coverage-total.svg: -------------------------------------------------------------------------------- 1 | monorepo coverage: 64.43%monorepo coverage64.43% -------------------------------------------------------------------------------- /coverage.svg: -------------------------------------------------------------------------------- 1 | body_part_selector coverage: 40.36%body_part_selector coverage40.36% -------------------------------------------------------------------------------- /coverage/lcov.info: -------------------------------------------------------------------------------- 1 | SF:lib/src/body_part_selector.dart 2 | DA:13,0 3 | DA:74,0 4 | DA:76,0 5 | DA:77,0 6 | DA:79,0 7 | DA:85,0 8 | DA:91,0 9 | DA:92,0 10 | DA:93,0 11 | DA:97,0 12 | DA:98,0 13 | DA:99,0 14 | DA:101,0 15 | DA:102,0 16 | DA:104,0 17 | DA:105,0 18 | DA:106,0 19 | DA:109,0 20 | DA:110,0 21 | DA:111,0 22 | DA:113,0 23 | DA:123,0 24 | DA:144,0 25 | DA:145,0 26 | DA:146,0 27 | DA:152,0 28 | DA:159,0 29 | DA:160,0 30 | DA:162,0 31 | DA:165,0 32 | DA:166,0 33 | DA:167,0 34 | DA:168,0 35 | DA:169,0 36 | DA:170,0 37 | DA:172,0 38 | DA:173,0 39 | DA:174,0 40 | DA:175,0 41 | DA:176,0 42 | DA:177,0 43 | DA:178,0 44 | DA:183,0 45 | DA:185,0 46 | DA:186,0 47 | DA:187,0 48 | DA:188,0 49 | DA:191,0 50 | DA:192,0 51 | DA:193,0 52 | DA:194,0 53 | DA:195,0 54 | DA:198,0 55 | DA:200,0 56 | DA:201,0 57 | DA:202,0 58 | DA:205,0 59 | DA:207,0 60 | DA:217,0 61 | LF:59 62 | LH:0 63 | end_of_record 64 | SF:lib/src/body_part_selector_turnable.dart 65 | DA:14,0 66 | DA:56,0 67 | DA:58,0 68 | DA:59,0 69 | DA:60,0 70 | DA:61,0 71 | DA:62,0 72 | DA:63,0 73 | DA:65,0 74 | DA:66,0 75 | DA:72,0 76 | DA:73,0 77 | DA:74,0 78 | DA:75,0 79 | DA:76,0 80 | DA:77,0 81 | DA:78,0 82 | LF:17 83 | LH:0 84 | end_of_record 85 | SF:lib/src/model/body_parts.dart 86 | DA:39,1 87 | DA:40,1 88 | DA:41,4 89 | DA:76,1 90 | DA:77,1 91 | DA:78,1 92 | DA:79,2 93 | DA:81,1 94 | DA:83,2 95 | DA:84,2 96 | DA:85,1 97 | DA:87,2 98 | DA:88,2 99 | DA:91,1 100 | DA:98,1 101 | DA:99,2 102 | LF:16 103 | LH:16 104 | end_of_record 105 | SF:lib/src/model/body_parts.g.dart 106 | DA:9,2 107 | DA:10,1 108 | DA:11,1 109 | DA:12,1 110 | DA:13,1 111 | DA:14,1 112 | DA:15,1 113 | DA:16,1 114 | DA:17,1 115 | DA:18,1 116 | DA:19,1 117 | DA:20,1 118 | DA:21,1 119 | DA:22,1 120 | DA:23,1 121 | DA:24,1 122 | DA:25,1 123 | DA:26,1 124 | DA:27,1 125 | DA:28,1 126 | DA:29,1 127 | DA:30,1 128 | DA:31,1 129 | DA:32,1 130 | DA:33,1 131 | DA:36,1 132 | DA:37,1 133 | DA:38,1 134 | DA:39,1 135 | DA:40,1 136 | DA:41,1 137 | DA:42,1 138 | DA:43,1 139 | DA:44,1 140 | DA:45,1 141 | DA:46,1 142 | DA:47,1 143 | DA:48,1 144 | DA:49,1 145 | DA:50,1 146 | DA:51,1 147 | DA:52,1 148 | DA:53,1 149 | DA:54,1 150 | DA:55,1 151 | DA:56,1 152 | DA:57,1 153 | DA:58,1 154 | DA:59,1 155 | DA:60,1 156 | DA:61,1 157 | LF:51 158 | LH:51 159 | end_of_record 160 | SF:lib/src/model/body_side.dart 161 | DA:23,0 162 | DA:26,0 163 | DA:33,0 164 | DA:35,0 165 | DA:37,0 166 | DA:39,0 167 | LF:6 168 | LH:0 169 | end_of_record 170 | SF:lib/src/service/svg_service.dart 171 | DA:10,0 172 | DA:11,0 173 | DA:14,0 174 | DA:17,0 175 | DA:27,0 176 | DA:28,0 177 | DA:29,0 178 | DA:30,0 179 | DA:31,0 180 | DA:34,0 181 | DA:35,0 182 | DA:36,0 183 | DA:40,0 184 | DA:44,0 185 | DA:45,0 186 | DA:52,0 187 | DA:53,0 188 | LF:17 189 | LH:0 190 | end_of_record 191 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timcreatedit/body_part_selector/06cd5e694e56f95ad9960b384f1cd9f37bba9b6c/demo.gif -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | android/ 2 | ios/ 3 | macos/ 4 | windows/ 5 | linux/ 6 | web/ 7 | 8 | # Miscellaneous 9 | *.class 10 | *.log 11 | *.pyc 12 | *.swp 13 | .DS_Store 14 | .atom/ 15 | .buildlog/ 16 | .history 17 | .svn/ 18 | migrate_working_dir/ 19 | 20 | # IntelliJ related 21 | *.iml 22 | *.ipr 23 | *.iws 24 | .idea/ 25 | 26 | # The .vscode folder contains launch configuration and tasks you configure in 27 | # VS Code which you may wish to be included in version control, so this line 28 | # is commented out by default. 29 | #.vscode/ 30 | 31 | # Flutter/Dart/Pub related 32 | **/doc/api/ 33 | **/ios/Flutter/.last_build_id 34 | .dart_tool/ 35 | .flutter-plugins 36 | .flutter-plugins-dependencies 37 | .packages 38 | .pub-cache/ 39 | .pub/ 40 | /build/ 41 | 42 | # Web related 43 | lib/generated_plugin_registrant.dart 44 | 45 | # Symbolication related 46 | app.*.symbols 47 | 48 | # Obfuscation related 49 | app.*.map.json 50 | 51 | # Android Studio will place build artifacts here 52 | /android/app/debug 53 | /android/app/profile 54 | /android/app/release 55 | -------------------------------------------------------------------------------- /example/.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: ee4e09cce01d6f2d7f4baebd247fde02e5008851 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: ee4e09cce01d6f2d7f4baebd247fde02e5008851 17 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 18 | - platform: android 19 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 20 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 21 | - platform: ios 22 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 23 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 24 | - platform: linux 25 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 26 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 27 | - platform: macos 28 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 29 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 30 | - platform: web 31 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 32 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 33 | - platform: windows 34 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 35 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 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 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lintervention/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | public_member_api_docs: false 6 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:body_part_selector/body_part_selector.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | runApp(const MyApp()); 6 | } 7 | 8 | class MyApp extends StatelessWidget { 9 | const MyApp({super.key}); 10 | 11 | // This widget is the root of your application. 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | title: 'Body Part Selector', 16 | theme: ThemeData( 17 | useMaterial3: true, 18 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple), 19 | ), 20 | home: const MyHomePage(title: 'Body Part Selector'), 21 | ); 22 | } 23 | } 24 | 25 | class MyHomePage extends StatefulWidget { 26 | const MyHomePage({required this.title, super.key}); 27 | 28 | final String title; 29 | 30 | @override 31 | State createState() => _MyHomePageState(); 32 | } 33 | 34 | class _MyHomePageState extends State { 35 | BodyParts _bodyParts = const BodyParts(); 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Scaffold( 40 | appBar: AppBar( 41 | title: Text(widget.title), 42 | ), 43 | body: SafeArea( 44 | child: BodyPartSelectorTurnable( 45 | bodyParts: _bodyParts, 46 | onSelectionUpdated: (p) => setState(() => _bodyParts = p), 47 | labelData: const RotationStageLabelData( 48 | front: 'Vorne', 49 | left: 'Links', 50 | right: 'Rechts', 51 | back: 'Hinten', 52 | ), 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: body_part_selector_example 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' 7 | 8 | version: 1.0.0+1 9 | 10 | environment: 11 | sdk: ">=3.0.0 <4.0.0" 12 | flutter: ">=3.10.0" 13 | 14 | 15 | dependencies: 16 | body_part_selector: 17 | path: ../ 18 | flutter: 19 | sdk: flutter 20 | 21 | dev_dependencies: 22 | flutter_test: 23 | sdk: flutter 24 | lintervention: ^0.1.1 25 | -------------------------------------------------------------------------------- /lib/body_part_selector.dart: -------------------------------------------------------------------------------- 1 | /// A simple and beautiful selector for body parts. 2 | library body_part_selector; 3 | 4 | export 'src/body_part_selector.dart'; 5 | export 'src/body_part_selector_turnable.dart'; 6 | export 'src/model/body_parts.dart'; 7 | export 'src/model/body_side.dart'; 8 | -------------------------------------------------------------------------------- /lib/m_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 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 | 44 | 52 | 60 | 68 | 76 | 84 | 88 | 96 | 104 | 112 | 120 | 124 | 132 | 140 | 148 | 156 | 164 | 172 | 180 | 188 | 196 | 197 | -------------------------------------------------------------------------------- /lib/m_front.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 27 | 35 | 43 | 46 | 54 | 62 | 70 | 78 | 81 | 89 | 97 | 105 | 113 | 121 | 129 | 137 | 145 | 153 | 161 | 169 | 170 | -------------------------------------------------------------------------------- /lib/m_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 42 | 50 | 58 | 66 | 74 | 82 | 90 | 98 | 106 | 114 | 122 | 130 | 138 | 141 | 142 | -------------------------------------------------------------------------------- /lib/m_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 27 | 35 | 43 | 51 | 59 | 67 | 75 | 83 | 91 | 99 | 107 | 115 | 118 | 119 | -------------------------------------------------------------------------------- /lib/src/body_part_selector.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:body_part_selector/src/model/body_parts.dart'; 4 | import 'package:body_part_selector/src/model/body_side.dart'; 5 | import 'package:body_part_selector/src/service/svg_service.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_svg/flutter_svg.dart'; 8 | import 'package:touchable/touchable.dart'; 9 | 10 | /// A widget that allows for selecting body parts. 11 | class BodyPartSelector extends StatelessWidget { 12 | /// Creates a [BodyPartSelector]. 13 | const BodyPartSelector({ 14 | required this.bodyParts, 15 | required this.onSelectionUpdated, 16 | required this.side, 17 | this.mirrored = false, 18 | this.selectedColor, 19 | this.unselectedColor, 20 | this.selectedOutlineColor, 21 | this.unselectedOutlineColor, 22 | super.key, 23 | }); 24 | 25 | /// {@template body_part_selector.body_parts} 26 | /// The current selection of body parts 27 | /// {@endtemplate} 28 | final BodyParts bodyParts; 29 | 30 | /// The side of the body to display. 31 | final BodySide side; 32 | 33 | /// {@template body_part_selector.on_selection_updated} 34 | /// Called when the selection of body parts is updated with the new selection. 35 | /// {@endtemplate} 36 | final void Function(BodyParts bodyParts)? onSelectionUpdated; 37 | 38 | /// {@template body_part_selector.mirrored} 39 | /// Whether the selection should be mirrored, or symmetric, such that when 40 | /// selecting the left arm for example, the right arm is selected as well. 41 | /// 42 | /// Defaults to false. 43 | /// {@endtemplate} 44 | final bool mirrored; 45 | 46 | /// {@template body_part_selector.selected_color} 47 | /// The color of the selected body parts. 48 | /// 49 | /// Defaults to [ThemeData.colorScheme.inversePrimary]. 50 | /// {@endtemplate} 51 | final Color? selectedColor; 52 | 53 | /// {@template body_part_selector.unselected_color} 54 | /// The color of the unselected body parts. 55 | /// 56 | /// Defaults to [ThemeData.colorScheme.inverseSurface]. 57 | /// {@endtemplate} 58 | final Color? unselectedColor; 59 | 60 | /// {@template body_part_selector.selected_outline_color} 61 | /// The color of the outline of the selected body parts. 62 | /// 63 | /// Defaults to [ThemeData.colorScheme.primary]. 64 | /// {@endtemplate} 65 | final Color? selectedOutlineColor; 66 | 67 | /// {@template body_part_selector.unselected_outline_color} 68 | /// The color of the outline of the unselected body parts. 69 | /// 70 | /// Defaults to [ThemeData.colorScheme.onInverseSurface]. 71 | /// {@endtemplate} 72 | final Color? unselectedOutlineColor; 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | final notifier = SvgService.instance.getSide(side); 77 | return ValueListenableBuilder( 78 | valueListenable: notifier, 79 | builder: (context, value, _) { 80 | if (value == null) { 81 | return const Center( 82 | child: CircularProgressIndicator.adaptive(), 83 | ); 84 | } else { 85 | return _buildBody(context, value); 86 | } 87 | }, 88 | ); 89 | } 90 | 91 | Widget _buildBody(BuildContext context, DrawableRoot drawable) { 92 | final colorScheme = Theme.of(context).colorScheme; 93 | return AnimatedSwitcher( 94 | duration: kThemeAnimationDuration, 95 | switchInCurve: Curves.easeOutCubic, 96 | switchOutCurve: Curves.easeOutCubic, 97 | child: SizedBox.expand( 98 | key: ValueKey(bodyParts), 99 | child: CanvasTouchDetector( 100 | gesturesToOverride: const [GestureType.onTapDown], 101 | builder: (context) => CustomPaint( 102 | painter: _BodyPainter( 103 | root: drawable, 104 | bodyParts: bodyParts, 105 | onTap: (s) => onSelectionUpdated?.call( 106 | bodyParts.withToggledId(s, mirror: mirrored), 107 | ), 108 | context: context, 109 | selectedColor: selectedColor ?? colorScheme.inversePrimary, 110 | unselectedColor: unselectedColor ?? colorScheme.inverseSurface, 111 | selectedOutlineColor: selectedOutlineColor ?? colorScheme.primary, 112 | unselectedOutlineColor: 113 | unselectedOutlineColor ?? colorScheme.onInverseSurface, 114 | ), 115 | ), 116 | ), 117 | ), 118 | ); 119 | } 120 | } 121 | 122 | class _BodyPainter extends CustomPainter { 123 | _BodyPainter({ 124 | required this.root, 125 | required this.bodyParts, 126 | required this.onTap, 127 | required this.context, 128 | required this.selectedColor, 129 | required this.unselectedColor, 130 | required this.unselectedOutlineColor, 131 | required this.selectedOutlineColor, 132 | }); 133 | 134 | final DrawableRoot root; 135 | final BuildContext context; 136 | final void Function(String) onTap; 137 | final BodyParts bodyParts; 138 | final Color selectedColor; 139 | final Color unselectedColor; 140 | final Color unselectedOutlineColor; 141 | 142 | final Color selectedOutlineColor; 143 | 144 | bool isSelected(String key) { 145 | final selections = bodyParts.toMap(); 146 | if (selections.containsKey(key) && selections[key]!) { 147 | return true; 148 | } 149 | return false; 150 | } 151 | 152 | void drawBodyParts({ 153 | required TouchyCanvas touchyCanvas, 154 | required Canvas plainCanvas, 155 | required Size size, 156 | required Iterable drawables, 157 | required Matrix4 fittingMatrix, 158 | }) { 159 | for (final element in drawables) { 160 | final id = element.id; 161 | if (id == null) { 162 | debugPrint("Found a drawable element without an ID. Skipping $element"); 163 | continue; 164 | } 165 | touchyCanvas.drawPath( 166 | (element as DrawableShape).path.transform(fittingMatrix.storage), 167 | Paint() 168 | ..color = isSelected(id) ? selectedColor : unselectedColor 169 | ..style = PaintingStyle.fill, 170 | onTapDown: (_) => onTap(id), 171 | ); 172 | plainCanvas.drawPath( 173 | element.path.transform(fittingMatrix.storage), 174 | Paint() 175 | ..color = 176 | isSelected(id) ? selectedOutlineColor : unselectedOutlineColor 177 | ..strokeWidth = 2 178 | ..style = PaintingStyle.stroke, 179 | ); 180 | } 181 | } 182 | 183 | @override 184 | void paint(Canvas canvas, Size size) { 185 | if (size != root.viewport.viewBoxRect.size) { 186 | final double scale = min( 187 | size.width / root.viewport.viewBoxRect.width, 188 | size.height / root.viewport.viewBoxRect.height, 189 | ); 190 | final scaledHalfViewBoxSize = 191 | root.viewport.viewBoxRect.size * scale / 2.0; 192 | final halfDesiredSize = size / 2.0; 193 | final shift = Offset( 194 | halfDesiredSize.width - scaledHalfViewBoxSize.width, 195 | halfDesiredSize.height - scaledHalfViewBoxSize.height, 196 | ); 197 | 198 | final bodyPartsCanvas = TouchyCanvas(context, canvas); 199 | 200 | final fittingMatrix = Matrix4.identity() 201 | ..translate(shift.dx, shift.dy) 202 | ..scale(scale); 203 | 204 | final drawables = 205 | root.children.where((element) => element.hasDrawableContent); 206 | 207 | drawBodyParts( 208 | touchyCanvas: bodyPartsCanvas, 209 | plainCanvas: canvas, 210 | size: size, 211 | drawables: drawables, 212 | fittingMatrix: fittingMatrix, 213 | ); 214 | } 215 | } 216 | 217 | @override 218 | bool shouldRepaint(CustomPainter oldDelegate) => true; 219 | } 220 | -------------------------------------------------------------------------------- /lib/src/body_part_selector_turnable.dart: -------------------------------------------------------------------------------- 1 | import 'package:body_part_selector/src/body_part_selector.dart'; 2 | import 'package:body_part_selector/src/model/body_parts.dart'; 3 | import 'package:body_part_selector/src/model/body_side.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:rotation_stage/rotation_stage.dart'; 6 | 7 | export 'package:rotation_stage/rotation_stage.dart'; 8 | 9 | /// A widget that allows for selecting body parts on a turnable body. 10 | /// 11 | /// This widget is a wrapper around [RotationStage] and [BodyPartSelector]. 12 | class BodyPartSelectorTurnable extends StatelessWidget { 13 | /// Creates a [BodyPartSelectorTurnable]. 14 | const BodyPartSelectorTurnable({ 15 | required this.bodyParts, 16 | super.key, 17 | this.onSelectionUpdated, 18 | this.mirrored = false, 19 | this.selectedColor, 20 | this.unselectedColor, 21 | this.selectedOutlineColor, 22 | this.unselectedOutlineColor, 23 | this.padding = EdgeInsets.zero, 24 | this.labelData, 25 | }); 26 | 27 | /// {@macro body_part_selector.body_parts} 28 | final BodyParts bodyParts; 29 | 30 | /// {@macro body_part_selector.on_selection_updated} 31 | final ValueChanged? onSelectionUpdated; 32 | 33 | /// {@macro body_part_selector.mirrored} 34 | final bool mirrored; 35 | 36 | /// {@macro body_part_selector.selected_color} 37 | final Color? selectedColor; 38 | 39 | /// {@macro body_part_selector.unselected_color} 40 | 41 | final Color? unselectedColor; 42 | 43 | /// {@macro body_part_selector.selected_outline_color} 44 | 45 | final Color? selectedOutlineColor; 46 | 47 | /// {@macro body_part_selector.unselected_outline_color} 48 | final Color? unselectedOutlineColor; 49 | 50 | /// The padding around the rendered body. 51 | final EdgeInsets padding; 52 | 53 | /// The labels for the sides of the [RotationStage]. 54 | final RotationStageLabelData? labelData; 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return RotationStageLabels( 59 | data: labelData ?? RotationStageLabelData.english, 60 | child: RotationStage( 61 | contentBuilder: (index, side, page) => Padding( 62 | padding: padding, 63 | child: Padding( 64 | padding: const EdgeInsets.all(16), 65 | child: BodyPartSelector( 66 | side: side.map( 67 | front: BodySide.front, 68 | left: BodySide.left, 69 | back: BodySide.back, 70 | right: BodySide.right, 71 | ), 72 | bodyParts: bodyParts, 73 | onSelectionUpdated: onSelectionUpdated, 74 | mirrored: mirrored, 75 | selectedColor: selectedColor, 76 | unselectedColor: unselectedColor, 77 | selectedOutlineColor: selectedOutlineColor, 78 | unselectedOutlineColor: unselectedOutlineColor, 79 | ), 80 | ), 81 | ), 82 | ), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/model/body_parts.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'body_parts.freezed.dart'; 4 | part 'body_parts.g.dart'; 5 | 6 | /// A class representing the different parts of the body that can be selected, 7 | /// and whether they are. 8 | @freezed 9 | class BodyParts with _$BodyParts { 10 | /// Creates a new [BodyParts] object. 11 | const factory BodyParts({ 12 | @Default(false) bool head, 13 | @Default(false) bool neck, 14 | @Default(false) bool leftShoulder, 15 | @Default(false) bool leftUpperArm, 16 | @Default(false) bool leftElbow, 17 | @Default(false) bool leftLowerArm, 18 | @Default(false) bool leftHand, 19 | @Default(false) bool rightShoulder, 20 | @Default(false) bool rightUpperArm, 21 | @Default(false) bool rightElbow, 22 | @Default(false) bool rightLowerArm, 23 | @Default(false) bool rightHand, 24 | @Default(false) bool upperBody, 25 | @Default(false) bool lowerBody, 26 | @Default(false) bool leftUpperLeg, 27 | @Default(false) bool leftKnee, 28 | @Default(false) bool leftLowerLeg, 29 | @Default(false) bool leftFoot, 30 | @Default(false) bool rightUpperLeg, 31 | @Default(false) bool rightKnee, 32 | @Default(false) bool rightLowerLeg, 33 | @Default(false) bool rightFoot, 34 | @Default(false) bool abdomen, 35 | @Default(false) bool vestibular, 36 | }) = _BodyParts; 37 | 38 | /// Creates a new [BodyParts] object from a JSON object. 39 | factory BodyParts.fromJson(Map json) => 40 | _$BodyPartsFromJson(json); 41 | const BodyParts._(); 42 | 43 | /// A constant representing a selection with all [BodyParts] selected. 44 | static const all = BodyParts( 45 | head: true, 46 | neck: true, 47 | leftShoulder: true, 48 | leftUpperArm: true, 49 | leftElbow: true, 50 | leftLowerArm: true, 51 | leftHand: true, 52 | rightShoulder: true, 53 | rightUpperArm: true, 54 | rightElbow: true, 55 | rightLowerArm: true, 56 | rightHand: true, 57 | upperBody: true, 58 | lowerBody: true, 59 | leftUpperLeg: true, 60 | leftKnee: true, 61 | leftLowerLeg: true, 62 | leftFoot: true, 63 | rightUpperLeg: true, 64 | rightKnee: true, 65 | rightLowerLeg: true, 66 | rightFoot: true, 67 | abdomen: true, 68 | vestibular: true, 69 | ); 70 | 71 | /// Toggles the BodyPart with the given [id]. 72 | /// 73 | /// If [id] doesn't represent a valid BodyPart, this returns an unchanged 74 | /// Object. If [mirror] is true, and the BodyPart is one that exists on both 75 | /// sides (e.g. Knee), the other side is toggled as well. 76 | BodyParts withToggledId(String id, {bool mirror = false}) { 77 | final map = toMap(); 78 | if (!map.containsKey(id)) return this; 79 | map[id] = !(map[id] ?? false); 80 | if (mirror) { 81 | if (id.contains("left")) { 82 | final mirroredId = 83 | id.replaceAll("left", "right").replaceAll("Left", "Right"); 84 | map[mirroredId] = map[id] ?? false; 85 | } else if (id.contains("right")) { 86 | final mirroredId = 87 | id.replaceAll("right", "left").replaceAll("Right", "Left"); 88 | map[mirroredId] = map[id] ?? false; 89 | } 90 | } 91 | return BodyParts.fromJson(map); 92 | } 93 | 94 | /// Returns a Map representation of this object. 95 | /// 96 | /// Similar to [toJson], but returns a Map instead of a 97 | /// Map. 98 | Map toMap() { 99 | return toJson().cast(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/model/body_parts.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target 5 | 6 | part of 'body_parts.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 16 | 17 | BodyParts _$BodyPartsFromJson(Map json) { 18 | return _BodyParts.fromJson(json); 19 | } 20 | 21 | /// @nodoc 22 | mixin _$BodyParts { 23 | bool get head => throw _privateConstructorUsedError; 24 | bool get neck => throw _privateConstructorUsedError; 25 | bool get leftShoulder => throw _privateConstructorUsedError; 26 | bool get leftUpperArm => throw _privateConstructorUsedError; 27 | bool get leftElbow => throw _privateConstructorUsedError; 28 | bool get leftLowerArm => throw _privateConstructorUsedError; 29 | bool get leftHand => throw _privateConstructorUsedError; 30 | bool get rightShoulder => throw _privateConstructorUsedError; 31 | bool get rightUpperArm => throw _privateConstructorUsedError; 32 | bool get rightElbow => throw _privateConstructorUsedError; 33 | bool get rightLowerArm => throw _privateConstructorUsedError; 34 | bool get rightHand => throw _privateConstructorUsedError; 35 | bool get upperBody => throw _privateConstructorUsedError; 36 | bool get lowerBody => throw _privateConstructorUsedError; 37 | bool get leftUpperLeg => throw _privateConstructorUsedError; 38 | bool get leftKnee => throw _privateConstructorUsedError; 39 | bool get leftLowerLeg => throw _privateConstructorUsedError; 40 | bool get leftFoot => throw _privateConstructorUsedError; 41 | bool get rightUpperLeg => throw _privateConstructorUsedError; 42 | bool get rightKnee => throw _privateConstructorUsedError; 43 | bool get rightLowerLeg => throw _privateConstructorUsedError; 44 | bool get rightFoot => throw _privateConstructorUsedError; 45 | bool get abdomen => throw _privateConstructorUsedError; 46 | bool get vestibular => throw _privateConstructorUsedError; 47 | 48 | Map toJson() => throw _privateConstructorUsedError; 49 | @JsonKey(ignore: true) 50 | $BodyPartsCopyWith get copyWith => 51 | throw _privateConstructorUsedError; 52 | } 53 | 54 | /// @nodoc 55 | abstract class $BodyPartsCopyWith<$Res> { 56 | factory $BodyPartsCopyWith(BodyParts value, $Res Function(BodyParts) then) = 57 | _$BodyPartsCopyWithImpl<$Res>; 58 | $Res call( 59 | {bool head, 60 | bool neck, 61 | bool leftShoulder, 62 | bool leftUpperArm, 63 | bool leftElbow, 64 | bool leftLowerArm, 65 | bool leftHand, 66 | bool rightShoulder, 67 | bool rightUpperArm, 68 | bool rightElbow, 69 | bool rightLowerArm, 70 | bool rightHand, 71 | bool upperBody, 72 | bool lowerBody, 73 | bool leftUpperLeg, 74 | bool leftKnee, 75 | bool leftLowerLeg, 76 | bool leftFoot, 77 | bool rightUpperLeg, 78 | bool rightKnee, 79 | bool rightLowerLeg, 80 | bool rightFoot, 81 | bool abdomen, 82 | bool vestibular}); 83 | } 84 | 85 | /// @nodoc 86 | class _$BodyPartsCopyWithImpl<$Res> implements $BodyPartsCopyWith<$Res> { 87 | _$BodyPartsCopyWithImpl(this._value, this._then); 88 | 89 | final BodyParts _value; 90 | // ignore: unused_field 91 | final $Res Function(BodyParts) _then; 92 | 93 | @override 94 | $Res call({ 95 | Object? head = freezed, 96 | Object? neck = freezed, 97 | Object? leftShoulder = freezed, 98 | Object? leftUpperArm = freezed, 99 | Object? leftElbow = freezed, 100 | Object? leftLowerArm = freezed, 101 | Object? leftHand = freezed, 102 | Object? rightShoulder = freezed, 103 | Object? rightUpperArm = freezed, 104 | Object? rightElbow = freezed, 105 | Object? rightLowerArm = freezed, 106 | Object? rightHand = freezed, 107 | Object? upperBody = freezed, 108 | Object? lowerBody = freezed, 109 | Object? leftUpperLeg = freezed, 110 | Object? leftKnee = freezed, 111 | Object? leftLowerLeg = freezed, 112 | Object? leftFoot = freezed, 113 | Object? rightUpperLeg = freezed, 114 | Object? rightKnee = freezed, 115 | Object? rightLowerLeg = freezed, 116 | Object? rightFoot = freezed, 117 | Object? abdomen = freezed, 118 | Object? vestibular = freezed, 119 | }) { 120 | return _then(_value.copyWith( 121 | head: head == freezed 122 | ? _value.head 123 | : head // ignore: cast_nullable_to_non_nullable 124 | as bool, 125 | neck: neck == freezed 126 | ? _value.neck 127 | : neck // ignore: cast_nullable_to_non_nullable 128 | as bool, 129 | leftShoulder: leftShoulder == freezed 130 | ? _value.leftShoulder 131 | : leftShoulder // ignore: cast_nullable_to_non_nullable 132 | as bool, 133 | leftUpperArm: leftUpperArm == freezed 134 | ? _value.leftUpperArm 135 | : leftUpperArm // ignore: cast_nullable_to_non_nullable 136 | as bool, 137 | leftElbow: leftElbow == freezed 138 | ? _value.leftElbow 139 | : leftElbow // ignore: cast_nullable_to_non_nullable 140 | as bool, 141 | leftLowerArm: leftLowerArm == freezed 142 | ? _value.leftLowerArm 143 | : leftLowerArm // ignore: cast_nullable_to_non_nullable 144 | as bool, 145 | leftHand: leftHand == freezed 146 | ? _value.leftHand 147 | : leftHand // ignore: cast_nullable_to_non_nullable 148 | as bool, 149 | rightShoulder: rightShoulder == freezed 150 | ? _value.rightShoulder 151 | : rightShoulder // ignore: cast_nullable_to_non_nullable 152 | as bool, 153 | rightUpperArm: rightUpperArm == freezed 154 | ? _value.rightUpperArm 155 | : rightUpperArm // ignore: cast_nullable_to_non_nullable 156 | as bool, 157 | rightElbow: rightElbow == freezed 158 | ? _value.rightElbow 159 | : rightElbow // ignore: cast_nullable_to_non_nullable 160 | as bool, 161 | rightLowerArm: rightLowerArm == freezed 162 | ? _value.rightLowerArm 163 | : rightLowerArm // ignore: cast_nullable_to_non_nullable 164 | as bool, 165 | rightHand: rightHand == freezed 166 | ? _value.rightHand 167 | : rightHand // ignore: cast_nullable_to_non_nullable 168 | as bool, 169 | upperBody: upperBody == freezed 170 | ? _value.upperBody 171 | : upperBody // ignore: cast_nullable_to_non_nullable 172 | as bool, 173 | lowerBody: lowerBody == freezed 174 | ? _value.lowerBody 175 | : lowerBody // ignore: cast_nullable_to_non_nullable 176 | as bool, 177 | leftUpperLeg: leftUpperLeg == freezed 178 | ? _value.leftUpperLeg 179 | : leftUpperLeg // ignore: cast_nullable_to_non_nullable 180 | as bool, 181 | leftKnee: leftKnee == freezed 182 | ? _value.leftKnee 183 | : leftKnee // ignore: cast_nullable_to_non_nullable 184 | as bool, 185 | leftLowerLeg: leftLowerLeg == freezed 186 | ? _value.leftLowerLeg 187 | : leftLowerLeg // ignore: cast_nullable_to_non_nullable 188 | as bool, 189 | leftFoot: leftFoot == freezed 190 | ? _value.leftFoot 191 | : leftFoot // ignore: cast_nullable_to_non_nullable 192 | as bool, 193 | rightUpperLeg: rightUpperLeg == freezed 194 | ? _value.rightUpperLeg 195 | : rightUpperLeg // ignore: cast_nullable_to_non_nullable 196 | as bool, 197 | rightKnee: rightKnee == freezed 198 | ? _value.rightKnee 199 | : rightKnee // ignore: cast_nullable_to_non_nullable 200 | as bool, 201 | rightLowerLeg: rightLowerLeg == freezed 202 | ? _value.rightLowerLeg 203 | : rightLowerLeg // ignore: cast_nullable_to_non_nullable 204 | as bool, 205 | rightFoot: rightFoot == freezed 206 | ? _value.rightFoot 207 | : rightFoot // ignore: cast_nullable_to_non_nullable 208 | as bool, 209 | abdomen: abdomen == freezed 210 | ? _value.abdomen 211 | : abdomen // ignore: cast_nullable_to_non_nullable 212 | as bool, 213 | vestibular: vestibular == freezed 214 | ? _value.vestibular 215 | : vestibular // ignore: cast_nullable_to_non_nullable 216 | as bool, 217 | )); 218 | } 219 | } 220 | 221 | /// @nodoc 222 | abstract class _$$_BodyPartsCopyWith<$Res> implements $BodyPartsCopyWith<$Res> { 223 | factory _$$_BodyPartsCopyWith( 224 | _$_BodyParts value, $Res Function(_$_BodyParts) then) = 225 | __$$_BodyPartsCopyWithImpl<$Res>; 226 | @override 227 | $Res call( 228 | {bool head, 229 | bool neck, 230 | bool leftShoulder, 231 | bool leftUpperArm, 232 | bool leftElbow, 233 | bool leftLowerArm, 234 | bool leftHand, 235 | bool rightShoulder, 236 | bool rightUpperArm, 237 | bool rightElbow, 238 | bool rightLowerArm, 239 | bool rightHand, 240 | bool upperBody, 241 | bool lowerBody, 242 | bool leftUpperLeg, 243 | bool leftKnee, 244 | bool leftLowerLeg, 245 | bool leftFoot, 246 | bool rightUpperLeg, 247 | bool rightKnee, 248 | bool rightLowerLeg, 249 | bool rightFoot, 250 | bool abdomen, 251 | bool vestibular}); 252 | } 253 | 254 | /// @nodoc 255 | class __$$_BodyPartsCopyWithImpl<$Res> extends _$BodyPartsCopyWithImpl<$Res> 256 | implements _$$_BodyPartsCopyWith<$Res> { 257 | __$$_BodyPartsCopyWithImpl( 258 | _$_BodyParts _value, $Res Function(_$_BodyParts) _then) 259 | : super(_value, (v) => _then(v as _$_BodyParts)); 260 | 261 | @override 262 | _$_BodyParts get _value => super._value as _$_BodyParts; 263 | 264 | @override 265 | $Res call({ 266 | Object? head = freezed, 267 | Object? neck = freezed, 268 | Object? leftShoulder = freezed, 269 | Object? leftUpperArm = freezed, 270 | Object? leftElbow = freezed, 271 | Object? leftLowerArm = freezed, 272 | Object? leftHand = freezed, 273 | Object? rightShoulder = freezed, 274 | Object? rightUpperArm = freezed, 275 | Object? rightElbow = freezed, 276 | Object? rightLowerArm = freezed, 277 | Object? rightHand = freezed, 278 | Object? upperBody = freezed, 279 | Object? lowerBody = freezed, 280 | Object? leftUpperLeg = freezed, 281 | Object? leftKnee = freezed, 282 | Object? leftLowerLeg = freezed, 283 | Object? leftFoot = freezed, 284 | Object? rightUpperLeg = freezed, 285 | Object? rightKnee = freezed, 286 | Object? rightLowerLeg = freezed, 287 | Object? rightFoot = freezed, 288 | Object? abdomen = freezed, 289 | Object? vestibular = freezed, 290 | }) { 291 | return _then(_$_BodyParts( 292 | head: head == freezed 293 | ? _value.head 294 | : head // ignore: cast_nullable_to_non_nullable 295 | as bool, 296 | neck: neck == freezed 297 | ? _value.neck 298 | : neck // ignore: cast_nullable_to_non_nullable 299 | as bool, 300 | leftShoulder: leftShoulder == freezed 301 | ? _value.leftShoulder 302 | : leftShoulder // ignore: cast_nullable_to_non_nullable 303 | as bool, 304 | leftUpperArm: leftUpperArm == freezed 305 | ? _value.leftUpperArm 306 | : leftUpperArm // ignore: cast_nullable_to_non_nullable 307 | as bool, 308 | leftElbow: leftElbow == freezed 309 | ? _value.leftElbow 310 | : leftElbow // ignore: cast_nullable_to_non_nullable 311 | as bool, 312 | leftLowerArm: leftLowerArm == freezed 313 | ? _value.leftLowerArm 314 | : leftLowerArm // ignore: cast_nullable_to_non_nullable 315 | as bool, 316 | leftHand: leftHand == freezed 317 | ? _value.leftHand 318 | : leftHand // ignore: cast_nullable_to_non_nullable 319 | as bool, 320 | rightShoulder: rightShoulder == freezed 321 | ? _value.rightShoulder 322 | : rightShoulder // ignore: cast_nullable_to_non_nullable 323 | as bool, 324 | rightUpperArm: rightUpperArm == freezed 325 | ? _value.rightUpperArm 326 | : rightUpperArm // ignore: cast_nullable_to_non_nullable 327 | as bool, 328 | rightElbow: rightElbow == freezed 329 | ? _value.rightElbow 330 | : rightElbow // ignore: cast_nullable_to_non_nullable 331 | as bool, 332 | rightLowerArm: rightLowerArm == freezed 333 | ? _value.rightLowerArm 334 | : rightLowerArm // ignore: cast_nullable_to_non_nullable 335 | as bool, 336 | rightHand: rightHand == freezed 337 | ? _value.rightHand 338 | : rightHand // ignore: cast_nullable_to_non_nullable 339 | as bool, 340 | upperBody: upperBody == freezed 341 | ? _value.upperBody 342 | : upperBody // ignore: cast_nullable_to_non_nullable 343 | as bool, 344 | lowerBody: lowerBody == freezed 345 | ? _value.lowerBody 346 | : lowerBody // ignore: cast_nullable_to_non_nullable 347 | as bool, 348 | leftUpperLeg: leftUpperLeg == freezed 349 | ? _value.leftUpperLeg 350 | : leftUpperLeg // ignore: cast_nullable_to_non_nullable 351 | as bool, 352 | leftKnee: leftKnee == freezed 353 | ? _value.leftKnee 354 | : leftKnee // ignore: cast_nullable_to_non_nullable 355 | as bool, 356 | leftLowerLeg: leftLowerLeg == freezed 357 | ? _value.leftLowerLeg 358 | : leftLowerLeg // ignore: cast_nullable_to_non_nullable 359 | as bool, 360 | leftFoot: leftFoot == freezed 361 | ? _value.leftFoot 362 | : leftFoot // ignore: cast_nullable_to_non_nullable 363 | as bool, 364 | rightUpperLeg: rightUpperLeg == freezed 365 | ? _value.rightUpperLeg 366 | : rightUpperLeg // ignore: cast_nullable_to_non_nullable 367 | as bool, 368 | rightKnee: rightKnee == freezed 369 | ? _value.rightKnee 370 | : rightKnee // ignore: cast_nullable_to_non_nullable 371 | as bool, 372 | rightLowerLeg: rightLowerLeg == freezed 373 | ? _value.rightLowerLeg 374 | : rightLowerLeg // ignore: cast_nullable_to_non_nullable 375 | as bool, 376 | rightFoot: rightFoot == freezed 377 | ? _value.rightFoot 378 | : rightFoot // ignore: cast_nullable_to_non_nullable 379 | as bool, 380 | abdomen: abdomen == freezed 381 | ? _value.abdomen 382 | : abdomen // ignore: cast_nullable_to_non_nullable 383 | as bool, 384 | vestibular: vestibular == freezed 385 | ? _value.vestibular 386 | : vestibular // ignore: cast_nullable_to_non_nullable 387 | as bool, 388 | )); 389 | } 390 | } 391 | 392 | /// @nodoc 393 | @JsonSerializable() 394 | class _$_BodyParts extends _BodyParts { 395 | const _$_BodyParts( 396 | {this.head = false, 397 | this.neck = false, 398 | this.leftShoulder = false, 399 | this.leftUpperArm = false, 400 | this.leftElbow = false, 401 | this.leftLowerArm = false, 402 | this.leftHand = false, 403 | this.rightShoulder = false, 404 | this.rightUpperArm = false, 405 | this.rightElbow = false, 406 | this.rightLowerArm = false, 407 | this.rightHand = false, 408 | this.upperBody = false, 409 | this.lowerBody = false, 410 | this.leftUpperLeg = false, 411 | this.leftKnee = false, 412 | this.leftLowerLeg = false, 413 | this.leftFoot = false, 414 | this.rightUpperLeg = false, 415 | this.rightKnee = false, 416 | this.rightLowerLeg = false, 417 | this.rightFoot = false, 418 | this.abdomen = false, 419 | this.vestibular = false}) 420 | : super._(); 421 | 422 | factory _$_BodyParts.fromJson(Map json) => 423 | _$$_BodyPartsFromJson(json); 424 | 425 | @override 426 | @JsonKey() 427 | final bool head; 428 | @override 429 | @JsonKey() 430 | final bool neck; 431 | @override 432 | @JsonKey() 433 | final bool leftShoulder; 434 | @override 435 | @JsonKey() 436 | final bool leftUpperArm; 437 | @override 438 | @JsonKey() 439 | final bool leftElbow; 440 | @override 441 | @JsonKey() 442 | final bool leftLowerArm; 443 | @override 444 | @JsonKey() 445 | final bool leftHand; 446 | @override 447 | @JsonKey() 448 | final bool rightShoulder; 449 | @override 450 | @JsonKey() 451 | final bool rightUpperArm; 452 | @override 453 | @JsonKey() 454 | final bool rightElbow; 455 | @override 456 | @JsonKey() 457 | final bool rightLowerArm; 458 | @override 459 | @JsonKey() 460 | final bool rightHand; 461 | @override 462 | @JsonKey() 463 | final bool upperBody; 464 | @override 465 | @JsonKey() 466 | final bool lowerBody; 467 | @override 468 | @JsonKey() 469 | final bool leftUpperLeg; 470 | @override 471 | @JsonKey() 472 | final bool leftKnee; 473 | @override 474 | @JsonKey() 475 | final bool leftLowerLeg; 476 | @override 477 | @JsonKey() 478 | final bool leftFoot; 479 | @override 480 | @JsonKey() 481 | final bool rightUpperLeg; 482 | @override 483 | @JsonKey() 484 | final bool rightKnee; 485 | @override 486 | @JsonKey() 487 | final bool rightLowerLeg; 488 | @override 489 | @JsonKey() 490 | final bool rightFoot; 491 | @override 492 | @JsonKey() 493 | final bool abdomen; 494 | @override 495 | @JsonKey() 496 | final bool vestibular; 497 | 498 | @override 499 | String toString() { 500 | return 'BodyParts(head: $head, neck: $neck, leftShoulder: $leftShoulder, leftUpperArm: $leftUpperArm, leftElbow: $leftElbow, leftLowerArm: $leftLowerArm, leftHand: $leftHand, rightShoulder: $rightShoulder, rightUpperArm: $rightUpperArm, rightElbow: $rightElbow, rightLowerArm: $rightLowerArm, rightHand: $rightHand, upperBody: $upperBody, lowerBody: $lowerBody, leftUpperLeg: $leftUpperLeg, leftKnee: $leftKnee, leftLowerLeg: $leftLowerLeg, leftFoot: $leftFoot, rightUpperLeg: $rightUpperLeg, rightKnee: $rightKnee, rightLowerLeg: $rightLowerLeg, rightFoot: $rightFoot, abdomen: $abdomen, vestibular: $vestibular)'; 501 | } 502 | 503 | @override 504 | bool operator ==(dynamic other) { 505 | return identical(this, other) || 506 | (other.runtimeType == runtimeType && 507 | other is _$_BodyParts && 508 | const DeepCollectionEquality().equals(other.head, head) && 509 | const DeepCollectionEquality().equals(other.neck, neck) && 510 | const DeepCollectionEquality() 511 | .equals(other.leftShoulder, leftShoulder) && 512 | const DeepCollectionEquality() 513 | .equals(other.leftUpperArm, leftUpperArm) && 514 | const DeepCollectionEquality().equals(other.leftElbow, leftElbow) && 515 | const DeepCollectionEquality() 516 | .equals(other.leftLowerArm, leftLowerArm) && 517 | const DeepCollectionEquality().equals(other.leftHand, leftHand) && 518 | const DeepCollectionEquality() 519 | .equals(other.rightShoulder, rightShoulder) && 520 | const DeepCollectionEquality() 521 | .equals(other.rightUpperArm, rightUpperArm) && 522 | const DeepCollectionEquality() 523 | .equals(other.rightElbow, rightElbow) && 524 | const DeepCollectionEquality() 525 | .equals(other.rightLowerArm, rightLowerArm) && 526 | const DeepCollectionEquality().equals(other.rightHand, rightHand) && 527 | const DeepCollectionEquality().equals(other.upperBody, upperBody) && 528 | const DeepCollectionEquality().equals(other.lowerBody, lowerBody) && 529 | const DeepCollectionEquality() 530 | .equals(other.leftUpperLeg, leftUpperLeg) && 531 | const DeepCollectionEquality().equals(other.leftKnee, leftKnee) && 532 | const DeepCollectionEquality() 533 | .equals(other.leftLowerLeg, leftLowerLeg) && 534 | const DeepCollectionEquality().equals(other.leftFoot, leftFoot) && 535 | const DeepCollectionEquality() 536 | .equals(other.rightUpperLeg, rightUpperLeg) && 537 | const DeepCollectionEquality().equals(other.rightKnee, rightKnee) && 538 | const DeepCollectionEquality() 539 | .equals(other.rightLowerLeg, rightLowerLeg) && 540 | const DeepCollectionEquality().equals(other.rightFoot, rightFoot) && 541 | const DeepCollectionEquality().equals(other.abdomen, abdomen) && 542 | const DeepCollectionEquality() 543 | .equals(other.vestibular, vestibular)); 544 | } 545 | 546 | @JsonKey(ignore: true) 547 | @override 548 | int get hashCode => Object.hashAll([ 549 | runtimeType, 550 | const DeepCollectionEquality().hash(head), 551 | const DeepCollectionEquality().hash(neck), 552 | const DeepCollectionEquality().hash(leftShoulder), 553 | const DeepCollectionEquality().hash(leftUpperArm), 554 | const DeepCollectionEquality().hash(leftElbow), 555 | const DeepCollectionEquality().hash(leftLowerArm), 556 | const DeepCollectionEquality().hash(leftHand), 557 | const DeepCollectionEquality().hash(rightShoulder), 558 | const DeepCollectionEquality().hash(rightUpperArm), 559 | const DeepCollectionEquality().hash(rightElbow), 560 | const DeepCollectionEquality().hash(rightLowerArm), 561 | const DeepCollectionEquality().hash(rightHand), 562 | const DeepCollectionEquality().hash(upperBody), 563 | const DeepCollectionEquality().hash(lowerBody), 564 | const DeepCollectionEquality().hash(leftUpperLeg), 565 | const DeepCollectionEquality().hash(leftKnee), 566 | const DeepCollectionEquality().hash(leftLowerLeg), 567 | const DeepCollectionEquality().hash(leftFoot), 568 | const DeepCollectionEquality().hash(rightUpperLeg), 569 | const DeepCollectionEquality().hash(rightKnee), 570 | const DeepCollectionEquality().hash(rightLowerLeg), 571 | const DeepCollectionEquality().hash(rightFoot), 572 | const DeepCollectionEquality().hash(abdomen), 573 | const DeepCollectionEquality().hash(vestibular) 574 | ]); 575 | 576 | @JsonKey(ignore: true) 577 | @override 578 | _$$_BodyPartsCopyWith<_$_BodyParts> get copyWith => 579 | __$$_BodyPartsCopyWithImpl<_$_BodyParts>(this, _$identity); 580 | 581 | @override 582 | Map toJson() { 583 | return _$$_BodyPartsToJson(this); 584 | } 585 | } 586 | 587 | abstract class _BodyParts extends BodyParts { 588 | const factory _BodyParts( 589 | {final bool head, 590 | final bool neck, 591 | final bool leftShoulder, 592 | final bool leftUpperArm, 593 | final bool leftElbow, 594 | final bool leftLowerArm, 595 | final bool leftHand, 596 | final bool rightShoulder, 597 | final bool rightUpperArm, 598 | final bool rightElbow, 599 | final bool rightLowerArm, 600 | final bool rightHand, 601 | final bool upperBody, 602 | final bool lowerBody, 603 | final bool leftUpperLeg, 604 | final bool leftKnee, 605 | final bool leftLowerLeg, 606 | final bool leftFoot, 607 | final bool rightUpperLeg, 608 | final bool rightKnee, 609 | final bool rightLowerLeg, 610 | final bool rightFoot, 611 | final bool abdomen, 612 | final bool vestibular}) = _$_BodyParts; 613 | const _BodyParts._() : super._(); 614 | 615 | factory _BodyParts.fromJson(Map json) = 616 | _$_BodyParts.fromJson; 617 | 618 | @override 619 | bool get head => throw _privateConstructorUsedError; 620 | @override 621 | bool get neck => throw _privateConstructorUsedError; 622 | @override 623 | bool get leftShoulder => throw _privateConstructorUsedError; 624 | @override 625 | bool get leftUpperArm => throw _privateConstructorUsedError; 626 | @override 627 | bool get leftElbow => throw _privateConstructorUsedError; 628 | @override 629 | bool get leftLowerArm => throw _privateConstructorUsedError; 630 | @override 631 | bool get leftHand => throw _privateConstructorUsedError; 632 | @override 633 | bool get rightShoulder => throw _privateConstructorUsedError; 634 | @override 635 | bool get rightUpperArm => throw _privateConstructorUsedError; 636 | @override 637 | bool get rightElbow => throw _privateConstructorUsedError; 638 | @override 639 | bool get rightLowerArm => throw _privateConstructorUsedError; 640 | @override 641 | bool get rightHand => throw _privateConstructorUsedError; 642 | @override 643 | bool get upperBody => throw _privateConstructorUsedError; 644 | @override 645 | bool get lowerBody => throw _privateConstructorUsedError; 646 | @override 647 | bool get leftUpperLeg => throw _privateConstructorUsedError; 648 | @override 649 | bool get leftKnee => throw _privateConstructorUsedError; 650 | @override 651 | bool get leftLowerLeg => throw _privateConstructorUsedError; 652 | @override 653 | bool get leftFoot => throw _privateConstructorUsedError; 654 | @override 655 | bool get rightUpperLeg => throw _privateConstructorUsedError; 656 | @override 657 | bool get rightKnee => throw _privateConstructorUsedError; 658 | @override 659 | bool get rightLowerLeg => throw _privateConstructorUsedError; 660 | @override 661 | bool get rightFoot => throw _privateConstructorUsedError; 662 | @override 663 | bool get abdomen => throw _privateConstructorUsedError; 664 | @override 665 | bool get vestibular => throw _privateConstructorUsedError; 666 | @override 667 | @JsonKey(ignore: true) 668 | _$$_BodyPartsCopyWith<_$_BodyParts> get copyWith => 669 | throw _privateConstructorUsedError; 670 | } 671 | -------------------------------------------------------------------------------- /lib/src/model/body_parts.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'body_parts.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_BodyParts _$$_BodyPartsFromJson(Map json) => _$_BodyParts( 10 | head: json['head'] as bool? ?? false, 11 | neck: json['neck'] as bool? ?? false, 12 | leftShoulder: json['leftShoulder'] as bool? ?? false, 13 | leftUpperArm: json['leftUpperArm'] as bool? ?? false, 14 | leftElbow: json['leftElbow'] as bool? ?? false, 15 | leftLowerArm: json['leftLowerArm'] as bool? ?? false, 16 | leftHand: json['leftHand'] as bool? ?? false, 17 | rightShoulder: json['rightShoulder'] as bool? ?? false, 18 | rightUpperArm: json['rightUpperArm'] as bool? ?? false, 19 | rightElbow: json['rightElbow'] as bool? ?? false, 20 | rightLowerArm: json['rightLowerArm'] as bool? ?? false, 21 | rightHand: json['rightHand'] as bool? ?? false, 22 | upperBody: json['upperBody'] as bool? ?? false, 23 | lowerBody: json['lowerBody'] as bool? ?? false, 24 | leftUpperLeg: json['leftUpperLeg'] as bool? ?? false, 25 | leftKnee: json['leftKnee'] as bool? ?? false, 26 | leftLowerLeg: json['leftLowerLeg'] as bool? ?? false, 27 | leftFoot: json['leftFoot'] as bool? ?? false, 28 | rightUpperLeg: json['rightUpperLeg'] as bool? ?? false, 29 | rightKnee: json['rightKnee'] as bool? ?? false, 30 | rightLowerLeg: json['rightLowerLeg'] as bool? ?? false, 31 | rightFoot: json['rightFoot'] as bool? ?? false, 32 | abdomen: json['abdomen'] as bool? ?? false, 33 | vestibular: json['vestibular'] as bool? ?? false, 34 | ); 35 | 36 | Map _$$_BodyPartsToJson(_$_BodyParts instance) => 37 | { 38 | 'head': instance.head, 39 | 'neck': instance.neck, 40 | 'leftShoulder': instance.leftShoulder, 41 | 'leftUpperArm': instance.leftUpperArm, 42 | 'leftElbow': instance.leftElbow, 43 | 'leftLowerArm': instance.leftLowerArm, 44 | 'leftHand': instance.leftHand, 45 | 'rightShoulder': instance.rightShoulder, 46 | 'rightUpperArm': instance.rightUpperArm, 47 | 'rightElbow': instance.rightElbow, 48 | 'rightLowerArm': instance.rightLowerArm, 49 | 'rightHand': instance.rightHand, 50 | 'upperBody': instance.upperBody, 51 | 'lowerBody': instance.lowerBody, 52 | 'leftUpperLeg': instance.leftUpperLeg, 53 | 'leftKnee': instance.leftKnee, 54 | 'leftLowerLeg': instance.leftLowerLeg, 55 | 'leftFoot': instance.leftFoot, 56 | 'rightUpperLeg': instance.rightUpperLeg, 57 | 'rightKnee': instance.rightKnee, 58 | 'rightLowerLeg': instance.rightLowerLeg, 59 | 'rightFoot': instance.rightFoot, 60 | 'abdomen': instance.abdomen, 61 | 'vestibular': instance.vestibular, 62 | }; 63 | -------------------------------------------------------------------------------- /lib/src/model/body_side.dart: -------------------------------------------------------------------------------- 1 | /// Represents the side from which the body is viewed. 2 | /// 3 | /// Values are ordered as if looking at the person from the front, and them 4 | /// then rotating them clockwise, so that their left side is visible next. 5 | enum BodySide { 6 | /// The front (ventral) side of the body. 7 | /// 8 | /// As if looking the person in the face. 9 | front, 10 | 11 | /// The left (sinister) side of the body, where the person's left hand is. 12 | left, 13 | 14 | /// The back (dorsal) side of the body. 15 | /// 16 | /// As if looking at the person's back. 17 | back, 18 | 19 | /// The right (dexter) side of the body, where the person's right hand is. 20 | right; 21 | 22 | /// Returns the [BodySide] for the given index. 23 | static BodySide forIndex(int i) => values[i % values.length]; 24 | 25 | /// Maps the side to a value of type [T]. 26 | T map({ 27 | required T front, 28 | required T left, 29 | required T back, 30 | required T right, 31 | }) { 32 | switch (this) { 33 | case BodySide.front: 34 | return front; 35 | case BodySide.left: 36 | return left; 37 | case BodySide.back: 38 | return back; 39 | case BodySide.right: 40 | return right; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/service/svg_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:body_part_selector/body_part_selector.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_svg/flutter_svg.dart'; 7 | 8 | /// A singleton service that loads the SVGs for the body sides. 9 | class SvgService { 10 | SvgService._() { 11 | _init(); 12 | } 13 | 14 | static final SvgService _instance = SvgService._(); 15 | 16 | /// The singleton instance of [SvgService]. 17 | static SvgService get instance => _instance; 18 | 19 | final ValueNotifier _front = ValueNotifier(null); 20 | final ValueNotifier _left = ValueNotifier(null); 21 | final ValueNotifier _back = ValueNotifier(null); 22 | final ValueNotifier _right = ValueNotifier(null); 23 | 24 | /// The [ValueNotifier] for the given [side]. 25 | /// 26 | /// It's value is null until the SVG is loaded. 27 | ValueNotifier getSide(BodySide side) => side.map( 28 | front: _front, 29 | left: _left, 30 | back: _back, 31 | right: _right, 32 | ); 33 | 34 | Future _init() async { 35 | await Future.wait([ 36 | for (final side in BodySide.values) _loadDrawable(side, getSide(side)), 37 | ]); 38 | } 39 | 40 | Future _loadDrawable( 41 | BodySide side, 42 | ValueNotifier notifier, 43 | ) async { 44 | final svgBytes = await rootBundle.load( 45 | side.map( 46 | front: "packages/body_part_selector/m_front.svg", 47 | left: "packages/body_part_selector/m_left.svg", 48 | back: "packages/body_part_selector/m_back.svg", 49 | right: "packages/body_part_selector/m_right.svg", 50 | ), 51 | ); 52 | notifier.value = 53 | await svg.fromSvgBytes(svgBytes.buffer.asUint8List(), "svg"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: body_part_selector_workspace 2 | 3 | packages: 4 | - . 5 | - "**example/" 6 | - packages/* 7 | 8 | command: 9 | bootstrap: 10 | hooks: 11 | pre: | 12 | dart pub global activate full_coverage 13 | version: 14 | updateGitTagRefs: true 15 | workspaceChangelog: false 16 | hooks: 17 | preCommit: | 18 | melos run generate 19 | git add . 20 | 21 | scripts: 22 | analyze: 23 | run: | 24 | dart analyze . --fatal-infos 25 | exec: 26 | # We are setting the concurrency to 1 because a higher concurrency can crash 27 | # the analysis server on low performance machines (like GitHub Actions). 28 | concurrency: 1 29 | description: | 30 | Run `dart analyze` in all packages. 31 | - Note: you can also rely on your IDEs Dart Analysis / Issues window. 32 | 33 | test:select: 34 | run: flutter test 35 | exec: 36 | failFast: true 37 | concurrency: 6 38 | packageFilters: 39 | dirExists: test 40 | description: Run `flutter test test` for selected packages. 41 | 42 | test: 43 | run: melos run test:select --no-select 44 | description: Run all tests in this project. 45 | 46 | coverage:select: 47 | run: | 48 | dart pub global run full_coverage --ignore '*}.dart' 49 | flutter test --coverage 50 | exec: 51 | failFast: true 52 | concurrency: 6 53 | packageFilters: 54 | dirExists: test 55 | description: Generate coverage for the selected package. 56 | 57 | coverage: 58 | run: melos run coverage:select --no-select 59 | description: Generate coverage for all packages. 60 | 61 | generate:select: 62 | description: Run code generation for selected packages. 63 | run: dart run build_runner build --delete-conflicting-outputs 64 | exec: 65 | concurrency: 1 66 | failFast: true 67 | packageFilters: 68 | dependsOn: 69 | - build_runner 70 | 71 | generate: 72 | description: Run code generation for all packages. 73 | run: melos run generate:select --no-select -------------------------------------------------------------------------------- /packages/rotation_stage/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .mason/ 12 | migrate_working_dir/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # See https://www.dartlang.org/guides/libraries/private-files 21 | 22 | # Files and directories created by pub 23 | .dart_tool/ 24 | .packages 25 | build/ 26 | pubspec.lock 27 | pubspec_overrides.yaml -------------------------------------------------------------------------------- /packages/rotation_stage/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 2 | 3 | > Note: This release has breaking changes. 4 | 5 | - **FEAT**: added `animateToSide` method to `RotationStageController`. 6 | - **BREAKING** **FEAT**: rotation stage handle are not uppercase by default anymore. 7 | 8 | ## 0.2.0 9 | 10 | > Note: This release has breaking changes. 11 | 12 | - **DOCS**(rotation_stage): documented all public classes. 13 | - **BREAKING** **FIX**: removed `labels` parameter in `RotationStage`. 14 | 15 | ## 0.1.0 16 | * Adjusted chips for Flutter 3.3 17 | 18 | ## 0.0.9 19 | * added center to bar 20 | 21 | ## 0.0.8 22 | * removed unnecessary center from handle 23 | 24 | ## 0.0.7 25 | * smaller start page to prevent exception 26 | 27 | ## 0.0.6 28 | * lint dependency 29 | 30 | ## 0.0.5 31 | * Fixed handle color 32 | 33 | ## 0.0.4 34 | * Fixed handle color 35 | * Allow custom handle colors 36 | 37 | ## 0.0.3 38 | * Added support for localization or custom labels via inherited widget 39 | ## 0.0.2 40 | 41 | * BREAKING: Removed initial page parameter from controller constructor 42 | 43 | ## 0.0.1 44 | 45 | * Initial Release, check README for details 46 | -------------------------------------------------------------------------------- /packages/rotation_stage/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jesper Bellenbaum, Tim Lehmann, Johann Schramm 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. -------------------------------------------------------------------------------- /packages/rotation_stage/README.md: -------------------------------------------------------------------------------- 1 | # Rotation Stage 2 | 3 | [![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) 4 | [![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) 5 | 6 | A four-sided stage for representing 3D objects with four widgets 7 | ![Demo GIF](https://raw.githubusercontent.com/fyzio/rotation_stage/main/example/demo.gif) 8 | 9 | 10 | ## Installation 💻 11 | 12 | **❗ In order to start using Rotation Stage you must have the [Dart SDK][dart_install_link] installed on your machine.** 13 | 14 | Install via `dart pub add`: 15 | 16 | ```sh 17 | dart pub add rotation_stage 18 | ``` 19 | 20 | ## Usage 21 | 22 | The simplest way is to use the ``RotationStage`` widget. 23 | You only have to provide a ``contentBuilder``, everything else is preconfigured. 24 | 25 | ```dart 26 | Widget build(BuildContext context) { 27 | return RotationStage( 28 | contentBuilder: (int index, 29 | RotationStageSide side, 30 | double currentPage,) => 31 | Card( 32 | child: Padding( 33 | padding: const EdgeInsets.all(8.0), 34 | child: Text( 35 | side.map( 36 | front: "Front", 37 | left: "Left", 38 | back: "Back", 39 | right: "Right", 40 | ), 41 | ), 42 | ), 43 | ), 44 | ); 45 | } 46 | ``` 47 | 48 | You can rotate the widget by swiping on the bottom bar. The top part is purposfully not swipeable, 49 | so you can listen to whatever gestures you want there. 50 | 51 | If you want more fine-grained control, check out the other parameters of the constructor, or 52 | ``RotationStageBar``, ``RotationStageHandle`` and ``RotationStageContent``. 53 | 54 | The source code for ``RotationStage`` should be a good starting point. 55 | 56 | ## Example 57 | 58 | To run the example open the ``example`` folder and run ``flutter create .`` 59 | 60 | --- 61 | 62 | 63 | [dart_install_link]: https://dart.dev/get-dart 64 | [github_actions_link]: https://docs.github.com/en/actions/learn-github-actions 65 | [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg 66 | [license_link]: https://opensource.org/licenses/MIT 67 | [mason_link]: https://github.com/felangel/mason 68 | [very_good_ventures_link]: https://verygood.ventures 69 | -------------------------------------------------------------------------------- /packages/rotation_stage/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lintervention/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /packages/rotation_stage/coverage.svg: -------------------------------------------------------------------------------- 1 | rotation_stage coverage: 94.70%rotation_stage coverage94.70% -------------------------------------------------------------------------------- /packages/rotation_stage/coverage/lcov.info: -------------------------------------------------------------------------------- 1 | SF:lib/rotation_stage.dart 2 | DA:36,1 3 | DA:63,1 4 | DA:64,1 5 | DA:70,1 6 | DA:72,3 7 | DA:73,1 8 | DA:76,1 9 | DA:78,1 10 | DA:81,1 11 | DA:82,1 12 | DA:83,1 13 | DA:84,1 14 | DA:85,2 15 | DA:91,1 16 | DA:92,1 17 | DA:93,2 18 | DA:94,2 19 | DA:95,2 20 | DA:96,3 21 | DA:98,2 22 | DA:99,2 23 | LF:21 24 | LH:21 25 | end_of_record 26 | SF:lib/src/model/rotation_stage_side.dart 27 | DA:24,4 28 | DA:27,1 29 | DA:34,1 30 | DA:35,1 31 | DA:36,1 32 | DA:37,1 33 | LF:6 34 | LH:6 35 | end_of_record 36 | SF:lib/src/rotation_stage_bar.dart 37 | DA:9,1 38 | DA:16,2 39 | DA:17,2 40 | DA:38,1 41 | DA:40,4 42 | DA:41,1 43 | DA:42,1 44 | DA:43,1 45 | DA:44,1 46 | DA:45,2 47 | DA:46,2 48 | DA:47,1 49 | DA:48,4 50 | DA:49,3 51 | DA:50,1 52 | DA:51,1 53 | DA:52,1 54 | DA:53,1 55 | DA:55,2 56 | DA:56,2 57 | DA:58,1 58 | LF:21 59 | LH:21 60 | end_of_record 61 | SF:lib/src/rotation_stage_content.dart 62 | DA:10,1 63 | DA:22,1 64 | DA:24,1 65 | DA:25,1 66 | DA:26,1 67 | DA:27,1 68 | DA:28,2 69 | DA:29,1 70 | DA:30,1 71 | DA:31,1 72 | DA:32,2 73 | DA:33,1 74 | DA:34,1 75 | DA:35,1 76 | DA:36,1 77 | DA:37,1 78 | DA:38,3 79 | DA:39,3 80 | DA:40,1 81 | DA:41,4 82 | DA:42,2 83 | DA:43,1 84 | DA:44,1 85 | DA:45,1 86 | DA:48,2 87 | DA:50,1 88 | LF:26 89 | LH:26 90 | end_of_record 91 | SF:lib/src/rotation_stage_controller.dart 92 | DA:19,1 93 | DA:21,1 94 | DA:25,2 95 | DA:26,1 96 | DA:32,1 97 | DA:34,2 98 | DA:35,1 99 | DA:36,3 100 | DA:42,2 101 | DA:43,5 102 | DA:44,5 103 | DA:45,3 104 | DA:51,2 105 | DA:56,4 106 | DA:65,2 107 | DA:70,8 108 | DA:71,2 109 | DA:72,2 110 | DA:73,3 111 | DA:74,6 112 | DA:75,4 113 | DA:76,2 114 | DA:77,6 115 | DA:83,0 116 | DA:85,0 117 | DA:86,0 118 | LF:26 119 | LH:23 120 | end_of_record 121 | SF:lib/src/rotation_stage_handle.dart 122 | DA:15,1 123 | DA:59,1 124 | DA:61,2 125 | DA:62,1 126 | DA:63,2 127 | DA:64,1 128 | DA:66,3 129 | DA:67,1 130 | DA:69,4 131 | DA:70,1 132 | DA:71,2 133 | DA:72,2 134 | DA:75,1 135 | DA:78,2 136 | DA:79,1 137 | DA:81,2 138 | DA:82,1 139 | DA:84,2 140 | DA:85,1 141 | DA:87,2 142 | LF:20 143 | LH:20 144 | end_of_record 145 | SF:lib/src/rotation_stage_labels.dart 146 | DA:7,3 147 | DA:35,2 148 | DA:36,1 149 | DA:37,1 150 | DA:38,1 151 | DA:39,1 152 | DA:48,0 153 | DA:60,1 154 | DA:62,1 155 | DA:63,0 156 | DA:66,0 157 | DA:68,0 158 | LF:12 159 | LH:8 160 | end_of_record 161 | -------------------------------------------------------------------------------- /packages/rotation_stage/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timcreatedit/body_part_selector/06cd5e694e56f95ad9960b384f1cd9f37bba9b6c/packages/rotation_stage/demo.gif -------------------------------------------------------------------------------- /packages/rotation_stage/example/.gitignore: -------------------------------------------------------------------------------- 1 | android/ 2 | ios/ 3 | macos/ 4 | windows/ 5 | linux/ 6 | web/ 7 | 8 | # Miscellaneous 9 | *.class 10 | *.log 11 | *.pyc 12 | *.swp 13 | .DS_Store 14 | .atom/ 15 | .buildlog/ 16 | .history 17 | .svn/ 18 | migrate_working_dir/ 19 | 20 | # IntelliJ related 21 | *.iml 22 | *.ipr 23 | *.iws 24 | .idea/ 25 | 26 | # The .vscode folder contains launch configuration and tasks you configure in 27 | # VS Code which you may wish to be included in version control, so this line 28 | # is commented out by default. 29 | #.vscode/ 30 | 31 | # Flutter/Dart/Pub related 32 | **/doc/api/ 33 | **/ios/Flutter/.last_build_id 34 | .dart_tool/ 35 | .flutter-plugins 36 | .flutter-plugins-dependencies 37 | .packages 38 | .pub-cache/ 39 | .pub/ 40 | /build/ 41 | 42 | # Web related 43 | lib/generated_plugin_registrant.dart 44 | 45 | # Symbolication related 46 | app.*.symbols 47 | 48 | # Obfuscation related 49 | app.*.map.json 50 | 51 | # Android Studio will place build artifacts here 52 | /android/app/debug 53 | /android/app/profile 54 | /android/app/release 55 | -------------------------------------------------------------------------------- /packages/rotation_stage/example/.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: ee4e09cce01d6f2d7f4baebd247fde02e5008851 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: ee4e09cce01d6f2d7f4baebd247fde02e5008851 17 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 18 | - platform: android 19 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 20 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 21 | - platform: ios 22 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 23 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 24 | - platform: linux 25 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 26 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 27 | - platform: macos 28 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 29 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 30 | - platform: web 31 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 32 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 33 | - platform: windows 34 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 35 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 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 | -------------------------------------------------------------------------------- /packages/rotation_stage/example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /packages/rotation_stage/example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lintervention/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | public_member_api_docs: false 6 | -------------------------------------------------------------------------------- /packages/rotation_stage/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rotation_stage/rotation_stage.dart'; 3 | 4 | void main() { 5 | runApp(const MyApp()); 6 | } 7 | 8 | class MyApp extends StatelessWidget { 9 | const MyApp({super.key}); 10 | 11 | // This widget is the root of your application. 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | title: 'Rotation Stage Example', 16 | theme: ThemeData( 17 | useMaterial3: true, 18 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), 19 | ), 20 | home: const MyHomePage(title: 'Rotation Stage'), 21 | ); 22 | } 23 | } 24 | 25 | class MyHomePage extends StatefulWidget { 26 | const MyHomePage({required this.title, super.key}); 27 | 28 | final String title; 29 | 30 | @override 31 | State createState() => _MyHomePageState(); 32 | } 33 | 34 | class _MyHomePageState extends State { 35 | @override 36 | Widget build(BuildContext context) { 37 | return Scaffold( 38 | appBar: AppBar( 39 | title: Text(widget.title), 40 | ), 41 | body: SafeArea( 42 | child: RotationStage( 43 | contentBuilder: ( 44 | int index, 45 | RotationStageSide side, 46 | double currentPage, 47 | ) => 48 | Padding( 49 | padding: const EdgeInsets.all(64), 50 | child: SizedBox.expand( 51 | child: Card( 52 | elevation: 0, 53 | color: Theme.of(context).colorScheme.inverseSurface, 54 | child: Center( 55 | child: Card( 56 | color: Colors.white, 57 | child: Padding( 58 | padding: const EdgeInsets.all(8), 59 | child: Text( 60 | side.map( 61 | front: "Front", 62 | left: "Left", 63 | back: "Back", 64 | right: "Right", 65 | ), 66 | ), 67 | ), 68 | ), 69 | ), 70 | ), 71 | ), 72 | ), 73 | ), 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/rotation_stage/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rotation_stage_example 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' 7 | 8 | version: 1.0.0+1 9 | 10 | environment: 11 | sdk: ">=3.0.0 <4.0.0" 12 | flutter: ">=3.10.0" 13 | 14 | 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | rotation_stage: 19 | path: ../ 20 | 21 | dev_dependencies: 22 | flutter_test: 23 | sdk: flutter 24 | lintervention: ^0.1.1 25 | -------------------------------------------------------------------------------- /packages/rotation_stage/lib/rotation_stage.dart: -------------------------------------------------------------------------------- 1 | /// A four-sided stage for representing 3D objects with four widgets 2 | library rotation_stage; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:rotation_stage/rotation_stage.dart'; 6 | 7 | export 'src/model/rotation_stage_side.dart'; 8 | export 'src/rotation_stage_bar.dart'; 9 | export 'src/rotation_stage_content.dart'; 10 | export 'src/rotation_stage_controller.dart'; 11 | export 'src/rotation_stage_handle.dart'; 12 | export 'src/rotation_stage_labels.dart'; 13 | 14 | /// The builder function for one side of the [RotationStage]. 15 | /// 16 | /// Takes the [index] of the side, the [side] itself, and the [currentPage] of 17 | /// the stage. The returned widget should be a representation of the side 18 | /// denoted by [side] and [index]. 19 | /// [currentPage] is passed to allow for building custom effects based on the 20 | /// current scroll position, since it can also fall bewtween two pages. 21 | typedef RotationStageBuilder = Widget Function( 22 | int index, 23 | RotationStageSide side, 24 | double currentPage, 25 | ); 26 | 27 | /// A widget that allows for rotating a widget with four sides in pseudo-3D, 28 | /// with a bar of handles for switching between the sides. 29 | /// 30 | /// Combines a [RotationStageContent] with a [RotationStageBar] and optional 31 | /// labels for the sides. 32 | /// 33 | /// {@macro rotation_stage_handle.labels} 34 | class RotationStage extends StatefulWidget { 35 | /// Creates a [RotationStage]. 36 | const RotationStage({ 37 | required this.contentBuilder, 38 | this.controller, 39 | this.viewHandleBuilder, 40 | this.barHeight = 64, 41 | this.barInteractable = true, 42 | super.key, 43 | }); 44 | 45 | /// The builder function for the content of the [RotationStage]. 46 | final RotationStageBuilder contentBuilder; 47 | 48 | /// The controller for the [RotationStage]. 49 | /// 50 | /// If not provided, a new controller will be created and disposed 51 | /// when the stage is disposed. 52 | final RotationStageController? controller; 53 | 54 | /// The builder function for the handles of the [RotationStage]. 55 | final RotationStageBuilder? viewHandleBuilder; 56 | 57 | /// The height of the bottom bar in logical pixels. 58 | final double barHeight; 59 | 60 | /// Whether the bar is interactable at the moment. 61 | final bool barInteractable; 62 | 63 | @override 64 | State createState() => _RotationStageState(); 65 | } 66 | 67 | class _RotationStageState extends State { 68 | late final RotationStageController _controller; 69 | 70 | @override 71 | void initState() { 72 | _controller = widget.controller ?? RotationStageController(); 73 | super.initState(); 74 | } 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | return Column( 79 | mainAxisSize: MainAxisSize.min, 80 | crossAxisAlignment: CrossAxisAlignment.stretch, 81 | children: [ 82 | Expanded( 83 | child: RotationStageContent( 84 | controller: _controller, 85 | contentBuilder: widget.contentBuilder, 86 | ), 87 | ), 88 | const Divider( 89 | height: 1, 90 | ), 91 | RotationStageBar( 92 | controller: _controller, 93 | interactable: widget.barInteractable, 94 | viewHandleBuilder: widget.viewHandleBuilder ?? 95 | (index, side, page) => RotationStageHandle( 96 | onTap: () => _controller.animateToPage(index), 97 | side: side, 98 | active: index == page.round(), 99 | backgroundTransparent: !widget.barInteractable, 100 | ), 101 | ), 102 | ], 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/rotation_stage/lib/src/model/rotation_stage_side.dart: -------------------------------------------------------------------------------- 1 | import 'package:rotation_stage/rotation_stage.dart'; 2 | 3 | /// Represents one of the four sides of the [RotationStage]. 4 | /// 5 | /// Values are ordered as if rotating the stage from left to right when looking 6 | /// at it from the front. 7 | enum RotationStageSide { 8 | /// The front side of the [RotationStage]. 9 | front, 10 | 11 | /// The left side of the [RotationStage]. 12 | left, 13 | 14 | /// The back side of the [RotationStage]. 15 | back, 16 | 17 | /// The right side of the [RotationStage]. 18 | right; 19 | 20 | /// Returns the [RotationStageSide] for the given index. 21 | /// 22 | /// The index is wrapped around the number of values in the enum, and the 23 | /// order is the same as the order of the values in the enum. 24 | static RotationStageSide forIndex(int i) => values[i % values.length]; 25 | 26 | /// Maps the side to a value of type [T]. 27 | T map({ 28 | required T front, 29 | required T left, 30 | required T back, 31 | required T right, 32 | }) { 33 | return switch (this) { 34 | RotationStageSide.front => front, 35 | RotationStageSide.left => left, 36 | RotationStageSide.back => back, 37 | RotationStageSide.right => right, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/rotation_stage/lib/src/rotation_stage_bar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:rotation_stage/rotation_stage.dart'; 5 | 6 | /// A bar that displays the handles for the [RotationStage]. 7 | class RotationStageBar extends StatelessWidget { 8 | /// Creates a [RotationStageBar]. 9 | const RotationStageBar({ 10 | required this.controller, 11 | required this.viewHandleBuilder, 12 | this.height = kToolbarHeight, 13 | this.interactable = true, 14 | this.minHandleOpacity = 0, 15 | super.key, 16 | }) : assert(minHandleOpacity >= 0, 'minHandleOpacity must be >= 0'), 17 | assert(minHandleOpacity <= 1, 'minHandleOpacity must be <= 1'); 18 | 19 | /// The controller for the [RotationStage]. 20 | final RotationStageController controller; 21 | 22 | /// The builder function for the handles of the [RotationStage]. 23 | final RotationStageBuilder viewHandleBuilder; 24 | 25 | /// Whether the bar is interactable at the moment. 26 | final bool interactable; 27 | 28 | /// The height of the bar in logical pixels. 29 | /// 30 | /// Defaults to [kToolbarHeight]. 31 | final double height; 32 | 33 | /// The minimum opacity of the handles when they are not visible. 34 | /// 35 | /// Must be in the range [0, 1] and defaults to 0. 36 | final double minHandleOpacity; 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | final visOffset = 0.5 / controller.pageController.viewportFraction; 41 | return SizedBox( 42 | height: height, 43 | child: ValueListenableBuilder( 44 | valueListenable: controller, 45 | builder: (context, page, _) => PageView.builder( 46 | controller: controller.pageController, 47 | itemBuilder: (context, index) { 48 | final offset = (page - index).abs().clamp(0, visOffset) / visOffset; 49 | final opacity = lerpDouble(minHandleOpacity, 1, 1 - offset); 50 | return Center( 51 | child: Opacity( 52 | opacity: Curves.ease.transform(opacity!), 53 | child: AnimatedOpacity( 54 | duration: kThemeAnimationDuration, 55 | opacity: interactable && index != index ? 0 : 1, 56 | child: viewHandleBuilder( 57 | index, 58 | RotationStageSide.forIndex(index), 59 | page, 60 | ), 61 | ), 62 | ), 63 | ); 64 | }, 65 | ), 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/rotation_stage/lib/src/rotation_stage_content.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:rotation_stage/rotation_stage.dart'; 5 | 6 | /// A widget that displays the content of the [RotationStage] and applies the 7 | /// visual transformations to the sides when roataing the stage. 8 | class RotationStageContent extends StatelessWidget { 9 | /// Creates a [RotationStageContent]. 10 | const RotationStageContent({ 11 | required this.controller, 12 | required this.contentBuilder, 13 | super.key, 14 | }); 15 | 16 | /// The controller for the [RotationStage]. 17 | final RotationStageController controller; 18 | 19 | /// The builder function for the content of the [RotationStage]. 20 | final RotationStageBuilder contentBuilder; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return ValueListenableBuilder( 25 | valueListenable: controller, 26 | builder: (context, page, _) { 27 | final index = page.round(); 28 | final betweenPages = page % 1 > 0; 29 | return Stack( 30 | children: [ 31 | for (int i = (betweenPages ? index - 1 : index); 32 | i < index + (betweenPages ? 2 : 1); 33 | i++) 34 | IgnorePointer( 35 | ignoring: i != index, 36 | child: Builder( 37 | builder: (context) { 38 | final diff = page < 3 || i != 0 ? (i - page) : (4 - page); 39 | final opacity = (1 - diff.abs()).clamp(0.0, 1.0); 40 | final cMatrix = Matrix4.identity() 41 | ..rotateY(-diff * pi / 2) 42 | ..setEntry(3, 0, 0.001 * diff); 43 | return Opacity( 44 | opacity: Curves.easeOutExpo.transform(opacity), 45 | child: Transform( 46 | transform: cMatrix, 47 | alignment: FractionalOffset.center, 48 | child: contentBuilder( 49 | i, 50 | RotationStageSide.forIndex(i), 51 | page, 52 | ), 53 | ), 54 | ); 55 | }, 56 | ), 57 | ), 58 | ], 59 | ); 60 | }, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/rotation_stage/lib/src/rotation_stage_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rotation_stage/rotation_stage.dart'; 3 | 4 | /// A workaround to achieve pseudo-infinite scroll with a default Flutter 5 | /// [PageController]. 6 | /// 7 | /// While scrolling forward is infinite, scrolling backwards is limited to the 8 | /// first page. Thus, the first page is set to [kInfiniteScrollStartPage] to 9 | /// allow for a large number of pages to be scrolled through in either 10 | /// direction. 11 | const int kInfiniteScrollStartPage = 500; 12 | 13 | /// A controller for the [RotationStage]. 14 | /// 15 | /// Wraps a [PageController] and provides a [ValueNotifier] for the current page 16 | /// of the [RotationStage]. 17 | class RotationStageController extends ValueNotifier { 18 | /// Creates a [RotationStageController]. 19 | RotationStageController({ 20 | double viewportFraction = 0.2, 21 | }) : pageController = PageController( 22 | initialPage: kInfiniteScrollStartPage, 23 | viewportFraction: viewportFraction, 24 | ), 25 | super(kInfiniteScrollStartPage.toDouble()) { 26 | _addPageControllerListener(); 27 | } 28 | 29 | /// Creates a [RotationStageController] with a custom [PageController]. 30 | /// 31 | /// This constructor is intended for testing purposes only. 32 | @visibleForTesting 33 | RotationStageController.customPageController(this.pageController) 34 | : super(kInfiniteScrollStartPage.toDouble()) { 35 | _addPageControllerListener(); 36 | pageController.jumpTo(kInfiniteScrollStartPage.toDouble()); 37 | } 38 | 39 | /// The [PageController] instance backing this controller. 40 | final PageController pageController; 41 | 42 | void _addPageControllerListener() { 43 | pageController.addListener(() { 44 | if (pageController.positions.isNotEmpty && pageController.page != null) { 45 | value = pageController.page!; 46 | } 47 | }); 48 | } 49 | 50 | /// Animates the [RotationStage] to the given page. 51 | void animateToPage( 52 | int page, { 53 | Duration duration = kThemeAnimationDuration, 54 | Curve curve = Curves.ease, 55 | }) { 56 | pageController.animateToPage( 57 | page, 58 | duration: duration, 59 | curve: curve, 60 | ); 61 | } 62 | 63 | /// Animates the [RotationStage] to the closest page that corresponds to the 64 | /// given [side]. 65 | void animateToSide( 66 | RotationStageSide side, { 67 | Duration duration = kThemeAnimationDuration, 68 | Curve curve = Curves.ease, 69 | }) { 70 | final currentIndex = (value % RotationStageSide.values.length).round(); 71 | final targetIndex = side.index; 72 | final difference = targetIndex - currentIndex; 73 | final shortestWay = difference > 2 ? difference - 4 : difference; 74 | final targetPage = value.round() + shortestWay; 75 | if (targetPage == value) return; 76 | animateToPage( 77 | value.round() + shortestWay, 78 | duration: duration, 79 | curve: curve, 80 | ); 81 | } 82 | 83 | @override 84 | void dispose() { 85 | pageController.dispose(); 86 | super.dispose(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/rotation_stage/lib/src/rotation_stage_handle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rotation_stage/rotation_stage.dart'; 3 | 4 | /// A handle for the [RotationStage] that represents one side of the stage. 5 | /// 6 | /// {@template rotation_stage_handle.labels} 7 | /// The handles will obtain their label from the [RotationStageLabels] in the 8 | /// widget tree, and if there is none, fall back to english labels. 9 | /// 10 | /// If you want to customize the labels, wrap the [RotationStage] in a 11 | /// [RotationStageLabels] widget with the desired labels. 12 | /// {@endtemplate} 13 | class RotationStageHandle extends StatelessWidget { 14 | /// Creates a [RotationStageHandle]. 15 | const RotationStageHandle({ 16 | required this.side, 17 | required this.active, 18 | required this.onTap, 19 | required this.backgroundTransparent, 20 | this.activeForegroundColor, 21 | this.inactiveForegroundColor, 22 | this.activeBackgroundColor, 23 | this.inactiveBackgroundColor, 24 | super.key, 25 | }); 26 | 27 | /// The [RotationStageSide] to represent. 28 | final RotationStageSide side; 29 | 30 | /// Whether this handle is active (the side is currently visible). 31 | final bool active; 32 | 33 | /// Whether the background of the handle is transparent. 34 | final bool backgroundTransparent; 35 | 36 | /// The callback to call when the handle is tapped. 37 | final VoidCallback onTap; 38 | 39 | /// The color of the foreground when the handle is active. 40 | /// 41 | /// Defaults to [ThemeData.colorScheme.onPrimary]. 42 | final Color? activeForegroundColor; 43 | 44 | /// The color of the foreground when the handle is inactive. 45 | /// 46 | /// Defaults to [ThemeData.colorScheme.onPrimaryContainer]. 47 | final Color? inactiveForegroundColor; 48 | 49 | /// The color of the background when the handle is active. 50 | /// 51 | /// Defaults to [ThemeData.colorScheme.primary]. 52 | final Color? activeBackgroundColor; 53 | 54 | /// The color of the background when the handle is inactive. 55 | /// 56 | /// Defaults to [ThemeData.colorScheme.primaryContainer]. 57 | final Color? inactiveBackgroundColor; 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | final colorScheme = Theme.of(context).colorScheme; 62 | final labels = RotationStageLabels.of(context); 63 | final name = labels.getForSide(side); 64 | return RawChip( 65 | showCheckmark: false, 66 | onSelected: (_) => onTap(), 67 | label: Text( 68 | name, 69 | style: Theme.of(context).textTheme.labelLarge?.copyWith( 70 | color: active 71 | ? activeForegroundColor ?? colorScheme.onPrimary 72 | : inactiveForegroundColor ?? colorScheme.onPrimaryContainer, 73 | ), 74 | ), 75 | selected: active, 76 | disabledColor: Colors.transparent, 77 | shadowColor: 78 | backgroundTransparent ? Colors.transparent : colorScheme.shadow, 79 | selectedShadowColor: backgroundTransparent 80 | ? Colors.transparent 81 | : activeBackgroundColor ?? colorScheme.primary, 82 | backgroundColor: backgroundTransparent 83 | ? Colors.transparent 84 | : inactiveBackgroundColor ?? colorScheme.primaryContainer, 85 | selectedColor: backgroundTransparent 86 | ? Colors.transparent 87 | : activeBackgroundColor ?? colorScheme.primary, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/rotation_stage/lib/src/rotation_stage_labels.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:rotation_stage/rotation_stage.dart'; 3 | 4 | /// Holds the labels for each [RotationStageSide]. 5 | class RotationStageLabelData { 6 | /// Creates a [RotationStageLabelData]. 7 | const RotationStageLabelData({ 8 | required this.front, 9 | required this.left, 10 | required this.right, 11 | required this.back, 12 | }); 13 | 14 | /// The default English labels for the sides of the [RotationStage]. 15 | static const english = RotationStageLabelData( 16 | front: "Front", 17 | left: "Left", 18 | right: "Right", 19 | back: "Back", 20 | ); 21 | 22 | /// The label for the front side. 23 | final String front; 24 | 25 | /// The label for the left side. 26 | final String left; 27 | 28 | /// The label for the right side. 29 | final String right; 30 | 31 | /// The label for the back side. 32 | final String back; 33 | 34 | /// Returns the label for the given [side]. 35 | String getForSide(RotationStageSide side) => side.map( 36 | front: front, 37 | left: left, 38 | back: back, 39 | right: right, 40 | ); 41 | } 42 | 43 | /// An [InheritedWidget] that holds the [RotationStageLabelData] for the 44 | /// [RotationStage] and provides them to the widgets below it in the widget 45 | /// tree. 46 | class RotationStageLabels extends InheritedWidget { 47 | /// Creates a [RotationStageLabels]. 48 | const RotationStageLabels({ 49 | required this.data, 50 | required super.child, 51 | super.key, 52 | }); 53 | 54 | /// The data for the labels. 55 | final RotationStageLabelData data; 56 | 57 | /// Returns the [RotationStageLabelData] for the [RotationStage] from the 58 | /// [context], or falls back to [RotationStageLabelData.english], if none 59 | /// are found. 60 | static RotationStageLabelData of(BuildContext context) { 61 | final result = 62 | context.dependOnInheritedWidgetOfExactType(); 63 | return result?.data ?? RotationStageLabelData.english; 64 | } 65 | 66 | @override 67 | bool updateShouldNotify(RotationStageLabels oldWidget) { 68 | return oldWidget.data != data; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/rotation_stage/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rotation_stage 2 | description: A four-sided stage for representing 3D objects with four widgets 3 | version: 0.3.0 4 | homepage: https://www.whynotmake.it 5 | repository: https://github.com/timcreatedit/body_part_selector/tree/main/packages/rotation_stage 6 | 7 | environment: 8 | sdk: ">=3.0.0 <4.0.0" 9 | flutter: ">=3.10.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | lintervention: ^0.1.1 19 | 20 | mocktail: ^1.0.3 21 | -------------------------------------------------------------------------------- /packages/rotation_stage/test/full_coverage_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unused_import 2 | import 'package:rotation_stage/rotation_stage.dart'; 3 | import 'package:rotation_stage/src/model/rotation_stage_side.dart'; 4 | import 'package:rotation_stage/src/rotation_stage_bar.dart'; 5 | import 'package:rotation_stage/src/rotation_stage_content.dart'; 6 | import 'package:rotation_stage/src/rotation_stage_controller.dart'; 7 | import 'package:rotation_stage/src/rotation_stage_handle.dart'; 8 | import 'package:rotation_stage/src/rotation_stage_labels.dart'; 9 | 10 | void main() {} 11 | -------------------------------------------------------------------------------- /packages/rotation_stage/test/rotation_stage_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:rotation_stage/rotation_stage.dart'; 4 | 5 | void main() { 6 | const sideColors = { 7 | RotationStageSide.front: Colors.red, 8 | RotationStageSide.back: Colors.green, 9 | RotationStageSide.left: Colors.blue, 10 | RotationStageSide.right: Colors.yellow, 11 | }; 12 | 13 | group('RotationStage', () { 14 | late RotationStageController controller; 15 | 16 | // ignore: prefer_function_declarations_over_variables 17 | final RotationStageBuilder contentBuilder = (index, side, currentPage) { 18 | return Container( 19 | color: sideColors[side], 20 | key: ValueKey(side), 21 | ); 22 | }; 23 | 24 | Widget build(RotationStage rotationStage) { 25 | return MaterialApp( 26 | home: Scaffold( 27 | body: rotationStage, 28 | ), 29 | ); 30 | } 31 | 32 | Widget buildDefault() { 33 | return build( 34 | RotationStage( 35 | contentBuilder: contentBuilder, 36 | controller: controller, 37 | ), 38 | ); 39 | } 40 | 41 | Finder findSide(RotationStageSide side) { 42 | return find.byKey(ValueKey(side)); 43 | } 44 | 45 | setUp(() { 46 | controller = RotationStageController(); 47 | }); 48 | 49 | group('Content', () { 50 | testWidgets( 51 | 'shows front side by default', 52 | (tester) async { 53 | await tester.pumpWidget(buildDefault()); 54 | await tester.pumpAndSettle(); 55 | final front = findSide(RotationStageSide.front); 56 | expect(front, findsOneWidget); 57 | expect( 58 | front.hitTestable(), 59 | findsOneWidget, 60 | reason: "Front side is hit-testable", 61 | ); 62 | }, 63 | ); 64 | 65 | testWidgets('other sides are not in widget tree', (tester) async { 66 | await tester.pumpWidget(buildDefault()); 67 | await tester.pumpAndSettle(); 68 | final back = findSide(RotationStageSide.back); 69 | final left = findSide(RotationStageSide.left); 70 | final right = findSide(RotationStageSide.right); 71 | expect(back, findsNothing); 72 | expect(left, findsNothing); 73 | expect(right, findsNothing); 74 | }); 75 | 76 | testWidgets('animates to other sides', (tester) async { 77 | await tester.pumpWidget(buildDefault()); 78 | await tester.pumpAndSettle(); 79 | 80 | for (final side in RotationStageSide.values) { 81 | controller.animateToSide(side); 82 | await tester.pumpAndSettle(); 83 | final foundSide = findSide(side); 84 | expect(foundSide, findsOneWidget); 85 | expect( 86 | foundSide.hitTestable(), 87 | findsOneWidget, 88 | reason: "$side side is hit-testable", 89 | ); 90 | for (final otherSide in RotationStageSide.values) { 91 | if (otherSide != side) { 92 | expect( 93 | findSide(otherSide), 94 | findsNothing, 95 | reason: "$otherSide side is not in the widget tree when $side " 96 | "is shown", 97 | ); 98 | } 99 | } 100 | } 101 | }); 102 | }); 103 | 104 | group('Rotation Stage Bar', () { 105 | const labels = RotationStageLabelData.english; 106 | 107 | testWidgets('shows bar with handles', (tester) async { 108 | await tester.pumpWidget(buildDefault()); 109 | await tester.pumpAndSettle(); 110 | final bar = find.byType(RotationStageBar); 111 | expect(bar, findsOneWidget); 112 | final handles = find.byType(RotationStageHandle); 113 | expect(handles, findsAtLeast(3)); 114 | }); 115 | 116 | testWidgets('front handle is active and centered by default', 117 | (tester) async { 118 | await tester.pumpWidget(buildDefault()); 119 | await tester.pumpAndSettle(); 120 | final handle = find.handleByText(labels.front); 121 | expect(handle, findsOneWidget); 122 | 123 | expect(tester.widget(handle).active, isTrue); 124 | expect( 125 | tester.getCenter(handle).dx, 126 | tester.getCenter(find.byType(MaterialApp)).dx, 127 | ); 128 | }); 129 | 130 | testWidgets('tapping handles changes side', (tester) async { 131 | await tester.pumpWidget(buildDefault()); 132 | await tester.pumpAndSettle(); 133 | 134 | for (final side in RotationStageSide.values) { 135 | final handle = find.handleByText(labels.getForSide(side)); 136 | await tester.tap(handle); 137 | await tester.pumpAndSettle(); 138 | expect( 139 | findSide(side), 140 | findsOneWidget, 141 | reason: "$side side is shown after tapping its handle", 142 | ); 143 | expect( 144 | tester.firstWidget(handle).active, 145 | isTrue, 146 | reason: "$side handle is active after tapping", 147 | ); 148 | } 149 | }); 150 | }); 151 | }); 152 | } 153 | 154 | extension on CommonFinders { 155 | Finder handleByText(String text) { 156 | return find.ancestor( 157 | of: find.text(text), 158 | matching: find.byType(RotationStageHandle), 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /packages/rotation_stage/test/src/rotation_stage_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:rotation_stage/rotation_stage.dart'; 5 | 6 | class _MockPageController extends Mock implements PageController {} 7 | 8 | void main() { 9 | group('RotationStageController', () { 10 | late _MockPageController pageController; 11 | late RotationStageController sut; 12 | 13 | setUp(() { 14 | pageController = _MockPageController(); 15 | registerFallbackValue(Duration.zero); 16 | registerFallbackValue(Curves.linear); 17 | when( 18 | () => pageController.animateToPage( 19 | any(), 20 | duration: any(named: 'duration'), 21 | curve: any(named: 'curve'), 22 | ), 23 | ).thenAnswer((_) async {}); 24 | sut = RotationStageController.customPageController(pageController); 25 | }); 26 | 27 | group('animateToPage', () { 28 | test('calls animateToPage on pageController with default values', () { 29 | sut.animateToPage(1); 30 | verify( 31 | () => pageController.animateToPage( 32 | 1, 33 | duration: kThemeAnimationDuration, 34 | curve: Curves.ease, 35 | ), 36 | ); 37 | }); 38 | 39 | test('forwards parameters', () async { 40 | sut.animateToPage( 41 | 1, 42 | duration: const Duration(seconds: 1), 43 | curve: Curves.easeInOut, 44 | ); 45 | verify( 46 | () => pageController.animateToPage( 47 | 1, 48 | duration: const Duration(seconds: 1), 49 | curve: Curves.easeInOut, 50 | ), 51 | ); 52 | }); 53 | }); 54 | 55 | group('animateToSide', () { 56 | void verifyAnimateToPageCalledWith(int page) { 57 | verify( 58 | () => pageController.animateToPage( 59 | page, 60 | duration: kThemeAnimationDuration, 61 | curve: Curves.ease, 62 | ), 63 | ); 64 | } 65 | 66 | test('calls animateToPage correctly for left', () { 67 | sut.animateToSide(RotationStageSide.left); 68 | verifyAnimateToPageCalledWith(kInfiniteScrollStartPage + 1); 69 | }); 70 | 71 | test('calls animateToPage correctly for back', () { 72 | sut.animateToSide(RotationStageSide.back); 73 | verifyAnimateToPageCalledWith(kInfiniteScrollStartPage + 2); 74 | }); 75 | 76 | test('calls animateToPage correctly for right', () { 77 | sut.animateToSide(RotationStageSide.right); 78 | verifyAnimateToPageCalledWith(kInfiniteScrollStartPage - 1); 79 | }); 80 | 81 | test('does not call animate to page for current side', () { 82 | sut.animateToSide(RotationStageSide.front); 83 | verifyNever( 84 | () => pageController.animateToPage( 85 | any(), 86 | duration: any(named: 'duration'), 87 | curve: any(named: 'curve'), 88 | ), 89 | ); 90 | }); 91 | }); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: body_part_selector 2 | description: A beautiful selector for different body parts 3 | version: 0.2.0 4 | homepage: https://www.whynotmake.it 5 | repository: https://github.com/timcreatedit/body_part_selector 6 | 7 | environment: 8 | sdk: ">=3.0.0 <4.0.0" 9 | flutter: ">=3.10.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | flutter_svg: ">=1.1.0 <2.0.0" # https://github.com/dnfield/flutter_svg/issues/969 15 | freezed_annotation: ">=2.4.0 <3.0.0" 16 | rotation_stage: ^0.3.0 17 | touchable: ^1.0.2 18 | 19 | dev_dependencies: 20 | flutter_test: 21 | sdk: flutter 22 | freezed: ">=2.4.0 <3.0.0" 23 | json_serializable: ">=6.8.0 <7.0.0" 24 | lintervention: ^0.1.1 25 | melos: ^6.0.0 26 | mocktail: ^1.0.3 27 | 28 | flutter: 29 | assets: 30 | - packages/body_part_selector/m_front.svg 31 | - packages/body_part_selector/m_left.svg 32 | - packages/body_part_selector/m_back.svg 33 | - packages/body_part_selector/m_right.svg 34 | -------------------------------------------------------------------------------- /test/full_coverage_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unused_import 2 | import 'package:body_part_selector/body_part_selector.dart'; 3 | import 'package:body_part_selector/src/body_part_selector.dart'; 4 | import 'package:body_part_selector/src/body_part_selector_turnable.dart'; 5 | import 'package:body_part_selector/src/model/body_parts.dart'; 6 | import 'package:body_part_selector/src/model/body_side.dart'; 7 | import 'package:body_part_selector/src/service/svg_service.dart'; 8 | 9 | void main() {} 10 | -------------------------------------------------------------------------------- /test/src/model/body_parts_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:body_part_selector/src/model/body_parts.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | group("BodyParts", () { 6 | group('.all', () { 7 | test("Equals all", () { 8 | // ignore: use_named_constants 9 | const bp = BodyParts( 10 | head: true, 11 | neck: true, 12 | upperBody: true, 13 | abdomen: true, 14 | vestibular: true, 15 | leftElbow: true, 16 | leftFoot: true, 17 | leftHand: true, 18 | leftKnee: true, 19 | leftLowerArm: true, 20 | leftLowerLeg: true, 21 | leftShoulder: true, 22 | leftUpperArm: true, 23 | leftUpperLeg: true, 24 | lowerBody: true, 25 | rightElbow: true, 26 | rightFoot: true, 27 | rightHand: true, 28 | rightKnee: true, 29 | rightLowerArm: true, 30 | rightLowerLeg: true, 31 | rightShoulder: true, 32 | rightUpperArm: true, 33 | rightUpperLeg: true, 34 | ); 35 | expect(bp, BodyParts.all); 36 | }); 37 | 38 | test("Doesn't equal any off", () { 39 | const bp = BodyParts.all; 40 | for (final key in bp.toMap().keys) { 41 | final bp = BodyParts.all.withToggledId(key); 42 | expect(bp, isNot(BodyParts.all)); 43 | } 44 | expect(bp, BodyParts.all); 45 | }); 46 | }); 47 | 48 | group('.withToggledId', () { 49 | void testIdToggle(String id) { 50 | const bp = BodyParts(); 51 | expect( 52 | bp.toJson()[id], 53 | false, 54 | reason: id, 55 | ); 56 | expect( 57 | bp.withToggledId(id).toJson()[id], 58 | true, 59 | reason: id, 60 | ); 61 | expect( 62 | bp.withToggledId(id).withToggledId(id).toJson()[id], 63 | false, 64 | reason: id, 65 | ); 66 | expect( 67 | bp.withToggledId(id, mirror: true).toJson()[id], 68 | true, 69 | reason: id, 70 | ); 71 | expect( 72 | bp 73 | .withToggledId(id, mirror: true) 74 | .withToggledId(id, mirror: true) 75 | .toJson()[id], 76 | false, 77 | reason: id, 78 | ); 79 | } 80 | 81 | test("Toggling by symmetric IDs works", () { 82 | const ids = ["head", "neck", "upperBody", "abdomen", "vestibular"]; 83 | for (final id in ids) { 84 | testIdToggle(id); 85 | } 86 | }); 87 | 88 | test("Toggling by asymmetric IDs works", () { 89 | const ids = [ 90 | "Shoulder", 91 | "UpperArm", 92 | "Elbow", 93 | "LowerArm", 94 | "Hand", 95 | "UpperLeg", 96 | "Knee", 97 | "LowerLeg", 98 | "Foot", 99 | ]; 100 | 101 | void testIds(String leftId, String rightId) { 102 | testIdToggle(leftId); 103 | testIdToggle(rightId); 104 | const bp = BodyParts(); 105 | 106 | expect( 107 | bp.withToggledId(leftId).toJson()[rightId], 108 | false, 109 | reason: "$leftId on, should leave off $rightId", 110 | ); 111 | expect( 112 | bp.withToggledId(leftId, mirror: true).toJson()[rightId], 113 | true, 114 | reason: "$leftId on mirrored, should turn on $rightId", 115 | ); 116 | expect( 117 | bp.withToggledId(rightId).toJson()[leftId], 118 | false, 119 | reason: "$rightId on, should leave off $leftId", 120 | ); 121 | expect( 122 | bp.withToggledId(rightId, mirror: true).toJson()[leftId], 123 | true, 124 | reason: "$rightId on mirrored, should turn on $leftId", 125 | ); 126 | expect( 127 | bp 128 | .withToggledId(rightId, mirror: true) 129 | .withToggledId(rightId, mirror: true) 130 | .toJson()[leftId], 131 | false, 132 | reason: "$rightId on and off mirrored, should leave off $leftId", 133 | ); 134 | expect( 135 | bp 136 | .withToggledId(leftId, mirror: true) 137 | .withToggledId(leftId, mirror: true) 138 | .toJson()[rightId], 139 | false, 140 | reason: "$leftId on and off mirrored, should leave off $rightId", 141 | ); 142 | expect( 143 | bp 144 | .withToggledId(leftId) 145 | .withToggledId(rightId, mirror: true) 146 | .toJson()[rightId], 147 | true, 148 | reason: 149 | "$leftId on, then $rightId on mirrored should turn on $rightId", 150 | ); 151 | expect( 152 | bp 153 | .withToggledId(leftId) 154 | .withToggledId(leftId, mirror: true) 155 | .toJson()[rightId], 156 | false, 157 | reason: "$leftId on, then $leftId off mirrored, should leave off " 158 | "$rightId", 159 | ); 160 | expect( 161 | bp 162 | .withToggledId(rightId) 163 | .withToggledId(leftId) 164 | .withToggledId(leftId, mirror: true) 165 | .toJson()[rightId], 166 | false, 167 | reason: "$rightId, then $leftId on, then $leftId mirrored off " 168 | "should turn off $rightId", 169 | ); 170 | } 171 | 172 | for (final partId in ids) { 173 | final leftId = "left$partId"; 174 | final rightId = "right$partId"; 175 | testIds(leftId, rightId); 176 | } 177 | }); 178 | }); 179 | }); 180 | } 181 | --------------------------------------------------------------------------------