├── .gitignore ├── EULA.txt ├── PRIVACY_POLICY.md ├── README.md ├── app ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── app │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.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 │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ └── RunnerTests │ │ └── RunnerTests.swift ├── lib │ ├── main.dart │ ├── models │ │ ├── bitmap_extensions.dart │ │ ├── color_extensions.dart │ │ ├── pencil_tool.dart │ │ ├── tool.dart │ │ └── tool_type.dart │ └── screens │ │ └── drawing │ │ ├── drawing_screen.dart │ │ ├── drawing_state.dart │ │ └── widgets │ │ ├── bottom_panel.dart │ │ ├── color_picker.dart │ │ ├── drawing_canvas.dart │ │ ├── layers_panel.dart │ │ └── tools_panel.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── graphics ├── .gitignore ├── .idea │ ├── .gitignore │ ├── discord.xml │ ├── graphics_core.iml │ ├── libraries │ │ ├── Dart_Packages.xml │ │ └── Dart_SDK.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── .vscode │ └── launch.json ├── 0SourceNodecache.png ├── 1EllipseNodecache.png ├── CHANGELOG.md ├── README.md ├── analysis_options.yaml ├── b1.png ├── base_bitmap_norm.png ├── jjjjjjj.png ├── joebiden.png ├── joebiden_2.png ├── layer_a.png ├── layer_ab.png ├── layer_acb.png ├── layer_bg.png ├── layers_a_b_graph.png ├── layers_a_b_graph_p.png ├── lib │ ├── graphics.dart │ └── src │ │ ├── algorithms │ │ ├── adjust_hsl.dart │ │ ├── blend_colors.dart │ │ ├── ellipse.dart │ │ ├── gaussian_blur.dart │ │ ├── interpolate_colors.dart │ │ ├── line.dart │ │ └── linear_gradient.dart │ │ ├── core │ │ ├── bitmap.dart │ │ ├── bitmap_iterator.dart │ │ ├── color.dart │ │ ├── point.dart │ │ ├── rect.dart │ │ ├── region.dart │ │ └── vector.dart │ │ ├── graphics_base.dart │ │ ├── pipeline │ │ ├── blur_node.dart │ │ ├── ellipse_node.dart │ │ ├── graph_traversal.dart │ │ ├── layer_manager.dart │ │ ├── line_node.dart │ │ ├── node.dart │ │ ├── node_graph.dart │ │ ├── over_node.dart │ │ ├── path_node.dart │ │ └── source_node.dart │ │ └── utils.dart ├── m1.png ├── mariobg.png ├── ok.png ├── ok2.png ├── output.png ├── output_JJJ.png ├── output_graph.png ├── overlayed_bitmap.png ├── pubspec.yaml ├── sunflowerfield.jpg └── test │ ├── assets │ ├── base_bitmap.jpg │ ├── dummy_image.jpg │ ├── dummy_image_blur_20.jpg │ ├── dummy_image_blur_5.jpg │ ├── dummy_image_blur_50.jpg │ ├── eiffel.png │ ├── eiffel_hue_minus_92.png │ ├── eiffel_hue_plus_72_sat_plus_100_colorize.png │ ├── eiffel_light_minus_60.png │ ├── eiffel_sat_plus_86.png │ ├── ellipse_x0y0_x1y1.png │ ├── ellipse_x0y0_x49y49.png │ ├── ellipse_x30y30_x199y89.png │ ├── layer_a.png │ ├── layer_b.png │ ├── layer_benchmark_a.png │ ├── layer_benchmark_bg.png │ ├── layer_bg.png │ ├── layer_c.png │ ├── layers_a.png │ ├── layers_a_b.png │ ├── layers_a_c.png │ ├── layers_a_c_b.png │ ├── layers_b.png │ ├── layers_c_a_b.png │ ├── line_x0y0_x23y8.png │ ├── line_x1221y3321_x5543y6934.png │ ├── line_x54y95_x124y421.png │ ├── overlay_bitmap.png │ ├── overlayed_bitmap.png │ ├── overlayed_bitmap_offset_x-312_y-192.png │ ├── overlayed_bitmap_offset_x137_y-132.png │ └── overlayed_bitmap_offset_x215_y93.png │ ├── src │ ├── algorithms │ │ ├── adjust_hsl_test.dart │ │ ├── blend_colors_test.dart │ │ ├── ellipse_test.dart │ │ ├── gaussian_blur_test.dart │ │ └── line_test.dart │ ├── core │ │ ├── bitmap_test.dart │ │ └── color_test.dart │ └── pipeline │ │ ├── generic_pipeline_metrics.dart │ │ ├── layer_manager_test.dart │ │ └── simple_test.dart │ └── utils.dart ├── server ├── .gitattributes ├── .gitignore ├── .gradle │ ├── 8.7 │ │ ├── checksums │ │ │ └── checksums.lock │ │ ├── dependencies-accessors │ │ │ └── gc.properties │ │ ├── executionHistory │ │ │ └── executionHistory.lock │ │ ├── fileChanges │ │ │ └── last-build.bin │ │ ├── fileHashes │ │ │ └── fileHashes.lock │ │ └── gc.properties │ ├── buildOutputCleanup │ │ ├── buildOutputCleanup.lock │ │ └── cache.properties │ └── vcs-1 │ │ └── gc.properties ├── HELP.md ├── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── pixapencil │ │ │ └── server │ │ │ ├── ServerApplication.kt │ │ │ ├── config │ │ │ ├── FreemarkerConfig.kt │ │ │ └── SecurityConfig.kt │ │ │ ├── controllers │ │ │ ├── CommentController.kt │ │ │ ├── CreationController.kt │ │ │ └── UserController.kt │ │ │ ├── domain │ │ │ ├── Comment.kt │ │ │ ├── Creation.kt │ │ │ ├── DailyCreation.kt │ │ │ ├── User.kt │ │ │ └── VerificationToken.kt │ │ │ ├── dtos │ │ │ ├── AddCommentDTO.kt │ │ │ ├── EditCommentDTO.kt │ │ │ ├── GetAuthorDTO.kt │ │ │ ├── GetCommentDTO.kt │ │ │ ├── GetCreationDTO.kt │ │ │ ├── GetDailyCreationDTO.kt │ │ │ ├── GetUserDTO.kt │ │ │ ├── LoginUserDTO.kt │ │ │ ├── RegisterUserDTO.kt │ │ │ └── UploadCreationDTO.kt │ │ │ ├── exceptions │ │ │ └── EmailAlreadyInUseException.kt │ │ │ ├── repos │ │ │ ├── CommentRepository.kt │ │ │ ├── CreationRepository.kt │ │ │ ├── DailyCreationRepository.kt │ │ │ ├── UserRepository.kt │ │ │ └── VerificationTokenRepository.kt │ │ │ └── services │ │ │ ├── AuthUser.kt │ │ │ ├── CommentService.kt │ │ │ ├── CreationService.kt │ │ │ ├── DailyCreationService.kt │ │ │ ├── MailService.kt │ │ │ ├── S3Service.kt │ │ │ ├── TokenCleanupService.kt │ │ │ ├── UserDetailsService.kt │ │ │ └── UserService.kt │ └── resources │ │ ├── application.yaml │ │ ├── dummy_data.sql │ │ ├── schema.sql │ │ └── templates │ │ └── verify-email.ftl │ └── test │ ├── kotlin │ └── com │ │ └── pixapencil │ │ └── server │ │ ├── DummyData.kt │ │ ├── ServerApplicationTests.kt │ │ ├── controllers │ │ ├── CreationControllerTests.kt │ │ └── UserControllerTests.kt │ │ └── services │ │ ├── CreationServiceTests.kt │ │ └── UserServiceTests.kt │ └── resources │ └── config │ └── application-test.yaml ├── web.iml └── web ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── mockServiceWorker.js └── vite.svg ├── src ├── App.css ├── api │ ├── root-api.ts │ └── root-handlers.ts ├── app │ ├── app-provider.tsx │ ├── index.tsx │ └── routes │ │ ├── creation-details-page.tsx │ │ ├── creations-page.tsx │ │ ├── login-page.tsx │ │ └── register-page.tsx ├── assets │ ├── icons │ │ ├── ICOArrowLeft.svg │ │ ├── ICOArrowRight.svg │ │ ├── ICOClose.svg │ │ ├── ICOComment.svg │ │ ├── ICOCommentFilled.svg │ │ ├── ICOHeart.svg │ │ ├── ICOHeartFilled.svg │ │ └── ICOSearch.svg │ ├── images │ │ ├── app-logo.png │ │ └── google-logo.png │ └── index.tsx ├── components │ ├── components-page.tsx │ ├── index.ts │ └── ui │ │ ├── px-button.tsx │ │ ├── px-icon-button.tsx │ │ ├── px-input.tsx │ │ ├── px-layout.tsx │ │ ├── px-modal.tsx │ │ ├── px-tag.tsx │ │ ├── px-tags.tsx │ │ └── px-textarea.tsx ├── features │ ├── auth │ │ ├── api │ │ │ └── auth-api.ts │ │ ├── components │ │ │ ├── LoginForm.tsx │ │ │ └── RegisterForm.tsx │ │ ├── hooks │ │ │ ├── useLogin.ts │ │ │ └── useRegister.ts │ │ ├── index.ts │ │ └── store │ │ │ └── auth-slice.tsx │ ├── comments │ │ ├── api │ │ │ ├── add-comment.ts │ │ │ ├── comments-api.ts │ │ │ ├── delete-comment.ts │ │ │ ├── edit-comment.ts │ │ │ └── get-comments.ts │ │ ├── components │ │ │ ├── add-comment.tsx │ │ │ ├── comment-item.tsx │ │ │ └── comments-list.tsx │ │ └── index.ts │ └── creations │ │ ├── api │ │ ├── creations-api.ts │ │ ├── get-creation.ts │ │ ├── get-creations.ts │ │ ├── get-daily-creation.ts │ │ ├── like-creations.ts │ │ ├── unlike-creation.ts │ │ └── upload-creation.ts │ │ ├── components │ │ ├── creation-card.tsx │ │ ├── creation-details-modal.tsx │ │ ├── creation-details.tsx │ │ ├── creations-grid.tsx │ │ ├── like-button-counter.tsx │ │ ├── like-button-icon.tsx │ │ ├── top-banner.tsx │ │ └── upload-file.tsx │ │ ├── hooks │ │ ├── use-arrow-key-nav.ts │ │ └── use-like-button.ts │ │ ├── index.ts │ │ ├── mocks │ │ ├── get-creation-handler.ts │ │ ├── get-creations-handler.ts │ │ ├── handlers.ts │ │ └── upload-creation-handler.ts │ │ └── tests │ │ └── upload-creation-modal.test.tsx ├── index.css ├── main.tsx ├── setup-tests.js ├── store │ └── app-store.ts ├── types │ └── root-types.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | web.iml -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | We are committed to respecting your privacy and want to assure you that we DO NOT collect any personal information from our users. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Get it on F-Droid 3 |

PixaPencil

4 | 5 | Join the [**official community**](https://discord.gg/cYtaTnuweW). 6 | 7 | [Get it on GitHub](https://github.com/therealbluepandabear/PixaPencil/releases/latest) 8 | [Get it on F-Droid](https://f-droid.org/en/packages/com.therealbluepandabear.pixapencil/) 9 |
10 | 11 | ## About 12 | 13 | PixaPencil is a sleek and user-friendly pixel art editor for Android, designed for both novice and expert artists. 14 | 15 | **In January 2024, PixaPencil transitioned to new leadership and is being rewritten in Flutter, you can view the old native code in the `native-archive` branch.** 16 | 17 | ## License Changes 18 | 19 | All source code, binaries, and releases are distributed under [**PixaPencil's EULA**](https://github.com/tomdoeslinux/PixaPencil/blob/main/EULA.txt). 20 | 21 | All source code prior to January 24, 2024 is distributed under the **GNU GPL v3**. 22 | 23 | All source code prior to Sept 6, 2022 is distributed under the **MIT license**. 24 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | -------------------------------------------------------------------------------- /app/.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: "603104015dd692ea3403755b55d07813d5cf8965" 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: 603104015dd692ea3403755b55d07813d5cf8965 17 | base_revision: 603104015dd692ea3403755b55d07813d5cf8965 18 | - platform: android 19 | create_revision: 603104015dd692ea3403755b55d07813d5cf8965 20 | base_revision: 603104015dd692ea3403755b55d07813d5cf8965 21 | - platform: ios 22 | create_revision: 603104015dd692ea3403755b55d07813d5cf8965 23 | base_revision: 603104015dd692ea3403755b55d07813d5cf8965 24 | - platform: linux 25 | create_revision: 603104015dd692ea3403755b55d07813d5cf8965 26 | base_revision: 603104015dd692ea3403755b55d07813d5cf8965 27 | - platform: macos 28 | create_revision: 603104015dd692ea3403755b55d07813d5cf8965 29 | base_revision: 603104015dd692ea3403755b55d07813d5cf8965 30 | - platform: web 31 | create_revision: 603104015dd692ea3403755b55d07813d5cf8965 32 | base_revision: 603104015dd692ea3403755b55d07813d5cf8965 33 | - platform: windows 34 | create_revision: 603104015dd692ea3403755b55d07813d5cf8965 35 | base_revision: 603104015dd692ea3403755b55d07813d5cf8965 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # app 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /app/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /app/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /app/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "com.example.app" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.example.app" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/android/app/src/main/kotlin/com/example/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 6 | -------------------------------------------------------------------------------- /app/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.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /app/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /app/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. -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | App 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | app 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /app/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/screens/drawing/drawing_screen.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | 6 | runApp(const MyApp()); 7 | } 8 | 9 | class MyApp extends StatelessWidget { 10 | const MyApp({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | title: 'PixaPencil', 16 | theme: ThemeData( 17 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 18 | useMaterial3: true, 19 | ), 20 | home: const MyHomePage(title: 'PixaPencil'), 21 | ); 22 | } 23 | } 24 | 25 | class MyHomePage extends StatefulWidget { 26 | const MyHomePage({super.key, required this.title}); 27 | 28 | final String title; 29 | 30 | @override 31 | State createState() => _MyHomePageState(); 32 | } 33 | 34 | class _MyHomePageState extends State { 35 | @override 36 | Widget build(BuildContext context) { 37 | // This method is rerun every time setState is called, for instance as done 38 | // by the _incrementCounter method above. 39 | // 40 | // The Flutter framework has been optimized to make rerunning build methods 41 | // fast, so that you can just rebuild anything that needs updating rather 42 | // than having to individually change instances of widgets. 43 | return Scaffold( 44 | appBar: AppBar( 45 | // TRY THIS: Try changing the color here to a specific color (to 46 | // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar 47 | // change color while the other colors stay the same. 48 | backgroundColor: Theme.of(context).colorScheme.inversePrimary, 49 | // Here we take the value from the MyHomePage object that was created by 50 | // the App.build method, and use it to set our appbar title. 51 | title: Text(widget.title), 52 | ), 53 | body: const DrawingScreen(), 54 | ); 55 | } 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/lib/models/bitmap_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:graphics/graphics.dart'; 5 | 6 | extension GBitmapExtensions on GBitmap { 7 | Future toFlutterImage() { 8 | final completer = Completer(); 9 | 10 | ui.decodeImageFromPixels( 11 | pixels, 12 | width, 13 | height, 14 | ui.PixelFormat.rgba8888, 15 | (img) => completer.complete(img), 16 | ); 17 | 18 | return completer.future; 19 | } 20 | } -------------------------------------------------------------------------------- /app/lib/models/color_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/graphics.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | extension GColorExtensions on GColor { 5 | Color toFlutterColor() { 6 | return Color.fromARGB(a, r, g, b); 7 | } 8 | } 9 | 10 | extension ColorExtensions on Color { 11 | GColor toGColor() { 12 | return GColors.rgba(red, green, blue, alpha); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/lib/models/pencil_tool.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/models/tool.dart'; 2 | import 'package:graphics/graphics.dart'; 3 | 4 | class PencilTool extends Tool { 5 | PathNode? _currentPathNode; 6 | final bool isEraser; 7 | 8 | PencilTool(super.drawingState, {this.isEraser = false}); 9 | 10 | @override 11 | void onTouchDown(GPoint point) { 12 | _currentPathNode = PathNode( 13 | inputNode: null, 14 | path: [point], 15 | color: !isEraser ? drawingState.selectedColor : GColors.rgba(0, 0, 0, 0), 16 | ); 17 | 18 | if (operatingNode.parentNode?.auxNode == operatingNode) { 19 | operatingNode.parentNode?.auxNode = _currentPathNode; 20 | } else { 21 | operatingNode.parentNode?.inputNode = _currentPathNode; 22 | } 23 | _currentPathNode?.inputNode = operatingNode; 24 | 25 | drawingState.nodeGraph.populateNodeLUT(); 26 | drawingState.layerManager.populateLayers(); 27 | } 28 | 29 | @override 30 | void onTouchMove(GPoint point) async { 31 | _currentPathNode?.addPoint(point); 32 | } 33 | 34 | @override 35 | void onTouchUp() { 36 | _currentPathNode = null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/lib/models/tool.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/screens/drawing/drawing_state.dart'; 2 | import 'package:graphics/graphics.dart'; 3 | 4 | abstract class Tool { 5 | final DrawingState drawingState; 6 | 7 | Node get operatingNode => drawingState.layers[drawingState.activeLayerIndex].rootNode; 8 | 9 | Tool(this.drawingState); 10 | 11 | void onTouchDown(GPoint point); 12 | void onTouchMove(GPoint point); 13 | void onTouchUp(); 14 | } 15 | -------------------------------------------------------------------------------- /app/lib/models/tool_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/screens/drawing/drawing_state.dart'; 2 | 3 | import 'pencil_tool.dart'; 4 | import 'tool.dart'; 5 | 6 | enum ToolType { 7 | pencil, 8 | eraser; 9 | 10 | Tool getToolInstance(DrawingState drawingState) { 11 | switch (this) { 12 | case ToolType.pencil: 13 | return PencilTool(drawingState); 14 | case ToolType.eraser: 15 | return PencilTool(drawingState, isEraser: true); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/lib/screens/drawing/drawing_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/screens/drawing/drawing_state.dart'; 2 | import 'package:app/screens/drawing/widgets/bottom_panel.dart'; 3 | import 'package:app/screens/drawing/widgets/color_picker.dart'; 4 | import 'package:app/screens/drawing/widgets/drawing_canvas.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class DrawingScreen extends StatelessWidget { 9 | const DrawingScreen({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return ChangeNotifierProvider( 14 | create: (context) => DrawingState(canvasWidth: 500, canvasHeight: 500), 15 | child: const Column( 16 | children: [ 17 | ColorPicker(), 18 | Expanded( 19 | child: DrawingCanvas(), 20 | ), 21 | BottomPanel(), 22 | ], 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/lib/screens/drawing/widgets/bottom_panel.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/screens/drawing/widgets/layers_panel.dart'; 2 | import 'package:app/screens/drawing/widgets/tools_panel.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class BottomPanel extends StatelessWidget { 6 | const BottomPanel({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return const DefaultTabController( 11 | length: 2, 12 | child: Column( 13 | children: [ 14 | TabBar( 15 | tabs: [Tab(text: 'Tools'), Tab(text: 'Layers')], 16 | ), 17 | SizedBox( 18 | height: 100, 19 | child: TabBarView(children: [ 20 | ToolsPanel(), 21 | LayersPanel(), 22 | ]), 23 | ) 24 | ], 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/lib/screens/drawing/widgets/color_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/models/color_extensions.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:graphics/graphics.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | import '../drawing_state.dart'; 7 | 8 | class ColorPicker extends StatelessWidget { 9 | static const dummyColors = [ 10 | Colors.red, 11 | Colors.green, 12 | Colors.blue, 13 | Colors.yellow, 14 | Colors.purple, 15 | Colors.orange, 16 | Colors.pink, 17 | Colors.cyan, 18 | Colors.lime, 19 | Colors.indigo, 20 | Colors.teal, 21 | Colors.brown, 22 | Colors.grey, 23 | ]; 24 | 25 | const ColorPicker({super.key}); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | final drawingState = Provider.of(context); 30 | 31 | print( 32 | "${drawingState.selectedColor.r} ${drawingState.selectedColor.r} ${drawingState.selectedColor.b}"); 33 | 34 | return Padding( 35 | padding: const EdgeInsets.all(16), 36 | child: Row( 37 | children: [ 38 | Expanded( 39 | child: SizedBox( 40 | height: 60, 41 | child: ListView.builder( 42 | scrollDirection: Axis.horizontal, 43 | itemCount: dummyColors.length, 44 | itemBuilder: (context, index) { 45 | final color = dummyColors[index]; 46 | 47 | return GestureDetector( 48 | onTap: () { 49 | drawingState.selectedColor = color.toGColor(); 50 | }, 51 | child: Container(width: 60, color: color), 52 | ); 53 | }, 54 | ), 55 | ), 56 | ), 57 | const SizedBox(width: 10), 58 | Container( 59 | height: 60, 60 | width: 60, 61 | color: drawingState.selectedColor.toFlutterColor(), 62 | ), 63 | ], 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/lib/screens/drawing/widgets/tools_panel.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/models/tool_type.dart'; 2 | import 'package:app/screens/drawing/drawing_state.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class ToolsPanel extends StatelessWidget { 7 | const ToolsPanel({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final drawingState = Provider.of(context); 12 | 13 | return Container( 14 | color: Colors.blue, 15 | child: Row( 16 | children: [ 17 | IconButton( 18 | onPressed: () { 19 | drawingState.selectedTool = ToolType.pencil; 20 | }, 21 | icon: const Icon(Icons.brush), 22 | color: drawingState.selectedTool == ToolType.pencil 23 | ? Colors.white 24 | : null, 25 | ), 26 | IconButton( 27 | onPressed: () { 28 | drawingState.selectedTool = ToolType.eraser; 29 | }, 30 | icon: const Icon(Icons.error), 31 | color: drawingState.selectedTool == ToolType.eraser 32 | ? Colors.white 33 | : null, 34 | ), 35 | ], 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'dart:ui'; 9 | 10 | import 'package:app/models/bitmap_extensions.dart'; 11 | import 'package:app/screens/drawing/widgets/drawing_canvas.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:flutter_test/flutter_test.dart'; 14 | import 'package:graphics/src/core/bitmap.dart'; 15 | 16 | import 'package:app/main.dart'; 17 | 18 | void main() { 19 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 20 | // Build our app and trigger a frame. 21 | await tester.pumpWidget(const MyApp()); 22 | 23 | // Verify that our counter starts at 0. 24 | expect(find.text('0'), findsOneWidget); 25 | expect(find.text('1'), findsNothing); 26 | 27 | // Tap the '+' icon and trigger a frame. 28 | await tester.tap(find.byIcon(Icons.add)); 29 | await tester.pump(); 30 | 31 | // Verify that our counter has incremented. 32 | expect(find.text('0'), findsNothing); 33 | expect(find.text('1'), findsOneWidget); 34 | }); 35 | 36 | test("CanvasPainter maintains correct aspect ratio for artboardRect", () async { 37 | final dummyBitmap = GBitmap(40, 90, config: GBitmapConfig.rgba); 38 | final image = await dummyBitmap.toFlutterImage(); 39 | final painter = CanvasPainter(image, 0, Offset.zero); 40 | 41 | const dummyWidth = 400.0; 42 | const dummyHeight = 800.0; 43 | 44 | final recorder = PictureRecorder(); 45 | final canvas = Canvas(recorder, const Rect.fromLTWH(0, 0, dummyWidth, dummyHeight)); 46 | painter.paint(canvas, const Size(dummyWidth, dummyHeight)); 47 | 48 | expect(painter.artboardRect.width / dummyBitmap.width == painter.artboardRect.height / dummyBitmap.height, isTrue); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /graphics/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /graphics/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /graphics/.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /graphics/.idea/graphics_core.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /graphics/.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /graphics/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /graphics/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /graphics/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /graphics/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "graphics", 9 | "request": "launch", 10 | "type": "dart" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /graphics/0SourceNodecache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/0SourceNodecache.png -------------------------------------------------------------------------------- /graphics/1EllipseNodecache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/1EllipseNodecache.png -------------------------------------------------------------------------------- /graphics/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /graphics/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /graphics/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /graphics/b1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/b1.png -------------------------------------------------------------------------------- /graphics/base_bitmap_norm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/base_bitmap_norm.png -------------------------------------------------------------------------------- /graphics/jjjjjjj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/jjjjjjj.png -------------------------------------------------------------------------------- /graphics/joebiden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/joebiden.png -------------------------------------------------------------------------------- /graphics/joebiden_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/joebiden_2.png -------------------------------------------------------------------------------- /graphics/layer_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/layer_a.png -------------------------------------------------------------------------------- /graphics/layer_ab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/layer_ab.png -------------------------------------------------------------------------------- /graphics/layer_acb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/layer_acb.png -------------------------------------------------------------------------------- /graphics/layer_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/layer_bg.png -------------------------------------------------------------------------------- /graphics/layers_a_b_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/layers_a_b_graph.png -------------------------------------------------------------------------------- /graphics/layers_a_b_graph_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/layers_a_b_graph_p.png -------------------------------------------------------------------------------- /graphics/lib/graphics.dart: -------------------------------------------------------------------------------- 1 | library graphics; 2 | 3 | export 'src/algorithms/adjust_hsl.dart'; 4 | export 'src/algorithms/blend_colors.dart'; 5 | export 'src/algorithms/gaussian_blur.dart'; 6 | export 'src/algorithms/interpolate_colors.dart'; 7 | export 'src/algorithms/line.dart'; 8 | export 'src/algorithms/linear_gradient.dart'; 9 | 10 | export 'src/core/bitmap_iterator.dart'; 11 | export 'src/core/bitmap.dart'; 12 | export 'src/core/color.dart'; 13 | export 'src/core/point.dart'; 14 | export 'src/core/rect.dart'; 15 | export 'src/core/vector.dart'; 16 | 17 | export 'src/pipeline/blur_node.dart'; 18 | export 'src/pipeline/layer_manager.dart'; 19 | export 'src/pipeline/node_graph.dart'; 20 | export 'src/pipeline/node.dart'; 21 | export 'src/pipeline/over_node.dart'; 22 | export 'src/pipeline/path_node.dart'; 23 | export 'src/pipeline/source_node.dart'; -------------------------------------------------------------------------------- /graphics/lib/src/algorithms/adjust_hsl.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/algorithms/interpolate_colors.dart'; 2 | 3 | import '../core/bitmap.dart'; 4 | import '../core/bitmap_iterator.dart'; 5 | import '../core/color.dart'; 6 | 7 | GBitmap adjustHsl( 8 | GBitmap source, 9 | double h, 10 | double s, 11 | double l, { 12 | bool colorize = false, 13 | }) { 14 | final sourceIterator = GBitmapIterator(source); 15 | 16 | final destBitmap = GBitmap(source.width, source.height, config: source.config); 17 | final destIterator = GBitmapIterator(destBitmap); 18 | 19 | final hsvComponents = List.filled(3, 0.0); 20 | 21 | do { 22 | final curPixel = sourceIterator.currentPixel; 23 | GColors.rgbToHsl(curPixel.r, curPixel.g, curPixel.b, hsvComponents); 24 | hsvComponents[0] = colorize ? h : (hsvComponents[0] + h) % 360; 25 | hsvComponents[1] = (hsvComponents[1] + s).clamp(0, 1); 26 | 27 | var newColor = 28 | GColors.hslToRgb(hsvComponents[0], hsvComponents[1], hsvComponents[2]); 29 | 30 | if (l < 0) { 31 | newColor = interpolateColors(newColor, GColors.black, l.abs()); 32 | } else if (l > 0) { 33 | newColor = interpolateColors(newColor, GColors.white, l); 34 | } 35 | 36 | destIterator.put(newColor); 37 | } while (sourceIterator.moveHorizontal() && destIterator.moveHorizontal()); 38 | 39 | return destBitmap; 40 | } 41 | -------------------------------------------------------------------------------- /graphics/lib/src/algorithms/interpolate_colors.dart: -------------------------------------------------------------------------------- 1 | import '../core/color.dart'; 2 | 3 | GColor interpolateColors(GColor c0, GColor c1, double factor) { 4 | final r = (c0.r + factor * (c1.r - c0.r)).round(); 5 | final g = (c0.g + factor * (c1.g - c0.g)).round(); 6 | final b = (c0.b + factor * (c1.b - c0.b)).round(); 7 | final a = (c0.a + factor * (c1.a - c0.a)).round(); 8 | 9 | return GColors.rgba(r, g, b, a); 10 | } 11 | -------------------------------------------------------------------------------- /graphics/lib/src/algorithms/line.dart: -------------------------------------------------------------------------------- 1 | import '../core/bitmap.dart'; 2 | import '../core/color.dart'; 3 | import '../core/point.dart'; 4 | 5 | void _drawLineY( 6 | GBitmap bitmap, 7 | GPoint from, 8 | GPoint to, 9 | GColor color, 10 | ) { 11 | var x = from.x; 12 | var y = from.y; 13 | 14 | final differenceX = to.x - x; 15 | var differenceY = to.y - y; 16 | 17 | var yi = 1; 18 | const xi = 1; 19 | 20 | if (differenceY < 0) { 21 | differenceY = -differenceY; 22 | yi = -1; 23 | } 24 | 25 | var p = 2 * differenceY - differenceX; 26 | 27 | while (x <= to.x) { 28 | bitmap.setPixel(x, y, color); 29 | ++x; 30 | 31 | if (p < 0) { 32 | p += 2 * differenceY; 33 | 34 | if (differenceY > differenceX) { 35 | x += xi; 36 | } 37 | } else { 38 | p = p + 2 * differenceY - 2 * differenceX; 39 | y += yi; 40 | } 41 | } 42 | } 43 | 44 | void _drawLineX( 45 | GBitmap bitmap, 46 | GPoint from, 47 | GPoint to, 48 | GColor color, 49 | ) { 50 | var x = from.x; 51 | var y = from.y; 52 | 53 | var differenceX = to.x - x; 54 | final differenceY = to.y - y; 55 | 56 | var xi = 1; 57 | 58 | if (differenceX <= 0) { 59 | differenceX = -differenceX; 60 | xi = -1; 61 | } 62 | 63 | var p = 2 * differenceX - differenceY; 64 | 65 | while (y <= to.y) { 66 | bitmap.setPixel(x, y, color); 67 | y++; 68 | 69 | if (p < 0) { 70 | p += 2 * differenceX; 71 | } else { 72 | p = p + 2 * differenceX - 2 * differenceY; 73 | x += xi; 74 | } 75 | } 76 | } 77 | 78 | void drawLine( 79 | GBitmap bitmap, 80 | GPoint from, 81 | GPoint to, 82 | GColor color, 83 | ) { 84 | if (from == to) { 85 | bitmap.setPixel(from.x, from.y, color); 86 | return; 87 | } 88 | 89 | final x = from.x; 90 | final y = from.y; 91 | 92 | final differenceX = to.x - x; 93 | final differenceY = to.y - y; 94 | 95 | if (differenceY <= differenceX) { 96 | if (differenceY.abs() > differenceX) { 97 | _drawLineX(bitmap, to, from, color); 98 | } else { 99 | _drawLineY(bitmap, from, to, color); 100 | } 101 | } else { 102 | if (differenceX.abs() > differenceY) { 103 | _drawLineY(bitmap, to, from, color); 104 | } else { 105 | _drawLineX(bitmap, from, to, color); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /graphics/lib/src/algorithms/linear_gradient.dart: -------------------------------------------------------------------------------- 1 | import '../core/bitmap.dart'; 2 | import '../core/bitmap_iterator.dart'; 3 | import '../core/color.dart'; 4 | import '../core/point.dart'; 5 | import '../core/vector.dart'; 6 | import 'interpolate_colors.dart'; 7 | 8 | void drawLinearGradient( 9 | GBitmap source, 10 | GPoint from, 11 | GPoint to, 12 | GColor fromColor, 13 | GColor toColor, 14 | ) { 15 | if (fromColor == toColor) { 16 | // clear 17 | } 18 | 19 | final GVector gradientStart = (from.x.toDouble(), from.y.toDouble()); 20 | final GVector gradientEnd = (to.x.toDouble(), to.y.toDouble()); 21 | 22 | final gradientLine = gradientEnd.subtract(gradientStart); 23 | final gradientLineMagnitude = gradientLine.magnitude(); 24 | 25 | final gradientLineNorm = gradientLine.normalize(); 26 | 27 | if (fromColor.a == 0 && toColor.a != 0) { 28 | fromColor = GColors.rgba(fromColor.r, fromColor.g, fromColor.b, 0); 29 | } else if (fromColor.a != 0 && toColor.a == 0) { 30 | toColor = GColors.rgba(toColor.r, toColor.g, toColor.b, 0); 31 | } 32 | 33 | final sourceIterator = GBitmapIterator(source); 34 | 35 | // This lookup table is crucial for performance 36 | const steps = 256; 37 | final colorLUT = List.generate(steps, (i) { 38 | final double factor = i / (steps - 1); 39 | return interpolateColors(fromColor, toColor, factor); 40 | }); 41 | 42 | GColor colorToSet; 43 | 44 | do { 45 | GVector curPixel = 46 | (sourceIterator.x.toDouble(), sourceIterator.y.toDouble()); 47 | curPixel = curPixel.subtract(gradientStart); 48 | 49 | final projFactor = curPixel.dot(gradientLineNorm) / gradientLineMagnitude; 50 | 51 | if (projFactor > 1) { 52 | colorToSet = toColor; 53 | } else if (projFactor < 0) { 54 | colorToSet = fromColor; 55 | } else { 56 | final lutIndex = (projFactor * (steps - 1)).clamp(0, steps - 1).toInt(); 57 | colorToSet = colorLUT[lutIndex]; 58 | } 59 | 60 | sourceIterator.put(colorToSet); 61 | } while (sourceIterator.moveNext()); 62 | } 63 | -------------------------------------------------------------------------------- /graphics/lib/src/core/bitmap_iterator.dart: -------------------------------------------------------------------------------- 1 | import 'bitmap.dart'; 2 | import 'color.dart'; 3 | 4 | // todo update 5 | class GBitmapIterator { 6 | final GBitmap _bitmap; 7 | 8 | GBitmapIterator(this._bitmap); 9 | 10 | int _x = 0; 11 | int _y = 0; 12 | 13 | int get x => _x; 14 | int get y => _y; 15 | 16 | int get currentPixelIndex => (_y * _bitmap.width + _x) * _bitmap.config.numChannels; 17 | 18 | int get currentPixel => _bitmap.getPixel(x, y); 19 | 20 | void put(GColor color) { 21 | _bitmap.setPixel(x, y, color); 22 | } 23 | 24 | GColor getPixel(int x, int y) => _bitmap.getPixel(x, y); 25 | 26 | void reset() { 27 | _x = 0; 28 | _y = 0; 29 | } 30 | 31 | bool moveNext() { 32 | // Move the iterator through the bitmap. 33 | // - Start by moving rightwards along the x-axis. 34 | // - Once the iterator reaches the final x position in the row, 35 | // - ...move downwards to the next row on the y-axis and reset the x position to 0. 36 | // - This continues until the entire bitmap is processed... 37 | 38 | if (_x < _bitmap.width - 1) { 39 | ++_x; 40 | } else if (_y < _bitmap.height - 1) { 41 | _x = 0; 42 | ++_y; 43 | } else { 44 | return false; 45 | } 46 | 47 | return true; 48 | } 49 | 50 | bool moveHorizontal([int steps = 1]) { 51 | final newX = _x + steps; 52 | 53 | if (newX < _bitmap.width) { 54 | _x = newX; 55 | } else if (_y < _bitmap.height - 1) { 56 | _x = 0; 57 | ++_y; 58 | } else { 59 | return false; 60 | } 61 | 62 | return true; 63 | } 64 | 65 | bool moveVertical([int steps = 1]) { 66 | final newY = _y + steps; 67 | 68 | if (newY < _bitmap.height) { 69 | _y = newY; 70 | } else if (_x < _bitmap.width - 1) { 71 | _y = 0; 72 | ++_x; 73 | } else { 74 | return false; 75 | } 76 | 77 | return true; 78 | } 79 | 80 | void moveTo(int x, int y) { 81 | _x = x; 82 | _y = y; 83 | } 84 | } -------------------------------------------------------------------------------- /graphics/lib/src/core/point.dart: -------------------------------------------------------------------------------- 1 | typedef GPoint = ({int x, int y}); 2 | -------------------------------------------------------------------------------- /graphics/lib/src/core/rect.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/core/point.dart'; 2 | 3 | class GRect { 4 | final int x, y, width, height; 5 | 6 | const GRect({ 7 | required this.x, 8 | required this.y, 9 | required this.width, 10 | required this.height, 11 | }); 12 | 13 | factory GRect.fromLTRB( 14 | int left, 15 | int top, 16 | int right, 17 | int bottom, 18 | ) { 19 | return GRect(x: left, y: top, width: right - left, height: bottom - top); 20 | } 21 | 22 | @override 23 | String toString() { 24 | return "GRect(x: $x, y: $y, width: $width, height: $height)"; 25 | } 26 | 27 | @override 28 | bool operator ==(Object other) => 29 | other is GRect && 30 | other.x == x && 31 | other.y == y && 32 | other.width == width && 33 | other.height == height; 34 | 35 | @override 36 | int get hashCode => Object.hash(x, y, width, height); 37 | 38 | static GRect union(GRect a, GRect b) { 39 | final left = a.x < b.x ? a.x : b.x; 40 | final top = a.y < b.y ? a.y : b.y; 41 | final right = 42 | (a.x + a.width) > (b.x + b.width) ? (a.x + a.width) : (b.x + b.width); 43 | final bottom = (a.y + a.height) > (b.y + b.height) 44 | ? (a.y + a.height) 45 | : (b.y + b.height); 46 | 47 | return GRect.fromLTRB(left, top, right, bottom); 48 | } 49 | 50 | static GRect between(GPoint from, GPoint to) { 51 | final left = from.x < to.x ? from.x : to.x; 52 | final top = from.y < to.y ? from.y : to.y; 53 | final right = from.x > to.x ? from.x : to.x; 54 | final bottom = from.y > to.y ? from.y : to.y; 55 | 56 | return GRect.fromLTRB(left, top, right, bottom); 57 | } 58 | 59 | // todo might not be needed 60 | static GRect intersection(GRect a, GRect b) { 61 | final left = (a.x > b.x) ? a.x : b.x; 62 | final top = (a.y > b.y) ? a.y : b.y; 63 | final right = 64 | (a.x + a.width < b.x + b.width) ? a.x + a.width : b.x + b.width; 65 | final bottom = 66 | (a.y + a.height < b.y + b.height) ? a.y + a.height : b.y + b.height; 67 | 68 | return GRect.fromLTRB(left, top, right, bottom); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /graphics/lib/src/core/region.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:graphics/src/core/rect.dart'; 3 | 4 | class GRegion { 5 | final List rectangles; 6 | 7 | const GRegion(this.rectangles); 8 | 9 | factory GRegion.rect(GRect rect) { 10 | return GRegion([rect]); 11 | } 12 | 13 | GRect get boundingBox { 14 | var left = rectangles.first.x; 15 | var top = rectangles.first.y; 16 | var right = rectangles.first.x + rectangles.first.width; 17 | var bottom = rectangles.first.y + rectangles.first.height; 18 | 19 | for (var rect in rectangles) { 20 | left = left < rect.x ? left : rect.x; 21 | top = top < rect.y ? top : rect.y; 22 | right = right > rect.x + rect.width ? right : rect.x + rect.width; 23 | bottom = bottom > rect.y + rect.height ? bottom : rect.y + rect.height; 24 | } 25 | 26 | return GRect.fromLTRB(left, top, right, bottom); 27 | } 28 | 29 | @override 30 | int get hashCode => rectangles.fold(0, (prev, rect) => prev ^ rect.hashCode); 31 | 32 | @override 33 | bool operator ==(Object other) => 34 | other is GRegion && 35 | const ListEquality().equals(rectangles, other.rectangles); 36 | } 37 | -------------------------------------------------------------------------------- /graphics/lib/src/core/vector.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | typedef GVector = (double x, double y); 4 | 5 | extension VectorExtensions on GVector { 6 | double get x => $1; 7 | double get y => $2; 8 | 9 | GVector subtract(GVector v1) { 10 | return (x - v1.x, y - v1.y); 11 | } 12 | 13 | GVector add(GVector v1) { 14 | return (x + v1.x, y + v1.y); 15 | } 16 | 17 | double dot(GVector v2) { 18 | return x * v2.x + y * v2.y; 19 | } 20 | 21 | double magnitude() { 22 | return sqrt(x * x + y * y); 23 | } 24 | 25 | GVector normalize() { 26 | final mag = magnitude(); 27 | 28 | if (mag == 0) { 29 | return (0, 0); 30 | } 31 | 32 | return (x / mag, y / mag); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /graphics/lib/src/graphics_base.dart: -------------------------------------------------------------------------------- 1 | // TODO: Put public facing types in this file. 2 | 3 | /// Checks if you are awesome. Spoiler: you are. 4 | class Awesome { 5 | bool get isAwesome => true; 6 | } 7 | -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/blur_node.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/core/region.dart'; 2 | 3 | import '../algorithms/gaussian_blur.dart'; 4 | import '../core/bitmap.dart'; 5 | import '../core/rect.dart'; 6 | import 'node.dart'; 7 | 8 | class BlurNode extends Node { 9 | final double radius; 10 | 11 | BlurNode({super.inputNode, required this.radius}); 12 | 13 | @override 14 | GBitmap operation(GRegion roi) { 15 | final inputBitmap = inputNode!.process(roi); 16 | 17 | return gaussianBlur(inputBitmap, radius); 18 | } 19 | 20 | @override 21 | GRect get boundingBox => inputNode!.boundingBox; 22 | } -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/ellipse_node.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/algorithms/ellipse.dart'; 2 | import 'package:graphics/src/core/bitmap.dart'; 3 | import 'package:graphics/src/core/color.dart'; 4 | import 'package:graphics/src/core/point.dart'; 5 | import 'package:graphics/src/core/rect.dart'; 6 | import 'package:graphics/src/core/region.dart'; 7 | import 'package:graphics/src/pipeline/node.dart'; 8 | 9 | class EllipseNode extends Node { 10 | final GPoint from; 11 | final GPoint to; 12 | final GColor color; 13 | 14 | EllipseNode({ 15 | required super.inputNode, 16 | required this.from, 17 | required this.to, 18 | required this.color, 19 | }); 20 | 21 | @override 22 | GBitmap operation(GRegion roi) { 23 | final inputBitmap = inputNode!.process(roi); 24 | 25 | drawEllipse( 26 | bitmap: inputBitmap, 27 | from: from, 28 | to: to, 29 | fillColor: color, 30 | strokeColor: color, 31 | strokeThickness: 0, 32 | ); 33 | 34 | return inputBitmap; 35 | } 36 | 37 | @override 38 | GRect get boundingBox => GRect.between(from, to); 39 | } 40 | -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/graph_traversal.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'node.dart'; 4 | 5 | void traverseGraphBFS(Node rootNode, void Function(Node) onNodeVisit) { 6 | final queue = Queue.of([rootNode]); 7 | 8 | while (queue.isNotEmpty) { 9 | final currentNode = queue.removeFirst(); 10 | 11 | onNodeVisit(currentNode); 12 | 13 | if (currentNode.inputNode != null) { 14 | queue.add(currentNode.inputNode!); 15 | } 16 | 17 | if (currentNode.auxNode != null) { 18 | queue.add(currentNode.auxNode!); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/line_node.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/algorithms/line.dart'; 2 | import 'package:graphics/src/core/bitmap.dart'; 3 | import 'package:graphics/src/core/color.dart'; 4 | import 'package:graphics/src/core/point.dart'; 5 | import 'package:graphics/src/core/rect.dart'; 6 | import 'package:graphics/src/core/region.dart'; 7 | import 'package:graphics/src/pipeline/node.dart'; 8 | 9 | class LineNode extends Node { 10 | final GPoint from; 11 | final GPoint to; 12 | final GColor color; 13 | 14 | LineNode({ 15 | required super.inputNode, 16 | required this.from, 17 | required this.to, 18 | required this.color, 19 | }); 20 | 21 | @override 22 | GBitmap operation(GRegion roi) { 23 | final inputBitmap = inputNode!.process(roi); 24 | 25 | drawLine(inputBitmap, from, to, color); 26 | 27 | return inputBitmap; 28 | } 29 | 30 | @override 31 | GRect get boundingBox => GRect.between(from, to); 32 | } 33 | -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/node.dart: -------------------------------------------------------------------------------- 1 | import "package:graphics/src/core/region.dart"; 2 | import "package:graphics/src/utils.dart"; 3 | 4 | import "../core/bitmap.dart"; 5 | import "../core/rect.dart"; 6 | 7 | abstract class Node { 8 | final Map _cache = {}; 9 | 10 | static var _idCounter = 0; 11 | final int id; 12 | 13 | Node? _inputNode; 14 | Node? _auxNode; 15 | Node? parentNode; 16 | 17 | Node({required Node? inputNode, Node? auxNode}) 18 | : id = _generateId(), 19 | _inputNode = inputNode, 20 | _auxNode = auxNode { 21 | _inputNode?.parentNode = this; 22 | _auxNode?.parentNode = this; 23 | } 24 | 25 | Node? get inputNode => _inputNode; 26 | set inputNode(Node? node) { 27 | _inputNode = node; 28 | node?.parentNode = this; 29 | } 30 | 31 | Node? get auxNode => _auxNode; 32 | set auxNode(Node? node) { 33 | _auxNode = node; 34 | node?.parentNode = this; 35 | } 36 | 37 | static int _generateId() { 38 | return _idCounter++; 39 | } 40 | 41 | GRect get boundingBox; 42 | 43 | GBitmap operation(GRegion roi); 44 | 45 | // todo not sure if roi should be nulalble 46 | GBitmap process(GRegion roi) { 47 | // final adjustedRoi = GRect.intersection(roi!, boundingBox); 48 | 49 | if (!_cache.containsKey(roi)) { 50 | final bitmap = operation(roi); 51 | 52 | _cache[boundingBox] = bitmap.crop(boundingBox); 53 | 54 | return bitmap; 55 | } else { 56 | return _cache[roi]!; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/node_graph.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/core/bitmap.dart'; 2 | import 'package:graphics/src/core/region.dart'; 3 | 4 | import 'graph_traversal.dart'; 5 | import 'node.dart'; 6 | 7 | class NodeGraph { 8 | Node rootNode; 9 | final Map nodeLUT = {}; 10 | 11 | NodeGraph(this.rootNode) { 12 | populateNodeLUT(); 13 | } 14 | 15 | void populateNodeLUT() { 16 | traverseGraphBFS(rootNode, (node) { 17 | nodeLUT[node.id] = node; 18 | }); 19 | } 20 | 21 | GBitmap process(GRegion outputRoi) { 22 | return rootNode.process(outputRoi); 23 | } 24 | } -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/over_node.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/core/bitmap.dart'; 2 | import 'package:graphics/src/core/rect.dart'; 3 | import 'package:graphics/src/core/region.dart'; 4 | import 'package:graphics/src/pipeline/node.dart'; 5 | 6 | 7 | class OverlayNode extends Node { 8 | OverlayNode({super.inputNode, super.auxNode}); 9 | 10 | @override 11 | GBitmap operation(GRegion roi) { 12 | final baseBitmap = inputNode!.process(roi); 13 | final overlayBitmap = inputNode!.process(roi); 14 | 15 | // final overlayBitmap = auxNode!.process(null); 16 | 17 | final overlayedBitmap = GBitmap.overlay(baseBitmap, overlayBitmap); 18 | 19 | return overlayedBitmap; 20 | } 21 | 22 | @override 23 | GRect get boundingBox => 24 | GRect.union(inputNode!.boundingBox, auxNode!.boundingBox); 25 | } 26 | -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/path_node.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/core/region.dart'; 2 | 3 | import '../algorithms/line.dart'; 4 | import '../core/bitmap.dart'; 5 | import '../core/color.dart'; 6 | import '../core/point.dart'; 7 | import '../core/rect.dart'; 8 | import 'node.dart'; 9 | 10 | class PathNode extends Node { 11 | final List path; 12 | final GColor color; 13 | 14 | PathNode({super.inputNode, required this.path, required this.color}); 15 | 16 | void addPoint(GPoint point) { 17 | path.add(point); 18 | } 19 | 20 | @override 21 | GBitmap operation(GRegion roi) { 22 | final inputBitmap = inputNode!.process(roi); 23 | 24 | GPoint? prevPoint; 25 | 26 | for (final point in path) { 27 | drawLine(inputBitmap, prevPoint ?? point, point, color); 28 | prevPoint = point; 29 | } 30 | 31 | return inputBitmap; 32 | } 33 | 34 | @override 35 | GRect get boundingBox { 36 | var minX = path[0].x; 37 | var minY = path[0].y; 38 | 39 | var maxX = path[0].x; 40 | var maxY = path[0].y; 41 | 42 | for (final point in path) { 43 | if (point.x < minX) minX = point.x; 44 | if (point.y < minY) minY = point.y; 45 | 46 | if (point.x > maxX) maxX = point.x; 47 | if (point.y > maxY) maxY = point.y; 48 | } 49 | 50 | return GRect( 51 | x: minX, 52 | y: minY, 53 | width: maxX - minX, 54 | height: maxY - minY, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /graphics/lib/src/pipeline/source_node.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/core/region.dart'; 2 | 3 | import '../core/bitmap.dart'; 4 | import '../core/rect.dart'; 5 | import 'node.dart'; 6 | 7 | class SourceNode extends Node { 8 | final GBitmap source; 9 | 10 | SourceNode({required this.source}) : super(inputNode: null); 11 | 12 | @override 13 | GBitmap operation(GRegion roi) { 14 | final roiBbox = roi.boundingBox; 15 | var bmpToReturn = GBitmap(roiBbox.width, roiBbox.height, config: source.config); 16 | 17 | for (var rect in roi.rectangles) { 18 | bmpToReturn = GBitmap.overlay(bmpToReturn, source.crop(rect)); 19 | } 20 | 21 | return bmpToReturn; 22 | } 23 | 24 | @override 25 | GRect get boundingBox => 26 | GRect(x: 0, y: 0, width: source.width, height: source.height); 27 | } 28 | -------------------------------------------------------------------------------- /graphics/m1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/m1.png -------------------------------------------------------------------------------- /graphics/mariobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/mariobg.png -------------------------------------------------------------------------------- /graphics/ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/ok.png -------------------------------------------------------------------------------- /graphics/ok2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/ok2.png -------------------------------------------------------------------------------- /graphics/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/output.png -------------------------------------------------------------------------------- /graphics/output_JJJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/output_JJJ.png -------------------------------------------------------------------------------- /graphics/output_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/output_graph.png -------------------------------------------------------------------------------- /graphics/overlayed_bitmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/overlayed_bitmap.png -------------------------------------------------------------------------------- /graphics/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: graphics 2 | description: A starting point for Dart libraries or applications. 3 | version: 1.0.0 4 | # repository: https://github.com/my_org/my_repo 5 | 6 | environment: 7 | sdk: ^3.4.4 8 | 9 | # Add regular dependencies here. 10 | dependencies: 11 | collection: ^1.19.1 12 | http: ^1.2.2 13 | image: ^4.2.0 14 | # path: ^1.8.0 15 | 16 | dev_dependencies: 17 | lints: ^4.0.0 18 | test: ^1.25.8 19 | -------------------------------------------------------------------------------- /graphics/sunflowerfield.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/sunflowerfield.jpg -------------------------------------------------------------------------------- /graphics/test/assets/base_bitmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/base_bitmap.jpg -------------------------------------------------------------------------------- /graphics/test/assets/dummy_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/dummy_image.jpg -------------------------------------------------------------------------------- /graphics/test/assets/dummy_image_blur_20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/dummy_image_blur_20.jpg -------------------------------------------------------------------------------- /graphics/test/assets/dummy_image_blur_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/dummy_image_blur_5.jpg -------------------------------------------------------------------------------- /graphics/test/assets/dummy_image_blur_50.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/dummy_image_blur_50.jpg -------------------------------------------------------------------------------- /graphics/test/assets/eiffel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/eiffel.png -------------------------------------------------------------------------------- /graphics/test/assets/eiffel_hue_minus_92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/eiffel_hue_minus_92.png -------------------------------------------------------------------------------- /graphics/test/assets/eiffel_hue_plus_72_sat_plus_100_colorize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/eiffel_hue_plus_72_sat_plus_100_colorize.png -------------------------------------------------------------------------------- /graphics/test/assets/eiffel_light_minus_60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/eiffel_light_minus_60.png -------------------------------------------------------------------------------- /graphics/test/assets/eiffel_sat_plus_86.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/eiffel_sat_plus_86.png -------------------------------------------------------------------------------- /graphics/test/assets/ellipse_x0y0_x1y1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/ellipse_x0y0_x1y1.png -------------------------------------------------------------------------------- /graphics/test/assets/ellipse_x0y0_x49y49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/ellipse_x0y0_x49y49.png -------------------------------------------------------------------------------- /graphics/test/assets/ellipse_x30y30_x199y89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/ellipse_x30y30_x199y89.png -------------------------------------------------------------------------------- /graphics/test/assets/layer_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layer_a.png -------------------------------------------------------------------------------- /graphics/test/assets/layer_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layer_b.png -------------------------------------------------------------------------------- /graphics/test/assets/layer_benchmark_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layer_benchmark_a.png -------------------------------------------------------------------------------- /graphics/test/assets/layer_benchmark_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layer_benchmark_bg.png -------------------------------------------------------------------------------- /graphics/test/assets/layer_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layer_bg.png -------------------------------------------------------------------------------- /graphics/test/assets/layer_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layer_c.png -------------------------------------------------------------------------------- /graphics/test/assets/layers_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layers_a.png -------------------------------------------------------------------------------- /graphics/test/assets/layers_a_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layers_a_b.png -------------------------------------------------------------------------------- /graphics/test/assets/layers_a_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layers_a_c.png -------------------------------------------------------------------------------- /graphics/test/assets/layers_a_c_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layers_a_c_b.png -------------------------------------------------------------------------------- /graphics/test/assets/layers_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layers_b.png -------------------------------------------------------------------------------- /graphics/test/assets/layers_c_a_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/layers_c_a_b.png -------------------------------------------------------------------------------- /graphics/test/assets/line_x0y0_x23y8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/line_x0y0_x23y8.png -------------------------------------------------------------------------------- /graphics/test/assets/line_x1221y3321_x5543y6934.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/line_x1221y3321_x5543y6934.png -------------------------------------------------------------------------------- /graphics/test/assets/line_x54y95_x124y421.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/line_x54y95_x124y421.png -------------------------------------------------------------------------------- /graphics/test/assets/overlay_bitmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/overlay_bitmap.png -------------------------------------------------------------------------------- /graphics/test/assets/overlayed_bitmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/overlayed_bitmap.png -------------------------------------------------------------------------------- /graphics/test/assets/overlayed_bitmap_offset_x-312_y-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/overlayed_bitmap_offset_x-312_y-192.png -------------------------------------------------------------------------------- /graphics/test/assets/overlayed_bitmap_offset_x137_y-132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/overlayed_bitmap_offset_x137_y-132.png -------------------------------------------------------------------------------- /graphics/test/assets/overlayed_bitmap_offset_x215_y93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/graphics/test/assets/overlayed_bitmap_offset_x215_y93.png -------------------------------------------------------------------------------- /graphics/test/src/algorithms/adjust_hsl_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/algorithms/adjust_hsl.dart'; 2 | import 'package:graphics/src/core/bitmap.dart'; 3 | import 'package:graphics/src/utils.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../../utils.dart'; 7 | 8 | void main() { 9 | late GBitmap eiffel; 10 | 11 | setUpAll(() async { 12 | eiffel = await loadBitmapFromImage("$testAssetPath/eiffel.png"); 13 | }); 14 | 15 | group("Adjust HSL tests", () { 16 | test("hue -92deg adjusts properly", () async { 17 | final adjusted = adjustHsl(eiffel, -92, 0, 0); 18 | final expected = 19 | await loadBitmapFromImage("$testAssetPath/eiffel_hue_minus_92.png"); 20 | 21 | expect(bitmapsAreEqual(adjusted, expected), isTrue); 22 | }); 23 | 24 | test("sat +86% adjusts properly", () async { 25 | final adjusted = adjustHsl(eiffel, 0, 0.86, 0); 26 | final expected = 27 | await loadBitmapFromImage("$testAssetPath/eiffel_sat_plus_86.png"); 28 | 29 | expect(bitmapsAreEqual(adjusted, expected), isTrue); 30 | }); 31 | 32 | test("colorize with hue +72deg and sat +100% works as expected", () async { 33 | final adjusted = adjustHsl(eiffel, 72, 1, 0, colorize: true); 34 | final expected = await loadBitmapFromImage( 35 | "$testAssetPath/eiffel_hue_plus_72_sat_plus_100_colorize.png"); 36 | 37 | expect(bitmapsAreEqual(adjusted, expected), isTrue); 38 | }); 39 | 40 | test("adjust hsl benchmark - change hue, sat, value of image", () { 41 | benchmark(() { 42 | adjustHsl(eiffel, 34, 0, 0); 43 | }, iterations: 200); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /graphics/test/src/algorithms/ellipse_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/algorithms/ellipse.dart'; 2 | import 'package:graphics/src/core/bitmap.dart'; 3 | import 'package:graphics/src/core/color.dart'; 4 | import 'package:graphics/src/utils.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import '../../utils.dart'; 8 | 9 | void main() { 10 | group("Ellipse tests", () { 11 | test("ellipse from (0, 0) to (49, 49) matches expected output", () async { 12 | final bitmap = GBitmap(50, 50, config: GBitmapConfig.rgba); 13 | 14 | drawEllipse( 15 | bitmap: bitmap, 16 | from: (x: 0, y: 0), 17 | to: (x: 49, y: 49), 18 | fillColor: GColors.blue, 19 | strokeColor: GColors.blue, 20 | strokeThickness: 0, 21 | ); 22 | 23 | expect( 24 | bitmapsAreEqual( 25 | bitmap, 26 | await loadBitmapFromImage("$testAssetPath/ellipse_x0y0_x49y49.png"), 27 | ), 28 | isTrue, 29 | ); 30 | }); 31 | 32 | test("ellipse from (30, 30) to (199, 89) matches expected output", 33 | () async { 34 | final bitmap = GBitmap(200, 90, config: GBitmapConfig.rgba); 35 | 36 | drawEllipse( 37 | bitmap: bitmap, 38 | from: (x: 30, y: 30), 39 | to: (x: 199, y: 89), 40 | fillColor: GColors.blue, 41 | strokeColor: GColors.blue, 42 | strokeThickness: 0, 43 | ); 44 | 45 | expect( 46 | bitmapsAreEqual( 47 | bitmap, 48 | await loadBitmapFromImage( 49 | "$testAssetPath/ellipse_x30y30_x199y89.png"), 50 | ), 51 | isTrue, 52 | ); 53 | }); 54 | 55 | test("ellipse from (0, 0) to (1, 1) matches expected output", () async { 56 | final bitmap = GBitmap(2, 2, config: GBitmapConfig.rgba); 57 | 58 | drawEllipse( 59 | bitmap: bitmap, 60 | from: (x: 0, y: 0), 61 | to: (x: 1, y: 1), 62 | fillColor: GColors.blue, 63 | strokeColor: GColors.blue, 64 | strokeThickness: 0, 65 | ); 66 | 67 | expect( 68 | bitmapsAreEqual( 69 | bitmap, 70 | await loadBitmapFromImage( 71 | "$testAssetPath/ellipse_x0y0_x1y1.png"), 72 | ), 73 | isTrue, 74 | ); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /graphics/test/src/algorithms/gaussian_blur_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/algorithms/gaussian_blur.dart'; 2 | import 'package:graphics/src/utils.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import '../../utils.dart'; 6 | 7 | void main() { 8 | group("Gaussian blur tests", () { 9 | Future runGaussianBlurTest({required double radius}) async { 10 | final bitmap = await loadBitmapFromImage( 11 | "$testAssetPath/dummy_image.jpg"); 12 | final blurredBitmap = gaussianBlur(bitmap, radius); 13 | 14 | final expectedFileName = "dummy_image_blur_$radius.jpg"; 15 | final expectedBitmap = 16 | await loadBitmapFromImage(expectedFileName); 17 | 18 | expect(bitmapsAreEqual(blurredBitmap, expectedBitmap), isTrue); 19 | } 20 | 21 | test("blur radius 5 matches expected output", () { 22 | runGaussianBlurTest(radius: 5); 23 | }); 24 | 25 | test("blur radius 20 matches expected output", () { 26 | runGaussianBlurTest(radius: 20); 27 | }); 28 | 29 | test("blur radius 50 matches expected output", () { 30 | runGaussianBlurTest(radius: 50); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /graphics/test/src/algorithms/line_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/algorithms/line.dart'; 2 | import 'package:graphics/src/core/bitmap.dart'; 3 | import 'package:graphics/src/core/color.dart'; 4 | import 'package:graphics/src/core/point.dart'; 5 | import 'package:graphics/src/utils.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import '../../utils.dart'; 9 | 10 | void main() { 11 | group("Line tests", () { 12 | Future runLineTest(GPoint from, GPoint to) async { 13 | final width = (from.x > to.x ? from.x : to.x) + 1; 14 | final height = (from.y > to.y ? from.y : to.y) + 1; 15 | 16 | final bitmap = GBitmap(width, height, config: GBitmapConfig.rgba); 17 | drawLine(bitmap, from, to, GColors.blue); 18 | 19 | final expectedFileName = 20 | "$testAssetPath/line_x${from.x}y${from.y}_x${to.x}y${to.y}.png"; 21 | final expectedBitmap = 22 | await loadBitmapFromImage(expectedFileName); 23 | 24 | expect(bitmapsAreEqual(bitmap, expectedBitmap), isTrue); 25 | } 26 | 27 | test("line from (0, 0) to (23, 8) matches expected output", () { 28 | runLineTest((x: 0, y: 0), (x: 23, y: 8)); 29 | }); 30 | 31 | test("line from (54, 95) to (124, 421) matches expected output", () { 32 | runLineTest((x: 54, y: 95), (x: 124, y: 421)); 33 | }); 34 | 35 | test("line from (1221, 3321) to (5543, 6934) matches expected output", () { 36 | runLineTest((x: 1221, y: 3321), (x: 5543, y: 6934)); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /graphics/test/src/pipeline/generic_pipeline_metrics.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/graphics.dart'; 2 | import 'package:graphics/src/core/bitmap.dart'; 3 | import 'package:graphics/src/core/rect.dart'; 4 | import 'package:graphics/src/pipeline/ellipse_node.dart'; 5 | import 'package:graphics/src/pipeline/layer_manager.dart'; 6 | import 'package:graphics/src/pipeline/node_graph.dart'; 7 | import 'package:graphics/src/pipeline/source_node.dart'; 8 | import 'package:graphics/src/utils.dart'; 9 | 10 | import '../../utils.dart'; 11 | 12 | void main() async { 13 | final nodeGraph = NodeGraph( 14 | SourceNode( 15 | source: GBitmap(500, 500, config: GBitmapConfig.rgba)..fillColor(GColors.green), 16 | ), 17 | ); 18 | 19 | final layerManager = LayerManager(nodeGraph); 20 | layerManager.addLayer( 21 | EllipseNode( 22 | from: (x: 0, y: 0), 23 | to: (x: 300, y: 499), 24 | color: GColors.red, 25 | inputNode: SourceNode( 26 | source: GBitmap(500, 500, config: GBitmapConfig.rgba), 27 | ), 28 | ), 29 | ); 30 | layerManager.addLayer( 31 | BlurNode( 32 | radius: 1, 33 | inputNode: PathNode( 34 | color: GColors.red, 35 | path: [ 36 | (x: 0, y: 0), 37 | (x: 55, y: 95), 38 | (x: 399, y: 34), 39 | (x: 0, y: 499), 40 | (x: 200, y: 200), 41 | (x: 499, y: 499), 42 | ], 43 | inputNode: SourceNode( 44 | source: GBitmap(500, 500, config: GBitmapConfig.rgba), 45 | ), 46 | ), 47 | ), 48 | ); 49 | 50 | saveBitmapToLocalDir(nodeGraph.process(GRect(x: 0, y: 0, width: 500, height: 500)), "output.png"); 51 | exportGraphToPNG(nodeGraph.rootNode, "output_graph"); 52 | 53 | // benchmark(() { 54 | // nodeGraph.process(GRect(x: 0, y: 0, width: 500, height: 500)); 55 | // }, iterations: 1); 56 | } 57 | -------------------------------------------------------------------------------- /graphics/test/src/pipeline/simple_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/core/color.dart'; 2 | import 'package:graphics/src/core/rect.dart'; 3 | import 'package:graphics/src/pipeline/ellipse_node.dart'; 4 | import 'package:graphics/src/pipeline/node_graph.dart'; 5 | import 'package:graphics/src/pipeline/source_node.dart'; 6 | import 'package:graphics/src/utils.dart'; 7 | 8 | void main() async { 9 | final nodeGraph = NodeGraph( 10 | EllipseNode( 11 | color: GColors.blue, 12 | from: (x: 50, y: 50), 13 | to: (x: 450, y: 250), 14 | inputNode: SourceNode( 15 | source: await loadBitmapFromImage("sunflowerfield.jpg"), 16 | ), 17 | ), 18 | ); 19 | 20 | await saveBitmapToLocalDir( 21 | nodeGraph.process(GRect(x: 0, y: 0, width: 539, height: 360)), 22 | "output.png", 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /graphics/test/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphics/src/core/bitmap.dart'; 2 | 3 | const testAssetPath = "test/assets"; 4 | 5 | bool bitmapsAreEqual(GBitmap a, GBitmap b) { 6 | if (a.width != b.width || a.height != b.height) { 7 | return false; 8 | } 9 | 10 | for (var x = 0; x < a.width; ++x) { 11 | for (var y = 0; y < a.height; ++y) { 12 | if (a.getPixel(x, y) != b.getPixel(x, y)) { 13 | return false; 14 | } 15 | } 16 | } 17 | 18 | return true; 19 | } 20 | 21 | void benchmark(void Function() action, {required int iterations}) { 22 | final times = []; 23 | final stopwatch = Stopwatch(); 24 | 25 | for (var i = 0; i < iterations; ++i) { 26 | stopwatch.reset(); 27 | stopwatch.start(); 28 | 29 | action(); 30 | 31 | stopwatch.stop(); 32 | times.add(stopwatch.elapsedMilliseconds); 33 | } 34 | 35 | final totalTime = times.fold(0, (sum, time) => sum + time); 36 | final avgTimeMillis = (totalTime / iterations.toDouble()); 37 | 38 | print( 39 | 'Average execution time over $iterations iterations: $avgTimeMillis ms'); 40 | } -------------------------------------------------------------------------------- /server/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sql linguist-detectable=true -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Kotlin ### 40 | .kotlin 41 | -------------------------------------------------------------------------------- /server/.gradle/8.7/checksums/checksums.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/server/.gradle/8.7/checksums/checksums.lock -------------------------------------------------------------------------------- /server/.gradle/8.7/dependencies-accessors/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/server/.gradle/8.7/dependencies-accessors/gc.properties -------------------------------------------------------------------------------- /server/.gradle/8.7/executionHistory/executionHistory.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/server/.gradle/8.7/executionHistory/executionHistory.lock -------------------------------------------------------------------------------- /server/.gradle/8.7/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/.gradle/8.7/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/server/.gradle/8.7/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /server/.gradle/8.7/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/server/.gradle/8.7/gc.properties -------------------------------------------------------------------------------- /server/.gradle/buildOutputCleanup/buildOutputCleanup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/server/.gradle/buildOutputCleanup/buildOutputCleanup.lock -------------------------------------------------------------------------------- /server/.gradle/buildOutputCleanup/cache.properties: -------------------------------------------------------------------------------- 1 | #Sun May 12 20:12:42 NZST 2024 2 | gradle.version=8.7 3 | -------------------------------------------------------------------------------- /server/.gradle/vcs-1/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/server/.gradle/vcs-1/gc.properties -------------------------------------------------------------------------------- /server/HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | 5 | For further reference, please consider the following sections: 6 | 7 | * [Official Gradle documentation](https://docs.gradle.org) 8 | * [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.2.5/gradle-plugin/reference/html/) 9 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.2.5/gradle-plugin/reference/html/#build-image) 10 | * [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/index.html#dummy_data.sql.jpa-and-spring-data) 11 | * [Spring Security](https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/index.html#web.security) 12 | * [Spring Web](https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/index.html#web) 13 | 14 | ### Guides 15 | 16 | The following guides illustrate how to use some features concretely: 17 | 18 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) 19 | * [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/) 20 | * [Securing a Web Application](https://spring.io/guides/gs/securing-web/) 21 | * [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/) 22 | * [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/) 23 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 24 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 25 | * [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) 26 | 27 | ### Additional Links 28 | 29 | These additional references should also help you: 30 | 31 | * [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) 32 | 33 | -------------------------------------------------------------------------------- /server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("org.springframework.boot") version "3.2.5" 5 | id("io.spring.dependency-management") version "1.1.4" 6 | kotlin("jvm") version "1.9.23" 7 | kotlin("plugin.spring") version "1.9.23" 8 | kotlin("plugin.jpa") version "1.9.23" 9 | } 10 | 11 | group = "com.pixapencil" 12 | version = "0.0.1-SNAPSHOT" 13 | extra["springCloudVersion"] = "3.0.0-RC1" 14 | 15 | java { 16 | sourceCompatibility = JavaVersion.VERSION_17 17 | } 18 | 19 | repositories { 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 25 | implementation("org.springframework.boot:spring-boot-starter-security") 26 | implementation("org.springframework.boot:spring-boot-starter-web") 27 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin") 28 | implementation("org.jetbrains.kotlin:kotlin-reflect") 29 | 30 | implementation("org.springframework.boot:spring-boot-starter-freemarker") 31 | implementation("io.awspring.cloud:spring-cloud-aws-starter-s3:3.0.0-RC2") 32 | implementation("io.awspring.cloud:spring-cloud-starter-aws-ses:2.4.4") 33 | implementation("io.awspring.cloud:spring-cloud-aws-starter-s3:3.0.0-RC2") 34 | implementation("io.github.oshai:kotlin-logging-jvm:5.1.0") 35 | 36 | implementation("jakarta.mail:jakarta.mail-api:2.1.3") 37 | implementation("org.springframework.boot:spring-boot-starter-mail") 38 | 39 | runtimeOnly("com.mysql:mysql-connector-j") 40 | testImplementation("org.springframework.boot:spring-boot-starter-test") { 41 | exclude(module = "mockito-core") 42 | } 43 | testImplementation("com.h2database:h2:2.2.224") 44 | testImplementation("org.springframework.security:spring-security-test") 45 | testImplementation("com.ninja-squad:springmockk:4.0.2") 46 | } 47 | 48 | dependencyManagement { 49 | imports { 50 | mavenBom("io.awspring.cloud:spring-cloud-aws-dependencies:${extra["springCloudVersion"]}") 51 | } 52 | } 53 | 54 | tasks.withType { 55 | kotlinOptions { 56 | freeCompilerArgs += "-Xjsr305=strict" 57 | jvmTarget = "17" 58 | } 59 | } 60 | 61 | tasks.withType { 62 | useJUnitPlatform() 63 | } 64 | -------------------------------------------------------------------------------- /server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /server/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "server" 2 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/ServerApplication.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | import org.springframework.scheduling.annotation.EnableScheduling 6 | 7 | @EnableScheduling 8 | @SpringBootApplication 9 | class ServerApplication 10 | 11 | fun main(args: Array) { 12 | runApplication(*args) 13 | } 14 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/config/FreemarkerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.config 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.context.annotation.Primary 6 | import org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean 7 | 8 | @Configuration 9 | class FreemarkerConfig { 10 | @Primary 11 | @Bean 12 | fun factoryBean(): FreeMarkerConfigurationFactoryBean { 13 | val bean = FreeMarkerConfigurationFactoryBean() 14 | bean.setTemplateLoaderPath("classpath:/templates/") 15 | return bean 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/config/SecurityConfig.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.config 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.http.HttpMethod 6 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 8 | import org.springframework.security.config.annotation.web.invoke 9 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 10 | import org.springframework.security.crypto.password.PasswordEncoder 11 | import org.springframework.security.web.SecurityFilterChain 12 | import org.springframework.web.cors.CorsConfiguration 13 | import org.springframework.web.cors.CorsConfigurationSource 14 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource 15 | 16 | @Configuration 17 | @EnableMethodSecurity 18 | class SecurityConfig { 19 | 20 | @Bean 21 | fun filterChain(http: HttpSecurity): SecurityFilterChain { 22 | http.invoke { 23 | csrf { disable() } 24 | cors { } 25 | 26 | authorizeHttpRequests { 27 | authorize(HttpMethod.POST, "/api/users/register", permitAll) 28 | authorize(HttpMethod.POST, "/api/users/login", permitAll) 29 | authorize(HttpMethod.POST, "/api/users/verify", permitAll) 30 | authorize(HttpMethod.GET, "/api/creations/gallery", permitAll) 31 | authorize(HttpMethod.GET, "/api/creations/daily-creation", permitAll) 32 | authorize(anyRequest, authenticated) 33 | } 34 | 35 | httpBasic { } 36 | } 37 | 38 | return http.build() 39 | } 40 | 41 | @Bean 42 | fun corsConfigurationSource(): CorsConfigurationSource { 43 | val configuration = CorsConfiguration().apply { 44 | allowedOrigins = listOf("*") 45 | allowedMethods = listOf("GET", "POST", "PUT", "PATCH", "DELETE") 46 | applyPermitDefaultValues() 47 | } 48 | 49 | val source = UrlBasedCorsConfigurationSource() 50 | source.registerCorsConfiguration("/api/**", configuration) 51 | 52 | return source 53 | } 54 | 55 | @Bean 56 | fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() 57 | } 58 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/controllers/CommentController.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.controllers 2 | 3 | import com.pixapencil.server.domain.User 4 | import com.pixapencil.server.dtos.AddCommentDTO 5 | import com.pixapencil.server.dtos.EditCommentDTO 6 | import com.pixapencil.server.services.AuthUser 7 | import com.pixapencil.server.services.CommentService 8 | import org.springframework.data.domain.PageRequest 9 | import org.springframework.security.core.annotation.AuthenticationPrincipal 10 | import org.springframework.web.bind.annotation.* 11 | 12 | @RestController 13 | @RequestMapping("/api/comments") 14 | class CommentController( 15 | private val commentService: CommentService, 16 | ) { 17 | @GetMapping 18 | fun getComments( 19 | @RequestParam creationId: Long, 20 | @RequestParam page: Int = 0, 21 | ) = commentService.getComments(creationId, PageRequest.of(page, 30)) 22 | 23 | @PostMapping 24 | fun addComment( 25 | @RequestBody addCommentDTO: AddCommentDTO, 26 | @AuthenticationPrincipal authUser: AuthUser, 27 | ) = commentService.addComment(addCommentDTO, authUser) 28 | 29 | @DeleteMapping("/{id}") 30 | fun deleteComment( 31 | @PathVariable id: Long, 32 | ) = commentService.deleteComment(id) 33 | 34 | @PutMapping("/{id}") 35 | fun editComment( 36 | @PathVariable id: Long, 37 | @RequestBody editCommentDTO: EditCommentDTO, 38 | ) = commentService.editComment(id, editCommentDTO) 39 | } 40 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/controllers/CreationController.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.controllers 2 | 3 | import com.pixapencil.server.dtos.UploadCreationDTO 4 | import com.pixapencil.server.services.AuthUser 5 | import com.pixapencil.server.services.CreationService 6 | import com.pixapencil.server.services.DailyCreationService 7 | import org.springframework.data.domain.PageRequest 8 | import org.springframework.security.core.annotation.AuthenticationPrincipal 9 | import org.springframework.web.bind.annotation.* 10 | 11 | @RestController 12 | @RequestMapping("/api/creations") 13 | class CreationController( 14 | private val creationService: CreationService, 15 | private val dailyCreationService: DailyCreationService, 16 | ) { 17 | 18 | @GetMapping("/gallery") 19 | fun getCreations( 20 | @RequestParam page: Int = 0, 21 | ) = creationService.getCreations(PageRequest.of(page, 30)) 22 | 23 | @GetMapping("/{id}") 24 | fun getCreation( 25 | @PathVariable id: Long, 26 | @AuthenticationPrincipal authUser: AuthUser, 27 | ) = creationService.getCreation(id, authUser) 28 | 29 | @PostMapping("/{id}/like") 30 | fun likeCreation( 31 | @PathVariable id: Long, 32 | @AuthenticationPrincipal authUser: AuthUser, 33 | ) = creationService.likeCreation(id, authUser) 34 | 35 | @PostMapping("/{id}/unlike") 36 | fun unlikeCreation( 37 | @PathVariable id: Long, 38 | @AuthenticationPrincipal authUser: AuthUser, 39 | ) = creationService.unlikeCreation(id, authUser) 40 | 41 | @DeleteMapping("/{id}") 42 | fun deleteCreation( 43 | @PathVariable id: Long, 44 | ) = creationService.deleteCreation(id) 45 | 46 | @GetMapping("/get-upload-url") 47 | fun getCreationUploadUrl( 48 | @RequestParam mimeType: String, 49 | ) = creationService.getCreationUploadUrl(mimeType) 50 | 51 | @PostMapping("/upload") 52 | fun uploadCreation( 53 | @AuthenticationPrincipal authUser: AuthUser, 54 | @RequestBody uploadCreation: UploadCreationDTO, 55 | ) = creationService.uploadCreation(uploadCreation, authUser) 56 | 57 | @GetMapping("/daily-creation") 58 | fun getDailyCreation() = dailyCreationService.getDailyCreation() 59 | } 60 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/controllers/UserController.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.controllers 2 | 3 | import com.pixapencil.server.domain.User 4 | import com.pixapencil.server.dtos.LoginUserDTO 5 | import com.pixapencil.server.dtos.RegisterUserDTO 6 | import com.pixapencil.server.services.UserService 7 | import org.springframework.http.HttpStatus 8 | import org.springframework.http.ResponseEntity 9 | import org.springframework.web.bind.annotation.* 10 | 11 | @RestController 12 | @RequestMapping("/api/users") 13 | class UserController(private val userService: UserService) { 14 | 15 | @PostMapping("/register") 16 | fun registerUser( 17 | @RequestBody registerUser: RegisterUserDTO, 18 | ) = userService.registerUser(registerUser) 19 | 20 | @PostMapping("/verify") 21 | fun verifyUser( 22 | @RequestParam userId: Long, 23 | @RequestParam code: String, 24 | ) = userService.verifyUser(userId, code) 25 | 26 | @PostMapping("/login") 27 | fun loginUser(@RequestBody loginUser: LoginUserDTO) = 28 | userService.authenticateUser(loginUser) 29 | ?.let { ResponseEntity.ok(it) } 30 | ?: ResponseEntity.status(HttpStatus.UNAUTHORIZED).build() 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/domain/Comment.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.domain 2 | 3 | import jakarta.persistence.* 4 | import org.hibernate.annotations.CreationTimestamp 5 | import org.hibernate.annotations.SQLInsert 6 | import org.hibernate.annotations.UpdateTimestamp 7 | import java.time.LocalDateTime 8 | 9 | @Table(name = "comments") 10 | @Entity 11 | class Comment( 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | var id: Long? = null, 15 | 16 | @Column(nullable = false, length = 1000) 17 | var text: String, 18 | 19 | @ManyToOne 20 | @JoinColumn(name = "user_id") 21 | var user: User, 22 | 23 | @ManyToOne 24 | @JoinColumn(name = "creation_id") 25 | var creation: Creation, 26 | 27 | @CreationTimestamp 28 | var createdAt: LocalDateTime = LocalDateTime.now(), 29 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/domain/Creation.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.domain 2 | 3 | import jakarta.persistence.* 4 | import org.hibernate.annotations.CreationTimestamp 5 | import java.time.LocalDateTime 6 | 7 | @Table(name = "creations") 8 | @Entity 9 | class Creation( 10 | @Id 11 | @GeneratedValue(strategy = GenerationType.IDENTITY) 12 | var id: Long? = null, 13 | 14 | @Column(nullable = false) 15 | var title: String, 16 | 17 | @Column(nullable = false, length = 5000) 18 | var description: String, 19 | 20 | @Column(nullable = false) 21 | var imageKey: String, 22 | 23 | @Column(nullable = false) 24 | var likeCount: Int = 0, 25 | 26 | @CreationTimestamp 27 | var createdAt: LocalDateTime = LocalDateTime.now(), 28 | 29 | @ManyToOne 30 | @JoinColumn(name = "user_id", nullable = false) 31 | var user: User, 32 | 33 | @ManyToMany(mappedBy = "likedCreations") 34 | val likedBy: MutableList = mutableListOf(), 35 | 36 | @Column(nullable = false) 37 | var commentCount: Int = 0, 38 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/domain/DailyCreation.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.domain 2 | 3 | import jakarta.persistence.* 4 | import java.time.LocalDate 5 | 6 | @Table(name = "daily_creations") 7 | @Entity 8 | class DailyCreation( 9 | @Id 10 | @GeneratedValue(strategy = GenerationType.IDENTITY) 11 | var id: Long? = null, 12 | 13 | @Column(nullable = false) 14 | var date: LocalDate = LocalDate.now(), 15 | 16 | @OneToOne 17 | @JoinColumn(name = "creation_id", nullable = false) 18 | var creation: Creation, 19 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/domain/User.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.domain 2 | 3 | import jakarta.persistence.* 4 | import org.hibernate.annotations.CreationTimestamp 5 | import java.time.LocalDateTime 6 | 7 | 8 | @Table(name = "users") 9 | @Entity 10 | class User( 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | var id: Long? = null, 14 | 15 | @Column(unique = true, nullable = false) 16 | var username: String, 17 | 18 | @Column(unique = true, nullable = false) 19 | var email: String, 20 | 21 | @Column(unique = true, nullable = false) 22 | @Lob 23 | var password: String, 24 | 25 | @Column(nullable = false) 26 | var profilePictureUrl: String, 27 | 28 | @Column(nullable = false) 29 | var isVerified: Boolean = false, 30 | 31 | @ManyToMany(cascade = [CascadeType.ALL]) 32 | @JoinTable( 33 | name = "creation_likes", 34 | joinColumns = [JoinColumn(name = "user_id")], 35 | inverseJoinColumns = [JoinColumn(name = "creation_id")] 36 | ) 37 | var likedCreations: MutableList = mutableListOf(), 38 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/domain/VerificationToken.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.domain 2 | 3 | import jakarta.persistence.* 4 | import java.time.LocalDateTime 5 | 6 | @Table(name = "verification_tokens") 7 | @Entity 8 | class VerificationToken( 9 | @Id 10 | @GeneratedValue(strategy = GenerationType.IDENTITY) 11 | var id: Long? = null, 12 | 13 | @Column(nullable = false) 14 | @Lob 15 | var code: String, 16 | 17 | @Column(nullable = false) 18 | var expiryDate: LocalDateTime, 19 | 20 | @OneToOne 21 | @JoinColumn(name = "user_id", nullable = false) 22 | var user: User, 23 | ) 24 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/AddCommentDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | data class AddCommentDTO( 4 | val text: String, 5 | val creationId: Long, 6 | ) 7 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/EditCommentDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | data class EditCommentDTO(val text: String) 4 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/GetAuthorDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | import com.pixapencil.server.domain.User 4 | 5 | data class GetAuthorDTO(val username: String, val profilePictureUrl: String) 6 | 7 | fun User.toGetAuthorDTO(): GetAuthorDTO { 8 | return GetAuthorDTO( 9 | username = this.username, 10 | profilePictureUrl = "https://pixapencil-gallery.s3.ap-southeast-2.amazonaws.com/" + this.profilePictureUrl, 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/GetCommentDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | import com.pixapencil.server.domain.Comment 4 | import java.time.LocalDate 5 | import java.time.LocalDateTime 6 | 7 | data class GetCommentDTO( 8 | val id: Long, 9 | val text: String, 10 | val author: GetAuthorDTO, 11 | val uploadDate: LocalDateTime, 12 | ) 13 | 14 | fun Comment.toGetCommentDTO(): GetCommentDTO { 15 | return GetCommentDTO( 16 | id = this.id!!, 17 | text = this.text, 18 | author = this.user.toGetAuthorDTO(), 19 | uploadDate = this.createdAt, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/GetCreationDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | import com.pixapencil.server.domain.Creation 4 | import com.pixapencil.server.domain.User 5 | 6 | data class GetCreationDTO( 7 | val id: Long, 8 | val title: String, 9 | val description: String, 10 | val imageUrl: String, 11 | val likeCount: Int, 12 | val isLiked: Boolean, 13 | val author: GetAuthorDTO, 14 | val uploadDate: String, 15 | val timeSince: String, 16 | ) 17 | 18 | fun Creation.toGetCreationDTO(context: User): GetCreationDTO { 19 | return GetCreationDTO( 20 | id = this.id!!, 21 | title = this.title, 22 | description = this.description, 23 | imageUrl = "https://pixapencil-gallery.s3.ap-southeast-2.amazonaws.com/" + this.imageKey, 24 | likeCount = this.likeCount, 25 | isLiked = this.likedBy.contains(context), 26 | uploadDate = this.createdAt.toString(), 27 | author = this.user.toGetAuthorDTO(), 28 | timeSince = this.createdAt.toString(), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/GetDailyCreationDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | import com.pixapencil.server.domain.DailyCreation 4 | 5 | data class GetDailyCreationDTO( 6 | val imageUrl: String, 7 | val author: GetAuthorDTO, 8 | ) 9 | 10 | fun DailyCreation.toGetDailyCreationDTO(): GetDailyCreationDTO { 11 | return GetDailyCreationDTO( 12 | imageUrl = "https://pixapencil-gallery.s3.ap-southeast-2.amazonaws.com/" + this.creation.imageKey, 13 | author = this.creation.user.toGetAuthorDTO() 14 | ) 15 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/GetUserDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | import com.pixapencil.server.domain.User 4 | 5 | data class GetUserDTO( 6 | val id: Long? = null, 7 | val username: String, 8 | val email: String, 9 | val profilePictureUrl: String 10 | ) 11 | 12 | fun User.toGetUserDTO(exposeId: Boolean = false): GetUserDTO { 13 | return GetUserDTO( 14 | id = if (exposeId) this.id else null, 15 | username = this.username, 16 | email = this.email, 17 | profilePictureUrl = this.profilePictureUrl, 18 | ) 19 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/LoginUserDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | data class LoginUserDTO(val email: String, val password: String) -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/RegisterUserDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | data class RegisterUserDTO( 4 | val username: String, 5 | val email: String, 6 | val password: String, 7 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/dtos/UploadCreationDTO.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.dtos 2 | 3 | data class UploadCreationDTO( 4 | val title: String, 5 | val description: String, 6 | val imageKey: String, 7 | ) 8 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/exceptions/EmailAlreadyInUseException.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.exceptions 2 | 3 | class EmailAlreadyInUseException : RuntimeException() 4 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/repos/CommentRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.repos 2 | 3 | import com.pixapencil.server.domain.Comment 4 | import org.springframework.data.domain.Page 5 | import org.springframework.data.domain.Pageable 6 | import org.springframework.data.jpa.repository.JpaRepository 7 | 8 | interface CommentRepository : JpaRepository { 9 | fun getCommentsByCreationId( 10 | creationId: Long, 11 | page: Pageable, 12 | ): Page 13 | } 14 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/repos/CreationRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.repos 2 | 3 | import com.pixapencil.server.domain.Creation 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | 6 | interface CreationRepository : JpaRepository 7 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/repos/DailyCreationRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.repos 2 | 3 | import com.pixapencil.server.domain.DailyCreation 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | import org.springframework.data.jpa.repository.Modifying 6 | import org.springframework.data.jpa.repository.Query 7 | import java.time.LocalDate 8 | import java.util.* 9 | 10 | interface DailyCreationRepository : JpaRepository { 11 | @Modifying 12 | @Query(value = """ 13 | INSERT INTO daily_creations (creation_id, date) 14 | SELECT c.id, CURRENT_DATE 15 | FROM creations c 16 | WHERE c.id NOT IN (SELECT dc.creation_id FROM daily_creations dc) 17 | ORDER BY RAND() LIMIT 1 18 | """, nativeQuery = true) 19 | fun updateDailyCreation() 20 | 21 | @Query( 22 | value = "SELECT * FROM daily_creations ORDER BY date DESC LIMIT 1", 23 | nativeQuery = true 24 | ) 25 | fun getDailyCreation(): DailyCreation? 26 | 27 | fun findByDate(date: LocalDate): DailyCreation? 28 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/repos/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.repos 2 | 3 | import com.pixapencil.server.domain.User 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | 6 | interface UserRepository : JpaRepository { 7 | fun findByEmail(email: String): User? 8 | 9 | fun findByUsername(username: String): User? 10 | } 11 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/repos/VerificationTokenRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.repos 2 | 3 | import com.pixapencil.server.domain.User 4 | import com.pixapencil.server.domain.VerificationToken 5 | import org.springframework.data.jpa.repository.JpaRepository 6 | import org.springframework.data.jpa.repository.Modifying 7 | import org.springframework.data.jpa.repository.Query 8 | import java.time.LocalDateTime 9 | 10 | interface VerificationTokenRepository : JpaRepository { 11 | fun findByUser(user: User): VerificationToken? 12 | 13 | @Modifying 14 | @Query("DELETE FROM VerificationToken t WHERE t.expiryDate < ?1") 15 | fun deleteExpiredTokens(expiryDate: LocalDateTime) 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/services/AuthUser.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.services 2 | 3 | import com.pixapencil.server.domain.User 4 | import org.springframework.security.core.GrantedAuthority 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority 6 | import org.springframework.security.core.userdetails.UserDetails 7 | 8 | class AuthUser(val user: User) : UserDetails { 9 | override fun getAuthorities(): MutableCollection { 10 | return mutableListOf(SimpleGrantedAuthority("ROLE_USER")) 11 | } 12 | 13 | override fun getPassword(): String { 14 | return user.password 15 | } 16 | 17 | override fun getUsername(): String { 18 | return user.username 19 | } 20 | 21 | override fun isAccountNonExpired(): Boolean { 22 | return true 23 | } 24 | 25 | override fun isAccountNonLocked(): Boolean { 26 | return true 27 | } 28 | 29 | override fun isCredentialsNonExpired(): Boolean { 30 | return true 31 | } 32 | 33 | override fun isEnabled(): Boolean { 34 | return user.isVerified 35 | } 36 | 37 | fun getUserId() = user.id 38 | } 39 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/services/DailyCreationService.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.services 2 | 3 | import com.pixapencil.server.domain.Creation 4 | import com.pixapencil.server.dtos.GetCreationDTO 5 | import com.pixapencil.server.dtos.GetDailyCreationDTO 6 | import com.pixapencil.server.dtos.toGetCreationDTO 7 | import com.pixapencil.server.dtos.toGetDailyCreationDTO 8 | import com.pixapencil.server.repos.DailyCreationRepository 9 | import com.pixapencil.server.repos.UserRepository 10 | import jakarta.persistence.EntityNotFoundException 11 | import jakarta.transaction.Transactional 12 | import org.springframework.boot.context.event.ApplicationReadyEvent 13 | import org.springframework.context.event.EventListener 14 | import org.springframework.data.repository.findByIdOrNull 15 | import org.springframework.scheduling.annotation.Scheduled 16 | import org.springframework.stereotype.Service 17 | import java.time.LocalDate 18 | 19 | @Transactional 20 | @Service 21 | class DailyCreationService( 22 | private val dailyCreationRepository: DailyCreationRepository, 23 | private val userRepository: UserRepository, 24 | ) { 25 | 26 | @EventListener(ApplicationReadyEvent::class) 27 | fun initializeDailyCreation() { 28 | updateDailyCreationIfNeeded() 29 | } 30 | 31 | @Scheduled(cron = "0 0 0 * * *") 32 | fun updateDailyCreation() { 33 | updateDailyCreationIfNeeded() 34 | } 35 | 36 | fun getDailyCreation(): GetDailyCreationDTO { 37 | val dailyCreation = dailyCreationRepository.getDailyCreation() ?: throw EntityNotFoundException() 38 | 39 | return dailyCreation.toGetDailyCreationDTO() 40 | } 41 | 42 | private fun updateDailyCreationIfNeeded() { 43 | val today = LocalDate.now() 44 | val existing = dailyCreationRepository.findByDate(today) 45 | 46 | if (existing == null) { 47 | dailyCreationRepository.updateDailyCreation() 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/services/MailService.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.services 2 | 3 | import freemarker.template.Configuration 4 | import org.springframework.beans.factory.annotation.Qualifier 5 | import org.springframework.mail.javamail.JavaMailSender 6 | import org.springframework.mail.javamail.MimeMessageHelper 7 | import org.springframework.stereotype.Service 8 | import org.springframework.ui.freemarker.FreeMarkerTemplateUtils 9 | import java.nio.charset.StandardCharsets 10 | import java.util.concurrent.CompletableFuture 11 | import java.util.concurrent.Executor 12 | 13 | data class PxpMail( 14 | val from: String, 15 | val to: String, 16 | val subject: String, 17 | val body: String, 18 | val isHtml: Boolean, 19 | ) 20 | 21 | @Service 22 | class MailService( 23 | private val javaMailSender: JavaMailSender, 24 | @Qualifier("applicationTaskExecutor") private val executor: Executor, 25 | private val freemarkerConfig: Configuration, 26 | ) { 27 | fun sendVerificationMail( 28 | to: String, 29 | verifCode: String, 30 | ) { 31 | val htmlContent = templateToString("verify-email.ftl", mapOf("verifCode" to verifCode)) 32 | val mail = PxpMail(from = "todoescode@gmail.com", to = to, subject = "Verify your account", body = htmlContent, isHtml = true) 33 | 34 | sendMail(mail) 35 | } 36 | 37 | private fun sendMail(mail: PxpMail) { 38 | CompletableFuture.runAsync({ 39 | val message = javaMailSender.createMimeMessage() 40 | val helper = MimeMessageHelper(message, MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, StandardCharsets.UTF_8.name()) 41 | helper.setText(mail.body, mail.isHtml) 42 | helper.setSubject(mail.subject) 43 | helper.setFrom(mail.from) 44 | helper.setTo(mail.to) 45 | javaMailSender.send(message) 46 | }, executor) 47 | } 48 | 49 | private fun templateToString( 50 | templateLocation: String, 51 | model: Map, 52 | ): String { 53 | return FreeMarkerTemplateUtils.processTemplateIntoString(freemarkerConfig.getTemplate(templateLocation), model) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/services/S3Service.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.services 2 | 3 | import io.awspring.cloud.s3.S3Template 4 | import org.springframework.stereotype.Service 5 | import java.time.Duration 6 | import java.util.* 7 | 8 | @Service 9 | class S3Service(private val s3Template: S3Template) { 10 | fun generateRandomKey(mimeType: String): String { 11 | val supportedMimeTypes = setOf("image/jpeg", "image/png") 12 | 13 | if (mimeType !in supportedMimeTypes) { 14 | throw IllegalArgumentException("Unsupported MIME type: $mimeType") 15 | } 16 | 17 | val ext = 18 | when (mimeType) { 19 | "image/jpeg" -> "jpg" 20 | "image/png" -> "png" 21 | else -> throw IllegalArgumentException("Unsupported mime type") 22 | } 23 | 24 | return "${UUID.randomUUID()}.$ext" 25 | } 26 | 27 | fun createSignedPutURL( 28 | key: String, 29 | bucketName: String = "pixapencil-gallery", 30 | ): String = s3Template.createSignedPutURL(bucketName, key, Duration.ofMinutes(1)).toString() 31 | 32 | fun deleteObject( 33 | key: String, 34 | bucketName: String = "pixapencil-gallery", 35 | ) = s3Template.deleteObject(key, bucketName) 36 | } 37 | -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/services/TokenCleanupService.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.services 2 | 3 | import com.pixapencil.server.repos.VerificationTokenRepository 4 | import jakarta.transaction.Transactional 5 | import org.springframework.scheduling.annotation.Scheduled 6 | import org.springframework.stereotype.Component 7 | import org.springframework.stereotype.Service 8 | import java.time.LocalDateTime 9 | 10 | @Transactional 11 | @Service 12 | class TokenCleanupService(private val tokenRepository: VerificationTokenRepository) { 13 | 14 | @Scheduled(cron = "0 0 1 * * ?") 15 | fun cleanup() { 16 | tokenRepository.deleteExpiredTokens(LocalDateTime.now()) 17 | } 18 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/com/pixapencil/server/services/UserDetailsService.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server.services 2 | 3 | import com.pixapencil.server.repos.UserRepository 4 | import org.springframework.security.core.userdetails.UserDetailsService 5 | import org.springframework.security.core.userdetails.UsernameNotFoundException 6 | import org.springframework.stereotype.Service 7 | 8 | @Service 9 | class UserDetailsService(private val userRepository: UserRepository) : UserDetailsService { 10 | override fun loadUserByUsername(username: String): AuthUser { 11 | val user = userRepository.findByUsername(username) ?: throw UsernameNotFoundException("User $username not found") 12 | 13 | return AuthUser(user) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: server 4 | sql: 5 | init: 6 | mode: always 7 | datasource: 8 | url: 'jdbc:mysql://localhost:3306/pixapencil' 9 | username: ${DATA_SOURCE_USERNAME} 10 | password: ${DATA_SOURCE_PASSWORD} 11 | driver-class-name: com.mysql.cj.jdbc.Driver 12 | cloud: 13 | aws: 14 | credentials: 15 | access-key: ${AWS_ACCESS_KEY} 16 | secret-key: ${AWS_SECRET_KEY} 17 | region: 18 | static: ap-southeast-2 19 | logging: 20 | level: 21 | org: 22 | springframework: 23 | security: DEBUG 24 | -------------------------------------------------------------------------------- /server/src/main/resources/dummy_data.sql: -------------------------------------------------------------------------------- 1 | -- Run this file to initialize dummy data for DB 2 | 3 | INSERT INTO users (username, email, password, is_verified, profile_picture_url, created_at) 4 | VALUES 5 | ( 6 | 'tomdoeslinux', 7 | 'todoescode@gmail.com', 8 | '$2a$10$o1eHOfSSgpj4UhOvBnq4EuiVn6eeZAtYh3m8wS4n1i5mlYr/PlPzG', 9 | TRUE, 10 | 'profile_pic.png', 11 | NOW() 12 | ); 13 | 14 | INSERT INTO creations (title, description, image_key, user_id) 15 | VALUES 16 | ('Beautiful Art', 'A beautiful pixel art creation', 'pixel_art_1.png', 1), 17 | ('Pixel Piece', 'An amazing pixel art piece', 'pixel_art_2.png', 1), 18 | ('Green Horizon', 'A stunning piece of pixel art', 'pixel_art_3.png', 1), 19 | ('New Beginnings', 'A wonderful example of pixel art', 'pixel_art_4.png', 1), 20 | ('Sunset on the Horizon', 'An extraordinary pixel art creation', 'pixel_art_5.png', 1); 21 | 22 | INSERT INTO comments (user_id, creation_id, text) 23 | VALUES 24 | (1, 1, 'This is an amazing creation! Really loved the details.'), 25 | (1, 2, 'Fantastic work! The colors are so vibrant.'), 26 | (1, 3, 'Great job on this one. Looks really impressive!'), 27 | (1, 4, 'I appreciate the effort you put into this creation. Well done!'), 28 | (1, 5, 'Superb! This is my favorite so far.'); -------------------------------------------------------------------------------- /server/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS users ( 2 | id BIGINT AUTO_INCREMENT PRIMARY KEY, 3 | username VARCHAR(255) NOT NULL UNIQUE, 4 | email VARCHAR(255) NOT NULL UNIQUE, 5 | password TEXT NOT NULL, 6 | is_verified BOOLEAN NOT NULL DEFAULT FALSE, 7 | profile_picture_url VARCHAR(255) NOT NULL, 8 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 9 | ); 10 | 11 | CREATE TABLE IF NOT EXISTS creations ( 12 | id BIGINT AUTO_INCREMENT PRIMARY KEY, 13 | title VARCHAR(255) NOT NULL, 14 | description VARCHAR(5000), 15 | image_key VARCHAR(255) NOT NULL, 16 | like_count INT NOT NULL DEFAULT 0, 17 | comment_count INT NOT NULL DEFAULT 0, 18 | user_id BIGINT NOT NULL, 19 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 20 | FOREIGN KEY (user_id) REFERENCES users(id) 21 | ); 22 | 23 | CREATE TABLE IF NOT EXISTS creation_likes ( 24 | user_id BIGINT NOT NULL, 25 | creation_id BIGINT NOT NULL, 26 | PRIMARY KEY (user_id, creation_id), 27 | FOREIGN KEY (user_id) REFERENCES users(id), 28 | FOREIGN KEY (creation_id) REFERENCES creations(id) 29 | ); 30 | 31 | CREATE TABLE IF NOT EXISTS comments ( 32 | id BIGINT AUTO_INCREMENT PRIMARY KEY, 33 | user_id BIGINT NOT NULL, 34 | creation_id BIGINT NOT NULL, 35 | text VARCHAR(1000) NOT NULL, 36 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 37 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 38 | FOREIGN KEY (user_id) REFERENCES users(id), 39 | FOREIGN KEY (creation_id) REFERENCES creations(id) 40 | ); 41 | 42 | CREATE TABLE IF NOT EXISTS verification_tokens ( 43 | id BIGINT AUTO_INCREMENT PRIMARY KEY, 44 | code VARCHAR(6) NOT NULL, 45 | user_id BIGINT NOT NULL, 46 | expiry_date TIMESTAMP NOT NULL, 47 | FOREIGN KEY (user_id) REFERENCES users(id) 48 | ); 49 | 50 | CREATE TABLE IF NOT EXISTS daily_creations ( 51 | id BIGINT AUTO_INCREMENT PRIMARY KEY, 52 | date DATE NOT NULL UNIQUE, 53 | creation_id BIGINT NOT NULL, 54 | FOREIGN KEY (creation_id) REFERENCES creations(id) 55 | ) -------------------------------------------------------------------------------- /server/src/main/resources/templates/verify-email.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Browser 6 | 9 | 10 | 11 |
12 |

PixaPencil Verification Code

13 |
14 |
15 | Hi! 16 | 17 | Please enter this code to verify your account: 18 |

${verifCode}

19 | 20 | We’re glad you’re here, 21 | 22 | The PixaPencil team 23 |
24 | 25 | -------------------------------------------------------------------------------- /server/src/test/kotlin/com/pixapencil/server/DummyData.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server 2 | 3 | import com.pixapencil.server.domain.User 4 | import com.pixapencil.server.services.AuthUser 5 | 6 | val authContext = AuthUser(User( 7 | id = 1L, 8 | username = "user", 9 | email = "user@gmail.com", 10 | password = "password", 11 | profilePictureUrl = "https://example.com/profile/dummyUser.jpg", 12 | isVerified = true 13 | )) 14 | -------------------------------------------------------------------------------- /server/src/test/kotlin/com/pixapencil/server/ServerApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.pixapencil.server 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 6 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase 7 | import org.springframework.boot.test.context.SpringBootTest 8 | import org.springframework.test.context.ActiveProfiles 9 | 10 | @SpringBootTest 11 | @ActiveProfiles("test") 12 | class ServerApplicationTests { 13 | @Test 14 | fun contextLoads() { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/src/test/resources/config/application-test.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | sql: 3 | init: 4 | mode: always 5 | datasource: 6 | url: 'jdbc:h2:mem:pixapencil;TRACE_LEVEL_FILE=0;TRACE_LEVEL_SYSTEM_OUT=0;DATABASE_TO_UPPER=false;MODE=MySQL;NON_KEYWORDS=VALUE' 7 | username: sa 8 | password: 9 | driverClassName: org.h2.Driver 10 | h2: 11 | console: 12 | enabled: true 13 | path: "/h2-console" 14 | jpa: 15 | defer-datasource-initialization: true -------------------------------------------------------------------------------- /web.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier", 9 | ], 10 | ignorePatterns: ["dist", ".eslintrc.cjs"], 11 | parser: "@typescript-eslint/parser", 12 | plugins: ["react-refresh", "prettier"], 13 | rules: { 14 | "react-refresh/only-export-components": [ 15 | "warn", 16 | { allowConstantExport: true }, 17 | ], 18 | "@typescript-eslint/no-explicit-any": "off", 19 | "prettier/prettier": "error", 20 | "no-restricted-imports": [ 21 | "error", { 22 | "patterns": ["@/features/*/*", "@/components/*/*", "@/app/*/*"] // TODO 23 | } 24 | ] 25 | }, 26 | } -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /web/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 80, 9 | "proseWrap": "always", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": false, 13 | "singleQuote": false, 14 | "tabWidth": 2, 15 | "trailingComma": "all", 16 | "useTabs": false, 17 | "endOfLine": "auto" 18 | } 19 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and 4 | some ESLint rules. 5 | 6 | Currently, two official plugins are available: 7 | 8 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) 9 | uses [Babel](https://babeljs.io/) for Fast Refresh 10 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) 11 | uses [SWC](https://swc.rs/) for Fast Refresh 12 | 13 | ## Expanding the ESLint configuration 14 | 15 | If you are developing a production application, we recommend updating the 16 | configuration to enable type aware lint rules: 17 | 18 | - Configure the top-level `parserOptions` property like this: 19 | 20 | ```js 21 | export default { 22 | // other rules... 23 | parserOptions: { 24 | ecmaVersion: "latest", 25 | sourceType: "module", 26 | project: ["./tsconfig.json", "./tsconfig.node.json"], 27 | tsconfigRootDir: __dirname, 28 | }, 29 | } 30 | ``` 31 | 32 | - Replace `plugin:@typescript-eslint/recommended` to 33 | `plugin:@typescript-eslint/recommended-type-checked` or 34 | `plugin:@typescript-eslint/strict-type-checked` 35 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 36 | - Install 37 | [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and 38 | add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` 39 | list 40 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "test": "vitest" 12 | }, 13 | "dependencies": { 14 | "@chakra-ui/react": "^2.8.2", 15 | "@emotion/react": "^11.11.4", 16 | "@emotion/styled": "^11.11.5", 17 | "@eslint/compat": "^1.0.3", 18 | "@eslint/js": "^9.4.0", 19 | "@reduxjs/toolkit": "^2.2.5", 20 | "@typescript-eslint/eslint-plugin": "^7.12.0", 21 | "@typescript-eslint/parser": "^7.12.0", 22 | "eslint": "^8.57.0", 23 | "eslint-config-prettier": "^9.1.0", 24 | "eslint-plugin-prettier": "^5.1.3", 25 | "eslint-plugin-react": "^7.34.2", 26 | "eslint-plugin-react-hooks": "^4.6.0", 27 | "eslint-plugin-react-refresh": "^0.4.6", 28 | "framer-motion": "^11.2.0", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-hook-form": "^7.51.4", 32 | "react-redux": "^9.1.2", 33 | "typescript-eslint": "^7.12.0", 34 | "wouter": "^3.1.3" 35 | }, 36 | "devDependencies": { 37 | "@testing-library/jest-dom": "^6.4.6", 38 | "@testing-library/react": "^16.0.0", 39 | "@types/react": "^18.3.3", 40 | "@types/react-dom": "^18.3.0", 41 | "@vitejs/plugin-react": "^4.2.1", 42 | "globals": "^15.8.0", 43 | "jsdom": "^24.1.0", 44 | "msw": "^2.3.1", 45 | "prettier": "3.3.0", 46 | "typescript": "^5.2.2", 47 | "vite": "^5.2.0", 48 | "vite-plugin-svgr": "^4.2.0", 49 | "vite-tsconfig-paths": "^4.3.2", 50 | "vitest": "^1.6.0" 51 | }, 52 | "msw": { 53 | "workerDirectory": [ 54 | "public" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/web/src/App.css -------------------------------------------------------------------------------- /web/src/api/root-api.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" 2 | 3 | export const s3BaseUrl = 4 | "https://pixapencil-gallery.s3.ap-southeast-2.amazonaws.com" 5 | export const baseUrl = "http://localhost:8080/api" 6 | 7 | const AUTH_REQUIRED_ENDPOINTS = [ 8 | "addComment", 9 | "deleteComment", 10 | "editComment", 11 | "likeCreation", 12 | "unlikeCreation", 13 | "uploadCreation", 14 | ] 15 | 16 | const baseQuery = fetchBaseQuery({ 17 | baseUrl, 18 | prepareHeaders: (headers, { endpoint }) => { 19 | console.log("Endpoint " + endpoint) 20 | if (AUTH_REQUIRED_ENDPOINTS.includes(endpoint)) { 21 | const username = "tomdoeslinux" 22 | const password = "password" 23 | const basicAuth = btoa(`${username}:${password}`) 24 | headers.set("Authorization", `Basic ${basicAuth}`) 25 | } 26 | 27 | return headers 28 | }, 29 | }) 30 | 31 | export const rootApi = createApi({ 32 | baseQuery, 33 | endpoints: () => ({}), 34 | }) 35 | 36 | export type RootEndpointBulder = Parameters< 37 | Parameters[0]["endpoints"] 38 | >[0] 39 | 40 | export type RootFetchWithBQ = Parameters< 41 | Exclude[0]["queryFn"], undefined> 42 | >[3] 43 | -------------------------------------------------------------------------------- /web/src/api/root-handlers.ts: -------------------------------------------------------------------------------- 1 | import { creationsHandlers } from "@/features/creations" 2 | import { setupServer } from "msw/node" 3 | 4 | const rootHandlers = [...creationsHandlers] 5 | 6 | export const server = setupServer(...rootHandlers) 7 | -------------------------------------------------------------------------------- /web/src/app/app-provider.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider, extendTheme, ThemeOverride } from "@chakra-ui/react" 2 | import { PropsWithChildren } from "react" 3 | import { Provider } from "react-redux" 4 | import store from "src/store/app-store" 5 | 6 | const chakraTheme = extendTheme({ 7 | fonts: { 8 | body: "Manrope, sans-serif", 9 | heading: "Manrope, sans-serif", 10 | }, 11 | colors: { 12 | px: { 13 | blue: { 14 | med: "#3660D3", 15 | dark: "#2548A7", 16 | darkest: "#1D3986", 17 | }, 18 | gray: "#CFCFCF", 19 | }, 20 | }, 21 | components: { 22 | Heading: { 23 | baseStyle: { 24 | color: "#242124", 25 | fontWeight: "medium", 26 | }, 27 | }, 28 | Text: { 29 | baseStyle: { 30 | color: "#242124", 31 | fontWeight: "medium", 32 | }, 33 | }, 34 | Button: { 35 | baseStyle: { 36 | fontWeight: "medium", 37 | }, 38 | }, 39 | }, 40 | global: { 41 | body: { 42 | color: "#242124", 43 | fontWeight: "medium", 44 | }, 45 | p: { 46 | color: "#242124", 47 | fontWeight: "medium", 48 | }, 49 | }, 50 | } satisfies ThemeOverride) 51 | 52 | export function AppProvider(props: PropsWithChildren) { 53 | return ( 54 | 55 | {props.children} 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /web/src/app/routes/creation-details-page.tsx: -------------------------------------------------------------------------------- 1 | import { CommentsList, useGetCommentsQuery } from "@/features/comments" 2 | import { CreationDetails, useGetCreationQuery } from "@/features/creations" 3 | import { Text } from "@chakra-ui/react" 4 | 5 | interface CreationDetailsPageProps { 6 | creationId: number 7 | } 8 | 9 | export default function CreationDetailsPage(props: CreationDetailsPageProps) { 10 | const { data: creation } = useGetCreationQuery(props.creationId) 11 | 12 | const { data: commentsResponse, refetch: refetchComments } = 13 | useGetCommentsQuery({ creationId: creation?.id ?? 0 }, { skip: !creation }) 14 | const comments = commentsResponse?.content 15 | 16 | if (!creation) { 17 | return Loading... 18 | } 19 | 20 | return ( 21 | {}} 24 | commentsSlot={ 25 | 30 | } 31 | /> 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /web/src/app/routes/login-page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "@/features/auth" 2 | import { useGetDailyCreationQuery } from "@/features/creations" 3 | 4 | export default function LoginPage() { 5 | const { data: dailyCreation } = useGetDailyCreationQuery() 6 | 7 | return dailyCreation && 8 | } 9 | -------------------------------------------------------------------------------- /web/src/app/routes/register-page.tsx: -------------------------------------------------------------------------------- 1 | import { RegisterForm } from "@/features/auth" 2 | 3 | export default function RegisterPage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /web/src/assets/icons/ICOArrowLeft.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/icons/ICOArrowRight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/icons/ICOClose.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/icons/ICOComment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/icons/ICOCommentFilled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/icons/ICOHeart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/icons/ICOHeartFilled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/icons/ICOSearch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/images/app-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/web/src/assets/images/app-logo.png -------------------------------------------------------------------------------- /web/src/assets/images/google-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomdoeslinux/PixaPencil/779b1ff6e6a17170db735a59892f2e1bf9eaec98/web/src/assets/images/google-logo.png -------------------------------------------------------------------------------- /web/src/assets/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | /// 3 | export { default as ICOComment } from "./icons/ICOComment.svg?react" 4 | export { default as ICOClose } from "./icons/ICOClose.svg?react" 5 | export { default as ICOArrowLeft } from "./icons/ICOArrowLeft.svg?react" 6 | export { default as ICOArrowRight } from "./icons/ICOArrowRight.svg?react" 7 | export { default as ICOSearch } from "./icons/ICOSearch.svg?react" 8 | export { default as ICOHeartFilled } from "./icons/ICOHeartFilled.svg?react" 9 | export { default as ICOHeart } from "./icons/ICOHeart.svg?react" 10 | export { default as ICOCommentFilled } from "./icons/ICOCommentFilled.svg?react" 11 | export { default as appLogo } from "./images/app-logo.png" 12 | export { default as googleLogo } from "./images/google-logo.png" 13 | -------------------------------------------------------------------------------- /web/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ComponentsPage } from "./components-page" 2 | export { default as PXButton } from "./ui/px-button" 3 | export { default as PXTag } from "./ui/px-tag" 4 | export { default as PXTags } from "./ui/px-tags" 5 | export { PXInput, PXInputGroup, PXInputLeftElement } from "./ui/px-input" 6 | export { default as PXTextarea } from "./ui/px-textarea" 7 | export { default as PXModal, usePXModal } from "./ui/px-modal" 8 | export type { PXModalProps } from "./ui/px-modal" 9 | export { default as PXIconButton } from "./ui/px-icon-button" 10 | -------------------------------------------------------------------------------- /web/src/components/ui/px-button.tsx: -------------------------------------------------------------------------------- 1 | import { ICOComment } from "@/assets" 2 | import { Box, Button, ButtonProps } from "@chakra-ui/react" 3 | 4 | type PXButtonVariant = "filled" | "outlined" 5 | 6 | function getButtonStylesForVariant(variant: PXButtonVariant): ButtonProps { 7 | switch (variant) { 8 | case "filled": 9 | return { 10 | background: "px.blue.med", 11 | color: "white", 12 | _hover: { background: "px.blue.dark" }, 13 | _active: { background: "px.blue.darkest" }, 14 | } 15 | case "outlined": 16 | return { 17 | background: "white", 18 | border: "1px solid", 19 | borderColor: "px.gray", 20 | _hover: { background: "gray.100" }, 21 | _active: { background: "gray.200" }, 22 | } 23 | } 24 | } 25 | 26 | interface PXButtonProps extends ButtonProps { 27 | variant?: PXButtonVariant 28 | } 29 | 30 | export default function PXButton({ 31 | variant = "filled", 32 | leftIcon, 33 | ...props 34 | }: PXButtonProps) { 35 | return ( 36 |