diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..50a4c7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: "fix: " +labels: bug +--- + +**Description** + +A clear and concise description of what the bug is. + +**Steps To Reproduce** + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected Behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Additional Context** + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/build.md b/.github/ISSUE_TEMPLATE/build.md new file mode 100644 index 0000000..0cf8e62 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/build.md @@ -0,0 +1,14 @@ +--- +name: Build System +about: Changes that affect the build system or external dependencies +title: "build: " +labels: build +--- + +**Description** + +Describe what changes need to be done to the build system and why. + +**Requirements** + +- [ ] The build system is passing diff --git a/.github/ISSUE_TEMPLATE/chore.md b/.github/ISSUE_TEMPLATE/chore.md new file mode 100644 index 0000000..498ebfd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/chore.md @@ -0,0 +1,14 @@ +--- +name: Chore +about: Other changes that don't modify src or test files +title: "chore: " +labels: chore +--- + +**Description** + +Clearly describe what change is needed and why. If this changes code then please use another issue type. + +**Requirements** + +- [ ] No functional changes to the code diff --git a/.github/ISSUE_TEMPLATE/ci.md b/.github/ISSUE_TEMPLATE/ci.md new file mode 100644 index 0000000..fa2dd9e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ci.md @@ -0,0 +1,14 @@ +--- +name: Continuous Integration +about: Changes to the CI configuration files and scripts +title: "ci: " +labels: ci +--- + +**Description** + +Describe what changes need to be done to the ci/cd system and why. + +**Requirements** + +- [ ] The ci system is passing diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ec4bb38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..f494a4d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,14 @@ +--- +name: Documentation +about: Improve the documentation so all collaborators have a common understanding +title: "docs: " +labels: documentation +--- + +**Description** + +Clearly describe what documentation you are looking to add or improve. + +**Requirements** + +- [ ] Requirements go here diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ddd2fcc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature Request +about: A new feature to be added to the project +title: "feat: " +labels: feature +--- + +**Description** + +Clearly describe what you are looking to add. The more context the better. + +**Requirements** + +- [ ] Checklist of requirements to be fulfilled + +**Additional Context** + +Add any other context or screenshots about the feature request go here. diff --git a/.github/ISSUE_TEMPLATE/performance.md b/.github/ISSUE_TEMPLATE/performance.md new file mode 100644 index 0000000..699b8d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance.md @@ -0,0 +1,14 @@ +--- +name: Performance Update +about: A code change that improves performance +title: "perf: " +labels: performance +--- + +**Description** + +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. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/.github/ISSUE_TEMPLATE/refactor.md b/.github/ISSUE_TEMPLATE/refactor.md new file mode 100644 index 0000000..1626c57 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor.md @@ -0,0 +1,14 @@ +--- +name: Refactor +about: A code change that neither fixes a bug nor adds a feature +title: "refactor: " +labels: refactor +--- + +**Description** + +Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/.github/ISSUE_TEMPLATE/revert.md b/.github/ISSUE_TEMPLATE/revert.md new file mode 100644 index 0000000..9d121dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/revert.md @@ -0,0 +1,16 @@ +--- +name: Revert Commit +about: Reverts a previous commit +title: "revert: " +labels: revert +--- + +**Description** + +Provide a link to a PR/Commit that you are looking to revert and why. + +**Requirements** + +- [ ] Change has been reverted +- [ ] No change in test coverage has happened +- [ ] A new ticket is created for any follow on work that needs to happen diff --git a/.github/ISSUE_TEMPLATE/style.md b/.github/ISSUE_TEMPLATE/style.md new file mode 100644 index 0000000..02244a7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/style.md @@ -0,0 +1,14 @@ +--- +name: Style Changes +about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc) +title: "style: " +labels: style +--- + +**Description** + +Clearly describe what you are looking to change and why. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/.github/ISSUE_TEMPLATE/test.md b/.github/ISSUE_TEMPLATE/test.md new file mode 100644 index 0000000..431a7ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test.md @@ -0,0 +1,14 @@ +--- +name: Test +about: Adding missing tests or correcting existing tests +title: "test: " +labels: test +--- + +**Description** + +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. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..931c1de --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ + + +## Description + + + +## Checklist + + +- [ ] My PR title is in the style of [conventional commits](https://www.conventionalcommits.org/) +- [ ] All public facing APIs are documented with [dartdoc](https://dart.dev/guides/language/effective-dart/documentation) +- [ ] I have added tests to cover my changes diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..0030a0e --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,17 @@ +version: 2 +enable-beta-ecosystems: true +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "pub" + directory: "/" + labels: + - dependabot + schedule: + interval: "daily" + commit-message: + prefix: chore + prefix-development: chore + include: scope diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..ed8d615 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,73 @@ +name: CI + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + semantic_pull_request: + name: Check PR Title + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 + + flutter-check: + name: Build Check + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + pull-requests: write + steps: + - name: ๐Ÿ“š Checkout + uses: actions/checkout@v4 + + - name: ๐Ÿฆ Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + cache: true + + - name: โ“‚๏ธ Set up Melos + uses: bluefireteam/melos-action@v2 + + - name: ๐Ÿงช Run Analyze + run: melos run analyze + + - name: ๐Ÿ“ Run Test + run: melos run coverage + + - name: ๐Ÿ“Š Generate Coverage + id: coverage-report + uses: whynotmake-it/dart-coverage-assistant@v1 + with: + generate_badges: pr + + check_generation: + name: Check Code Generation + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ“š Checkout + uses: actions/checkout@v4 + + - name: ๐Ÿฆ Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + cache: true + + - name: โ“‚๏ธ Set up Melos + uses: bluefireteam/melos-action@v2 + + - name: ๐Ÿ”จ Generate + run: melos run generate + + - name: ๐Ÿ”Ž Check there are no uncommitted changes + run: git diff --exit-code + \ No newline at end of file diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml new file mode 100644 index 0000000..ad4f8f7 --- /dev/null +++ b/.github/workflows/version.yaml @@ -0,0 +1,35 @@ +name: Version + +on: + workflow_dispatch: + +jobs: + version: + name: Version + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: ๐Ÿ“š Checkout + uses: actions/checkout@v4 + + - name: ๐ŸŽฏ Setup Dart + uses: dart-lang/setup-dart@v1.6.4 + with: + sdk: "stable" + + - name: โ“‚๏ธ Set up Melos + uses: bluefireteam/melos-action@5a8367ec4b9942d712528c398ff3f996e03bc230 + with: + run-versioning: true + publish-dry-run: true + tag: true + + - name: ๐ŸŽ‹ Create Pull Request + uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 + with: + title: "chore(release): Publish packages" + body: "Prepared all packages to be released to pub.dev" + branch: chore/release + delete-branch: true diff --git a/.gitignore b/.gitignore index 96486fd..8318291 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.mason/ migrate_working_dir/ # IntelliJ related @@ -16,15 +17,11 @@ migrate_working_dir/ *.iws .idea/ -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ +# See https://www.dartlang.org/guides/libraries/private-files -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ +# Files and directories created by pub .dart_tool/ .packages build/ +pubspec.lock +pubspec_overrides.yaml \ No newline at end of file diff --git a/.metadata b/.metadata deleted file mode 100644 index c79dee4..0000000 --- a/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - channel: stable - -project_type: package diff --git a/CHANGELOG.md b/CHANGELOG.md index c049801..54cc80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,3 @@ -## 0.0.3 -* bump rotation_stage version -* allow for custom labels +## 0.1.0 -## 0.0.2 -* added demo GIF to README -## 0.0.1 - -* Initial Release +- feat: initial commit ๐ŸŽ‰ diff --git a/README.md b/README.md index 5ff3918..4f3637b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,34 @@ +# Body Part Selector A simple and beautiful selector for body parts. -![Demo GIF](https://raw.githubusercontent.com/fyzio/body_part_selector/main/example/demo.gif) +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) + +![Demo GIF](./demo.gif) + + +## Installation ๐Ÿ’ป + +**โ— In order to start using Body Part Selector you must have the [Dart SDK][dart_install_link] installed on your machine.** + +Install via `dart pub add`: + +```sh +dart pub add body_part_selector +``` ## Usage -There are two widgets: `BodyPartSelector` and `BodyPartSelectorTurnable`, the latter one can be seen in the GIF. +There are two widgets: `BodyPartSelector` and `BodyPartSelectorTurnable`, the latter can be seen in the GIF. Check out the example file for a simple usage pattern. + ## Example +To run the example open the ``example`` folder and run ``flutter create .`` + +--- -To run the example open the ``example`` folder and run ``flutter create .`` \ No newline at end of file +[dart_install_link]: https://dart.dev/get-dart +[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[mason_link]: https://github.com/felangel/mason diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..245ed9f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +include: package:lintervention/analysis_options.yaml diff --git a/example/demo.gif b/demo.gif similarity index 100% rename from example/demo.gif rename to demo.gif diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 61b6c4d..4106b14 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,29 +1,5 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. +include: package:lintervention/analysis_options.yaml -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. +linter: rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + public_member_api_docs: false diff --git a/example/lib/main.dart b/example/lib/main.dart index 2ef2521..f44e48d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,7 +6,7 @@ void main() { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); // This widget is the root of your application. @override @@ -23,7 +23,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); + const MyHomePage({required this.title, super.key}); final String title; diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index 7115b72..0000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,238 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - body_part_selector: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.0.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.16.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - flutter_svg: - dependency: transitive - description: - name: flutter_svg - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.3" - json_annotation: - dependency: transitive - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "4.5.0" - lints: - dependency: transitive - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.11" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.4" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - path_drawing: - dependency: transitive - description: - name: path_drawing - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - path_parsing: - dependency: transitive - description: - name: path_parsing - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.dartlang.org" - source: hosted - version: "5.0.0" - rotation_stage: - dependency: transitive - description: - name: rotation_stage - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.3" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.9" - touchable: - dependency: transitive - description: - name: touchable - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.2" - xml: - dependency: transitive - description: - name: xml - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.0" -sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=2.11.0-0.1.pre" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4b7a37d..62aea85 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,71 +3,22 @@ description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: - flutter: - sdk: flutter - body_part_selector: path: ../ - cupertino_icons: ^1.0.2 + flutter: + sdk: flutter dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^2.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + lintervention: ^0.1.1 diff --git a/lib/body_part_selector.dart b/lib/body_part_selector.dart index 35b65ca..5005ba0 100644 --- a/lib/body_part_selector.dart +++ b/lib/body_part_selector.dart @@ -1,3 +1,6 @@ +/// A simple and beautiful selector for body parts. +library body_part_selector; + export 'src/body_part_selector.dart'; export 'src/body_part_selector_turnable.dart'; export 'src/model/body_parts.dart'; diff --git a/lib/src/body_part_selector.dart b/lib/src/body_part_selector.dart index 207d793..7936bbf 100644 --- a/lib/src/body_part_selector.dart +++ b/lib/src/body_part_selector.dart @@ -7,44 +7,85 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:touchable/touchable.dart'; +/// A widget that allows for selecting body parts. class BodyPartSelector extends StatelessWidget { + /// Creates a [BodyPartSelector]. const BodyPartSelector({ - super.key, - required this.side, required this.bodyParts, required this.onSelectionUpdated, + required this.side, this.mirrored = false, this.selectedColor, this.unselectedColor, this.selectedOutlineColor, this.unselectedOutlineColor, + super.key, }); - final BodySide side; + /// {@template body_part_selector.body_parts} + /// The current selection of body parts + /// {@endtemplate} final BodyParts bodyParts; + + /// The side of the body to display. + final BodySide side; + + /// {@template body_part_selector.on_selection_updated} + /// Called when the selection of body parts is updated with the new selection. + /// {@endtemplate} final void Function(BodyParts bodyParts)? onSelectionUpdated; + /// {@template body_part_selector.mirrored} + /// Whether the selection should be mirrored, or symmetric, such that when + /// selecting the left arm for example, the right arm is selected as well. + /// + /// Defaults to false. + /// {@endtemplate} final bool mirrored; + /// {@template body_part_selector.selected_color} + /// The color of the selected body parts. + /// + /// Defaults to [ThemeData.colorScheme.inversePrimary]. + /// {@endtemplate} final Color? selectedColor; + + /// {@template body_part_selector.unselected_color} + /// The color of the unselected body parts. + /// + /// Defaults to [ThemeData.colorScheme.inverseSurface]. + /// {@endtemplate} final Color? unselectedColor; + + /// {@template body_part_selector.selected_outline_color} + /// The color of the outline of the selected body parts. + /// + /// Defaults to [ThemeData.colorScheme.primary]. + /// {@endtemplate} final Color? selectedOutlineColor; + + /// {@template body_part_selector.unselected_outline_color} + /// The color of the outline of the unselected body parts. + /// + /// Defaults to [ThemeData.colorScheme.onInverseSurface]. + /// {@endtemplate} final Color? unselectedOutlineColor; @override Widget build(BuildContext context) { final notifier = SvgService.instance.getSide(side); return ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, _) { - if (value == null) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } else { - return _buildBody(context, value); - } - }); + valueListenable: notifier, + builder: (context, value, _) { + if (value == null) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } else { + return _buildBody(context, value); + } + }, + ); } Widget _buildBody(BuildContext context, DrawableRoot drawable) { @@ -146,17 +187,17 @@ class _BodyPainter extends CustomPainter { size.width / root.viewport.viewBoxRect.width, size.height / root.viewport.viewBoxRect.height, ); - final Size scaledHalfViewBoxSize = + final scaledHalfViewBoxSize = root.viewport.viewBoxRect.size * scale / 2.0; - final Size halfDesiredSize = size / 2.0; - final Offset shift = Offset( + final halfDesiredSize = size / 2.0; + final shift = Offset( halfDesiredSize.width - scaledHalfViewBoxSize.width, halfDesiredSize.height - scaledHalfViewBoxSize.height, ); final bodyPartsCanvas = TouchyCanvas(context, canvas); - final Matrix4 fittingMatrix = Matrix4.identity() + final fittingMatrix = Matrix4.identity() ..translate(shift.dx, shift.dy) ..scale(scale); diff --git a/lib/src/body_part_selector_turnable.dart b/lib/src/body_part_selector_turnable.dart index 579eefd..bd44769 100644 --- a/lib/src/body_part_selector_turnable.dart +++ b/lib/src/body_part_selector_turnable.dart @@ -6,44 +6,80 @@ import 'package:rotation_stage/rotation_stage.dart'; export 'package:rotation_stage/rotation_stage.dart'; +/// A widget that allows for selecting body parts on a turnable body. +/// +/// This widget is a wrapper around [RotationStage] and [BodyPartSelector]. class BodyPartSelectorTurnable extends StatelessWidget { + /// Creates a [BodyPartSelectorTurnable]. const BodyPartSelectorTurnable({ - super.key, required this.bodyParts, + super.key, this.onSelectionUpdated, this.mirrored = false, + this.selectedColor, + this.unselectedColor, + this.selectedOutlineColor, + this.unselectedOutlineColor, this.padding = EdgeInsets.zero, this.labelData, }); + /// {@macro body_part_selector.body_parts} final BodyParts bodyParts; - final Function(BodyParts)? onSelectionUpdated; + + /// {@macro body_part_selector.on_selection_updated} + final ValueChanged? onSelectionUpdated; + + /// {@macro body_part_selector.mirrored} final bool mirrored; + + /// {@macro body_part_selector.selected_color} + final Color? selectedColor; + + /// {@macro body_part_selector.unselected_color} + + final Color? unselectedColor; + + /// {@macro body_part_selector.selected_outline_color} + + final Color? selectedOutlineColor; + + /// {@macro body_part_selector.unselected_outline_color} + final Color? unselectedOutlineColor; + + /// The padding around the rendered body. final EdgeInsets padding; + + /// The labels for the sides of the [RotationStage]. final RotationStageLabelData? labelData; @override Widget build(BuildContext context) { - return RotationStage( - viewHandleBuilder: (index, side, currentPage) => , - contentBuilder: (index, side, page) => Padding( - padding: padding, - child: Padding( - padding: const EdgeInsets.all(16), - child: BodyPartSelector( - side: side.map( - front: BodySide.front, - left: BodySide.left, - back: BodySide.back, - right: BodySide.right, + return RotationStageLabels( + data: labelData ?? RotationStageLabelData.english, + child: RotationStage( + contentBuilder: (index, side, page) => Padding( + padding: padding, + child: Padding( + padding: const EdgeInsets.all(16), + child: BodyPartSelector( + side: side.map( + front: BodySide.front, + left: BodySide.left, + back: BodySide.back, + right: BodySide.right, + ), + bodyParts: bodyParts, + onSelectionUpdated: onSelectionUpdated, + mirrored: mirrored, + selectedColor: selectedColor, + unselectedColor: unselectedColor, + selectedOutlineColor: selectedOutlineColor, + unselectedOutlineColor: unselectedOutlineColor, ), - bodyParts: bodyParts, - onSelectionUpdated: onSelectionUpdated, - mirrored: mirrored, ), ), ), - labels: labelData, ); } } diff --git a/lib/src/model/body_parts.dart b/lib/src/model/body_parts.dart index c7c606a..ad9729b 100644 --- a/lib/src/model/body_parts.dart +++ b/lib/src/model/body_parts.dart @@ -3,10 +3,11 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'body_parts.freezed.dart'; part 'body_parts.g.dart'; +/// A class representing the different parts of the body that can be selected, +/// and whether they are. @freezed class BodyParts with _$BodyParts { - const BodyParts._(); - + /// Creates a new [BodyParts] object. const factory BodyParts({ @Default(false) bool head, @Default(false) bool neck, @@ -34,6 +35,12 @@ class BodyParts with _$BodyParts { @Default(false) bool vestibular, }) = _BodyParts; + /// Creates a new [BodyParts] object from a JSON object. + factory BodyParts.fromJson(Map json) => + _$BodyPartsFromJson(json); + const BodyParts._(); + + /// A constant representing a selection with all [BodyParts] selected. static const all = BodyParts( head: true, neck: true, @@ -61,9 +68,6 @@ class BodyParts with _$BodyParts { vestibular: true, ); - factory BodyParts.fromJson(Map json) => - _$BodyPartsFromJson(json); - /// Toggles the BodyPart with the given [id]. /// /// If [id] doesn't represent a valid BodyPart, this returns an unchanged @@ -72,18 +76,23 @@ class BodyParts with _$BodyParts { BodyParts withToggledId(String id, {bool mirror = false}) { final map = toJson(); if (!map.containsKey(id)) return this; - map[id] = !map[id]; + map[id] = !(map[id] ?? false); if (mirror) { if (id.contains("left")) { final mirroredId = id.replaceAll("left", "right").replaceAll("Left", "Right"); - map[mirroredId] = map[id]; + map[mirroredId] = map[id] ?? false; } else if (id.contains("right")) { final mirroredId = id.replaceAll("right", "left").replaceAll("Right", "Left"); - map[mirroredId] = map[id]; + map[mirroredId] = map[id] ?? false; } } return BodyParts.fromJson(map); } + + @override + Map toJson() { + return super.toJson().cast(); + } } diff --git a/lib/src/model/body_side.dart b/lib/src/model/body_side.dart index 58c66ae..5b70390 100644 --- a/lib/src/model/body_side.dart +++ b/lib/src/model/body_side.dart @@ -1,11 +1,28 @@ +/// Represents the side from which the body is viewed. +/// +/// Values are ordered as if looking at the person from the front, and them +/// then rotating them clockwise, so that their left side is visible next. enum BodySide { + /// The front (ventral) side of the body. + /// + /// As if looking the person in the face. front, + + /// The left (sinister) side of the body, where the person's left hand is. left, + + /// The back (dorsal) side of the body. + /// + /// As if looking at the person's back. back, + + /// The right (dexter) side of the body, where the person's right hand is. right; + /// Returns the [BodySide] for the given index. static BodySide forIndex(int i) => values[i % values.length]; + /// Maps the side to a value of type [T]. T map({ required T front, required T left, diff --git a/lib/src/service/svg_service.dart b/lib/src/service/svg_service.dart index fa2e04d..cafd551 100644 --- a/lib/src/service/svg_service.dart +++ b/lib/src/service/svg_service.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; +/// A singleton service that loads the SVGs for the body sides. class SvgService { SvgService._() { _init(); @@ -12,6 +13,7 @@ class SvgService { static final SvgService _instance = SvgService._(); + /// The singleton instance of [SvgService]. static SvgService get instance => _instance; final ValueNotifier _front = ValueNotifier(null); @@ -19,6 +21,9 @@ class SvgService { final ValueNotifier _back = ValueNotifier(null); final ValueNotifier _right = ValueNotifier(null); + /// The [ValueNotifier] for the given [side]. + /// + /// It's value is null until the SVG is loaded. ValueNotifier getSide(BodySide side) => side.map( front: _front, left: _left, @@ -33,7 +38,9 @@ class SvgService { } Future _loadDrawable( - BodySide side, ValueNotifier notifier) async { + BodySide side, + ValueNotifier notifier, + ) async { final svgBytes = await rootBundle.load( side.map( front: "packages/body_part_selector/m_front.svg", diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 0000000..6aee3fa --- /dev/null +++ b/melos.yaml @@ -0,0 +1,68 @@ +name: body_part_selector_workspace + +packages: + - . + - ./example + - packages/* + +command: + version: + updateGitTagRefs: true + workspaceChangelog: false + hooks: + preCommit: | + melos run generate + git add . + +scripts: + analyze: + run: | + dart analyze . --fatal-infos + exec: + # We are setting the concurrency to 1 because a higher concurrency can crash + # the analysis server on low performance machines (like GitHub Actions). + concurrency: 1 + description: | + Run `dart analyze` in all packages. + - Note: you can also rely on your IDEs Dart Analysis / Issues window. + + test:select: + run: flutter test + exec: + failFast: true + concurrency: 6 + packageFilters: + dirExists: test + description: Run `flutter test test` for selected packages. + + test: + run: melos run test:select --no-select + description: Run all tests in this project. + + coverage:select: + run: | + flutter test --coverage + exec: + failFast: true + concurrency: 6 + packageFilters: + dirExists: test + description: Generate coverage for the selected package. + + coverage: + run: melos run coverage:select --no-select + description: Generate coverage for all packages. + + generate:select: + description: Run code generation for selected packages. + run: dart run build_runner build --delete-conflicting-outputs + exec: + concurrency: 1 + failFast: true + packageFilters: + dependsOn: + - build_runner + + generate: + description: Run code generation for all packages. + run: melos run generate:select --no-select \ No newline at end of file diff --git a/packages/rotation_stage/.gitignore b/packages/rotation_stage/.gitignore new file mode 100644 index 0000000..8318291 --- /dev/null +++ b/packages/rotation_stage/.gitignore @@ -0,0 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.mason/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock +pubspec_overrides.yaml \ No newline at end of file diff --git a/packages/rotation_stage/CHANGELOG.md b/packages/rotation_stage/CHANGELOG.md new file mode 100644 index 0000000..54cc80c --- /dev/null +++ b/packages/rotation_stage/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +- feat: initial commit ๐ŸŽ‰ diff --git a/packages/rotation_stage/README.md b/packages/rotation_stage/README.md new file mode 100644 index 0000000..f1db7c9 --- /dev/null +++ b/packages/rotation_stage/README.md @@ -0,0 +1,68 @@ +# Rotation Stage + +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) + +A four-sided stage for representing 3D objects with four widgets +![Demo GIF](https://raw.githubusercontent.com/fyzio/rotation_stage/main/example/demo.gif) + + +## Installation ๐Ÿ’ป + +**โ— In order to start using Rotation Stage you must have the [Dart SDK][dart_install_link] installed on your machine.** + +Install via `dart pub add`: + +```sh +dart pub add rotation_stage +``` + +## Usage + +The simplest way is to use the ``RotationStage`` widget. +You only have to provide a ``contentBuilder``, everything else is preconfigured. + +```dart +Widget build(BuildContext context) { + return RotationStage( + contentBuilder: (int index, + RotationStageSide side, + double currentPage,) => + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + side.map( + front: "Front", + left: "Left", + back: "Back", + right: "Right", + ), + ), + ), + ), + ); +} +``` + +You can rotate the widget by swiping on the bottom bar. The top part is purposfully not swipeable, +so you can listen to whatever gestures you want there. + +If you want more fine-grained control, check out the other parameters of the constructor, or +``RotationStageBar``, ``RotationStageHandle`` and ``RotationStageContent``. + +The source code for ``RotationStage`` should be a good starting point. + +## Example + +To run the example open the ``example`` folder and run ``flutter create .`` + +--- + + +[dart_install_link]: https://dart.dev/get-dart +[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[mason_link]: https://github.com/felangel/mason +[very_good_ventures_link]: https://verygood.ventures diff --git a/packages/rotation_stage/analysis_options.yaml b/packages/rotation_stage/analysis_options.yaml new file mode 100644 index 0000000..245ed9f --- /dev/null +++ b/packages/rotation_stage/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lintervention/analysis_options.yaml diff --git a/packages/rotation_stage/demo.gif b/packages/rotation_stage/demo.gif new file mode 100644 index 0000000..d874c7e Binary files /dev/null and b/packages/rotation_stage/demo.gif differ diff --git a/packages/rotation_stage/example/.gitignore b/packages/rotation_stage/example/.gitignore new file mode 100644 index 0000000..e7786a5 --- /dev/null +++ b/packages/rotation_stage/example/.gitignore @@ -0,0 +1,54 @@ +android/ +ios/ +macos/ +windows/ +linux/ +web/ + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/rotation_stage/example/.metadata b/packages/rotation_stage/example/.metadata new file mode 100644 index 0000000..ce13b42 --- /dev/null +++ b/packages/rotation_stage/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: android + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: ios + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: linux + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: macos + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: web + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: windows + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/rotation_stage/example/README.md b/packages/rotation_stage/example/README.md new file mode 100644 index 0000000..2b3fce4 --- /dev/null +++ b/packages/rotation_stage/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/rotation_stage/example/analysis_options.yaml b/packages/rotation_stage/example/analysis_options.yaml new file mode 100644 index 0000000..4106b14 --- /dev/null +++ b/packages/rotation_stage/example/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:lintervention/analysis_options.yaml + +linter: + rules: + public_member_api_docs: false diff --git a/packages/rotation_stage/example/lib/main.dart b/packages/rotation_stage/example/lib/main.dart new file mode 100644 index 0000000..a96cad3 --- /dev/null +++ b/packages/rotation_stage/example/lib/main.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:rotation_stage/rotation_stage.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Rotation Stage Example', + theme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + ), + home: const MyHomePage(title: 'Rotation Stage'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({required this.title, super.key}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: SafeArea( + child: RotationStage( + contentBuilder: ( + int index, + RotationStageSide side, + double currentPage, + ) => + Padding( + padding: const EdgeInsets.all(64), + child: SizedBox.expand( + child: Card( + elevation: 0, + color: Theme.of(context).colorScheme.inverseSurface, + child: Center( + child: Card( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + side.map( + front: "Front", + left: "Left", + back: "Back", + right: "Right", + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/rotation_stage/example/pubspec.yaml b/packages/rotation_stage/example/pubspec.yaml new file mode 100644 index 0000000..665b1fb --- /dev/null +++ b/packages/rotation_stage/example/pubspec.yaml @@ -0,0 +1,24 @@ +name: example +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" + + +dependencies: + flutter: + sdk: flutter + rotation_stage: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + lintervention: ^0.1.1 diff --git a/packages/rotation_stage/lib/rotation_stage.dart b/packages/rotation_stage/lib/rotation_stage.dart new file mode 100644 index 0000000..015ed62 --- /dev/null +++ b/packages/rotation_stage/lib/rotation_stage.dart @@ -0,0 +1,105 @@ +/// A four-sided stage for representing 3D objects with four widgets +library rotation_stage; + +import 'package:flutter/material.dart'; +import 'package:rotation_stage/rotation_stage.dart'; + +export 'src/model/rotation_stage_side.dart'; +export 'src/rotation_stage_bar.dart'; +export 'src/rotation_stage_content.dart'; +export 'src/rotation_stage_controller.dart'; +export 'src/rotation_stage_handle.dart'; +export 'src/rotation_stage_labels.dart'; + +/// The builder function for one side of the [RotationStage]. +/// +/// Takes the [index] of the side, the [side] itself, and the [currentPage] of +/// the stage. The returned widget should be a representation of the side +/// denoted by [side] and [index]. +/// [currentPage] is passed to allow for building custom effects based on the +/// current scroll position, since it can also fall bewtween two pages. +typedef RotationStageBuilder = Widget Function( + int index, + RotationStageSide side, + double currentPage, +); + +/// A widget that allows for rotating a widget with four sides in pseudo-3D, +/// with a bar of handles for switching between the sides. +/// +/// Combines a [RotationStageContent] with a [RotationStageBar] and optional +/// labels for the sides. +/// +/// {@macro rotation_stage_handle.labels} +class RotationStage extends StatefulWidget { + /// Creates a [RotationStage]. + const RotationStage({ + required this.contentBuilder, + this.controller, + this.viewHandleBuilder, + this.barHeight = 64, + this.barInteractable = true, + super.key, + }); + + /// The builder function for the content of the [RotationStage]. + final RotationStageBuilder contentBuilder; + + /// The controller for the [RotationStage]. + /// + /// If not provided, a new controller will be created and disposed + /// when the stage is disposed. + final RotationStageController? controller; + + /// The builder function for the handles of the [RotationStage]. + final RotationStageBuilder? viewHandleBuilder; + + /// The height of the bottom bar in logical pixels. + final double barHeight; + + /// Whether the bar is interactable at the moment. + final bool barInteractable; + + @override + State createState() => _RotationStageState(); +} + +class _RotationStageState extends State { + late final RotationStageController _controller; + + @override + void initState() { + _controller = widget.controller ?? RotationStageController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: RotationStageContent( + controller: _controller, + contentBuilder: widget.contentBuilder, + ), + ), + const Divider( + height: 1, + ), + RotationStageBar( + controller: _controller, + interactable: widget.barInteractable, + viewHandleBuilder: widget.viewHandleBuilder ?? + (index, side, page) => RotationStageHandle( + onTap: () => _controller.animateToPage(index), + side: side, + active: index == page.round(), + backgroundTransparent: !widget.barInteractable, + ), + ), + ], + ); + } +} diff --git a/packages/rotation_stage/lib/src/model/rotation_stage_side.dart b/packages/rotation_stage/lib/src/model/rotation_stage_side.dart new file mode 100644 index 0000000..526956b --- /dev/null +++ b/packages/rotation_stage/lib/src/model/rotation_stage_side.dart @@ -0,0 +1,40 @@ +import 'package:rotation_stage/rotation_stage.dart'; + +/// Represents one of the four sides of the [RotationStage]. +/// +/// Values are ordered as if rotating the stage from left to right when looking +/// at it from the front. +enum RotationStageSide { + /// The front side of the [RotationStage]. + front, + + /// The left side of the [RotationStage]. + left, + + /// The back side of the [RotationStage]. + back, + + /// The right side of the [RotationStage]. + right; + + /// Returns the [RotationStageSide] for the given index. + /// + /// The index is wrapped around the number of values in the enum, and the + /// order is the same as the order of the values in the enum. + static RotationStageSide forIndex(int i) => values[i % values.length]; + + /// Maps the side to a value of type [T]. + T map({ + required T front, + required T left, + required T back, + required T right, + }) { + return switch (this) { + RotationStageSide.front => front, + RotationStageSide.left => left, + RotationStageSide.back => back, + RotationStageSide.right => right, + }; + } +} diff --git a/packages/rotation_stage/lib/src/rotation_stage_bar.dart b/packages/rotation_stage/lib/src/rotation_stage_bar.dart new file mode 100644 index 0000000..f9e97a7 --- /dev/null +++ b/packages/rotation_stage/lib/src/rotation_stage_bar.dart @@ -0,0 +1,69 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:rotation_stage/rotation_stage.dart'; + +/// A bar that displays the handles for the [RotationStage]. +class RotationStageBar extends StatelessWidget { + /// Creates a [RotationStageBar]. + const RotationStageBar({ + required this.controller, + required this.viewHandleBuilder, + this.height = kToolbarHeight, + this.interactable = true, + this.minHandleOpacity = 0, + super.key, + }) : assert(minHandleOpacity >= 0, 'minHandleOpacity must be >= 0'), + assert(minHandleOpacity <= 1, 'minHandleOpacity must be <= 1'); + + /// The controller for the [RotationStage]. + final RotationStageController controller; + + /// The builder function for the handles of the [RotationStage]. + final RotationStageBuilder viewHandleBuilder; + + /// Whether the bar is interactable at the moment. + final bool interactable; + + /// The height of the bar in logical pixels. + /// + /// Defaults to [kToolbarHeight]. + final double height; + + /// The minimum opacity of the handles when they are not visible. + /// + /// Must be in the range [0, 1] and defaults to 0. + final double minHandleOpacity; + + @override + Widget build(BuildContext context) { + final visOffset = 0.5 / controller.pageController.viewportFraction; + return SizedBox( + height: height, + child: ValueListenableBuilder( + valueListenable: controller, + builder: (context, page, _) => PageView.builder( + controller: controller.pageController, + itemBuilder: (context, index) { + final offset = (page - index).abs().clamp(0, visOffset) / visOffset; + final opacity = lerpDouble(minHandleOpacity, 1, 1 - offset); + return Center( + child: Opacity( + opacity: Curves.ease.transform(opacity!), + child: AnimatedOpacity( + duration: kThemeAnimationDuration, + opacity: interactable && index != index ? 0 : 1, + child: viewHandleBuilder( + index, + RotationStageSide.forIndex(index), + page, + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/packages/rotation_stage/lib/src/rotation_stage_content.dart b/packages/rotation_stage/lib/src/rotation_stage_content.dart new file mode 100644 index 0000000..91eeb1c --- /dev/null +++ b/packages/rotation_stage/lib/src/rotation_stage_content.dart @@ -0,0 +1,63 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:rotation_stage/rotation_stage.dart'; + +/// A widget that displays the content of the [RotationStage] and applies the +/// visual transformations to the sides when roataing the stage. +class RotationStageContent extends StatelessWidget { + /// Creates a [RotationStageContent]. + const RotationStageContent({ + required this.controller, + required this.contentBuilder, + super.key, + }); + + /// The controller for the [RotationStage]. + final RotationStageController controller; + + /// The builder function for the content of the [RotationStage]. + final RotationStageBuilder contentBuilder; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, page, _) { + final index = page.round(); + final betweenPages = page % 1 > 0; + return Stack( + children: [ + for (int i = (betweenPages ? index - 1 : index); + i < index + (betweenPages ? 2 : 1); + i++) + IgnorePointer( + ignoring: i != index, + child: Builder( + builder: (context) { + final diff = page < 3 || i != 0 ? (i - page) : (4 - page); + final opacity = (1 - diff.abs()).clamp(0.0, 1.0); + final cMatrix = Matrix4.identity() + ..rotateY(-diff * pi / 2) + ..setEntry(3, 0, 0.001 * diff); + return Opacity( + opacity: Curves.easeOutExpo.transform(opacity), + child: Transform( + transform: cMatrix, + alignment: FractionalOffset.center, + child: contentBuilder( + i, + RotationStageSide.forIndex(i), + page, + ), + ), + ); + }, + ), + ), + ], + ); + }, + ); + } +} diff --git a/packages/rotation_stage/lib/src/rotation_stage_controller.dart b/packages/rotation_stage/lib/src/rotation_stage_controller.dart new file mode 100644 index 0000000..c10acb4 --- /dev/null +++ b/packages/rotation_stage/lib/src/rotation_stage_controller.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:rotation_stage/rotation_stage.dart'; + +/// A workaround to achieve pseudo-infinite scroll with a default Flutter +/// [PageController]. +/// +/// While scrolling forward is infinite, scrolling backwards is limited to the +/// first page. Thus, the first page is set to [kInfiniteScrollStartPage] to +/// allow for a large number of pages to be scrolled through in either +/// direction. +const int kInfiniteScrollStartPage = 500; + +/// A controller for the [RotationStage]. +/// +/// Wraps a [PageController] and provides a [ValueNotifier] for the current page +/// of the [RotationStage]. +class RotationStageController extends ValueNotifier { + /// Creates a [RotationStageController]. + RotationStageController({ + double viewportFraction = 0.2, + }) : pageController = PageController( + initialPage: kInfiniteScrollStartPage, + viewportFraction: viewportFraction, + ), + super(kInfiniteScrollStartPage.toDouble()) { + pageController.addListener(() { + if (pageController.positions.isNotEmpty && pageController.page != null) { + value = pageController.page!; + } + }); + } + + /// The [PageController] instance backing this controller. + final PageController pageController; + + /// Animates the [RotationStage] to the given page. + void animateToPage( + int page, { + Duration duration = kThemeAnimationDuration, + Curve curve = Curves.ease, + }) { + pageController.animateToPage( + page, + duration: duration, + curve: curve, + ); + } + + @override + void dispose() { + pageController.dispose(); + super.dispose(); + } +} diff --git a/packages/rotation_stage/lib/src/rotation_stage_handle.dart b/packages/rotation_stage/lib/src/rotation_stage_handle.dart new file mode 100644 index 0000000..e62741d --- /dev/null +++ b/packages/rotation_stage/lib/src/rotation_stage_handle.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:rotation_stage/rotation_stage.dart'; + +/// A handle for the [RotationStage] that represents one side of the stage. +/// +/// {@template rotation_stage_handle.labels} +/// The handles will obtain their label from the [RotationStageLabels] in the +/// widget tree, and if there is none, fall back to english labels. +/// +/// If you want to customize the labels, wrap the [RotationStage] in a +/// [RotationStageLabels] widget with the desired labels. +/// {@endtemplate} +class RotationStageHandle extends StatelessWidget { + /// Creates a [RotationStageHandle]. + const RotationStageHandle({ + required this.side, + required this.active, + required this.onTap, + required this.backgroundTransparent, + this.activeForegroundColor, + this.inactiveForegroundColor, + this.activeBackgroundColor, + this.inactiveBackgroundColor, + super.key, + }); + + /// The [RotationStageSide] to represent. + final RotationStageSide side; + + /// Whether this handle is active (the side is currently visible). + final bool active; + + /// Whether the background of the handle is transparent. + final bool backgroundTransparent; + + /// The callback to call when the handle is tapped. + final VoidCallback onTap; + + /// The color of the foreground when the handle is active. + /// + /// Defaults to [ThemeData.colorScheme.onPrimary]. + final Color? activeForegroundColor; + + /// The color of the foreground when the handle is inactive. + /// + /// Defaults to [ThemeData.colorScheme.onPrimaryContainer]. + final Color? inactiveForegroundColor; + + /// The color of the background when the handle is active. + /// + /// Defaults to [ThemeData.colorScheme.primary]. + final Color? activeBackgroundColor; + + /// The color of the background when the handle is inactive. + /// + /// Defaults to [ThemeData.colorScheme.primaryContainer]. + final Color? inactiveBackgroundColor; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final labels = RotationStageLabels.of(context); + final name = labels.getForSide(side); + return RawChip( + showCheckmark: false, + onSelected: (_) => onTap(), + label: Text( + name.toUpperCase(), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: active + ? activeForegroundColor ?? colorScheme.onPrimary + : inactiveForegroundColor ?? colorScheme.onPrimaryContainer, + ), + ), + selected: active, + disabledColor: Colors.transparent, + shadowColor: + backgroundTransparent ? Colors.transparent : colorScheme.shadow, + selectedShadowColor: backgroundTransparent + ? Colors.transparent + : activeBackgroundColor ?? colorScheme.primary, + backgroundColor: backgroundTransparent + ? Colors.transparent + : inactiveBackgroundColor ?? colorScheme.primaryContainer, + selectedColor: backgroundTransparent + ? Colors.transparent + : activeBackgroundColor ?? colorScheme.primary, + ); + } +} diff --git a/packages/rotation_stage/lib/src/rotation_stage_labels.dart b/packages/rotation_stage/lib/src/rotation_stage_labels.dart new file mode 100644 index 0000000..3b39207 --- /dev/null +++ b/packages/rotation_stage/lib/src/rotation_stage_labels.dart @@ -0,0 +1,70 @@ +import 'package:flutter/widgets.dart'; +import 'package:rotation_stage/rotation_stage.dart'; + +/// Holds the labels for each [RotationStageSide]. +class RotationStageLabelData { + /// Creates a [RotationStageLabelData]. + const RotationStageLabelData({ + required this.front, + required this.left, + required this.right, + required this.back, + }); + + /// The default English labels for the sides of the [RotationStage]. + static const english = RotationStageLabelData( + front: "Front", + left: "Left", + right: "Right", + back: "Back", + ); + + /// The label for the front side. + final String front; + + /// The label for the left side. + final String left; + + /// The label for the right side. + final String right; + + /// The label for the back side. + final String back; + + /// Returns the label for the given [side]. + String getForSide(RotationStageSide side) => side.map( + front: front, + left: left, + back: back, + right: right, + ); +} + +/// An [InheritedWidget] that holds the [RotationStageLabelData] for the +/// [RotationStage] and provides them to the widgets below it in the widget +/// tree. +class RotationStageLabels extends InheritedWidget { + /// Creates a [RotationStageLabels]. + const RotationStageLabels({ + required this.data, + required super.child, + super.key, + }); + + /// The data for the labels. + final RotationStageLabelData data; + + /// Returns the [RotationStageLabelData] for the [RotationStage] from the + /// [context], or falls back to [RotationStageLabelData.english], if none + /// are found. + static RotationStageLabelData of(BuildContext context) { + final result = + context.dependOnInheritedWidgetOfExactType(); + return result?.data ?? RotationStageLabelData.english; + } + + @override + bool updateShouldNotify(RotationStageLabels oldWidget) { + return oldWidget.data != data; + } +} diff --git a/packages/rotation_stage/pubspec.yaml b/packages/rotation_stage/pubspec.yaml new file mode 100644 index 0000000..1b52316 --- /dev/null +++ b/packages/rotation_stage/pubspec.yaml @@ -0,0 +1,19 @@ +name: rotation_stage +description: A four-sided stage for representing 3D objects with four widgets +version: 0.1.0 + + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + lintervention: ^0.1.1 + + mocktail: ^1.0.3 diff --git a/pubspec.yaml b/pubspec.yaml index d08b59d..e1d62ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,35 +1,30 @@ name: body_part_selector description: A beautiful selector for different body parts version: 0.0.3 -homepage: https://www.fyzo.de -repository: https://github.com/fyzio/body_part_selector -issue_tracker: https://github.com/fyzio/body_part_selector +homepage: https://www.whynotmake.it +repository: https://github.com/timcreatedit/body_part_selector environment: - sdk: ">=2.17.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: sdk: flutter - flutter_svg: ^1.1.0 + flutter_svg: ^1.1.6 + freezed_annotation: ^2.4.1 + rotation_stage: ^0.1.0 touchable: ^1.0.2 - freezed_annotation: ^2.0.3 - rotation_stage: ^0.0.3 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 - build_runner: ^2.1.11 - freezed: ^2.0.3+1 - json_serializable: ^6.2.0 + freezed: ^2.5.2 + json_serializable: ^6.8.0 + lintervention: ^0.1.1 + melos: ^6.0.0 + mocktail: ^1.0.3 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: assets: - packages/body_part_selector/m_front.svg