├── .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 | [](https://github.com/felangel/mason)
5 | [](https://github.com/invertase/melos)
6 |
7 | 
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 |
--------------------------------------------------------------------------------
/coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
197 |
--------------------------------------------------------------------------------
/lib/m_front.svg:
--------------------------------------------------------------------------------
1 |
2 |
170 |
--------------------------------------------------------------------------------
/lib/m_left.svg:
--------------------------------------------------------------------------------
1 |
2 |
142 |
--------------------------------------------------------------------------------
/lib/m_right.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 | [](https://github.com/felangel/mason)
4 | [](https://github.com/invertase/melos)
5 |
6 | A four-sided stage for representing 3D objects with four widgets
7 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------