chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: ['https://boosty.to/t8rin']
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: Tell us what happened...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
placeholder: Tell us what should have happened...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
placeholder: You can drag and drop images here...
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
placeholder: e.g., Samsung Galaxy S23
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: OS
|
||||
placeholder: e.g., Android 14
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: locale
|
||||
attributes:
|
||||
label: Locale
|
||||
placeholder: e.g., en-US, ru-RU
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: App Version
|
||||
placeholder: e.g., 1.2.3
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: variant
|
||||
attributes:
|
||||
label: FOSS or Market
|
||||
description: Which variant of the app are you using?
|
||||
options:
|
||||
- FOSS
|
||||
- Market (Google Play)
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
placeholder: A clear and concise description of what the problem is...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
placeholder: A clear and concise description of what you want to happen...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
placeholder: A clear and concise description of any alternative solutions or features you've considered...
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Android CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'adopt'
|
||||
java-package: 'jdk'
|
||||
cache: gradle
|
||||
|
||||
- name: Set up Keystore
|
||||
run: |
|
||||
sudo apt update -y || true
|
||||
sudo apt install -y --no-install-recommends coreutils
|
||||
mkdir -p $RUNNER_TEMP/keystores
|
||||
echo "${{ secrets.SIGNING_KEY }}" | base64 --decode > $RUNNER_TEMP/keystores/keystore.jks
|
||||
|
||||
- name: Build FOSS APKs
|
||||
run: bash ./gradlew assembleFossRelease
|
||||
|
||||
- name: Sign FOSS APKs
|
||||
run: |
|
||||
ANDROID_SDK_PATH=$ANDROID_HOME/build-tools/35.0.0/apksigner
|
||||
for apk in app/build/outputs/apk/foss/release/*.apk; do
|
||||
$ANDROID_SDK_PATH sign \
|
||||
--ks $RUNNER_TEMP/keystores/keystore.jks \
|
||||
--ks-key-alias ${{ secrets.ALIAS }} \
|
||||
--ks-pass pass:${{ secrets.KEY_STORE_PASS }} \
|
||||
--key-pass pass:${{ secrets.KEY_STORE_PASS }} \
|
||||
--out "$apk" \
|
||||
"$apk"
|
||||
done
|
||||
|
||||
- name: Upload FOSS APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Signed FOSS APKs
|
||||
path: app/build/outputs/apk/foss/release/*.apk
|
||||
|
||||
- name: Delete FOSS APKs
|
||||
run: rm -rf app/build/outputs/apk/foss
|
||||
|
||||
- name: Build Market APKs
|
||||
run: bash ./gradlew assembleMarketRelease
|
||||
|
||||
- name: Sign Market APKs
|
||||
run: |
|
||||
ANDROID_SDK_PATH=$ANDROID_HOME/build-tools/35.0.0/apksigner
|
||||
for apk in app/build/outputs/apk/market/release/*.apk; do
|
||||
$ANDROID_SDK_PATH sign \
|
||||
--ks $RUNNER_TEMP/keystores/keystore.jks \
|
||||
--ks-key-alias ${{ secrets.ALIAS }} \
|
||||
--ks-pass pass:${{ secrets.KEY_STORE_PASS }} \
|
||||
--key-pass pass:${{ secrets.KEY_STORE_PASS }} \
|
||||
--out "$apk" \
|
||||
"$apk"
|
||||
done
|
||||
|
||||
- name: Upload Market APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Signed Market APKs
|
||||
path: app/build/outputs/apk/market/release/*.apk
|
||||
|
||||
- name: Delete Market APKs
|
||||
run: rm -rf app/build/outputs/apk/market
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Android CI FOSS
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'adopt'
|
||||
java-package: 'jdk'
|
||||
cache: gradle
|
||||
|
||||
- name: Set up Keystore
|
||||
run: |
|
||||
sudo apt update -y || true
|
||||
sudo apt install -y --no-install-recommends coreutils
|
||||
mkdir -p $RUNNER_TEMP/keystores
|
||||
echo "${{ secrets.SIGNING_KEY }}" | base64 --decode > $RUNNER_TEMP/keystores/keystore.jks
|
||||
|
||||
- name: Build FOSS APKs
|
||||
run: bash ./gradlew assembleFossRelease
|
||||
|
||||
- name: Sign FOSS APKs
|
||||
run: |
|
||||
ANDROID_SDK_PATH=$ANDROID_HOME/build-tools/35.0.0/apksigner
|
||||
for apk in app/build/outputs/apk/foss/release/*.apk; do
|
||||
$ANDROID_SDK_PATH sign \
|
||||
--ks $RUNNER_TEMP/keystores/keystore.jks \
|
||||
--ks-key-alias ${{ secrets.ALIAS }} \
|
||||
--ks-pass pass:${{ secrets.KEY_STORE_PASS }} \
|
||||
--key-pass pass:${{ secrets.KEY_STORE_PASS }} \
|
||||
--out "$apk" \
|
||||
"$apk"
|
||||
done
|
||||
|
||||
- name: Upload FOSS APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Signed FOSS APKs
|
||||
path: app/build/outputs/apk/foss/release/*.apk
|
||||
|
||||
- name: Delete FOSS APKs
|
||||
run: rm -rf app/build/outputs/apk/foss
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Android CI Market
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'adopt'
|
||||
java-package: 'jdk'
|
||||
cache: gradle
|
||||
|
||||
- name: Set up Keystore
|
||||
run: |
|
||||
sudo apt update -y || true
|
||||
sudo apt install -y --no-install-recommends coreutils
|
||||
mkdir -p $RUNNER_TEMP/keystores
|
||||
echo "${{ secrets.SIGNING_KEY }}" | base64 --decode > $RUNNER_TEMP/keystores/keystore.jks
|
||||
|
||||
- name: Build Market APKs
|
||||
run: bash ./gradlew assembleMarketRelease
|
||||
|
||||
- name: Sign Market APKs
|
||||
run: |
|
||||
ANDROID_SDK_PATH=$ANDROID_HOME/build-tools/35.0.0/apksigner
|
||||
for apk in app/build/outputs/apk/market/release/*.apk; do
|
||||
$ANDROID_SDK_PATH sign \
|
||||
--ks $RUNNER_TEMP/keystores/keystore.jks \
|
||||
--ks-key-alias ${{ secrets.ALIAS }} \
|
||||
--ks-pass pass:${{ secrets.KEY_STORE_PASS }} \
|
||||
--key-pass pass:${{ secrets.KEY_STORE_PASS }} \
|
||||
--out "$apk" \
|
||||
"$apk"
|
||||
done
|
||||
|
||||
- name: Upload Market APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Signed Market APKs
|
||||
path: app/build/outputs/apk/market/release/*.apk
|
||||
|
||||
- name: Delete Market APKs
|
||||
run: rm -rf app/build/outputs/apk/market
|
||||
@@ -0,0 +1,113 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
#on:
|
||||
# push:
|
||||
# tags:
|
||||
# - '*'
|
||||
|
||||
jobs:
|
||||
build_and_release:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
|
||||
- name: Set version variable
|
||||
run: echo "GITHUB_REF_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Assemble release
|
||||
env:
|
||||
VERSION_NAME: ${{ env.GITHUB_REF_NAME }}
|
||||
run: bash ./gradlew assembleRelease
|
||||
|
||||
- uses: iota9star/sign-android-release@v1.0.5
|
||||
name: Sign FOSS APK
|
||||
# ID used to access action output
|
||||
id: sign_app_foss
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/foss/release
|
||||
fileRex: .*apk
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASS }}
|
||||
keyPassword: ${{ secrets.KEY_STORE_PASS }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
|
||||
- name: Rename foss file stage
|
||||
run: |
|
||||
ls -la app/build/outputs/apk/market/release
|
||||
mv "app/build/outputs/apk/foss/release/image-toolbox-$GITHUB_REF_NAME-foss-arm64-v8a-release-unsigned-signed.apk" "image-toolbox-$GITHUB_REF_NAME-foss-arm64-v8a.apk"
|
||||
mv "app/build/outputs/apk/foss/release/image-toolbox-$GITHUB_REF_NAME-foss-universal-release-unsigned-signed.apk" "image-toolbox-$GITHUB_REF_NAME-foss-universal.apk"
|
||||
mv "app/build/outputs/apk/foss/release/image-toolbox-$GITHUB_REF_NAME-foss-armeabi-v7a-release-unsigned-signed.apk" "image-toolbox-$GITHUB_REF_NAME-foss-armeabi-v7a.apk"
|
||||
mv "app/build/outputs/apk/foss/release/image-toolbox-$GITHUB_REF_NAME-foss-x86_64-release-unsigned-signed.apk" "image-toolbox-$GITHUB_REF_NAME-foss-x86_64.apk"
|
||||
|
||||
- uses: iota9star/sign-android-release@v1.0.5
|
||||
name: Sign Market APK
|
||||
# ID used to access action output
|
||||
id: sign_app_market
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/market/release
|
||||
fileRex: .*apk
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASS }}
|
||||
keyPassword: ${{ secrets.KEY_STORE_PASS }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
|
||||
- name: Rename market file stage
|
||||
env:
|
||||
VERSION_NAME: ${{ env.GITHUB_REF_NAME }}
|
||||
run: |
|
||||
ls -la app/build/outputs/apk/market/release
|
||||
mv "app/build/outputs/apk/market/release/image-toolbox-$VERSION_NAME-market-arm64-v8a-release-unsigned-signed.apk" "image-toolbox-$VERSION_NAME-arm64-v8a.apk"
|
||||
mv "app/build/outputs/apk/market/release/image-toolbox-$VERSION_NAME-market-universal-release-unsigned-signed.apk" "image-toolbox-$VERSION_NAME-universal.apk"
|
||||
mv "app/build/outputs/apk/market/release/image-toolbox-$VERSION_NAME-market-armeabi-v7a-release-unsigned-signed.apk" "image-toolbox-$VERSION_NAME-armeabi-v7a.apk"
|
||||
mv "app/build/outputs/apk/market/release/image-toolbox-$VERSION_NAME-market-x86_64-release-unsigned-signed.apk" "image-toolbox-$VERSION_NAME-x86_64.apk"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
id: signed-market-apk
|
||||
with:
|
||||
name: Signed apks Market
|
||||
path: "*.apk"
|
||||
|
||||
create_release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
needs:
|
||||
- build_and_release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
- name: Display all downloaded files
|
||||
run: ls -la
|
||||
- name: Set version variable
|
||||
run: echo "GITHUB_REF_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
|
||||
- name: Set Pre-release flag
|
||||
run: |
|
||||
if [[ "$GITHUB_REF_NAME" == *-* ]]; then
|
||||
# If GITHUB_REF_NAME contains a hyphen, set GITHUB_OTHER_ENV to true
|
||||
echo "PRE_RELEASE_FLAG=true" >> $GITHUB_ENV
|
||||
else
|
||||
# If GITHUB_REF_NAME does not contain a hyphen, set GITHUB_OTHER_ENV to false
|
||||
echo "PRE_RELEASE_FLAG=false" >> $GITHUB_ENV
|
||||
fi
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "*.apk"
|
||||
prerelease: ${{ env.PRE_RELEASE_FLAG }}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/app/release
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
/.idea
|
||||
/libs/nQuant/build
|
||||
/.kotlin/errors/
|
||||
/.kotlin
|
||||
/scripts/
|
||||
+572
@@ -0,0 +1,572 @@
|
||||
# 📐 Modules Graph
|
||||
|
||||
```mermaid
|
||||
%%{
|
||||
init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"mainBkg": "#121418",
|
||||
"primaryColor": "#1b3a1b",
|
||||
"primaryTextColor": "#e0e3de",
|
||||
"primaryBorderColor": "#76c893",
|
||||
"nodeBorder": "#76c893",
|
||||
"lineColor": "#76c893",
|
||||
"secondaryColor": "#1f2721",
|
||||
"tertiaryColor": "#232c26",
|
||||
"clusterBkg": "#182018",
|
||||
"clusterBorder": "#4caf50",
|
||||
"nodeTextColor": "#e0e3de",
|
||||
"edgeLabelBackground": "#111316",
|
||||
"edgeLabelColor": "#cfe9de",
|
||||
"fontSize": "28px",
|
||||
"fontFamily": "JetBrains Mono, Inter, system-ui"
|
||||
}
|
||||
}
|
||||
}%%
|
||||
|
||||
graph LR
|
||||
subgraph :core
|
||||
:core:data("data")
|
||||
:core:ui("ui")
|
||||
:core:domain("domain")
|
||||
:core:resources("resources")
|
||||
:core:settings("settings")
|
||||
:core:di("di")
|
||||
:core:crash("crash")
|
||||
:core:utils("utils")
|
||||
:core:filters("filters")
|
||||
:core:ksp("ksp")
|
||||
end
|
||||
subgraph :feature
|
||||
:feature:root("root")
|
||||
:feature:main("main")
|
||||
:feature:load-net-image("load-net-image")
|
||||
:feature:crop("crop")
|
||||
:feature:limits-resize("limits-resize")
|
||||
:feature:cipher("cipher")
|
||||
:feature:image-preview("image-preview")
|
||||
:feature:weight-resize("weight-resize")
|
||||
:feature:compare("compare")
|
||||
:feature:delete-exif("delete-exif")
|
||||
:feature:palette-tools("palette-tools")
|
||||
:feature:resize-convert("resize-convert")
|
||||
:feature:pdf-tools("pdf-tools")
|
||||
:feature:single-edit("single-edit")
|
||||
:feature:erase-background("erase-background")
|
||||
:feature:draw("draw")
|
||||
:feature:filters("filters")
|
||||
:feature:image-stitch("image-stitch")
|
||||
:feature:pick-color("pick-color")
|
||||
:feature:recognize-text("recognize-text")
|
||||
:feature:gradient-maker("gradient-maker")
|
||||
:feature:watermarking("watermarking")
|
||||
:feature:gif-tools("gif-tools")
|
||||
:feature:apng-tools("apng-tools")
|
||||
:feature:zip("zip")
|
||||
:feature:jxl-tools("jxl-tools")
|
||||
:feature:settings("settings")
|
||||
:feature:easter-egg("easter-egg")
|
||||
:feature:svg-maker("svg-maker")
|
||||
:feature:format-conversion("format-conversion")
|
||||
:feature:document-scanner("document-scanner")
|
||||
:feature:scan-qr-code("scan-qr-code")
|
||||
:feature:image-stacking("image-stacking")
|
||||
:feature:image-splitting("image-splitting")
|
||||
:feature:color-tools("color-tools")
|
||||
:feature:webp-tools("webp-tools")
|
||||
:feature:noise-generation("noise-generation")
|
||||
:feature:collage-maker("collage-maker")
|
||||
:feature:libraries-info("libraries-info")
|
||||
:feature:markup-layers("markup-layers")
|
||||
:feature:base64-tools("base64-tools")
|
||||
:feature:checksum-tools("checksum-tools")
|
||||
:feature:mesh-gradients("mesh-gradients")
|
||||
:feature:edit-exif("edit-exif")
|
||||
:feature:image-cutting("image-cutting")
|
||||
:feature:audio-cover-extractor("audio-cover-extractor")
|
||||
:feature:library-details("library-details")
|
||||
:feature:wallpapers-export("wallpapers-export")
|
||||
:feature:ascii-art("ascii-art")
|
||||
:feature:ai-tools("ai-tools")
|
||||
:feature:media-picker("media-picker")
|
||||
:feature:quick-tiles("quick-tiles")
|
||||
end
|
||||
:feature:root --> :core:data
|
||||
:feature:root --> :core:ui
|
||||
:feature:root --> :core:domain
|
||||
:feature:root --> :core:resources
|
||||
:feature:root --> :core:settings
|
||||
:feature:root --> :core:di
|
||||
:feature:root --> :core:crash
|
||||
:feature:root --> :feature:main
|
||||
:feature:root --> :feature:load-net-image
|
||||
:feature:root --> :feature:crop
|
||||
:feature:root --> :feature:limits-resize
|
||||
:feature:root --> :feature:cipher
|
||||
:feature:root --> :feature:image-preview
|
||||
:feature:root --> :feature:weight-resize
|
||||
:feature:root --> :feature:compare
|
||||
:feature:root --> :feature:delete-exif
|
||||
:feature:root --> :feature:palette-tools
|
||||
:feature:root --> :feature:resize-convert
|
||||
:feature:root --> :feature:pdf-tools
|
||||
:feature:root --> :feature:single-edit
|
||||
:feature:root --> :feature:erase-background
|
||||
:feature:root --> :feature:draw
|
||||
:feature:root --> :feature:filters
|
||||
:feature:root --> :feature:image-stitch
|
||||
:feature:root --> :feature:pick-color
|
||||
:feature:root --> :feature:recognize-text
|
||||
:feature:root --> :feature:gradient-maker
|
||||
:feature:root --> :feature:watermarking
|
||||
:feature:root --> :feature:gif-tools
|
||||
:feature:root --> :feature:apng-tools
|
||||
:feature:root --> :feature:zip
|
||||
:feature:root --> :feature:jxl-tools
|
||||
:feature:root --> :feature:settings
|
||||
:feature:root --> :feature:easter-egg
|
||||
:feature:root --> :feature:svg-maker
|
||||
:feature:root --> :feature:format-conversion
|
||||
:feature:root --> :feature:document-scanner
|
||||
:feature:root --> :feature:scan-qr-code
|
||||
:feature:root --> :feature:image-stacking
|
||||
:feature:root --> :feature:image-splitting
|
||||
:feature:root --> :feature:color-tools
|
||||
:feature:root --> :feature:webp-tools
|
||||
:feature:root --> :feature:noise-generation
|
||||
:feature:root --> :feature:collage-maker
|
||||
:feature:root --> :feature:libraries-info
|
||||
:feature:root --> :feature:markup-layers
|
||||
:feature:root --> :feature:base64-tools
|
||||
:feature:root --> :feature:checksum-tools
|
||||
:feature:root --> :feature:mesh-gradients
|
||||
:feature:root --> :feature:edit-exif
|
||||
:feature:root --> :feature:image-cutting
|
||||
:feature:root --> :feature:audio-cover-extractor
|
||||
:feature:root --> :feature:library-details
|
||||
:feature:root --> :feature:wallpapers-export
|
||||
:feature:root --> :feature:ascii-art
|
||||
:feature:root --> :feature:ai-tools
|
||||
:feature:erase-background --> :core:data
|
||||
:feature:erase-background --> :core:ui
|
||||
:feature:erase-background --> :core:domain
|
||||
:feature:erase-background --> :core:resources
|
||||
:feature:erase-background --> :core:settings
|
||||
:feature:erase-background --> :core:di
|
||||
:feature:erase-background --> :core:crash
|
||||
:feature:erase-background --> :feature:draw
|
||||
:feature:edit-exif --> :core:data
|
||||
:feature:edit-exif --> :core:ui
|
||||
:feature:edit-exif --> :core:domain
|
||||
:feature:edit-exif --> :core:resources
|
||||
:feature:edit-exif --> :core:settings
|
||||
:feature:edit-exif --> :core:di
|
||||
:feature:edit-exif --> :core:crash
|
||||
:feature:limits-resize --> :core:data
|
||||
:feature:limits-resize --> :core:ui
|
||||
:feature:limits-resize --> :core:domain
|
||||
:feature:limits-resize --> :core:resources
|
||||
:feature:limits-resize --> :core:settings
|
||||
:feature:limits-resize --> :core:di
|
||||
:feature:limits-resize --> :core:crash
|
||||
:feature:jxl-tools --> :core:data
|
||||
:feature:jxl-tools --> :core:ui
|
||||
:feature:jxl-tools --> :core:domain
|
||||
:feature:jxl-tools --> :core:resources
|
||||
:feature:jxl-tools --> :core:settings
|
||||
:feature:jxl-tools --> :core:di
|
||||
:feature:jxl-tools --> :core:crash
|
||||
:feature:libraries-info --> :core:data
|
||||
:feature:libraries-info --> :core:ui
|
||||
:feature:libraries-info --> :core:domain
|
||||
:feature:libraries-info --> :core:resources
|
||||
:feature:libraries-info --> :core:settings
|
||||
:feature:libraries-info --> :core:di
|
||||
:feature:libraries-info --> :core:crash
|
||||
:feature:gif-tools --> :core:data
|
||||
:feature:gif-tools --> :core:ui
|
||||
:feature:gif-tools --> :core:domain
|
||||
:feature:gif-tools --> :core:resources
|
||||
:feature:gif-tools --> :core:settings
|
||||
:feature:gif-tools --> :core:di
|
||||
:feature:gif-tools --> :core:crash
|
||||
:feature:library-details --> :core:data
|
||||
:feature:library-details --> :core:ui
|
||||
:feature:library-details --> :core:domain
|
||||
:feature:library-details --> :core:resources
|
||||
:feature:library-details --> :core:settings
|
||||
:feature:library-details --> :core:di
|
||||
:feature:library-details --> :core:crash
|
||||
:feature:pdf-tools --> :core:data
|
||||
:feature:pdf-tools --> :core:ui
|
||||
:feature:pdf-tools --> :core:domain
|
||||
:feature:pdf-tools --> :core:resources
|
||||
:feature:pdf-tools --> :core:settings
|
||||
:feature:pdf-tools --> :core:di
|
||||
:feature:pdf-tools --> :core:crash
|
||||
:feature:watermarking --> :core:data
|
||||
:feature:watermarking --> :core:ui
|
||||
:feature:watermarking --> :core:domain
|
||||
:feature:watermarking --> :core:resources
|
||||
:feature:watermarking --> :core:settings
|
||||
:feature:watermarking --> :core:di
|
||||
:feature:watermarking --> :core:crash
|
||||
:feature:watermarking --> :feature:compare
|
||||
:app --> :core:data
|
||||
:app --> :core:ui
|
||||
:app --> :core:domain
|
||||
:app --> :core:resources
|
||||
:app --> :core:settings
|
||||
:app --> :core:di
|
||||
:app --> :core:crash
|
||||
:app --> :core:utils
|
||||
:app --> :feature:root
|
||||
:app --> :feature:media-picker
|
||||
:app --> :feature:quick-tiles
|
||||
:feature:resize-convert --> :core:data
|
||||
:feature:resize-convert --> :core:ui
|
||||
:feature:resize-convert --> :core:domain
|
||||
:feature:resize-convert --> :core:resources
|
||||
:feature:resize-convert --> :core:settings
|
||||
:feature:resize-convert --> :core:di
|
||||
:feature:resize-convert --> :core:crash
|
||||
:feature:resize-convert --> :feature:compare
|
||||
:feature:easter-egg --> :core:data
|
||||
:feature:easter-egg --> :core:ui
|
||||
:feature:easter-egg --> :core:domain
|
||||
:feature:easter-egg --> :core:resources
|
||||
:feature:easter-egg --> :core:settings
|
||||
:feature:easter-egg --> :core:di
|
||||
:feature:easter-egg --> :core:crash
|
||||
:feature:webp-tools --> :core:data
|
||||
:feature:webp-tools --> :core:ui
|
||||
:feature:webp-tools --> :core:domain
|
||||
:feature:webp-tools --> :core:resources
|
||||
:feature:webp-tools --> :core:settings
|
||||
:feature:webp-tools --> :core:di
|
||||
:feature:webp-tools --> :core:crash
|
||||
:feature:markup-layers --> :core:data
|
||||
:feature:markup-layers --> :core:ui
|
||||
:feature:markup-layers --> :core:domain
|
||||
:feature:markup-layers --> :core:resources
|
||||
:feature:markup-layers --> :core:settings
|
||||
:feature:markup-layers --> :core:di
|
||||
:feature:markup-layers --> :core:crash
|
||||
:feature:media-picker --> :core:data
|
||||
:feature:media-picker --> :core:ui
|
||||
:feature:media-picker --> :core:domain
|
||||
:feature:media-picker --> :core:resources
|
||||
:feature:media-picker --> :core:settings
|
||||
:feature:media-picker --> :core:di
|
||||
:feature:media-picker --> :core:crash
|
||||
:feature:image-stitch --> :core:data
|
||||
:feature:image-stitch --> :core:ui
|
||||
:feature:image-stitch --> :core:domain
|
||||
:feature:image-stitch --> :core:resources
|
||||
:feature:image-stitch --> :core:settings
|
||||
:feature:image-stitch --> :core:di
|
||||
:feature:image-stitch --> :core:crash
|
||||
:feature:image-stitch --> :core:filters
|
||||
:core:ui --> :core:resources
|
||||
:core:ui --> :core:domain
|
||||
:core:ui --> :core:utils
|
||||
:core:ui --> :core:di
|
||||
:core:ui --> :core:settings
|
||||
:feature:noise-generation --> :core:data
|
||||
:feature:noise-generation --> :core:ui
|
||||
:feature:noise-generation --> :core:domain
|
||||
:feature:noise-generation --> :core:resources
|
||||
:feature:noise-generation --> :core:settings
|
||||
:feature:noise-generation --> :core:di
|
||||
:feature:noise-generation --> :core:crash
|
||||
:feature:wallpapers-export --> :core:data
|
||||
:feature:wallpapers-export --> :core:ui
|
||||
:feature:wallpapers-export --> :core:domain
|
||||
:feature:wallpapers-export --> :core:resources
|
||||
:feature:wallpapers-export --> :core:settings
|
||||
:feature:wallpapers-export --> :core:di
|
||||
:feature:wallpapers-export --> :core:crash
|
||||
:feature:document-scanner --> :core:data
|
||||
:feature:document-scanner --> :core:ui
|
||||
:feature:document-scanner --> :core:domain
|
||||
:feature:document-scanner --> :core:resources
|
||||
:feature:document-scanner --> :core:settings
|
||||
:feature:document-scanner --> :core:di
|
||||
:feature:document-scanner --> :core:crash
|
||||
:feature:document-scanner --> :feature:pdf-tools
|
||||
:feature:gradient-maker --> :core:data
|
||||
:feature:gradient-maker --> :core:ui
|
||||
:feature:gradient-maker --> :core:domain
|
||||
:feature:gradient-maker --> :core:resources
|
||||
:feature:gradient-maker --> :core:settings
|
||||
:feature:gradient-maker --> :core:di
|
||||
:feature:gradient-maker --> :core:crash
|
||||
:feature:gradient-maker --> :feature:compare
|
||||
:feature:zip --> :core:data
|
||||
:feature:zip --> :core:ui
|
||||
:feature:zip --> :core:domain
|
||||
:feature:zip --> :core:resources
|
||||
:feature:zip --> :core:settings
|
||||
:feature:zip --> :core:di
|
||||
:feature:zip --> :core:crash
|
||||
:core:utils --> :core:domain
|
||||
:core:utils --> :core:resources
|
||||
:core:utils --> :core:settings
|
||||
:feature:cipher --> :core:data
|
||||
:feature:cipher --> :core:ui
|
||||
:feature:cipher --> :core:domain
|
||||
:feature:cipher --> :core:resources
|
||||
:feature:cipher --> :core:settings
|
||||
:feature:cipher --> :core:di
|
||||
:feature:cipher --> :core:crash
|
||||
:feature:draw --> :core:data
|
||||
:feature:draw --> :core:ui
|
||||
:feature:draw --> :core:domain
|
||||
:feature:draw --> :core:resources
|
||||
:feature:draw --> :core:settings
|
||||
:feature:draw --> :core:di
|
||||
:feature:draw --> :core:crash
|
||||
:feature:draw --> :core:filters
|
||||
:feature:draw --> :feature:pick-color
|
||||
:feature:ai-tools --> :core:data
|
||||
:feature:ai-tools --> :core:ui
|
||||
:feature:ai-tools --> :core:domain
|
||||
:feature:ai-tools --> :core:resources
|
||||
:feature:ai-tools --> :core:settings
|
||||
:feature:ai-tools --> :core:di
|
||||
:feature:ai-tools --> :core:crash
|
||||
:feature:audio-cover-extractor --> :core:data
|
||||
:feature:audio-cover-extractor --> :core:ui
|
||||
:feature:audio-cover-extractor --> :core:domain
|
||||
:feature:audio-cover-extractor --> :core:resources
|
||||
:feature:audio-cover-extractor --> :core:settings
|
||||
:feature:audio-cover-extractor --> :core:di
|
||||
:feature:audio-cover-extractor --> :core:crash
|
||||
:core:crash --> :core:ui
|
||||
:core:crash --> :core:settings
|
||||
:feature:delete-exif --> :core:data
|
||||
:feature:delete-exif --> :core:ui
|
||||
:feature:delete-exif --> :core:domain
|
||||
:feature:delete-exif --> :core:resources
|
||||
:feature:delete-exif --> :core:settings
|
||||
:feature:delete-exif --> :core:di
|
||||
:feature:delete-exif --> :core:crash
|
||||
:feature:collage-maker --> :core:data
|
||||
:feature:collage-maker --> :core:ui
|
||||
:feature:collage-maker --> :core:domain
|
||||
:feature:collage-maker --> :core:resources
|
||||
:feature:collage-maker --> :core:settings
|
||||
:feature:collage-maker --> :core:di
|
||||
:feature:collage-maker --> :core:crash
|
||||
:feature:compare --> :core:data
|
||||
:feature:compare --> :core:ui
|
||||
:feature:compare --> :core:domain
|
||||
:feature:compare --> :core:resources
|
||||
:feature:compare --> :core:settings
|
||||
:feature:compare --> :core:di
|
||||
:feature:compare --> :core:crash
|
||||
:feature:mesh-gradients --> :core:data
|
||||
:feature:mesh-gradients --> :core:ui
|
||||
:feature:mesh-gradients --> :core:domain
|
||||
:feature:mesh-gradients --> :core:resources
|
||||
:feature:mesh-gradients --> :core:settings
|
||||
:feature:mesh-gradients --> :core:di
|
||||
:feature:mesh-gradients --> :core:crash
|
||||
:core:settings --> :core:domain
|
||||
:core:settings --> :core:resources
|
||||
:core:settings --> :core:di
|
||||
:feature:scan-qr-code --> :core:data
|
||||
:feature:scan-qr-code --> :core:ui
|
||||
:feature:scan-qr-code --> :core:domain
|
||||
:feature:scan-qr-code --> :core:resources
|
||||
:feature:scan-qr-code --> :core:settings
|
||||
:feature:scan-qr-code --> :core:di
|
||||
:feature:scan-qr-code --> :core:crash
|
||||
:feature:scan-qr-code --> :core:filters
|
||||
:feature:svg-maker --> :core:data
|
||||
:feature:svg-maker --> :core:ui
|
||||
:feature:svg-maker --> :core:domain
|
||||
:feature:svg-maker --> :core:resources
|
||||
:feature:svg-maker --> :core:settings
|
||||
:feature:svg-maker --> :core:di
|
||||
:feature:svg-maker --> :core:crash
|
||||
:feature:weight-resize --> :core:data
|
||||
:feature:weight-resize --> :core:ui
|
||||
:feature:weight-resize --> :core:domain
|
||||
:feature:weight-resize --> :core:resources
|
||||
:feature:weight-resize --> :core:settings
|
||||
:feature:weight-resize --> :core:di
|
||||
:feature:weight-resize --> :core:crash
|
||||
:feature:image-splitting --> :core:data
|
||||
:feature:image-splitting --> :core:ui
|
||||
:feature:image-splitting --> :core:domain
|
||||
:feature:image-splitting --> :core:resources
|
||||
:feature:image-splitting --> :core:settings
|
||||
:feature:image-splitting --> :core:di
|
||||
:feature:image-splitting --> :core:crash
|
||||
:benchmark --> :app
|
||||
:feature:checksum-tools --> :core:data
|
||||
:feature:checksum-tools --> :core:ui
|
||||
:feature:checksum-tools --> :core:domain
|
||||
:feature:checksum-tools --> :core:resources
|
||||
:feature:checksum-tools --> :core:settings
|
||||
:feature:checksum-tools --> :core:di
|
||||
:feature:checksum-tools --> :core:crash
|
||||
:feature:base64-tools --> :core:data
|
||||
:feature:base64-tools --> :core:ui
|
||||
:feature:base64-tools --> :core:domain
|
||||
:feature:base64-tools --> :core:resources
|
||||
:feature:base64-tools --> :core:settings
|
||||
:feature:base64-tools --> :core:di
|
||||
:feature:base64-tools --> :core:crash
|
||||
:feature:palette-tools --> :core:data
|
||||
:feature:palette-tools --> :core:ui
|
||||
:feature:palette-tools --> :core:domain
|
||||
:feature:palette-tools --> :core:resources
|
||||
:feature:palette-tools --> :core:settings
|
||||
:feature:palette-tools --> :core:di
|
||||
:feature:palette-tools --> :core:crash
|
||||
:feature:palette-tools --> :feature:pick-color
|
||||
:feature:settings --> :core:data
|
||||
:feature:settings --> :core:ui
|
||||
:feature:settings --> :core:domain
|
||||
:feature:settings --> :core:resources
|
||||
:feature:settings --> :core:settings
|
||||
:feature:settings --> :core:di
|
||||
:feature:settings --> :core:crash
|
||||
:core:data --> :core:utils
|
||||
:core:data --> :core:domain
|
||||
:core:data --> :core:resources
|
||||
:core:data --> :core:filters
|
||||
:core:data --> :core:settings
|
||||
:core:data --> :core:di
|
||||
:feature:pick-color --> :core:data
|
||||
:feature:pick-color --> :core:ui
|
||||
:feature:pick-color --> :core:domain
|
||||
:feature:pick-color --> :core:resources
|
||||
:feature:pick-color --> :core:settings
|
||||
:feature:pick-color --> :core:di
|
||||
:feature:pick-color --> :core:crash
|
||||
:feature:load-net-image --> :core:data
|
||||
:feature:load-net-image --> :core:ui
|
||||
:feature:load-net-image --> :core:domain
|
||||
:feature:load-net-image --> :core:resources
|
||||
:feature:load-net-image --> :core:settings
|
||||
:feature:load-net-image --> :core:di
|
||||
:feature:load-net-image --> :core:crash
|
||||
:feature:quick-tiles --> :core:data
|
||||
:feature:quick-tiles --> :core:ui
|
||||
:feature:quick-tiles --> :core:domain
|
||||
:feature:quick-tiles --> :core:resources
|
||||
:feature:quick-tiles --> :core:settings
|
||||
:feature:quick-tiles --> :core:di
|
||||
:feature:quick-tiles --> :core:crash
|
||||
:feature:quick-tiles --> :feature:erase-background
|
||||
:feature:recognize-text --> :core:data
|
||||
:feature:recognize-text --> :core:ui
|
||||
:feature:recognize-text --> :core:domain
|
||||
:feature:recognize-text --> :core:resources
|
||||
:feature:recognize-text --> :core:settings
|
||||
:feature:recognize-text --> :core:di
|
||||
:feature:recognize-text --> :core:crash
|
||||
:feature:recognize-text --> :core:filters
|
||||
:feature:recognize-text --> :feature:single-edit
|
||||
:core:domain --> :core:resources
|
||||
:feature:single-edit --> :core:data
|
||||
:feature:single-edit --> :core:ui
|
||||
:feature:single-edit --> :core:domain
|
||||
:feature:single-edit --> :core:resources
|
||||
:feature:single-edit --> :core:settings
|
||||
:feature:single-edit --> :core:di
|
||||
:feature:single-edit --> :core:crash
|
||||
:feature:single-edit --> :feature:crop
|
||||
:feature:single-edit --> :feature:erase-background
|
||||
:feature:single-edit --> :feature:draw
|
||||
:feature:single-edit --> :feature:filters
|
||||
:feature:single-edit --> :feature:pick-color
|
||||
:feature:single-edit --> :feature:compare
|
||||
:feature:image-cutting --> :core:data
|
||||
:feature:image-cutting --> :core:ui
|
||||
:feature:image-cutting --> :core:domain
|
||||
:feature:image-cutting --> :core:resources
|
||||
:feature:image-cutting --> :core:settings
|
||||
:feature:image-cutting --> :core:di
|
||||
:feature:image-cutting --> :core:crash
|
||||
:feature:image-cutting --> :feature:compare
|
||||
:feature:crop --> :core:data
|
||||
:feature:crop --> :core:ui
|
||||
:feature:crop --> :core:domain
|
||||
:feature:crop --> :core:resources
|
||||
:feature:crop --> :core:settings
|
||||
:feature:crop --> :core:di
|
||||
:feature:crop --> :core:crash
|
||||
:feature:color-tools --> :core:data
|
||||
:feature:color-tools --> :core:ui
|
||||
:feature:color-tools --> :core:domain
|
||||
:feature:color-tools --> :core:resources
|
||||
:feature:color-tools --> :core:settings
|
||||
:feature:color-tools --> :core:di
|
||||
:feature:color-tools --> :core:crash
|
||||
:feature:format-conversion --> :core:data
|
||||
:feature:format-conversion --> :core:ui
|
||||
:feature:format-conversion --> :core:domain
|
||||
:feature:format-conversion --> :core:resources
|
||||
:feature:format-conversion --> :core:settings
|
||||
:feature:format-conversion --> :core:di
|
||||
:feature:format-conversion --> :core:crash
|
||||
:feature:format-conversion --> :feature:compare
|
||||
:feature:ascii-art --> :core:data
|
||||
:feature:ascii-art --> :core:ui
|
||||
:feature:ascii-art --> :core:domain
|
||||
:feature:ascii-art --> :core:resources
|
||||
:feature:ascii-art --> :core:settings
|
||||
:feature:ascii-art --> :core:di
|
||||
:feature:ascii-art --> :core:crash
|
||||
:feature:ascii-art --> :feature:filters
|
||||
:core:filters --> :core:domain
|
||||
:core:filters --> :core:ui
|
||||
:core:filters --> :core:resources
|
||||
:core:filters --> :core:settings
|
||||
:core:filters --> :core:utils
|
||||
:feature:apng-tools --> :core:data
|
||||
:feature:apng-tools --> :core:ui
|
||||
:feature:apng-tools --> :core:domain
|
||||
:feature:apng-tools --> :core:resources
|
||||
:feature:apng-tools --> :core:settings
|
||||
:feature:apng-tools --> :core:di
|
||||
:feature:apng-tools --> :core:crash
|
||||
:feature:image-preview --> :core:data
|
||||
:feature:image-preview --> :core:ui
|
||||
:feature:image-preview --> :core:domain
|
||||
:feature:image-preview --> :core:resources
|
||||
:feature:image-preview --> :core:settings
|
||||
:feature:image-preview --> :core:di
|
||||
:feature:image-preview --> :core:crash
|
||||
:feature:filters --> :core:filters
|
||||
:feature:filters --> :core:data
|
||||
:feature:filters --> :core:ui
|
||||
:feature:filters --> :core:domain
|
||||
:feature:filters --> :core:resources
|
||||
:feature:filters --> :core:settings
|
||||
:feature:filters --> :core:di
|
||||
:feature:filters --> :core:crash
|
||||
:feature:filters --> :core:ksp
|
||||
:feature:filters --> :feature:draw
|
||||
:feature:filters --> :feature:pick-color
|
||||
:feature:filters --> :feature:compare
|
||||
:feature:image-stacking --> :core:data
|
||||
:feature:image-stacking --> :core:ui
|
||||
:feature:image-stacking --> :core:domain
|
||||
:feature:image-stacking --> :core:resources
|
||||
:feature:image-stacking --> :core:settings
|
||||
:feature:image-stacking --> :core:di
|
||||
:feature:image-stacking --> :core:crash
|
||||
:feature:main --> :core:data
|
||||
:feature:main --> :core:ui
|
||||
:feature:main --> :core:domain
|
||||
:feature:main --> :core:resources
|
||||
:feature:main --> :core:settings
|
||||
:feature:main --> :core:di
|
||||
:feature:main --> :core:crash
|
||||
:feature:main --> :feature:settings
|
||||
```
|
||||
+612
@@ -0,0 +1,612 @@
|
||||
%%{
|
||||
init: {
|
||||
"theme": "base",
|
||||
"maxEdges": 1000,
|
||||
"themeVariables": {
|
||||
"mainBkg": "#121418",
|
||||
"primaryColor": "#1b3a1b",
|
||||
"primaryTextColor": "#e0e3de",
|
||||
"primaryBorderColor": "#76c893",
|
||||
"nodeBorder": "#76c893",
|
||||
"lineColor": "#76c893",
|
||||
"secondaryColor": "#1f2721",
|
||||
"tertiaryColor": "#232c26",
|
||||
"clusterBkg": "#182018",
|
||||
"clusterBorder": "#4caf50",
|
||||
"nodeTextColor": "#e0e3de",
|
||||
"edgeLabelBackground": "#111316",
|
||||
"edgeLabelColor": "#cfe9de",
|
||||
"fontSize": "28px",
|
||||
"fontFamily": "JetBrains Mono, Inter, system-ui"
|
||||
}
|
||||
}
|
||||
}%%
|
||||
|
||||
graph LR
|
||||
subgraph :core
|
||||
:core:data("data")
|
||||
:core:ui("ui")
|
||||
:core:domain("domain")
|
||||
:core:resources("resources")
|
||||
:core:settings("settings")
|
||||
:core:di("di")
|
||||
:core:crash("crash")
|
||||
:core:utils("utils")
|
||||
:core:filters("filters")
|
||||
:core:ksp("ksp")
|
||||
end
|
||||
subgraph :feature
|
||||
:feature:root("root")
|
||||
:feature:main("main")
|
||||
:feature:load-net-image("load-net-image")
|
||||
:feature:crop("crop")
|
||||
:feature:limits-resize("limits-resize")
|
||||
:feature:cipher("cipher")
|
||||
:feature:image-preview("image-preview")
|
||||
:feature:weight-resize("weight-resize")
|
||||
:feature:compare("compare")
|
||||
:feature:delete-exif("delete-exif")
|
||||
:feature:palette-tools("palette-tools")
|
||||
:feature:resize-convert("resize-convert")
|
||||
:feature:pdf-tools("pdf-tools")
|
||||
:feature:single-edit("single-edit")
|
||||
:feature:erase-background("erase-background")
|
||||
:feature:draw("draw")
|
||||
:feature:filters("filters")
|
||||
:feature:image-stitch("image-stitch")
|
||||
:feature:pick-color("pick-color")
|
||||
:feature:recognize-text("recognize-text")
|
||||
:feature:gradient-maker("gradient-maker")
|
||||
:feature:watermarking("watermarking")
|
||||
:feature:gif-tools("gif-tools")
|
||||
:feature:apng-tools("apng-tools")
|
||||
:feature:zip("zip")
|
||||
:feature:jxl-tools("jxl-tools")
|
||||
:feature:settings("settings")
|
||||
:feature:easter-egg("easter-egg")
|
||||
:feature:svg-maker("svg-maker")
|
||||
:feature:format-conversion("format-conversion")
|
||||
:feature:document-scanner("document-scanner")
|
||||
:feature:scan-qr-code("scan-qr-code")
|
||||
:feature:image-stacking("image-stacking")
|
||||
:feature:image-splitting("image-splitting")
|
||||
:feature:color-tools("color-tools")
|
||||
:feature:webp-tools("webp-tools")
|
||||
:feature:noise-generation("noise-generation")
|
||||
:feature:collage-maker("collage-maker")
|
||||
:feature:libraries-info("libraries-info")
|
||||
:feature:markup-layers("markup-layers")
|
||||
:feature:base64-tools("base64-tools")
|
||||
:feature:checksum-tools("checksum-tools")
|
||||
:feature:mesh-gradients("mesh-gradients")
|
||||
:feature:edit-exif("edit-exif")
|
||||
:feature:image-cutting("image-cutting")
|
||||
:feature:audio-cover-extractor("audio-cover-extractor")
|
||||
:feature:library-details("library-details")
|
||||
:feature:wallpapers-export("wallpapers-export")
|
||||
:feature:ascii-art("ascii-art")
|
||||
:feature:ai-tools("ai-tools")
|
||||
:feature:color-library("color-library")
|
||||
:feature:media-picker("media-picker")
|
||||
:feature:quick-tiles("quick-tiles")
|
||||
end
|
||||
subgraph :lib
|
||||
:lib:neural-tools("neural-tools")
|
||||
:lib:opencv-tools("opencv-tools")
|
||||
:lib:dynamic-theme("dynamic-theme")
|
||||
:lib:snowfall("snowfall")
|
||||
:lib:documentscanner("documentscanner")
|
||||
:lib:collages("collages")
|
||||
:lib:palette("palette")
|
||||
:lib:curves("curves")
|
||||
:lib:ascii("ascii")
|
||||
end
|
||||
:feature:root --> :core:data
|
||||
:feature:root --> :core:ui
|
||||
:feature:root --> :core:domain
|
||||
:feature:root --> :core:resources
|
||||
:feature:root --> :core:settings
|
||||
:feature:root --> :core:di
|
||||
:feature:root --> :core:crash
|
||||
:feature:root --> :feature:main
|
||||
:feature:root --> :feature:load-net-image
|
||||
:feature:root --> :feature:crop
|
||||
:feature:root --> :feature:limits-resize
|
||||
:feature:root --> :feature:cipher
|
||||
:feature:root --> :feature:image-preview
|
||||
:feature:root --> :feature:weight-resize
|
||||
:feature:root --> :feature:compare
|
||||
:feature:root --> :feature:delete-exif
|
||||
:feature:root --> :feature:palette-tools
|
||||
:feature:root --> :feature:resize-convert
|
||||
:feature:root --> :feature:pdf-tools
|
||||
:feature:root --> :feature:single-edit
|
||||
:feature:root --> :feature:erase-background
|
||||
:feature:root --> :feature:draw
|
||||
:feature:root --> :feature:filters
|
||||
:feature:root --> :feature:image-stitch
|
||||
:feature:root --> :feature:pick-color
|
||||
:feature:root --> :feature:recognize-text
|
||||
:feature:root --> :feature:gradient-maker
|
||||
:feature:root --> :feature:watermarking
|
||||
:feature:root --> :feature:gif-tools
|
||||
:feature:root --> :feature:apng-tools
|
||||
:feature:root --> :feature:zip
|
||||
:feature:root --> :feature:jxl-tools
|
||||
:feature:root --> :feature:settings
|
||||
:feature:root --> :feature:easter-egg
|
||||
:feature:root --> :feature:svg-maker
|
||||
:feature:root --> :feature:format-conversion
|
||||
:feature:root --> :feature:document-scanner
|
||||
:feature:root --> :feature:scan-qr-code
|
||||
:feature:root --> :feature:image-stacking
|
||||
:feature:root --> :feature:image-splitting
|
||||
:feature:root --> :feature:color-tools
|
||||
:feature:root --> :feature:webp-tools
|
||||
:feature:root --> :feature:noise-generation
|
||||
:feature:root --> :feature:collage-maker
|
||||
:feature:root --> :feature:libraries-info
|
||||
:feature:root --> :feature:markup-layers
|
||||
:feature:root --> :feature:base64-tools
|
||||
:feature:root --> :feature:checksum-tools
|
||||
:feature:root --> :feature:mesh-gradients
|
||||
:feature:root --> :feature:edit-exif
|
||||
:feature:root --> :feature:image-cutting
|
||||
:feature:root --> :feature:audio-cover-extractor
|
||||
:feature:root --> :feature:library-details
|
||||
:feature:root --> :feature:wallpapers-export
|
||||
:feature:root --> :feature:ascii-art
|
||||
:feature:root --> :feature:ai-tools
|
||||
:feature:root --> :feature:color-library
|
||||
:feature:erase-background --> :core:data
|
||||
:feature:erase-background --> :core:ui
|
||||
:feature:erase-background --> :core:domain
|
||||
:feature:erase-background --> :core:resources
|
||||
:feature:erase-background --> :core:settings
|
||||
:feature:erase-background --> :core:di
|
||||
:feature:erase-background --> :core:crash
|
||||
:feature:erase-background --> :lib:neural-tools
|
||||
:feature:erase-background --> :feature:draw
|
||||
:feature:edit-exif --> :core:data
|
||||
:feature:edit-exif --> :core:ui
|
||||
:feature:edit-exif --> :core:domain
|
||||
:feature:edit-exif --> :core:resources
|
||||
:feature:edit-exif --> :core:settings
|
||||
:feature:edit-exif --> :core:di
|
||||
:feature:edit-exif --> :core:crash
|
||||
:feature:limits-resize --> :core:data
|
||||
:feature:limits-resize --> :core:ui
|
||||
:feature:limits-resize --> :core:domain
|
||||
:feature:limits-resize --> :core:resources
|
||||
:feature:limits-resize --> :core:settings
|
||||
:feature:limits-resize --> :core:di
|
||||
:feature:limits-resize --> :core:crash
|
||||
:feature:jxl-tools --> :core:data
|
||||
:feature:jxl-tools --> :core:ui
|
||||
:feature:jxl-tools --> :core:domain
|
||||
:feature:jxl-tools --> :core:resources
|
||||
:feature:jxl-tools --> :core:settings
|
||||
:feature:jxl-tools --> :core:di
|
||||
:feature:jxl-tools --> :core:crash
|
||||
:feature:libraries-info --> :core:data
|
||||
:feature:libraries-info --> :core:ui
|
||||
:feature:libraries-info --> :core:domain
|
||||
:feature:libraries-info --> :core:resources
|
||||
:feature:libraries-info --> :core:settings
|
||||
:feature:libraries-info --> :core:di
|
||||
:feature:libraries-info --> :core:crash
|
||||
:feature:gif-tools --> :core:data
|
||||
:feature:gif-tools --> :core:ui
|
||||
:feature:gif-tools --> :core:domain
|
||||
:feature:gif-tools --> :core:resources
|
||||
:feature:gif-tools --> :core:settings
|
||||
:feature:gif-tools --> :core:di
|
||||
:feature:gif-tools --> :core:crash
|
||||
:feature:library-details --> :core:data
|
||||
:feature:library-details --> :core:ui
|
||||
:feature:library-details --> :core:domain
|
||||
:feature:library-details --> :core:resources
|
||||
:feature:library-details --> :core:settings
|
||||
:feature:library-details --> :core:di
|
||||
:feature:library-details --> :core:crash
|
||||
:feature:pdf-tools --> :core:data
|
||||
:feature:pdf-tools --> :core:ui
|
||||
:feature:pdf-tools --> :core:domain
|
||||
:feature:pdf-tools --> :core:resources
|
||||
:feature:pdf-tools --> :core:settings
|
||||
:feature:pdf-tools --> :core:di
|
||||
:feature:pdf-tools --> :core:crash
|
||||
:feature:watermarking --> :core:data
|
||||
:feature:watermarking --> :core:ui
|
||||
:feature:watermarking --> :core:domain
|
||||
:feature:watermarking --> :core:resources
|
||||
:feature:watermarking --> :core:settings
|
||||
:feature:watermarking --> :core:di
|
||||
:feature:watermarking --> :core:crash
|
||||
:feature:watermarking --> :feature:compare
|
||||
:app --> :core:data
|
||||
:app --> :core:ui
|
||||
:app --> :core:domain
|
||||
:app --> :core:resources
|
||||
:app --> :core:settings
|
||||
:app --> :core:di
|
||||
:app --> :core:crash
|
||||
:app --> :core:utils
|
||||
:app --> :feature:root
|
||||
:app --> :feature:media-picker
|
||||
:app --> :feature:quick-tiles
|
||||
:app --> :lib:opencv-tools
|
||||
:app --> :lib:neural-tools
|
||||
:feature:resize-convert --> :core:data
|
||||
:feature:resize-convert --> :core:ui
|
||||
:feature:resize-convert --> :core:domain
|
||||
:feature:resize-convert --> :core:resources
|
||||
:feature:resize-convert --> :core:settings
|
||||
:feature:resize-convert --> :core:di
|
||||
:feature:resize-convert --> :core:crash
|
||||
:feature:resize-convert --> :feature:compare
|
||||
:feature:easter-egg --> :core:data
|
||||
:feature:easter-egg --> :core:ui
|
||||
:feature:easter-egg --> :core:domain
|
||||
:feature:easter-egg --> :core:resources
|
||||
:feature:easter-egg --> :core:settings
|
||||
:feature:easter-egg --> :core:di
|
||||
:feature:easter-egg --> :core:crash
|
||||
:feature:webp-tools --> :core:data
|
||||
:feature:webp-tools --> :core:ui
|
||||
:feature:webp-tools --> :core:domain
|
||||
:feature:webp-tools --> :core:resources
|
||||
:feature:webp-tools --> :core:settings
|
||||
:feature:webp-tools --> :core:di
|
||||
:feature:webp-tools --> :core:crash
|
||||
:feature:markup-layers --> :core:data
|
||||
:feature:markup-layers --> :core:ui
|
||||
:feature:markup-layers --> :core:domain
|
||||
:feature:markup-layers --> :core:resources
|
||||
:feature:markup-layers --> :core:settings
|
||||
:feature:markup-layers --> :core:di
|
||||
:feature:markup-layers --> :core:crash
|
||||
:feature:media-picker --> :core:data
|
||||
:feature:media-picker --> :core:ui
|
||||
:feature:media-picker --> :core:domain
|
||||
:feature:media-picker --> :core:resources
|
||||
:feature:media-picker --> :core:settings
|
||||
:feature:media-picker --> :core:di
|
||||
:feature:media-picker --> :core:crash
|
||||
:feature:image-stitch --> :core:data
|
||||
:feature:image-stitch --> :core:ui
|
||||
:feature:image-stitch --> :core:domain
|
||||
:feature:image-stitch --> :core:resources
|
||||
:feature:image-stitch --> :core:settings
|
||||
:feature:image-stitch --> :core:di
|
||||
:feature:image-stitch --> :core:crash
|
||||
:feature:image-stitch --> :core:filters
|
||||
:feature:image-stitch --> :lib:opencv-tools
|
||||
:core:ui --> :core:resources
|
||||
:core:ui --> :core:domain
|
||||
:core:ui --> :core:utils
|
||||
:core:ui --> :lib:dynamic-theme
|
||||
:core:ui --> :lib:snowfall
|
||||
:core:ui --> :core:di
|
||||
:core:ui --> :core:settings
|
||||
:core:ui --> :lib:documentscanner
|
||||
:feature:color-library --> :core:data
|
||||
:feature:color-library --> :core:ui
|
||||
:feature:color-library --> :core:domain
|
||||
:feature:color-library --> :core:resources
|
||||
:feature:color-library --> :core:settings
|
||||
:feature:color-library --> :core:di
|
||||
:feature:color-library --> :core:crash
|
||||
:feature:noise-generation --> :core:data
|
||||
:feature:noise-generation --> :core:ui
|
||||
:feature:noise-generation --> :core:domain
|
||||
:feature:noise-generation --> :core:resources
|
||||
:feature:noise-generation --> :core:settings
|
||||
:feature:noise-generation --> :core:di
|
||||
:feature:noise-generation --> :core:crash
|
||||
:feature:wallpapers-export --> :core:data
|
||||
:feature:wallpapers-export --> :core:ui
|
||||
:feature:wallpapers-export --> :core:domain
|
||||
:feature:wallpapers-export --> :core:resources
|
||||
:feature:wallpapers-export --> :core:settings
|
||||
:feature:wallpapers-export --> :core:di
|
||||
:feature:wallpapers-export --> :core:crash
|
||||
:feature:document-scanner --> :core:data
|
||||
:feature:document-scanner --> :core:ui
|
||||
:feature:document-scanner --> :core:domain
|
||||
:feature:document-scanner --> :core:resources
|
||||
:feature:document-scanner --> :core:settings
|
||||
:feature:document-scanner --> :core:di
|
||||
:feature:document-scanner --> :core:crash
|
||||
:feature:document-scanner --> :feature:pdf-tools
|
||||
:feature:gradient-maker --> :core:data
|
||||
:feature:gradient-maker --> :core:ui
|
||||
:feature:gradient-maker --> :core:domain
|
||||
:feature:gradient-maker --> :core:resources
|
||||
:feature:gradient-maker --> :core:settings
|
||||
:feature:gradient-maker --> :core:di
|
||||
:feature:gradient-maker --> :core:crash
|
||||
:feature:gradient-maker --> :feature:compare
|
||||
:feature:zip --> :core:data
|
||||
:feature:zip --> :core:ui
|
||||
:feature:zip --> :core:domain
|
||||
:feature:zip --> :core:resources
|
||||
:feature:zip --> :core:settings
|
||||
:feature:zip --> :core:di
|
||||
:feature:zip --> :core:crash
|
||||
:core:utils --> :core:domain
|
||||
:core:utils --> :core:resources
|
||||
:core:utils --> :core:settings
|
||||
:feature:cipher --> :core:data
|
||||
:feature:cipher --> :core:ui
|
||||
:feature:cipher --> :core:domain
|
||||
:feature:cipher --> :core:resources
|
||||
:feature:cipher --> :core:settings
|
||||
:feature:cipher --> :core:di
|
||||
:feature:cipher --> :core:crash
|
||||
:feature:draw --> :core:data
|
||||
:feature:draw --> :core:ui
|
||||
:feature:draw --> :core:domain
|
||||
:feature:draw --> :core:resources
|
||||
:feature:draw --> :core:settings
|
||||
:feature:draw --> :core:di
|
||||
:feature:draw --> :core:crash
|
||||
:feature:draw --> :core:filters
|
||||
:feature:draw --> :feature:pick-color
|
||||
:feature:ai-tools --> :core:data
|
||||
:feature:ai-tools --> :core:ui
|
||||
:feature:ai-tools --> :core:domain
|
||||
:feature:ai-tools --> :core:resources
|
||||
:feature:ai-tools --> :core:settings
|
||||
:feature:ai-tools --> :core:di
|
||||
:feature:ai-tools --> :core:crash
|
||||
:feature:ai-tools --> :lib:neural-tools
|
||||
:feature:audio-cover-extractor --> :core:data
|
||||
:feature:audio-cover-extractor --> :core:ui
|
||||
:feature:audio-cover-extractor --> :core:domain
|
||||
:feature:audio-cover-extractor --> :core:resources
|
||||
:feature:audio-cover-extractor --> :core:settings
|
||||
:feature:audio-cover-extractor --> :core:di
|
||||
:feature:audio-cover-extractor --> :core:crash
|
||||
:core:crash --> :core:ui
|
||||
:core:crash --> :core:settings
|
||||
:lib:documentscanner --> :lib:opencv-tools
|
||||
:feature:delete-exif --> :core:data
|
||||
:feature:delete-exif --> :core:ui
|
||||
:feature:delete-exif --> :core:domain
|
||||
:feature:delete-exif --> :core:resources
|
||||
:feature:delete-exif --> :core:settings
|
||||
:feature:delete-exif --> :core:di
|
||||
:feature:delete-exif --> :core:crash
|
||||
:feature:collage-maker --> :core:data
|
||||
:feature:collage-maker --> :core:ui
|
||||
:feature:collage-maker --> :core:domain
|
||||
:feature:collage-maker --> :core:resources
|
||||
:feature:collage-maker --> :core:settings
|
||||
:feature:collage-maker --> :core:di
|
||||
:feature:collage-maker --> :core:crash
|
||||
:feature:collage-maker --> :lib:collages
|
||||
:feature:compare --> :core:data
|
||||
:feature:compare --> :core:ui
|
||||
:feature:compare --> :core:domain
|
||||
:feature:compare --> :core:resources
|
||||
:feature:compare --> :core:settings
|
||||
:feature:compare --> :core:di
|
||||
:feature:compare --> :core:crash
|
||||
:feature:compare --> :lib:opencv-tools
|
||||
:feature:mesh-gradients --> :core:data
|
||||
:feature:mesh-gradients --> :core:ui
|
||||
:feature:mesh-gradients --> :core:domain
|
||||
:feature:mesh-gradients --> :core:resources
|
||||
:feature:mesh-gradients --> :core:settings
|
||||
:feature:mesh-gradients --> :core:di
|
||||
:feature:mesh-gradients --> :core:crash
|
||||
:core:settings --> :lib:dynamic-theme
|
||||
:core:settings --> :core:domain
|
||||
:core:settings --> :core:resources
|
||||
:core:settings --> :core:di
|
||||
:feature:scan-qr-code --> :core:data
|
||||
:feature:scan-qr-code --> :core:ui
|
||||
:feature:scan-qr-code --> :core:domain
|
||||
:feature:scan-qr-code --> :core:resources
|
||||
:feature:scan-qr-code --> :core:settings
|
||||
:feature:scan-qr-code --> :core:di
|
||||
:feature:scan-qr-code --> :core:crash
|
||||
:feature:scan-qr-code --> :core:filters
|
||||
:feature:svg-maker --> :core:data
|
||||
:feature:svg-maker --> :core:ui
|
||||
:feature:svg-maker --> :core:domain
|
||||
:feature:svg-maker --> :core:resources
|
||||
:feature:svg-maker --> :core:settings
|
||||
:feature:svg-maker --> :core:di
|
||||
:feature:svg-maker --> :core:crash
|
||||
:feature:weight-resize --> :core:data
|
||||
:feature:weight-resize --> :core:ui
|
||||
:feature:weight-resize --> :core:domain
|
||||
:feature:weight-resize --> :core:resources
|
||||
:feature:weight-resize --> :core:settings
|
||||
:feature:weight-resize --> :core:di
|
||||
:feature:weight-resize --> :core:crash
|
||||
:feature:image-splitting --> :core:data
|
||||
:feature:image-splitting --> :core:ui
|
||||
:feature:image-splitting --> :core:domain
|
||||
:feature:image-splitting --> :core:resources
|
||||
:feature:image-splitting --> :core:settings
|
||||
:feature:image-splitting --> :core:di
|
||||
:feature:image-splitting --> :core:crash
|
||||
:benchmark --> :app
|
||||
:feature:checksum-tools --> :core:data
|
||||
:feature:checksum-tools --> :core:ui
|
||||
:feature:checksum-tools --> :core:domain
|
||||
:feature:checksum-tools --> :core:resources
|
||||
:feature:checksum-tools --> :core:settings
|
||||
:feature:checksum-tools --> :core:di
|
||||
:feature:checksum-tools --> :core:crash
|
||||
:feature:base64-tools --> :core:data
|
||||
:feature:base64-tools --> :core:ui
|
||||
:feature:base64-tools --> :core:domain
|
||||
:feature:base64-tools --> :core:resources
|
||||
:feature:base64-tools --> :core:settings
|
||||
:feature:base64-tools --> :core:di
|
||||
:feature:base64-tools --> :core:crash
|
||||
:feature:palette-tools --> :core:data
|
||||
:feature:palette-tools --> :core:ui
|
||||
:feature:palette-tools --> :core:domain
|
||||
:feature:palette-tools --> :core:resources
|
||||
:feature:palette-tools --> :core:settings
|
||||
:feature:palette-tools --> :core:di
|
||||
:feature:palette-tools --> :core:crash
|
||||
:feature:palette-tools --> :feature:pick-color
|
||||
:feature:palette-tools --> :lib:palette
|
||||
:feature:settings --> :core:data
|
||||
:feature:settings --> :core:ui
|
||||
:feature:settings --> :core:domain
|
||||
:feature:settings --> :core:resources
|
||||
:feature:settings --> :core:settings
|
||||
:feature:settings --> :core:di
|
||||
:feature:settings --> :core:crash
|
||||
:core:data --> :core:utils
|
||||
:core:data --> :core:domain
|
||||
:core:data --> :core:resources
|
||||
:core:data --> :core:filters
|
||||
:core:data --> :core:settings
|
||||
:core:data --> :core:di
|
||||
:feature:pick-color --> :core:data
|
||||
:feature:pick-color --> :core:ui
|
||||
:feature:pick-color --> :core:domain
|
||||
:feature:pick-color --> :core:resources
|
||||
:feature:pick-color --> :core:settings
|
||||
:feature:pick-color --> :core:di
|
||||
:feature:pick-color --> :core:crash
|
||||
:feature:load-net-image --> :core:data
|
||||
:feature:load-net-image --> :core:ui
|
||||
:feature:load-net-image --> :core:domain
|
||||
:feature:load-net-image --> :core:resources
|
||||
:feature:load-net-image --> :core:settings
|
||||
:feature:load-net-image --> :core:di
|
||||
:feature:load-net-image --> :core:crash
|
||||
:feature:quick-tiles --> :core:data
|
||||
:feature:quick-tiles --> :core:ui
|
||||
:feature:quick-tiles --> :core:domain
|
||||
:feature:quick-tiles --> :core:resources
|
||||
:feature:quick-tiles --> :core:settings
|
||||
:feature:quick-tiles --> :core:di
|
||||
:feature:quick-tiles --> :core:crash
|
||||
:feature:quick-tiles --> :feature:erase-background
|
||||
:feature:recognize-text --> :core:data
|
||||
:feature:recognize-text --> :core:ui
|
||||
:feature:recognize-text --> :core:domain
|
||||
:feature:recognize-text --> :core:resources
|
||||
:feature:recognize-text --> :core:settings
|
||||
:feature:recognize-text --> :core:di
|
||||
:feature:recognize-text --> :core:crash
|
||||
:feature:recognize-text --> :core:filters
|
||||
:feature:recognize-text --> :feature:single-edit
|
||||
:core:domain --> :core:resources
|
||||
:feature:single-edit --> :core:data
|
||||
:feature:single-edit --> :core:ui
|
||||
:feature:single-edit --> :core:domain
|
||||
:feature:single-edit --> :core:resources
|
||||
:feature:single-edit --> :core:settings
|
||||
:feature:single-edit --> :core:di
|
||||
:feature:single-edit --> :core:crash
|
||||
:feature:single-edit --> :feature:crop
|
||||
:feature:single-edit --> :feature:erase-background
|
||||
:feature:single-edit --> :feature:draw
|
||||
:feature:single-edit --> :feature:filters
|
||||
:feature:single-edit --> :feature:pick-color
|
||||
:feature:single-edit --> :feature:compare
|
||||
:feature:single-edit --> :lib:curves
|
||||
:feature:image-cutting --> :core:data
|
||||
:feature:image-cutting --> :core:ui
|
||||
:feature:image-cutting --> :core:domain
|
||||
:feature:image-cutting --> :core:resources
|
||||
:feature:image-cutting --> :core:settings
|
||||
:feature:image-cutting --> :core:di
|
||||
:feature:image-cutting --> :core:crash
|
||||
:feature:image-cutting --> :feature:compare
|
||||
:feature:crop --> :core:data
|
||||
:feature:crop --> :core:ui
|
||||
:feature:crop --> :core:domain
|
||||
:feature:crop --> :core:resources
|
||||
:feature:crop --> :core:settings
|
||||
:feature:crop --> :core:di
|
||||
:feature:crop --> :core:crash
|
||||
:feature:crop --> :lib:opencv-tools
|
||||
:feature:color-tools --> :core:data
|
||||
:feature:color-tools --> :core:ui
|
||||
:feature:color-tools --> :core:domain
|
||||
:feature:color-tools --> :core:resources
|
||||
:feature:color-tools --> :core:settings
|
||||
:feature:color-tools --> :core:di
|
||||
:feature:color-tools --> :core:crash
|
||||
:feature:format-conversion --> :core:data
|
||||
:feature:format-conversion --> :core:ui
|
||||
:feature:format-conversion --> :core:domain
|
||||
:feature:format-conversion --> :core:resources
|
||||
:feature:format-conversion --> :core:settings
|
||||
:feature:format-conversion --> :core:di
|
||||
:feature:format-conversion --> :core:crash
|
||||
:feature:format-conversion --> :feature:compare
|
||||
:feature:ascii-art --> :core:data
|
||||
:feature:ascii-art --> :core:ui
|
||||
:feature:ascii-art --> :core:domain
|
||||
:feature:ascii-art --> :core:resources
|
||||
:feature:ascii-art --> :core:settings
|
||||
:feature:ascii-art --> :core:di
|
||||
:feature:ascii-art --> :core:crash
|
||||
:feature:ascii-art --> :feature:filters
|
||||
:feature:ascii-art --> :lib:ascii
|
||||
:core:filters --> :core:domain
|
||||
:core:filters --> :core:ui
|
||||
:core:filters --> :core:resources
|
||||
:core:filters --> :core:settings
|
||||
:core:filters --> :core:utils
|
||||
:core:filters --> :lib:curves
|
||||
:core:filters --> :lib:ascii
|
||||
:core:filters --> :lib:neural-tools
|
||||
:feature:apng-tools --> :core:data
|
||||
:feature:apng-tools --> :core:ui
|
||||
:feature:apng-tools --> :core:domain
|
||||
:feature:apng-tools --> :core:resources
|
||||
:feature:apng-tools --> :core:settings
|
||||
:feature:apng-tools --> :core:di
|
||||
:feature:apng-tools --> :core:crash
|
||||
:feature:image-preview --> :core:data
|
||||
:feature:image-preview --> :core:ui
|
||||
:feature:image-preview --> :core:domain
|
||||
:feature:image-preview --> :core:resources
|
||||
:feature:image-preview --> :core:settings
|
||||
:feature:image-preview --> :core:di
|
||||
:feature:image-preview --> :core:crash
|
||||
:feature:filters --> :core:filters
|
||||
:feature:filters --> :core:data
|
||||
:feature:filters --> :core:ui
|
||||
:feature:filters --> :core:domain
|
||||
:feature:filters --> :core:resources
|
||||
:feature:filters --> :core:settings
|
||||
:feature:filters --> :core:di
|
||||
:feature:filters --> :core:crash
|
||||
:feature:filters --> :core:ksp
|
||||
:feature:filters --> :feature:draw
|
||||
:feature:filters --> :feature:pick-color
|
||||
:feature:filters --> :feature:compare
|
||||
:feature:filters --> :lib:opencv-tools
|
||||
:feature:filters --> :lib:neural-tools
|
||||
:feature:filters --> :lib:curves
|
||||
:feature:filters --> :lib:ascii
|
||||
:feature:image-stacking --> :core:data
|
||||
:feature:image-stacking --> :core:ui
|
||||
:feature:image-stacking --> :core:domain
|
||||
:feature:image-stacking --> :core:resources
|
||||
:feature:image-stacking --> :core:settings
|
||||
:feature:image-stacking --> :core:di
|
||||
:feature:image-stacking --> :core:crash
|
||||
:feature:main --> :core:data
|
||||
:feature:main --> :core:ui
|
||||
:feature:main --> :core:domain
|
||||
:feature:main --> :core:resources
|
||||
:feature:main --> :core:settings
|
||||
:feature:main --> :core:di
|
||||
:feature:main --> :core:crash
|
||||
:feature:main --> :feature:settings
|
||||
@@ -0,0 +1,89 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
This documentation contains set of guidelines to help you during the contribution process.
|
||||
|
||||
# Submitting Contributions👨🏻💻
|
||||
Below you will find the process and workflow used to review and merge your changes.
|
||||
## 🌟 : Choose an issue/ Create an issue
|
||||
|
||||
- Look for the existing issue or create your own issue.
|
||||
- Comment on the respective issue you would like to work before creating a Pull Request.
|
||||
- Wait for the issue to be assigned to you after which you can start working on it.
|
||||
|
||||
## 🌟 : Fork the repository
|
||||
|
||||
- Fork this repository "ImageToolbox" by clicking on the "Fork" button. This will create a local copy of this respository on your GitHub profile.
|
||||
|
||||
## 🌟 : Clone the forked repository
|
||||
|
||||
- Once the repository is forked you need to clone it to your local machine.
|
||||
- Click on the "Code" button in the repository page and copy the link provided in the dropdown menu.
|
||||
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<your-username>/<ImageToolbox>
|
||||
```
|
||||
|
||||
- Keep a reference to the original project in `upstream` remote.
|
||||
|
||||
```bash
|
||||
cd <repo-name>
|
||||
git remote add upstream https://github.com/<upstream-owner>/<ImageToolbox>
|
||||
git remote -v # To the check the remotes for this repository
|
||||
```
|
||||
|
||||
- If the project is forked already, update the copy before working.
|
||||
|
||||
```bash
|
||||
git remote update
|
||||
git checkout <branch-name>
|
||||
git rebase upstream/<branch-name>
|
||||
```
|
||||
|
||||
## 🌟 : Create a new branch
|
||||
|
||||
- Always create a new branch and name it accordingly so as to identify the issue you are addressing.
|
||||
|
||||
```bash
|
||||
# It will create a new branch with name branch_name and switch to that branch
|
||||
git checkout -b branch_name
|
||||
```
|
||||
## 🌟 : Work on the issue assigned
|
||||
|
||||
- Work on the issue(s) assigned to you, make the necessary changes in the files/folders needed.
|
||||
- After making the changes add them to the branch you've created.
|
||||
|
||||
```bash
|
||||
# To add all new files to branch Branch_Name
|
||||
git add .
|
||||
|
||||
# To add only a few files to Branch_Name
|
||||
git add <file name>
|
||||
```
|
||||
## 🌟 : Commit the changes
|
||||
|
||||
- Add your commits.
|
||||
- Along with the commit give a descriptive message that reflects your changes.
|
||||
|
||||
```bash
|
||||
git commit -m "message"
|
||||
```
|
||||
- Note : A Pull Request should always have only one commit.
|
||||
|
||||
## 🌟 : Push the changes
|
||||
|
||||
- Push the committed changes in your branch to your remote repository.
|
||||
|
||||
```bash
|
||||
git push origin branch_name
|
||||
```
|
||||
## 🌟 : Create a Pull Request
|
||||
|
||||
- Go to your repository in the browser and click on compare and pull request.
|
||||
- Add a title and description to your pull request that best describes your contribution.
|
||||
- After which the pull request will be reviewed and the maintainer will provide the reviews required for the changes.
|
||||
|
||||
If no changes are needed, this means that your Pull Request has been reviewed and will be merged to the original code base by the maintainer.
|
||||
|
||||
|
||||
Happy Hacking!
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`T8RIN/ImageToolbox`
|
||||
- 原始仓库:https://github.com/T8RIN/ImageToolbox
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,10 @@
|
||||
/build/
|
||||
/release/
|
||||
/foss/
|
||||
/market/
|
||||
/jxl/
|
||||
/build
|
||||
/release
|
||||
/foss
|
||||
/market
|
||||
/jxl
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.image.toolbox.application)
|
||||
alias(libs.plugins.image.toolbox.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
val supportedAbi = arrayOf("armeabi-v7a", "arm64-v8a", "x86_64")
|
||||
|
||||
namespace = "com.t8rin.imagetoolbox"
|
||||
|
||||
defaultConfig {
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
//Maintained for compatibility with old version
|
||||
applicationId = "ru.tech.imageresizershrinker"
|
||||
|
||||
versionCode = libs.versions.versionCode.get().toIntOrNull()
|
||||
versionName = System.getenv("VERSION_NAME") ?: libs.versions.versionName.get()
|
||||
|
||||
ndk {
|
||||
abiFilters.clear()
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters += supportedAbi.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
flavorDimensions += "app"
|
||||
|
||||
productFlavors {
|
||||
create("foss") {
|
||||
dimension = "app"
|
||||
versionNameSuffix = "-foss"
|
||||
extra.set("gmsEnabled", false)
|
||||
}
|
||||
create("market") {
|
||||
dimension = "app"
|
||||
extra.set("gmsEnabled", true)
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_launcher_name", "Image Toolbox DEBUG")
|
||||
resValue("string", "file_provider", "com.t8rin.imagetoolbox.fileprovider.debug")
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
resValue("string", "app_launcher_name", "Image Toolbox")
|
||||
resValue("string", "file_provider", "com.t8rin.imagetoolbox.fileprovider")
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(buildTypes.getByName("release"))
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
// Detect app bundle and conditionally disable split abis
|
||||
// This is needed due to a "Sequence contains more than one matching element" error
|
||||
// present since AGP 8.9.0, for more info see:
|
||||
// https://issuetracker.google.com/issues/402800800
|
||||
|
||||
// AppBundle tasks usually contain "bundle" in their name
|
||||
//noinspection WrongGradleMethod
|
||||
val isBuildingBundle =
|
||||
gradle.startParameter.taskNames.any { it.lowercase().contains("bundle") }
|
||||
|
||||
// Disable split abis when building appBundle
|
||||
isEnable = !isBuildingBundle
|
||||
reset()
|
||||
//noinspection ChromeOsAbiSupport
|
||||
include(*supportedAbi)
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += "Instantiatable"
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
keepDebugSymbols.add("**/*.so")
|
||||
pickFirsts.add("lib/*/libcoder.so")
|
||||
pickFirsts.add("**/libc++_shared.so")
|
||||
pickFirsts.add("**/libdatstore_shared_counter.so")
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
resources {
|
||||
excludes += "META-INF/"
|
||||
excludes += "kotlin/"
|
||||
excludes += "org/"
|
||||
excludes += ".properties"
|
||||
excludes += ".bin"
|
||||
excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
resValues = true
|
||||
}
|
||||
}
|
||||
|
||||
base {
|
||||
archivesName = "image-toolbox-${android.defaultConfig.versionName}"
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
export.excludeFields.addAll("generated")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.feature.root)
|
||||
implementation(projects.feature.mediaPicker)
|
||||
implementation(projects.feature.quickTiles)
|
||||
|
||||
implementation(projects.lib.opencvTools)
|
||||
implementation(projects.lib.neuralTools)
|
||||
implementation(projects.lib.collages)
|
||||
|
||||
implementation(libs.bouncycastle.pkix)
|
||||
implementation(libs.bouncycastle.provider)
|
||||
implementation(libs.pdfbox)
|
||||
|
||||
"marketImplementation"(libs.quickie.bundled)
|
||||
"fossImplementation"(libs.quickie.foss)
|
||||
}
|
||||
|
||||
dependencySubstitution {
|
||||
substitute(
|
||||
dependency = "com.caverock:androidsvg-aar:1.4",
|
||||
using = "com.github.deckerst:androidsvg:cc9d59a88f"
|
||||
)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants(selector().all()) { variantBuilder ->
|
||||
val flavorName = variantBuilder.productFlavors.firstOrNull()?.second.orEmpty()
|
||||
val flavorCap = flavorName.replaceFirstChar(Char::uppercase)
|
||||
|
||||
val gmsEnabled = android.productFlavors
|
||||
.findByName(flavorName)
|
||||
?.extra
|
||||
?.get("gmsEnabled") == true
|
||||
|
||||
tasks.configureEach {
|
||||
val isTargetTask = listOf("GoogleServices", "Crashlytics").any { marker ->
|
||||
name.contains(marker)
|
||||
} && name.contains(flavorCap)
|
||||
|
||||
if (isTargetTask) {
|
||||
enabled = gmsEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Project.dependencySubstitution(action: DependencySubstitutions.() -> Unit) {
|
||||
allprojects {
|
||||
configurations.all {
|
||||
resolutionStrategy.dependencySubstitution(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DependencySubstitutions.substitute(
|
||||
dependency: String,
|
||||
using: String
|
||||
) {
|
||||
substitute(module(dependency)).using(module(using))
|
||||
}
|
||||
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator CREATOR;
|
||||
}
|
||||
|
||||
-keep class * implements com.t8rin.imagetoolbox.core.filters.presentation.model.UiFilter
|
||||
-keepclassmembers class * implements com.t8rin.imagetoolbox.core.filters.presentation.model.UiFilter {
|
||||
<init>(...);
|
||||
}
|
||||
-keep class * implements com.t8rin.imagetoolbox.core.filters.domain.model.Filter
|
||||
-keepclassmembers class * implements com.t8rin.imagetoolbox.core.filters.domain.model.Filter {
|
||||
<init>(...);
|
||||
}
|
||||
-keepclassmembers class com.t8rin.imagetoolbox.core.filters.** {
|
||||
<init>(...);
|
||||
}
|
||||
-keep class com.t8rin.imagetoolbox.core.filters.**
|
||||
-keep class com.t8rin.imagetoolbox.core.filters.*
|
||||
|
||||
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||
-dontwarn org.conscrypt.Conscrypt$Version
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
|
||||
-keep class org.beyka.tiffbitmapfactory.**{ *; }
|
||||
|
||||
-keep class org.bouncycastle.jcajce.provider.** { *; }
|
||||
-keep class org.bouncycastle.jce.provider.** { *; }
|
||||
|
||||
-dontwarn com.google.re2j.Matcher
|
||||
-dontwarn com.google.re2j.Pattern
|
||||
|
||||
-keepclassmembers class * {
|
||||
public java.lang.String name();
|
||||
}
|
||||
|
||||
-keep enum * { *; }
|
||||
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * extends java.lang.Enum {
|
||||
public java.lang.String name();
|
||||
}
|
||||
|
||||
-keep class ai.onnxruntime.** { *; }
|
||||
|
||||
-keep class com.google.firebase.crashlytics.** { *; }
|
||||
-keep class com.google.firebase.analytics.** { *; }
|
||||
-keep class androidx.pdf.** { *; }
|
||||
-keepnames class androidx.pdf.** { *; }
|
||||
|
||||
# Moshi reflective adapters need generic signatures in release.
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class kotlin.Metadata { *; }
|
||||
|
||||
-keep class com.t8rin.imagetoolbox.core.filters.domain.model.shader.** { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.filters.domain.model.shader.**
|
||||
-keep class com.t8rin.imagetoolbox.core.filters.domain.model.shader.*
|
||||
|
||||
-keep class com.t8rin.imagetoolbox.feature.markup_layers.data.project.** { *; }
|
||||
-keep class com.t8rin.imagetoolbox.feature.markup_layers.data.project.**
|
||||
-keep class com.t8rin.imagetoolbox.feature.markup_layers.data.project.*
|
||||
|
||||
# Moshi reflects image export preset models when saving/loading presets.
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ImageExportProfiles { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ImageExportProfile { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.Preset { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.Preset$* { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ResizeType { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ResizeType$* { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.Quality { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.image.model.Quality$* { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.model.IntegerSize { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.data.json.PresetJson { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.data.json.ResizeTypeJson { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.data.json.ImageScaleModeJson { *; }
|
||||
|
||||
# Moshi reflects app history/statistics models for recent tools.
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.history.model.LastUsedTool { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.domain.history.model.AppUsageStatistics { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.data.history.LastUsedTools { *; }
|
||||
-keep class com.t8rin.imagetoolbox.core.data.history.SavedFormatCounters { *; }
|
||||
|
||||
# coil-resvg uses JNA/Uniffi generated names at runtime.
|
||||
-keep class com.hashsequence.coilresvg.** { *; }
|
||||
-keep class com.sun.jna.** { *; }
|
||||
-dontwarn java.awt.Component
|
||||
-dontwarn java.awt.GraphicsEnvironment
|
||||
-dontwarn java.awt.HeadlessException
|
||||
-dontwarn java.awt.Window
|
||||
|
||||
-dontwarn javax.naming.NamingEnumeration
|
||||
-dontwarn javax.naming.NamingException
|
||||
-dontwarn javax.naming.directory.Attribute
|
||||
-dontwarn javax.naming.directory.Attributes
|
||||
-dontwarn javax.naming.directory.DirContext
|
||||
-dontwarn javax.naming.directory.InitialDirContext
|
||||
-dontwarn javax.naming.directory.SearchControls
|
||||
-dontwarn javax.naming.directory.SearchResult
|
||||
|
||||
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
|
||||
static boolean isLinkBufferComposerEnabled return true;
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
~ You should have received a copy of the Apache License
|
||||
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="
|
||||
com.google.mlkit.vision.segmentation.subject,
|
||||
androidx.pdf,
|
||||
androidx.pdf.viewer.fragment,
|
||||
androidx.pdf.document.service,
|
||||
androidx.pdf.core" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission
|
||||
android:name="com.google.android.gms.permission.AD_ID"
|
||||
tools:node="remove" />
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_ADSERVICES_AD_ID"
|
||||
tools:node="remove" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_PHONE_STATE"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.PROJECT_MEDIA" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<application
|
||||
android:name=".app.presentation.components.ImageToolboxApplication"
|
||||
android:allowBackup="true"
|
||||
android:colorMode="wideColorGamut"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_launcher_name"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ImageToolbox.Launcher"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:allowBackup, android:label"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="29" />
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="false" />
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<activity
|
||||
android:name=".app.presentation.AppActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/app_launcher_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="com.android.camera.action.REVIEW" />
|
||||
<action android:name="android.provider.action.REVIEW" />
|
||||
<action android:name="android.provider.action.REVIEW_SECURE" />
|
||||
|
||||
<data
|
||||
android:scheme="content"
|
||||
tools:ignore="AppLinkUrlError" />
|
||||
<data android:scheme="file" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:pathSuffix=".jxl" />
|
||||
<data android:pathSuffix=".qoi" />
|
||||
<data android:pathSuffix=".itp" />
|
||||
<data android:mimeType="application/vnd.shana.informed.formtemplate" />
|
||||
<data android:mimeType="application/x-imagetoolbox-project" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
<data android:mimeType="*/pdf" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ACTION_BUG_REPORT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.EDIT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data
|
||||
android:mimeType="image/*"
|
||||
tools:ignore="AppLinkUrlError" />
|
||||
<data android:mimeType="application/vnd.shana.informed.formtemplate" />
|
||||
<data android:mimeType="application/x-imagetoolbox-project" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:pathSuffix=".jxl" />
|
||||
<data android:pathSuffix=".qoi" />
|
||||
<data android:pathSuffix=".itp" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
<data android:mimeType="*/pdf" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.INSERT_OR_EDIT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data
|
||||
android:mimeType="image/*"
|
||||
tools:ignore="AppLinkUrlError" />
|
||||
<data android:mimeType="application/vnd.shana.informed.formtemplate" />
|
||||
<data android:mimeType="application/x-imagetoolbox-project" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:pathSuffix=".jxl" />
|
||||
<data android:pathSuffix=".qoi" />
|
||||
<data android:pathSuffix=".itp" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
<data android:mimeType="*/pdf" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.INSERT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data
|
||||
android:mimeType="image/*"
|
||||
tools:ignore="AppLinkUrlError" />
|
||||
<data android:mimeType="application/vnd.shana.informed.formtemplate" />
|
||||
<data android:mimeType="application/x-imagetoolbox-project" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:pathSuffix=".jxl" />
|
||||
<data android:pathSuffix=".qoi" />
|
||||
<data android:pathSuffix=".itp" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
<data android:mimeType="*/pdf" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="@string/file_provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<!-- Prompt Google Play services to install the backported photo picker module -->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.google.android.gms.metadata.ModuleDependencies"
|
||||
android:enabled="false"
|
||||
android:exported="false"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="photopicker_activity:0:required"
|
||||
android:value="" />
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.arkivanov.decompose.retainedComponent
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.ComposeActivity
|
||||
import com.t8rin.imagetoolbox.feature.root.presentation.RootContent
|
||||
import com.t8rin.imagetoolbox.feature.root.presentation.screenLogic.RootComponent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppActivity : ComposeActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var rootComponentFactory: RootComponent.Factory
|
||||
|
||||
private val component: RootComponent by lazy {
|
||||
retainedComponent(factory = rootComponentFactory::invoke)
|
||||
}
|
||||
|
||||
override fun handleIntent(intent: Intent) = component.handleDeeplinks(intent)
|
||||
|
||||
@Composable
|
||||
override fun Content() = RootContent(component = component)
|
||||
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components
|
||||
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.attachLogWriter
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.initCollages
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.initColorNames
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.initNeuralTool
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.initOpenCV
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.initPdfBox
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.initQrScanner
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.injectBaseComponent
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.registerSecurityProviders
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.functions.setupFlags
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.utils.isMain
|
||||
import com.t8rin.imagetoolbox.core.crash.presentation.components.applyGlobalExceptionHandler
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.AppScope
|
||||
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.KeepAliveService
|
||||
import com.t8rin.imagetoolbox.core.resources.emoji.Emoji.initEmoji
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.ComposeApplication
|
||||
import com.t8rin.imagetoolbox.core.utils.initAppContext
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import io.ktor.client.HttpClient
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@HiltAndroidApp
|
||||
class ImageToolboxApplication : ComposeApplication() {
|
||||
|
||||
@Inject
|
||||
lateinit var keepAliveService: KeepAliveService
|
||||
|
||||
@Inject
|
||||
lateinit var appScope: AppScope
|
||||
|
||||
@Inject
|
||||
lateinit var httpClient: HttpClient
|
||||
|
||||
@Inject
|
||||
lateinit var analyticsManager: AnalyticsManager
|
||||
|
||||
private var isSetupCompleted: Boolean = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
runSetup()
|
||||
}
|
||||
|
||||
override fun runSetup() {
|
||||
if (isSetupCompleted) return
|
||||
|
||||
if (isMain()) {
|
||||
setupFlags()
|
||||
initAppContext()
|
||||
initEmoji()
|
||||
initOpenCV()
|
||||
initNeuralTool()
|
||||
initColorNames()
|
||||
initQrScanner()
|
||||
attachLogWriter()
|
||||
applyGlobalExceptionHandler()
|
||||
registerSecurityProviders()
|
||||
initPdfBox()
|
||||
injectBaseComponent()
|
||||
initCollages()
|
||||
|
||||
isSetupCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.ImageToolboxApplication
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.DeviceInfo
|
||||
import com.t8rin.imagetoolbox.core.utils.Logger
|
||||
import com.t8rin.imagetoolbox.core.utils.attachLogWriter
|
||||
|
||||
|
||||
internal fun ImageToolboxApplication.attachLogWriter() {
|
||||
Logger.attachLogWriter(
|
||||
context = this@attachLogWriter,
|
||||
fileProvider = getString(R.string.file_provider),
|
||||
logsFilename = "image_toolbox_logs.txt",
|
||||
startupLog = Logger.Log(
|
||||
tag = "Device Info",
|
||||
message = "--${DeviceInfo.get()}--",
|
||||
level = Logger.Level.Info
|
||||
),
|
||||
errorHandler = analyticsManager::sendReport,
|
||||
isSyncCreate = false
|
||||
)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import com.t8rin.collages.public.CollageConstants
|
||||
import com.t8rin.imagetoolbox.core.data.image.utils.static
|
||||
|
||||
fun initCollages() =
|
||||
CollageConstants.requestMapper { static() }
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import com.t8rin.colors.parser.ColorNameParser
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.ImageToolboxApplication
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal fun ImageToolboxApplication.initColorNames() =
|
||||
appScope.launch { ColorNameParser.init(this@initColorNames) }
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.ImageToolboxApplication
|
||||
import com.t8rin.imagetoolbox.core.domain.HF_BASE_URL
|
||||
import com.t8rin.neural_tools.NeuralTool
|
||||
|
||||
internal fun ImageToolboxApplication.initNeuralTool() = NeuralTool.init(
|
||||
context = this,
|
||||
httpClient = httpClient,
|
||||
baseUrl = HF_BASE_URL
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import android.app.Application
|
||||
import com.t8rin.opencv_tools.utils.OpenCV
|
||||
|
||||
internal fun Application.initOpenCV() = OpenCV.init(this)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import android.app.Application
|
||||
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
|
||||
|
||||
internal fun Application.initPdfBox() = PDFBoxResourceLoader.init(this)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.ImageUtils.applyPadding
|
||||
import com.t8rin.opencv_tools.qr_prepare.QrPrepareHelper
|
||||
import io.github.g00fy2.quickie.extensions.QrProcessor
|
||||
|
||||
internal fun initQrScanner() = QrProcessor.setProcessor(::prepareBitmap)
|
||||
|
||||
private fun prepareBitmap(bitmap: Bitmap): Bitmap =
|
||||
QrPrepareHelper.prepareQrForDecode(bitmap.applyPadding(100))
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import com.t8rin.imagetoolbox.app.presentation.components.ImageToolboxApplication
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.BaseComponent
|
||||
|
||||
internal fun ImageToolboxApplication.injectBaseComponent() = BaseComponent.inject(keepAliveService)
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import com.t8rin.imagetoolbox.core.domain.model.CipherType
|
||||
import com.t8rin.imagetoolbox.core.domain.model.HashingType
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.operator.DefaultAlgorithmNameFinder
|
||||
import java.security.Provider
|
||||
import java.security.Security
|
||||
|
||||
internal fun registerSecurityProviders() {
|
||||
initBouncyCastle()
|
||||
|
||||
HashingType.registerSecurityMessageDigests(
|
||||
Security.getAlgorithms("MessageDigest").filterNotNull()
|
||||
)
|
||||
|
||||
val finder = DefaultAlgorithmNameFinder()
|
||||
|
||||
CipherType.registerSecurityCiphers(
|
||||
Security.getAlgorithms("Cipher").filterNotNull().mapNotNull { cipher ->
|
||||
if (CipherType.BROKEN.any { cipher.contains(it, true) }) return@mapNotNull null
|
||||
|
||||
val oid = cipher.removePrefix("OID.")
|
||||
if (oid.all { it.isDigit() || it.isWhitespace() || it == '.' }) {
|
||||
CipherType.getInstance(
|
||||
cipher = cipher,
|
||||
name = finder.getAlgorithmName(
|
||||
ASN1ObjectIdentifier(oid)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
CipherType.getInstance(
|
||||
cipher = cipher
|
||||
)
|
||||
}.also {
|
||||
val extraExclude = it.cipher == "DES"
|
||||
|| it.name == "DES/CBC"
|
||||
|| it.name == "THREEFISH-512"
|
||||
|| it.name == "THREEFISH-1024"
|
||||
|| it.name == "CCM"
|
||||
|
||||
if (extraExclude) return@mapNotNull null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun initBouncyCastle() {
|
||||
if (Security.getProvider(WORKAROUND_NAME) != null) return
|
||||
|
||||
try {
|
||||
logProviders("OLD")
|
||||
|
||||
Security.addProvider(BouncyCastleWorkaroundProvider())
|
||||
|
||||
logProviders("NEW")
|
||||
} catch (e: Throwable) {
|
||||
e.makeLog()
|
||||
"Failed to register BouncyCastleWorkaroundProvider".makeLog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun logProviders(tag: String): Int {
|
||||
val providers = Security.getProviders()
|
||||
providers.forEachIndexed { index, provider ->
|
||||
"$tag [$index]: ${provider.name} - ${provider.info}".makeLog("Providers")
|
||||
}
|
||||
return providers.size
|
||||
}
|
||||
|
||||
private class BouncyCastleWorkaroundProvider(
|
||||
bouncyCastleProvider: Provider = BouncyCastleProvider()
|
||||
) : Provider(
|
||||
WORKAROUND_NAME,
|
||||
bouncyCastleProvider.version,
|
||||
bouncyCastleProvider.info
|
||||
) {
|
||||
init {
|
||||
for ((key, value) in bouncyCastleProvider.entries) {
|
||||
put(key.toString(), value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val WORKAROUND_NAME = "BC_WORKAROUND"
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.functions
|
||||
|
||||
import androidx.compose.foundation.ComposeFoundationFlags
|
||||
import androidx.compose.material3.ComposeMaterial3Flags
|
||||
import androidx.compose.runtime.ComposeRuntimeFlags
|
||||
import androidx.compose.runtime.Composer
|
||||
import androidx.compose.runtime.ExperimentalComposeApi
|
||||
import androidx.compose.runtime.tooling.ComposeStackTraceMode
|
||||
import com.arkivanov.decompose.DecomposeSettings
|
||||
|
||||
@OptIn(ExperimentalComposeApi::class)
|
||||
internal fun setupFlags() {
|
||||
ComposeRuntimeFlags.isLinkBufferComposerEnabled = true
|
||||
ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled = true
|
||||
ComposeMaterial3Flags.isCheckboxStylingFixEnabled = true
|
||||
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.GroupKeys)
|
||||
DecomposeSettings.update { it.copy(duplicateConfigurationsEnabled = true) }
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.app.presentation.components.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.content.Context.ACTIVITY_SERVICE
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
|
||||
internal fun Application.isMain(): Boolean =
|
||||
getProcessName().makeLog("Current Process") == packageName.makeLog("Current packageName")
|
||||
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
internal fun Application.getProcessName(): String? {
|
||||
if (SDK_INT >= 28) {
|
||||
return Application.getProcessName()
|
||||
}
|
||||
|
||||
// Try using ActivityThread to determine the current process name.
|
||||
try {
|
||||
val activityThread = Class.forName(
|
||||
"android.app.ActivityThread",
|
||||
false,
|
||||
this::class.java.classLoader
|
||||
)
|
||||
val packageName: Any?
|
||||
val currentProcessName = activityThread.getDeclaredMethod("currentProcessName")
|
||||
currentProcessName.isAccessible = true
|
||||
packageName = currentProcessName.invoke(null)
|
||||
if (packageName is String) {
|
||||
return packageName
|
||||
}
|
||||
} catch (exception: Throwable) {
|
||||
exception.makeLog()
|
||||
}
|
||||
|
||||
// Fallback to the most expensive way
|
||||
val pid: Int = android.os.Process.myPid()
|
||||
val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||
|
||||
val processes = am.runningAppProcesses
|
||||
if (processes != null && processes.isNotEmpty()) {
|
||||
for (process in processes) {
|
||||
if (process.pid == pid) {
|
||||
return process.processName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# ImageToolbox is an image editor for android
|
||||
# Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# You should have received a copy of the Apache License
|
||||
# along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
unqualifiedResLocale=en
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "698299287298",
|
||||
"project_id": "imagetoolbox-13e3c",
|
||||
"storage_bucket": "imagetoolbox-13e3c.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:698299287298:android:11af7edf43198644b6e41b",
|
||||
"android_client_info": {
|
||||
"package_name": "ru.tech.imageresizershrinker.debug"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "698299287298-5ukroo831v2bn1vj8ff0oa7ch5h94mfg.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBH5QgaBw6XAfuxhPVJzq-r9s8ZMFalXzU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "698299287298-5ukroo831v2bn1vj8ff0oa7ch5h94mfg.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "698299287298",
|
||||
"project_id": "imagetoolbox-13e3c",
|
||||
"storage_bucket": "imagetoolbox-13e3c.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:698299287298:android:11af7edf43198644b6e41b",
|
||||
"android_client_info": {
|
||||
"package_name": "ru.tech.imageresizershrinker"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "698299287298-5ukroo831v2bn1vj8ff0oa7ch5h94mfg.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBH5QgaBw6XAfuxhPVJzq-r9s8ZMFalXzU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "698299287298-5ukroo831v2bn1vj8ff0oa7ch5h94mfg.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
|
||||
plugins {
|
||||
id("com.android.test")
|
||||
id("androidx.baselineprofile")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.t8rin.imagetoolbox.benchmark"
|
||||
compileSdk = libs.versions.androidCompileSdk.get().toIntOrNull()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
targetSdk = libs.versions.androidTargetSdk.get().toIntOrNull()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get())
|
||||
targetCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get())
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// This benchmark buildType is used for benchmarking, and should function like your
|
||||
// release build (for example, with minification on). It"s signed with a debug key
|
||||
// for easy local/CI testing.
|
||||
create("benchmark") {
|
||||
isDebuggable = true
|
||||
signingConfig = getByName("debug").signingConfig
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += listOf("app")
|
||||
productFlavors {
|
||||
create("foss") { dimension = "app" }
|
||||
create("market") { dimension = "app" }
|
||||
}
|
||||
|
||||
targetProjectPath = ":app"
|
||||
experimentalProperties["android.experimental.self-instrumenting"] = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
implementation(libs.espresso)
|
||||
implementation(libs.uiautomator)
|
||||
implementation(libs.benchmark.macro.junit4)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants(selector().all()) {
|
||||
it.enable = it.buildType == "benchmark"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
~ You should have received a copy of the Apache License
|
||||
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
-->
|
||||
|
||||
<manifest />
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.benchmark
|
||||
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@RequiresApi(28)
|
||||
class BaselineProfileGenerator {
|
||||
@get:Rule
|
||||
val baselineProfileRule = BaselineProfileRule()
|
||||
|
||||
@Test
|
||||
fun startup() = baselineProfileRule.collect(
|
||||
packageName = "com.t8rin.imagetoolbox",
|
||||
includeInStartupProfile = true,
|
||||
profileBlock = {
|
||||
startActivityAndWait()
|
||||
device.pressBack()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/app/release
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
/.idea
|
||||
/.kotlin/
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
group = "com.t8rin.imagetoolbox.buildlogic"
|
||||
|
||||
// Configure the build-logic plugins to target JDK 17
|
||||
// This matches the JDK used to build the project, and is not related to what is running on device.
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.agp.gradle)
|
||||
compileOnly(libs.kotlin.gradle)
|
||||
compileOnly(libs.detekt.gradle)
|
||||
compileOnly(libs.compose.compiler.gradle)
|
||||
compileOnly(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
// register the convention plugin
|
||||
plugins {
|
||||
register("imageToolboxLibrary") {
|
||||
id = "image.toolbox.library"
|
||||
implementationClass = "ImageToolboxLibraryPlugin"
|
||||
}
|
||||
register("imageToolboxHiltPlugin") {
|
||||
id = "image.toolbox.hilt"
|
||||
implementationClass = "ImageToolboxHiltPlugin"
|
||||
}
|
||||
register("imageToolboxLibraryFeature") {
|
||||
id = "image.toolbox.feature"
|
||||
implementationClass = "ImageToolboxLibraryFeaturePlugin"
|
||||
}
|
||||
register("imageToolboxLibraryComposePlugin") {
|
||||
id = "image.toolbox.compose"
|
||||
implementationClass = "ImageToolboxLibraryComposePlugin"
|
||||
}
|
||||
register("imageToolboxApplicationPlugin") {
|
||||
id = "image.toolbox.application"
|
||||
implementationClass = "ImageToolboxApplicationPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.t8rin.imagetoolbox.configureCompose
|
||||
import com.t8rin.imagetoolbox.configureDetekt
|
||||
import com.t8rin.imagetoolbox.configureKotlinAndroid
|
||||
import com.t8rin.imagetoolbox.core
|
||||
import com.t8rin.imagetoolbox.crash
|
||||
import com.t8rin.imagetoolbox.data
|
||||
import com.t8rin.imagetoolbox.di
|
||||
import com.t8rin.imagetoolbox.domain
|
||||
import com.t8rin.imagetoolbox.implementation
|
||||
import com.t8rin.imagetoolbox.libs
|
||||
import com.t8rin.imagetoolbox.projects
|
||||
import com.t8rin.imagetoolbox.resources
|
||||
import com.t8rin.imagetoolbox.settings
|
||||
import com.t8rin.imagetoolbox.ui
|
||||
import com.t8rin.imagetoolbox.utils
|
||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class ImageToolboxApplicationPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
apply(plugin = "com.android.application")
|
||||
apply(plugin = "kotlin-parcelize")
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
apply(plugin = "com.google.firebase.crashlytics")
|
||||
apply(plugin = "com.mikepenz.aboutlibraries.plugin.android")
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||
apply(plugin = "io.gitlab.arturbosch.detekt")
|
||||
|
||||
configureDetekt(extensions.getByType<DetektExtension>())
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
configureKotlinAndroid(
|
||||
commonExtension = this,
|
||||
createFlavors = false
|
||||
)
|
||||
defaultConfig.targetSdk = libs.versions.androidTargetSdk.get().toIntOrNull()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidxCore)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.domain)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.settings)
|
||||
implementation(projects.core.di)
|
||||
implementation(projects.core.crash)
|
||||
implementation(projects.core.utils)
|
||||
}
|
||||
|
||||
configureCompose(extensions.getByType<ApplicationExtension>())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
import com.t8rin.imagetoolbox.implementation
|
||||
import com.t8rin.imagetoolbox.ksp
|
||||
import com.t8rin.imagetoolbox.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class ImageToolboxHiltPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
apply(plugin = "dagger.hilt.android.plugin")
|
||||
apply(plugin = "com.google.devtools.ksp")
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger.hilt.android)
|
||||
ksp(libs.dagger.hilt.compiler)
|
||||
ksp(libs.kotlin.metadata.jvm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
import com.t8rin.imagetoolbox.configureCompose
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class ImageToolboxLibraryComposePlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
apply(plugin = "com.android.library")
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||
|
||||
configureCompose(extensions.getByType<LibraryExtension>())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
import com.t8rin.imagetoolbox.configureDetekt
|
||||
import com.t8rin.imagetoolbox.core
|
||||
import com.t8rin.imagetoolbox.crash
|
||||
import com.t8rin.imagetoolbox.data
|
||||
import com.t8rin.imagetoolbox.di
|
||||
import com.t8rin.imagetoolbox.domain
|
||||
import com.t8rin.imagetoolbox.implementation
|
||||
import com.t8rin.imagetoolbox.projects
|
||||
import com.t8rin.imagetoolbox.resources
|
||||
import com.t8rin.imagetoolbox.settings
|
||||
import com.t8rin.imagetoolbox.ui
|
||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class ImageToolboxLibraryFeaturePlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
configureDetekt(extensions.getByType<DetektExtension>())
|
||||
dependencies {
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.domain)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.settings)
|
||||
implementation(projects.core.di)
|
||||
implementation(projects.core.crash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
import com.t8rin.imagetoolbox.configureDetekt
|
||||
import com.t8rin.imagetoolbox.configureKotlinAndroid
|
||||
import com.t8rin.imagetoolbox.implementation
|
||||
import com.t8rin.imagetoolbox.libs
|
||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class ImageToolboxLibraryPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.android.library")
|
||||
apply("kotlin-parcelize")
|
||||
apply("kotlinx-serialization")
|
||||
apply(libs.detekt.gradle.get().group)
|
||||
}
|
||||
|
||||
configureDetekt(extensions.getByType<DetektExtension>())
|
||||
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.minSdk = libs.versions.androidMinSdk.get().toIntOrNull()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidxCore)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox
|
||||
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
|
||||
|
||||
internal fun Project.configureCompose(
|
||||
commonExtension: CommonExtension
|
||||
) {
|
||||
commonExtension.apply {
|
||||
buildFeatures.apply {
|
||||
compose = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.window.sizeclass)
|
||||
implementation(libs.androidx.material)
|
||||
implementation(libs.compose.preview)
|
||||
debugImplementation(libs.compose.tooling)
|
||||
}
|
||||
}
|
||||
|
||||
extensions.configure<ComposeCompilerGradlePluginExtension> {
|
||||
stabilityConfigurationFiles.addAll(
|
||||
rootProject.layout.projectDirectory.file("compose_compiler_config.conf")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox
|
||||
|
||||
|
||||
import io.gitlab.arturbosch.detekt.Detekt
|
||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.gradle.kotlin.dsl.named
|
||||
|
||||
internal fun Project.configureDetekt(extension: DetektExtension) = extension.apply {
|
||||
tasks.named<Detekt>("detekt") {
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
html.required.set(true)
|
||||
txt.required.set(true)
|
||||
sarif.required.set(true)
|
||||
md.required.set(true)
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins(libs.detekt.formatting)
|
||||
detektPlugins(libs.detekt.compose)
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox
|
||||
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.assign
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.gradle.kotlin.dsl.provideDelegate
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
internal fun Project.configureKotlinAndroid(
|
||||
commonExtension: CommonExtension,
|
||||
createFlavors: Boolean = true
|
||||
) {
|
||||
commonExtension.apply {
|
||||
compileSdk = libs.versions.androidCompileSdk.get().toIntOrNull()
|
||||
compileSdkExtension = libs.versions.androidCompileSdkExtension.get().toIntOrNull()
|
||||
|
||||
defaultConfig.apply {
|
||||
minSdk = libs.versions.androidMinSdk.get().toIntOrNull()
|
||||
}
|
||||
|
||||
if (createFlavors) {
|
||||
flavorDimensions += "app"
|
||||
|
||||
productFlavors.apply {
|
||||
create("foss") {
|
||||
dimension = "app"
|
||||
}
|
||||
create("market") {
|
||||
dimension = "app"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions.apply {
|
||||
sourceCompatibility = javaVersion
|
||||
targetCompatibility = javaVersion
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
buildFeatures.apply {
|
||||
compose = false
|
||||
aidl = false
|
||||
shaders = false
|
||||
buildConfig = false
|
||||
resValues = false
|
||||
}
|
||||
|
||||
packaging.apply {
|
||||
resources {
|
||||
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
|
||||
}
|
||||
}
|
||||
|
||||
lint.apply {
|
||||
disable += "UsingMaterialAndMaterial3Libraries"
|
||||
disable += "ModifierParameter"
|
||||
disable += "ConvertLongToDuration"
|
||||
}
|
||||
}
|
||||
|
||||
configureKotlin<KotlinAndroidProjectExtension>()
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.desugaring)
|
||||
}
|
||||
}
|
||||
|
||||
val Project.javaVersion: JavaVersion
|
||||
get() = JavaVersion.toVersion(
|
||||
libs.versions.jvmTarget.get()
|
||||
)
|
||||
|
||||
/**
|
||||
* Configure base Kotlin options
|
||||
*/
|
||||
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() = configure<T> {
|
||||
val args = listOf(
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=com.arkivanov.decompose.ExperimentalDecomposeApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=coil3.annotation.DelicateCoilApi",
|
||||
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.ui.text.ExperimentalTextApi",
|
||||
"-opt-in=kotlinx.coroutines.DelicateCoroutinesApi",
|
||||
"-Xannotation-default-target=param-property",
|
||||
"-XXLanguage:+PropertyParamAnnotationDefaultTargetMode"
|
||||
)
|
||||
// Treat all Kotlin warnings as errors (disabled by default)
|
||||
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
|
||||
val warningsAsErrors: String? by project
|
||||
when (this) {
|
||||
is KotlinAndroidProjectExtension -> compilerOptions
|
||||
is KotlinJvmProjectExtension -> compilerOptions
|
||||
else -> error("Unsupported project extension $this ${T::class}")
|
||||
}.apply {
|
||||
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||
allWarningsAsErrors = warningsAsErrors.toBoolean()
|
||||
freeCompilerArgs.addAll(args)
|
||||
}
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions.freeCompilerArgs.addAll(args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox
|
||||
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.kotlin.dsl.DependencyHandlerScope
|
||||
import org.gradle.kotlin.dsl.the
|
||||
|
||||
val Project.libs
|
||||
get(): LibrariesForLibs = the<LibrariesForLibs>()
|
||||
|
||||
val Project.projects
|
||||
get(): Projects = object : Projects, ProjectHolder {
|
||||
override fun project(path: String): Project = this@projects.project(path)
|
||||
}
|
||||
|
||||
val Projects.core
|
||||
get(): CoreProjects = object : CoreProjects, ProjectHolder by this {}
|
||||
|
||||
val CoreProjects.data
|
||||
get(): ProjectLibrary = project(":core:data")
|
||||
|
||||
val CoreProjects.ui
|
||||
get(): ProjectLibrary = project(":core:ui")
|
||||
|
||||
val CoreProjects.domain
|
||||
get(): ProjectLibrary = project(":core:domain")
|
||||
|
||||
val CoreProjects.resources
|
||||
get(): ProjectLibrary = project(":core:resources")
|
||||
|
||||
val CoreProjects.settings
|
||||
get(): ProjectLibrary = project(":core:settings")
|
||||
|
||||
val CoreProjects.di
|
||||
get(): ProjectLibrary = project(":core:di")
|
||||
|
||||
val CoreProjects.crash
|
||||
get(): ProjectLibrary = project(":core:crash")
|
||||
|
||||
val CoreProjects.utils
|
||||
get(): ProjectLibrary = project(":core:utils")
|
||||
|
||||
fun DependencyHandlerScope.debugImplementation(
|
||||
dependency: Library
|
||||
) = add("debugImplementation", dependency)
|
||||
|
||||
fun DependencyHandlerScope.implementation(
|
||||
dependency: Library
|
||||
) = add("implementation", dependency)
|
||||
|
||||
fun DependencyHandlerScope.coreLibraryDesugaring(
|
||||
dependency: Library
|
||||
) = add("coreLibraryDesugaring", dependency)
|
||||
|
||||
fun DependencyHandlerScope.implementation(
|
||||
dependency: ProjectLibrary
|
||||
) = add("implementation", dependency)
|
||||
|
||||
fun DependencyHandlerScope.ksp(
|
||||
dependency: Library
|
||||
) = add("ksp", dependency)
|
||||
|
||||
fun DependencyHandlerScope.detektPlugins(
|
||||
dependency: Library
|
||||
) = add("detektPlugins", dependency)
|
||||
|
||||
typealias Library = Provider<MinimalExternalModuleDependency>
|
||||
|
||||
typealias ProjectLibrary = Project
|
||||
|
||||
interface Projects : ProjectHolder
|
||||
|
||||
interface CoreProjects : ProjectHolder
|
||||
|
||||
interface ProjectHolder {
|
||||
fun project(path: String): Project
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
// make sure the file rootProject/gradle/verison.toml exists!
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "build-logic"
|
||||
include(":convention")
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { setUrl("https://jitpack.io") }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath(libs.kotlinx.serialization.gradle)
|
||||
classpath(libs.ksp.gradle)
|
||||
classpath(libs.agp.gradle)
|
||||
classpath(libs.kotlin.gradle)
|
||||
classpath(libs.hilt.gradle)
|
||||
classpath(libs.gms.gradle)
|
||||
classpath(libs.firebase.crashlytics.gradle)
|
||||
classpath(libs.baselineprofile.gradle)
|
||||
classpath(libs.detekt.gradle)
|
||||
classpath(libs.aboutlibraries.gradle)
|
||||
classpath(libs.compose.compiler.gradle)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete::class) {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
java.time.ZoneId
|
||||
java.time.ZoneOffset
|
||||
java.text.DecimalFormat
|
||||
java.text.NumberFormat
|
||||
java.time.Instant
|
||||
java.time.Duration
|
||||
java.time.LocalDate
|
||||
java.time.LocalDateTime
|
||||
java.time.LocalTime
|
||||
java.time.ZonedDateTime
|
||||
java.util.Date
|
||||
java.util.Locale
|
||||
java.io.File
|
||||
|
||||
kotlin.collections.*
|
||||
kotlin.time.Duration
|
||||
|
||||
android.net.Uri
|
||||
android.graphics.Bitmap
|
||||
|
||||
androidx.compose.ui.graphics.painter.Painter
|
||||
androidx.compose.ui.graphics.vector.ImageVector
|
||||
androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
com.t8rin.imagetoolbox.core.crash.*
|
||||
com.t8rin.imagetoolbox.core.filters.*
|
||||
com.t8rin.imagetoolbox.core.resources.*
|
||||
com.t8rin.imagetoolbox.core.data.*
|
||||
com.t8rin.imagetoolbox.core.domain.*
|
||||
com.t8rin.imagetoolbox.core.ui.*
|
||||
com.t8rin.imagetoolbox.core.settings.*
|
||||
|
||||
com.t8rin.imagetoolbox.core.*
|
||||
com.t8rin.imagetoolbox.feature.*
|
||||
|
||||
nl.dionsegijn.konfetti.core.*
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.image.toolbox.library)
|
||||
alias(libs.plugins.image.toolbox.hilt)
|
||||
alias(libs.plugins.image.toolbox.compose)
|
||||
}
|
||||
|
||||
android.namespace = "com.t8rin.imagetoolbox.core.crash"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.settings)
|
||||
|
||||
"marketImplementation"(platform(libs.firebase.bom))
|
||||
"marketImplementation"(libs.firebase.crashlytics)
|
||||
"marketImplementation"(libs.firebase.analytics)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.data
|
||||
|
||||
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
|
||||
|
||||
internal object AnalyticsManagerImpl : AnalyticsManager {
|
||||
|
||||
override var allowCollectCrashlytics: Boolean = false
|
||||
|
||||
override var allowCollectAnalytics: Boolean = false
|
||||
|
||||
override fun updateAnalyticsCollectionEnabled(value: Boolean) = Unit
|
||||
|
||||
override fun updateAllowCollectCrashlytics(value: Boolean) = Unit
|
||||
|
||||
override fun sendReport(throwable: Throwable) = Unit
|
||||
|
||||
override fun registerScreenOpen(screenName: String) = Unit
|
||||
|
||||
override fun pushMetric(
|
||||
tag: String,
|
||||
metric: String
|
||||
) = Unit
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
~ You should have received a copy of the Apache License
|
||||
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity android:name=".presentation.CrashActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.di
|
||||
|
||||
import com.t8rin.imagetoolbox.core.crash.data.AnalyticsManagerImpl
|
||||
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CrashModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun analyticsManager(): AnalyticsManager = AnalyticsManagerImpl
|
||||
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.arkivanov.decompose.retainedComponent
|
||||
import com.t8rin.imagetoolbox.core.crash.presentation.components.CrashHandler
|
||||
import com.t8rin.imagetoolbox.core.crash.presentation.components.CrashRootContent
|
||||
import com.t8rin.imagetoolbox.core.crash.presentation.screenLogic.CrashComponent
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.ComposeActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CrashActivity : ComposeActivity(), CrashHandler {
|
||||
|
||||
@Inject
|
||||
lateinit var componentFactory: CrashComponent.Factory
|
||||
|
||||
private val component: CrashComponent by lazy {
|
||||
retainedComponent { componentContext ->
|
||||
componentFactory(
|
||||
componentContext = componentContext,
|
||||
crashInfo = getCrashInfo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() = CrashRootContent(component = component)
|
||||
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.domain.TELEGRAM_GROUP_LINK
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Github
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.MobileShare
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Telegram
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.Black
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.Blue
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.White
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.outlineVariant
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.animation.springySpec
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedCircularProgressIndicator
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
internal fun CrashActionButtons(
|
||||
onCopyCrashInfo: () -> Unit,
|
||||
onShareLogs: () -> Unit,
|
||||
isSendingLogs: Boolean,
|
||||
githubLink: String
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val containerWidth = maxWidth
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val linkHandler = LocalUriHandler.current
|
||||
LargeEnhancedButton(
|
||||
onClick = {
|
||||
onCopyCrashInfo()
|
||||
linkHandler.openUri(TELEGRAM_GROUP_LINK)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.width(containerWidth / 2f)
|
||||
.padding(end = 8.dp),
|
||||
containerColor = Blue,
|
||||
contentColor = White,
|
||||
icon = Icons.Rounded.Telegram,
|
||||
text = stringResource(R.string.contact_me)
|
||||
)
|
||||
LargeEnhancedButton(
|
||||
onClick = {
|
||||
onCopyCrashInfo()
|
||||
linkHandler.openUri(githubLink)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.width(containerWidth / 2f),
|
||||
containerColor = Black,
|
||||
contentColor = White,
|
||||
icon = Icons.Rounded.Github,
|
||||
text = stringResource(id = R.string.create_issue),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
LargeEnhancedButton(
|
||||
onClick = {
|
||||
onCopyCrashInfo()
|
||||
onShareLogs()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
icon = Icons.Rounded.MobileShare,
|
||||
text = stringResource(id = R.string.send_logs),
|
||||
isLoading = isSendingLogs
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LargeEnhancedButton(
|
||||
onClick: () -> Unit,
|
||||
containerColor: Color,
|
||||
contentColor: Color,
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
isLoading: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val progressAnimatable = remember { Animatable(if (isLoading) 1f else 0f) }
|
||||
val progress = progressAnimatable.value
|
||||
|
||||
LaunchedEffect(isLoading) {
|
||||
delay(400)
|
||||
if (isLoading) {
|
||||
progressAnimatable.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = springySpec()
|
||||
)
|
||||
} else {
|
||||
progressAnimatable.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(200)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EnhancedButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.height(48.dp),
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
borderColor = MaterialTheme.colorScheme.outlineVariant(
|
||||
onTopOf = containerColor
|
||||
),
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding
|
||||
) {
|
||||
AnimatedContent(progress > 0) { showLoading ->
|
||||
if (showLoading) {
|
||||
EnhancedCircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp * progress),
|
||||
trackColor = MaterialTheme.colorScheme.onPrimary.copy(0.2f),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = text
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AutoSizeText(
|
||||
text = text,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.t8rin.imagetoolbox.core.settings.presentation.provider.LocalSettingsState
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.blend
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.takeColorFromScheme
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.EmojiItem
|
||||
|
||||
@Composable
|
||||
internal fun CrashAttentionCard(
|
||||
title: String,
|
||||
description: String,
|
||||
emoji: String
|
||||
) {
|
||||
val isNightMode = LocalSettingsState.current.isNightMode
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.container(
|
||||
shape = ShapeDefaults.large,
|
||||
resultPadding = 16.dp,
|
||||
color = takeColorFromScheme {
|
||||
if (isNightMode) {
|
||||
errorContainer.blend(surfaceContainerLow, 0.75f)
|
||||
} else {
|
||||
errorContainer.blend(surfaceContainerLow, 0.65f)
|
||||
}
|
||||
}
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val contentColor = takeColorFromScheme {
|
||||
if (isNightMode) {
|
||||
onError.blend(onSurface, 0.75f)
|
||||
} else {
|
||||
error.blend(onSurface, 0.6f)
|
||||
}
|
||||
}
|
||||
EmojiItem(
|
||||
emoji = "",
|
||||
fontSize = 0.sp,
|
||||
animatedEmoji = emoji,
|
||||
shape = ShapeDefaults.default,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.statusBarsPadding(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 26.sp,
|
||||
color = contentColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = description,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 20.sp,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.ContentCopy
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.RestartAlt
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedFloatingActionButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedFloatingActionButtonType
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
|
||||
|
||||
@Composable
|
||||
internal fun CrashBottomButtons(
|
||||
modifier: Modifier,
|
||||
onCopy: (() -> Unit)?,
|
||||
onRestartApp: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(8.dp)
|
||||
.navigationBarsPadding()
|
||||
.displayCutoutPadding()
|
||||
) {
|
||||
EnhancedFloatingActionButton(
|
||||
modifier = Modifier
|
||||
.weight(1f, false),
|
||||
onClick = onRestartApp,
|
||||
content = {
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.RestartAlt,
|
||||
contentDescription = stringResource(R.string.restart_app)
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
AutoSizeText(
|
||||
text = stringResource(R.string.restart_app),
|
||||
maxLines = 1
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
}
|
||||
)
|
||||
onCopy?.let {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
EnhancedFloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
onClick = onCopy,
|
||||
type = EnhancedFloatingActionButtonType.SecondaryHorizontal
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ContentCopy,
|
||||
contentDescription = stringResource(R.string.copy)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.t8rin.imagetoolbox.core.domain.ISSUE_TRACKER
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.DeviceInfo
|
||||
import com.t8rin.imagetoolbox.core.utils.encodeEscaped
|
||||
|
||||
interface CrashHandler {
|
||||
|
||||
fun getIntent(): Intent
|
||||
|
||||
private val intent: Intent
|
||||
@JvmName("getIntentValue")
|
||||
get() = getIntent()
|
||||
|
||||
private fun getCrashReason(): String = intent.getStringExtra(EXCEPTION_EXTRA) ?: ""
|
||||
|
||||
fun getCrashInfo(): CrashInfo {
|
||||
val crashReason = getCrashReason()
|
||||
val splitData = crashReason.split(DELIMITER)
|
||||
val exceptionName = splitData.first().trim()
|
||||
val stackTrace = splitData.drop(1).joinToString(DELIMITER)
|
||||
|
||||
val title = "[Bug] App Crash: $exceptionName"
|
||||
|
||||
val deviceInfo = DeviceInfo.getAsString()
|
||||
|
||||
val body = listOf(deviceInfo, stackTrace).joinToString(DELIMITER)
|
||||
|
||||
return CrashInfo(
|
||||
title = title,
|
||||
body = body,
|
||||
exceptionName = exceptionName,
|
||||
stackTrace = stackTrace
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val EXCEPTION_EXTRA = "EXCEPTION_EXTRA"
|
||||
|
||||
fun getCrashInfoAsExtra(
|
||||
throwable: Throwable
|
||||
): String {
|
||||
val exceptionName = throwable::class.java.simpleName
|
||||
val stackTrace = Log.getStackTraceString(throwable)
|
||||
|
||||
return listOf(exceptionName, stackTrace).joinToString(DELIMITER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CrashInfo(
|
||||
val title: String,
|
||||
val body: String,
|
||||
val exceptionName: String,
|
||||
val stackTrace: String
|
||||
) {
|
||||
val isOutOfMemory: Boolean = listOf(
|
||||
title,
|
||||
body,
|
||||
exceptionName,
|
||||
stackTrace
|
||||
).any { text ->
|
||||
text.contains(
|
||||
other = "OutOfMemoryError",
|
||||
ignoreCase = true
|
||||
) || text.contains(
|
||||
other = "Failed to allocate",
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
|
||||
val isForegroundServiceDidNotStartInTime: Boolean = listOf(
|
||||
title,
|
||||
body,
|
||||
exceptionName,
|
||||
stackTrace
|
||||
).any { text ->
|
||||
text.contains(
|
||||
other = "ForegroundServiceDidNotStartInTimeException",
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
|
||||
val isNonReportable: Boolean = isOutOfMemory || isForegroundServiceDidNotStartInTime
|
||||
|
||||
val emoji: String = if (isNonReportable) {
|
||||
if (isOutOfMemory) {
|
||||
"file:///android_asset/lottie/transportation/construction.lottie"
|
||||
} else {
|
||||
"file:///android_asset/lottie/objects/bomb.lottie"
|
||||
}
|
||||
} else {
|
||||
"file:///android_asset/lottie/objects/siren.lottie"
|
||||
}
|
||||
|
||||
val textToSend = listOf(title, body).joinToString(DELIMITER)
|
||||
|
||||
val githubLink =
|
||||
"$ISSUE_TRACKER/new?title=${title.encodeEscaped()}&body=${body.encodeEscaped()}"
|
||||
}
|
||||
|
||||
private const val DELIMITER = "\n\n"
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.BugReport
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.ExpandableItem
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
|
||||
|
||||
@Composable
|
||||
internal fun CrashInfoCard(crashInfo: CrashInfo) {
|
||||
ExpandableItem(
|
||||
shape = ShapeDefaults.extraLarge,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visibleContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.BugReport,
|
||||
contentDescription = "crash",
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
top = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
)
|
||||
AutoSizeText(
|
||||
text = crashInfo.exceptionName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
},
|
||||
expandableContent = {
|
||||
AnimatedVisibility(visible = it) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = crashInfo.stackTrace,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation.components
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.crash.presentation.screenLogic.CrashComponent
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.settings.presentation.model.toUiState
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.AppActivityClass
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.Clipboard
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.provider.ImageToolboxCompositionLocals
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.enhancedVerticalScroll
|
||||
|
||||
@Composable
|
||||
internal fun CrashRootContent(component: CrashComponent) {
|
||||
val context = LocalContext.current
|
||||
val crashInfo = component.crashInfo
|
||||
|
||||
|
||||
ImageToolboxCompositionLocals(
|
||||
settingsState = component.settingsState.toUiState()
|
||||
) {
|
||||
val copyCrashInfo: () -> Unit = {
|
||||
Clipboard.copy(crashInfo.textToSend)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.enhancedVerticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 80.dp)
|
||||
.navigationBarsPadding()
|
||||
.displayCutoutPadding(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
when {
|
||||
crashInfo.isOutOfMemory -> {
|
||||
CrashAttentionCard(
|
||||
title = stringResource(R.string.crash_oom_title),
|
||||
description = stringResource(R.string.crash_oom_description),
|
||||
emoji = crashInfo.emoji
|
||||
)
|
||||
}
|
||||
|
||||
crashInfo.isForegroundServiceDidNotStartInTime -> {
|
||||
CrashAttentionCard(
|
||||
title = stringResource(R.string.crash_foreground_service_timeout_title),
|
||||
description = stringResource(R.string.crash_foreground_service_timeout_description),
|
||||
emoji = crashInfo.emoji
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
CrashAttentionCard(
|
||||
title = stringResource(R.string.crash_title),
|
||||
description = stringResource(R.string.crash_subtitle),
|
||||
emoji = crashInfo.emoji
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
CrashActionButtons(
|
||||
onCopyCrashInfo = copyCrashInfo,
|
||||
onShareLogs = component::shareLogs,
|
||||
isSendingLogs = component.isSendingLogs,
|
||||
githubLink = crashInfo.githubLink
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
CrashInfoCard(crashInfo = crashInfo)
|
||||
}
|
||||
|
||||
CrashBottomButtons(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
onCopy = copyCrashInfo.takeIf { !crashInfo.isNonReportable },
|
||||
onRestartApp = {
|
||||
context.startActivity(
|
||||
Intent(context, AppActivityClass)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation.components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.t8rin.imagetoolbox.core.crash.di.CrashModule
|
||||
import com.t8rin.imagetoolbox.core.crash.presentation.CrashActivity
|
||||
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private class GlobalExceptionHandler<T : CrashHandler> private constructor(
|
||||
private val applicationContext: Context,
|
||||
private val defaultHandler: Thread.UncaughtExceptionHandler?,
|
||||
private val activityToBeLaunched: Class<T>
|
||||
) : Thread.UncaughtExceptionHandler {
|
||||
|
||||
override fun uncaughtException(
|
||||
p0: Thread,
|
||||
p1: Throwable
|
||||
) {
|
||||
sendReport(p1)
|
||||
|
||||
runCatching {
|
||||
p1.makeLog("FATAL_EXCEPTION")
|
||||
|
||||
applicationContext.launchActivity(activityToBeLaunched, p1)
|
||||
exitProcess(0)
|
||||
}.getOrElse {
|
||||
defaultHandler?.uncaughtException(p0, p1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.launchActivity(
|
||||
activity: Class<*>,
|
||||
throwable: Throwable
|
||||
) = applicationContext.startActivity(
|
||||
Intent(applicationContext, activity).putExtra(
|
||||
CrashHandler.EXCEPTION_EXTRA,
|
||||
CrashHandler.getCrashInfoAsExtra(throwable)
|
||||
).addFlags(defFlags)
|
||||
)
|
||||
|
||||
companion object : AnalyticsManager by CrashModule.analyticsManager() {
|
||||
|
||||
fun <T : CrashHandler> initialize(
|
||||
applicationContext: Context,
|
||||
activityToBeLaunched: Class<T>,
|
||||
) = Thread.setDefaultUncaughtExceptionHandler(
|
||||
GlobalExceptionHandler(
|
||||
applicationContext = applicationContext,
|
||||
defaultHandler = Thread.getDefaultUncaughtExceptionHandler()!!,
|
||||
activityToBeLaunched = activityToBeLaunched
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private const val defFlags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
fun Context.applyGlobalExceptionHandler() = GlobalExceptionHandler.initialize(
|
||||
applicationContext = applicationContext,
|
||||
activityToBeLaunched = CrashActivity::class.java,
|
||||
)
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.presentation.screenLogic
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.t8rin.imagetoolbox.core.crash.presentation.components.CrashInfo
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ShareProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.SettingsManager
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.model.SettingsState
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.BaseComponent
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.state.update
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class CrashComponent @AssistedInject internal constructor(
|
||||
@Assisted componentContext: ComponentContext,
|
||||
@Assisted val crashInfo: CrashInfo,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val shareProvider: ShareProvider,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : BaseComponent(dispatchersHolder, componentContext) {
|
||||
|
||||
private val _settingsState = mutableStateOf(SettingsState.Default)
|
||||
val settingsState: SettingsState by _settingsState
|
||||
|
||||
private val _isSendingLogs = mutableStateOf(false)
|
||||
val isSendingLogs by _isSendingLogs
|
||||
|
||||
init {
|
||||
runBlocking {
|
||||
_settingsState.value = settingsManager.getSettingsState()
|
||||
}
|
||||
settingsManager.settingsState.onEach {
|
||||
_settingsState.value = it
|
||||
}.launchIn(componentScope)
|
||||
|
||||
crashInfo.makeLog("CrashComponent")
|
||||
}
|
||||
|
||||
fun shareLogs() {
|
||||
componentScope.launch {
|
||||
_isSendingLogs.update { true }
|
||||
runSuspendCatching {
|
||||
shareProvider.shareUri(
|
||||
uri = settingsManager.createLogsExport(),
|
||||
onComplete = {}
|
||||
)
|
||||
}
|
||||
_isSendingLogs.update { false }
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
operator fun invoke(
|
||||
componentContext: ComponentContext,
|
||||
crashInfo: CrashInfo
|
||||
): CrashComponent
|
||||
}
|
||||
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.crash.data
|
||||
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.analytics.FirebaseAnalytics.Event
|
||||
import com.google.firebase.analytics.FirebaseAnalytics.Param
|
||||
import com.google.firebase.analytics.analytics
|
||||
import com.google.firebase.analytics.logEvent
|
||||
import com.google.firebase.crashlytics.crashlytics
|
||||
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.DeviceInfo.Companion.get
|
||||
|
||||
internal object AnalyticsManagerImpl : AnalyticsManager {
|
||||
|
||||
override var allowCollectCrashlytics: Boolean = false
|
||||
|
||||
override var allowCollectAnalytics: Boolean = false
|
||||
|
||||
override fun updateAnalyticsCollectionEnabled(value: Boolean) {
|
||||
analytics.setAnalyticsCollectionEnabled(value)
|
||||
allowCollectAnalytics = value
|
||||
}
|
||||
|
||||
override fun updateAllowCollectCrashlytics(value: Boolean) {
|
||||
crashlytics.isCrashlyticsCollectionEnabled = value
|
||||
allowCollectCrashlytics = value
|
||||
|
||||
if (value) {
|
||||
crashlytics.sendUnsentReports()
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendReport(throwable: Throwable) {
|
||||
if (allowCollectCrashlytics) {
|
||||
crashlytics.apply {
|
||||
recordException(throwable)
|
||||
sendUnsentReports()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerScreenOpen(screenName: String) {
|
||||
if (allowCollectAnalytics) {
|
||||
analytics.apply {
|
||||
logEvent(Event.SELECT_CONTENT) { param(Param.CONTENT_TYPE, screenName) }
|
||||
logEvent(screenName) { param(Param.CONTENT, deviceInfo()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pushMetric(
|
||||
tag: String,
|
||||
metric: String
|
||||
) {
|
||||
if (allowCollectAnalytics) {
|
||||
analytics.logEvent(tag) { param(Param.CONTENT, metric) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun deviceInfo(): String {
|
||||
val info = get()
|
||||
|
||||
return listOf(
|
||||
"Device: ${info.device}",
|
||||
"App Version: ${info.appVersion}"
|
||||
).joinToString(",")
|
||||
}
|
||||
|
||||
private val analytics get() = Firebase.analytics
|
||||
private val crashlytics get() = Firebase.crashlytics
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.image.toolbox.library)
|
||||
alias(libs.plugins.image.toolbox.hilt)
|
||||
}
|
||||
|
||||
android.namespace = "com.t8rin.imagetoolbox.core.data"
|
||||
|
||||
dependencies {
|
||||
api(libs.coil)
|
||||
implementation(libs.coilNetwork)
|
||||
api(libs.ktor)
|
||||
api(libs.ktor.logging)
|
||||
implementation(libs.coilGif)
|
||||
implementation(libs.coilSvg)
|
||||
implementation(libs.coil.resvg)
|
||||
implementation(libs.trickle)
|
||||
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
|
||||
api(libs.datastore.preferences.android)
|
||||
api(libs.datastore.core.android)
|
||||
|
||||
implementation(libs.avif.coder)
|
||||
implementation(libs.jxl.coder.coil) {
|
||||
exclude(module = "com.github.awxkee:jxl-coder")
|
||||
}
|
||||
implementation(libs.jxl.coder)
|
||||
|
||||
implementation(libs.aire)
|
||||
implementation(libs.jpegli.coder)
|
||||
|
||||
implementation(libs.moshi)
|
||||
implementation(libs.moshi.adapters)
|
||||
|
||||
api(libs.androidx.documentfile)
|
||||
|
||||
implementation(libs.toolbox.gifConverter)
|
||||
implementation(libs.toolbox.exif)
|
||||
implementation(libs.tiffdecoder)
|
||||
implementation(libs.toolbox.qoiCoder)
|
||||
implementation(libs.toolbox.jp2decoder)
|
||||
implementation(libs.toolbox.awebp)
|
||||
implementation(libs.toolbox.psd)
|
||||
implementation(libs.toolbox.apng)
|
||||
implementation(libs.toolbox.djvuCoder)
|
||||
implementation(libs.pdfbox)
|
||||
implementation(libs.trickle)
|
||||
|
||||
implementation(projects.core.domain)
|
||||
implementation(projects.core.resources)
|
||||
|
||||
implementation(projects.core.filters)
|
||||
|
||||
implementation(projects.core.settings)
|
||||
implementation(projects.core.di)
|
||||
api(projects.core.utils)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
~ You should have received a copy of the Apache License
|
||||
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".saving.KeepAliveForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import android.util.Base64
|
||||
import coil3.ImageLoader
|
||||
import coil3.Uri
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.isBase64
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.trimToBase64
|
||||
import okio.Buffer
|
||||
|
||||
internal class Base64Fetcher(
|
||||
private val options: Options,
|
||||
private val base64: String
|
||||
) : Fetcher {
|
||||
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
val byteArray = runCatching {
|
||||
Base64.decode(base64, Base64.DEFAULT)
|
||||
}.getOrNull() ?: return null
|
||||
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(
|
||||
source = Buffer().apply { write(byteArray) },
|
||||
fileSystem = options.fileSystem,
|
||||
),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.MEMORY,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Fetcher.Factory<Uri> {
|
||||
override fun create(
|
||||
data: Uri,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader,
|
||||
): Fetcher? {
|
||||
val stripped = data.toString().trimToBase64()
|
||||
return if (stripped.isBase64()) {
|
||||
Base64Fetcher(
|
||||
options = options,
|
||||
base64 = stripped
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import coil3.util.DebugLogger
|
||||
import coil3.util.Logger
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import com.t8rin.imagetoolbox.core.utils.Logger as RealLogger
|
||||
|
||||
internal class CoilLogger : Logger {
|
||||
|
||||
private val delegate = DebugLogger()
|
||||
|
||||
override var minLevel: Logger.Level
|
||||
get() = delegate.minLevel
|
||||
set(value) {
|
||||
delegate.minLevel = value
|
||||
}
|
||||
|
||||
override fun log(
|
||||
tag: String,
|
||||
level: Logger.Level,
|
||||
message: String?,
|
||||
throwable: Throwable?
|
||||
) {
|
||||
message?.takeIf {
|
||||
"NullRequestData" !in it && "PDF" !in it
|
||||
}?.makeLog(tag, level.toLogger())
|
||||
|
||||
throwable?.takeIf {
|
||||
"The request's data is null" !in it.message.orEmpty() && "PDF" !in it.message.orEmpty()
|
||||
}?.makeLog(tag)
|
||||
|
||||
delegate.log(
|
||||
tag = tag,
|
||||
level = level,
|
||||
message = message,
|
||||
throwable = throwable
|
||||
)
|
||||
}
|
||||
|
||||
private fun Logger.Level.toLogger(): RealLogger.Level = when (this) {
|
||||
Logger.Level.Verbose -> RealLogger.Level.Verbose
|
||||
Logger.Level.Debug -> RealLogger.Level.Debug
|
||||
Logger.Level.Info -> RealLogger.Level.Info
|
||||
Logger.Level.Warn -> RealLogger.Level.Warn
|
||||
Logger.Level.Error -> RealLogger.Level.Error
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.request.allowHardware
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.bitmapConfig
|
||||
import coil3.size.Scale
|
||||
import coil3.size.Size
|
||||
import coil3.size.pxOrElse
|
||||
import com.radzivon.bartoshyk.avif.coder.Coder
|
||||
import com.radzivon.bartoshyk.avif.coder.PreferredColorConfig
|
||||
import com.radzivon.bartoshyk.avif.coder.ScaleMode
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
|
||||
class HeifDecoder(
|
||||
private val source: SourceFetchResult,
|
||||
private val options: Options,
|
||||
private val exceptionLogger: ((Exception) -> Unit)? = null,
|
||||
) : Decoder {
|
||||
|
||||
private val coder = Coder()
|
||||
|
||||
override suspend fun decode(): DecodeResult? = runInterruptible {
|
||||
try {
|
||||
// ColorSpace is preferred to be ignored due to lib is trying to handle all color profile by itself
|
||||
val sourceData = source.source.source().readByteArray()
|
||||
|
||||
var mPreferredColorConfig: PreferredColorConfig = when (options.bitmapConfig) {
|
||||
Bitmap.Config.ALPHA_8 -> PreferredColorConfig.RGBA_8888
|
||||
Bitmap.Config.RGB_565 -> if (options.allowRgb565) PreferredColorConfig.RGB_565 else PreferredColorConfig.DEFAULT
|
||||
Bitmap.Config.ARGB_8888 -> PreferredColorConfig.RGBA_8888
|
||||
else -> {
|
||||
if (options.allowHardware) {
|
||||
PreferredColorConfig.DEFAULT
|
||||
} else {
|
||||
PreferredColorConfig.RGBA_8888
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.bitmapConfig == Bitmap.Config.RGBA_F16) {
|
||||
mPreferredColorConfig = PreferredColorConfig.RGBA_F16
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.bitmapConfig == Bitmap.Config.HARDWARE) {
|
||||
mPreferredColorConfig = PreferredColorConfig.HARDWARE
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && options.bitmapConfig == Bitmap.Config.RGBA_1010102) {
|
||||
mPreferredColorConfig = PreferredColorConfig.RGBA_1010102
|
||||
}
|
||||
|
||||
if (options.size == Size.ORIGINAL) {
|
||||
val originalImage = coder.decode(
|
||||
byteArray = sourceData,
|
||||
preferredColorConfig = mPreferredColorConfig
|
||||
)
|
||||
|
||||
return@runInterruptible DecodeResult(
|
||||
image = originalImage.asImage(),
|
||||
isSampled = false
|
||||
)
|
||||
}
|
||||
|
||||
val dstWidth = options.size.width.pxOrElse { 0 }
|
||||
val dstHeight = options.size.height.pxOrElse { 0 }
|
||||
val scaleMode = when (options.scale) {
|
||||
Scale.FILL -> ScaleMode.FILL
|
||||
Scale.FIT -> ScaleMode.FIT
|
||||
}
|
||||
|
||||
val originalImage = coder.decodeSampled(
|
||||
byteArray = sourceData,
|
||||
scaledWidth = dstWidth,
|
||||
scaledHeight = dstHeight,
|
||||
preferredColorConfig = mPreferredColorConfig,
|
||||
scaleMode = scaleMode,
|
||||
)
|
||||
|
||||
return@runInterruptible DecodeResult(
|
||||
image = originalImage.asImage(),
|
||||
isSampled = true
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.makeLog("HeifDecoder")
|
||||
exceptionLogger?.invoke(e)
|
||||
return@runInterruptible null
|
||||
}
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
override fun create(
|
||||
result: SourceFetchResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Decoder? {
|
||||
return if (
|
||||
AVAILABLE_BRANDS.any {
|
||||
result.source.source().rangeEquals(4, it)
|
||||
}
|
||||
) {
|
||||
HeifDecoder(
|
||||
source = result,
|
||||
options = options
|
||||
)
|
||||
} else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val MIF = "ftypmif1".encodeUtf8()
|
||||
private val MSF = "ftypmsf1".encodeUtf8()
|
||||
private val HEIC = "ftypheic".encodeUtf8()
|
||||
private val HEIX = "ftypheix".encodeUtf8()
|
||||
private val HEVC = "ftyphevc".encodeUtf8()
|
||||
private val HEVX = "ftyphevx".encodeUtf8()
|
||||
private val AVIF = "ftypavif".encodeUtf8()
|
||||
private val AVIS = "ftypavis".encodeUtf8()
|
||||
|
||||
private val AVAILABLE_BRANDS = listOf(MIF, MSF, HEIC, HEIX, HEVC, HEVX, AVIF, AVIS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import coil3.memory.MemoryCache
|
||||
|
||||
fun MemoryCache.remove(key: String) = keys.filter {
|
||||
it.key == key
|
||||
}.ifEmpty {
|
||||
listOf(MemoryCache.Key(key))
|
||||
}.all(::remove)
|
||||
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
@file:Suppress("MagicNumber", "ReturnCount")
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.BitmapFactoryDecoder
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.toBitmap
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.util.Locale
|
||||
|
||||
internal class NefDecoder private constructor(
|
||||
private val source: ImageSource,
|
||||
private val options: Options
|
||||
) : Decoder {
|
||||
|
||||
override suspend fun decode(): DecodeResult? {
|
||||
val preview = runCatching {
|
||||
NefPreviewExtractor(
|
||||
file = source.file().toFile()
|
||||
).extract()
|
||||
}.getOrNull() ?: return null
|
||||
|
||||
val result = BitmapFactoryDecoder(
|
||||
source = ImageSource(
|
||||
source = Buffer().write(preview.bytes),
|
||||
fileSystem = options.fileSystem
|
||||
),
|
||||
options = options
|
||||
).decode()
|
||||
|
||||
return result.applyOrientation(preview.orientation)
|
||||
}
|
||||
|
||||
private fun DecodeResult.applyOrientation(
|
||||
orientation: Int
|
||||
): DecodeResult {
|
||||
if (orientation == ORIENTATION_NORMAL) return this
|
||||
|
||||
val bitmap = image.toBitmap()
|
||||
val matrix = Matrix().applyTiffOrientation(orientation) ?: return this
|
||||
val rotated = Bitmap.createBitmap(
|
||||
bitmap,
|
||||
0,
|
||||
0,
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
matrix,
|
||||
true
|
||||
)
|
||||
|
||||
if (rotated != bitmap) bitmap.recycle()
|
||||
|
||||
return DecodeResult(
|
||||
image = rotated.asImage(),
|
||||
isSampled = isSampled
|
||||
)
|
||||
}
|
||||
|
||||
private fun Matrix.applyTiffOrientation(
|
||||
orientation: Int
|
||||
): Matrix? {
|
||||
val transformation = TIFF_ORIENTATION_TRANSFORMS[orientation] ?: return null
|
||||
transformation(this)
|
||||
return this
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
override fun create(
|
||||
result: SourceFetchResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Decoder? {
|
||||
return if (isTiff(result.source.source())) {
|
||||
NefDecoder(
|
||||
source = result.source,
|
||||
options = options
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTiff(source: BufferedSource): Boolean {
|
||||
val littleEndianMagic = byteArrayOf(0x49, 0x49, 0x2a, 0x00)
|
||||
val bigEndianMagic = byteArrayOf(0x4d, 0x4d, 0x00, 0x2a)
|
||||
|
||||
return source.rangeEquals(0, littleEndianMagic.toByteString()) ||
|
||||
source.rangeEquals(0, bigEndianMagic.toByteString())
|
||||
}
|
||||
}
|
||||
|
||||
private class NefPreviewExtractor(
|
||||
private val file: File
|
||||
) {
|
||||
fun extract(): PreviewData? = RandomAccessFile(file, "r").use { raf ->
|
||||
val reader = TiffReader.create(raf) ?: return null
|
||||
val rootDirectory = reader.readDirectory(reader.firstDirectoryOffset) ?: return null
|
||||
val make = rootDirectory.asciiValue(TAG_MAKE, reader)
|
||||
?.uppercase(Locale.ROOT)
|
||||
.orEmpty()
|
||||
|
||||
if (!make.contains("NIKON")) return null
|
||||
|
||||
val subIfdOffsets = rootDirectory.longValues(TAG_SUB_IFD, reader)
|
||||
if (subIfdOffsets.isEmpty()) return null
|
||||
|
||||
val subIfds = subIfdOffsets.mapNotNull(reader::readDirectory)
|
||||
if (!subIfds.any { it.isNikonRawImage(reader) }) return null
|
||||
|
||||
val preview = subIfds
|
||||
.mapNotNull { it.jpegPreview(reader) }
|
||||
.filter(reader::isJpeg)
|
||||
.maxByOrNull(JpegPreview::length)
|
||||
?: return null
|
||||
|
||||
val bytes = reader.readBytes(
|
||||
offset = preview.offset,
|
||||
byteCount = preview.length
|
||||
) ?: return null
|
||||
|
||||
PreviewData(
|
||||
bytes = bytes,
|
||||
orientation = rootDirectory.longValue(TAG_ORIENTATION, reader)?.toInt()
|
||||
?: ORIENTATION_NORMAL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class TiffReader private constructor(
|
||||
private val raf: RandomAccessFile,
|
||||
private val byteOrder: ByteOrder,
|
||||
val firstDirectoryOffset: Long
|
||||
) {
|
||||
|
||||
fun readDirectory(offset: Long): TiffDirectory? {
|
||||
if (offset <= 0 || offset >= raf.length()) return null
|
||||
|
||||
raf.seek(offset)
|
||||
val entryCount = readUShort()
|
||||
if (entryCount > MAX_IFD_ENTRIES) return null
|
||||
|
||||
val entries = buildMap {
|
||||
repeat(entryCount) {
|
||||
val bytes = ByteArray(IFD_ENTRY_SIZE)
|
||||
raf.readFully(bytes)
|
||||
|
||||
val entry = TiffEntry(
|
||||
tag = readUShort(bytes, offset = 0),
|
||||
type = readUShort(bytes, offset = 2),
|
||||
count = readUInt(bytes, offset = 4),
|
||||
valueOrOffset = readUInt(bytes, offset = 8),
|
||||
inlineValue = bytes.copyOfRange(8, 12)
|
||||
)
|
||||
put(entry.tag, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return TiffDirectory(entries)
|
||||
}
|
||||
|
||||
fun values(entry: TiffEntry): ByteArray? {
|
||||
val typeSize = TYPE_SIZES[entry.type] ?: return null
|
||||
val byteCount = entry.count * typeSize
|
||||
if (byteCount <= 0 || byteCount > Int.MAX_VALUE) return null
|
||||
|
||||
if (byteCount <= INLINE_VALUE_SIZE) {
|
||||
return entry.inlineValue.copyOf(byteCount.toInt())
|
||||
}
|
||||
|
||||
return readBytes(
|
||||
offset = entry.valueOrOffset,
|
||||
byteCount = byteCount
|
||||
)
|
||||
}
|
||||
|
||||
fun readBytes(
|
||||
offset: Long,
|
||||
byteCount: Long
|
||||
): ByteArray? {
|
||||
if (offset < 0 || byteCount <= 0 || byteCount > Int.MAX_VALUE) return null
|
||||
if (offset + byteCount > raf.length()) return null
|
||||
|
||||
val bytes = ByteArray(byteCount.toInt())
|
||||
raf.seek(offset)
|
||||
raf.readFully(bytes)
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun isJpeg(preview: JpegPreview): Boolean {
|
||||
val bytes = readBytes(
|
||||
offset = preview.offset,
|
||||
byteCount = JPEG_SIGNATURE_SIZE
|
||||
) ?: return false
|
||||
|
||||
return bytes[0] == JPEG_START_BYTE_0 && bytes[1] == JPEG_START_BYTE_1
|
||||
}
|
||||
|
||||
fun readUShort(bytes: ByteArray, offset: Int): Int {
|
||||
val first = bytes[offset].toInt() and 0xff
|
||||
val second = bytes[offset + 1].toInt() and 0xff
|
||||
|
||||
return when (byteOrder) {
|
||||
ByteOrder.LittleEndian -> first or (second shl 8)
|
||||
ByteOrder.BigEndian -> (first shl 8) or second
|
||||
}
|
||||
}
|
||||
|
||||
fun readUInt(bytes: ByteArray, offset: Int): Long {
|
||||
val first = bytes[offset].toLong() and 0xff
|
||||
val second = bytes[offset + 1].toLong() and 0xff
|
||||
val third = bytes[offset + 2].toLong() and 0xff
|
||||
val fourth = bytes[offset + 3].toLong() and 0xff
|
||||
|
||||
return when (byteOrder) {
|
||||
ByteOrder.LittleEndian -> first or (second shl 8) or (third shl 16) or (fourth shl 24)
|
||||
ByteOrder.BigEndian -> (first shl 24) or (second shl 16) or (third shl 8) or fourth
|
||||
}
|
||||
}
|
||||
|
||||
private fun readUShort(): Int {
|
||||
val bytes = ByteArray(2)
|
||||
raf.readFully(bytes)
|
||||
return readUShort(bytes, offset = 0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(raf: RandomAccessFile): TiffReader? {
|
||||
if (raf.length() < TIFF_HEADER_SIZE) return null
|
||||
|
||||
raf.seek(0)
|
||||
val header = ByteArray(TIFF_HEADER_SIZE)
|
||||
raf.readFully(header)
|
||||
|
||||
val byteOrder = when {
|
||||
header[0] == LITTLE_ENDIAN_BYTE && header[1] == LITTLE_ENDIAN_BYTE -> {
|
||||
ByteOrder.LittleEndian
|
||||
}
|
||||
|
||||
header[0] == BIG_ENDIAN_BYTE && header[1] == BIG_ENDIAN_BYTE -> {
|
||||
ByteOrder.BigEndian
|
||||
}
|
||||
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val reader = TiffReader(
|
||||
raf = raf,
|
||||
byteOrder = byteOrder,
|
||||
firstDirectoryOffset = 0
|
||||
)
|
||||
val magic = reader.readUShort(header, offset = 2)
|
||||
if (magic != TIFF_MAGIC) return null
|
||||
|
||||
return TiffReader(
|
||||
raf = raf,
|
||||
byteOrder = byteOrder,
|
||||
firstDirectoryOffset = reader.readUInt(header, offset = 4)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class TiffDirectory(
|
||||
private val entries: Map<Int, TiffEntry>
|
||||
) {
|
||||
|
||||
fun longValue(
|
||||
tag: Int,
|
||||
reader: TiffReader
|
||||
): Long? = longValues(tag, reader).firstOrNull()
|
||||
|
||||
fun longValues(
|
||||
tag: Int,
|
||||
reader: TiffReader
|
||||
): List<Long> {
|
||||
val entry = entries[tag] ?: return emptyList()
|
||||
val bytes = reader.values(entry) ?: return emptyList()
|
||||
val count = entry.count.toIntOrNull() ?: return emptyList()
|
||||
|
||||
return when (entry.type) {
|
||||
TYPE_BYTE -> List(count) { index ->
|
||||
bytes[index].toLong() and 0xff
|
||||
}
|
||||
|
||||
TYPE_SHORT -> List(count) { index ->
|
||||
reader.readUShort(bytes, index * SHORT_SIZE).toLong()
|
||||
}
|
||||
|
||||
TYPE_LONG -> List(count) { index ->
|
||||
reader.readUInt(bytes, index * LONG_SIZE)
|
||||
}
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun asciiValue(
|
||||
tag: Int,
|
||||
reader: TiffReader
|
||||
): String? {
|
||||
val entry = entries[tag] ?: return null
|
||||
if (entry.type != TYPE_ASCII) return null
|
||||
|
||||
val bytes = reader.values(entry) ?: return null
|
||||
return String(bytes, Charsets.US_ASCII).substringBefore(NULL_CHAR)
|
||||
}
|
||||
|
||||
fun isNikonRawImage(reader: TiffReader): Boolean {
|
||||
val compression = longValue(TAG_COMPRESSION, reader)
|
||||
val photometric = longValue(TAG_PHOTOMETRIC, reader)
|
||||
val bitsPerSample = longValue(TAG_BITS_PER_SAMPLE, reader)
|
||||
|
||||
return compression == COMPRESSION_NIKON_RAW ||
|
||||
(photometric == PHOTOMETRIC_CFA && bitsPerSample != null && bitsPerSample > 8)
|
||||
}
|
||||
|
||||
fun jpegPreview(reader: TiffReader): JpegPreview? {
|
||||
val offset = longValue(TAG_JPEG_INTERCHANGE_FORMAT, reader) ?: return null
|
||||
val length = longValue(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, reader) ?: return null
|
||||
|
||||
if (length <= 0) return null
|
||||
return JpegPreview(
|
||||
offset = offset,
|
||||
length = length
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class TiffEntry(
|
||||
val tag: Int,
|
||||
val type: Int,
|
||||
val count: Long,
|
||||
val valueOrOffset: Long,
|
||||
val inlineValue: ByteArray
|
||||
)
|
||||
|
||||
private data class JpegPreview(
|
||||
val offset: Long,
|
||||
val length: Long
|
||||
)
|
||||
|
||||
private class PreviewData(
|
||||
val bytes: ByteArray,
|
||||
val orientation: Int
|
||||
)
|
||||
|
||||
private enum class ByteOrder {
|
||||
LittleEndian,
|
||||
BigEndian
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TIFF_HEADER_SIZE = 8
|
||||
private const val TIFF_MAGIC = 42
|
||||
private const val IFD_ENTRY_SIZE = 12
|
||||
private const val INLINE_VALUE_SIZE = 4
|
||||
private const val MAX_IFD_ENTRIES = 512
|
||||
private const val SHORT_SIZE = 2
|
||||
private const val LONG_SIZE = 4
|
||||
private const val JPEG_SIGNATURE_SIZE = 2L
|
||||
|
||||
private const val LITTLE_ENDIAN_BYTE = 0x49.toByte()
|
||||
private const val BIG_ENDIAN_BYTE = 0x4d.toByte()
|
||||
private const val JPEG_START_BYTE_0 = 0xff.toByte()
|
||||
private const val JPEG_START_BYTE_1 = 0xd8.toByte()
|
||||
private const val NULL_CHAR = '\u0000'
|
||||
|
||||
private const val TYPE_BYTE = 1
|
||||
private const val TYPE_ASCII = 2
|
||||
private const val TYPE_SHORT = 3
|
||||
private const val TYPE_LONG = 4
|
||||
|
||||
private const val TAG_SUB_IFD = 330
|
||||
private const val TAG_MAKE = 271
|
||||
private const val TAG_ORIENTATION = 274
|
||||
private const val TAG_BITS_PER_SAMPLE = 258
|
||||
private const val TAG_COMPRESSION = 259
|
||||
private const val TAG_PHOTOMETRIC = 262
|
||||
private const val TAG_JPEG_INTERCHANGE_FORMAT = 513
|
||||
private const val TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514
|
||||
|
||||
private const val COMPRESSION_NIKON_RAW = 34713L
|
||||
private const val PHOTOMETRIC_CFA = 32803L
|
||||
|
||||
private const val ORIENTATION_NORMAL = 1
|
||||
private const val ORIENTATION_FLIP_HORIZONTAL = 2
|
||||
private const val ORIENTATION_ROTATE_180 = 3
|
||||
private const val ORIENTATION_FLIP_VERTICAL = 4
|
||||
private const val ORIENTATION_TRANSPOSE = 5
|
||||
private const val ORIENTATION_ROTATE_90 = 6
|
||||
private const val ORIENTATION_TRANSVERSE = 7
|
||||
private const val ORIENTATION_ROTATE_270 = 8
|
||||
|
||||
private val TIFF_ORIENTATION_TRANSFORMS = mapOf<Int, Matrix.() -> Unit>(
|
||||
ORIENTATION_FLIP_HORIZONTAL to {
|
||||
postScale(-1f, 1f)
|
||||
},
|
||||
ORIENTATION_ROTATE_180 to {
|
||||
postRotate(180f)
|
||||
},
|
||||
ORIENTATION_FLIP_VERTICAL to {
|
||||
postScale(1f, -1f)
|
||||
},
|
||||
ORIENTATION_TRANSPOSE to {
|
||||
postRotate(90f)
|
||||
postScale(1f, -1f)
|
||||
},
|
||||
ORIENTATION_ROTATE_90 to {
|
||||
postRotate(90f)
|
||||
},
|
||||
ORIENTATION_TRANSVERSE to {
|
||||
postRotate(90f)
|
||||
postScale(-1f, 1f)
|
||||
},
|
||||
ORIENTATION_ROTATE_270 to {
|
||||
postRotate(270f)
|
||||
}
|
||||
)
|
||||
|
||||
private val TYPE_SIZES = mapOf(
|
||||
TYPE_BYTE to 1L,
|
||||
TYPE_ASCII to 1L,
|
||||
TYPE_SHORT to SHORT_SIZE.toLong(),
|
||||
TYPE_LONG to LONG_SIZE.toLong()
|
||||
)
|
||||
|
||||
private fun Long.toIntOrNull(): Int? = takeIf {
|
||||
it in 0..Int.MAX_VALUE.toLong()
|
||||
}?.toInt()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import coil3.map.Mapper
|
||||
import coil3.request.Options
|
||||
import coil3.toUri
|
||||
import com.t8rin.imagetoolbox.core.utils.UriReplacements
|
||||
|
||||
internal object OriginalUriMapper : Mapper<Any, Any> {
|
||||
override fun map(
|
||||
data: Any,
|
||||
options: Options
|
||||
): Any? = when (data) {
|
||||
is String -> UriReplacements.resolve(data).takeIf { it != data }
|
||||
is android.net.Uri -> UriReplacements.resolve(data).takeIf { it != data }
|
||||
is coil3.Uri -> UriReplacements.resolve(data.toString())
|
||||
.takeIf { it != data.toString() }
|
||||
?.toUri()
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
@file:Suppress("FunctionName", "unused")
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.core.graphics.createBitmap
|
||||
import coil3.Extras
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.getExtra
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.Options
|
||||
import coil3.size.Size
|
||||
import coil3.size.pxOrElse
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.domain.model.flexibleResize
|
||||
import com.t8rin.imagetoolbox.core.utils.appContext
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
internal class PdfDecoder(
|
||||
private val source: ImageSource,
|
||||
private val options: Options,
|
||||
) : Decoder {
|
||||
|
||||
override suspend fun decode(): DecodeResult {
|
||||
val file = source.file().toFile()
|
||||
|
||||
val image = ParcelFileDescriptor.open(
|
||||
file,
|
||||
ParcelFileDescriptor.MODE_READ_ONLY
|
||||
).use { fileDescriptor ->
|
||||
PdfRenderer(fileDescriptor).use { renderer ->
|
||||
val pageIndex = options.pdfPage.coerceIn(0, renderer.pageCount - 1)
|
||||
|
||||
renderer.openPage(pageIndex).use { page ->
|
||||
val originalWidth = page.width
|
||||
val originalHeight = page.height
|
||||
|
||||
val targetSize = IntegerSize(
|
||||
width = originalWidth,
|
||||
height = originalHeight
|
||||
).flexibleResize(
|
||||
w = options.size.width.pxOrElse { 0 },
|
||||
h = options.size.height.pxOrElse { 0 }
|
||||
)
|
||||
|
||||
val scaleX = targetSize.width.toFloat() / originalWidth
|
||||
val scaleY = targetSize.height.toFloat() / originalHeight
|
||||
val scale = minOf(scaleX, scaleY).coerceAtMost(2f)
|
||||
|
||||
val bitmap = createBitmap(
|
||||
(originalWidth * scale).roundToInt().coerceAtLeast(1),
|
||||
(originalHeight * scale).roundToInt().coerceAtLeast(1)
|
||||
).apply {
|
||||
eraseColor(Color.WHITE)
|
||||
}
|
||||
|
||||
page.render(
|
||||
bitmap,
|
||||
null,
|
||||
Matrix().apply { setScale(scale, scale) },
|
||||
PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
|
||||
)
|
||||
|
||||
bitmap.asImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DecodeResult(
|
||||
image = image,
|
||||
isSampled = true
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
override fun create(
|
||||
result: SourceFetchResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Decoder? {
|
||||
return if (isPdf(result)) {
|
||||
PdfDecoder(
|
||||
source = result.source,
|
||||
options = options
|
||||
)
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun isPdf(result: SourceFetchResult): Boolean {
|
||||
val pdfMagic = byteArrayOf(0x25, 0x50, 0x44, 0x46).toByteString()
|
||||
|
||||
return result.source.source()
|
||||
.rangeEquals(0, pdfMagic) || result.mimeType == "application/pdf"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun PdfImageRequest(
|
||||
data: Any?,
|
||||
pdfPage: Int = 0,
|
||||
size: Size? = null
|
||||
): ImageRequest = ImageRequest.Builder(appContext)
|
||||
.data(data)
|
||||
.pdfPage(pdfPage)
|
||||
.memoryCacheKey(data.toString() + pdfPage)
|
||||
.diskCacheKey(data.toString() + pdfPage)
|
||||
.apply {
|
||||
size?.let(::size)
|
||||
}
|
||||
.build()
|
||||
|
||||
fun ImageRequest.Builder.pdfPage(pdfPage: Int) = apply {
|
||||
extras[pdfPageKey] = pdfPage
|
||||
}
|
||||
|
||||
val ImageRequest.pdfPage: Int
|
||||
get() = getExtra(pdfPageKey)
|
||||
|
||||
val Options.pdfPage: Int
|
||||
get() = getExtra(pdfPageKey)
|
||||
|
||||
val Extras.Key.Companion.pdfPage: Extras.Key<Int>
|
||||
get() = pdfPageKey
|
||||
|
||||
private val pdfPageKey = Extras.Key(default = 0)
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.DecodeUtils
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.size.Size
|
||||
import coil3.size.pxOrElse
|
||||
import coil3.svg.SvgDecoder
|
||||
import coil3.svg.isSvg
|
||||
import com.hashsequence.coilresvg.ResvgDecoder
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
|
||||
internal class SvgDecoderCompat(
|
||||
private val source: ImageSource,
|
||||
options: Options,
|
||||
minimumSize: Int? = null
|
||||
) : Decoder {
|
||||
|
||||
private val options = minimumSize?.let {
|
||||
options.copy(
|
||||
size = options.size.coerceAtLeast(minimumSize)
|
||||
)
|
||||
} ?: options
|
||||
|
||||
private fun source() = ImageSource(
|
||||
file = source.file(),
|
||||
fileSystem = source.fileSystem,
|
||||
metadata = source.metadata
|
||||
)
|
||||
|
||||
override suspend fun decode(): DecodeResult {
|
||||
if (!isResvgAvailable) return decodeDefault()
|
||||
|
||||
return runSuspendCatching {
|
||||
decodeNative()
|
||||
}.onFailure {
|
||||
if (it is LinkageError) isResvgAvailable = false
|
||||
}.getOrNull() ?: decodeDefault()
|
||||
}
|
||||
|
||||
private suspend fun decodeNative() = ResvgDecoder(
|
||||
source = source(),
|
||||
options = options
|
||||
).decode()
|
||||
|
||||
private suspend fun decodeDefault() = SvgDecoder(
|
||||
source = source(),
|
||||
options = options
|
||||
).decode().also {
|
||||
"fallback coil-svg decoder".makeLog()
|
||||
}
|
||||
|
||||
private fun Size.coerceAtLeast(size: Int): Size = Size(
|
||||
width = width.pxOrElse { 0 }.coerceAtLeast(size),
|
||||
height = height.pxOrElse { 0 }.coerceAtLeast(size)
|
||||
)
|
||||
|
||||
class Factory(
|
||||
private val minimumSize: Int? = null
|
||||
) : Decoder.Factory {
|
||||
|
||||
override fun create(
|
||||
result: SourceFetchResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Decoder? {
|
||||
if (!isApplicable(result)) return null
|
||||
|
||||
return SvgDecoderCompat(
|
||||
source = result.source,
|
||||
options = options,
|
||||
minimumSize = minimumSize
|
||||
)
|
||||
}
|
||||
|
||||
private fun isApplicable(result: SourceFetchResult): Boolean {
|
||||
return result.mimeType == MIME_TYPE_SVG ||
|
||||
result.mimeType == MIME_TYPE_XML ||
|
||||
DecodeUtils.isSvg(result.source.source())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val MIME_TYPE_SVG = "image/svg+xml"
|
||||
private const val MIME_TYPE_XML = "text/xml"
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
@Volatile
|
||||
private var isResvgAvailable = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.request.bitmapConfig
|
||||
import coil3.size.Size
|
||||
import coil3.size.pxOrElse
|
||||
import okio.BufferedSource
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||
import oupson.apng.utils.Utils.flexibleResize
|
||||
|
||||
internal class TiffDecoder private constructor(
|
||||
private val source: ImageSource,
|
||||
private val options: Options
|
||||
) : Decoder {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun decode(): DecodeResult? {
|
||||
val config = options.bitmapConfig.takeIf {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
it != Bitmap.Config.HARDWARE
|
||||
} else true
|
||||
} ?: Bitmap.Config.ARGB_8888
|
||||
|
||||
val decoded = runCatching {
|
||||
TiffBitmapFactory.decodeFile(
|
||||
source.file().toFile()
|
||||
)
|
||||
}.getOrNull() ?: return null
|
||||
|
||||
val image = decoded
|
||||
.createScaledBitmap(options.size)
|
||||
.copy(config, false)
|
||||
.asImage()
|
||||
|
||||
return DecodeResult(
|
||||
image = image,
|
||||
isSampled = options.size != Size.ORIGINAL
|
||||
)
|
||||
}
|
||||
|
||||
private fun Bitmap.createScaledBitmap(
|
||||
size: Size
|
||||
): Bitmap {
|
||||
if (size == Size.ORIGINAL) return this
|
||||
|
||||
return flexibleResize(
|
||||
maxOf(
|
||||
size.width.pxOrElse { 1 },
|
||||
size.height.pxOrElse { 1 }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
override fun create(
|
||||
result: SourceFetchResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Decoder? {
|
||||
return if (isTiff(result.source.source())) {
|
||||
TiffDecoder(
|
||||
source = result.source,
|
||||
options = options
|
||||
)
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun isTiff(source: BufferedSource): Boolean {
|
||||
val magic1 = byteArrayOf(0x49, 0x49, 0x2a, 0x00)
|
||||
val magic2 = byteArrayOf(0x4d, 0x4d, 0x00, 0x2a)
|
||||
val cr2Magic = byteArrayOf(0x49, 0x49, 0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52)
|
||||
|
||||
if (source.rangeEquals(0, cr2Magic.toByteString())) return false
|
||||
if (source.rangeEquals(0, magic1.toByteString())) return true
|
||||
return source.rangeEquals(0, magic2.toByteString())
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.transformations
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
|
||||
internal object TimeMeasureInterceptor : Interceptor {
|
||||
|
||||
override suspend fun intercept(
|
||||
chain: Interceptor.Chain
|
||||
): ImageResult {
|
||||
val time = System.currentTimeMillis()
|
||||
val result = chain.proceed()
|
||||
val endTime = System.currentTimeMillis()
|
||||
|
||||
val delta = endTime - time
|
||||
|
||||
val transformations = chain.request.transformations.joinToString(", ") {
|
||||
it.toString()
|
||||
}
|
||||
if (transformations.isNotEmpty()) {
|
||||
"Time $delta ms for transformations = $transformations, with ${result.request.sizeResolver.size()}".makeLog(
|
||||
"RealImageLoader"
|
||||
)
|
||||
}
|
||||
"Time $delta ms for ${chain.size}".makeLog("RealImageLoader")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coil
|
||||
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.size.Scale
|
||||
import coil3.size.Size
|
||||
import coil3.size.pxOrElse
|
||||
import com.t8rin.trickle.VvcDecoder
|
||||
import com.t8rin.trickle.VvcScaleMode
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
import okio.ByteString.Companion.toByteString
|
||||
|
||||
class VVCDecoder(
|
||||
private val source: SourceFetchResult,
|
||||
private val options: Options,
|
||||
private val exceptionLogger: ((Exception) -> Unit)? = null
|
||||
) : Decoder {
|
||||
|
||||
override suspend fun decode(): DecodeResult? = runInterruptible {
|
||||
try {
|
||||
val sourceData = source.source.source().readByteArray()
|
||||
|
||||
if (options.size == Size.ORIGINAL) {
|
||||
return@runInterruptible DecodeResult(
|
||||
image = VvcDecoder.decode(sourceData).asImage(),
|
||||
isSampled = false
|
||||
)
|
||||
}
|
||||
|
||||
val width = options.size.width.pxOrElse { 0 }
|
||||
val height = options.size.height.pxOrElse { 0 }
|
||||
val scaleMode = when (options.scale) {
|
||||
Scale.FILL -> VvcScaleMode.FILL
|
||||
Scale.FIT -> VvcScaleMode.FIT
|
||||
}
|
||||
|
||||
DecodeResult(
|
||||
image = VvcDecoder.decodeSampled(
|
||||
encoded = sourceData,
|
||||
scaledWidth = width,
|
||||
scaledHeight = height,
|
||||
scaleMode = scaleMode
|
||||
).asImage(),
|
||||
isSampled = true
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
exceptionLogger?.invoke(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
override fun create(
|
||||
result: SourceFetchResult,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Decoder? {
|
||||
val source = result.source.source()
|
||||
val isRawVvc = RAW_VVC_START_CODES.any { source.rangeEquals(0, it) }
|
||||
val isVvcInHeif = source.rangeEquals(4, FTYP) &&
|
||||
source.indexOf(VVC_ITEM_TYPE, 0, SEARCH_LIMIT) >= 0
|
||||
|
||||
return if (isRawVvc || isVvcInHeif) {
|
||||
VVCDecoder(
|
||||
source = result,
|
||||
options = options
|
||||
)
|
||||
} else null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SEARCH_LIMIT = 64L * 1024L
|
||||
val FTYP = "ftyp".encodeUtf8()
|
||||
val VVC_ITEM_TYPE = "vvc1".encodeUtf8()
|
||||
val RAW_VVC_START_CODES = listOf(
|
||||
byteArrayOf(0, 0, 1).toByteString(),
|
||||
byteArrayOf(0, 0, 0, 1).toByteString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coroutines
|
||||
|
||||
import com.t8rin.imagetoolbox.core.di.DecodingDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.DefaultDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.EncodingDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.IoDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.UiDispatcher
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal data class AndroidDispatchersHolder @Inject constructor(
|
||||
@UiDispatcher override val uiDispatcher: CoroutineContext,
|
||||
@IoDispatcher override val ioDispatcher: CoroutineContext,
|
||||
@EncodingDispatcher override val encodingDispatcher: CoroutineContext,
|
||||
@DecodingDispatcher override val decodingDispatcher: CoroutineContext,
|
||||
@DefaultDispatcher override val defaultDispatcher: CoroutineContext
|
||||
) : DispatchersHolder
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.coroutines
|
||||
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.AppScope
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
internal class AppScopeImpl @Inject constructor(
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : AppScope, DispatchersHolder by dispatchersHolder {
|
||||
override val coroutineContext: CoroutineContext =
|
||||
defaultDispatcher + CoroutineExceptionHandler { _, e -> e.makeLog("AppScopeImpl") } + SupervisorJob()
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.di
|
||||
|
||||
import com.t8rin.imagetoolbox.core.data.coroutines.AndroidDispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.data.coroutines.AppScopeImpl
|
||||
import com.t8rin.imagetoolbox.core.data.utils.executorDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.DecodingDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.DefaultDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.EncodingDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.IoDispatcher
|
||||
import com.t8rin.imagetoolbox.core.di.UiDispatcher
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.AppScope
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface CoroutinesModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun dispatchersHolder(
|
||||
dispatchers: AndroidDispatchersHolder
|
||||
): DispatchersHolder
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun appScope(
|
||||
impl: AppScopeImpl
|
||||
): AppScope
|
||||
|
||||
companion object {
|
||||
|
||||
@DefaultDispatcher
|
||||
@Singleton
|
||||
@Provides
|
||||
fun defaultDispatcher(): CoroutineContext = executorDispatcher {
|
||||
Executors.newCachedThreadPool()
|
||||
}
|
||||
|
||||
@DecodingDispatcher
|
||||
@Singleton
|
||||
@Provides
|
||||
fun decodingDispatcher(): CoroutineContext = executorDispatcher {
|
||||
Executors.newFixedThreadPool(
|
||||
2 * Runtime.getRuntime().availableProcessors() + 1
|
||||
)
|
||||
}
|
||||
|
||||
@EncodingDispatcher
|
||||
@Singleton
|
||||
@Provides
|
||||
fun encodingDispatcher(): CoroutineContext = executorDispatcher {
|
||||
Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
@IoDispatcher
|
||||
@Singleton
|
||||
@Provides
|
||||
fun ioDispatcher(): CoroutineContext = Dispatchers.IO
|
||||
|
||||
@UiDispatcher
|
||||
@Singleton
|
||||
@Provides
|
||||
fun uiDispatcher(): CoroutineContext = Dispatchers.Main.immediate
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.di
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import coil3.ComponentRegistry
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.disk.directory
|
||||
import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.gif.GifDecoder
|
||||
import coil3.imageLoader
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.network.DeDupeConcurrentRequestStrategy
|
||||
import coil3.network.ktor3.KtorNetworkFetcherFactory
|
||||
import coil3.request.allowHardware
|
||||
import coil3.request.maxBitmapSize
|
||||
import coil3.size.Size
|
||||
import coil3.util.Logger
|
||||
import com.awxkee.jxlcoder.coil.AnimatedJxlDecoder
|
||||
import com.gemalto.jp2.coil.Jpeg2000Decoder
|
||||
import com.t8rin.awebp.coil.AnimatedWebPDecoder
|
||||
import com.t8rin.djvu_coder.coil.DjvuDecoder
|
||||
import com.t8rin.imagetoolbox.core.data.coil.Base64Fetcher
|
||||
import com.t8rin.imagetoolbox.core.data.coil.CoilLogger
|
||||
import com.t8rin.imagetoolbox.core.data.coil.HeifDecoder
|
||||
import com.t8rin.imagetoolbox.core.data.coil.NefDecoder
|
||||
import com.t8rin.imagetoolbox.core.data.coil.OriginalUriMapper
|
||||
import com.t8rin.imagetoolbox.core.data.coil.PdfDecoder
|
||||
import com.t8rin.imagetoolbox.core.data.coil.SvgDecoderCompat
|
||||
import com.t8rin.imagetoolbox.core.data.coil.TiffDecoder
|
||||
import com.t8rin.imagetoolbox.core.data.coil.TimeMeasureInterceptor
|
||||
import com.t8rin.imagetoolbox.core.data.coil.VVCDecoder
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.resources.BuildConfig
|
||||
import com.t8rin.psd.coil.PsdDecoder
|
||||
import com.t8rin.qoi_coder.coil.QoiDecoder
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.ktor.client.HttpClient
|
||||
import oupson.apng.coil.AnimatedPngDecoder
|
||||
import java.io.File
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object ImageLoaderModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideImageLoader(
|
||||
@ApplicationContext context: Context,
|
||||
logger: Logger?,
|
||||
componentRegistry: ComponentRegistry,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
): ImageLoader = context.imageLoader.newBuilder()
|
||||
.components(componentRegistry)
|
||||
.coroutineContext(dispatchersHolder.defaultDispatcher)
|
||||
.decoderCoroutineContext(dispatchersHolder.decodingDispatcher)
|
||||
.fetcherCoroutineContext(dispatchersHolder.ioDispatcher)
|
||||
.allowHardware(false)
|
||||
.maxBitmapSize(Size.ORIGINAL)
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(File(context.cacheDir, "coil").apply(File::mkdirs))
|
||||
.maxSizePercent(0.2)
|
||||
.cleanupCoroutineContext(dispatchersHolder.ioDispatcher)
|
||||
.build()
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder()
|
||||
.maxSizePercent(context, 0.3)
|
||||
.build()
|
||||
}
|
||||
.logger(logger)
|
||||
.build()
|
||||
.also(SingletonImageLoader::setUnsafe)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCoilLogger(): Logger = CoilLogger()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideComponentRegistry(
|
||||
client: HttpClient
|
||||
): ComponentRegistry = ComponentRegistry.Builder()
|
||||
.apply {
|
||||
add(OriginalUriMapper)
|
||||
add(
|
||||
KtorNetworkFetcherFactory(
|
||||
httpClient = client,
|
||||
concurrentRequestStrategy = DeDupeConcurrentRequestStrategy()
|
||||
)
|
||||
)
|
||||
add(AnimatedPngDecoder.Factory())
|
||||
if (Build.VERSION.SDK_INT >= 28) add(AnimatedImageDecoder.Factory())
|
||||
else {
|
||||
add(GifDecoder.Factory())
|
||||
add(AnimatedWebPDecoder.Factory())
|
||||
}
|
||||
add(SvgDecoderCompat.Factory())
|
||||
add(VVCDecoder.Factory())
|
||||
add(HeifDecoder.Factory())
|
||||
add(AnimatedJxlDecoder.Factory())
|
||||
add(Jpeg2000Decoder.Factory())
|
||||
add(NefDecoder.Factory())
|
||||
add(TiffDecoder.Factory())
|
||||
add(QoiDecoder.Factory())
|
||||
add(PsdDecoder.Factory())
|
||||
add(DjvuDecoder.Factory())
|
||||
add(Base64Fetcher.Factory())
|
||||
add(PdfDecoder.Factory())
|
||||
if (BuildConfig.DEBUG) add(TimeMeasureInterceptor)
|
||||
}
|
||||
.build()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.di
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.t8rin.imagetoolbox.core.data.image.AndroidImageCompressor
|
||||
import com.t8rin.imagetoolbox.core.data.image.AndroidImageGetter
|
||||
import com.t8rin.imagetoolbox.core.data.image.AndroidImagePreviewCreator
|
||||
import com.t8rin.imagetoolbox.core.data.image.AndroidImageScaler
|
||||
import com.t8rin.imagetoolbox.core.data.image.AndroidImageTransformer
|
||||
import com.t8rin.imagetoolbox.core.data.image.AndroidShareProvider
|
||||
import com.t8rin.imagetoolbox.core.data.image.ImageExportProfilesUseCaseImpl
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageCompressor
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageExportProfilesUseCase
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImagePreviewCreator
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageShareProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ShareProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ImageModule {
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideImageManager(
|
||||
transformer: AndroidImageTransformer
|
||||
): ImageTransformer<Bitmap>
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideImageScaler(
|
||||
scaler: AndroidImageScaler
|
||||
): ImageScaler<Bitmap>
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideImageCompressor(
|
||||
compressor: AndroidImageCompressor
|
||||
): ImageCompressor<Bitmap>
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideImageGetter(
|
||||
getter: AndroidImageGetter
|
||||
): ImageGetter<Bitmap>
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideImagePreviewCreator(
|
||||
creator: AndroidImagePreviewCreator
|
||||
): ImagePreviewCreator<Bitmap>
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideShareProvider(
|
||||
provider: AndroidShareProvider
|
||||
): ShareProvider
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideImageShareProvider(
|
||||
provider: AndroidShareProvider
|
||||
): ImageShareProvider<Bitmap>
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideImageExportProfilesUseCase(
|
||||
useCase: ImageExportProfilesUseCaseImpl
|
||||
): ImageExportProfilesUseCase
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.t8rin.imagetoolbox.core.data.json.ImageFormatJsonAdapter
|
||||
import com.t8rin.imagetoolbox.core.data.json.ImageScaleModeJsonAdapter
|
||||
import com.t8rin.imagetoolbox.core.data.json.MoshiParser
|
||||
import com.t8rin.imagetoolbox.core.data.json.PresetJsonAdapter
|
||||
import com.t8rin.imagetoolbox.core.data.json.ResizeTypeJsonAdapter
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.Quality
|
||||
import com.t8rin.imagetoolbox.core.domain.json.JsonParser
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.model.FilenameBehavior
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.model.ShapeType
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface JsonModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun parser(
|
||||
impl: MoshiParser,
|
||||
): JsonParser
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun moshi(): Moshi = Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Quality::class.java, "quality_type")
|
||||
.withSubtype(Quality.Jxl::class.java, "jxl")
|
||||
.withSubtype(Quality.Avif::class.java, "avif")
|
||||
.withSubtype(Quality.Heic::class.java, "heic")
|
||||
.withSubtype(Quality.Vvc::class.java, "vvc")
|
||||
.withSubtype(Quality.PngLossy::class.java, "png")
|
||||
.withSubtype(Quality.PngQuant::class.java, "pngquant")
|
||||
.withSubtype(Quality.Tiff::class.java, "tiff")
|
||||
.withSubtype(Quality.Base::class.java, "base")
|
||||
.withDefaultValue(Quality.Base())
|
||||
)
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(ShapeType::class.java, "shape_type")
|
||||
.withSubtype(ShapeType.Rounded::class.java, "rounded")
|
||||
.withSubtype(ShapeType.Cut::class.java, "cut")
|
||||
.withSubtype(ShapeType.Squircle::class.java, "squircle")
|
||||
.withSubtype(ShapeType.Smooth::class.java, "smooth")
|
||||
.withSubtype(ShapeType.Wavy::class.java, "wavy")
|
||||
.withSubtype(ShapeType.Scoop::class.java, "scoop")
|
||||
.withSubtype(ShapeType.Notch::class.java, "notch")
|
||||
.withDefaultValue(ShapeType.Rounded())
|
||||
)
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(FilenameBehavior::class.java, "filename_type")
|
||||
.withSubtype(FilenameBehavior.None::class.java, "none")
|
||||
.withSubtype(FilenameBehavior.Overwrite::class.java, "overwrite")
|
||||
.withSubtype(FilenameBehavior.Checksum::class.java, "checksum")
|
||||
.withSubtype(FilenameBehavior.Random::class.java, "random")
|
||||
.withDefaultValue(FilenameBehavior.None())
|
||||
)
|
||||
.add(ImageFormatJsonAdapter())
|
||||
.add(PresetJsonAdapter())
|
||||
.add(ResizeTypeJsonAdapter())
|
||||
.add(ImageScaleModeJsonAdapter())
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.t8rin.imagetoolbox.core.domain.GLOBAL_STORAGE_NAME
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object LocalModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun dataStore(
|
||||
@ApplicationContext context: Context
|
||||
): DataStore<Preferences> = PreferenceDataStoreFactory.create(
|
||||
produceFile = { context.preferencesDataStoreFile(GLOBAL_STORAGE_NAME) }
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.di
|
||||
|
||||
import com.t8rin.imagetoolbox.core.data.remote.AndroidDownloadManager
|
||||
import com.t8rin.imagetoolbox.core.data.remote.AndroidRemoteResourcesStore
|
||||
import com.t8rin.imagetoolbox.core.domain.remote.DownloadManager
|
||||
import com.t8rin.imagetoolbox.core.domain.remote.RemoteResourcesStore
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface RemoteModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun remoteResources(
|
||||
impl: AndroidRemoteResourcesStore
|
||||
): RemoteResourcesStore
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun downloadManager(
|
||||
impl: AndroidDownloadManager
|
||||
): DownloadManager
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun client(): HttpClient = HttpClient {
|
||||
install(HttpTimeout) {
|
||||
val timeout = 5.minutes.inWholeMilliseconds
|
||||
requestTimeoutMillis = timeout
|
||||
connectTimeoutMillis = timeout
|
||||
socketTimeoutMillis = timeout
|
||||
}
|
||||
|
||||
install(Logging) {
|
||||
logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
message.makeLog("Ktor")
|
||||
}
|
||||
}
|
||||
level = LogLevel.HEADERS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.di
|
||||
|
||||
import com.t8rin.imagetoolbox.core.data.resource.AndroidResourceManager
|
||||
import com.t8rin.imagetoolbox.core.domain.resource.ResourceManager
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ResourcesModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun resManager(
|
||||
impl: AndroidResourceManager
|
||||
): ResourceManager
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.di
|
||||
|
||||
import com.t8rin.imagetoolbox.core.data.history.AppHistoryRepositoryImpl
|
||||
import com.t8rin.imagetoolbox.core.data.saving.AndroidFileController
|
||||
import com.t8rin.imagetoolbox.core.data.saving.AndroidFilenameCreator
|
||||
import com.t8rin.imagetoolbox.core.data.saving.AndroidKeepAliveService
|
||||
import com.t8rin.imagetoolbox.core.data.saving.FileControllerEventEmitter
|
||||
import com.t8rin.imagetoolbox.core.data.saving.OriginalFileDeletionHelper
|
||||
import com.t8rin.imagetoolbox.core.domain.history.AppHistoryRepository
|
||||
import com.t8rin.imagetoolbox.core.domain.image.MetadataProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.FileController
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.FileController.Companion.toMetadataProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.FilenameCreator
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.KeepAliveService
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface SavingModule {
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideFileController(
|
||||
impl: AndroidFileController
|
||||
): FileController
|
||||
|
||||
@Binds
|
||||
fun provideFileControllerEventEmitter(
|
||||
impl: OriginalFileDeletionHelper
|
||||
): FileControllerEventEmitter
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun filenameCreator(
|
||||
impl: AndroidFilenameCreator
|
||||
): FilenameCreator
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun service(
|
||||
impl: AndroidKeepAliveService
|
||||
): KeepAliveService
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun history(
|
||||
impl: AppHistoryRepositoryImpl
|
||||
): AppHistoryRepository
|
||||
|
||||
companion object {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideMetadata(
|
||||
impl: AndroidFileController
|
||||
): MetadataProvider = impl.toMetadataProvider()
|
||||
}
|
||||
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.history
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.t8rin.imagetoolbox.core.domain.history.AppHistoryRepository
|
||||
import com.t8rin.imagetoolbox.core.domain.history.model.AppUsageStatistics
|
||||
import com.t8rin.imagetoolbox.core.domain.history.model.LastUsedTool
|
||||
import com.t8rin.imagetoolbox.core.domain.json.JsonParser
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AppHistoryRepositoryImpl @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
private val jsonParser: JsonParser,
|
||||
private val settingsProvider: SettingsProvider
|
||||
) : AppHistoryRepository {
|
||||
|
||||
private val rawLastUsedTools: Flow<List<LastUsedTool>>
|
||||
get() = dataStore.data.map { preferences ->
|
||||
preferences[LAST_USED_TOOLS]?.let { lastTools ->
|
||||
jsonParser.fromJson<LastUsedTools>(
|
||||
json = lastTools,
|
||||
type = LastUsedTools::class.java
|
||||
)?.tools
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
override fun lastUsedTools(
|
||||
maxCount: Int
|
||||
): Flow<List<LastUsedTool>> = settingsProvider.settingsState.flatMapLatest { settings ->
|
||||
if (settings.showToolsHistory) {
|
||||
rawLastUsedTools.map { rawList ->
|
||||
rawList.sortedByDescending { it.openCount }.let {
|
||||
if (maxCount < Int.MAX_VALUE) {
|
||||
val last = rawList.lastOrNull()
|
||||
val limited = it.take(maxCount)
|
||||
|
||||
if (last != null) {
|
||||
listOf(last) + limited.minus(last)
|
||||
} else {
|
||||
limited
|
||||
}
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flowOf(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun toolUsageStatistics(): Flow<List<LastUsedTool>> = rawLastUsedTools.map { rawList ->
|
||||
rawList.sortedWith(
|
||||
compareByDescending<LastUsedTool> { it.openCount }
|
||||
.thenByDescending { it.lastOpenedTimestamp }
|
||||
)
|
||||
}
|
||||
|
||||
override fun successfulSavesCount(): Flow<Int> = dataStore.data.map { preferences ->
|
||||
preferences[SUCCESSFUL_SAVES_COUNT] ?: 0
|
||||
}
|
||||
|
||||
override fun appUsageStatistics(): Flow<AppUsageStatistics> =
|
||||
dataStore.data.map { preferences ->
|
||||
AppUsageStatistics(
|
||||
successfulSavesCount = preferences[SUCCESSFUL_SAVES_COUNT] ?: 0,
|
||||
savedBytes = preferences[SAVED_BYTES] ?: 0,
|
||||
lastActivityDayEpoch = preferences[LAST_ACTIVITY_DAY_EPOCH] ?: 0,
|
||||
currentActivityStreak = preferences[CURRENT_ACTIVITY_STREAK] ?: 0,
|
||||
savedFormatCounts = preferences[SAVED_FORMAT_COUNTERS]?.let { counters ->
|
||||
jsonParser.fromJson<SavedFormatCounters>(
|
||||
json = counters,
|
||||
type = SavedFormatCounters::class.java
|
||||
)?.counters
|
||||
}.orEmpty()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun pushLastTool(screenId: Int) {
|
||||
dataStore.edit { preferences ->
|
||||
val todayEpochDay = LocalDate.now().toEpochDay()
|
||||
val lastActivityDayEpoch = preferences[LAST_ACTIVITY_DAY_EPOCH]
|
||||
|
||||
if (lastActivityDayEpoch != todayEpochDay) {
|
||||
preferences[CURRENT_ACTIVITY_STREAK] =
|
||||
if (lastActivityDayEpoch == todayEpochDay - 1) {
|
||||
(preferences[CURRENT_ACTIVITY_STREAK] ?: 0) + 1
|
||||
} else {
|
||||
1
|
||||
}
|
||||
preferences[LAST_ACTIVITY_DAY_EPOCH] = todayEpochDay
|
||||
}
|
||||
|
||||
val current = preferences[LAST_USED_TOOLS]?.let {
|
||||
jsonParser.fromJson(
|
||||
json = it,
|
||||
type = LastUsedTools::class.java
|
||||
)
|
||||
} ?: LastUsedTools(emptyList())
|
||||
|
||||
val screenEntry = current.tools.find {
|
||||
it.screenId == screenId
|
||||
} ?: LastUsedTool(
|
||||
screenId = screenId,
|
||||
openCount = 0,
|
||||
lastOpenedTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val newValue = current.copy(
|
||||
tools = current.tools
|
||||
.minus(screenEntry)
|
||||
.plus(
|
||||
screenEntry.copy(
|
||||
openCount = screenEntry.openCount + 1,
|
||||
lastOpenedTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
preferences[LAST_USED_TOOLS] = jsonParser.toJson(
|
||||
obj = newValue,
|
||||
type = LastUsedTools::class.java
|
||||
) ?: preferences[LAST_USED_TOOLS].orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun registerSuccessfulSave(
|
||||
savedBytes: Long,
|
||||
savedFormat: String
|
||||
) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[SUCCESSFUL_SAVES_COUNT] = (preferences[SUCCESSFUL_SAVES_COUNT] ?: 0) + 1
|
||||
preferences[SAVED_BYTES] = (preferences[SAVED_BYTES] ?: 0) + savedBytes.coerceAtLeast(0)
|
||||
|
||||
val format = savedFormat.lowercase().trim()
|
||||
if (format.isNotEmpty()) {
|
||||
val current = preferences[SAVED_FORMAT_COUNTERS]?.let {
|
||||
runSuspendCatching {
|
||||
jsonParser.fromJson<SavedFormatCounters>(
|
||||
json = it,
|
||||
type = SavedFormatCounters::class.java
|
||||
)
|
||||
}.getOrNull()
|
||||
} ?: SavedFormatCounters(emptyMap())
|
||||
|
||||
preferences[SAVED_FORMAT_COUNTERS] = jsonParser.toJson(
|
||||
obj = current.copy(
|
||||
counters = current.counters + (format to ((current.counters[format]
|
||||
?: 0) + 1))
|
||||
),
|
||||
type = SavedFormatCounters::class.java
|
||||
) ?: preferences[SAVED_FORMAT_COUNTERS].orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetUsageStatistics() {
|
||||
dataStore.edit { preferences ->
|
||||
preferences.remove(LAST_USED_TOOLS)
|
||||
preferences.remove(SUCCESSFUL_SAVES_COUNT)
|
||||
preferences.remove(SAVED_BYTES)
|
||||
preferences.remove(LAST_ACTIVITY_DAY_EPOCH)
|
||||
preferences.remove(CURRENT_ACTIVITY_STREAK)
|
||||
preferences.remove(SAVED_FORMAT_COUNTERS)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class LastUsedTools(
|
||||
val tools: List<LastUsedTool>
|
||||
)
|
||||
|
||||
private data class SavedFormatCounters(
|
||||
val counters: Map<String, Int>
|
||||
)
|
||||
|
||||
private val LAST_USED_TOOLS = stringPreferencesKey("LAST_USED_TOOLS")
|
||||
private val SUCCESSFUL_SAVES_COUNT = intPreferencesKey("SUCCESSFUL_SAVES_COUNT")
|
||||
private val SAVED_BYTES = longPreferencesKey("SAVED_BYTES")
|
||||
private val LAST_ACTIVITY_DAY_EPOCH = longPreferencesKey("LAST_ACTIVITY_DAY_EPOCH")
|
||||
private val CURRENT_ACTIVITY_STREAK = intPreferencesKey("CURRENT_ACTIVITY_STREAK")
|
||||
private val SAVED_FORMAT_COUNTERS = stringPreferencesKey("SAVED_FORMAT_COUNTERS")
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.image
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.net.toUri
|
||||
import com.t8rin.imagetoolbox.core.data.image.utils.ImageCompressorBackend
|
||||
import com.t8rin.imagetoolbox.core.data.utils.toSoftware
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageCompressor
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ShareProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageFormat
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.Quality
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.alphaContainedEntries
|
||||
import com.t8rin.imagetoolbox.core.domain.model.sizeTo
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.model.FilenameBehavior
|
||||
import com.t8rin.imagetoolbox.core.utils.fileSize
|
||||
import com.t8rin.trickle.Trickle
|
||||
import dagger.Lazy
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AndroidImageCompressor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val imageTransformer: ImageTransformer<Bitmap>,
|
||||
private val imageScaler: ImageScaler<Bitmap>,
|
||||
private val imageGetter: ImageGetter<Bitmap>,
|
||||
private val shareProvider: Lazy<ShareProvider>,
|
||||
private val settingsProvider: SettingsProvider,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : DispatchersHolder by dispatchersHolder, ImageCompressor<Bitmap> {
|
||||
|
||||
private val settingsState get() = settingsProvider.settingsState.value
|
||||
|
||||
override suspend fun compress(
|
||||
image: Bitmap,
|
||||
imageFormat: ImageFormat,
|
||||
quality: Quality
|
||||
): ByteArray = withContext(encodingDispatcher) {
|
||||
val coercedQuality = quality.coerceIn(imageFormat)
|
||||
val transformedImage = image.toSoftware().let { software ->
|
||||
val enableForAlpha = settingsState.enableBackgroundColorForAlphaFormats
|
||||
val isNonAlpha = imageFormat !in ImageFormat.alphaContainedEntries
|
||||
|
||||
if (isNonAlpha || coercedQuality.isNonAlpha() || enableForAlpha) {
|
||||
withContext(defaultDispatcher) {
|
||||
Trickle.drawColorBehind(
|
||||
color = settingsState.backgroundForNoAlphaImageFormats.colorInt,
|
||||
input = software
|
||||
)
|
||||
}
|
||||
} else software
|
||||
}
|
||||
|
||||
ImageCompressorBackend.Factory()
|
||||
.create(
|
||||
imageFormat = imageFormat,
|
||||
context = context,
|
||||
imageScaler = imageScaler
|
||||
)
|
||||
.compress(
|
||||
image = transformedImage,
|
||||
quality = coercedQuality
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun compressAndTransform(
|
||||
image: Bitmap,
|
||||
imageInfo: ImageInfo,
|
||||
onImageReadyToCompressInterceptor: suspend (Bitmap) -> Bitmap,
|
||||
applyImageTransformations: Boolean
|
||||
): ByteArray = withContext(encodingDispatcher) {
|
||||
val currentImage = if (applyImageTransformations) {
|
||||
val size = imageInfo.originalUri?.let {
|
||||
imageGetter.getImage(
|
||||
data = it,
|
||||
originalSize = true
|
||||
)?.run { width sizeTo height }
|
||||
}
|
||||
ensureActive()
|
||||
imageScaler
|
||||
.scaleImage(
|
||||
image = imageTransformer.rotate(
|
||||
image = image.apply { setHasAlpha(true) },
|
||||
degrees = imageInfo.rotationDegrees
|
||||
),
|
||||
width = imageInfo.width,
|
||||
height = imageInfo.height,
|
||||
resizeType = imageInfo.resizeType.withOriginalSizeIfCrop(size),
|
||||
imageScaleMode = imageInfo.imageScaleMode
|
||||
)
|
||||
.let {
|
||||
imageTransformer.flip(
|
||||
image = it,
|
||||
isFlipped = imageInfo.isFlipped
|
||||
)
|
||||
}
|
||||
.let {
|
||||
onImageReadyToCompressInterceptor(it)
|
||||
}
|
||||
} else onImageReadyToCompressInterceptor(image)
|
||||
|
||||
val extension = imageInfo.originalUri?.let { imageGetter.getExtension(it) }
|
||||
|
||||
val imageFormat =
|
||||
if (settingsState.filenameBehavior is FilenameBehavior.Overwrite && extension != null) {
|
||||
val target = ImageFormat[extension]
|
||||
|
||||
if (imageInfo.imageFormat.extension == target.extension) {
|
||||
imageInfo.imageFormat
|
||||
} else {
|
||||
target
|
||||
}
|
||||
} else imageInfo.imageFormat
|
||||
|
||||
ensureActive()
|
||||
compress(
|
||||
image = currentImage,
|
||||
imageFormat = imageFormat,
|
||||
quality = imageInfo.quality
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun calculateImageSize(
|
||||
image: Bitmap,
|
||||
imageInfo: ImageInfo
|
||||
): Long = withContext(encodingDispatcher) {
|
||||
val newInfo = imageInfo.let {
|
||||
if (it.width == 0 || it.height == 0) {
|
||||
it.copy(
|
||||
width = image.width,
|
||||
height = image.height
|
||||
)
|
||||
} else it
|
||||
}
|
||||
compressAndTransform(
|
||||
image = image,
|
||||
imageInfo = newInfo
|
||||
).let {
|
||||
shareProvider.get().cacheByteArray(
|
||||
byteArray = it,
|
||||
filename = "temp.${newInfo.imageFormat.extension}"
|
||||
)?.toUri()
|
||||
?.fileSize() ?: it.size.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.image
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.net.toUri
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transformations
|
||||
import coil3.size.Precision
|
||||
import coil3.size.Size
|
||||
import coil3.toBitmap
|
||||
import com.t8rin.imagetoolbox.core.data.image.utils.static
|
||||
import com.t8rin.imagetoolbox.core.data.utils.toCoil
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.AppScope
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
|
||||
import com.t8rin.imagetoolbox.core.domain.image.MetadataProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageData
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageFormat
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.FailureNotifier
|
||||
import com.t8rin.imagetoolbox.core.domain.transformation.Transformation
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
|
||||
import com.t8rin.imagetoolbox.core.utils.extension
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import dagger.Lazy
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AndroidImageGetter @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val appScope: AppScope,
|
||||
private val failureNotifier: FailureNotifier,
|
||||
metadataProvider: Lazy<MetadataProvider>,
|
||||
settingsProvider: SettingsProvider,
|
||||
dispatchersHolder: DispatchersHolder,
|
||||
) : DispatchersHolder by dispatchersHolder, ImageGetter<Bitmap> {
|
||||
|
||||
private val _settingsState = settingsProvider.settingsState
|
||||
|
||||
private val settingsState get() = _settingsState.value
|
||||
|
||||
private val metadataProvider by lazy {
|
||||
metadataProvider.get()
|
||||
}
|
||||
|
||||
override suspend fun getImage(
|
||||
uri: String,
|
||||
originalSize: Boolean,
|
||||
onFailure: ((Throwable) -> Unit)?
|
||||
): ImageData<Bitmap>? = withContext(defaultDispatcher) {
|
||||
getImageImpl(
|
||||
data = uri,
|
||||
size = null,
|
||||
addSizeToRequest = originalSize,
|
||||
onFailure = onFailure ?: failureNotifier::send
|
||||
)?.let { bitmap ->
|
||||
ImageData(
|
||||
image = bitmap,
|
||||
imageInfo = ImageInfo(
|
||||
width = bitmap.width,
|
||||
height = bitmap.height,
|
||||
imageFormat = settingsState.defaultImageFormat
|
||||
?: ImageFormat[getExtension(uri)],
|
||||
originalUri = uri,
|
||||
resizeType = settingsState.defaultResizeType
|
||||
),
|
||||
metadata = metadataProvider.readMetadata(uri)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getImage(
|
||||
data: Any,
|
||||
originalSize: Boolean
|
||||
): Bitmap? = getImageImpl(
|
||||
data = data,
|
||||
size = null,
|
||||
addSizeToRequest = originalSize
|
||||
)
|
||||
|
||||
override suspend fun getImage(
|
||||
data: Any,
|
||||
size: IntegerSize?
|
||||
): Bitmap? = getImageImpl(
|
||||
data = data,
|
||||
size = size
|
||||
)
|
||||
|
||||
override suspend fun getImage(
|
||||
data: Any,
|
||||
size: Int?
|
||||
): Bitmap? = getImageImpl(
|
||||
data = data,
|
||||
size = size?.let {
|
||||
IntegerSize(
|
||||
width = it,
|
||||
height = it
|
||||
)
|
||||
},
|
||||
precision = Precision.INEXACT
|
||||
)
|
||||
|
||||
override suspend fun getImageData(
|
||||
uri: String,
|
||||
size: Int?,
|
||||
onFailure: (Throwable) -> Unit
|
||||
): ImageData<Bitmap>? = withContext(defaultDispatcher) {
|
||||
getImageImpl(
|
||||
data = uri,
|
||||
size = size?.let {
|
||||
IntegerSize(
|
||||
width = it,
|
||||
height = it
|
||||
)
|
||||
},
|
||||
precision = Precision.INEXACT,
|
||||
onFailure = onFailure
|
||||
)?.let { bitmap ->
|
||||
ImageData(
|
||||
image = bitmap,
|
||||
imageInfo = ImageInfo(
|
||||
width = bitmap.width,
|
||||
height = bitmap.height,
|
||||
imageFormat = settingsState.defaultImageFormat
|
||||
?: ImageFormat[getExtension(uri)],
|
||||
originalUri = uri,
|
||||
resizeType = settingsState.defaultResizeType
|
||||
),
|
||||
metadata = metadataProvider.readMetadata(uri)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getImageWithTransformations(
|
||||
uri: String,
|
||||
transformations: List<Transformation<Bitmap>>,
|
||||
originalSize: Boolean
|
||||
): ImageData<Bitmap>? = withContext(defaultDispatcher) {
|
||||
getImageImpl(
|
||||
data = uri,
|
||||
transformations = transformations,
|
||||
size = null,
|
||||
addSizeToRequest = originalSize
|
||||
)?.let { bitmap ->
|
||||
ImageData(
|
||||
image = bitmap,
|
||||
imageInfo = ImageInfo(
|
||||
width = bitmap.width,
|
||||
height = bitmap.height,
|
||||
imageFormat = ImageFormat[getExtension(uri)],
|
||||
originalUri = uri,
|
||||
resizeType = settingsState.defaultResizeType
|
||||
),
|
||||
metadata = metadataProvider.readMetadata(uri)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getImageWithTransformations(
|
||||
data: Any,
|
||||
transformations: List<Transformation<Bitmap>>,
|
||||
size: IntegerSize?
|
||||
): Bitmap? = getImageImpl(
|
||||
data = data,
|
||||
transformations = transformations,
|
||||
size = size
|
||||
)
|
||||
|
||||
override fun getImageAsync(
|
||||
uri: String,
|
||||
originalSize: Boolean,
|
||||
onGetImage: (ImageData<Bitmap>) -> Unit,
|
||||
onFailure: (Throwable) -> Unit
|
||||
) {
|
||||
appScope.launch {
|
||||
var failureDelivered = false
|
||||
|
||||
val imageData = getImage(
|
||||
uri = uri,
|
||||
originalSize = originalSize,
|
||||
onFailure = {
|
||||
failureDelivered = true
|
||||
onFailure(it)
|
||||
}
|
||||
)
|
||||
|
||||
if (imageData != null) {
|
||||
onGetImage(imageData)
|
||||
} else if (!failureDelivered) {
|
||||
onFailure(
|
||||
IllegalStateException(context.getString(R.string.failed_to_open))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getExtension(uri: String): String? = uri.toUri().extension(context)
|
||||
|
||||
private suspend fun getImageImpl(
|
||||
data: Any,
|
||||
size: IntegerSize?,
|
||||
precision: Precision = Precision.EXACT,
|
||||
transformations: List<Transformation<Bitmap>> = emptyList(),
|
||||
onFailure: (Throwable) -> Unit = {},
|
||||
addSizeToRequest: Boolean = true
|
||||
): Bitmap? = withContext(defaultDispatcher) {
|
||||
if ((size == null || !addSizeToRequest) && data is Bitmap) return@withContext data
|
||||
|
||||
val request = ImageRequest
|
||||
.Builder(context)
|
||||
.data(data)
|
||||
.static()
|
||||
.precision(precision)
|
||||
.transformations(
|
||||
transformations.map(Transformation<Bitmap>::toCoil)
|
||||
)
|
||||
.apply {
|
||||
if (addSizeToRequest) {
|
||||
size(
|
||||
size?.let {
|
||||
Size(size.width, size.height)
|
||||
} ?: Size.ORIGINAL
|
||||
)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
ensureActive()
|
||||
|
||||
runSuspendCatching {
|
||||
imageLoader.execute(request).image?.toBitmap()
|
||||
}.onFailure {
|
||||
it.makeLog("ImageGetter")
|
||||
onFailure(it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageCompressor
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImagePreviewCreator
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ResizeType
|
||||
import com.t8rin.imagetoolbox.core.domain.transformation.Transformation
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
internal class AndroidImagePreviewCreator @Inject constructor(
|
||||
private val imageCompressor: ImageCompressor<Bitmap>,
|
||||
private val imageGetter: ImageGetter<Bitmap>,
|
||||
private val imageTransformer: ImageTransformer<Bitmap>,
|
||||
private val imageScaler: ImageScaler<Bitmap>,
|
||||
settingsProvider: SettingsProvider,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : DispatchersHolder by dispatchersHolder, ImagePreviewCreator<Bitmap> {
|
||||
|
||||
private val _settingsState = settingsProvider.settingsState
|
||||
private val settingsState get() = _settingsState.value
|
||||
|
||||
override suspend fun createPreview(
|
||||
image: Bitmap,
|
||||
imageInfo: ImageInfo,
|
||||
transformations: List<Transformation<Bitmap>>,
|
||||
onGetByteCount: (Int) -> Unit
|
||||
): Bitmap? = withContext(defaultDispatcher) {
|
||||
launch(encodingDispatcher) {
|
||||
onGetByteCount(0)
|
||||
ensureActive()
|
||||
onGetByteCount(
|
||||
imageCompressor.calculateImageSize(
|
||||
image = image,
|
||||
imageInfo = imageInfo
|
||||
).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
if (!settingsState.generatePreviews) return@withContext null
|
||||
|
||||
if (imageInfo.height == 0 || imageInfo.width == 0) return@withContext image
|
||||
|
||||
ensureActive()
|
||||
val shouldTransform = transformations.isNotEmpty()
|
||||
|| (imageInfo.width != image.width)
|
||||
|| (imageInfo.height != image.height)
|
||||
|| !imageInfo.quality.isDefault()
|
||||
|| (imageInfo.rotationDegrees != 0f)
|
||||
|| imageInfo.isFlipped
|
||||
|
||||
val targetImage = if (shouldTransform) {
|
||||
var width = imageInfo.width
|
||||
var height = imageInfo.height
|
||||
|
||||
var scaleFactor = 1f
|
||||
while (height * width * 4 > SMALL_SIZE) {
|
||||
height = (height * 0.85f).roundToInt()
|
||||
width = (width * 0.85f).roundToInt()
|
||||
scaleFactor *= 0.85f
|
||||
}
|
||||
ensureActive()
|
||||
val bytes = imageCompressor.compressAndTransform(
|
||||
image = image,
|
||||
imageInfo = imageInfo.copy(
|
||||
width = width,
|
||||
height = height,
|
||||
resizeType = if (imageInfo.resizeType is ResizeType.CenterCrop) {
|
||||
(imageInfo.resizeType as ResizeType.CenterCrop).copy(scaleFactor = scaleFactor)
|
||||
} else imageInfo.resizeType
|
||||
),
|
||||
onImageReadyToCompressInterceptor = {
|
||||
ensureActive()
|
||||
imageTransformer.transform(
|
||||
image = it,
|
||||
transformations = transformations
|
||||
) ?: it
|
||||
}
|
||||
)
|
||||
|
||||
ensureActive()
|
||||
imageGetter.getImage(bytes) ?: image
|
||||
} else {
|
||||
image
|
||||
}
|
||||
|
||||
ensureActive()
|
||||
imageScaler.scaleUntilCanShow(targetImage)
|
||||
}
|
||||
|
||||
override fun canShow(image: Bitmap?): Boolean = image?.run { size() <= BIG_SIZE } ?: false
|
||||
|
||||
private val Bitmap.configSize: Int
|
||||
get() = when (config) {
|
||||
Bitmap.Config.RGB_565 -> 2
|
||||
else -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (config == Bitmap.Config.RGBA_F16) 8 else 4
|
||||
} else 4
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.size(): Int = width * height * configSize
|
||||
|
||||
}
|
||||
|
||||
private const val SMALL_SIZE = 1500 * 1500 * 3
|
||||
private const val BIG_SIZE = 3096 * 3096 * 4
|
||||
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import androidx.core.graphics.BitmapCompat
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.scale
|
||||
import com.awxkee.aire.Aire
|
||||
import com.awxkee.aire.ResizeFunction
|
||||
import com.t8rin.imagetoolbox.core.data.image.utils.drawBitmap
|
||||
import com.t8rin.imagetoolbox.core.data.utils.aspectRatio
|
||||
import com.t8rin.imagetoolbox.core.data.utils.safeConfig
|
||||
import com.t8rin.imagetoolbox.core.data.utils.toSoftware
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageScaleMode
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ResizeAnchor
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ResizeType
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ScaleColorSpace
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.domain.model.Position
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
|
||||
import com.t8rin.imagetoolbox.core.filters.domain.FilterProvider
|
||||
import com.t8rin.imagetoolbox.core.filters.domain.model.Filter
|
||||
import com.t8rin.imagetoolbox.core.filters.domain.model.createFilter
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import com.awxkee.aire.ScaleColorSpace as AireScaleColorSpace
|
||||
|
||||
internal class AndroidImageScaler @Inject constructor(
|
||||
settingsProvider: SettingsProvider,
|
||||
private val imageTransformer: ImageTransformer<Bitmap>,
|
||||
private val filterProvider: FilterProvider<Bitmap>,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : DispatchersHolder by dispatchersHolder, ImageScaler<Bitmap> {
|
||||
|
||||
private val _settingsState = settingsProvider.settingsState
|
||||
private val settingsState get() = _settingsState.value
|
||||
|
||||
override suspend fun scaleImage(
|
||||
image: Bitmap,
|
||||
width: Int,
|
||||
height: Int,
|
||||
resizeType: ResizeType,
|
||||
imageScaleMode: ImageScaleMode
|
||||
): Bitmap = withContext(defaultDispatcher) {
|
||||
|
||||
val widthInternal = width.takeIf { it > 0 } ?: image.width
|
||||
val heightInternal = height.takeIf { it > 0 } ?: image.height
|
||||
|
||||
runSuspendCatching {
|
||||
when (resizeType) {
|
||||
ResizeType.Explicit -> {
|
||||
createScaledBitmap(
|
||||
image = image,
|
||||
width = widthInternal,
|
||||
height = heightInternal,
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
}
|
||||
|
||||
is ResizeType.Flexible -> {
|
||||
flexibleResize(
|
||||
image = image,
|
||||
width = widthInternal,
|
||||
height = heightInternal,
|
||||
resizeAnchor = resizeType.resizeAnchor,
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
}
|
||||
|
||||
is ResizeType.CenterCrop -> {
|
||||
resizeType.performCenterCrop(
|
||||
image = image,
|
||||
targetWidth = widthInternal,
|
||||
targetHeight = heightInternal,
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
}
|
||||
|
||||
is ResizeType.Fit -> {
|
||||
resizeType.performFitResize(
|
||||
image = image,
|
||||
targetWidth = widthInternal,
|
||||
targetHeight = heightInternal,
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.makeLog("AndroidImageScaler")
|
||||
}.getOrNull() ?: image
|
||||
}
|
||||
|
||||
override suspend fun scaleUntilCanShow(
|
||||
image: Bitmap?
|
||||
): Bitmap? = withContext(defaultDispatcher) {
|
||||
if (image == null) return@withContext null
|
||||
if (canShow(image.width * image.height * 4)) return@withContext image
|
||||
|
||||
var (height, width) = image.run { height to width }
|
||||
|
||||
var iterations = 0
|
||||
while (!canShow(size = height * width * 4)) {
|
||||
height = (height * 0.85f).roundToInt()
|
||||
width = (width * 0.85f).roundToInt()
|
||||
iterations++
|
||||
}
|
||||
|
||||
if (iterations == 0) image
|
||||
else scaleImage(
|
||||
image = image,
|
||||
height = height,
|
||||
width = width,
|
||||
imageScaleMode = ImageScaleMode.Bicubic()
|
||||
)
|
||||
}
|
||||
|
||||
private fun canShow(size: Int): Boolean {
|
||||
return size < 3096 * 3096 * 3
|
||||
}
|
||||
|
||||
private suspend fun Bitmap.fitResize(
|
||||
targetWidth: Int,
|
||||
targetHeight: Int,
|
||||
imageScaleMode: ImageScaleMode
|
||||
): Bitmap {
|
||||
val aspectRatio = width.toFloat() / height.toFloat()
|
||||
val targetAspectRatio = targetWidth.toFloat() / targetHeight.toFloat()
|
||||
|
||||
val finalWidth: Int
|
||||
val finalHeight: Int
|
||||
|
||||
if (aspectRatio > targetAspectRatio) {
|
||||
// Image is wider than the target aspect ratio
|
||||
finalWidth = targetWidth
|
||||
finalHeight = (targetWidth / aspectRatio).toInt()
|
||||
} else {
|
||||
// Image is taller than or equal to the target aspect ratio
|
||||
finalWidth = (targetHeight * aspectRatio).toInt()
|
||||
finalHeight = targetHeight
|
||||
}
|
||||
|
||||
return createScaledBitmap(
|
||||
image = this,
|
||||
width = finalWidth,
|
||||
height = finalHeight,
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun ResizeType.Fit.performFitResize(
|
||||
image: Bitmap,
|
||||
targetWidth: Int,
|
||||
targetHeight: Int,
|
||||
imageScaleMode: ImageScaleMode
|
||||
): Bitmap = withContext(defaultDispatcher) {
|
||||
if (targetWidth == image.width && targetHeight == image.height) {
|
||||
return@withContext image
|
||||
}
|
||||
|
||||
val originalWidth: Int
|
||||
val originalHeight: Int
|
||||
|
||||
val aspect = image.aspectRatio
|
||||
val originalAspect = image.aspectRatio
|
||||
|
||||
if (abs(aspect - originalAspect) > 0.001f) {
|
||||
originalWidth = image.height
|
||||
originalHeight = image.width
|
||||
} else {
|
||||
originalWidth = image.width
|
||||
originalHeight = image.height
|
||||
}
|
||||
|
||||
val drawImage = image.fitResize(
|
||||
targetWidth = targetWidth,
|
||||
targetHeight = targetHeight,
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
|
||||
val blurredBitmap = imageTransformer.transform(
|
||||
image = drawImage.let { bitmap ->
|
||||
val xScale: Float = targetWidth.toFloat() / originalWidth
|
||||
val yScale: Float = targetHeight.toFloat() / originalHeight
|
||||
val scale = xScale.coerceAtLeast(yScale)
|
||||
createScaledBitmap(
|
||||
image = bitmap,
|
||||
width = (scale * originalWidth).toInt(),
|
||||
height = (scale * originalHeight).toInt(),
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
},
|
||||
transformations = listOf(
|
||||
filterProvider.filterToTransformation(
|
||||
createFilter<Float, Filter.NativeStackBlur>(
|
||||
blurRadius.toFloat() / 1000 * max(targetWidth, targetHeight)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
createBitmap(targetWidth, targetHeight, drawImage.safeConfig).apply { setHasAlpha(true) }
|
||||
.applyCanvas {
|
||||
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
||||
canvasColor?.let {
|
||||
drawColor(it)
|
||||
} ?: blurredBitmap?.let {
|
||||
drawBitmap(
|
||||
bitmap = blurredBitmap,
|
||||
position = Position.Center
|
||||
)
|
||||
}
|
||||
drawBitmap(
|
||||
bitmap = drawImage,
|
||||
position = position
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ResizeType.CenterCrop.performCenterCrop(
|
||||
image: Bitmap,
|
||||
targetWidth: Int,
|
||||
targetHeight: Int,
|
||||
imageScaleMode: ImageScaleMode
|
||||
): Bitmap = withContext(defaultDispatcher) {
|
||||
val originalSize = if (!originalSize.isDefined()) {
|
||||
IntegerSize(
|
||||
width = image.width,
|
||||
height = image.height
|
||||
)
|
||||
} else {
|
||||
originalSize
|
||||
} * scaleFactor
|
||||
|
||||
if (targetWidth == originalSize.width && targetHeight == originalSize.height) {
|
||||
return@withContext image
|
||||
}
|
||||
|
||||
val originalWidth: Int
|
||||
val originalHeight: Int
|
||||
|
||||
val aspect = image.aspectRatio
|
||||
val originalAspect = originalSize.aspectRatio
|
||||
|
||||
if (abs(aspect - originalAspect) > 0.001f) {
|
||||
originalWidth = originalSize.height
|
||||
originalHeight = originalSize.width
|
||||
} else {
|
||||
originalWidth = originalSize.width
|
||||
originalHeight = originalSize.height
|
||||
}
|
||||
|
||||
val drawImage = createScaledBitmap(
|
||||
image = image,
|
||||
width = originalWidth,
|
||||
height = originalHeight,
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
|
||||
val blurredBitmap = if (canvasColor == null) {
|
||||
imageTransformer.transform(
|
||||
image = drawImage.let { bitmap ->
|
||||
val xScale: Float = targetWidth.toFloat() / originalWidth
|
||||
val yScale: Float = targetHeight.toFloat() / originalHeight
|
||||
val scale = xScale.coerceAtLeast(yScale)
|
||||
createScaledBitmap(
|
||||
image = bitmap,
|
||||
width = (scale * originalWidth).toInt(),
|
||||
height = (scale * originalHeight).toInt(),
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
},
|
||||
transformations = listOf(
|
||||
filterProvider.filterToTransformation(
|
||||
createFilter<Float, Filter.NativeStackBlur>(
|
||||
blurRadius.toFloat() / 1000 * max(targetWidth, targetHeight)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else null
|
||||
|
||||
createBitmap(targetWidth, targetHeight, drawImage.safeConfig).apply { setHasAlpha(true) }
|
||||
.applyCanvas {
|
||||
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
||||
canvasColor?.let {
|
||||
drawColor(it)
|
||||
} ?: blurredBitmap?.let {
|
||||
drawBitmap(
|
||||
bitmap = blurredBitmap,
|
||||
position = Position.Center
|
||||
)
|
||||
}
|
||||
drawBitmap(
|
||||
bitmap = drawImage,
|
||||
position = position
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createScaledBitmap(
|
||||
image: Bitmap,
|
||||
width: Int,
|
||||
height: Int,
|
||||
imageScaleMode: ImageScaleMode
|
||||
): Bitmap = withContext(defaultDispatcher) {
|
||||
if (width == image.width && height == image.height) return@withContext image
|
||||
|
||||
val softwareImage = image.toSoftware()
|
||||
|
||||
if (imageScaleMode is ImageScaleMode.Base) {
|
||||
return@withContext if (width < softwareImage.width && height < softwareImage.width) {
|
||||
BitmapCompat.createScaledBitmap(softwareImage, width, height, null, true)
|
||||
} else {
|
||||
softwareImage.scale(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
val mode = imageScaleMode.takeIf {
|
||||
it != ImageScaleMode.NotPresent && it.value >= 0
|
||||
} ?: settingsState.defaultImageScaleMode
|
||||
|
||||
Aire.scale(
|
||||
bitmap = softwareImage,
|
||||
dstWidth = width,
|
||||
dstHeight = height,
|
||||
scaleMode = mode.toResizeFunction(),
|
||||
colorSpace = mode.scaleColorSpace.toColorSpace()
|
||||
)
|
||||
}
|
||||
|
||||
private fun ImageScaleMode.toResizeFunction(): ResizeFunction = when (this) {
|
||||
ImageScaleMode.NotPresent,
|
||||
ImageScaleMode.Base -> ResizeFunction.Bilinear
|
||||
|
||||
is ImageScaleMode.Bilinear -> ResizeFunction.Bilinear
|
||||
is ImageScaleMode.Nearest -> ResizeFunction.Nearest
|
||||
is ImageScaleMode.Cubic -> ResizeFunction.Cubic
|
||||
is ImageScaleMode.Mitchell -> ResizeFunction.MitchellNetravalli
|
||||
is ImageScaleMode.Catmull -> ResizeFunction.CatmullRom
|
||||
is ImageScaleMode.Hermite -> ResizeFunction.Hermite
|
||||
is ImageScaleMode.BSpline -> ResizeFunction.BSpline
|
||||
is ImageScaleMode.Hann -> ResizeFunction.Hann
|
||||
is ImageScaleMode.Bicubic -> ResizeFunction.Bicubic
|
||||
is ImageScaleMode.Hamming -> ResizeFunction.Hamming
|
||||
is ImageScaleMode.Hanning -> ResizeFunction.Hanning
|
||||
is ImageScaleMode.Blackman -> ResizeFunction.Blackman
|
||||
is ImageScaleMode.Welch -> ResizeFunction.Welch
|
||||
is ImageScaleMode.Quadric -> ResizeFunction.Quadric
|
||||
is ImageScaleMode.Gaussian -> ResizeFunction.Gaussian
|
||||
is ImageScaleMode.Sphinx -> ResizeFunction.Sphinx
|
||||
is ImageScaleMode.Bartlett -> ResizeFunction.Bartlett
|
||||
is ImageScaleMode.Robidoux -> ResizeFunction.Robidoux
|
||||
is ImageScaleMode.RobidouxSharp -> ResizeFunction.RobidouxSharp
|
||||
is ImageScaleMode.Spline16 -> ResizeFunction.Spline16
|
||||
is ImageScaleMode.Spline36 -> ResizeFunction.Spline36
|
||||
is ImageScaleMode.Spline64 -> ResizeFunction.Spline64
|
||||
is ImageScaleMode.Kaiser -> ResizeFunction.Kaiser
|
||||
is ImageScaleMode.BartlettHann -> ResizeFunction.BartlettHann
|
||||
is ImageScaleMode.Box -> ResizeFunction.Box
|
||||
is ImageScaleMode.Bohman -> ResizeFunction.Bohman
|
||||
is ImageScaleMode.Lanczos2 -> ResizeFunction.Lanczos2
|
||||
is ImageScaleMode.Lanczos3 -> ResizeFunction.Lanczos3
|
||||
is ImageScaleMode.Lanczos4 -> ResizeFunction.Lanczos4
|
||||
is ImageScaleMode.Lanczos2Jinc -> ResizeFunction.Lanczos2Jinc
|
||||
is ImageScaleMode.Lanczos3Jinc -> ResizeFunction.Lanczos3Jinc
|
||||
is ImageScaleMode.Lanczos4Jinc -> ResizeFunction.Lanczos4Jinc
|
||||
is ImageScaleMode.EwaHanning -> ResizeFunction.EwaHanning
|
||||
is ImageScaleMode.EwaRobidoux -> ResizeFunction.EwaRobidoux
|
||||
is ImageScaleMode.EwaBlackman -> ResizeFunction.EwaBlackman
|
||||
is ImageScaleMode.EwaQuadric -> ResizeFunction.EwaQuadric
|
||||
is ImageScaleMode.EwaRobidouxSharp -> ResizeFunction.EwaRobidouxSharp
|
||||
is ImageScaleMode.EwaLanczos3Jinc -> ResizeFunction.EwaLanczos3Jinc
|
||||
is ImageScaleMode.Ginseng -> ResizeFunction.Ginseng
|
||||
is ImageScaleMode.EwaGinseng -> ResizeFunction.EwaGinseng
|
||||
is ImageScaleMode.EwaLanczosSharp -> ResizeFunction.EwaLanczosSharp
|
||||
is ImageScaleMode.EwaLanczos4Sharpest -> ResizeFunction.EwaLanczos4Sharpest
|
||||
is ImageScaleMode.EwaLanczosSoft -> ResizeFunction.EwaLanczosSoft
|
||||
is ImageScaleMode.HaasnSoft -> ResizeFunction.HaasnSoft
|
||||
is ImageScaleMode.Lagrange2 -> ResizeFunction.Lagrange2
|
||||
is ImageScaleMode.Lagrange3 -> ResizeFunction.Lagrange3
|
||||
is ImageScaleMode.Lanczos6 -> ResizeFunction.Lanczos6
|
||||
is ImageScaleMode.Lanczos6Jinc -> ResizeFunction.Lanczos6Jinc
|
||||
is ImageScaleMode.Area -> ResizeFunction.Area
|
||||
}
|
||||
|
||||
private fun ScaleColorSpace.toColorSpace(): AireScaleColorSpace = when (this) {
|
||||
ScaleColorSpace.LAB -> AireScaleColorSpace.LAB
|
||||
ScaleColorSpace.Linear -> AireScaleColorSpace.LINEAR
|
||||
ScaleColorSpace.SRGB -> AireScaleColorSpace.SRGB
|
||||
ScaleColorSpace.LUV -> AireScaleColorSpace.LUV
|
||||
ScaleColorSpace.Sigmoidal -> AireScaleColorSpace.SIGMOIDAL
|
||||
ScaleColorSpace.XYZ -> AireScaleColorSpace.XYZ
|
||||
ScaleColorSpace.F32Gamma22 -> AireScaleColorSpace.LINEAR_F32_GAMMA_2_2
|
||||
ScaleColorSpace.F32Gamma28 -> AireScaleColorSpace.LINEAR_F32_GAMMA_2_8
|
||||
ScaleColorSpace.F32Rec709 -> AireScaleColorSpace.LINEAR_F32_REC709
|
||||
ScaleColorSpace.F32sRGB -> AireScaleColorSpace.LINEAR_F32_SRGB
|
||||
ScaleColorSpace.LCH -> AireScaleColorSpace.LCH
|
||||
ScaleColorSpace.OklabGamma22 -> AireScaleColorSpace.OKLAB_GAMMA_2_2
|
||||
ScaleColorSpace.OklabGamma28 -> AireScaleColorSpace.OKLAB_GAMMA_2_8
|
||||
ScaleColorSpace.OklabRec709 -> AireScaleColorSpace.OKLAB_REC709
|
||||
ScaleColorSpace.OklabSRGB -> AireScaleColorSpace.OKLAB_SRGB
|
||||
ScaleColorSpace.JzazbzGamma22 -> AireScaleColorSpace.JZAZBZ_GAMMA_2_2
|
||||
ScaleColorSpace.JzazbzGamma28 -> AireScaleColorSpace.JZAZBZ_GAMMA_2_8
|
||||
ScaleColorSpace.JzazbzRec709 -> AireScaleColorSpace.JZAZBZ_REC709
|
||||
ScaleColorSpace.JzazbzSRGB -> AireScaleColorSpace.JZAZBZ_SRGB
|
||||
}
|
||||
|
||||
private suspend fun flexibleResize(
|
||||
image: Bitmap,
|
||||
width: Int,
|
||||
height: Int,
|
||||
resizeAnchor: ResizeAnchor,
|
||||
imageScaleMode: ImageScaleMode
|
||||
): Bitmap = withContext(defaultDispatcher) {
|
||||
val max = max(width, height)
|
||||
|
||||
val scaleByWidth = suspend {
|
||||
val aspectRatio = image.aspectRatio
|
||||
createScaledBitmap(
|
||||
image = image,
|
||||
width = width,
|
||||
height = (width / aspectRatio).toInt(),
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
}
|
||||
|
||||
val scaleByHeight = suspend {
|
||||
val aspectRatio = image.aspectRatio
|
||||
createScaledBitmap(
|
||||
image = image,
|
||||
width = (height * aspectRatio).toInt(),
|
||||
height = height,
|
||||
imageScaleMode = imageScaleMode
|
||||
)
|
||||
}
|
||||
|
||||
when (resizeAnchor) {
|
||||
ResizeAnchor.Max -> {
|
||||
if (width >= height) {
|
||||
scaleByWidth()
|
||||
} else scaleByHeight()
|
||||
}
|
||||
|
||||
ResizeAnchor.Min -> {
|
||||
if (width >= height) {
|
||||
scaleByHeight()
|
||||
} else scaleByWidth()
|
||||
}
|
||||
|
||||
ResizeAnchor.Width -> scaleByWidth()
|
||||
|
||||
ResizeAnchor.Height -> scaleByHeight()
|
||||
|
||||
ResizeAnchor.Default -> {
|
||||
runSuspendCatching {
|
||||
if (image.height >= image.width) {
|
||||
val aspectRatio = image.aspectRatio
|
||||
val targetWidth = (max * aspectRatio).toInt()
|
||||
createScaledBitmap(image, targetWidth, max, imageScaleMode)
|
||||
} else {
|
||||
val aspectRatio = 1f / image.aspectRatio
|
||||
val targetHeight = (max * aspectRatio).toInt()
|
||||
createScaledBitmap(image, max, targetHeight, imageScaleMode)
|
||||
}
|
||||
}.getOrNull() ?: image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.image
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transformations
|
||||
import coil3.size.Size
|
||||
import coil3.toBitmap
|
||||
import com.t8rin.imagetoolbox.core.data.utils.toCoil
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageFormat
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.Preset
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.Quality
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ResizeType
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.domain.model.sizeTo
|
||||
import com.t8rin.imagetoolbox.core.domain.transformation.Transformation
|
||||
import com.t8rin.imagetoolbox.core.utils.makeLog
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
internal class AndroidImageTransformer @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val imageLoader: ImageLoader,
|
||||
defaultDispatchersHolder: DispatchersHolder
|
||||
) : DispatchersHolder by defaultDispatchersHolder, ImageTransformer<Bitmap> {
|
||||
|
||||
override suspend fun transform(
|
||||
image: Bitmap,
|
||||
transformations: List<Transformation<Bitmap>>,
|
||||
originalSize: Boolean
|
||||
): Bitmap? = withContext(defaultDispatcher) {
|
||||
if (transformations.isEmpty()) return@withContext image
|
||||
|
||||
val request = ImageRequest
|
||||
.Builder(context)
|
||||
.data(image)
|
||||
.transformations(
|
||||
transformations.map {
|
||||
it.toCoil()
|
||||
}
|
||||
)
|
||||
.apply {
|
||||
if (originalSize) size(Size.ORIGINAL)
|
||||
}
|
||||
.build()
|
||||
|
||||
return@withContext imageLoader.execute(request).image?.toBitmap()
|
||||
}
|
||||
|
||||
override suspend fun transform(
|
||||
image: Bitmap,
|
||||
transformations: List<Transformation<Bitmap>>,
|
||||
size: IntegerSize
|
||||
): Bitmap? = withContext(defaultDispatcher) {
|
||||
if (transformations.isEmpty()) return@withContext image
|
||||
|
||||
val request = ImageRequest
|
||||
.Builder(context)
|
||||
.data(image)
|
||||
.transformations(
|
||||
transformations.map {
|
||||
it.toCoil()
|
||||
}
|
||||
)
|
||||
.size(size.width, size.height)
|
||||
.build()
|
||||
|
||||
return@withContext imageLoader.execute(request).image?.toBitmap()
|
||||
}
|
||||
|
||||
override suspend fun applyPresetBy(
|
||||
image: Bitmap?,
|
||||
preset: Preset,
|
||||
currentInfo: ImageInfo
|
||||
): ImageInfo = withContext(defaultDispatcher) {
|
||||
if (image == null || preset is Preset.None) return@withContext currentInfo
|
||||
|
||||
val size = currentInfo.originalUri.makeLog("applyPresetBy originalUri")?.let { uri ->
|
||||
imageLoader.execute(
|
||||
ImageRequest.Builder(context)
|
||||
.data(uri)
|
||||
.size(Size.ORIGINAL)
|
||||
.build()
|
||||
).image?.run { width sizeTo height }.makeLog("applyPresetBy using orig size")
|
||||
} ?: IntegerSize(image.width, image.height).makeLog("applyPresetBy using image size")
|
||||
|
||||
val rotated = abs(currentInfo.rotationDegrees) % 180 != 0f
|
||||
fun calcWidth() = if (rotated) size.height else size.width
|
||||
fun calcHeight() = if (rotated) size.width else size.height
|
||||
fun Int.calc(cnt: Int): Int = (this * (cnt / 100f)).toInt()
|
||||
|
||||
when (preset) {
|
||||
is Preset.Telegram -> {
|
||||
currentInfo.copy(
|
||||
width = 512,
|
||||
height = 512,
|
||||
imageFormat = ImageFormat.Png.Lossless,
|
||||
resizeType = ResizeType.Flexible,
|
||||
quality = Quality.Base(100)
|
||||
)
|
||||
}
|
||||
|
||||
is Preset.Percentage -> currentInfo.copy(
|
||||
width = calcWidth().calc(preset.value),
|
||||
height = calcHeight().calc(preset.value),
|
||||
)
|
||||
|
||||
is Preset.AspectRatio -> {
|
||||
val originalWidth = calcWidth().toFloat()
|
||||
val originalHeight = calcHeight().toFloat()
|
||||
|
||||
val newWidth: Float
|
||||
val newHeight: Float
|
||||
|
||||
val condition = if (preset.isFit) preset.ratio > originalWidth / originalHeight
|
||||
else preset.ratio < originalWidth / originalHeight
|
||||
|
||||
if (condition) {
|
||||
newWidth = originalHeight * preset.ratio
|
||||
newHeight = originalHeight
|
||||
} else {
|
||||
newWidth = originalWidth
|
||||
newHeight = originalWidth / preset.ratio
|
||||
}
|
||||
|
||||
currentInfo.copy(
|
||||
width = newWidth.roundToInt(),
|
||||
height = newHeight.roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
Preset.None -> currentInfo
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun flip(
|
||||
image: Bitmap,
|
||||
isFlipped: Boolean
|
||||
): Bitmap = withContext(defaultDispatcher) {
|
||||
if (isFlipped) {
|
||||
val matrix = Matrix().apply { postScale(-1f, 1f, image.width / 2f, image.height / 2f) }
|
||||
Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
|
||||
} else image
|
||||
}
|
||||
|
||||
override suspend fun rotate(
|
||||
image: Bitmap,
|
||||
degrees: Float
|
||||
): Bitmap = withContext(defaultDispatcher) {
|
||||
if (degrees == 0f) return@withContext image
|
||||
|
||||
if (degrees % 90 == 0f) {
|
||||
val matrix = Matrix().apply { postRotate(degrees) }
|
||||
Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
|
||||
} else {
|
||||
val matrix = Matrix().apply {
|
||||
setRotate(degrees, image.width.toFloat() / 2, image.height.toFloat() / 2)
|
||||
}
|
||||
Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.core.data.image
|
||||
|
||||
import com.t8rin.exif.ExifInterface
|
||||
import com.t8rin.imagetoolbox.core.domain.image.Metadata
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.MetadataTag
|
||||
import com.t8rin.imagetoolbox.core.domain.image.toMap
|
||||
import java.io.FileDescriptor
|
||||
|
||||
private data class ExifInterfaceMetadata(
|
||||
private val exifInterface: ExifInterface
|
||||
) : Metadata {
|
||||
|
||||
override fun saveAttributes(): Metadata = apply {
|
||||
exifInterface.saveAttributes()
|
||||
}
|
||||
|
||||
override fun getAttribute(
|
||||
tag: MetadataTag
|
||||
): String? = exifInterface.getAttribute(tag.key)
|
||||
|
||||
override fun setAttribute(
|
||||
tag: MetadataTag,
|
||||
value: String?
|
||||
): Metadata = apply {
|
||||
exifInterface.setAttribute(tag.key, value)
|
||||
}
|
||||
|
||||
override fun toString(): String = "Android(${toMap()})"
|
||||
|
||||
}
|
||||
|
||||
internal fun ExifInterface.toMetadata(): Metadata = ExifInterfaceMetadata(this)
|
||||
|
||||
internal fun FileDescriptor.toMetadata(): Metadata = ExifInterface(this).toMetadata()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user