├── .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 |

3 |
PixaPencil
4 |
5 | Join the [**official community**](https://discord.gg/cYtaTnuweW).
6 |
7 | [

](https://github.com/therealbluepandabear/PixaPencil/releases/latest)
8 | [

](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 |
5 |
6 |
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 |
4 |
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 |