├── .fvmrc ├── .github ├── pull_request_template.md └── workflows │ ├── main.yml │ ├── sync_with_crowdin.yml │ └── update_app_identifiers.sh ├── .gitignore ├── .metadata ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── org │ │ │ │ └── catrobat │ │ │ │ └── paintroid │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-mdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── icon │ ├── 1.5x │ │ ├── pocketpaint_intro_landscape.png │ │ ├── pocketpaint_intro_portrait.png │ │ └── pocketpaint_logo_small.png │ ├── 2.0x │ │ ├── pocketpaint_intro_landscape.png │ │ ├── pocketpaint_intro_portrait.png │ │ └── pocketpaint_logo_small.png │ ├── 3.0x │ │ ├── pocketpaint_intro_landscape.png │ │ ├── pocketpaint_intro_portrait.png │ │ └── pocketpaint_logo_small.png │ ├── app_icon.png │ └── app_icon_foreground.png ├── img │ ├── checkerboard.png │ ├── pocketpaint_intro_landscape.png │ ├── pocketpaint_intro_portrait.png │ └── pocketpaint_logo_small.png ├── lang │ └── app_translations_en.arb └── svg │ ├── ic_brush.svg │ ├── ic_clipboard.svg │ ├── ic_clipping.svg │ ├── ic_cursor.svg │ ├── ic_edit_circle.svg │ ├── ic_eraser.svg │ ├── ic_fill.svg │ ├── ic_hand.svg │ ├── ic_import.svg │ ├── ic_layers.svg │ ├── ic_line.svg │ ├── ic_pipette.svg │ ├── ic_shapes.svg │ ├── ic_smudge.svg │ ├── ic_spray_can.svg │ ├── ic_stamp.svg │ ├── ic_text.svg │ ├── ic_tools.svg │ ├── ic_transform.svg │ └── ic_watercolor.svg ├── build.yaml ├── crowdin.yaml ├── flutter_launcher_icons.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── app.dart ├── core │ ├── commands │ │ ├── command_factory │ │ │ ├── command_factory.dart │ │ │ ├── command_factory_provider.dart │ │ │ └── command_factory_provider.g.dart │ │ ├── command_implementation │ │ │ ├── command.dart │ │ │ └── graphic │ │ │ │ ├── draw_path_command.g.dart │ │ │ │ ├── graphic_command.dart │ │ │ │ ├── line_command.dart │ │ │ │ ├── line_command.g.dart │ │ │ │ ├── path_command.dart │ │ │ │ ├── path_command.g.dart │ │ │ │ ├── shape │ │ │ │ ├── circle_shape_command.dart │ │ │ │ ├── circle_shape_command.g.dart │ │ │ │ ├── shape_command.dart │ │ │ │ ├── square_shape_command.dart │ │ │ │ └── square_shape_command.g.dart │ │ │ │ ├── spray_command.dart │ │ │ │ └── spray_command.g.dart │ │ ├── command_manager │ │ │ ├── command_manager.dart │ │ │ ├── command_manager_provider.dart │ │ │ └── command_manager_provider.g.dart │ │ ├── command_painter.dart │ │ ├── graphic_factory │ │ │ ├── graphic_factory.dart │ │ │ ├── graphic_factory_provider.dart │ │ │ └── graphic_factory_provider.g.dart │ │ └── path_with_action_history.dart │ ├── database │ │ ├── project_dao.dart │ │ ├── project_database.dart │ │ └── project_database.g.dart │ ├── enums │ │ ├── bounding_box_corners.dart │ │ ├── image_format.dart │ │ ├── image_location.dart │ │ ├── shape_type.dart │ │ └── tool_types.dart │ ├── extensions │ │ ├── offset_extension.dart │ │ └── path_extension.dart │ ├── json_serialization │ │ ├── converter │ │ │ ├── offset_converter.dart │ │ │ ├── paint_converter.dart │ │ │ ├── path_action_converter.dart │ │ │ └── path_with_action_history_converter.dart │ │ └── versioning │ │ │ ├── serializer_version.dart │ │ │ └── version_strategy.dart │ ├── localization │ │ ├── app_localizations.dart │ │ └── app_localizations_en.dart │ ├── models │ │ ├── catrobat_image.dart │ │ ├── catrobat_image.g.dart │ │ ├── database │ │ │ └── project.dart │ │ ├── image_from_file.dart │ │ ├── image_meta_data.dart │ │ ├── image_with_pixel_info.dart │ │ └── loggable_mixin.dart │ ├── providers │ │ ├── object │ │ │ ├── canvas_painter_provider.dart │ │ │ ├── canvas_painter_provider.g.dart │ │ │ ├── device_service.dart │ │ │ ├── file_service.dart │ │ │ ├── image_service.dart │ │ │ ├── io_handler.dart │ │ │ ├── load_image_from_file_manager.dart │ │ │ ├── load_image_from_photo_library.dart │ │ │ ├── permission_service.dart │ │ │ ├── photo_library_service.dart │ │ │ ├── render_image_for_export.dart │ │ │ ├── save_as_catrobat_image.dart │ │ │ ├── save_as_raster_image.dart │ │ │ ├── shapes_tool_options_state_provider.dart │ │ │ ├── shapes_tool_options_state_provider.g.dart │ │ │ └── tools │ │ │ │ ├── brush_tool_provider.dart │ │ │ │ ├── brush_tool_provider.g.dart │ │ │ │ ├── eraser_tool_provider.dart │ │ │ │ ├── eraser_tool_provider.g.dart │ │ │ │ ├── hand_tool_provider.dart │ │ │ │ ├── hand_tool_provider.g.dart │ │ │ │ ├── line_tool_provider.dart │ │ │ │ ├── line_tool_provider.g.dart │ │ │ │ ├── shapes_tool_provider.dart │ │ │ │ └── shapes_tool_provider.g.dart │ │ └── state │ │ │ ├── app_bar_provider.dart │ │ │ ├── app_bar_provider.g.dart │ │ │ ├── canvas_state_data.dart │ │ │ ├── canvas_state_data.freezed.dart │ │ │ ├── canvas_state_provider.dart │ │ │ ├── canvas_state_provider.g.dart │ │ │ ├── paint_provider.dart │ │ │ ├── paint_provider.g.dart │ │ │ ├── shapes_tool_options_state_data.dart │ │ │ ├── shapes_tool_options_state_data.freezed.dart │ │ │ ├── spray_tool_provider.dart │ │ │ ├── spray_tool_provider.g.dart │ │ │ ├── tool_options_visibility_state_provider.dart │ │ │ ├── tool_options_visibility_state_provider.g.dart │ │ │ ├── toolbox_state_data.dart │ │ │ ├── toolbox_state_data.freezed.dart │ │ │ ├── toolbox_state_provider.dart │ │ │ ├── toolbox_state_provider.g.dart │ │ │ ├── workspace_state.dart │ │ │ ├── workspace_state.freezed.dart │ │ │ ├── workspace_state_notifier.dart │ │ │ └── workspace_state_notifier.g.dart │ ├── tools │ │ ├── implementation │ │ │ ├── brush_tool.dart │ │ │ ├── eraser_tool.dart │ │ │ ├── hand_tool.dart │ │ │ ├── shapes_tool │ │ │ │ ├── bounding_box.dart │ │ │ │ └── shapes_tool.dart │ │ │ └── spray_tool.dart │ │ ├── line_tool │ │ │ ├── line_tool.dart │ │ │ ├── vertex.dart │ │ │ └── vertex_stack.dart │ │ ├── tool.dart │ │ └── tool_data.dart │ └── utils │ │ ├── color_utils.dart │ │ ├── constants.dart │ │ ├── date_time_converter.dart │ │ ├── distance_calculator.dart │ │ ├── failure.dart │ │ ├── load_image_failure.dart │ │ ├── open_url.dart │ │ ├── save_image_failure.dart │ │ └── widget_identifier.dart ├── main.dart └── ui │ ├── pages │ ├── landing_page │ │ ├── components │ │ │ ├── custom_action_button.dart │ │ │ ├── image_preview.dart │ │ │ ├── main_overflow_menu.dart │ │ │ ├── project_list_tile.dart │ │ │ └── project_overflow_menu.dart │ │ └── landing_page.dart │ ├── onboarding_page │ │ ├── components │ │ │ ├── bottom_nav_bar_container.dart │ │ │ ├── onboarding_page_app_bar.dart │ │ │ └── onboarding_page_bottom_nav_bar.dart │ │ ├── onboarding_page.dart │ │ └── screens │ │ │ ├── screen1.dart │ │ │ ├── screen2.dart │ │ │ ├── screen3.dart │ │ │ ├── screen4.dart │ │ │ └── screen5.dart │ └── workspace_page │ │ ├── components │ │ ├── bottom_bar │ │ │ ├── bottom_nav_bar.dart │ │ │ ├── bottom_nav_bar_items.dart │ │ │ ├── tool_options │ │ │ │ ├── shapes_tool_options.dart │ │ │ │ ├── spray_tool_options.dart │ │ │ │ ├── stroke_tool_options.dart │ │ │ │ ├── tool_option.dart │ │ │ │ ├── tool_options.dart │ │ │ │ └── widgets │ │ │ │ │ ├── radius_slider.dart │ │ │ │ │ ├── shapes_tool_shape_type_options.dart │ │ │ │ │ ├── shapes_tool_transformation_mode_options.dart │ │ │ │ │ ├── stroke_cap_chips.dart │ │ │ │ │ └── stroke_width_slider.dart │ │ │ └── tools │ │ │ │ ├── tool_button.dart │ │ │ │ └── tools_bottom_sheet.dart │ │ ├── drawing_surface │ │ │ ├── canvas_painter.dart │ │ │ ├── checkerboard_pattern.dart │ │ │ ├── drawing_canvas.dart │ │ │ └── exit_fullscreen_button.dart │ │ └── top_bar │ │ │ ├── overflow_menu.dart │ │ │ └── top_app_bar.dart │ │ └── workspace_page.dart │ ├── shared │ ├── action_button.dart │ ├── bottom_nav_bar_icon.dart │ ├── custom_action_chip.dart │ ├── dialogs │ │ ├── about_dialog.dart │ │ ├── delete_project_dialog.dart │ │ ├── discard_changes_dialog.dart │ │ ├── generic_dialog.dart │ │ ├── load_image_dialog.dart │ │ ├── overwrite_dialog.dart │ │ ├── project_details_dialog.dart │ │ ├── rename_project_dialog.dart │ │ └── save_image_dialog.dart │ ├── icon_button_with_label.dart │ ├── icon_svg.dart │ ├── image_format_info.dart │ ├── images │ │ ├── checkerboard.dart │ │ ├── pocketpaint_intro_landscape.dart │ │ ├── pocketpaint_intro_portrait.dart │ │ └── pocketpaint_logo_small.dart │ ├── loading_overlay.dart │ ├── pop_menu_button.dart │ └── text_input_field.dart │ ├── theme │ ├── data │ │ ├── custom_colors.dart │ │ ├── dark_paintroid_theme_data.dart │ │ ├── font_size.dart │ │ ├── light_paintroid_theme_data.dart │ │ ├── paintroid_theme.dart │ │ ├── paintroid_theme_data.dart │ │ └── spacing.dart │ ├── state │ │ ├── theme_mode_state_provider.dart │ │ └── theme_mode_state_provider.g.dart │ └── theme.dart │ └── utils │ ├── toast_utils.dart │ └── top_bar_action_data.dart ├── packages └── colorpicker │ ├── .metadata │ ├── analysis_options.yaml │ ├── assets │ └── img │ │ └── checkerboard.png │ ├── lib │ ├── colorpicker.dart │ ├── src │ │ ├── colorpicker.dart │ │ ├── components │ │ │ ├── checkerboard_square.dart │ │ │ ├── color_comparison.dart │ │ │ ├── color_square.dart │ │ │ ├── opacity_slider.dart │ │ │ └── slider_indicator_shape.dart │ │ ├── constants │ │ │ └── colors.dart │ │ └── state │ │ │ ├── color_picker_state_data.dart │ │ │ ├── color_picker_state_data.freezed.dart │ │ │ ├── color_picker_state_provider.dart │ │ │ └── color_picker_state_provider.g.dart │ └── utils │ │ └── assets.dart │ ├── pubspec.yaml │ └── test │ └── unit │ └── color_state_test.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── assets └── images │ ├── test.jpg │ ├── test.png │ └── test1.png ├── integration ├── app_workflow_test.dart ├── brush_tool_test.dart ├── command_manager_test.dart ├── driver │ └── driver.dart ├── eraser_tool_test.dart ├── line_tool_test.dart ├── shapes_tool_test.dart └── spray_tool_test.dart ├── unit ├── command │ ├── command_factory_test.dart │ ├── draw_path_command_test.dart │ ├── draw_path_command_test.mocks.dart │ ├── shape_command_test.dart │ └── shape_command_test.mocks.dart ├── database │ └── project_database_test.dart ├── provider │ ├── file_service_test.dart │ ├── image_service_test.dart │ ├── load_image_from_photo_library_test.dart │ ├── load_image_from_photo_library_test.mocks.dart │ ├── paint_provider_test.dart │ ├── photo_library_service_test.dart │ ├── photo_library_service_test.mocks.dart │ ├── save_as_raster_image_test.dart │ └── save_as_raster_image_test.mocks.dart ├── serialization │ ├── command │ │ ├── circle_shape_serializer_test.dart │ │ ├── line_command_serializer_test.dart │ │ ├── path_command_serializer_test.dart │ │ └── rectangle_shape_serializer_test.dart │ ├── converter │ │ ├── offset_converter_test.dart │ │ ├── paint_converter_test.dart │ │ ├── path_action_converter_test.dart │ │ └── path_with_action_history_converter_test.dart │ ├── image │ │ └── catrobat_image_serializer_test.dart │ └── utils │ │ ├── dummy_command_factory.dart │ │ ├── dummy_paint_factory.dart │ │ ├── dummy_path_factory.dart │ │ └── dummy_version_strategy.dart ├── tools │ ├── bounding_box_test.dart │ ├── brush_tool_test.dart │ ├── eraser_tool_test.dart │ ├── hand_tool_test.dart │ ├── line_tool_test.dart │ ├── shapes_tool_test.dart │ └── spray_tool_test.dart └── workspace │ ├── render_image_for_export_test.dart │ └── render_image_for_export_test.mocks.dart ├── utils ├── bottom_nav_bar_interactions.dart ├── canvas_interactions.dart ├── canvas_positions.dart ├── interactive_viewer_interactions.dart ├── test_utils.dart ├── ui_interaction.dart └── widget_finder.dart └── widget ├── landing_page ├── landing_page_test.dart └── landing_page_test.mocks.dart ├── onboarding_page └── onboarding_page_test.dart └── workspace_page ├── bottom_control_navigation_bar_test.dart ├── eraser_tool_test.dart ├── hand_tool_test.dart ├── line_tool_test.dart ├── shapes_tool_test.dart ├── top_bar_test.dart └── workspace_page_test.dart /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.29.2" 3 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | *Please enter a short description of your pull request and add a reference to the Jira ticket.* 2 | 3 | ## New Features and Enhancements 4 | - New Features and Enhancements 5 | ## Refactorings and Bug Fixes 6 | - Refactorings and Bug Fixes 7 | 8 | ## Checklist 9 | 10 | #### Your checklist for this pull request 11 | Please review the [contributing guidelines](https://github.com/Catrobat/Paintroid/blob/develop/README.md) and [wiki pages](https://github.com/Catrobat/Catroid/wiki/) of this repository. 12 | 13 | - [ ] Include the name of the Jira ticket in the PR’s title 14 | - [ ] Add the link to the ticket in Jira in the description of the PR 15 | - [ ] Include a summary of the changes plus the relevant context 16 | - [ ] Choose the proper base branch (*develop*) 17 | - [ ] Confirm that the changes follow the project’s coding guidelines (Wiki) 18 | - [ ] Verify that the changes generate no compiler or linter warnings 19 | - [ ] Perform a self-review of the changes 20 | - [ ] Verify to commit no other files than the intentionally changed ones 21 | - [ ] Include reasonable and readable tests verifying the added or changed behavior 22 | - [ ] Confirm that new and existing tests pass locally 23 | - [ ] Check that the commits’ message style matches the [project’s guideline](https://github.com/Catrobat/Catroid/wiki/Commit-Message-Guidelines) 24 | - [ ] Verify that your changes do not have any conflicts with the base branch 25 | - [ ] After the PR, verify that all CI checks have passed 26 | - [ ] Add new information to the [Wiki](https://github.com/Catrobat/Paintroid-Flutter/wiki) 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test and Analyze 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | main: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4.1.4 13 | - uses: subosito/flutter-action@v2.10.0 14 | with: 15 | flutter-version: "3.29.2" 16 | channel: "stable" 17 | cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" 18 | cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" 19 | architecture: x64 20 | 21 | - name: Setup 22 | run: make get 23 | 24 | - name: Static Analysis 25 | run: make lint 26 | 27 | - name: Unit Tests 28 | run: make unit 29 | 30 | - name: Widget Tests 31 | run: make widget 32 | 33 | - name: Integration Tests 34 | run: make integration 35 | 36 | - name: Install xmlstarlet 37 | run: sudo apt-get install -y xmlstarlet 38 | 39 | - name: Prepare App Name and Identifier 40 | run: | 41 | chmod +x ./.github/workflows/update_app_identifiers.sh 42 | ./.github/workflows/update_app_identifiers.sh ${{ github.event.number }} 43 | 44 | - name: Build release package 45 | run: | 46 | flutter build apk --release 47 | mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/flutter-paint-${{ github.event.number }}.apk 48 | 49 | - name: Archive build artifacts 50 | uses: actions/upload-artifact@v4.3.3 51 | with: 52 | name: flutter-paint-apk-${{ github.event.number }} 53 | path: | 54 | build/app/outputs/flutter-apk/flutter-paint-${{ github.event.number }}.apk 55 | -------------------------------------------------------------------------------- /.github/workflows/sync_with_crowdin.yml: -------------------------------------------------------------------------------- 1 | name: Synchronize Crowdin Translations 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | sync-with-crowdin: 10 | name: Synchronize Crowdin Translations 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: crowdin/github-action@1.4.11 15 | with: 16 | upload_sources: true 17 | upload_translations: false 18 | download_translations: true 19 | config: 'crowdin.yaml' 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 23 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # VSCode related 22 | .vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | coverage 35 | 36 | # Commented out to push generated code: 37 | # *.mocks.dart 38 | # *.pb*.dart 39 | # *.g.dart 40 | # *.freezed.dart 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 | 56 | # Melos 57 | pubspec_overrides.yaml 58 | 59 | # Packages 60 | packages/*/pubspec.lock 61 | packages/*/build 62 | 63 | packages/features/*/pubspec.lock 64 | packages/features/*/build 65 | 66 | # FVM Version Cache 67 | .fvm/ -------------------------------------------------------------------------------- /.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 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | linter: 3 | rules: 4 | always_use_package_imports: true 5 | avoid_relative_lib_imports: true 6 | prefer_relative_imports: false 7 | prefer_single_quotes: true 8 | avoid_void_async: true 9 | constant_identifier_names: false 10 | 11 | analyzer: 12 | errors: 13 | missing_enum_constant_in_switch: error 14 | exhaustive_cases: error 15 | unused_element: error 16 | type_annotate_public_apis: error 17 | missing_required_param: error 18 | invalid_use_of_protected_member: error 19 | unused_import: error 20 | 21 | exclude: 22 | - lib/src/**.pb*.dart 23 | - lib/**.g.dart -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | android { 16 | namespace = "org.catrobat.paintroid" 17 | compileSdkVersion 35 18 | ndkVersion flutter.ndkVersion 19 | 20 | compileOptions { 21 | sourceCompatibility JavaVersion.VERSION_1_8 22 | targetCompatibility JavaVersion.VERSION_1_8 23 | } 24 | 25 | kotlinOptions { 26 | jvmTarget = '1.8' 27 | } 28 | 29 | sourceSets { 30 | main.java.srcDirs += 'src/main/kotlin' 31 | } 32 | 33 | defaultConfig { 34 | applicationId "org.catrobat.paintroidflutter" 35 | minSdkVersion 24 36 | targetSdkVersion 35 37 | versionCode 1 38 | versionName "1.0.0" 39 | } 40 | 41 | buildTypes { 42 | release { 43 | signingConfig signingConfigs.debug 44 | minifyEnabled true 45 | } 46 | } 47 | } 48 | 49 | flutter { 50 | source '../..' 51 | } 52 | 53 | dependencies { 54 | implementation "androidx.window:window:1.0.0" 55 | } 56 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #bff8fb 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.1.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.21" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /assets/icon/1.5x/pocketpaint_intro_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/1.5x/pocketpaint_intro_landscape.png -------------------------------------------------------------------------------- /assets/icon/1.5x/pocketpaint_intro_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/1.5x/pocketpaint_intro_portrait.png -------------------------------------------------------------------------------- /assets/icon/1.5x/pocketpaint_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/1.5x/pocketpaint_logo_small.png -------------------------------------------------------------------------------- /assets/icon/2.0x/pocketpaint_intro_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/2.0x/pocketpaint_intro_landscape.png -------------------------------------------------------------------------------- /assets/icon/2.0x/pocketpaint_intro_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/2.0x/pocketpaint_intro_portrait.png -------------------------------------------------------------------------------- /assets/icon/2.0x/pocketpaint_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/2.0x/pocketpaint_logo_small.png -------------------------------------------------------------------------------- /assets/icon/3.0x/pocketpaint_intro_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/3.0x/pocketpaint_intro_landscape.png -------------------------------------------------------------------------------- /assets/icon/3.0x/pocketpaint_intro_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/3.0x/pocketpaint_intro_portrait.png -------------------------------------------------------------------------------- /assets/icon/3.0x/pocketpaint_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/3.0x/pocketpaint_logo_small.png -------------------------------------------------------------------------------- /assets/icon/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/app_icon.png -------------------------------------------------------------------------------- /assets/icon/app_icon_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/icon/app_icon_foreground.png -------------------------------------------------------------------------------- /assets/img/checkerboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/img/checkerboard.png -------------------------------------------------------------------------------- /assets/img/pocketpaint_intro_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/img/pocketpaint_intro_landscape.png -------------------------------------------------------------------------------- /assets/img/pocketpaint_intro_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/img/pocketpaint_intro_portrait.png -------------------------------------------------------------------------------- /assets/img/pocketpaint_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/assets/img/pocketpaint_logo_small.png -------------------------------------------------------------------------------- /assets/lang/app_translations_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "fullscreen": "Fullscreen", 4 | "saveImage": "Save image", 5 | "loadImage": "Load image", 6 | "newImage": "New image", 7 | "saveProject": "Save project", 8 | "tools": "Tools", 9 | "brush": "Brush", 10 | "color": "Color", 11 | "layers": "Layers" 12 | } -------------------------------------------------------------------------------- /assets/svg/ic_brush.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/ic_clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/ic_clipping.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/ic_cursor.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/svg/ic_edit_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /assets/svg/ic_eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/ic_fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/ic_hand.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/svg/ic_import.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/ic_layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /assets/svg/ic_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/ic_pipette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/ic_shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /assets/svg/ic_smudge.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /assets/svg/ic_spray_can.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /assets/svg/ic_stamp.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/ic_text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/ic_tools.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | -------------------------------------------------------------------------------- /assets/svg/ic_transform.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | riverpod_generator: 5 | options: 6 | # Could be changed to "my", such that riverpod_generator 7 | # would generate "myCountProvider" instead of "countProvider" 8 | provider_name_prefix: "" # (default) 9 | # Similar to provider_name_prefix, this is an option for renaming 10 | # providers with parameters ("families"). 11 | # This takes precedence over provider_name_prefix. 12 | provider_family_name_prefix: "" # (default) 13 | # Could be changed to "Pod", such that riverpod_generator 14 | # would generate "countPod" instead of "countProvider" 15 | provider_name_suffix: "" # (default) 16 | # Similar to provider_name_suffix, this is an option for renaming 17 | # providers with parameters ("families"). 18 | # This takes precedence over provider_name_suffix. 19 | provider_family_name_suffix: "" # (default) -------------------------------------------------------------------------------- /crowdin.yaml: -------------------------------------------------------------------------------- 1 | "project_id_env": "CROWDIN_PROJECT_ID" 2 | "api_token_env": "CROWDIN_PERSONAL_TOKEN" 3 | "base_path": "." 4 | "base_url": "https://api.crowdin.com" 5 | 6 | "preserve_hierarchy": true 7 | 8 | files: [ 9 | { 10 | "source": "/assets/l10n/en.arb", 11 | "translation": "/assets/l10n/%locale%.arb", 12 | "dest": "/paintroid-flutter/en.arb", 13 | "type": "arb", 14 | "update_option": "update_as_unapproved", 15 | "translate_attributes": 0, 16 | } 17 | ] -------------------------------------------------------------------------------- /flutter_launcher_icons.yaml: -------------------------------------------------------------------------------- 1 | flutter_icons: 2 | image_path: "assets/icon/app_icon.png" 3 | android: true 4 | ios: true 5 | adaptive_icon_foreground: "assets/icon/app_icon_foreground.png" 6 | adaptive_icon_background: "#bff8fb" -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | 41 | # https://pub.dev/packages/permission_handler 42 | target.build_configurations.each do |config| 43 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' 44 | 45 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 46 | '$(inherited)', 47 | 'PERMISSION_PHOTOS=1', 48 | 'PERMISSION_PHOTOS_ADD_ONLY=1' 49 | ] 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/core/commands/command_factory/command_factory.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:paintroid/core/commands/command_implementation/graphic/line_command.dart'; 4 | import 'package:paintroid/core/commands/command_implementation/graphic/path_command.dart'; 5 | import 'package:paintroid/core/commands/command_implementation/graphic/shape/circle_shape_command.dart'; 6 | import 'package:paintroid/core/commands/command_implementation/graphic/shape/square_shape_command.dart'; 7 | import 'package:paintroid/core/commands/command_implementation/graphic/spray_command.dart'; 8 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 9 | 10 | class CommandFactory { 11 | const CommandFactory(); 12 | 13 | PathCommand createPathCommand( 14 | PathWithActionHistory path, 15 | Paint paint, 16 | ) => 17 | PathCommand(path, paint); 18 | 19 | LineCommand createLineCommand( 20 | PathWithActionHistory path, 21 | Paint paint, 22 | Offset startPoint, 23 | Offset endPoint, 24 | ) => 25 | LineCommand(path, paint, startPoint, endPoint); 26 | 27 | SquareShapeCommand createSquareShapeCommand( 28 | Paint paint, 29 | Offset topLeft, 30 | Offset topRight, 31 | Offset bottomLeft, 32 | Offset bottomRight, 33 | ) => 34 | SquareShapeCommand(paint, topLeft, topRight, bottomLeft, bottomRight); 35 | 36 | CircleShapeCommand createCircleShapeCommand( 37 | Paint paint, 38 | double radius, 39 | Offset center, 40 | ) => 41 | CircleShapeCommand(paint, radius, center); 42 | 43 | SprayCommand createSprayCommand(List points, Paint paint) { 44 | return SprayCommand(points, paint); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/core/commands/command_factory/command_factory_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/commands/command_factory/command_factory.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | part 'command_factory_provider.g.dart'; 5 | 6 | @Riverpod(keepAlive: true) 7 | class CommandFactoryProvider extends _$CommandFactoryProvider { 8 | @override 9 | CommandFactory build() { 10 | return const CommandFactory(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/commands/command_factory/command_factory_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'command_factory_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$commandFactoryProviderHash() => 10 | r'03be76e2b66c068d6868cd1482f29ff39591a6f0'; 11 | 12 | /// See also [CommandFactoryProvider]. 13 | @ProviderFor(CommandFactoryProvider) 14 | final commandFactoryProvider = 15 | NotifierProvider.internal( 16 | CommandFactoryProvider.new, 17 | name: r'commandFactoryProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$commandFactoryProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$CommandFactoryProvider = Notifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/command.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:paintroid/core/commands/command_implementation/graphic/line_command.dart'; 3 | import 'package:paintroid/core/commands/command_implementation/graphic/path_command.dart'; 4 | import 'package:paintroid/core/commands/command_implementation/graphic/shape/circle_shape_command.dart'; 5 | import 'package:paintroid/core/commands/command_implementation/graphic/shape/square_shape_command.dart'; 6 | import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart'; 7 | 8 | abstract class Command with EquatableMixin { 9 | const Command(); 10 | 11 | Map toJson(); 12 | 13 | factory Command.fromJson(Map json) { 14 | String type = json['type'] as String; 15 | switch (type) { 16 | case SerializerType.PATH_COMMAND: 17 | return PathCommand.fromJson(json); 18 | case SerializerType.LINE_COMMAND: 19 | return LineCommand.fromJson(json); 20 | case SerializerType.SQUARE_SHAPE_COMMAND: 21 | return SquareShapeCommand.fromJson(json); 22 | case SerializerType.CIRCLE_SHAPE_COMMAND: 23 | return CircleShapeCommand.fromJson(json); 24 | default: 25 | return PathCommand.fromJson(json); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/draw_path_command.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'path_command.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | PathCommand _$PathCommandFromJson(Map json) => PathCommand( 10 | const PathWithActionHistoryConverter() 11 | .fromJson(json['path'] as Map), 12 | const PaintConverter().fromJson(json['paint'] as Map), 13 | type: json['type'] as String? ?? SerializerType.PATH_COMMAND, 14 | version: (json['version'] as num?)?.toInt(), 15 | ); 16 | 17 | Map _$PathCommandToJson(PathCommand instance) => 18 | { 19 | 'paint': const PaintConverter().toJson(instance.paint), 20 | 'type': instance.type, 21 | 'version': instance.version, 22 | 'path': const PathWithActionHistoryConverter().toJson(instance.path), 23 | }; 24 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/graphic_command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:paintroid/core/commands/command_implementation/command.dart'; 4 | import 'package:paintroid/core/json_serialization/converter/paint_converter.dart'; 5 | 6 | abstract class GraphicCommand extends Command { 7 | const GraphicCommand(this.paint); 8 | 9 | @PaintConverter() 10 | final Paint paint; 11 | 12 | void call(Canvas canvas); 13 | } 14 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/line_command.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'line_command.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | LineCommand _$LineCommandFromJson(Map json) => LineCommand( 10 | const PathWithActionHistoryConverter() 11 | .fromJson(json['path'] as Map), 12 | const PaintConverter().fromJson(json['paint'] as Map), 13 | const OffsetConverter() 14 | .fromJson(json['startPoint'] as Map), 15 | const OffsetConverter() 16 | .fromJson(json['endPoint'] as Map), 17 | type: json['type'] as String? ?? SerializerType.LINE_COMMAND, 18 | version: (json['version'] as num?)?.toInt(), 19 | )..isSourcePath = json['isSourcePath'] as bool; 20 | 21 | Map _$LineCommandToJson(LineCommand instance) => 22 | { 23 | 'paint': const PaintConverter().toJson(instance.paint), 24 | 'type': instance.type, 25 | 'version': instance.version, 26 | 'isSourcePath': instance.isSourcePath, 27 | 'path': const PathWithActionHistoryConverter().toJson(instance.path), 28 | 'startPoint': const OffsetConverter().toJson(instance.startPoint), 29 | 'endPoint': const OffsetConverter().toJson(instance.endPoint), 30 | }; 31 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/path_command.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'path_command.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | PathCommand _$PathCommandFromJson(Map json) => PathCommand( 10 | const PathWithActionHistoryConverter() 11 | .fromJson(json['path'] as Map), 12 | const PaintConverter().fromJson(json['paint'] as Map), 13 | type: json['type'] as String? ?? SerializerType.PATH_COMMAND, 14 | version: (json['version'] as num?)?.toInt(), 15 | ); 16 | 17 | Map _$PathCommandToJson(PathCommand instance) => 18 | { 19 | 'paint': const PaintConverter().toJson(instance.paint), 20 | 'type': instance.type, 21 | 'version': instance.version, 22 | 'path': const PathWithActionHistoryConverter().toJson(instance.path), 23 | }; 24 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/shape/circle_shape_command.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'circle_shape_command.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CircleShapeCommand _$CircleShapeCommandFromJson(Map json) => 10 | CircleShapeCommand( 11 | const PaintConverter().fromJson(json['paint'] as Map), 12 | (json['radius'] as num).toDouble(), 13 | const OffsetConverter().fromJson(json['center'] as Map), 14 | version: (json['version'] as num?)?.toInt(), 15 | type: json['type'] as String? ?? SerializerType.CIRCLE_SHAPE_COMMAND, 16 | ); 17 | 18 | Map _$CircleShapeCommandToJson(CircleShapeCommand instance) => 19 | { 20 | 'paint': const PaintConverter().toJson(instance.paint), 21 | 'radius': instance.radius, 22 | 'center': const OffsetConverter().toJson(instance.center), 23 | 'version': instance.version, 24 | 'type': instance.type, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/shape/shape_command.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/commands/command_implementation/graphic/graphic_command.dart'; 2 | 3 | abstract class ShapeCommand extends GraphicCommand { 4 | ShapeCommand(super.paint); 5 | } 6 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/shape/square_shape_command.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'square_shape_command.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SquareShapeCommand _$SquareShapeCommandFromJson(Map json) => 10 | SquareShapeCommand( 11 | const PaintConverter().fromJson(json['paint'] as Map), 12 | const OffsetConverter().fromJson(json['topLeft'] as Map), 13 | const OffsetConverter() 14 | .fromJson(json['topRight'] as Map), 15 | const OffsetConverter() 16 | .fromJson(json['bottomLeft'] as Map), 17 | const OffsetConverter() 18 | .fromJson(json['bottomRight'] as Map), 19 | version: (json['version'] as num?)?.toInt(), 20 | type: json['type'] as String? ?? SerializerType.SQUARE_SHAPE_COMMAND, 21 | ); 22 | 23 | Map _$SquareShapeCommandToJson(SquareShapeCommand instance) => 24 | { 25 | 'paint': const PaintConverter().toJson(instance.paint), 26 | 'topLeft': const OffsetConverter().toJson(instance.topLeft), 27 | 'topRight': const OffsetConverter().toJson(instance.topRight), 28 | 'bottomLeft': const OffsetConverter().toJson(instance.bottomLeft), 29 | 'bottomRight': const OffsetConverter().toJson(instance.bottomRight), 30 | 'version': instance.version, 31 | 'type': instance.type, 32 | }; 33 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/spray_command.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'dart:ui'; 4 | 5 | import 'package:freezed_annotation/freezed_annotation.dart'; 6 | import 'package:paintroid/core/commands/command_implementation/graphic/graphic_command.dart'; 7 | import 'package:paintroid/core/json_serialization/converter/offset_converter.dart'; 8 | import 'package:paintroid/core/json_serialization/converter/paint_converter.dart'; 9 | import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart'; 10 | import 'package:paintroid/core/json_serialization/versioning/version_strategy.dart'; 11 | 12 | part 'spray_command.g.dart'; 13 | 14 | @JsonSerializable() 15 | class SprayCommand extends GraphicCommand { 16 | final String type; 17 | final int version; 18 | 19 | @OffsetConverter() 20 | List points; 21 | 22 | SprayCommand( 23 | this.points, 24 | super.paint, { 25 | this.type = SerializerType.SPRAY_COMMAND, 26 | int? version, 27 | }) : version = 28 | version ?? VersionStrategyManager.strategy.getSprayCommandVersion(); 29 | 30 | @override 31 | void call(Canvas canvas) { 32 | canvas.drawPoints(PointMode.points, points, paint); 33 | } 34 | 35 | @override 36 | List get props => [paint, points]; 37 | 38 | @override 39 | Map toJson() => _$SprayCommandToJson(this); 40 | 41 | factory SprayCommand.fromJson(Map json) { 42 | int version = json['version'] as int; 43 | 44 | switch (version) { 45 | case Version.v1: 46 | return _$SprayCommandFromJson(json); 47 | default: 48 | return _$SprayCommandFromJson(json); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/core/commands/command_implementation/graphic/spray_command.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'spray_command.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SprayCommand _$SprayCommandFromJson(Map json) => SprayCommand( 10 | (json['points'] as List) 11 | .map((e) => 12 | const OffsetConverter().fromJson(e as Map)) 13 | .toList(), 14 | const PaintConverter().fromJson(json['paint'] as Map), 15 | type: json['type'] as String? ?? SerializerType.SPRAY_COMMAND, 16 | version: (json['version'] as num?)?.toInt(), 17 | ); 18 | 19 | Map _$SprayCommandToJson(SprayCommand instance) => 20 | { 21 | 'paint': const PaintConverter().toJson(instance.paint), 22 | 'type': instance.type, 23 | 'version': instance.version, 24 | 'points': instance.points.map(const OffsetConverter().toJson).toList(), 25 | }; 26 | -------------------------------------------------------------------------------- /lib/core/commands/command_manager/command_manager_provider.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import 'package:paintroid/core/commands/command_manager/command_manager.dart'; 5 | 6 | part 'command_manager_provider.g.dart'; 7 | 8 | @Riverpod(keepAlive: true) 9 | class CommandManagerProvider extends _$CommandManagerProvider { 10 | @override 11 | CommandManager build() { 12 | return CommandManager(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/commands/command_manager/command_manager_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'command_manager_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$commandManagerProviderHash() => 10 | r'f40d2cb8d7d2c4c3804c6abab3f7f80b92937117'; 11 | 12 | /// See also [CommandManagerProvider]. 13 | @ProviderFor(CommandManagerProvider) 14 | final commandManagerProvider = 15 | NotifierProvider.internal( 16 | CommandManagerProvider.new, 17 | name: r'commandManagerProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$commandManagerProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$CommandManagerProvider = Notifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/commands/graphic_factory/graphic_factory_provider.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory.dart'; 5 | 6 | part 'graphic_factory_provider.g.dart'; 7 | 8 | @Riverpod(keepAlive: true) 9 | class GraphicFactoryProvider extends _$GraphicFactoryProvider { 10 | @override 11 | GraphicFactory build() { 12 | return const GraphicFactory(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/commands/graphic_factory/graphic_factory_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'graphic_factory_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$graphicFactoryProviderHash() => 10 | r'76b18b61cdda5d2ea42ac4e813b5373398860afd'; 11 | 12 | /// See also [GraphicFactoryProvider]. 13 | @ProviderFor(GraphicFactoryProvider) 14 | final graphicFactoryProvider = 15 | NotifierProvider.internal( 16 | GraphicFactoryProvider.new, 17 | name: r'graphicFactoryProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$graphicFactoryProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$GraphicFactoryProvider = Notifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/database/project_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:floor/floor.dart'; 2 | 3 | import 'package:paintroid/core/models/database/project.dart'; 4 | 5 | @dao 6 | abstract class ProjectDAO { 7 | @Insert(onConflict: OnConflictStrategy.replace) 8 | Future insertProject(Project project); 9 | 10 | @Insert(onConflict: OnConflictStrategy.replace) 11 | Future> insertProjects(List projects); 12 | 13 | @Query('DELETE FROM Project WHERE id = :id') 14 | Future deleteProject(int id); 15 | 16 | @delete 17 | Future deleteProjects(List projects); 18 | 19 | @Query('SELECT * FROM Project order by lastModified desc') 20 | Future> getProjects(); 21 | 22 | @Query('SELECT * FROM Project WHERE name = :name') 23 | Future getProjectByName(String name); 24 | } 25 | -------------------------------------------------------------------------------- /lib/core/database/project_database.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:floor/floor.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:sqflite/sqflite.dart' as sqflite; 6 | 7 | import 'package:paintroid/core/database/project_dao.dart'; 8 | import 'package:paintroid/core/models/database/project.dart'; 9 | import 'package:paintroid/core/utils/date_time_converter.dart'; 10 | 11 | part 'project_database.g.dart'; 12 | 13 | String databaseName = 'project_database.db'; 14 | 15 | @TypeConverters([DateTimeConverter]) 16 | @Database(version: 1, entities: [Project]) 17 | abstract class ProjectDatabase extends FloorDatabase { 18 | ProjectDAO get projectDAO; 19 | 20 | static final provider = FutureProvider((ref) => 21 | $FloorProjectDatabase.databaseBuilder('project_database.db').build()); 22 | } 23 | -------------------------------------------------------------------------------- /lib/core/enums/bounding_box_corners.dart: -------------------------------------------------------------------------------- 1 | enum BoundingBoxCorner { 2 | none, 3 | topLeft, 4 | topRight, 5 | bottomLeft, 6 | bottomRight, 7 | } 8 | -------------------------------------------------------------------------------- /lib/core/enums/image_format.dart: -------------------------------------------------------------------------------- 1 | enum ImageFormat { 2 | png('png'), 3 | jpg('jpg'), 4 | catrobatImage('catrobat-image'); 5 | 6 | const ImageFormat(this.extension); 7 | 8 | final String extension; 9 | } 10 | -------------------------------------------------------------------------------- /lib/core/enums/image_location.dart: -------------------------------------------------------------------------------- 1 | enum ImageLocation { photos, files } 2 | -------------------------------------------------------------------------------- /lib/core/enums/shape_type.dart: -------------------------------------------------------------------------------- 1 | enum ShapeType { 2 | circle, 3 | square, 4 | } 5 | -------------------------------------------------------------------------------- /lib/core/enums/tool_types.dart: -------------------------------------------------------------------------------- 1 | enum ToolType { 2 | BRUSH, 3 | HAND, 4 | ERASER, 5 | LINE, 6 | SHAPES, 7 | FILL, 8 | SPRAY, 9 | CURSOR, 10 | TEXT, 11 | CLIPBOARD, 12 | TRANSFORM, 13 | IMPORT, 14 | PIPETTE, 15 | WATERCOLOR, 16 | SMUDGE, 17 | CLIPPING 18 | } 19 | -------------------------------------------------------------------------------- /lib/core/extensions/offset_extension.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | extension OffsetExtensions on Offset { 4 | double distanceTo(Offset other) => (this - other).distance; 5 | 6 | bool isWithinRadius(Offset other, double radius) => 7 | distanceTo(other) < radius; 8 | 9 | Offset moveTowards({ 10 | required Offset towards, 11 | required double distance, 12 | Offset? from, 13 | double rotation = 0, 14 | }) => 15 | move(distance, (this - towards).direction + rotation, from: from); 16 | 17 | Offset move(double distance, double direction, {Offset? from}) => 18 | (from ?? this) + Offset.fromDirection(direction, distance); 19 | } 20 | -------------------------------------------------------------------------------- /lib/core/extensions/path_extension.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | extension PathExtension on Path { 4 | void moveToOffset(Offset offset) => moveTo(offset.dx, offset.dy); 5 | void lineToOffset(Offset offset) => lineTo(offset.dx, offset.dy); 6 | } 7 | -------------------------------------------------------------------------------- /lib/core/json_serialization/converter/offset_converter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | 5 | class OffsetConverter implements JsonConverter> { 6 | const OffsetConverter(); 7 | 8 | @override 9 | Offset fromJson(Map json) { 10 | return Offset(json['dx'] as double, json['dy'] as double); 11 | } 12 | 13 | @override 14 | Map toJson(Offset offset) { 15 | return { 16 | 'dx': offset.dx, 17 | 'dy': offset.dy, 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/core/json_serialization/converter/path_action_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 4 | import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart'; 5 | 6 | class PathActionConverter 7 | implements JsonConverter> { 8 | const PathActionConverter(); 9 | 10 | @override 11 | PathAction fromJson(Map json) { 12 | switch (json['type'] as String) { 13 | case SerializerType.MOVE_TO_ACTION: 14 | return MoveToAction(json['x'] as double, json['y'] as double); 15 | case SerializerType.LINE_TO_ACTION: 16 | return LineToAction(json['x'] as double, json['y'] as double); 17 | case SerializerType.CLOSE_ACTION: 18 | return const CloseAction(); 19 | default: 20 | return const CloseAction(); 21 | } 22 | } 23 | 24 | @override 25 | Map toJson(PathAction action) { 26 | switch (action.runtimeType) { 27 | case == MoveToAction: 28 | action as MoveToAction; 29 | return { 30 | 'type': SerializerType.MOVE_TO_ACTION, 31 | 'x': action.x, 32 | 'y': action.y, 33 | }; 34 | case == LineToAction: 35 | action as LineToAction; 36 | return { 37 | 'type': SerializerType.LINE_TO_ACTION, 38 | 'x': action.x, 39 | 'y': action.y, 40 | }; 41 | default: 42 | return {'type': SerializerType.CLOSE_ACTION}; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/core/json_serialization/converter/path_with_action_history_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 4 | import 'package:paintroid/core/json_serialization/converter/path_action_converter.dart'; 5 | 6 | class PathWithActionHistoryConverter 7 | implements JsonConverter> { 8 | const PathWithActionHistoryConverter(); 9 | 10 | @override 11 | PathWithActionHistory fromJson(Map json) { 12 | var pathWithActionHistory = PathWithActionHistory(); 13 | var actionsJson = json['actions'] as List; 14 | for (var actionJson in actionsJson) { 15 | var action = const PathActionConverter() 16 | .fromJson(actionJson as Map); 17 | 18 | if (action is MoveToAction) { 19 | pathWithActionHistory.moveTo(action.x, action.y); 20 | } else if (action is LineToAction) { 21 | pathWithActionHistory.lineTo(action.x, action.y); 22 | } else if (action is CloseAction) { 23 | pathWithActionHistory.close(); 24 | } 25 | } 26 | return pathWithActionHistory; 27 | } 28 | 29 | @override 30 | Map toJson(PathWithActionHistory pathWithActionHistory) => { 31 | 'actions': pathWithActionHistory.actions 32 | .map((action) => const PathActionConverter().toJson(action)) 33 | .toList(), 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /lib/core/json_serialization/versioning/serializer_version.dart: -------------------------------------------------------------------------------- 1 | class SerializerVersion { 2 | static const int PAINT_VERSION = Version.v1; 3 | static const int CATROBAT_IMAGE_VERSION = Version.v1; 4 | static const int PATH_COMMAND_VERSION = Version.v1; 5 | static const int LINE_COMMAND_VERSION = Version.v1; 6 | static const int SQUARE_SHAPE_COMMAND_VERSION = Version.v1; 7 | static const int CIRCLE_SHAPE_COMMAND_VERSION = Version.v1; 8 | static const int SPRAY_COMMAND_VERSION = Version.v1; 9 | } 10 | 11 | class Version { 12 | static const int v1 = 1; 13 | static const int v2 = 2; 14 | static const int v3 = 3; 15 | } 16 | 17 | class SerializerType { 18 | static const String PATH_COMMAND = 'PathCommand'; 19 | static const String LINE_COMMAND = 'LineCommand'; 20 | static const String MOVE_TO_ACTION = 'MoveToAction'; 21 | static const String LINE_TO_ACTION = 'LineToAction'; 22 | static const String CLOSE_ACTION = 'CloseAction'; 23 | static const String SQUARE_SHAPE_COMMAND = 'SquareShapeCommand'; 24 | static const String CIRCLE_SHAPE_COMMAND = 'CircleShapeCommand'; 25 | static const String SPRAY_COMMAND = 'SprayCommand'; 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/json_serialization/versioning/version_strategy.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart'; 2 | 3 | abstract class IVersionStrategy { 4 | int getCatrobatImageVersion(); 5 | 6 | int getPathCommandVersion(); 7 | 8 | int getLineCommandVersion(); 9 | 10 | int getSquareShapeCommandVersion(); 11 | 12 | int getCircleShapeCommandVersion(); 13 | 14 | int getSprayCommandVersion(); 15 | } 16 | 17 | class ProductionVersionStrategy implements IVersionStrategy { 18 | @override 19 | int getCatrobatImageVersion() => SerializerVersion.CATROBAT_IMAGE_VERSION; 20 | 21 | @override 22 | int getPathCommandVersion() => SerializerVersion.PATH_COMMAND_VERSION; 23 | 24 | @override 25 | int getLineCommandVersion() => SerializerVersion.LINE_COMMAND_VERSION; 26 | 27 | @override 28 | int getSquareShapeCommandVersion() => 29 | SerializerVersion.SQUARE_SHAPE_COMMAND_VERSION; 30 | 31 | @override 32 | int getCircleShapeCommandVersion() => 33 | SerializerVersion.CIRCLE_SHAPE_COMMAND_VERSION; 34 | 35 | @override 36 | int getSprayCommandVersion() => SerializerVersion.SPRAY_COMMAND_VERSION; 37 | } 38 | 39 | class VersionStrategyManager { 40 | static IVersionStrategy _strategy = ProductionVersionStrategy(); 41 | 42 | static void setStrategy(IVersionStrategy strategy) { 43 | _strategy = strategy; 44 | } 45 | 46 | static IVersionStrategy get strategy => _strategy; 47 | } 48 | -------------------------------------------------------------------------------- /lib/core/localization/app_localizations_en.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/localization/app_localizations.dart'; 2 | 3 | class AppLocalizationsEn extends AppLocalizations { 4 | AppLocalizationsEn([super.locale = 'en']); 5 | 6 | @override 7 | String get fullscreen => 'Fullscreen'; 8 | 9 | @override 10 | String get saveImage => 'Save image'; 11 | 12 | @override 13 | String get loadImage => 'Load image'; 14 | 15 | @override 16 | String get newImage => 'New image'; 17 | 18 | @override 19 | String get saveProject => 'Save project'; 20 | 21 | @override 22 | String get tools => 'Tools'; 23 | 24 | @override 25 | String get brush => 'Brush'; 26 | 27 | @override 28 | String get color => 'Color'; 29 | 30 | @override 31 | String get layers => 'Layers'; 32 | } 33 | -------------------------------------------------------------------------------- /lib/core/models/catrobat_image.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'catrobat_image.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CatrobatImage _$CatrobatImageFromJson(Map json) => 10 | CatrobatImage( 11 | (json['commands'] as List) 12 | .map((e) => Command.fromJson(e as Map)), 13 | (json['width'] as num).toInt(), 14 | (json['height'] as num).toInt(), 15 | json['backgroundImage'] as String, 16 | version: (json['version'] as num?)?.toInt(), 17 | magicValue: json['magicValue'] as String? ?? 'CATROBAT', 18 | ); 19 | 20 | Map _$CatrobatImageToJson(CatrobatImage instance) => 21 | { 22 | 'magicValue': instance.magicValue, 23 | 'version': instance.version, 24 | 'width': instance.width, 25 | 'height': instance.height, 26 | 'commands': instance.commands.toList(), 27 | 'backgroundImage': instance.backgroundImage, 28 | }; 29 | -------------------------------------------------------------------------------- /lib/core/models/database/project.dart: -------------------------------------------------------------------------------- 1 | import 'package:floor/floor.dart'; 2 | 3 | @entity 4 | class Project { 5 | String name; 6 | String path; 7 | DateTime lastModified; 8 | DateTime creationDate; 9 | String? resolution; 10 | String? format; 11 | int? size; 12 | String? imagePreviewPath; 13 | @PrimaryKey(autoGenerate: true) 14 | final int? id; 15 | 16 | Project({ 17 | required this.name, 18 | required this.path, 19 | required this.lastModified, 20 | required this.creationDate, 21 | this.resolution, 22 | this.format, 23 | this.size, 24 | this.imagePreviewPath, 25 | this.id, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/core/models/image_from_file.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:paintroid/core/models/catrobat_image.dart'; 4 | 5 | class ImageFromFile { 6 | final Image? rasterImage; 7 | final CatrobatImage? catrobatImage; 8 | 9 | const ImageFromFile.catrobatImage( 10 | CatrobatImage image, { 11 | Image? backgroundImage, 12 | }) : catrobatImage = image, 13 | rasterImage = backgroundImage; 14 | 15 | const ImageFromFile.rasterImage(Image image) 16 | : rasterImage = image, 17 | catrobatImage = null; 18 | } 19 | -------------------------------------------------------------------------------- /lib/core/models/image_meta_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/enums/image_format.dart'; 2 | 3 | abstract class ImageMetaData { 4 | final String name; 5 | final ImageFormat format; 6 | 7 | const ImageMetaData(this.name, this.format); 8 | 9 | @override 10 | String toString() => '$name.${format.extension}'; 11 | } 12 | 13 | class JpgMetaData extends ImageMetaData { 14 | /// Value between 1-100 (both inclusive) 15 | final int quality; 16 | 17 | const JpgMetaData(String name, this.quality) : super(name, ImageFormat.jpg); 18 | 19 | @override 20 | String toString() => '$name.${format.extension} - $quality%'; 21 | } 22 | 23 | class PngMetaData extends ImageMetaData { 24 | const PngMetaData(String name) : super(name, ImageFormat.png); 25 | } 26 | 27 | class CatrobatImageMetaData extends ImageMetaData { 28 | const CatrobatImageMetaData(String name) 29 | : super(name, ImageFormat.catrobatImage); 30 | } 31 | -------------------------------------------------------------------------------- /lib/core/models/loggable_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:logging/logging.dart'; 2 | 3 | mixin LoggableMixin { 4 | late final logger = Logger(runtimeType.toString()); 5 | } 6 | -------------------------------------------------------------------------------- /lib/core/providers/object/canvas_painter_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | 3 | part 'canvas_painter_provider.g.dart'; 4 | 5 | @riverpod 6 | class CanvasPainterProvider extends _$CanvasPainterProvider { 7 | @override 8 | CanvasPainterProvider build() => this; 9 | 10 | void repaint() => ref.notifyListeners(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/providers/object/canvas_painter_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'canvas_painter_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$canvasPainterProviderHash() => 10 | r'dee0580b6e68f1babfa1853fff0ef6a2764aff8f'; 11 | 12 | /// See also [CanvasPainterProvider]. 13 | @ProviderFor(CanvasPainterProvider) 14 | final canvasPainterProvider = AutoDisposeNotifierProvider.internal( 16 | CanvasPainterProvider.new, 17 | name: r'canvasPainterProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$canvasPainterProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$CanvasPainterProvider = AutoDisposeNotifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/providers/object/device_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 | 9 | abstract class IDeviceService { 10 | Future getSizeInPixels(); 11 | 12 | static final provider = Provider((ref) { 13 | const channel = MethodChannel('org.catrobat.paintroid/device'); 14 | return DeviceService(channel); 15 | }); 16 | 17 | static final sizeProvider = FutureProvider( 18 | (ref) => ref.watch(provider).getSizeInPixels(), 19 | ); 20 | } 21 | 22 | class DeviceService implements IDeviceService { 23 | DeviceService(this._methodChannel); 24 | 25 | final MethodChannel _methodChannel; 26 | final ui.Size _testSize = const ui.Size(1179, 2556); 27 | 28 | @override 29 | Future getSizeInPixels() async { 30 | final firstView = WidgetsBinding.instance.platformDispatcher.views.first; 31 | if (Platform.isAndroid) { 32 | final height = await _methodChannel.invokeMethod('getHeightInPixels'); 33 | return ui.Size(firstView.physicalSize.width, height); 34 | } else if (Platform.isIOS) { 35 | return firstView.physicalSize; 36 | } else { 37 | return _testSize; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/core/providers/object/load_image_from_photo_library.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:oxidized/oxidized.dart'; 5 | 6 | import 'package:paintroid/core/providers/object/image_service.dart'; 7 | import 'package:paintroid/core/providers/object/permission_service.dart'; 8 | import 'package:paintroid/core/providers/object/photo_library_service.dart'; 9 | import 'package:paintroid/core/utils/failure.dart'; 10 | import 'package:paintroid/core/utils/load_image_failure.dart'; 11 | 12 | class LoadImageFromPhotoLibrary { 13 | final IImageService imageService; 14 | final IPermissionService permissionService; 15 | final IPhotoLibraryService photoLibraryService; 16 | 17 | const LoadImageFromPhotoLibrary( 18 | this.imageService, this.permissionService, this.photoLibraryService); 19 | 20 | static final provider = Provider((ref) { 21 | final imageService = ref.watch(IImageService.provider); 22 | final permissionService = ref.watch(IPermissionService.provider); 23 | final photoLibraryService = ref.watch(IPhotoLibraryService.provider); 24 | return LoadImageFromPhotoLibrary( 25 | imageService, permissionService, photoLibraryService); 26 | }); 27 | 28 | Future> call() async { 29 | if (!(await permissionService.requestAccessToPickPhotos())) { 30 | return const Result.err(LoadImageFailure.permissionDenied); 31 | } 32 | return await photoLibraryService 33 | .pick() 34 | .andThenAsync((imageBytes) => imageService.import(imageBytes)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/core/providers/object/save_as_raster_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart' show Provider; 4 | import 'package:oxidized/oxidized.dart'; 5 | 6 | import 'package:paintroid/core/models/image_meta_data.dart'; 7 | import 'package:paintroid/core/providers/object/image_service.dart'; 8 | import 'package:paintroid/core/providers/object/permission_service.dart'; 9 | import 'package:paintroid/core/providers/object/photo_library_service.dart'; 10 | import 'package:paintroid/core/utils/failure.dart'; 11 | import 'package:paintroid/core/utils/save_image_failure.dart'; 12 | 13 | class SaveAsRasterImage { 14 | final IImageService imageService; 15 | final IPermissionService permissionService; 16 | final IPhotoLibraryService photoLibraryService; 17 | 18 | const SaveAsRasterImage( 19 | this.imageService, this.permissionService, this.photoLibraryService); 20 | 21 | static final provider = Provider((ref) { 22 | final imageService = ref.watch(IImageService.provider); 23 | final permissionService = ref.watch(IPermissionService.provider); 24 | final photoLibraryService = ref.watch(IPhotoLibraryService.provider); 25 | return SaveAsRasterImage( 26 | imageService, permissionService, photoLibraryService); 27 | }); 28 | 29 | Future> call(ImageMetaData data, Image image) async { 30 | final nameWithExt = '${data.name}.${data.format.extension}'; 31 | if (!(await permissionService.requestAccessForSavingToPhotos())) { 32 | return const Result.err(SaveImageFailure.permissionDenied); 33 | } 34 | return await (data is JpgMetaData 35 | ? imageService.exportAsJpg(image, data.quality) 36 | : imageService.exportAsPng(image)) 37 | .andThenAsync( 38 | (imageBytes) => photoLibraryService.save(nameWithExt, imageBytes)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/core/providers/object/shapes_tool_options_state_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/enums/shape_type.dart'; 2 | import 'package:paintroid/core/enums/tool_types.dart'; 3 | import 'package:paintroid/core/providers/object/canvas_painter_provider.dart'; 4 | import 'package:paintroid/core/providers/state/shapes_tool_options_state_data.dart'; 5 | import 'package:paintroid/core/providers/state/toolbox_state_provider.dart'; 6 | import 'package:paintroid/core/tools/implementation/shapes_tool/shapes_tool.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'shapes_tool_options_state_provider.g.dart'; 10 | 11 | @riverpod 12 | class ShapesToolOptionsStateProvider extends _$ShapesToolOptionsStateProvider { 13 | @override 14 | ShapesToolOptionsStateData build() { 15 | return const ShapesToolOptionsStateData( 16 | isRotating: false, 17 | shapeType: ShapeType.square, 18 | ); 19 | } 20 | 21 | void setIsRotating({required bool isRotating}) { 22 | state = state.copyWith(isRotating: isRotating); 23 | _handleCurrentTool(); 24 | ref.read(canvasPainterProvider.notifier).repaint(); 25 | ref.notifyListeners(); 26 | } 27 | 28 | void setShapeType({required ShapeType shapeType}) { 29 | state = state.copyWith(shapeType: shapeType); 30 | _handleCurrentTool(); 31 | ref.read(canvasPainterProvider.notifier).repaint(); 32 | ref.notifyListeners(); 33 | } 34 | 35 | void _handleCurrentTool() { 36 | if (ref.read(toolBoxStateProvider).currentTool.type == ToolType.SHAPES) { 37 | final shapesTool = 38 | ref.read(toolBoxStateProvider).currentTool as ShapesTool; 39 | shapesTool.isRotating = state.isRotating; 40 | shapesTool.shapeType = state.shapeType; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/core/providers/object/shapes_tool_options_state_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'shapes_tool_options_state_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$shapesToolOptionsStateProviderHash() => 10 | r'59c7dadcade7fea4964b2c7c1a455e5b80deb49f'; 11 | 12 | /// See also [ShapesToolOptionsStateProvider]. 13 | @ProviderFor(ShapesToolOptionsStateProvider) 14 | final shapesToolOptionsStateProvider = AutoDisposeNotifierProvider< 15 | ShapesToolOptionsStateProvider, ShapesToolOptionsStateData>.internal( 16 | ShapesToolOptionsStateProvider.new, 17 | name: r'shapesToolOptionsStateProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$shapesToolOptionsStateProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$ShapesToolOptionsStateProvider 26 | = AutoDisposeNotifier; 27 | // ignore_for_file: type=lint 28 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 29 | -------------------------------------------------------------------------------- /lib/core/providers/object/tools/brush_tool_provider.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import 'package:paintroid/core/commands/command_factory/command_factory_provider.dart'; 5 | import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart'; 6 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory_provider.dart'; 7 | import 'package:paintroid/core/enums/tool_types.dart'; 8 | import 'package:paintroid/core/tools/implementation/brush_tool.dart'; 9 | 10 | part 'brush_tool_provider.g.dart'; 11 | 12 | @riverpod 13 | class BrushToolProvider extends _$BrushToolProvider { 14 | @override 15 | BrushTool build() { 16 | return BrushTool( 17 | commandManager: ref.watch(commandManagerProvider), 18 | commandFactory: ref.watch(commandFactoryProvider), 19 | graphicFactory: ref.watch(graphicFactoryProvider), 20 | type: ToolType.BRUSH, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/core/providers/object/tools/brush_tool_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'brush_tool_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$brushToolProviderHash() => r'097ab3e3003d7fd63210aba5980917eb3a449173'; 10 | 11 | /// See also [BrushToolProvider]. 12 | @ProviderFor(BrushToolProvider) 13 | final brushToolProvider = 14 | AutoDisposeNotifierProvider.internal( 15 | BrushToolProvider.new, 16 | name: r'brushToolProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$brushToolProviderHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$BrushToolProvider = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 27 | -------------------------------------------------------------------------------- /lib/core/providers/object/tools/eraser_tool_provider.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import 'package:paintroid/core/commands/command_factory/command_factory_provider.dart'; 5 | import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart'; 6 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory_provider.dart'; 7 | import 'package:paintroid/core/enums/tool_types.dart'; 8 | import 'package:paintroid/core/tools/implementation/eraser_tool.dart'; 9 | 10 | part 'eraser_tool_provider.g.dart'; 11 | 12 | @riverpod 13 | class EraserToolProvider extends _$EraserToolProvider { 14 | @override 15 | EraserTool build() { 16 | return EraserTool( 17 | commandManager: ref.watch(commandManagerProvider), 18 | commandFactory: ref.watch(commandFactoryProvider), 19 | graphicFactory: ref.watch(graphicFactoryProvider), 20 | type: ToolType.ERASER, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/core/providers/object/tools/eraser_tool_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'eraser_tool_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$eraserToolProviderHash() => 10 | r'b66c9a0d7cc9a79cd50bcc4e320a47383d12e851'; 11 | 12 | /// See also [EraserToolProvider]. 13 | @ProviderFor(EraserToolProvider) 14 | final eraserToolProvider = 15 | AutoDisposeNotifierProvider.internal( 16 | EraserToolProvider.new, 17 | name: r'eraserToolProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$eraserToolProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$EraserToolProvider = AutoDisposeNotifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/providers/object/tools/hand_tool_provider.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import 'package:paintroid/core/commands/command_factory/command_factory_provider.dart'; 5 | import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart'; 6 | import 'package:paintroid/core/enums/tool_types.dart'; 7 | import 'package:paintroid/core/tools/implementation/hand_tool.dart'; 8 | 9 | part 'hand_tool_provider.g.dart'; 10 | 11 | @riverpod 12 | class HandToolProvider extends _$HandToolProvider { 13 | @override 14 | HandTool build() { 15 | return HandTool( 16 | commandManager: ref.watch(commandManagerProvider), 17 | commandFactory: ref.watch(commandFactoryProvider), 18 | type: ToolType.HAND, 19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /lib/core/providers/object/tools/hand_tool_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'hand_tool_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$handToolProviderHash() => r'7cd05f4d63f9a6790dfb4a4248db7859e5c44596'; 10 | 11 | /// See also [HandToolProvider]. 12 | @ProviderFor(HandToolProvider) 13 | final handToolProvider = 14 | AutoDisposeNotifierProvider.internal( 15 | HandToolProvider.new, 16 | name: r'handToolProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$handToolProviderHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$HandToolProvider = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 27 | -------------------------------------------------------------------------------- /lib/core/providers/object/tools/line_tool_provider.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import 'package:paintroid/core/commands/command_factory/command_factory_provider.dart'; 5 | import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart'; 6 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory_provider.dart'; 7 | import 'package:paintroid/core/enums/tool_types.dart'; 8 | import 'package:paintroid/core/providers/state/canvas_state_provider.dart'; 9 | import 'package:paintroid/core/tools/line_tool/line_tool.dart'; 10 | 11 | part 'line_tool_provider.g.dart'; 12 | 13 | @riverpod 14 | class LineToolProvider extends _$LineToolProvider { 15 | @override 16 | LineTool build() { 17 | return LineTool( 18 | type: ToolType.LINE, 19 | commandManager: ref.watch(commandManagerProvider), 20 | commandFactory: ref.watch(commandFactoryProvider), 21 | graphicFactory: ref.watch(graphicFactoryProvider), 22 | drawingSurfaceSize: ref.watch( 23 | canvasStateProvider.select((state) => state.size), 24 | ), 25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /lib/core/providers/object/tools/line_tool_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'line_tool_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$lineToolProviderHash() => r'ddd41ee46e06c9c30f63aa9b00d3ddecfa18f9ea'; 10 | 11 | /// See also [LineToolProvider]. 12 | @ProviderFor(LineToolProvider) 13 | final lineToolProvider = 14 | AutoDisposeNotifierProvider.internal( 15 | LineToolProvider.new, 16 | name: r'lineToolProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$lineToolProviderHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$LineToolProvider = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 27 | -------------------------------------------------------------------------------- /lib/core/providers/object/tools/shapes_tool_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/painting.dart'; 2 | import 'package:paintroid/core/providers/state/canvas_state_provider.dart'; 3 | import 'package:paintroid/core/tools/implementation/shapes_tool/bounding_box.dart'; 4 | import 'package:paintroid/core/tools/implementation/shapes_tool/shapes_tool.dart'; 5 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 | import 'package:paintroid/core/commands/command_factory/command_factory_provider.dart'; 7 | import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart'; 8 | import 'package:paintroid/core/enums/tool_types.dart'; 9 | 10 | part 'shapes_tool_provider.g.dart'; 11 | 12 | @riverpod 13 | class ShapesToolProvider extends _$ShapesToolProvider { 14 | @override 15 | ShapesTool build() { 16 | Rect initialBoundingBox = Rect.fromCenter( 17 | center: ref.read(canvasStateProvider).size.center(Offset.zero), 18 | width: 300, 19 | height: 300, 20 | ); 21 | return ShapesTool( 22 | commandManager: ref.watch(commandManagerProvider), 23 | commandFactory: ref.watch(commandFactoryProvider), 24 | type: ToolType.SHAPES, 25 | boundingBox: BoundingBox( 26 | initialBoundingBox.topLeft, 27 | initialBoundingBox.topRight, 28 | initialBoundingBox.bottomLeft, 29 | initialBoundingBox.bottomRight, 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/core/providers/object/tools/shapes_tool_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'shapes_tool_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$shapesToolProviderHash() => 10 | r'bc04ece278549c5a00ded19c08be50f360979776'; 11 | 12 | /// See also [ShapesToolProvider]. 13 | @ProviderFor(ShapesToolProvider) 14 | final shapesToolProvider = 15 | AutoDisposeNotifierProvider.internal( 16 | ShapesToolProvider.new, 17 | name: r'shapesToolProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$shapesToolProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$ShapesToolProvider = AutoDisposeNotifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/providers/state/app_bar_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | 3 | part 'app_bar_provider.g.dart'; 4 | 5 | @riverpod 6 | class AppBarProvider extends _$AppBarProvider { 7 | @override 8 | AppBarProvider build() => this; 9 | 10 | void update() => ref.notifyListeners(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/providers/state/app_bar_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_bar_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$appBarProviderHash() => r'951b700b58e840e7cee4e46d91b67dd4008b723a'; 10 | 11 | /// See also [AppBarProvider]. 12 | @ProviderFor(AppBarProvider) 13 | final appBarProvider = 14 | AutoDisposeNotifierProvider.internal( 15 | AppBarProvider.new, 16 | name: r'appBarProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$appBarProviderHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$AppBarProvider = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 27 | -------------------------------------------------------------------------------- /lib/core/providers/state/canvas_state_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:freezed_annotation/freezed_annotation.dart'; 5 | import 'package:paintroid/core/commands/command_manager/command_manager.dart'; 6 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory.dart'; 7 | 8 | part 'canvas_state_data.freezed.dart'; 9 | 10 | @immutable 11 | @freezed 12 | class CanvasStateData with _$CanvasStateData { 13 | const factory CanvasStateData({ 14 | ui.Image? backgroundImage, 15 | ui.Image? cachedImage, 16 | required Size size, 17 | required CommandManager commandManager, 18 | required GraphicFactory graphicFactory, 19 | }) = _CanvasStateData; 20 | } 21 | -------------------------------------------------------------------------------- /lib/core/providers/state/canvas_state_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'canvas_state_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$canvasStateProviderHash() => 10 | r'679bba9b579d049bcfbf4cc5231ae14a2d5baeab'; 11 | 12 | /// See also [CanvasStateProvider]. 13 | @ProviderFor(CanvasStateProvider) 14 | final canvasStateProvider = 15 | NotifierProvider.internal( 16 | CanvasStateProvider.new, 17 | name: r'canvasStateProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$canvasStateProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$CanvasStateProvider = Notifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/providers/state/paint_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory.dart'; 4 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory_provider.dart'; 5 | import 'package:paintroid/core/enums/tool_types.dart'; 6 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 7 | 8 | part 'paint_provider.g.dart'; 9 | 10 | @Riverpod(keepAlive: true) 11 | class PaintProvider extends _$PaintProvider { 12 | @override 13 | Paint build() { 14 | return ref.watch(graphicFactoryProvider).createPaint() 15 | ..style = PaintingStyle.stroke 16 | ..strokeJoin = StrokeJoin.round 17 | ..color = const Color(0xff00abbb) 18 | ..strokeCap = StrokeCap.round 19 | ..strokeWidth = 25; 20 | } 21 | 22 | void updateStrokeWidth(double newStrokeWidth) { 23 | state = GraphicFactory.copyPaintWith( 24 | original: state, 25 | strokeWidth: newStrokeWidth, 26 | ); 27 | } 28 | 29 | void updateStrokeCap(StrokeCap newStrokeCap) { 30 | state = GraphicFactory.copyPaintWith( 31 | original: state, 32 | strokeCap: newStrokeCap, 33 | ); 34 | } 35 | 36 | void updateColor(Color newColor) { 37 | state = GraphicFactory.copyPaintWith(original: state, color: newColor); 38 | } 39 | 40 | void updateBlendMode(BlendMode newMode) { 41 | state = GraphicFactory.copyPaintWith(original: state, blendMode: newMode); 42 | } 43 | 44 | void updateBlendModeByToolType(ToolType toolType) { 45 | switch (toolType) { 46 | case ToolType.ERASER: 47 | updateBlendMode(BlendMode.clear); 48 | break; 49 | default: 50 | updateBlendMode(BlendMode.srcOver); 51 | break; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/core/providers/state/paint_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'paint_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$paintProviderHash() => r'cdcdb845f24c29a6168d48244ec2dbf26fa72163'; 10 | 11 | /// See also [PaintProvider]. 12 | @ProviderFor(PaintProvider) 13 | final paintProvider = NotifierProvider.internal( 14 | PaintProvider.new, 15 | name: r'paintProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$paintProviderHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$PaintProvider = Notifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 26 | -------------------------------------------------------------------------------- /lib/core/providers/state/shapes_tool_options_state_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:paintroid/core/enums/shape_type.dart'; 3 | 4 | part 'shapes_tool_options_state_data.freezed.dart'; 5 | 6 | @immutable 7 | @freezed 8 | class ShapesToolOptionsStateData with _$ShapesToolOptionsStateData { 9 | const factory ShapesToolOptionsStateData({ 10 | required bool isRotating, 11 | required ShapeType shapeType, 12 | }) = _ShapesToolOptionsData; 13 | } 14 | -------------------------------------------------------------------------------- /lib/core/providers/state/spray_tool_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/commands/command_factory/command_factory_provider.dart'; 2 | import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart'; 3 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory_provider.dart'; 4 | import 'package:paintroid/core/enums/tool_types.dart'; 5 | import 'package:paintroid/core/providers/state/canvas_state_provider.dart'; 6 | import 'package:paintroid/core/tools/implementation/spray_tool.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'spray_tool_provider.g.dart'; 10 | 11 | @riverpod 12 | class SprayToolProvider extends _$SprayToolProvider { 13 | @override 14 | SprayTool build() { 15 | return SprayTool( 16 | type: ToolType.SPRAY, 17 | commandManager: ref.watch(commandManagerProvider), 18 | commandFactory: ref.watch(commandFactoryProvider), 19 | graphicFactory: ref.watch(graphicFactoryProvider), 20 | drawingSurfaceSize: 21 | ref.watch(canvasStateProvider.select((state) => state.size)), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/core/providers/state/spray_tool_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'spray_tool_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$sprayToolProviderHash() => r'7f2f003751d63db37267264ce921a1ed35e4cdd1'; 10 | 11 | /// See also [SprayToolProvider]. 12 | @ProviderFor(SprayToolProvider) 13 | final sprayToolProvider = 14 | AutoDisposeNotifierProvider.internal( 15 | SprayToolProvider.new, 16 | name: r'sprayToolProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$sprayToolProviderHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$SprayToolProvider = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 27 | -------------------------------------------------------------------------------- /lib/core/providers/state/tool_options_visibility_state_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | 3 | part 'tool_options_visibility_state_provider.g.dart'; 4 | 5 | @riverpod 6 | class ToolOptionsVisibilityStateProvider 7 | extends _$ToolOptionsVisibilityStateProvider { 8 | void toggleVisibility() { 9 | state = !state; 10 | } 11 | 12 | @override 13 | bool build() { 14 | return true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/core/providers/state/tool_options_visibility_state_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'tool_options_visibility_state_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$toolOptionsVisibilityStateProviderHash() => 10 | r'd7d8ae4af78baa1ee812d71bb03cc48b28a06256'; 11 | 12 | /// See also [ToolOptionsVisibilityStateProvider]. 13 | @ProviderFor(ToolOptionsVisibilityStateProvider) 14 | final toolOptionsVisibilityStateProvider = AutoDisposeNotifierProvider< 15 | ToolOptionsVisibilityStateProvider, bool>.internal( 16 | ToolOptionsVisibilityStateProvider.new, 17 | name: r'toolOptionsVisibilityStateProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$toolOptionsVisibilityStateProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$ToolOptionsVisibilityStateProvider = AutoDisposeNotifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/providers/state/toolbox_state_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:paintroid/core/tools/tool.dart'; 3 | 4 | part 'toolbox_state_data.freezed.dart'; 5 | 6 | @immutable 7 | @freezed 8 | class ToolBoxStateData with _$ToolBoxStateData { 9 | const factory ToolBoxStateData({ 10 | required Tool currentTool, 11 | required bool isDown, 12 | }) = _ToolBoxStateData; 13 | } 14 | -------------------------------------------------------------------------------- /lib/core/providers/state/toolbox_state_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'toolbox_state_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$toolBoxStateProviderHash() => 10 | r'23e3ddde3194c0acc46abe79fe6d0b0b4bf77ec6'; 11 | 12 | /// See also [ToolBoxStateProvider]. 13 | @ProviderFor(ToolBoxStateProvider) 14 | final toolBoxStateProvider = AutoDisposeNotifierProvider.internal( 16 | ToolBoxStateProvider.new, 17 | name: r'toolBoxStateProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$toolBoxStateProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$ToolBoxStateProvider = AutoDisposeNotifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/providers/state/workspace_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'workspace_state.freezed.dart'; 4 | 5 | @immutable 6 | @freezed 7 | class WorkspaceState with _$WorkspaceState { 8 | const factory WorkspaceState({ 9 | required bool isFullscreen, 10 | required bool isPerformingIOTask, 11 | required bool hasUnsavedChanges, 12 | required int commandCountWhenLastSaved, 13 | }) = _WorkspaceState; 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/providers/state/workspace_state_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart'; 2 | import 'package:paintroid/core/providers/state/workspace_state.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | part 'workspace_state_notifier.g.dart'; 6 | 7 | @Riverpod(keepAlive: true) 8 | class WorkspaceStateProvider extends _$WorkspaceStateProvider { 9 | @override 10 | WorkspaceState build() { 11 | return WorkspaceState( 12 | isFullscreen: false, 13 | isPerformingIOTask: false, 14 | hasUnsavedChanges: ref.watch(commandManagerProvider).undoStack.isNotEmpty, 15 | commandCountWhenLastSaved: 0, 16 | ); 17 | } 18 | 19 | bool get hasUnsavedChanges => state.hasUnsavedChanges; 20 | 21 | bool get hasSavedLastWork => 22 | state.commandCountWhenLastSaved == 23 | ref.read(commandManagerProvider).undoStack.length; 24 | 25 | void markUnsavedChanges() { 26 | state = state.copyWith(hasUnsavedChanges: true); 27 | } 28 | 29 | void updateLastSavedCommandCount() { 30 | state = state.copyWith(hasUnsavedChanges: false); 31 | state = state.copyWith( 32 | commandCountWhenLastSaved: 33 | ref.read(commandManagerProvider).undoStack.length, 34 | ); 35 | } 36 | 37 | Future performIOTask(Future Function() task) async { 38 | state = state.copyWith(isPerformingIOTask: true); 39 | final result = await task(); 40 | state = state.copyWith(isPerformingIOTask: false); 41 | return result; 42 | } 43 | 44 | void toggleFullscreen(bool isEnabled) { 45 | state = state.copyWith(isFullscreen: isEnabled); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/core/providers/state/workspace_state_notifier.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'workspace_state_notifier.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$workspaceStateProviderHash() => 10 | r'8e2b86e217dc6d98af2b604544c39d34172e4bf4'; 11 | 12 | /// See also [WorkspaceStateProvider]. 13 | @ProviderFor(WorkspaceStateProvider) 14 | final workspaceStateProvider = 15 | NotifierProvider.internal( 16 | WorkspaceStateProvider.new, 17 | name: r'workspaceStateProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$workspaceStateProviderHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$WorkspaceStateProvider = Notifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /lib/core/tools/implementation/brush_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory.dart'; 6 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 7 | import 'package:paintroid/core/tools/tool.dart'; 8 | 9 | class BrushTool extends Tool { 10 | final GraphicFactory graphicFactory; 11 | 12 | @visibleForTesting 13 | late PathWithActionHistory pathToDraw; 14 | 15 | BrushTool({ 16 | required super.commandFactory, 17 | required super.commandManager, 18 | required this.graphicFactory, 19 | required super.type, 20 | super.hasAddFunctionality = false, 21 | super.hasFinalizeFunctionality = false, 22 | }); 23 | 24 | @override 25 | void onDown(Offset point, Paint paint) { 26 | pathToDraw = graphicFactory.createPathWithActionHistory() 27 | ..moveTo(point.dx, point.dy); 28 | Paint savedPaint = graphicFactory.copyPaint(paint); 29 | final command = commandFactory.createPathCommand(pathToDraw, savedPaint); 30 | commandManager.addGraphicCommand(command); 31 | } 32 | 33 | @override 34 | void onDrag(Offset point, Paint paint) { 35 | pathToDraw.lineTo(point.dx, point.dy); 36 | } 37 | 38 | @override 39 | void onUp(Offset point, Paint paint) { 40 | if (pathToDraw.path.getBounds().size == Size.zero) { 41 | pathToDraw.close(); 42 | } 43 | } 44 | 45 | @override 46 | void onCancel() { 47 | commandManager.discardLastCommand(); 48 | } 49 | 50 | @override 51 | void onCheckmark(Paint paint) {} 52 | 53 | @override 54 | void onPlus() {} 55 | 56 | @override 57 | void onRedo() { 58 | commandManager.redo(); 59 | } 60 | 61 | @override 62 | void onUndo() { 63 | commandManager.undo(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/core/tools/implementation/eraser_tool.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/enums/tool_types.dart'; 2 | import 'package:paintroid/core/tools/implementation/brush_tool.dart'; 3 | 4 | class EraserTool extends BrushTool { 5 | EraserTool({ 6 | required super.commandFactory, 7 | required super.commandManager, 8 | required super.graphicFactory, 9 | required super.type, 10 | }); 11 | 12 | @override 13 | ToolType get type => ToolType.ERASER; 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/tools/implementation/hand_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:paintroid/core/tools/tool.dart'; 4 | 5 | class HandTool extends Tool { 6 | HandTool({ 7 | required super.commandFactory, 8 | required super.commandManager, 9 | required super.type, 10 | super.hasAddFunctionality = false, 11 | super.hasFinalizeFunctionality = false, 12 | }); 13 | 14 | @override 15 | void onDown(Offset point, Paint paint) {} 16 | 17 | @override 18 | void onDrag(Offset point, Paint paint) {} 19 | 20 | @override 21 | void onUp(Offset point, Paint paint) {} 22 | 23 | @override 24 | void onCancel() {} 25 | 26 | @override 27 | void onCheckmark(Paint paint) {} 28 | 29 | @override 30 | void onPlus() {} 31 | 32 | @override 33 | void onRedo() {} 34 | 35 | @override 36 | void onUndo() {} 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/tools/line_tool/vertex.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'package:paintroid/core/commands/command_implementation/graphic/line_command.dart'; 5 | import 'package:paintroid/core/utils/distance_calculator.dart'; 6 | 7 | class Vertex { 8 | Vertex({ 9 | required this.vertexCenter, 10 | required this.outgoingPathCommand, 11 | required this.ingoingPathCommand, 12 | }); 13 | 14 | Offset vertexCenter; 15 | LineCommand? ingoingPathCommand; 16 | LineCommand? outgoingPathCommand; 17 | 18 | static const int PAINT_ALPHA = 128; 19 | static const double VERTEX_PAINT_STROKE_WIDTH = 2.0; 20 | static const double VERTEX_RADIUS = 30.0; 21 | 22 | void setOutgoingPath(LineCommand updatedOutgoingPath) { 23 | outgoingPathCommand = updatedOutgoingPath; 24 | } 25 | 26 | void updateVertexCenter(Offset updatedVertexCenter) { 27 | vertexCenter = updatedVertexCenter; 28 | } 29 | 30 | bool wasClicked(Offset clickedCoordinate) { 31 | double distanceFromCenter = 32 | DistanceCalculator.calculateDistanceBetweenTwoPoints(vertexCenter.dx, 33 | vertexCenter.dy, clickedCoordinate.dx, clickedCoordinate.dy); 34 | if (distanceFromCenter <= VERTEX_RADIUS) return true; 35 | return false; 36 | } 37 | 38 | static Paint getVertexPaint() { 39 | return Paint() 40 | ..style = PaintingStyle.fill 41 | ..color = Colors.grey.shade600.withAlpha(PAINT_ALPHA) 42 | ..strokeWidth = VERTEX_PAINT_STROKE_WIDTH; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/core/tools/line_tool/vertex_stack.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:paintroid/core/tools/line_tool/vertex.dart'; 4 | 5 | class VertexStack extends Iterable { 6 | final _stack = Queue(); 7 | 8 | @override 9 | Iterator get iterator => _stack.iterator; 10 | 11 | @override 12 | bool get isEmpty => _stack.isEmpty; 13 | 14 | @override 15 | Vertex get last => _stack.last; 16 | 17 | void add(Vertex vertex) { 18 | _stack.add(vertex); 19 | } 20 | 21 | void clear() { 22 | _stack.clear(); 23 | } 24 | 25 | void removeLast() { 26 | _stack.removeLast(); 27 | } 28 | 29 | Vertex? getPredecessor(Vertex vertex) { 30 | if (_stack.isEmpty) return null; 31 | if (_stack.length == 1) return null; 32 | if (_stack.first == vertex) return null; 33 | return _stack.elementAt(_stack.toList().indexOf(vertex) - 1); 34 | } 35 | 36 | Vertex? getSuccessor(Vertex vertex) { 37 | if (_stack.isEmpty) return null; 38 | if (_stack.length == 1) return null; 39 | if (_stack.last == vertex) return null; 40 | return _stack.elementAt(_stack.toList().indexOf(vertex) + 1); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/core/tools/tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:paintroid/core/commands/command_factory/command_factory.dart'; 4 | import 'package:paintroid/core/commands/command_manager/command_manager.dart'; 5 | import 'package:paintroid/core/enums/tool_types.dart'; 6 | 7 | abstract class Tool { 8 | final ToolType type; 9 | final CommandManager commandManager; 10 | final CommandFactory commandFactory; 11 | final bool hasAddFunctionality; 12 | final bool hasFinalizeFunctionality; 13 | 14 | const Tool({ 15 | required this.commandManager, 16 | required this.commandFactory, 17 | required this.type, 18 | required this.hasAddFunctionality, 19 | required this.hasFinalizeFunctionality, 20 | }); 21 | 22 | void onDown(Offset point, Paint paint); 23 | 24 | void onDrag(Offset point, Paint paint); 25 | 26 | void onUp(Offset point, Paint paint); 27 | 28 | void onCancel(); 29 | 30 | void onCheckmark(Paint paint); 31 | 32 | void onPlus(); 33 | 34 | void onUndo(); 35 | 36 | void onRedo(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/utils/color_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension ColorUtils on Color { 4 | int toValue() { 5 | final alpha = (a * 255).toInt(); 6 | final red = (r * 255).toInt(); 7 | final green = (g * 255).toInt(); 8 | final blue = (b * 255).toInt(); 9 | return (alpha << 24) | (red << 16) | (green << 8) | blue; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/utils/constants.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | const double HALF_PI = pi / 2; -------------------------------------------------------------------------------- /lib/core/utils/date_time_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:floor/floor.dart'; 2 | 3 | class DateTimeConverter extends TypeConverter { 4 | @override 5 | DateTime decode(int databaseValue) { 6 | return DateTime.fromMillisecondsSinceEpoch(databaseValue); 7 | } 8 | 9 | @override 10 | int encode(DateTime value) { 11 | return value.millisecondsSinceEpoch; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/core/utils/distance_calculator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | class DistanceCalculator { 4 | static double calculateDistanceBetweenTwoPoints( 5 | double x0, double y0, double x1, double y1) { 6 | return sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/core/utils/failure.dart: -------------------------------------------------------------------------------- 1 | abstract class Failure { 2 | const Failure(this.message); 3 | 4 | final String message; 5 | 6 | @override 7 | String toString() => '$runtimeType{message: $message}'; 8 | } 9 | -------------------------------------------------------------------------------- /lib/core/utils/load_image_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/utils/failure.dart'; 2 | 3 | class LoadImageFailure extends Failure { 4 | const LoadImageFailure._(super.message); 5 | 6 | static const permissionDenied = 7 | LoadImageFailure._('Permission to view photos is denied in settings'); 8 | static const userCancelled = 9 | LoadImageFailure._('User did not choose an image'); 10 | static const invalidImage = LoadImageFailure._('Invalid image'); 11 | static const unidentified = LoadImageFailure._('Could not load image'); 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/utils/open_url.dart: -------------------------------------------------------------------------------- 1 | import 'package:url_launcher/url_launcher.dart'; 2 | 3 | Future openUrl(url) async { 4 | final uri = Uri.parse(url); 5 | if (await canLaunchUrl(uri)) { 6 | await launchUrl(uri); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/core/utils/save_image_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/utils/failure.dart'; 2 | 3 | class SaveImageFailure extends Failure { 4 | const SaveImageFailure._(super.message); 5 | 6 | static const permissionDenied = 7 | SaveImageFailure._('Permission to save photos is denied in settings'); 8 | static const userCancelled = 9 | SaveImageFailure._('User did not choose a save location'); 10 | static const unidentified = SaveImageFailure._('Could not save image'); 11 | static const deletionFailed = SaveImageFailure._('Could not delete image'); 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/utils/widget_identifier.dart: -------------------------------------------------------------------------------- 1 | class WidgetIdentifier { 2 | static const canvasPainter = 'CanvasPainter'; 3 | static const newImageActionButton = 'NewImageActionButton'; 4 | static const circleShapeTypeChip = 'CircleShapeTypeChip'; 5 | static const backButton = 'BackButton'; 6 | 7 | // GenericDialogAction 8 | static const genericDialogActionDone = 'GenericDialogActionDone'; 9 | static const genericDialogActionDiscard = 'GenericDialogActionDiscard'; 10 | static const genericDialogActionSave = 'GenericDialogActionSave'; 11 | static const genericDialogActionPhotos = 'GenericDialogActionPhotos'; 12 | static const genericDialogActionFiles = 'GenericDialogActionFiles'; 13 | static const genericDialogActionCancel = 'GenericDialogActionCancel'; 14 | static const genericDialogActionDelete = 'GenericDialogActionDelete'; 15 | static const genericDialogActionOk = 'GenericDialogActionOk'; 16 | static const genericDialogActionRename = 'GenericDialogActionRename'; 17 | static const genericDialogActionYes = 'GenericDialogActionYes'; 18 | } 19 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:logging/logging.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | import 'package:paintroid/app.dart'; 10 | 11 | void main() async { 12 | Logger.root.onRecord.listen( 13 | (record) { 14 | log( 15 | record.message, 16 | time: record.time, 17 | sequenceNumber: record.sequenceNumber, 18 | level: record.level.value, 19 | name: record.loggerName, 20 | zone: record.zone, 21 | error: record.error, 22 | stackTrace: record.stackTrace, 23 | ); 24 | }, 25 | ); 26 | 27 | WidgetsFlutterBinding.ensureInitialized(); 28 | final prefs = await SharedPreferences.getInstance(); 29 | final showOnboarding = prefs.getBool('showOnboarding') ?? true; 30 | 31 | runApp(ProviderScope(child: App(showOnboardingPage: showOnboarding))); 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/pages/landing_page/components/custom_action_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/theme/theme.dart'; 4 | 5 | class CustomActionButton extends StatelessWidget { 6 | final String heroTag; 7 | final IconData icon; 8 | final VoidCallback onPressed; 9 | final String hint; 10 | 11 | const CustomActionButton({ 12 | super.key, 13 | required this.heroTag, 14 | required this.icon, 15 | required this.onPressed, 16 | required this.hint, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return FloatingActionButton( 22 | heroTag: heroTag, 23 | backgroundColor: PaintroidTheme.of(context).orangeColor, 24 | foregroundColor: PaintroidTheme.of(context).onSurfaceColor, 25 | tooltip: hint, 26 | child: Icon(icon), 27 | onPressed: () async => onPressed(), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/pages/landing_page/components/image_preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'package:paintroid/core/models/database/project.dart'; 5 | import 'package:paintroid/core/providers/object/image_service.dart'; 6 | import 'package:paintroid/ui/utils/toast_utils.dart'; 7 | 8 | class ImagePreview extends StatelessWidget { 9 | final Project? project; 10 | final double? width; 11 | final Color color; 12 | final IImageService imageService; 13 | 14 | const ImagePreview({ 15 | super.key, 16 | this.width, 17 | required this.color, 18 | this.project, 19 | required this.imageService, 20 | }); 21 | 22 | ImageProvider _getProjectPreviewImageProvider(Uint8List img) => 23 | Image.memory(img, fit: BoxFit.cover).image; 24 | 25 | Uint8List? _getProjectPreview(String? path) => 26 | imageService.getProjectPreview(path).when( 27 | ok: (preview) => preview, 28 | err: (failure) { 29 | ToastUtils.showShortToast(message: failure.message); 30 | return null; 31 | }, 32 | ); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | Uint8List? img; 37 | if (project != null) { 38 | img = _getProjectPreview(project!.imagePreviewPath); 39 | } 40 | var imgPreview = BoxDecoration(color: color); 41 | if (img != null) { 42 | imgPreview = BoxDecoration( 43 | color: color, 44 | image: DecorationImage( 45 | image: _getProjectPreviewImageProvider(img), 46 | ), 47 | ); 48 | } 49 | if (width != null) { 50 | return Container( 51 | width: width!, 52 | decoration: imgPreview, 53 | ); 54 | } else { 55 | return Container( 56 | decoration: imgPreview, 57 | ); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/ui/pages/landing_page/components/project_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:intl/intl.dart'; 4 | 5 | import 'package:paintroid/core/models/database/project.dart'; 6 | import 'package:paintroid/core/providers/object/image_service.dart'; 7 | import 'package:paintroid/ui/pages/landing_page/components/image_preview.dart'; 8 | import 'package:paintroid/ui/pages/landing_page/components/project_overflow_menu.dart'; 9 | import 'package:paintroid/ui/theme/theme.dart'; 10 | 11 | class ProjectListTile extends StatelessWidget { 12 | final Project project; 13 | final IImageService imageService; 14 | final int index; 15 | final VoidCallback onTap; 16 | 17 | const ProjectListTile({ 18 | super.key, 19 | required this.project, 20 | required this.imageService, 21 | required this.index, 22 | required this.onTap, 23 | }); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final DateFormat dateFormat = DateFormat('dd-MM-yyyy'); 28 | 29 | return Card( 30 | child: ListTile( 31 | leading: ImagePreview( 32 | project: project, 33 | imageService: imageService, 34 | width: 80, 35 | color: PaintroidTheme.of(context).onSurfaceColor, 36 | ), 37 | dense: false, 38 | title: Text( 39 | project.name, 40 | style: const TextStyle(color: Color(0xFFFFFFFF)), 41 | ), 42 | subtitle: Text( 43 | 'last modified: ${dateFormat.format(project.lastModified)}', 44 | style: const TextStyle(color: Color(0xFFFFFFFF)), 45 | ), 46 | trailing: ProjectOverflowMenu( 47 | key: Key('ProjectOverflowMenu Key$index'), 48 | project: project, 49 | ), 50 | enabled: true, 51 | onTap: onTap, 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/ui/pages/onboarding_page/components/bottom_nav_bar_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/pages/onboarding_page/components/onboarding_page_bottom_nav_bar.dart'; 4 | import 'package:paintroid/ui/theme/theme.dart'; 5 | 6 | class BottomNavigationBarContainer extends StatelessWidget { 7 | final List navBarItems; 8 | final List onPressedFunctions; 9 | 10 | const BottomNavigationBarContainer({ 11 | super.key, 12 | required this.navBarItems, 13 | required this.onPressedFunctions, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Container( 19 | padding: const EdgeInsets.symmetric(vertical: 10), 20 | color: PaintroidTheme.of(context).surfaceColor, 21 | child: OnboardingPageBottomNavigationBar( 22 | onPressedFunctions: onPressedFunctions, 23 | barItems: navBarItems, 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/ui/pages/onboarding_page/components/onboarding_page_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class OnboardingPageAppBar extends AppBar { 4 | final List onPressed; 5 | 6 | OnboardingPageAppBar({ 7 | super.key, 8 | required String title, 9 | required this.onPressed, 10 | }) : super( 11 | title: Text(title), 12 | centerTitle: false, 13 | automaticallyImplyLeading: false, 14 | actions: [ 15 | IconButton( 16 | key: const Key('undoButton'), 17 | onPressed: onPressed[0], 18 | icon: const Icon(Icons.undo), 19 | ), 20 | IconButton( 21 | key: const Key('redoButton'), 22 | onPressed: onPressed[1], 23 | icon: const Icon(Icons.redo), 24 | ), 25 | ], 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/ui/pages/onboarding_page/components/onboarding_page_bottom_nav_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/theme/theme.dart'; 4 | 5 | 6 | class OnboardingPageBottomNavigationBar extends StatefulWidget { 7 | final List onPressedFunctions; 8 | final List barItems; 9 | 10 | const OnboardingPageBottomNavigationBar( 11 | {super.key, required this.onPressedFunctions, required this.barItems}); 12 | 13 | @override 14 | State createState() => 15 | _OnboardingPageBottomNavigationBarState(); 16 | } 17 | 18 | class _OnboardingPageBottomNavigationBarState 19 | extends State { 20 | int _currentIndex = 0; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return BottomNavigationBar( 25 | type: BottomNavigationBarType.fixed, 26 | currentIndex: _currentIndex, 27 | backgroundColor: PaintroidTheme.of(context).surfaceColor, 28 | selectedItemColor: PaintroidTheme.of(context).onSurfaceColor, 29 | unselectedItemColor: PaintroidTheme.of(context).onSurfaceColor, 30 | selectedFontSize: 12, 31 | unselectedFontSize: 12, 32 | onTap: (value) { 33 | widget.onPressedFunctions[value](); 34 | setState(() => _currentIndex = value); 35 | }, 36 | items: widget.barItems, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/ui/pages/onboarding_page/screens/screen1.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/theme/theme.dart'; 4 | 5 | 6 | class Screen1 extends StatelessWidget { 7 | const Screen1({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Container( 12 | color: PaintroidTheme.of(context).surfaceColor, 13 | padding: const EdgeInsets.only(top: 100, left: 50, right: 50), 14 | child: Column( 15 | children: [ 16 | Text( 17 | 'Welcome To Pocket Paint', 18 | style: TextStyle( 19 | color: PaintroidTheme.of(context).onSurfaceColor, 20 | fontSize: 25, 21 | ), 22 | ), 23 | Container( 24 | padding: const EdgeInsets.symmetric(vertical: 50), 25 | child: Text( 26 | 'With Pocket Paint there are no limits to your creativity. If you are new, start the intro, or skip it if you are already familiar with Pocket Paint.', 27 | style: TextStyle( 28 | color: PaintroidTheme.of(context).onSurfaceColor, 29 | fontSize: 15, 30 | ), 31 | ), 32 | ), 33 | ], 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/bottom_bar/bottom_nav_bar_items.dart: -------------------------------------------------------------------------------- 1 | enum BottomNavBarItem { 2 | TOOLS, 3 | TOOL_OPTIONS, 4 | COLOR, 5 | LAYERS, 6 | } 7 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/bottom_bar/tool_options/shapes_tool_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/widgets/shapes_tool_shape_type_options.dart'; 3 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/widgets/shapes_tool_transformation_mode_options.dart'; 4 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/widgets/stroke_width_slider.dart'; 5 | 6 | class ShapesToolOptions extends StatelessWidget { 7 | const ShapesToolOptions({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return const Column( 12 | children: [ 13 | StrokeWidthSlider(), 14 | Spacer(), 15 | Row( 16 | children: [ 17 | ShapesToolShapeTypeOptions(), 18 | Spacer(), 19 | ShapesToolTransformationModeOptions() 20 | ], 21 | ), 22 | ], 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/bottom_bar/tool_options/spray_tool_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/widgets/radius_slider.dart'; 3 | 4 | class SprayToolOptions extends StatelessWidget { 5 | const SprayToolOptions({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return const Column(children: [ 10 | RadiusSlider(), 11 | Spacer(), 12 | ]); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/bottom_bar/tool_options/stroke_tool_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/widgets/stroke_cap_chips.dart'; 3 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/widgets/stroke_width_slider.dart'; 4 | 5 | class StrokeToolOptions extends StatelessWidget { 6 | const StrokeToolOptions({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return const Column(children: [ 11 | StrokeWidthSlider(), 12 | Spacer(), 13 | StrokeCapChips(), 14 | ]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/bottom_bar/tool_options/tool_option.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ToolOption extends StatelessWidget { 4 | final bool isIgnoring; 5 | final double opacity; 6 | final Widget child; 7 | final Duration duration; 8 | 9 | const ToolOption({ 10 | super.key, 11 | required this.isIgnoring, 12 | required this.opacity, 13 | required this.child, 14 | this.duration = const Duration(milliseconds: 300), 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return IgnorePointer( 20 | ignoring: isIgnoring, 21 | child: AnimatedOpacity( 22 | opacity: opacity, 23 | duration: duration, 24 | child: child, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/bottom_bar/tool_options/tool_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:paintroid/core/enums/tool_types.dart'; 4 | import 'package:paintroid/core/providers/state/tool_options_visibility_state_provider.dart'; 5 | import 'package:paintroid/core/providers/state/toolbox_state_provider.dart'; 6 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/shapes_tool_options.dart'; 7 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/spray_tool_options.dart'; 8 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/stroke_tool_options.dart'; 9 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/tool_option.dart'; 10 | 11 | class ToolOptions extends ConsumerWidget { 12 | const ToolOptions({super.key}); 13 | 14 | final maxOpacity = 1.0; 15 | final minOpacity = 0.0; 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | bool visible = ref.watch(toolOptionsVisibilityStateProvider); 20 | final currentToolType = ref.watch( 21 | toolBoxStateProvider.select((value) => value.currentTool.type), 22 | ); 23 | 24 | return Padding( 25 | padding: const EdgeInsets.all(8), 26 | child: ToolOption( 27 | isIgnoring: !visible, 28 | opacity: visible ? maxOpacity : minOpacity, 29 | child: switch (currentToolType) { 30 | ToolType.BRUSH => const StrokeToolOptions(), 31 | ToolType.ERASER => const StrokeToolOptions(), 32 | ToolType.LINE => const StrokeToolOptions(), 33 | ToolType.SHAPES => const ShapesToolOptions(), 34 | ToolType.SPRAY => const SprayToolOptions(), 35 | _ => Container(), 36 | }, 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/bottom_bar/tools/tool_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | import 'package:paintroid/core/providers/state/toolbox_state_provider.dart'; 6 | import 'package:paintroid/core/tools/tool_data.dart'; 7 | import 'package:paintroid/ui/shared/icon_button_with_label.dart'; 8 | import 'package:paintroid/ui/shared/icon_svg.dart'; 9 | import 'package:paintroid/ui/theme/theme.dart'; 10 | 11 | class ToolButton extends StatelessWidget { 12 | final ToolData toolData; 13 | 14 | const ToolButton({ 15 | super.key, 16 | required this.toolData, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Consumer( 22 | builder: (context, ref, child) { 23 | return SizedBox( 24 | width: 50.0, 25 | child: Padding( 26 | padding: const EdgeInsets.symmetric( 27 | horizontal: 8.0, 28 | vertical: 8.0, 29 | ), 30 | child: IconButtonWithLabel( 31 | icon: IconSvg( 32 | path: toolData.svgAssetPath, 33 | height: 30.0, 34 | width: 30.0, 35 | color: PaintroidTheme.of(context).onSurfaceColor, 36 | ), 37 | label: toolData.name, 38 | key: ValueKey(toolData.name), 39 | onPressed: () { 40 | Navigator.pop(context); 41 | ref.read(toolBoxStateProvider.notifier).switchTool(toolData); 42 | }, 43 | ), 44 | ), 45 | ); 46 | }, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/bottom_bar/tools/tools_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/core/tools/tool_data.dart'; 4 | import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tools/tool_button.dart'; 5 | 6 | 7 | 8 | class ToolsBottomSheet extends StatelessWidget { 9 | const ToolsBottomSheet({ 10 | super.key, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | const tools = ToolData.allToolsData; 16 | Orientation currentOrientation = MediaQuery.of(context).orientation; 17 | return GridView.count( 18 | crossAxisCount: currentOrientation == Orientation.portrait ? 4 : 8, 19 | mainAxisSpacing: 0.0, 20 | crossAxisSpacing: 0.0, 21 | childAspectRatio: 1.0, 22 | children: tools.map((toolData) { 23 | return ToolButton( 24 | toolData: toolData, 25 | ); 26 | }).toList(), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/drawing_surface/checkerboard_pattern.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/shared/images/checkerboard.dart'; 4 | 5 | class CheckerboardPattern extends StatelessWidget { 6 | final Widget? child; 7 | 8 | const CheckerboardPattern({super.key, this.child}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Stack( 13 | children: [ 14 | const Positioned.fill( 15 | child: CheckerboardImage(), 16 | ), 17 | if (child != null) child!, 18 | ], 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/ui/pages/workspace_page/components/drawing_surface/exit_fullscreen_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | import 'package:paintroid/core/providers/state/toolbox_state_provider.dart'; 6 | import 'package:paintroid/core/providers/state/workspace_state_notifier.dart'; 7 | import 'package:paintroid/ui/theme/theme.dart'; 8 | 9 | class ExitFullscreenButton extends ConsumerWidget { 10 | const ExitFullscreenButton({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final isUserDrawing = ref.watch( 15 | toolBoxStateProvider.select((state) => state.isDown), 16 | ); 17 | return AnimatedOpacity( 18 | opacity: isUserDrawing ? 0 : 1, 19 | duration: const Duration(milliseconds: 200), 20 | child: IconButton( 21 | onPressed: () { 22 | ref.read(workspaceStateProvider.notifier).toggleFullscreen(false); 23 | }, 24 | icon: Icon( 25 | Icons.fullscreen_exit, 26 | color: PaintroidTheme.of(context).shadowColor, 27 | ), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/ui/shared/action_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:paintroid/ui/theme/data/paintroid_theme.dart'; 3 | 4 | class ActionButton extends StatelessWidget { 5 | final VoidCallback? onPressed; 6 | final IconData icon; 7 | final String valueKey; 8 | 9 | const ActionButton({ 10 | super.key, 11 | required this.onPressed, 12 | required this.icon, 13 | required this.valueKey, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return SizedBox( 19 | width: 36, 20 | child: IconButton( 21 | padding: EdgeInsets.zero, 22 | constraints: const BoxConstraints(), 23 | key: ValueKey(valueKey), 24 | icon: Icon(icon), 25 | onPressed: onPressed, 26 | disabledColor: PaintroidTheme.of(context).onSurfaceColor 27 | ..withValues(alpha: 0.4), 28 | color: PaintroidTheme.of(context).onSurfaceColor, 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/shared/bottom_nav_bar_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/shared/icon_svg.dart'; 4 | import 'package:paintroid/ui/theme/theme.dart'; 5 | 6 | class BottomBarIcon extends StatelessWidget { 7 | final String asset; 8 | 9 | const BottomBarIcon({super.key, required this.asset}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return IconSvg( 14 | path: asset, 15 | height: 24.0, 16 | width: 24.0, 17 | color: PaintroidTheme.of(context).onSurfaceColor, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/ui/shared/custom_action_chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomActionChip extends StatelessWidget { 4 | final Icon chipIcon; 5 | final VoidCallback onPressed; 6 | final OutlinedBorder? shape; 7 | final Color chipBackgroundColor; 8 | final EdgeInsetsGeometry? padding; 9 | final MaterialTapTargetSize? materialTapTargetSize; 10 | final String hint; 11 | 12 | const CustomActionChip({ 13 | super.key, 14 | required this.chipIcon, 15 | required this.onPressed, 16 | required this.chipBackgroundColor, 17 | required this.hint, 18 | this.shape, 19 | this.padding, 20 | this.materialTapTargetSize, 21 | }); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return ActionChip( 26 | tooltip: hint, 27 | label: chipIcon, 28 | onPressed: onPressed, 29 | shape: shape ?? 30 | const RoundedRectangleBorder( 31 | borderRadius: BorderRadius.all(Radius.circular(20)), 32 | ), 33 | backgroundColor: chipBackgroundColor, 34 | padding: padding ?? const EdgeInsets.fromLTRB(8, 0, 8, 0), 35 | materialTapTargetSize: 36 | materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/ui/shared/dialogs/delete_project_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:paintroid/core/utils/widget_identifier.dart'; 3 | import 'package:paintroid/ui/shared/dialogs/generic_dialog.dart'; 4 | 5 | Future showDeleteDialog(BuildContext context, String name) => 6 | showGeneralDialog( 7 | context: context, 8 | pageBuilder: (_, __, ___) => GenericDialog( 9 | title: 'Delete $name', 10 | text: 'Do you really want to delete your project?', 11 | actions: [ 12 | GenericDialogAction( 13 | title: 'Cancel', 14 | onPressed: () => Navigator.of(context).pop(false), 15 | identifier: WidgetIdentifier.genericDialogActionCancel, 16 | ), 17 | GenericDialogAction( 18 | title: 'Delete', 19 | onPressed: () => Navigator.of(context).pop(true), 20 | identifier: WidgetIdentifier.genericDialogActionDelete, 21 | ), 22 | ]), 23 | barrierDismissible: true, 24 | barrierLabel: 'Show delete project dialog box'); 25 | -------------------------------------------------------------------------------- /lib/ui/shared/dialogs/discard_changes_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:paintroid/core/utils/widget_identifier.dart'; 3 | import 'package:paintroid/ui/shared/dialogs/generic_dialog.dart'; 4 | 5 | Future showDiscardChangesDialog(BuildContext context) => 6 | showGeneralDialog( 7 | context: context, 8 | pageBuilder: (_, __, ___) => GenericDialog( 9 | title: 'Discard changes', 10 | text: 11 | 'You have not saved your last changes. They will be lost!', 12 | actions: [ 13 | GenericDialogAction( 14 | title: 'Discard', 15 | onPressed: () => Navigator.of(context).pop(true), 16 | identifier: WidgetIdentifier.genericDialogActionDiscard, 17 | ), 18 | GenericDialogAction( 19 | title: 'Save', 20 | onPressed: () => Navigator.of(context).pop(false), 21 | identifier: WidgetIdentifier.genericDialogActionSave, 22 | ), 23 | ]), 24 | barrierDismissible: true, 25 | barrierLabel: 'Dismiss discard changes dialog box'); 26 | -------------------------------------------------------------------------------- /lib/ui/shared/dialogs/load_image_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:paintroid/core/enums/image_location.dart'; 3 | import 'package:paintroid/core/utils/widget_identifier.dart'; 4 | import 'package:paintroid/ui/shared/dialogs/generic_dialog.dart'; 5 | 6 | Future showLoadImageDialog(BuildContext context) => 7 | showGeneralDialog( 8 | context: context, 9 | pageBuilder: (_, __, ___) => GenericDialog( 10 | title: 'Load image', 11 | text: 'Where do you want to load the image from?', 12 | actions: [ 13 | GenericDialogAction( 14 | title: 'Photos', 15 | onPressed: () => 16 | Navigator.of(context).pop(ImageLocation.photos), 17 | identifier: WidgetIdentifier.genericDialogActionPhotos, 18 | ), 19 | GenericDialogAction( 20 | title: 'Files', 21 | onPressed: () => 22 | Navigator.of(context).pop(ImageLocation.files), 23 | identifier: WidgetIdentifier.genericDialogActionFiles, 24 | ), 25 | ], 26 | ), 27 | barrierDismissible: true, 28 | barrierLabel: 'Dismiss load image dialog box'); 29 | -------------------------------------------------------------------------------- /lib/ui/shared/dialogs/overwrite_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:paintroid/core/utils/widget_identifier.dart'; 3 | import 'package:paintroid/ui/shared/dialogs/generic_dialog.dart'; 4 | 5 | Future showOverwriteDialog(BuildContext context) => 6 | showGeneralDialog( 7 | context: context, 8 | pageBuilder: (_, __, ___) => GenericDialog( 9 | title: 'Overwrite', 10 | text: 'Are you sure you want to overwrite the existing' 11 | ' file?\n\nThis action cannot be undone.', 12 | actions: [ 13 | GenericDialogAction( 14 | title: 'Cancel', 15 | onPressed: () => Navigator.of(context).pop(true), 16 | identifier: WidgetIdentifier.genericDialogActionCancel, 17 | ), 18 | GenericDialogAction( 19 | title: 'Yes', 20 | onPressed: () => Navigator.of(context).pop(false), 21 | identifier: WidgetIdentifier.genericDialogActionYes, 22 | ), 23 | ], 24 | ), 25 | barrierDismissible: true, 26 | barrierLabel: 'Show overwrite dialog', 27 | ); 28 | -------------------------------------------------------------------------------- /lib/ui/shared/icon_button_with_label.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | // Project imports: 3 | import 'package:paintroid/ui/theme/data/paintroid_theme.dart'; 4 | 5 | class IconButtonWithLabel extends StatelessWidget { 6 | final Widget icon; 7 | final String label; 8 | final VoidCallback onPressed; 9 | 10 | const IconButtonWithLabel({ 11 | super.key, 12 | required this.icon, 13 | required this.label, 14 | required this.onPressed, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Column( 20 | children: [ 21 | IconButton( 22 | icon: icon, 23 | onPressed: onPressed, 24 | ), 25 | FittedBox( 26 | child: Text( 27 | label, 28 | style: PaintroidTheme.of(context).descStyle.copyWith(fontSize: 14), 29 | ), 30 | ), 31 | ], 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/shared/icon_svg.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_svg/svg.dart'; 4 | 5 | class IconSvg extends StatelessWidget { 6 | final String path; 7 | final double height; 8 | final double width; 9 | final Color? color; 10 | 11 | const IconSvg({ 12 | super.key, 13 | required this.path, 14 | required this.height, 15 | required this.width, 16 | this.color, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return SvgPicture.asset( 22 | path, 23 | height: height, 24 | width: width, 25 | color: color, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/ui/shared/image_format_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/core/enums/image_format.dart'; 4 | import 'package:paintroid/ui/theme/theme.dart'; 5 | 6 | extension on ImageFormat { 7 | TextSpan get info { 8 | switch (this) { 9 | case ImageFormat.png: 10 | return const TextSpan( 11 | text: 'Lossless compression. Transparency is preserved'); 12 | case ImageFormat.jpg: 13 | return const TextSpan( 14 | text: 'Takes up ', 15 | children: [ 16 | TextSpan( 17 | text: 'minimal storage space.\nNo transparency ', 18 | style: TextStyle(fontWeight: FontWeight.bold), 19 | ), 20 | TextSpan(text: 'is remembered.'), 21 | ], 22 | ); 23 | case ImageFormat.catrobatImage: 24 | return const TextSpan( 25 | text: 'Pocket Paint\'s native image format. ' 26 | 'This format remembers commands and layers.'); 27 | } 28 | } 29 | } 30 | 31 | class ImageFormatInfo extends StatelessWidget { 32 | final ImageFormat format; 33 | 34 | const ImageFormatInfo(this.format, {super.key}); 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Row( 39 | children: [ 40 | Icon(Icons.info_outline, color: PaintroidTheme.of(context).shadowColor), 41 | VerticalDivider( 42 | width: 8, 43 | color: PaintroidTheme.of(context).shadowColor, 44 | ), 45 | Flexible( 46 | child: Text.rich( 47 | format.info, 48 | style: TextStyle( 49 | fontSize: 11, 50 | color: PaintroidTheme.of(context).shadowColor, 51 | ), 52 | ), 53 | ) 54 | ], 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/ui/shared/images/checkerboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class CheckerboardImage extends StatelessWidget { 4 | const CheckerboardImage({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return SizedBox( 9 | child: Image.asset( 10 | 'assets/img/checkerboard.png', 11 | repeat: ImageRepeat.repeat, 12 | cacheWidth: 50, 13 | cacheHeight: 50, 14 | filterQuality: FilterQuality.none, 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/ui/shared/images/pocketpaint_intro_landscape.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class PocketPaintIntroLandscape extends StatelessWidget { 4 | const PocketPaintIntroLandscape({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return SizedBox( 9 | child: Image.asset( 10 | 'assets/img/pocketpaint_intro_landscape.png', 11 | fit: BoxFit.contain, 12 | ), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/ui/shared/images/pocketpaint_intro_portrait.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class PocketPaintIntroPortrait extends StatelessWidget { 4 | const PocketPaintIntroPortrait({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return SizedBox( 9 | child: Image.asset( 10 | 'assets/img/pocketpaint_intro_portrait.png', 11 | fit: BoxFit.fitHeight, 12 | ), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/ui/shared/images/pocketpaint_logo_small.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class PocketPaintLogoSmall extends StatelessWidget { 4 | const PocketPaintLogoSmall({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return SizedBox( 9 | child: Image.asset( 10 | 'assets/img/pocketpaint_logo_small.png', 11 | ), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/ui/shared/pop_menu_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/theme/theme.dart'; 4 | 5 | class StyledPopMenuButton extends StatelessWidget { 6 | final void Function(T) onSelected; 7 | final PopupMenuItemBuilder itemBuilder; 8 | 9 | const StyledPopMenuButton({ 10 | super.key, 11 | required this.onSelected, 12 | required this.itemBuilder, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Theme( 18 | data: Theme.of(context), 19 | child: PopupMenuButton( 20 | color: PaintroidTheme.of(context).backgroundColor, 21 | icon: Icon( 22 | Icons.more_vert, 23 | color: PaintroidTheme.of(context).onSurfaceColor, 24 | ), 25 | elevation: 7.0, 26 | shape: RoundedRectangleBorder( 27 | side: BorderSide( 28 | width: 0, 29 | color: PaintroidTheme.of(context).backgroundColor, 30 | ), 31 | borderRadius: BorderRadius.circular(5), 32 | ), 33 | onSelected: onSelected, 34 | itemBuilder: itemBuilder, 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/ui/shared/text_input_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/theme/data/paintroid_theme.dart'; 4 | 5 | class TextInputField extends StatelessWidget { 6 | final TextEditingController controller; 7 | final String? hintText; 8 | final String? Function(String?)? validator; 9 | 10 | const TextInputField({ 11 | super.key, 12 | required this.controller, 13 | this.hintText, 14 | required this.validator, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return TextFormField( 20 | controller: controller, 21 | decoration: InputDecoration( 22 | hintText: hintText ?? '', 23 | hintStyle: PaintroidTheme.of(context).hintStyle, 24 | filled: true, 25 | fillColor: PaintroidTheme.of(context).secondaryContainerColor, 26 | border: const OutlineInputBorder( 27 | borderRadius: BorderRadius.all(Radius.circular(8))), 28 | ), 29 | validator: validator, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/theme/data/font_size.dart: -------------------------------------------------------------------------------- 1 | abstract class FontSize { 2 | static const double small = 13; 3 | static const double smallMedium = 14; 4 | static const double medium = 16; 5 | static const double mediumLarge = 18; 6 | static const double large = 24; 7 | static const double xLarge = 30; 8 | static const double xxLarge = 64; 9 | } 10 | -------------------------------------------------------------------------------- /lib/ui/theme/data/paintroid_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/ui/theme/theme.dart'; 4 | 5 | 6 | class PaintroidTheme extends InheritedWidget { 7 | const PaintroidTheme({ 8 | required super.child, 9 | required this.lightTheme, 10 | required this.darkTheme, 11 | super.key, 12 | }); 13 | 14 | final PaintroidThemeData lightTheme; 15 | final PaintroidThemeData darkTheme; 16 | 17 | @override 18 | bool updateShouldNotify( 19 | PaintroidTheme oldWidget, 20 | ) => 21 | oldWidget.lightTheme != lightTheme || oldWidget.darkTheme != darkTheme; 22 | 23 | static PaintroidThemeData of(BuildContext context) { 24 | final PaintroidTheme? inheritedTheme = 25 | context.dependOnInheritedWidgetOfExactType(); 26 | assert(inheritedTheme != null, 'No PaintroidTheme found in context'); 27 | final currentBrightness = Theme.of(context).brightness; 28 | return currentBrightness == Brightness.dark 29 | ? inheritedTheme!.darkTheme 30 | : inheritedTheme!.lightTheme; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/theme/data/spacing.dart: -------------------------------------------------------------------------------- 1 | abstract class Spacing { 2 | static const double xSmall = 4; 3 | static const double small = 8; 4 | static const double medium = 12; 5 | static const double mediumLarge = 16; 6 | static const double large = 20; 7 | static const double xLarge = 24; 8 | static const double xxLarge = 48; 9 | static const double xxxLarge = 64; 10 | } 11 | -------------------------------------------------------------------------------- /lib/ui/theme/state/theme_mode_state_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | part 'theme_mode_state_provider.g.dart'; 6 | 7 | @riverpod 8 | class ThemeModeState extends _$ThemeModeState { 9 | Future getDefaultThemeMode() async { 10 | final prefs = await SharedPreferences.getInstance(); 11 | final String? themeMode = prefs.getString('themeMode'); 12 | 13 | if (themeMode == null) { 14 | return; 15 | } 16 | 17 | switch (themeMode) { 18 | case 'light': 19 | state = ThemeMode.light; 20 | break; 21 | case 'dark': 22 | state = ThemeMode.dark; 23 | break; 24 | case 'system': 25 | state = ThemeMode.system; 26 | break; 27 | default: 28 | state = ThemeMode.system; 29 | } 30 | } 31 | 32 | Future setThemeMode(ThemeMode themeMode) async { 33 | final prefs = await SharedPreferences.getInstance(); 34 | state = ThemeMode.system; 35 | switch (themeMode) { 36 | case ThemeMode.light: 37 | await prefs.setString('themeMode', 'light'); 38 | break; 39 | case ThemeMode.dark: 40 | await prefs.setString('themeMode', 'dark'); 41 | break; 42 | case ThemeMode.system: 43 | await prefs.setString('themeMode', 'system'); 44 | break; 45 | } 46 | } 47 | 48 | @override 49 | ThemeMode build() { 50 | return ThemeMode.system; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/ui/theme/state/theme_mode_state_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'theme_mode_state_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$themeModeStateHash() => r'b632c908a95b55027eb70f5c49e5c358ddc660c9'; 10 | 11 | /// See also [ThemeModeState]. 12 | @ProviderFor(ThemeModeState) 13 | final themeModeState = 14 | AutoDisposeNotifierProvider.internal( 15 | ThemeModeState.new, 16 | name: r'themeModeState', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$themeModeStateHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$ThemeModeState = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 27 | -------------------------------------------------------------------------------- /lib/ui/theme/theme.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'data/custom_colors.dart'; 4 | export 'data/dark_paintroid_theme_data.dart'; 5 | export 'data/font_size.dart'; 6 | export 'data/light_paintroid_theme_data.dart'; 7 | export 'data/paintroid_theme.dart'; 8 | export 'data/paintroid_theme_data.dart'; 9 | export 'data/spacing.dart'; 10 | export 'state/theme_mode_state_provider.dart'; 11 | -------------------------------------------------------------------------------- /lib/ui/utils/toast_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:toast/toast.dart'; 2 | 3 | class ToastUtils { 4 | const ToastUtils._(); 5 | 6 | static void showShortToast({required String message}) { 7 | Toast.show(message, duration: Toast.lengthShort, gravity: Toast.bottom); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/ui/utils/top_bar_action_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TopBarActionData { 4 | final String name; 5 | final IconData iconData; 6 | 7 | const TopBarActionData._( 8 | this.name, 9 | this.iconData, 10 | ); 11 | 12 | static const CHECKMARK = TopBarActionData._( 13 | 'CheckMarkButton', 14 | Icons.check, 15 | ); 16 | 17 | static const PLUS = TopBarActionData._( 18 | 'PlusButton', 19 | Icons.add, 20 | ); 21 | 22 | static const UNDO = TopBarActionData._( 23 | 'UndoButton', 24 | Icons.undo, 25 | ); 26 | 27 | static const REDO = TopBarActionData._( 28 | 'RedoButton', 29 | Icons.redo, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/colorpicker/.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 and should not be manually edited. 5 | 6 | version: 7 | revision: "9e1c857886f07d342cf106f2cd588bcd5e031bb2" 8 | channel: "stable" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /packages/colorpicker/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | linter: 3 | rules: 4 | always_use_package_imports: true 5 | avoid_relative_lib_imports: true 6 | prefer_relative_imports: false 7 | prefer_single_quotes: true 8 | avoid_void_async: true 9 | constant_identifier_names: false 10 | 11 | analyzer: 12 | errors: 13 | missing_enum_constant_in_switch: error 14 | exhaustive_cases: error 15 | unused_element: error 16 | type_annotate_public_apis: error 17 | missing_required_param: error 18 | invalid_use_of_protected_member: error 19 | unused_import: error 20 | 21 | exclude: 22 | - lib/src/**.pb*.dart 23 | - lib/src/data/*.g.dart 24 | -------------------------------------------------------------------------------- /packages/colorpicker/assets/img/checkerboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/packages/colorpicker/assets/img/checkerboard.png -------------------------------------------------------------------------------- /packages/colorpicker/lib/colorpicker.dart: -------------------------------------------------------------------------------- 1 | library colorpicker; 2 | 3 | export 'src/colorpicker.dart'; 4 | export 'src/constants/colors.dart'; 5 | export 'src/components/color_comparison.dart'; 6 | export 'src/components/opacity_slider.dart'; 7 | export 'src/components/slider_indicator_shape.dart'; 8 | -------------------------------------------------------------------------------- /packages/colorpicker/lib/src/components/checkerboard_square.dart: -------------------------------------------------------------------------------- 1 | import 'package:colorpicker/utils/assets.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CheckerboardSquare extends StatelessWidget { 5 | const CheckerboardSquare({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Container( 10 | decoration: BoxDecoration( 11 | image: DecorationImage( 12 | image: PackageAssets.getCheckerboardImgAsset(), 13 | fit: BoxFit.contain, 14 | repeat: ImageRepeat.repeat, 15 | ), 16 | ), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/colorpicker/lib/src/components/color_square.dart: -------------------------------------------------------------------------------- 1 | import 'package:colorpicker/src/state/color_picker_state_provider.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | class ColorSquare extends ConsumerWidget { 6 | const ColorSquare({ 7 | super.key, 8 | required this.color, 9 | }); 10 | 11 | final Color color; 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | return GestureDetector( 16 | onTap: () { 17 | ref.read(colorPickerStateProvider.notifier).updateColor(color); 18 | }, 19 | child: Container( 20 | decoration: BoxDecoration( 21 | color: color, 22 | ), 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/colorpicker/lib/src/components/slider_indicator_shape.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SliderIndicatorShape extends SliderComponentShape { 4 | SliderIndicatorShape(); 5 | 6 | @override 7 | Size getPreferredSize(bool isEnabled, bool isDiscrete) { 8 | return const Size(5.0, 26.0); 9 | } 10 | 11 | @override 12 | void paint( 13 | PaintingContext context, 14 | Offset center, { 15 | Animation? activationAnimation, 16 | Animation? enableAnimation, 17 | bool? isDiscrete, 18 | TextPainter? labelPainter, 19 | RenderBox? parentBox, 20 | SliderThemeData? sliderTheme, 21 | TextDirection? textDirection, 22 | double? value, 23 | double? textScaleFactor, 24 | Size? sizeWithOverflow, 25 | }) { 26 | final Canvas canvas = context.canvas; 27 | 28 | canvas.drawRect( 29 | Rect.fromCenter( 30 | center: center, 31 | width: 5.0, 32 | height: 26.0, 33 | ), 34 | Paint() 35 | ..color = const Color.fromARGB(255, 62, 62, 62) 36 | ..style = PaintingStyle.stroke 37 | ..strokeWidth = 2.0, 38 | ); 39 | } 40 | } 41 | 42 | class CustomTrackShape extends RoundedRectSliderTrackShape { 43 | @override 44 | Rect getPreferredRect({ 45 | required RenderBox parentBox, 46 | Offset offset = Offset.zero, 47 | required SliderThemeData sliderTheme, 48 | bool isEnabled = false, 49 | bool isDiscrete = false, 50 | }) { 51 | final double trackHeight = sliderTheme.trackHeight ?? 2.0; 52 | final double trackWidth = parentBox.size.width; 53 | final double trackTop = 54 | offset.dy + (parentBox.size.height - trackHeight) / 2; 55 | return Rect.fromLTWH(offset.dx, trackTop, trackWidth, trackHeight); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/colorpicker/lib/src/constants/colors.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | class DisplayColors { 4 | static const colors = [ 5 | Color(0xff0073cc), 6 | Color(0xff00b4f1), 7 | Color(0xff068708), 8 | Color(0xff8EC430), 9 | Color(0xff61290e), 10 | Color(0xffa84818), 11 | Color(0xffdeae66), 12 | Color(0xfff0e4a8), 13 | Color(0xff7d1378), 14 | Color(0xffa643d1), 15 | Color(0xffca0086), 16 | Color(0xffeb90d3), 17 | Color(0xffc5060e), 18 | Color(0xffeb4618), 19 | Color(0xfff9921c), 20 | Color(0xfff2d606), 21 | Color(0xff000000), 22 | Color(0xffa3a3a3), 23 | Color(0xffffffff), 24 | ]; 25 | } 26 | -------------------------------------------------------------------------------- /packages/colorpicker/lib/src/state/color_picker_state_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'color_picker_state_data.freezed.dart'; 5 | 6 | @immutable 7 | @freezed 8 | class ColorPickerStateData with _$ColorPickerStateData { 9 | const factory ColorPickerStateData({ 10 | required Color? currentColor, 11 | required double currentOpacity, 12 | }) = _ColorPickerStateData; 13 | } 14 | -------------------------------------------------------------------------------- /packages/colorpicker/lib/src/state/color_picker_state_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:colorpicker/src/state/color_picker_state_data.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | import 'package:colorpicker/src/constants/colors.dart'; 6 | 7 | part 'color_picker_state_provider.g.dart'; 8 | 9 | @riverpod 10 | class ColorPickerState extends _$ColorPickerState { 11 | final colors = DisplayColors.colors; 12 | @override 13 | ColorPickerStateData build() { 14 | return const ColorPickerStateData( 15 | currentColor: null, 16 | currentOpacity: 1.0, 17 | ); 18 | } 19 | 20 | void updateColor(Color newColor) { 21 | state = state.copyWith(currentColor: newColor); 22 | } 23 | 24 | void updateOpacity(double newOpacity) { 25 | state = state.copyWith(currentOpacity: newOpacity); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/colorpicker/lib/src/state/color_picker_state_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'color_picker_state_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$colorPickerStateHash() => r'd9193012d8a7cbf6b50a034ba7f1b9483095df9f'; 10 | 11 | /// See also [ColorPickerState]. 12 | @ProviderFor(ColorPickerState) 13 | final colorPickerStateProvider = AutoDisposeNotifierProvider.internal( 15 | ColorPickerState.new, 16 | name: r'colorPickerStateProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$colorPickerStateHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$ColorPickerState = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 27 | -------------------------------------------------------------------------------- /packages/colorpicker/lib/utils/assets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PackageAssets { 4 | static ImageProvider getCheckerboardImgAsset() { 5 | return const AssetImage( 6 | 'packages/colorpicker/assets/img/checkerboard.png', 7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/colorpicker/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: colorpicker 2 | description: "A new Flutter package project." 3 | version: 0.0.1 4 | publish_to: "none" 5 | 6 | environment: 7 | sdk: ">=2.17.0 <3.0.0" 8 | flutter: ">=1.17.0" 9 | 10 | # this package should be built separately and not depend on other app modules 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | flutter_riverpod: ^2.3.6 15 | riverpod_annotation: ^2.1.1 16 | freezed_annotation: ^2.4.1 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | flutter_lints: ^2.0.0 22 | build_runner: ^2.2.0 23 | riverpod_generator: ^2.2.3 24 | riverpod_lint: ^2.1.1 25 | freezed: ^2.4.1 26 | 27 | flutter: 28 | uses-material-design: true 29 | 30 | assets: 31 | - assets/img/ -------------------------------------------------------------------------------- /packages/colorpicker/test/unit/color_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:colorpicker/src/state/color_picker_state_provider.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | late ProviderContainer container; 8 | 9 | setUp(() { 10 | container = ProviderContainer(); 11 | }); 12 | 13 | tearDown(() { 14 | container.dispose(); 15 | }); 16 | 17 | test('updateColor updates the color correctly', () { 18 | final state = container.read(colorPickerStateProvider.notifier); 19 | const newColor = Colors.red; 20 | state.updateColor(newColor); 21 | expect(container.read(colorPickerStateProvider).currentColor, newColor); 22 | }); 23 | 24 | test('updateOpacity updates the opacity correctly', () { 25 | final state = container.read(colorPickerStateProvider.notifier); 26 | const newOpacity = 0.5; 27 | state.updateOpacity(newOpacity); 28 | expect(container.read(colorPickerStateProvider).currentOpacity, newOpacity); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: paintroid 2 | description: The standard image manipulation app for Catroid. 3 | 4 | publish_to: "none" 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ^3.6.2 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | flutter_localizations: 15 | sdk: flutter 16 | 17 | flutter_localization: ^0.3.2 18 | intl: ^0.19.0 19 | logging: ^1.0.2 20 | flutter_riverpod: ^2.3.6 21 | riverpod_annotation: ^2.1.1 22 | shared_preferences: ^2.0.15 23 | freezed_annotation: ^2.4.1 24 | equatable: ^2.0.3 25 | json_annotation: ^4.8.1 26 | collection: ^1.17.1 27 | url_launcher: ^6.1.6 28 | toast: ^0.3.0 29 | oxidized: ^5.2.0 30 | flutter_svg: ^1.1.0 31 | launch_review_latest: ^1.0.0 32 | package_info_plus: ^8.3.0 33 | filesize: ^2.0.1 34 | smooth_page_indicator: ^1.0.0+2 35 | image: ^3.2.0 36 | permission_handler: ^11.4.0 37 | device_info_plus: ^9.1.2 38 | image_picker: ^1.1.2 39 | path_provider: ^2.0.11 40 | file_picker: ^8.3.5 41 | floor: ^1.2.0 42 | sqflite: ^2.3.0 43 | colorpicker: 44 | path: packages/colorpicker 45 | 46 | dev_dependencies: 47 | flutter_test: 48 | sdk: flutter 49 | integration_test: 50 | sdk: flutter 51 | 52 | mockito: ^5.2.0 53 | flutter_launcher_icons: ^0.9.3 54 | flutter_lints: ^5.0.0 55 | floor_generator: ^1.4.2 56 | riverpod_generator: ^2.2.4 57 | riverpod_lint: ^2.0.0 58 | build_runner: ^2.2.0 59 | freezed: ^2.4.1 60 | riverpod: ^2.3.7 61 | json_serializable: ^6.7.1 62 | 63 | 64 | flutter: 65 | uses-material-design: true 66 | 67 | assets: 68 | - assets/icon/ 69 | - assets/img/ 70 | - assets/svg/ 71 | - assets/lang/ 72 | - test/assets/images/ 73 | -------------------------------------------------------------------------------- /test/assets/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/test/assets/images/test.jpg -------------------------------------------------------------------------------- /test/assets/images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/test/assets/images/test.png -------------------------------------------------------------------------------- /test/assets/images/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Catrobat/Paintroid-Flutter/135cf28db1ad631b7bdf93984e47d31480dab1ed/test/assets/images/test1.png -------------------------------------------------------------------------------- /test/integration/driver/driver.dart: -------------------------------------------------------------------------------- 1 | import 'package:integration_test/integration_test_driver_extended.dart'; 2 | 3 | Future main() => integrationDriver(); 4 | -------------------------------------------------------------------------------- /test/unit/command/command_factory_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'package:paintroid/core/commands/command_factory/command_factory.dart'; 6 | import 'package:paintroid/core/commands/command_implementation/graphic/path_command.dart'; 7 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 8 | 9 | void main() { 10 | late PathWithActionHistory testPath; 11 | late Paint testPaint; 12 | late CommandFactory sut; 13 | 14 | setUp(() { 15 | testPath = PathWithActionHistory(); 16 | testPaint = Paint(); 17 | sut = const CommandFactory(); 18 | }); 19 | 20 | test('Should return a valid instance of PathCommand', () { 21 | final expected = PathCommand(testPath, testPaint); 22 | final command = sut.createPathCommand(testPath, testPaint); 23 | expect(command, isA()); 24 | expect(command, equals(expected)); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/command/draw_path_command_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/annotations.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import 'package:paintroid/core/commands/command_implementation/graphic/path_command.dart'; 8 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 9 | import 'draw_path_command_test.mocks.dart'; 10 | 11 | @GenerateMocks([Canvas]) 12 | void main() { 13 | late MockCanvas mockCanvas; 14 | late PathCommand drawPath; 15 | 16 | setUp(() { 17 | mockCanvas = MockCanvas(); 18 | }); 19 | 20 | test( 21 | 'drawPath method is called on the Canvas with given Path and Paint objects', 22 | () { 23 | final testPath = PathWithActionHistory(); 24 | final testPaint = Paint(); 25 | drawPath = PathCommand(testPath, testPaint); 26 | when(mockCanvas.drawPath(testPath.path, testPaint)).thenReturn(null); 27 | drawPath.call(mockCanvas); 28 | verify(mockCanvas.drawPath(testPath.path, testPaint)); 29 | verifyNoMoreInteractions(mockCanvas); 30 | }, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /test/unit/command/shape_command_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/annotations.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | import 'package:paintroid/core/commands/command_implementation/graphic/shape/circle_shape_command.dart'; 6 | import 'package:paintroid/core/commands/command_implementation/graphic/shape/square_shape_command.dart'; 7 | import 'shape_command_test.mocks.dart'; 8 | 9 | @GenerateMocks([Canvas]) 10 | void main() { 11 | Paint testPaint = Paint(); 12 | late MockCanvas mockCanvas; 13 | 14 | final squareShapeCommand = SquareShapeCommand( 15 | testPaint, 16 | const Offset(0, 0), 17 | const Offset(200, 0), 18 | const Offset(0, 200), 19 | const Offset(200, 200), 20 | ); 21 | 22 | const radius = 5.0; 23 | const center = Offset(200, 200); 24 | final circleShapeCommand = CircleShapeCommand(testPaint, radius, center); 25 | 26 | setUp(() => mockCanvas = MockCanvas()); 27 | 28 | test('SquareShapeCommand: should call drawPath path', () { 29 | when(mockCanvas.drawPath(any, testPaint)).thenReturn(null); 30 | squareShapeCommand.call(mockCanvas); 31 | verify(mockCanvas.drawPath(any, testPaint)); 32 | verifyNoMoreInteractions(mockCanvas); 33 | }); 34 | 35 | test('CircleShapeCommand: should call drawCircle with radius, center', () { 36 | when(mockCanvas.drawCircle(any, any, testPaint)).thenReturn(null); 37 | circleShapeCommand.call(mockCanvas); 38 | verify(mockCanvas.drawCircle(any, any, testPaint)); 39 | verifyNoMoreInteractions(mockCanvas); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/unit/provider/image_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import 'package:paintroid/core/providers/object/image_service.dart'; 7 | 8 | void main() async { 9 | TestWidgetsFlutterBinding.ensureInitialized(); 10 | final testPngFile = await rootBundle.load('test/assets/images/test.png'); 11 | final testJpgFile = await rootBundle.load('test/assets/images/test.jpg'); 12 | late ImageService sut; 13 | 14 | setUp(() async { 15 | sut = ImageService(); 16 | }); 17 | 18 | test('Should provide valid ImageService', () async { 19 | final container = ProviderContainer(); 20 | final imageService = container.read(IImageService.provider); 21 | expect(imageService, isA()); 22 | }); 23 | 24 | group('import method', () { 25 | test('Should return jpg image with valid dimensions', () async { 26 | final result = await sut.import(testJpgFile.buffer.asUint8List()); 27 | final img = result.unwrapOrElse((failure) => fail(failure.message)); 28 | expect(img.width, equals(50)); 29 | expect(img.height, equals(50)); 30 | }); 31 | 32 | test('Should return jpg image with valid dimensions', () async { 33 | final result = await sut.import(testPngFile.buffer.asUint8List()); 34 | final img = result.unwrapOrElse((failure) => fail(failure.message)); 35 | expect(img.width, equals(50)); 36 | expect(img.height, equals(50)); 37 | }); 38 | }); 39 | 40 | test('Should return project preview', () { 41 | const path = 'test/assets/images/test.png'; 42 | final result = sut.getProjectPreview(path); 43 | final imgPreview = result.unwrapOrElse((failure) => fail(failure.message)); 44 | expect(imgPreview, isA()); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/unit/serialization/command/circle_shape_serializer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:paintroid/core/commands/command_implementation/graphic/shape/circle_shape_command.dart'; 3 | import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart'; 4 | 5 | import '../utils/dummy_command_factory.dart'; 6 | import '../utils/dummy_paint_factory.dart'; 7 | 8 | void main() { 9 | group('Version 1', () { 10 | test('Test Circle serialization', () { 11 | const type = SerializerType.CIRCLE_SHAPE_COMMAND; 12 | final originalPaint = DummyPaintFactory.createPaint(version: Version.v1); 13 | const center = Offset(100, 100); 14 | const radius = 50.0; 15 | 16 | final command = DummyCommandFactory.createCircleShapeCommand( 17 | originalPaint, 18 | radius, 19 | center, 20 | version: Version.v1, 21 | ); 22 | 23 | final deserializedCommand = CircleShapeCommand.fromJson(command.toJson()); 24 | 25 | expect( 26 | DummyPaintFactory.comparePaint( 27 | originalPaint, 28 | deserializedCommand.paint, 29 | version: Version.v1, 30 | ), 31 | isTrue); 32 | expect(command.version, equals(deserializedCommand.version)); 33 | expect(deserializedCommand.center, equals(center)); 34 | expect(deserializedCommand.radius, equals(radius)); 35 | expect(deserializedCommand.type, equals(type)); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /test/unit/serialization/command/rectangle_shape_serializer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:paintroid/core/commands/command_implementation/graphic/shape/square_shape_command.dart'; 3 | import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart'; 4 | 5 | import '../utils/dummy_command_factory.dart'; 6 | import '../utils/dummy_paint_factory.dart'; 7 | 8 | void main() { 9 | group('Version 1', () { 10 | test('Test SquareShapeCommand serialization', () { 11 | const type = SerializerType.SQUARE_SHAPE_COMMAND; 12 | 13 | final originalPaint = DummyPaintFactory.createPaint(version: Version.v1); 14 | const originalTopLeft = Offset(0, 0); 15 | const originalTopRight = Offset(1, 0); 16 | const originalBottomLeft = Offset(0, 1); 17 | const originalBottomRight = Offset(1, 1); 18 | 19 | final command = DummyCommandFactory.createSquareShapeCommand( 20 | originalPaint, 21 | originalTopLeft, 22 | originalTopRight, 23 | originalBottomLeft, 24 | originalBottomRight, 25 | version: Version.v1, 26 | ); 27 | 28 | final deserializedCommand = SquareShapeCommand.fromJson(command.toJson()); 29 | 30 | expect( 31 | DummyPaintFactory.comparePaint( 32 | originalPaint, 33 | deserializedCommand.paint, 34 | version: Version.v1, 35 | ), 36 | isTrue); 37 | expect(command.version, equals(deserializedCommand.version)); 38 | expect(deserializedCommand.topLeft, equals(originalTopLeft)); 39 | expect(deserializedCommand.topRight, equals(originalTopRight)); 40 | expect(deserializedCommand.bottomLeft, equals(originalBottomLeft)); 41 | expect(deserializedCommand.bottomRight, equals(originalBottomRight)); 42 | expect(deserializedCommand.type, equals(type)); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/unit/serialization/converter/offset_converter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'package:paintroid/core/json_serialization/converter/offset_converter.dart'; 4 | 5 | void main() { 6 | OffsetConverter converter = const OffsetConverter(); 7 | 8 | test('Basic Offset', () { 9 | Offset offset = const Offset(0.0, 0.0); 10 | var json = converter.toJson(offset); 11 | 12 | Offset deserializedOffset = converter.fromJson(json); 13 | expect(offset, equals(deserializedOffset)); 14 | }); 15 | 16 | test('Positive Offset', () { 17 | Offset offset = const Offset(2.0, 2.0); 18 | var json = converter.toJson(offset); 19 | 20 | Offset deserializedOffset = converter.fromJson(json); 21 | expect(offset, equals(deserializedOffset)); 22 | }); 23 | 24 | test('Negative Offset', () { 25 | Offset offset = const Offset(-1.0, -2.0); 26 | var json = converter.toJson(offset); 27 | 28 | Offset deserializedOffset = converter.fromJson(json); 29 | expect(offset, equals(deserializedOffset)); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/unit/serialization/converter/path_action_converter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 4 | import 'package:paintroid/core/json_serialization/converter/path_action_converter.dart'; 5 | 6 | void main() { 7 | const PathActionConverter converter = PathActionConverter(); 8 | 9 | test('Test converter for MoveToAction', () { 10 | double xExpected = 1.0; 11 | double yExpected = 2.0; 12 | 13 | MoveToAction moveToAction = MoveToAction(xExpected, yExpected); 14 | 15 | var json = converter.toJson(moveToAction); 16 | 17 | PathAction deserializedMoveToAction = converter.fromJson(json); 18 | 19 | expect(deserializedMoveToAction, isA()); 20 | deserializedMoveToAction as MoveToAction; 21 | expect(moveToAction, equals(deserializedMoveToAction)); 22 | }); 23 | 24 | test('Test converter for LineToAction', () { 25 | double xExpected = 1.0; 26 | double yExpected = 2.0; 27 | 28 | LineToAction lineToAction = LineToAction(xExpected, yExpected); 29 | 30 | var json = converter.toJson(lineToAction); 31 | 32 | PathAction deserializedLineToAction = converter.fromJson(json); 33 | 34 | expect(deserializedLineToAction, isA()); 35 | deserializedLineToAction as LineToAction; 36 | expect(lineToAction, equals(deserializedLineToAction)); 37 | }); 38 | 39 | test('Test converter for CloseAction', () { 40 | CloseAction closeAction = const CloseAction(); 41 | 42 | var json = converter.toJson(closeAction); 43 | 44 | PathAction deserializedCloseAction = converter.fromJson(json); 45 | 46 | expect(deserializedCloseAction, isA()); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/unit/serialization/converter/path_with_action_history_converter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 4 | import 'package:paintroid/core/json_serialization/converter/path_with_action_history_converter.dart'; 5 | import '../utils/dummy_path_factory.dart'; 6 | 7 | void main() { 8 | const PathWithActionHistoryConverter converter = 9 | PathWithActionHistoryConverter(); 10 | 11 | test('Test converter for PathWithActionHistory with one path', () { 12 | PathWithActionHistory path = 13 | DummyPathFactory.createPathWithActionHistory(1); 14 | 15 | var json = converter.toJson(path); 16 | 17 | PathWithActionHistory deserializedPath = converter.fromJson(json); 18 | 19 | expect(path, equals(deserializedPath)); 20 | }); 21 | 22 | test('Test converter for PathWithActionHistory with two paths', () { 23 | PathWithActionHistory path = 24 | DummyPathFactory.createPathWithActionHistory(2); 25 | 26 | var json = converter.toJson(path); 27 | 28 | PathWithActionHistory deserializedPath = converter.fromJson(json); 29 | 30 | expect(path, equals(deserializedPath)); 31 | }); 32 | 33 | test('Test converter for PathWithActionHistory with multiple paths', () { 34 | PathWithActionHistory path = 35 | DummyPathFactory.createPathWithActionHistory(10); 36 | 37 | var json = converter.toJson(path); 38 | 39 | PathWithActionHistory deserializedPath = converter.fromJson(json); 40 | 41 | expect(path, equals(deserializedPath)); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/unit/serialization/utils/dummy_paint_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart'; 4 | 5 | class DummyPaintFactory { 6 | static Paint createPaint({int version = Version.v1}) { 7 | Paint paint = Paint(); 8 | if (version >= Version.v1) { 9 | paint.color = Colors.blue; 10 | paint.strokeWidth = 5.0; 11 | paint.strokeCap = StrokeCap.round; 12 | paint.isAntiAlias = true; 13 | paint.style = PaintingStyle.fill; 14 | paint.strokeJoin = StrokeJoin.bevel; 15 | paint.blendMode = BlendMode.clear; 16 | } 17 | if (version >= Version.v2) { 18 | // paint.newAttribute = newAttribute; 19 | } 20 | return paint; 21 | } 22 | 23 | static bool comparePaint(Paint paint1, Paint paint2, 24 | {int version = Version.v1}) { 25 | bool result = true; 26 | if (version >= Version.v1) { 27 | result = paint1.color == paint2.color && 28 | paint1.strokeWidth == paint2.strokeWidth && 29 | paint1.strokeCap == paint2.strokeCap && 30 | paint1.isAntiAlias == paint2.isAntiAlias && 31 | paint1.style == paint2.style && 32 | paint1.strokeJoin == paint2.strokeJoin && 33 | paint1.blendMode == paint2.blendMode; 34 | } 35 | if (version >= Version.v2) { 36 | // result = result && paint1.newAttribute == paint2.newAttribute; 37 | } 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/unit/serialization/utils/dummy_path_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/commands/path_with_action_history.dart'; 2 | 3 | class DummyPathFactory { 4 | static PathWithActionHistory createPathWithActionHistory( 5 | int numberOfActions) { 6 | PathWithActionHistory pathWithActionHistory = PathWithActionHistory(); 7 | for (int i = 0; i < numberOfActions; i++) { 8 | pathWithActionHistory.moveTo(i.toDouble(), i.toDouble() + 1); 9 | pathWithActionHistory.lineTo(i.toDouble() + 2, i.toDouble() + 3); 10 | } 11 | pathWithActionHistory.close(); 12 | return pathWithActionHistory; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/unit/serialization/utils/dummy_version_strategy.dart: -------------------------------------------------------------------------------- 1 | import 'package:paintroid/core/json_serialization/versioning/serializer_version.dart'; 2 | import 'package:paintroid/core/json_serialization/versioning/version_strategy.dart'; 3 | 4 | class DummyVersionStrategy implements IVersionStrategy { 5 | final int pathCommandVersion; 6 | final int lineCommandVersion; 7 | final int catrobatImageVersion; 8 | final int squareShapeCommandVersion; 9 | final int circleShapeCommandVersion; 10 | final int sprayCommandVersion; 11 | 12 | DummyVersionStrategy({ 13 | this.pathCommandVersion = SerializerVersion.PATH_COMMAND_VERSION, 14 | this.catrobatImageVersion = SerializerVersion.CATROBAT_IMAGE_VERSION, 15 | this.lineCommandVersion = SerializerVersion.LINE_COMMAND_VERSION, 16 | this.squareShapeCommandVersion = 17 | SerializerVersion.SQUARE_SHAPE_COMMAND_VERSION, 18 | this.circleShapeCommandVersion = 19 | SerializerVersion.CIRCLE_SHAPE_COMMAND_VERSION, 20 | this.sprayCommandVersion = SerializerVersion.SPRAY_COMMAND_VERSION, 21 | }); 22 | 23 | @override 24 | int getCatrobatImageVersion() => catrobatImageVersion; 25 | 26 | @override 27 | int getPathCommandVersion() => pathCommandVersion; 28 | 29 | @override 30 | int getLineCommandVersion() => lineCommandVersion; 31 | 32 | @override 33 | int getSquareShapeCommandVersion() => squareShapeCommandVersion; 34 | 35 | @override 36 | int getCircleShapeCommandVersion() => circleShapeCommandVersion; 37 | 38 | @override 39 | int getSprayCommandVersion() => sprayCommandVersion; 40 | } 41 | -------------------------------------------------------------------------------- /test/unit/tools/eraser_tool_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'package:paintroid/core/commands/command_factory/command_factory.dart'; 4 | import 'package:paintroid/core/commands/command_manager/command_manager.dart'; 5 | import 'package:paintroid/core/commands/graphic_factory/graphic_factory.dart'; 6 | import 'package:paintroid/core/enums/tool_types.dart'; 7 | import 'package:paintroid/core/tools/implementation/eraser_tool.dart'; 8 | 9 | void main() { 10 | late EraserTool sut; 11 | 12 | setUp(() { 13 | sut = EraserTool( 14 | type: ToolType.ERASER, 15 | commandFactory: const CommandFactory(), 16 | commandManager: CommandManager(), 17 | graphicFactory: const GraphicFactory(), 18 | ); 19 | }); 20 | 21 | test('Should return Eraser as ToolType', () { 22 | expect(sut.type, ToolType.ERASER); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/unit/tools/hand_tool_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'package:paintroid/core/commands/command_factory/command_factory.dart'; 4 | import 'package:paintroid/core/commands/command_manager/command_manager.dart'; 5 | import 'package:paintroid/core/enums/tool_types.dart'; 6 | import 'package:paintroid/core/tools/implementation/hand_tool.dart'; 7 | 8 | void main() { 9 | late HandTool sut; 10 | setUp(() { 11 | sut = HandTool( 12 | type: ToolType.HAND, 13 | commandFactory: const CommandFactory(), 14 | commandManager: CommandManager(), 15 | ); 16 | }); 17 | 18 | test('Should return Hand as ToolType', () { 19 | expect(sut.type, ToolType.HAND); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/utils/interactive_viewer_interactions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | class InterActiveViewerInteractions { 6 | InterActiveViewerInteractions(this._tester); 7 | 8 | double epsilon = 0.1; 9 | 10 | final WidgetTester _tester; 11 | 12 | Future panAndVerify(Offset offset) async { 13 | final finder = find.byType(InteractiveViewer); 14 | 15 | expect(finder, findsOneWidget); 16 | 17 | InteractiveViewer interactiveViewer = _tester.widget(finder); 18 | 19 | TransformationController controller = 20 | interactiveViewer.transformationController!; 21 | 22 | expect(controller, isNotNull); 23 | 24 | final initialMatrix = controller.value; 25 | 26 | await _tester.drag(finder, const Offset(-50, 50)); 27 | await _tester.pumpAndSettle(); 28 | 29 | double expectedX = initialMatrix.getTranslation().x - 50; 30 | double expectedY = initialMatrix.getTranslation().y + 50; 31 | 32 | expect(controller.value.getTranslation().x, closeTo(expectedX, epsilon)); 33 | expect(controller.value.getTranslation().y, closeTo(expectedY, epsilon)); 34 | return this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/utils/test_utils.dart: -------------------------------------------------------------------------------- 1 | export 'bottom_nav_bar_interactions.dart'; 2 | export 'canvas_interactions.dart'; 3 | export 'canvas_positions.dart'; 4 | export 'interactive_viewer_interactions.dart'; 5 | export 'ui_interaction.dart'; 6 | export 'widget_finder.dart'; 7 | -------------------------------------------------------------------------------- /test/widget/workspace_page/shapes_tool_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:integration_test/integration_test.dart'; 6 | 7 | import 'package:paintroid/app.dart'; 8 | import 'package:paintroid/core/tools/tool_data.dart'; 9 | import '../../utils/test_utils.dart'; 10 | 11 | void main() { 12 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 13 | 14 | late Widget sut; 15 | 16 | setUp(() async => sut = ProviderScope(child: App(showOnboardingPage: false))); 17 | 18 | testWidgets('[SHAPES_TOOL]: selecting shapes tool shows checkmark', 19 | (WidgetTester tester) async { 20 | UIInteraction.initialize(tester); 21 | await tester.pumpWidget(sut); 22 | await UIInteraction.createNewImage(); 23 | 24 | expect(WidgetFinder.plusButton, findsNothing); 25 | expect(WidgetFinder.checkMark, findsNothing); 26 | 27 | await UIInteraction.selectTool(ToolData.SHAPES.name); 28 | 29 | expect(WidgetFinder.plusButton, findsNothing); 30 | expect(WidgetFinder.checkMark, findsOneWidget); 31 | }); 32 | 33 | testWidgets('[SHAPES_TOOL]: selecting other tool hides plus and checkmark', 34 | (WidgetTester tester) async { 35 | UIInteraction.initialize(tester); 36 | await tester.pumpWidget(sut); 37 | await UIInteraction.createNewImage(); 38 | 39 | expect(WidgetFinder.plusButton, findsNothing); 40 | expect(WidgetFinder.checkMark, findsNothing); 41 | 42 | await UIInteraction.selectTool(ToolData.SHAPES.name); 43 | 44 | expect(WidgetFinder.plusButton, findsNothing); 45 | expect(WidgetFinder.checkMark, findsOneWidget); 46 | 47 | await UIInteraction.selectTool(ToolData.BRUSH.name); 48 | 49 | expect(WidgetFinder.plusButton, findsNothing); 50 | expect(WidgetFinder.checkMark, findsNothing); 51 | }); 52 | } 53 | --------------------------------------------------------------------------------