Code cleanup and remove vague dependencies (#3)
* cleanup * gemfile dep fix
This commit is contained in:
47
.detoxrc.js
47
.detoxrc.js
@@ -1,47 +0,0 @@
|
|||||||
/** @type {Detox.DetoxConfig} */
|
|
||||||
module.exports = {
|
|
||||||
testRunner: {
|
|
||||||
args: {
|
|
||||||
$0: 'jest',
|
|
||||||
config: 'e2e/jest.config.js',
|
|
||||||
},
|
|
||||||
jest: {
|
|
||||||
setupTimeout: 120000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apps: {
|
|
||||||
'android.debug': {
|
|
||||||
type: 'android.apk',
|
|
||||||
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
|
|
||||||
testBinaryPath:
|
|
||||||
'android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk',
|
|
||||||
build:
|
|
||||||
'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug --no-daemon',
|
|
||||||
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 --no-daemon',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devices: {
|
|
||||||
emulator: {
|
|
||||||
type: 'android.emulator',
|
|
||||||
// avdName is set by reactivecircus/android-emulator-runner in CI.
|
|
||||||
// For local runs, create an AVD named below or override via DETOX_AVD_NAME.
|
|
||||||
device: {avdName: process.env.DETOX_AVD_NAME ?? 'Pixel_6_API_34'},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
configurations: {
|
|
||||||
'android.emu.debug': {
|
|
||||||
device: 'emulator',
|
|
||||||
app: 'android.debug',
|
|
||||||
},
|
|
||||||
'android.emu.release': {
|
|
||||||
device: 'emulator',
|
|
||||||
app: 'android.release',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
120
.github/workflows/e2e-android.yml
vendored
120
.github/workflows/e2e-android.yml
vendored
@@ -1,120 +0,0 @@
|
|||||||
name: E2E Android (Detox)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, master]
|
|
||||||
pull_request:
|
|
||||||
branches: [main, master]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
e2e:
|
|
||||||
name: Detox E2E (debug)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
ANDROID_HOME: /home/runner/android-sdk
|
|
||||||
ANDROID_SDK_ROOT: /home/runner/android-sdk
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: temurin
|
|
||||||
java-version: 17
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Cache Android SDK
|
|
||||||
id: cache-sdk
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: /home/runner/android-sdk
|
|
||||||
key: android-sdk-${{ hashFiles('android/build.gradle') }}
|
|
||||||
|
|
||||||
- name: Install Android SDK
|
|
||||||
if: steps.cache-sdk.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
mkdir -p $ANDROID_HOME/cmdline-tools
|
|
||||||
curl -sSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o cmdline-tools.zip
|
|
||||||
unzip -q cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools
|
|
||||||
mv $ANDROID_HOME/cmdline-tools/cmdline-tools $ANDROID_HOME/cmdline-tools/latest
|
|
||||||
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_HOME \
|
|
||||||
"platforms;android-35" "build-tools;35.0.0" "platform-tools"
|
|
||||||
|
|
||||||
- name: Cache Gradle
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
android/.gradle
|
|
||||||
key: gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/**/*.gradle') }}
|
|
||||||
restore-keys: gradle-
|
|
||||||
|
|
||||||
- name: chmod gradlew
|
|
||||||
run: chmod +x android/gradlew
|
|
||||||
|
|
||||||
# debuggableVariants=[] in build.gradle means JS is bundled into debug APK too,
|
|
||||||
# so no Metro server is needed at runtime.
|
|
||||||
- name: Build APKs
|
|
||||||
working-directory: android
|
|
||||||
run: ./gradlew assembleDebug assembleAndroidTest --no-daemon
|
|
||||||
|
|
||||||
- name: Enable KVM
|
|
||||||
run: |
|
|
||||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
||||||
sudo udevadm control --reload-rules
|
|
||||||
sudo udevadm trigger --name-match=kvm
|
|
||||||
|
|
||||||
# Cache the AVD snapshot to speed up subsequent runs
|
|
||||||
- name: Cache AVD snapshot
|
|
||||||
uses: actions/cache@v5
|
|
||||||
id: avd-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.android/avd/*
|
|
||||||
~/.android/adb*
|
|
||||||
key: avd-34-x86_64-google_apis
|
|
||||||
|
|
||||||
- name: Create AVD snapshot
|
|
||||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
with:
|
|
||||||
api-level: 34
|
|
||||||
target: google_apis
|
|
||||||
arch: x86_64
|
|
||||||
profile: pixel_6
|
|
||||||
avd-name: Pixel_6_API_34
|
|
||||||
force-avd-creation: false
|
|
||||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
disable-animations: false
|
|
||||||
script: echo "AVD snapshot ready"
|
|
||||||
|
|
||||||
- name: Run Detox E2E
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
with:
|
|
||||||
api-level: 34
|
|
||||||
target: google_apis
|
|
||||||
arch: x86_64
|
|
||||||
profile: pixel_6
|
|
||||||
avd-name: Pixel_6_API_34
|
|
||||||
force-avd-creation: false
|
|
||||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
disable-animations: true
|
|
||||||
script: npx detox test --configuration android.emu.debug --headless --record-logs failing
|
|
||||||
|
|
||||||
- name: Upload Detox logs
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: detox-logs-${{ github.run_number }}
|
|
||||||
path: artifacts/
|
|
||||||
retention-days: 7
|
|
||||||
20
App.tsx
20
App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import React, {useState, useCallback, useEffect} from 'react';
|
import React, {useState, useCallback} from 'react';
|
||||||
import {Alert} from 'react-native';
|
import {Alert} from 'react-native';
|
||||||
import HomeScreen from './src/screens/HomeScreen';
|
import HomeScreen from './src/screens/HomeScreen';
|
||||||
import ScanScreen from './src/screens/ScanScreen';
|
import ScanScreen from './src/screens/ScanScreen';
|
||||||
@@ -14,24 +14,6 @@ export default function App() {
|
|||||||
useDocumentStore();
|
useDocumentStore();
|
||||||
|
|
||||||
const [screen, setScreen] = useState<Screen>('home');
|
const [screen, setScreen] = useState<Screen>('home');
|
||||||
|
|
||||||
// Seed a fake document when launched with detoxSeedDocument=1 (E2E only)
|
|
||||||
useEffect(() => {
|
|
||||||
if (__DEV__ || process.env.DETOX_SEED) {
|
|
||||||
const args = (global as any).__detox_launch_args ?? {};
|
|
||||||
if (args.detoxSeedDocument === '1') {
|
|
||||||
createDocument([
|
|
||||||
{
|
|
||||||
id: 'seed-page-1',
|
|
||||||
uri: 'https://via.placeholder.com/600x800.jpg',
|
|
||||||
width: 600,
|
|
||||||
height: 800,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
const [pendingPages, setPendingPages] = useState<ScannedPage[]>([]);
|
const [pendingPages, setPendingPages] = useState<ScannedPage[]>([]);
|
||||||
const [viewingDoc, setViewingDoc] = useState<ScannedDocument | null>(null);
|
const [viewingDoc, setViewingDoc] = useState<ScannedDocument | null>(null);
|
||||||
|
|
||||||
|
|||||||
4
Gemfile
4
Gemfile
@@ -1,7 +1,7 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||||
ruby ">= 2.6.10"
|
ruby ">= 3.1.0"
|
||||||
|
|
||||||
gem 'cocoapods', '~> 1.13'
|
gem 'cocoapods', '~> 1.13'
|
||||||
gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'
|
gem 'activesupport', '>= 7.2.3.1'
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -57,15 +57,6 @@ Unit tests (Jest + React Native Testing Library):
|
|||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
E2E (Detox, Android emulator):
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run test:e2e:build
|
|
||||||
npm run test:e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
The E2E build currently targets `android.emu.debug`. There's a launch-arg seed path (`detoxSeedDocument=1`) in `App.tsx` for preloading a fake document, but the bridge that wires it through to the native side isn't hooked up yet — so the viewer E2E is disabled for now.
|
|
||||||
|
|
||||||
## Builds
|
## Builds
|
||||||
|
|
||||||
Android release APKs are split per-ABI (armeabi-v7a, arm64-v8a, x86_64) — see `android/app/build.gradle`. Per-arch APKs are smaller than a universal one.
|
Android release APKs are split per-ABI (armeabi-v7a, arm64-v8a, x86_64) — see `android/app/build.gradle`. Per-arch APKs are smaller than a universal one.
|
||||||
@@ -78,11 +69,10 @@ CI builds live in `.github/workflows/`.
|
|||||||
App.tsx # screen switcher + top-level state
|
App.tsx # screen switcher + top-level state
|
||||||
src/
|
src/
|
||||||
screens/ # Home, Scan, Review, Viewer
|
screens/ # Home, Scan, Review, Viewer
|
||||||
components/ # DocumentCard, EdgeOverlay, FilterPicker
|
components/ # DocumentCard, FilterPicker
|
||||||
hooks/ # useDocumentStore
|
hooks/ # useDocumentStore
|
||||||
utils/ # generatePdf, imageProcessing
|
utils/ # generatePdf, imageProcessing
|
||||||
types/ # shared types
|
types/ # shared types
|
||||||
e2e/ # Detox specs
|
|
||||||
patches/ # patch-package patches applied postinstall
|
patches/ # patch-package patches applied postinstall
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,6 @@ react {
|
|||||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||||
// cliFile = file("../node_modules/react-native/cli.js")
|
// cliFile = file("../node_modules/react-native/cli.js")
|
||||||
|
|
||||||
/* Variants */
|
|
||||||
// Empty list means the JS bundle is embedded in ALL variants including debug.
|
|
||||||
// This is required for E2E tests in CI where no Metro server is running.
|
|
||||||
// For local dev, Metro still takes precedence if the packager is running.
|
|
||||||
debuggableVariants = []
|
|
||||||
|
|
||||||
/* Bundling */
|
/* Bundling */
|
||||||
// A list containing the node command and its flags. Default is just 'node'.
|
// A list containing the node command and its flags. Default is just 'node'.
|
||||||
// nodeExecutableAndArgs = ["node"]
|
// nodeExecutableAndArgs = ["node"]
|
||||||
@@ -106,8 +100,6 @@ android {
|
|||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
reset()
|
reset()
|
||||||
// Only split release builds — debug keeps a single universal APK
|
|
||||||
// so Detox/E2E can find the expected app-debug.apk path.
|
|
||||||
enable gradle.startParameter.taskNames.any { it.toLowerCase().contains("release") }
|
enable gradle.startParameter.taskNames.any { it.toLowerCase().contains("release") }
|
||||||
universalApk false
|
universalApk false
|
||||||
include (*project.property("reactNativeArchitectures").split(","))
|
include (*project.property("reactNativeArchitectures").split(","))
|
||||||
@@ -131,10 +123,6 @@ dependencies {
|
|||||||
// The version of react-native is set by the React Native Gradle Plugin
|
// The version of react-native is set by the React Native Gradle Plugin
|
||||||
implementation("com.facebook.react:react-android")
|
implementation("com.facebook.react:react-android")
|
||||||
implementation("com.facebook.react:flipper-integration")
|
implementation("com.facebook.react:flipper-integration")
|
||||||
androidTestImplementation("androidx.test:runner:1.6.2")
|
|
||||||
androidTestImplementation("androidx.test:rules:1.6.1")
|
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
|
||||||
androidTestImplementation("com.wix:detox:20.50.1") { transitive = true }
|
|
||||||
|
|
||||||
if (hermesEnabled.toBoolean()) {
|
if (hermesEnabled.toBoolean()) {
|
||||||
implementation("com.facebook.react:hermes-android")
|
implementation("com.facebook.react:hermes-android")
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="com.wix.detox" />
|
|
||||||
</manifest>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.lensapp;
|
|
||||||
|
|
||||||
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
|
|
||||||
public ActivityTestRule<MainActivity> 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 = 180;
|
|
||||||
|
|
||||||
Detox.runTests(mActivityRule, detoxConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,21 +19,3 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: "com.facebook.react.rootproject"
|
apply plugin: "com.facebook.react.rootproject"
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
// Route com.wix.detox lookups to Detox's bundled local Maven repo so
|
|
||||||
// they bypass JitPack (which RN's Gradle plugin adds as a fallback and
|
|
||||||
// which returns an HTML error page instead of a valid POM).
|
|
||||||
exclusiveContent {
|
|
||||||
forRepository {
|
|
||||||
maven {
|
|
||||||
url "$rootDir/../node_modules/detox/Detox-android"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filter {
|
|
||||||
includeModule "com.wix", "detox"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import {device, element, by, expect as detoxExpect} from 'detox';
|
|
||||||
|
|
||||||
describe('Home Screen', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await device.launchApp({newInstance: true});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await device.reloadReactNative();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows the app title', async () => {
|
|
||||||
await detoxExpect(element(by.text('Lens'))).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows empty state when no documents exist', async () => {
|
|
||||||
await detoxExpect(element(by.id('empty-state'))).toBeVisible();
|
|
||||||
await detoxExpect(element(by.text('No scans yet'))).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows the scan FAB', async () => {
|
|
||||||
await detoxExpect(element(by.id('scan-fab'))).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('scan FAB label reads Scan', async () => {
|
|
||||||
await detoxExpect(element(by.text('Scan'))).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
|
||||||
module.exports = {
|
|
||||||
rootDir: '..',
|
|
||||||
testMatch: ['<rootDir>/e2e/**/*.e2e.ts'],
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": ["detox", "jest"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.e2e.ts", "jest.config.js"],
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
||||||
1169
package-lock.json
generated
1169
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,6 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"start": "react-native start",
|
"start": "react-native start",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:e2e:build": "detox build --configuration android.emu.debug",
|
|
||||||
"test:e2e": "detox test --configuration android.emu.debug --headless",
|
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -18,7 +16,6 @@
|
|||||||
"react-native-document-scanner-plugin": "^2.0.4",
|
"react-native-document-scanner-plugin": "^2.0.4",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-html-to-pdf": "^1.3.0",
|
"react-native-html-to-pdf": "^1.3.0",
|
||||||
"react-native-image-crop-picker": "^0.51.1",
|
|
||||||
"react-native-share": "^12.2.6"
|
"react-native-share": "^12.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -35,7 +32,6 @@
|
|||||||
"@types/react": "^18.2.6",
|
"@types/react": "^18.2.6",
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"babel-jest": "^29.6.3",
|
"babel-jest": "^29.6.3",
|
||||||
"detox": "^20.50.1",
|
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"jest": "^29.6.3",
|
"jest": "^29.6.3",
|
||||||
"jest-circus": "^30.3.0",
|
"jest-circus": "^30.3.0",
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import React, {useRef} from 'react';
|
|
||||||
import {View, StyleSheet, PanResponder, Animated} from 'react-native';
|
|
||||||
import {Corners, Point} from '../types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
corners: Corners;
|
|
||||||
imageWidth: number;
|
|
||||||
imageHeight: number;
|
|
||||||
containerWidth: number;
|
|
||||||
containerHeight: number;
|
|
||||||
onChange: (corners: Corners) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type CornerKey = keyof Corners;
|
|
||||||
|
|
||||||
const HANDLE_SIZE = 24;
|
|
||||||
|
|
||||||
export default function EdgeOverlay({
|
|
||||||
corners,
|
|
||||||
imageWidth,
|
|
||||||
imageHeight,
|
|
||||||
containerWidth,
|
|
||||||
containerHeight,
|
|
||||||
onChange,
|
|
||||||
}: Props) {
|
|
||||||
const scaleX = containerWidth / imageWidth;
|
|
||||||
const scaleY = containerHeight / imageHeight;
|
|
||||||
|
|
||||||
const toDisplay = (p: Point) => ({x: p.x * scaleX, y: p.y * scaleY});
|
|
||||||
const toImage = (p: Point) => ({x: p.x / scaleX, y: p.y / scaleY});
|
|
||||||
|
|
||||||
const makeHandle = (key: CornerKey) => {
|
|
||||||
const pos = toDisplay(corners[key]);
|
|
||||||
const pan = useRef(new Animated.ValueXY({x: pos.x, y: pos.y})).current;
|
|
||||||
|
|
||||||
const responder = PanResponder.create({
|
|
||||||
onStartShouldSetPanResponder: () => true,
|
|
||||||
onPanResponderMove: (_, gs) => {
|
|
||||||
const nx = Math.max(0, Math.min(containerWidth, pos.x + gs.dx));
|
|
||||||
const ny = Math.max(0, Math.min(containerHeight, pos.y + gs.dy));
|
|
||||||
pan.setValue({x: nx, y: ny});
|
|
||||||
},
|
|
||||||
onPanResponderRelease: (_, gs) => {
|
|
||||||
const nx = Math.max(0, Math.min(containerWidth, pos.x + gs.dx));
|
|
||||||
const ny = Math.max(0, Math.min(containerHeight, pos.y + gs.dy));
|
|
||||||
onChange({...corners, [key]: toImage({x: nx, y: ny})});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
key={key}
|
|
||||||
style={[
|
|
||||||
styles.handle,
|
|
||||||
{transform: [{translateX: Animated.subtract(pan.x, HANDLE_SIZE / 2)}, {translateY: Animated.subtract(pan.y, HANDLE_SIZE / 2)}]},
|
|
||||||
]}
|
|
||||||
{...responder.panHandlers}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pts = (Object.keys(corners) as CornerKey[]).map(k => toDisplay(corners[k]));
|
|
||||||
const polygon = pts.map(p => `${p.x},${p.y}`).join(' ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[styles.overlay, {width: containerWidth, height: containerHeight}]} pointerEvents="box-none">
|
|
||||||
{/* SVG-style quad outline using absolute positioned thin lines via borders */}
|
|
||||||
{(Object.keys(corners) as CornerKey[]).map(k => makeHandle(k))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
overlay: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
},
|
|
||||||
handle: {
|
|
||||||
position: 'absolute',
|
|
||||||
width: HANDLE_SIZE,
|
|
||||||
height: HANDLE_SIZE,
|
|
||||||
borderRadius: HANDLE_SIZE / 2,
|
|
||||||
backgroundColor: 'rgba(0, 122, 255, 0.9)',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#fff',
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOpacity: 0.4,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 6,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -8,22 +8,8 @@ export interface ScannedDocument {
|
|||||||
export interface ScannedPage {
|
export interface ScannedPage {
|
||||||
id: string;
|
id: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
corners?: Corners;
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Corners {
|
|
||||||
topLeft: Point;
|
|
||||||
topRight: Point;
|
|
||||||
bottomLeft: Point;
|
|
||||||
bottomRight: Point;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Point {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ScanMode = 'auto' | 'manual';
|
|
||||||
export type FilterMode = 'original' | 'grayscale' | 'blackwhite' | 'enhanced';
|
export type FilterMode = 'original' | 'grayscale' | 'blackwhite' | 'enhanced';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {FilterMode, Corners} from '../types';
|
import {FilterMode} from '../types';
|
||||||
|
|
||||||
export function getFilterStyle(mode: FilterMode): object {
|
export function getFilterStyle(mode: FilterMode): object {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
@@ -13,17 +13,6 @@ export function getFilterStyle(mode: FilterMode): object {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds a CSS-style matrix string for perspective transform preview
|
|
||||||
export function cornersToTransform(corners: Corners, width: number, height: number): string {
|
|
||||||
const {topLeft, topRight, bottomLeft, bottomRight} = corners;
|
|
||||||
// Normalized 0-1 coordinates for display
|
|
||||||
const tl = `${(topLeft.x / width) * 100}% ${(topLeft.y / height) * 100}%`;
|
|
||||||
const tr = `${(topRight.x / width) * 100}% ${(topRight.y / height) * 100}%`;
|
|
||||||
const br = `${(bottomRight.x / width) * 100}% ${(bottomRight.y / height) * 100}%`;
|
|
||||||
const bl = `${(bottomLeft.x / width) * 100}% ${(bottomLeft.y / height) * 100}%`;
|
|
||||||
return `polygon(${tl}, ${tr}, ${br}, ${bl})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDate(date: Date): string {
|
export function formatDate(date: Date): string {
|
||||||
return date.toLocaleDateString(undefined, {
|
return date.toLocaleDateString(undefined, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|||||||
Reference in New Issue
Block a user