diff --git a/README.MD b/README.MD index ff38f9b6..a019cbb7 100644 --- a/README.MD +++ b/README.MD @@ -12,8 +12,8 @@ [![stars](https://img.shields.io/github/stars/jimmydaddy/react-native-image-marker?logo=github&style=for-the-badge)](https://github.com/JimmyDaddy/react-native-image-marker) [![forks](https://img.shields.io/github/forks/jimmydaddy/react-native-image-marker?logo=github&style=for-the-badge)](https://github.com/JimmyDaddy/react-native-image-marker/fork) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?logo=github&style=for-the-badge)](https://github.com/JimmyDaddy/react-native-image-marker/pulls) ![license](https://img.shields.io/npm/l/react-native-image-marker?style=for-the-badge) [![github](https://img.shields.io/badge/github-repo-blue?logo=github&style=for-the-badge)](https://github.com/JimmyDaddy/react-native-image-marker) - [![CI](https://github.com/JimmyDaddy/react-native-image-marker/actions/workflows/ci.yml/badge.svg)](https://github.com/JimmyDaddy/react-native-image-marker/actions/workflows/ci.yml) - ![platform-iOS](https://img.shields.io/badge/iOS-black?logo=Apple&style=for-the-badge) ![platform-Android](https://img.shields.io/badge/Android-black?logo=Android&style=for-the-badge) + [![CI](https://img.shields.io/github/actions/workflow/status/jimmydaddy/react-native-image-marker/ci.yml?style=for-the-badge)](https://github.com/JimmyDaddy/react-native-image-marker/actions/workflows/ci.yml) + ![platform-iOS](https://img.shields.io/badge/iOS-gray?logo=Apple&style=for-the-badge) ![platform-Android](https://img.shields.io/badge/Android-gray?logo=Android&style=for-the-badge)
diff --git a/example/.detoxrc.js b/example/.detoxrc.js new file mode 100644 index 00000000..81da9fc1 --- /dev/null +++ b/example/.detoxrc.js @@ -0,0 +1,83 @@ +/** @type {Detox.DetoxConfig} */ +module.exports = { + testRunner: { + args: { + '$0': 'jest', + config: 'e2e/jest.config.js' + }, + jest: { + setupTimeout: 120000 + } + }, + apps: { + 'ios.debug': { + type: 'ios.app', + binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/ImageMarkerExample.app', + build: 'xcodebuild -workspace ios/ImageMarkerExample.xcworkspace -scheme ImageMarkerExample -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build' + }, + 'ios.release': { + type: 'ios.app', + binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/ImageMarkerExample.app', + build: 'xcodebuild -workspace ios/ImageMarkerExample.xcworkspace -scheme ImageMarkerExample -configuration Release -sdk iphonesimulator -derivedDataPath ios/build' + }, + 'android.debug': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', + build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', + reversePorts: [ + 8081 + ] + }, + 'android.release': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', + build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release' + } + }, + devices: { + simulator: { + type: 'ios.simulator', + device: { + type: 'iPhone 15' + } + }, + attached: { + type: 'android.attached', + device: { + adbName: '.*' + } + }, + emulator: { + type: 'android.emulator', + device: { + avdName: 'Pixel_XL_API_34' + } + } + }, + configurations: { + 'ios.sim.debug': { + device: 'simulator', + app: 'ios.debug' + }, + 'ios.sim.release': { + device: 'simulator', + app: 'ios.release' + }, + 'android.att.debug': { + device: 'attached', + app: 'android.debug' + }, + 'android.att.release': { + device: 'attached', + app: 'android.release' + }, + 'android.emu.debug': { + device: 'emulator', + app: 'android.debug' + }, + 'android.emu.release': { + device: 'emulator', + app: 'android.release' + } + } +}; diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index f7000abf..80abbf5f 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -2,7 +2,7 @@ apply plugin: "com.android.application" apply plugin: "com.facebook.react" apply plugin: 'kotlin-android' -def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["kotlin_version"] +def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ImageMarkerExample_kotlinVersion"] import com.android.build.OutputFile @@ -103,6 +103,8 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" + testBuildType System.getProperty('testBuildType', 'debug') + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } splits { @@ -137,6 +139,7 @@ android { signingConfig signingConfigs.release minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro" } } @@ -171,6 +174,10 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + androidTestImplementation('com.wix:detox:+') + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation project(':react-native-vector-icons') } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) +apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle") diff --git a/example/android/app/src/androidTest/java/com/imagemarkerexample/DetoxTest.java b/example/android/app/src/androidTest/java/com/imagemarkerexample/DetoxTest.java new file mode 100644 index 00000000..09afa878 --- /dev/null +++ b/example/android/app/src/androidTest/java/com/imagemarkerexample/DetoxTest.java @@ -0,0 +1,29 @@ +package com.imagemarkerexample; + +import com.wix.detox.Detox; +import com.wix.detox.config.DetoxConfig; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class DetoxTest { + @Rule // (2) + public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); + + @Test + public void runDetoxTests() { + DetoxConfig detoxConfig = new DetoxConfig(); + detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; + detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; + detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60); + + Detox.runTests(mActivityRule, detoxConfig); + } +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 4122f36a..23843407 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,8 @@ android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" + android:networkSecurityConfig="@xml/network_security_config" + android:usesCleartextTraffic="true" android:theme="@style/AppTheme"> + + + 10.0.2.2 + localhost + + diff --git a/example/android/build.gradle b/example/android/build.gradle index f7e02686..f23dcd8d 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["kotlin_version"] + def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ImageMarkerExample_kotlinVersion"] ext { buildToolsVersion = "34.0.0" @@ -21,6 +21,9 @@ buildscript { maven { url "https://repository.jboss.org/maven2" } maven { url 'https://maven.google.com' } maven { url 'https://maven.fabric.io/public' } + maven { + url("$rootDir/../node_modules/detox/Detox-android") + } } dependencies { classpath("com.android.tools.build:gradle") @@ -29,4 +32,19 @@ buildscript { } } +allprojects { + repositories { + google() + mavenCentral() + jcenter() + maven { url 'https://dl.google.com/dl/android/maven2' } + maven { url "https://repository.jboss.org/maven2" } + maven { url 'https://maven.google.com' } + maven { url 'https://maven.fabric.io/public' } + maven { + url("$rootDir/../node_modules/detox/Detox-android") + } + } +} + apply plugin: "com.facebook.react.rootproject" diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 9194f2da..414e55c3 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -40,4 +40,4 @@ newArchEnabled=false # If set to false, you will be using JSC instead. hermesEnabled=true -kotlin_version=1.8.0 +ImageMarkerExample_kotlinVersion=1.8.0 diff --git a/example/android/settings.gradle b/example/android/settings.gradle index df5352ac..dfff2ff6 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'ImageMarkerExample' apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' -includeBuild('../node_modules/@react-native/gradle-plugin') \ No newline at end of file +includeBuild('../node_modules/@react-native/gradle-plugin') +include ':react-native-vector-icons' +project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') diff --git a/example/babel.config.js b/example/babel.config.js index d9addbba..ef67d0c7 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -14,4 +14,9 @@ module.exports = { }, ], ], + env: { + production: { + plugins: ['react-native-paper/babel'], + }, + }, }; diff --git a/example/e2e/App.test.js b/example/e2e/App.test.js new file mode 100644 index 00000000..2240cc29 --- /dev/null +++ b/example/e2e/App.test.js @@ -0,0 +1,90 @@ +import { device, element, by, waitFor } from 'detox'; +import assert from 'power-assert'; + +describe('e2e/App.test.js', () => { + beforeAll(async () => { + await device.launchApp(); + }); + + beforeEach(async () => { + await device.reloadReactNative(); + }); + + it('should display correctly', async () => { + await expect(element(by.id('backgroundImageFormatLabel'))).toBeVisible(); + await expect(element(by.id('backgroundImageFormatLabel'))).toHaveText( + 'background image format:' + ); + + await expect(element(by.id('backgroundImageFormatBtn'))).toBeVisible(); + await expect(element(by.id('backgroundImageFormatBtn'))).toHaveLabel( + 'image' + ); + + await expect(element(by.id('watermarkTypeLabel'))).toBeVisible(); + await expect(element(by.id('watermarkTypeLabel'))).toHaveText( + 'watermark type:' + ); + + await expect(element(by.id('watermarkTypeBtn'))).toBeVisible(); + await expect(element(by.id('watermarkTypeBtn'))).toHaveLabel('text'); + + await expect(element(by.id('exportResultFormatLabel'))).toBeVisible(); + await expect(element(by.id('exportResultFormatLabel'))).toHaveText( + 'export result format:' + ); + + await expect(element(by.id('exportResultFormatBtn'))).toBeVisible(); + await expect(element(by.id('exportResultFormatBtn'))).toHaveLabel('png'); + + await expect(element(by.id('selectBgBtn'))).toBeVisible(); + await expect(element(by.id('selectBgBtn'))).toHaveLabel('select bg'); + + // await expect(element(by.id('selectWatermarkBtn'))).toBeVisible(); + // await expect(element(by.id('selectWatermarkBtn'))).toHaveLabel( + // 'select watermark' + // ); + + await expect(element(by.id('resultFileSizeLabel'))).toBeVisible(); + await expect(element(by.id('resultFilePathLabel'))).toBeVisible(); + if (device.getPlatform() === 'ios') { + const resultFileSizeLabel = await element( + by.id('resultFileSizeLabel') + ).getAttributes('text'); + + assert.ok( + /^result file size:\d+(.\d+)?\s(KB|MB)$/.test(resultFileSizeLabel.text) + ); + const resultFilePathLabel = await element( + by.id('resultFilePathLabel') + ).getAttributes('text'); + assert.ok(/^file path:.*\.png$/.test(resultFilePathLabel.text)); + } else { + await expect(element(by.id('resultFileSizeLabel'))).toHaveLabel( + /^result file size:\d+(\.\d+)?\s(KB|MB)$/ + ); + await expect(element(by.id('resultFilePathLabel'))).toBeVisible(); + await expect(element(by.id('resultFilePathLabel'))).toHaveText( + /^file path:.*\.png$/ + ); + } + + await expect(element(by.id('resultImage'))).toBeVisible(); + }); + + describe('when click backgroundImageFormatBtn', () => { + it('should display correctly', async () => { + await expect(element(by.id('backgroundImageFormatBtn'))).toHaveLabel( + 'image' + ); + await element(by.id('backgroundImageFormatBtn')).tap(); + await waitFor(element(by.type('RCTModalHostView'))) + .toBeVisible() + .withTimeout(2000); + await element(by.label('base64')).tap(); + await expect(element(by.id('backgroundImageFormatBtn'))).toHaveLabel( + 'base64' + ); + }); + }); +}); diff --git a/example/e2e/jest.config.js b/example/e2e/jest.config.js new file mode 100644 index 00000000..4f980203 --- /dev/null +++ b/example/e2e/jest.config.js @@ -0,0 +1,12 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + rootDir: '..', + testMatch: ['/e2e/**/*.test.js'], + testTimeout: 120000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + verbose: true, +}; diff --git a/example/ios/ImageMarkerExample.xcodeproj/project.pbxproj b/example/ios/ImageMarkerExample.xcodeproj/project.pbxproj index 6a5818eb..b771a3b2 100644 --- a/example/ios/ImageMarkerExample.xcodeproj/project.pbxproj +++ b/example/ios/ImageMarkerExample.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A119E9E12B162437000C0527 /* ImageMarkerExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A119E9E02B162437000C0527 /* ImageMarkerExampleUITests.swift */; }; A119E9E32B162437000C0527 /* ImageMarkerExampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A119E9E22B162437000C0527 /* ImageMarkerExampleUITestsLaunchTests.swift */; }; + A1C8E4732B26D5F600D9233E /* Fonts in Resources */ = {isa = PBXBuildFile; fileRef = A1C8E4722B26D5F600D9233E /* Fonts */; }; + A1C8E4742B26D5F600D9233E /* Fonts in Resources */ = {isa = PBXBuildFile; fileRef = A1C8E4722B26D5F600D9233E /* Fonts */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,14 +38,17 @@ 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ImageMarkerExample/main.m; sourceTree = ""; }; 15E0FEF5E88EB5BA856E015A /* Pods-ImageMarkerExample-ImageMarkerExampleUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageMarkerExample-ImageMarkerExampleUITests.release.xcconfig"; path = "Target Support Files/Pods-ImageMarkerExample-ImageMarkerExampleUITests/Pods-ImageMarkerExample-ImageMarkerExampleUITests.release.xcconfig"; sourceTree = ""; }; 3B4392A12AC88292D35C810B /* Pods-ImageMarkerExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageMarkerExample.debug.xcconfig"; path = "Target Support Files/Pods-ImageMarkerExample/Pods-ImageMarkerExample.debug.xcconfig"; sourceTree = ""; }; + 4BD0221D883ACD1958386DAA /* Pods-ImageMarkerExample-ImageMarkerExampleUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageMarkerExample-ImageMarkerExampleUITests.debug.xcconfig"; path = "Target Support Files/Pods-ImageMarkerExample-ImageMarkerExampleUITests/Pods-ImageMarkerExample-ImageMarkerExampleUITests.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-ImageMarkerExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageMarkerExample.release.xcconfig"; path = "Target Support Files/Pods-ImageMarkerExample/Pods-ImageMarkerExample.release.xcconfig"; sourceTree = ""; }; 5DCACB8F33CDC322A6C60F78 /* libPods-ImageMarkerExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ImageMarkerExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 6F2EF2314753552DE3ED8A96 /* libPods-ImageMarkerExample-ImageMarkerExampleUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ImageMarkerExample-ImageMarkerExampleUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ImageMarkerExample/LaunchScreen.storyboard; sourceTree = ""; }; A0BC82937F23256F1BD51A72 /* Pods-ImageMarkerExample-ImageMarkerExampleUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ImageMarkerExample-ImageMarkerExampleUITests.debug.xcconfig"; path = "Target Support Files/Pods-ImageMarkerExample-ImageMarkerExampleUITests/Pods-ImageMarkerExample-ImageMarkerExampleUITests.debug.xcconfig"; sourceTree = ""; }; + 894CD00B2EE09ABC6790EC62 /* libPods-ImageMarkerExample-ImageMarkerExampleUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ImageMarkerExample-ImageMarkerExampleUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A119E9DE2B162437000C0527 /* ImageMarkerExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageMarkerExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A119E9E02B162437000C0527 /* ImageMarkerExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageMarkerExampleUITests.swift; sourceTree = ""; }; A119E9E22B162437000C0527 /* ImageMarkerExampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageMarkerExampleUITestsLaunchTests.swift; sourceTree = ""; }; + A1C8E4722B26D5F600D9233E /* Fonts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Fonts; path = "../node_modules/react-native-vector-icons/Fonts"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -100,6 +105,7 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + A1C8E4722B26D5F600D9233E /* Fonts */, 13B07FAE1A68108700A75B9A /* ImageMarkerExample */, 832341AE1AAA6A7D00B99B32 /* Libraries */, A119E9DF2B162437000C0527 /* ImageMarkerExampleUITests */, @@ -228,6 +234,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1C8E4732B26D5F600D9233E /* Fonts in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, ); @@ -237,6 +244,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1C8E4742B26D5F600D9233E /* Fonts in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -504,7 +512,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.4; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", @@ -576,7 +584,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.4; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", diff --git a/example/ios/ImageMarkerExample/Info.plist b/example/ios/ImageMarkerExample/Info.plist index fc03d6b9..1338aaaa 100644 --- a/example/ios/ImageMarkerExample/Info.plist +++ b/example/ios/ImageMarkerExample/Info.plist @@ -2,6 +2,32 @@ + UIAppFonts + + AntDesign.ttf + Entypo.ttf + EvilIcons.ttf + Feather.ttf + FontAwesome.ttf + FontAwesome5_Brands.ttf + FontAwesome5_Regular.ttf + FontAwesome5_Solid.ttf + FontAwesome6_Brands.ttf + FontAwesome6_Regular.ttf + FontAwesome6_Solid.ttf + Foundation.ttf + Ionicons.ttf + MaterialIcons.ttf + MaterialCommunityIcons.ttf + SimpleLineIcons.ttf + Octicons.ttf + Zocial.ttf + Fontisto.ttf + + NSCameraUsageDescription + want to use + NSPhotoLibraryUsageDescription + want to use CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/example/ios/ImageMarkerExampleUITests/ImageMarkerExampleUITests.swift b/example/ios/ImageMarkerExampleUITests/ImageMarkerExampleUITests.swift index e334a0d2..9f5c42e2 100644 --- a/example/ios/ImageMarkerExampleUITests/ImageMarkerExampleUITests.swift +++ b/example/ios/ImageMarkerExampleUITests/ImageMarkerExampleUITests.swift @@ -7,32 +7,44 @@ import XCTest +@MainActor final class ImageMarkerExampleUITests: XCTestCase { - override func setUpWithError() throws { + super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. - + // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false - + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. } - + override func tearDownWithError() throws { + super.tearDown() // Put teardown code here. This method is called after the invocation of each test method in the class. } - - func testApp() throws { + + func testAppDisplay() throws { // UI tests must launch the application that they test. let app = XCUIApplication() app.launch() sleep(5) + var ele = app.textFields["100"] XCTAssert(ele.exists) - ele = app.staticTexts["watermark type:"] + ele = app.staticTexts["watermark type"] XCTAssert(ele.exists) - ele = app.staticTexts["background image format:"] + ele = app.staticTexts["bg format:"] XCTAssert(ele.exists) + var predicate = NSPredicate(format: "label BEGINSWITH %@", "file path:") + ele = app.staticTexts.element(matching: predicate) + XCTAssert(ele.exists, "File path label does not exist.") + predicate = NSPredicate(format: "label MATCHES %@", "result file size:[0-9]+\\.?[0-9]*\\s(MB|KB)") + ele = app.staticTexts.element(matching: predicate) + XCTAssert(ele.exists, "result file size label does not exist.") + let button = app.otherElements["watermarkTypeBtn"] + XCTAssert(button.exists, "WatermarkTypeBtn button does not exist.") + } func testLaunchPerformance() throws { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 17b76699..985f7625 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1196,6 +1196,7 @@ DEPENDENCIES: - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - RNVectorIcons (from `../node_modules/react-native-vector-icons`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1316,6 +1317,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/utils" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNVectorIcons: + :path: "../node_modules/react-native-vector-icons" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" diff --git a/example/package.json b/example/package.json index 8f21eae1..ae74be99 100644 --- a/example/package.json +++ b/example/package.json @@ -8,7 +8,13 @@ "start": "react-native start", "pods": "pod-install", "m1-pods": "cd ios && arch -x86_64 pod install", - "postinstall": "patch-package" + "postinstall": "patch-package", + "e2e-test-build:android": "detox build -c android.emu.debug", + "e2e-test-build:ios": "detox build -c ios.sim.debug", + "e2e-test-run:android": "detox test -c android.emu.debug", + "e2e-test-run:ios": "detox test -c ios.sim.debug", + "e2e-test:android": "run-s e2e-test-build:android e2e-test-run:android", + "e2e-test:ios": "run-s e2e-test-build:ios e2e-test-run:ios" }, "dependencies": { "@expo/react-native-action-sheet": "^4.0.1", @@ -17,8 +23,11 @@ "react-native": "0.73.1", "react-native-blob-util": "^0.19.2", "react-native-image-picker": "^5.6.0", + "react-native-paper": "^5.11.3", "react-native-reanimated-table": "^0.0.2", - "react-native-toast-message": "^2.1.6" + "react-native-safe-area-context": "^4.8.0", + "react-native-toast-message": "^2.1.6", + "react-native-vector-icons": "^10.0.2" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -33,6 +42,11 @@ "prettier": "2.8.8", "typescript": "5.0.4", "patch-package": "^8.0.0", - "postinstall-postinstall": "^2.1.0" + "postinstall-postinstall": "^2.1.0", + "detox": "^20.13.5", + "jest": "^29.7.0", + "metro-react-native-babel-preset": "0.73.10", + "npm-run-all": "^4.1.5", + "power-assert": "^1.6.1" } } diff --git a/example/react-native.config.js b/example/react-native.config.js index a5166956..3f2dd5ac 100644 --- a/example/react-native.config.js +++ b/example/react-native.config.js @@ -6,5 +6,10 @@ module.exports = { [pak.name]: { root: path.join(__dirname, '..'), }, + 'react-native-vector-icons': { + platforms: { + ios: null, + }, + }, }, }; diff --git a/example/src/App.tsx b/example/src/App.tsx index 38d1d741..ba582c3a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,44 +1,34 @@ /* eslint-disable react-native/no-inline-styles */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { StyleSheet, View, - Text, Dimensions, TouchableOpacity, ScrollView, ActivityIndicator, - Platform, Image, - TextInput, Switch, Modal, + TextInput as RNTextInput, } from 'react-native'; -import Marker, { - Position, - ImageFormat, - TextBackgroundType, -} from 'react-native-image-marker'; -import { launchImageLibrary } from 'react-native-image-picker'; -import { - ActionSheetProvider, - useActionSheet, -} from '@expo/react-native-action-sheet'; +import { ImageFormat, TextBackgroundType } from 'react-native-image-marker'; +import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import Toast from 'react-native-toast-message'; -import RNBlobUtil from 'react-native-blob-util'; -import filesize from 'filesize'; +import { PaperProvider, Text } from 'react-native-paper'; -const icon = require('./icon.jpeg'); -const icon1 = require('./yahaha.jpeg'); -const bg = require('./bg.png'); -const base64Bg = require('./bas64bg').default; +import useViewModel from './ViewModel'; +import RadioGroup from './components/RadioGroup'; +import ImageOptions from './components/ImageOptions'; +import TextOptions from './components/TextOptions'; +import { Row, RowSplit } from './components/Row'; const { width, height } = Dimensions.get('window'); const s = StyleSheet.create({ container: { flex: 1, - marginTop: 50, + marginTop: 30, }, op: { marginTop: 10, @@ -50,13 +40,6 @@ const s = StyleSheet.create({ paddingRight: 10, paddingBottom: 5, }, - row: { - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - marginTop: 5, - flex: 1, - }, btn: { padding: 10, paddingTop: 5, @@ -118,7 +101,6 @@ const s = StyleSheet.create({ padding: 10, }, shortTextInput: { - width: 30, height: 30, backgroundColor: '#ffA', borderColor: '#00B96B5A', @@ -130,690 +112,8 @@ const s = StyleSheet.create({ marginRight: 2, textAlign: 'left', }, - rowSplit: { - flex: 1, - alignItems: 'center', - justifyContent: 'flex-start', - flexDirection: 'row', - }, }); -function RowSplit(props: any) { - return {props.children}; -} - -function ImageOptions(props: { - alpha: number; - scale: number; - rotate: number; - quality: number; - setAlpha: (alpha: number) => void; - setScale: (scale: number) => void; - setRotate: (rotate: number) => void; - setQuality: (quality: number) => void; -}) { - const { - alpha, - scale, - rotate, - quality, - setAlpha, - setScale, - setRotate, - setQuality, - } = props; - return ( - - scale: - { - const value = Number(v); - if (value < 0) { - Toast.show({ - type: 'error', - text1: 'scale range error', - text2: 'scale must greater than or equal to 1', - }); - return; - } - setScale(value); - }} - /> - alpha: - { - const value = Number(v); - if (value < 0 || value > 1) { - Toast.show({ - type: 'error', - text1: 'alpha range error', - text2: 'alpha must be between 0 and 1', - }); - return; - } - setAlpha(value); - }} - /> - rotate: - { - const value = Number(v); - if (value < -360 || value > 360) { - Toast.show({ - type: 'error', - text1: 'rotate range error', - text2: 'rotate must be between -360 and 360', - }); - return; - } - setRotate(value); - }} - /> - quality: - { - const value = Number(v); - if (value < 0 || value > 100) { - Toast.show({ - type: 'error', - text1: 'quality range error', - text2: 'quality must be between 0 and 100', - }); - return; - } - setQuality(value); - }} - /> - - ); -} - -function useViewModel() { - const { showActionSheetWithOptions } = useActionSheet(); - const [backgroundFormat, setBackgroundFormat] = useState< - 'normal image' | 'base64' - >('normal image'); - const [waterMarkType, setWaterMarkType] = useState<'text' | 'image'>('text'); - const [saveFormat, setSaveFormat] = useState(ImageFormat.png); - const [image, setImage] = useState(bg); - const [uri, setUri] = useState(''); - const [marker, setMarker] = useState(icon); - const [text, setText] = useState('hello world \n 你好'); - const [useTextShadow, setUseTextShadow] = useState(true); - const [useTextBgStyle, setUseTextBgStyle] = useState(true); - const [textBgStretch, setTextBgStretch] = useState( - TextBackgroundType.none - ); - const [position, setPosition] = useState(Position.topLeft); - const [X, setX] = useState(20); - const [Y, setY] = useState(20); - const [loading, setLoading] = useState(false); - const [show, setShow] = useState(false); - const [underline, setUnderline] = useState(false); - const [italic, setItalic] = useState(false); - const [bold, setBold] = useState(false); - const [strikeThrough, setStrikeThrough] = useState(false); - const [textAlign, setTextAlign] = useState<'left' | 'right' | 'center'>( - 'left' - ); - - const [textRotate, setTextRotate] = useState(0); - - const [textOptionsVisible, setTextOptionsVisible] = useState(false); - - const [backgroundScale, setBackgroundScale] = useState(1); - const [backgroundRotate, setBackgroundRotate] = useState(0); - const [backgroundAlpha, setBackgroundAlpha] = useState(1); - const [watermarkScale, setWatermarkScale] = useState(1); - const [watermarkRotate, setWatermarkRotate] = useState(0); - const [watermarkAlpha, setWatermarkAlpha] = useState(1); - const [quality, setQuality] = useState(100); - const [fileSize, setFileSize] = useState('0'); - const [fontSize, setFontSize] = useState(44); - - function showBackgroundFormatSelector() { - const options = ['normal image', 'base64', 'cancel']; - const cancelButtonIndex = 2; - - showActionSheetWithOptions( - { - options, - title: 'select background format', - cancelButtonIndex, - useModal: true, - }, - (buttonIndex) => { - if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; - else setBackgroundFormat(options[buttonIndex] as any); - } - ); - } - - function showWatermarkTypeSelector() { - const options = ['image', 'text', 'cancel']; - const cancelButtonIndex = 2; - - showActionSheetWithOptions( - { - options, - title: 'select watermark type', - cancelButtonIndex, - useModal: true, - }, - (buttonIndex) => { - if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; - else setWaterMarkType(options[buttonIndex] as any); - } - ); - } - - function showExportResultTypeSelector() { - const options = [ - ImageFormat.png, - ImageFormat.jpg, - ImageFormat.base64, - 'cancel', - ]; - const cancelButtonIndex = 3; - - showActionSheetWithOptions( - { - options, - title: 'select export result format type', - cancelButtonIndex, - useModal: true, - }, - (buttonIndex) => { - if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; - else setSaveFormat(options[buttonIndex] as any); - } - ); - } - - function showPositionSelector() { - const options = [ - Position.topLeft, - Position.topCenter, - Position.topRight, - Position.center, - Position.bottomLeft, - Position.bottomCenter, - Position.bottomRight, - 'cancel', - ]; - const cancelButtonIndex = 7; - - showActionSheetWithOptions( - { - options, - title: 'select export result format type', - cancelButtonIndex, - useModal: true, - }, - (buttonIndex) => { - if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; - else { - setPosition(options[buttonIndex] as any); - } - } - ); - } - - function showTextBgStretchSelector() { - const options = [ - TextBackgroundType.none, - TextBackgroundType.stretchX, - TextBackgroundType.stretchY, - 'cancel', - ]; - const cancelButtonIndex = 3; - - showActionSheetWithOptions( - { - options, - title: 'select text bg stretch type', - cancelButtonIndex, - useModal: true, - }, - (buttonIndex) => { - if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; - else { - setTextBgStretch(options[buttonIndex] as any); - } - } - ); - } - - function showTextAlignSelector() { - const options = ['left', 'right', 'center', 'cancel']; - const cancelButtonIndex = 3; - - showActionSheetWithOptions( - { - options, - title: 'select text align type', - cancelButtonIndex, - useModal: true, - }, - (buttonIndex) => { - if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; - else { - setTextAlign(options[buttonIndex] as any); - } - } - ); - } - - useEffect(() => { - if (backgroundFormat === 'normal image') { - setImage(bg); - } else { - setImage(base64Bg); - } - }, [backgroundFormat]); - - function showLoading() { - setLoading(true); - } - - async function markByPosition() { - showLoading(); - let path = ''; - - try { - if (waterMarkType === 'image') { - path = await Marker.markImage({ - backgroundImage: { - src: image, - scale: backgroundScale, - alpha: backgroundAlpha, - rotate: backgroundRotate, - }, - watermarkImage: { - src: marker, - scale: watermarkScale, - alpha: watermarkAlpha, - rotate: watermarkRotate, - }, - watermarkPositions: { - position, - }, - quality, - saveFormat: saveFormat, - watermarkImages: [ - { - src: icon1, - scale: watermarkScale, - alpha: watermarkAlpha, - rotate: watermarkRotate, - position: { - position: Position.topLeft, - }, - }, - { - src: marker, - scale: watermarkScale, - alpha: watermarkAlpha, - rotate: watermarkRotate, - position: { - position: Position.topRight, - }, - }, - ], - }); - } else { - path = await Marker.markText({ - backgroundImage: { - src: image, - scale: backgroundScale, - alpha: backgroundAlpha, - rotate: backgroundRotate, - }, - watermarkTexts: [ - { - text, - positionOptions: { - position, - }, - style: { - color: '#FF0000AA', - fontName: 'Arial', - fontSize, - underline, - bold, - italic, - strikeThrough, - textAlign: textAlign, - rotate: textRotate, - shadowStyle: useTextShadow - ? { - dx: 10.5, - dy: 20.8, - radius: 20.9, - color: '#0000FF', - } - : null, - textBackgroundStyle: useTextBgStyle - ? { - type: textBgStretch, - paddingBottom: '15%', - paddingRight: '10%', - paddingTop: '15%', - paddingLeft: '100', - color: '#0f0A', - } - : null, - }, - }, - { - text: `text marker normal`, - positionOptions: { - position: Position.center, - }, - style: { - color: '#FF00AA9F', - fontName: 'NotoSansSC-Regular', - fontSize, - underline, - bold, - italic, - strikeThrough, - textAlign: textAlign, - rotate: textRotate, - shadowStyle: useTextShadow - ? { - dx: 10.5, - dy: 20.8, - radius: 20.9, - color: '#00EEFF', - } - : null, - textBackgroundStyle: useTextBgStyle - ? { - type: textBgStretch, - padding: '10%', - color: '#0fA', - cornerRadius: { - topLeft: { - x: '20%', - y: '50%', - }, - topRight: { - x: '20%', - y: '50%', - }, - }, - } - : null, - }, - }, - ], - quality, - saveFormat: saveFormat, - }); - } - setUri( - saveFormat === ImageFormat.base64 - ? path - : Platform.OS === 'android' - ? 'file:' + path - : path - ); - setLoading(false); - setShow(true); - const stat = await RNBlobUtil.fs.stat(path); - setFileSize(filesize.filesize(stat.size)); - } catch (err) { - console.log('===================================='); - console.log(err, 'err'); - console.log('===================================='); - } - } - - useEffect(() => { - if (position) { - markByPosition(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [position]); - - async function mark() { - showLoading(); - let path = ''; - try { - if (waterMarkType === 'image') { - path = await Marker.markImage({ - backgroundImage: { - src: image, - scale: backgroundScale, - rotate: backgroundRotate, - alpha: backgroundAlpha, - }, - watermarkImages: [ - { - src: marker, - scale: watermarkScale, - alpha: watermarkAlpha, - rotate: watermarkRotate, - position: { X, Y }, - }, - { - src: icon1, - scale: watermarkScale, - alpha: watermarkAlpha, - rotate: watermarkRotate, - position: { X: 200, Y: 100 }, - }, - ], - quality, - saveFormat: saveFormat, - }); - } else { - path = await Marker.markText({ - backgroundImage: { - src: image, - scale: backgroundScale, - alpha: backgroundAlpha, - rotate: backgroundRotate, - }, - watermarkTexts: [ - { - text, - positionOptions: { - X, - Y, - }, - style: { - underline, - strikeThrough, - color: '#FF0', - fontName: 'NotoSansSC-Regular', - fontSize, - bold, - italic, - textAlign: textAlign, - rotate: textRotate, - shadowStyle: useTextShadow - ? { - dx: 10.5, - dy: 20.8, - radius: 20.9, - color: '#0000FF', - } - : null, - textBackgroundStyle: useTextBgStyle - ? { - type: textBgStretch, - paddingX: 10, - paddingY: 10, - color: '#00B96B', - } - : null, - }, - }, - { - text, - positionOptions: { - X: 500, - Y: 600, - }, - style: { - underline: true, - strikeThrough: true, - bold: true, - italic: true, - color: '#FF0', - fontSize, - textAlign: textAlign, - rotate: textRotate, - shadowStyle: useTextShadow - ? { - dx: 10.5, - dy: 20.8, - radius: 20.9, - color: '#0000FF', - } - : null, - textBackgroundStyle: useTextBgStyle - ? { - type: textBgStretch, - // paddingX: 10, - // paddingY: 10, - padding: '10%', - color: '#0f09', - } - : null, - }, - }, - ], - quality, - saveFormat: saveFormat, - }); - } - setUri( - saveFormat === ImageFormat.base64 - ? path - : Platform.OS === 'android' - ? 'file:' + path - : path - ); - setShow(true); - setLoading(false); - const stat = await RNBlobUtil.fs.stat(path); - setFileSize(filesize.filesize(stat.size)); - } catch (error) { - console.log('===================================='); - console.log(error, 'error'); - console.log('===================================='); - } - } - - async function pickImage(type: any) { - const response = await launchImageLibrary({ - quality: 0.5, - mediaType: 'photo', - maxWidth: 2000, - maxHeight: 2000, - selectionLimit: 1, - }); - - if (response.didCancel) { - console.log('User cancelled photo picker'); - } else if (response.errorCode) { - console.log('ImagePickerManager Error: ', response.errorMessage); - } else { - // You can display the image using either: - // const source = {uri: 'data:image/jpeg;base64,' + response.data, isStatic: true}; - const myUri = response.assets?.[0]?.uri; - if (type === 'image') { - setImage(myUri); - } else { - setMarker(myUri); - } - } - } - - return { - state: { - image, - uri, - marker, - loading, - show, - backgroundFormat, - saveFormat, - useTextShadow, - useTextBgStyle, - textBgStretch, - waterMarkType, - text, - position, - underline, - strikeThrough, - bold, - italic, - X, - Y, - backgroundScale, - backgroundAlpha, - backgroundRotate, - watermarkScale, - watermarkAlpha, - watermarkRotate, - textOptionsVisible, - textAlign, - textRotate, - quality, - fileSize, - fontSize, - }, - setLoading, - setImage, - setMarker, - setShow, - setUri, - setSaveFormat, - setUseTextShadow, - setUseTextBgStyle, - setTextBgStretch, - mark, - markByPosition, - pickImage, - showBackgroundFormatSelector, - showWatermarkTypeSelector, - showExportResultTypeSelector, - setText, - showPositionSelector, - showTextBgStretchSelector, - setItalic, - setBold, - setStrikeThrough, - setUnderline, - setX, - setY, - setBackgroundAlpha, - setBackgroundScale, - setBackgroundRotate, - setWatermarkAlpha, - setWatermarkRotate, - setWatermarkScale, - setTextOptionsVisible, - setTextAlign, - setTextRotate, - showTextAlignSelector, - setQuality, - setFontSize, - }; -} - function App() { const { state, @@ -821,9 +121,6 @@ function App() { mark, setUseTextShadow, setUseTextBgStyle, - showBackgroundFormatSelector, - showWatermarkTypeSelector, - showExportResultTypeSelector, setText, showPositionSelector, showTextBgStretchSelector, @@ -844,352 +141,341 @@ function App() { setTextRotate, setQuality, setFontSize, + setBackgroundFormat, + setWaterMarkType, + setSaveFormat, } = useViewModel(); return ( - - - - - - background image format: - - - - {state.backgroundFormat} - - - - - - watermark type: - - - - {state.waterMarkType} - - - - - - export result format: - - - - {state.saveFormat} - - - - - - - + + + + + + + + + + + + + + + + pickImage('image'), + }} + rotate={{ + value: state.backgroundRotate, + setValue: setBackgroundRotate, + testID: 'bgRotate', + }} + scale={{ + value: state.backgroundScale, + setValue: setBackgroundScale, + testID: 'bgScale', + }} + alpha={{ + value: state.backgroundAlpha, + setValue: setBackgroundAlpha, + testID: 'bgAlpha', + }} + quality={{ + value: state.quality, + setValue: setQuality, + testID: 'bgQuality', + }} + /> + + {state.waterMarkType === 'image' ? ( + + pickImage('mark'), }} - > - pickImage('image')} - > - select bg - - - - - + /> - - {state.waterMarkType === 'image' ? ( - - + ) : ( + + + + )} + + + + + + given position: + + + + {state.position} + + + + + + custom x/y: + + + X: + setX(v)} + /> + Y: + setY(v)} + /> + + mark + + + + + + + + + + text shadow: + + + + text background: + + + + {state.useTextBgStyle ? ( + + + text bg stretch: pickImage('mark')} + style={s.btn} + onPress={showTextBgStretchSelector} > - select watermark icon + + {state.textBgStretch == null || + state.textBgStretch === TextBackgroundType.none + ? 'fit' + : state.textBgStretch} + - - - - - ) : ( - - - text watermark: - - - - fontSize: - { - const value = Number(v); - if (value <= 0) { - Toast.show({ - type: 'error', - text1: 'fontSize range error', - text2: 'fontSize must be greater than 0', - }); - return; - } - setFontSize(value); - }} - /> + + text align: setTextOptionsVisible(true)} + style={s.btn} + onPress={showTextAlignSelector} > - options + {state.textAlign} - - )} - - - - - - given position: - - - - {state.position} - - - - - - custom x/y: - - - X: - setX(v)} - /> - Y: - setY(v)} - /> - - mark - - - - - - - - - - text shadow: - + + ) : null} + + + + underline: - text background: - - {state.useTextBgStyle ? ( - - - text bg stretch: - - - {state.textBgStretch == null || - state.textBgStretch === TextBackgroundType.none - ? 'fit' - : state.textBgStretch} - - - - - text align: - - {state.textAlign} - - - - ) : null} - - - - underline: - - - - + + + + italic: - - italic: - - - - + - - - - - bold: - - - - + + + + + + bold: - - strikeThrough: - - - - + + + + + + strikeThrough: - - - rotate: - { - const value = Number(v); - if (value < -360 || value > 360) { - Toast.show({ - type: 'error', - text1: 'rotate range error', - text2: 'rotate must be between -360 and 360', - }); - return; - } - setTextRotate(value); - }} + - {/* + + + + + rotate: + { + const value = Number(v); + if (value < -360 || value > 360) { + Toast.show({ + type: 'error', + text1: 'rotate range error', + text2: 'rotate must be between -360 and 360', + }); + return; + } + setTextRotate(value); + }} + /> + + {/* */} - { - setTextOptionsVisible(false); - }} - > - Confirm - - - + { + setTextOptionsVisible(false); + }} + > + Confirm + + - - - - result file size: {state.fileSize} - - {state.show ? ( - - ) : null} - + + + + result file size:{state.fileSize} + + + file path:{state.uri} + + {state.show ? ( + + ) : null} + {state.loading && ( loading... )} - + ); } export default function AppContainer() { return ( - <> - - - + + <> + + + + ); } diff --git a/example/src/ViewModel.ts b/example/src/ViewModel.ts new file mode 100644 index 00000000..0a52901f --- /dev/null +++ b/example/src/ViewModel.ts @@ -0,0 +1,536 @@ +import { useState, useEffect } from 'react'; +import { Platform } from 'react-native'; +import { launchImageLibrary } from 'react-native-image-picker'; +import { useActionSheet } from '@expo/react-native-action-sheet'; +import Marker, { + ImageFormat, + Position, + TextBackgroundType, +} from 'react-native-image-marker'; +import RNBlobUtil from 'react-native-blob-util'; +import filesize from 'filesize'; + +const icon = require('./icon.jpeg'); +const icon1 = require('./yahaha.jpeg'); +const bg = require('./bg.png'); +const base64Bg = require('./bas64bg').default; + +function useViewModel() { + const { showActionSheetWithOptions } = useActionSheet(); + const [backgroundFormat, setBackgroundFormat] = useState<'image' | 'base64'>( + 'image' + ); + const [waterMarkType, setWaterMarkType] = useState<'text' | 'image'>('text'); + const [saveFormat, setSaveFormat] = useState(ImageFormat.png); + const [image, setImage] = useState(bg); + const [uri, setUri] = useState(''); + const [marker, setMarker] = useState(icon); + const [text, setText] = useState('hello world \n 你好'); + const [useTextShadow, setUseTextShadow] = useState(true); + const [useTextBgStyle, setUseTextBgStyle] = useState(true); + const [textBgStretch, setTextBgStretch] = useState( + TextBackgroundType.none + ); + const [position, setPosition] = useState(Position.topLeft); + const [X, setX] = useState(20); + const [Y, setY] = useState(20); + const [loading, setLoading] = useState(false); + const [show, setShow] = useState(false); + const [underline, setUnderline] = useState(false); + const [italic, setItalic] = useState(false); + const [bold, setBold] = useState(false); + const [strikeThrough, setStrikeThrough] = useState(false); + const [textAlign, setTextAlign] = useState<'left' | 'right' | 'center'>( + 'left' + ); + + const [textRotate, setTextRotate] = useState(0); + + const [textOptionsVisible, setTextOptionsVisible] = useState(false); + + const [backgroundScale, setBackgroundScale] = useState(1); + const [backgroundRotate, setBackgroundRotate] = useState(0); + const [backgroundAlpha, setBackgroundAlpha] = useState(1); + const [watermarkScale, setWatermarkScale] = useState(1); + const [watermarkRotate, setWatermarkRotate] = useState(0); + const [watermarkAlpha, setWatermarkAlpha] = useState(1); + const [quality, setQuality] = useState(100); + const [fileSize, setFileSize] = useState('0'); + const [fontSize, setFontSize] = useState(44); + + function showPositionSelector() { + const options = [ + Position.topLeft, + Position.topCenter, + Position.topRight, + Position.center, + Position.bottomLeft, + Position.bottomCenter, + Position.bottomRight, + 'cancel', + ]; + const cancelButtonIndex = 7; + + showActionSheetWithOptions( + { + options, + title: 'select export result format type', + cancelButtonIndex, + useModal: true, + }, + (buttonIndex) => { + if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; + else { + setPosition(options[buttonIndex] as any); + } + } + ); + } + + function showTextBgStretchSelector() { + const options = [ + TextBackgroundType.none, + TextBackgroundType.stretchX, + TextBackgroundType.stretchY, + 'cancel', + ]; + const cancelButtonIndex = 3; + + showActionSheetWithOptions( + { + options, + title: 'select text bg stretch type', + cancelButtonIndex, + useModal: true, + }, + (buttonIndex) => { + if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; + else { + setTextBgStretch(options[buttonIndex] as any); + } + } + ); + } + + function showTextAlignSelector() { + const options = ['left', 'right', 'center', 'cancel']; + const cancelButtonIndex = 3; + + showActionSheetWithOptions( + { + options, + title: 'select text align type', + cancelButtonIndex, + useModal: true, + }, + (buttonIndex) => { + if (buttonIndex === cancelButtonIndex || buttonIndex == null) return; + else { + setTextAlign(options[buttonIndex] as any); + } + } + ); + } + + useEffect(() => { + if (backgroundFormat === 'image') { + setImage(bg); + } else { + setImage(base64Bg); + } + }, [backgroundFormat]); + + function showLoading() { + setLoading(true); + } + + async function markByPosition() { + showLoading(); + let path = ''; + + try { + if (waterMarkType === 'image') { + path = await Marker.markImage({ + backgroundImage: { + src: image, + scale: backgroundScale, + alpha: backgroundAlpha, + rotate: backgroundRotate, + }, + watermarkImage: { + src: marker, + scale: watermarkScale, + alpha: watermarkAlpha, + rotate: watermarkRotate, + }, + watermarkPositions: { + position, + }, + quality, + saveFormat: saveFormat, + watermarkImages: [ + { + src: icon1, + scale: watermarkScale, + alpha: watermarkAlpha, + rotate: watermarkRotate, + position: { + position: Position.topLeft, + }, + }, + { + src: marker, + scale: watermarkScale, + alpha: watermarkAlpha, + rotate: watermarkRotate, + position: { + position: Position.topRight, + }, + }, + ], + }); + } else { + path = await Marker.markText({ + backgroundImage: { + src: image, + scale: backgroundScale, + alpha: backgroundAlpha, + rotate: backgroundRotate, + }, + watermarkTexts: [ + { + text, + positionOptions: { + position, + }, + style: { + color: '#FF0000AA', + fontName: 'Arial', + fontSize, + underline, + bold, + italic, + strikeThrough, + textAlign: textAlign, + rotate: textRotate, + shadowStyle: useTextShadow + ? { + dx: 10.5, + dy: 20.8, + radius: 20.9, + color: '#0000FF', + } + : null, + textBackgroundStyle: useTextBgStyle + ? { + type: textBgStretch, + paddingBottom: '15%', + paddingRight: '10%', + paddingTop: '15%', + paddingLeft: '100', + color: '#0f0A', + } + : null, + }, + }, + { + text: `text marker normal`, + positionOptions: { + position: Position.center, + }, + style: { + color: '#FF00AA9F', + fontName: 'NotoSansSC-Regular', + fontSize, + underline, + bold, + italic, + strikeThrough, + textAlign: textAlign, + rotate: textRotate, + shadowStyle: useTextShadow + ? { + dx: 10.5, + dy: 20.8, + radius: 20.9, + color: '#00EEFF', + } + : null, + textBackgroundStyle: useTextBgStyle + ? { + type: textBgStretch, + padding: '10%', + color: '#0fA', + cornerRadius: { + topLeft: { + x: '20%', + y: '50%', + }, + topRight: { + x: '20%', + y: '50%', + }, + }, + } + : null, + }, + }, + ], + quality, + saveFormat: saveFormat, + }); + } + setUri( + saveFormat === ImageFormat.base64 + ? path + : Platform.OS === 'android' + ? 'file:' + path + : path + ); + setLoading(false); + setShow(true); + const stat = await RNBlobUtil.fs.stat(path); + setFileSize(filesize.filesize(stat.size)); + } catch (err) { + console.log('===================================='); + console.log(err, 'err'); + console.log('===================================='); + } + } + + useEffect(() => { + if (position) { + markByPosition(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [position]); + + async function mark() { + showLoading(); + let path = ''; + try { + if (waterMarkType === 'image') { + path = await Marker.markImage({ + backgroundImage: { + src: image, + scale: backgroundScale, + rotate: backgroundRotate, + alpha: backgroundAlpha, + }, + watermarkImages: [ + { + src: marker, + scale: watermarkScale, + alpha: watermarkAlpha, + rotate: watermarkRotate, + position: { X, Y }, + }, + { + src: icon1, + scale: watermarkScale, + alpha: watermarkAlpha, + rotate: watermarkRotate, + position: { X: 200, Y: 100 }, + }, + ], + quality, + saveFormat: saveFormat, + }); + } else { + path = await Marker.markText({ + backgroundImage: { + src: image, + scale: backgroundScale, + alpha: backgroundAlpha, + rotate: backgroundRotate, + }, + watermarkTexts: [ + { + text, + positionOptions: { + X, + Y, + }, + style: { + underline, + strikeThrough, + color: '#FF0', + fontName: 'NotoSansSC-Regular', + fontSize, + bold, + italic, + textAlign: textAlign, + rotate: textRotate, + shadowStyle: useTextShadow + ? { + dx: 10.5, + dy: 20.8, + radius: 20.9, + color: '#0000FF', + } + : null, + textBackgroundStyle: useTextBgStyle + ? { + type: textBgStretch, + paddingX: 10, + paddingY: 10, + color: '#00B96B', + } + : null, + }, + }, + { + text, + positionOptions: { + X: 500, + Y: 600, + }, + style: { + underline: true, + strikeThrough: true, + bold: true, + italic: true, + color: '#FF0', + fontSize, + textAlign: textAlign, + rotate: textRotate, + shadowStyle: useTextShadow + ? { + dx: 10.5, + dy: 20.8, + radius: 20.9, + color: '#0000FF', + } + : null, + textBackgroundStyle: useTextBgStyle + ? { + type: textBgStretch, + // paddingX: 10, + // paddingY: 10, + padding: '10%', + color: '#0f09', + } + : null, + }, + }, + ], + quality, + saveFormat: saveFormat, + }); + } + setUri( + saveFormat === ImageFormat.base64 + ? path + : Platform.OS === 'android' + ? 'file:' + path + : path + ); + setShow(true); + setLoading(false); + const stat = await RNBlobUtil.fs.stat(path); + setFileSize(filesize.filesize(stat.size)); + } catch (error) { + console.log('===================================='); + console.log(error, 'error'); + console.log('===================================='); + } + } + + async function pickImage(type: any) { + const response = await launchImageLibrary({ + quality: 0.5, + mediaType: 'photo', + maxWidth: 2000, + maxHeight: 2000, + selectionLimit: 1, + }); + + if (response.didCancel) { + console.log('User cancelled photo picker'); + } else if (response.errorCode) { + console.log('ImagePickerManager Error: ', response.errorMessage); + } else { + // You can display the image using either: + // const source = {uri: 'data:image/jpeg;base64,' + response.data, isStatic: true}; + const myUri = response.assets?.[0]?.uri; + if (type === 'image') { + setImage(myUri); + } else { + setMarker(myUri); + } + } + } + + return { + state: { + image, + uri, + marker, + loading, + show, + backgroundFormat, + saveFormat, + useTextShadow, + useTextBgStyle, + textBgStretch, + waterMarkType, + text, + position, + underline, + strikeThrough, + bold, + italic, + X, + Y, + backgroundScale, + backgroundAlpha, + backgroundRotate, + watermarkScale, + watermarkAlpha, + watermarkRotate, + textOptionsVisible, + textAlign, + textRotate, + quality, + fileSize, + fontSize, + }, + setLoading, + setImage, + setMarker, + setShow, + setUri, + setSaveFormat, + setUseTextShadow, + setUseTextBgStyle, + setTextBgStretch, + mark, + markByPosition, + pickImage, + setWaterMarkType, + setText, + showPositionSelector, + showTextBgStretchSelector, + setItalic, + setBold, + setStrikeThrough, + setUnderline, + setX, + setY, + setBackgroundAlpha, + setBackgroundScale, + setBackgroundRotate, + setWatermarkAlpha, + setWatermarkRotate, + setWatermarkScale, + setTextOptionsVisible, + setTextAlign, + setTextRotate, + showTextAlignSelector, + setQuality, + setFontSize, + setBackgroundFormat, + }; +} + +export default useViewModel; diff --git a/example/src/components/ImageOptions.tsx b/example/src/components/ImageOptions.tsx new file mode 100644 index 00000000..d8cb4bf2 --- /dev/null +++ b/example/src/components/ImageOptions.tsx @@ -0,0 +1,144 @@ +import * as React from 'react'; +import { Card, Button } from 'react-native-paper'; +import { StyleSheet } from 'react-native'; + +const s = StyleSheet.create({ + row: { + marginVertical: 8, + flex: 1, + marginHorizontal: 8, + paddingHorizontal: 8, + }, +}); + +import TextInput from './TextInput'; + +export interface ValueOptions { + value: number; + setValue: (value: number) => void; + testID?: string; +} + +export default function ImageOptions(props: { + alpha: ValueOptions; + scale: ValueOptions; + rotate: ValueOptions; + quality?: ValueOptions; + pickerOptions: { + pickImage: () => void; + testID?: string; + label: string; + }; +}) { + const { alpha, scale, rotate, quality } = props; + return ( + + + + + + { + const value = Number(v); + scale.setValue(value); + }} + validate={(v) => { + const value = Number(v); + if (value < 0) { + return { + pass: false, + message: 'scale must be greater than 0', + }; + } + return { + pass: true, + message: '', + }; + }} + /> + { + const value = Number(v); + alpha.setValue(value); + }} + validate={(v) => { + const value = Number(v); + if (value < 0 || value > 1) { + return { + pass: false, + message: 'alpha must be between 0 and 1', + }; + } + return { + pass: true, + message: '', + }; + }} + /> + { + const value = Number(v); + rotate.setValue(value); + }} + validate={(v) => { + const value = Number(v); + if (value < -360 || value > 360) { + return { + pass: false, + message: 'rotate must be between -360 and 360', + }; + } + return { + pass: true, + message: '', + }; + }} + /> + {quality && ( + { + const value = Number(v); + quality.setValue(value); + }} + validate={(v) => { + const value = Number(v); + if (value < 0 || value > 100) { + return { + pass: false, + message: 'quality must be between 0 and 100', + }; + } + return { + pass: true, + message: '', + }; + }} + /> + )} + + + ); +} diff --git a/example/src/components/RadioGroup.tsx b/example/src/components/RadioGroup.tsx new file mode 100644 index 00000000..594715b6 --- /dev/null +++ b/example/src/components/RadioGroup.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { RadioButton, Card } from 'react-native-paper'; +import { View, StyleSheet } from 'react-native'; + +const s = StyleSheet.create({ + card: { + marginVertical: 8, + flex: 1, + marginHorizontal: 8, + paddingHorizontal: 8, + }, + cardTitle: { + minHeight: 16, + paddingLeft: 8, + paddingTop: 8, + marginRight: 8, + }, + cardTitleText: { + lineHeight: 16, + fontWeight: 'bold', + minHeight: 16, + }, + cardContent: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'flex-start', + flex: 1, + paddingVertical: 0, + paddingHorizontal: 0, + paddingBottom: 0, + }, + radioButtons: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'flex-start', + flex: 1, + }, + radioItem: { + paddingVertical: 0, + paddingLeft: 0, + paddingRight: 0, + marginLeft: 0, + }, +}); + +export default (props: { + defaultValue: string; + title?: string; + subTitle?: string; + onValueChange: (value: any) => void; + options: Array<{ + label: string; + value: string; + testID?: string; + }>; +}) => { + const [value, setValue] = React.useState(props.defaultValue); + + React.useEffect(() => { + typeof props.onValueChange === 'function' && props.onValueChange(value); + }, [props, value]); + + return ( + + {props.title && ( + + )} + + setValue(newValue)} + value={value} + > + + {props.options.map((option) => ( + + ))} + + + + + ); +}; diff --git a/example/src/components/Row.tsx b/example/src/components/Row.tsx new file mode 100644 index 00000000..c1e32d3a --- /dev/null +++ b/example/src/components/Row.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +const s = StyleSheet.create({ + row: { + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + marginTop: 5, + flex: 1, + }, + rowSplit: { + flex: 1, + alignItems: 'center', + justifyContent: 'flex-start', + flexDirection: 'row', + }, +}); + +export function RowSplit(props: any) { + return {props.children}; +} + +export function Row(props: any) { + return {props.children}; +} diff --git a/example/src/components/TextInput.tsx b/example/src/components/TextInput.tsx new file mode 100644 index 00000000..9c38e687 --- /dev/null +++ b/example/src/components/TextInput.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { TextInput, HelperText } from 'react-native-paper'; +import { StyleSheet, View } from 'react-native'; + +const s = StyleSheet.create({ + shortTextInput: { + height: 30, + backgroundColor: '#ffA', + borderColor: '#00B96B5A', + borderWidth: 0, + padding: 0, + fontSize: 12, + }, +}); + +export default (props: { + label: string; + defaultValue: any; + placeholder: string; + multiline?: boolean; + onChangeText: (value: any) => void; + validate: (value: string) => { pass: boolean; message: string }; + testID?: string; + textColor?: string; + style?: any; + numberOfLines?: number; + contentStyle?: any; +}) => { + const [value, setValue] = React.useState(props.defaultValue); + const [error, setError] = React.useState(''); + + React.useEffect(() => { + typeof props.onChangeText === 'function' && props.onChangeText(value); + }, [props, value]); + + return ( + + { + if (typeof props.validate === 'function') { + const { pass, message } = props.validate(v); + if (!pass) { + setError(message); + return; + } else { + setError(''); + } + } + setValue(value); + }} + autoFocus={false} + /> + + {error} + + + ); +}; diff --git a/example/src/components/TextOptions.tsx b/example/src/components/TextOptions.tsx new file mode 100644 index 00000000..b116c521 --- /dev/null +++ b/example/src/components/TextOptions.tsx @@ -0,0 +1,172 @@ +import * as React from 'react'; +import { Card, Text, Switch } from 'react-native-paper'; +import { StyleSheet } from 'react-native'; + +import TextInput from './TextInput'; +import { Row, RowSplit } from './Row'; + +const s = StyleSheet.create({ + label: { + marginRight: 8, + }, + container: { + flex: 1, + }, + markTextInputContentStyle: { + minHeight: 120, + paddingTop: 8, + paddingBottom: 8, + }, + markTextInputContainerStyle: { + minHeight: 120, + marginRight: 8, + }, + splitContent: { + display: 'flex', + flexDirection: 'column', + }, +}); + +export default function TextOptions(props: { + text: string; + setText: (value: string) => void; + fontSize: number; + setFontSize: (value: number) => void; + useTextShadow: boolean; + setUseTextShadow: (value: boolean) => void; + useTextBgStyle: boolean; + setUseTextBgStyle: (value: boolean) => void; + underline: boolean; + setUnderline: (value: boolean) => void; + italic: boolean; + setItalic: (value: boolean) => void; + bold: boolean; + setBold: (value: boolean) => void; + strikeThrough: boolean; + setStrikeThrough: (value: boolean) => void; +}) { + const { + text, + fontSize, + setText, + setFontSize, + useTextBgStyle, + setUseTextBgStyle, + useTextShadow, + setUseTextShadow, + underline, + setUnderline, + italic, + setItalic, + bold, + setBold, + strikeThrough, + setStrikeThrough, + } = props; + return ( + + + + + { + setText(v); + }} + validate={(v) => { + if (!v) { + return { + pass: false, + message: 'text water mark must not be empty', + }; + } + return { + pass: true, + message: '', + }; + }} + /> + + + + { + const value = Number(v); + if (value <= 0) { + return { + pass: false, + message: 'font size must be greater than 0', + }; + } + return { + pass: true, + message: '', + }; + }} + defaultValue={fontSize} + onChangeText={setFontSize} + /> + + + shadow: + + + + + + + background: + + + + + + + underline: + + + + + + + + italic: + + + + + + + + + + bold: + + + + + + + + strikeThrough: + + + + + + + + + ); +}