From 59f23546fd6b8f70486257487e30b0820194d226 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 8 May 2024 10:43:09 +0700 Subject: [PATCH] feat: new image editor --- package.json | 1 + src/state/gallery.ts | 21 +- .../com/composer/photos/EditImageDialog.tsx | 14 ++ .../composer/photos/EditImageDialog.web.tsx | 188 ++++++++++++++++++ src/view/com/composer/photos/Gallery.tsx | 34 ++-- yarn.lock | 5 + 6 files changed, 232 insertions(+), 31 deletions(-) create mode 100644 src/view/com/composer/photos/EditImageDialog.tsx create mode 100644 src/view/com/composer/photos/EditImageDialog.web.tsx diff --git a/package.json b/package.json index 26d6b061b1..8284e3b4d6 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "react": "18.2.0", "react-avatar-editor": "^13.0.0", "react-dom": "^18.2.0", + "react-image-crop": "^11.0.5", "react-keyed-flatten-children": "^3.0.0", "react-native": "0.73.2", "react-native-date-picker": "^4.4.0", diff --git a/src/state/gallery.ts b/src/state/gallery.ts index ed0c5d8229..dcd7cdf5af 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -8,10 +8,6 @@ import { import { Action, ActionCrop, - ActionExtent, - ActionFlip, - ActionResize, - ActionRotate, manipulateAsync, SaveFormat, } from 'expo-image-manipulator' @@ -23,9 +19,7 @@ import {openCropper} from '#/lib/media/picker' import {getDataUriSize} from '#/lib/media/util' import {isIOS, isNative} from '#/platform/detection' -export type ImageTransformation = Partial< - ActionCrop & ActionExtent & ActionFlip & ActionResize & ActionRotate -> +export type ImageTransformation = Partial export type ImageMeta = { path: string @@ -44,10 +38,11 @@ type ComposerImageBase = { } type ComposerImageWithoutTransformation = ComposerImageBase & { transformed?: undefined + manips?: undefined } type ComposerImageWithTransformation = ComposerImageBase & { transformed: ImageMeta - transformations?: ImageTransformation + manips?: ImageTransformation } export type ComposerImage = @@ -161,13 +156,7 @@ export async function manipulateImage( img: ComposerImage, trans: ImageTransformation, ): Promise { - const rawActions: (Action | undefined)[] = [ - trans.extent && {extent: trans.extent}, - trans.flip && {flip: trans.flip}, - trans.rotate !== undefined ? {rotate: trans.rotate} : undefined, - trans.resize && {resize: trans.resize}, - trans.crop && {crop: trans.crop}, - ] + const rawActions: (Action | undefined)[] = [trans.crop && {crop: trans.crop}] const actions = rawActions.filter((a): a is Action => a !== undefined) @@ -193,7 +182,7 @@ export async function manipulateImage( height: result.height, mime: 'image/png', }, - transformations: trans, + manips: trans, } } diff --git a/src/view/com/composer/photos/EditImageDialog.tsx b/src/view/com/composer/photos/EditImageDialog.tsx new file mode 100644 index 0000000000..4263587fd4 --- /dev/null +++ b/src/view/com/composer/photos/EditImageDialog.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +import {ComposerImage} from '#/state/gallery' +import * as Dialog from '#/components/Dialog' + +export type EditImageDialogProps = { + control: Dialog.DialogOuterProps['control'] + image: ComposerImage + onChange: (next: ComposerImage) => void +} + +export const EditImageDialog = ({}: EditImageDialogProps): React.ReactNode => { + return null +} diff --git a/src/view/com/composer/photos/EditImageDialog.web.tsx b/src/view/com/composer/photos/EditImageDialog.web.tsx new file mode 100644 index 0000000000..ff1542f8f0 --- /dev/null +++ b/src/view/com/composer/photos/EditImageDialog.web.tsx @@ -0,0 +1,188 @@ +import 'react-image-crop/dist/ReactCrop.css' + +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import ReactCrop, {PixelCrop} from 'react-image-crop' + +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Text} from '#/components/Typography' +import {EditImageDialogProps} from './EditImageDialog' + +export const EditImageDialog = (props: EditImageDialogProps) => { + return ( + + + + ) +} + +const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => { + const {_} = useLingui() + const t = useTheme() + + const source = image.source + const manips = image.manips + + const {initialCrop, initialAspect, sourceAspect} = React.useMemo(() => { + const initialArea = manips?.crop + + let crop: PixelCrop | undefined + + if (initialArea) { + crop = { + unit: 'px', + x: initialArea.originX, + y: initialArea.originY, + width: initialArea.width, + height: initialArea.height, + } + } + + const originalAspect = source.width / source.height + + return { + initialCrop: crop, + initialAspect: crop ? crop.width / crop.height : originalAspect, + sourceAspect: originalAspect, + } + }, [source, manips]) + + const [crop, setCrop] = React.useState(initialCrop) + const [aspect, setAspect] = React.useState(initialAspect) + + const isEmpty = !crop || (crop.width || crop.height) === 0 + const isNew = initialCrop ? true : !isEmpty + + const onPressSubmit = React.useCallback(async () => { + const result = await manipulateImage(image, { + crop: !isEmpty + ? { + originX: crop.x, + originY: crop.y, + width: crop.width, + height: crop.height, + } + : undefined, + }) + + onChange(result) + control.close() + }, [crop, isEmpty, image, control, onChange]) + + const changeAspect = (next: number) => { + if (next !== aspect) { + setAspect(next) + setCrop(undefined) + } + } + + return ( + + + Edit image + + + + + + + + + + + changeAspect(sourceAspect)}> + + + + + + changeAspect(16 / 9)}> + + + + + + changeAspect(1 / 1)}> + + + + + + + + + + ) +} + +const ToolbarButton = ({ + children, + label, + onPress, +}: { + children: React.ReactElement + label: string + onPress?: () => void +}) => { + return ( + + ) +} +import {Path, Svg} from 'react-native-svg' + +import {manipulateImage} from '#/state/gallery' diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index a2606ae602..31f2af06ba 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -14,6 +14,7 @@ import {isNative} from 'platform/detection' import {Text} from 'view/com/util/text/Text' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' +import {EditImageDialog} from './EditImageDialog' import {ImageAltTextDialog} from './ImageAltTextDialog' const IMAGE_GAP = 8 @@ -137,12 +138,15 @@ const GalleryItem = ({ const t = useTheme() const altTextControl = Dialog.useDialogControl() + const editControl = Dialog.useDialogControl() const onEdit = () => { if (isNative) { cropImage(image).then(next => { onChange(next) }) + } else { + editControl.open() } } @@ -173,21 +177,15 @@ const GalleryItem = ({ - {isNative && ( - - - - )} + + + + + ) } diff --git a/yarn.lock b/yarn.lock index 4edc4ce430..96e183a9af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18664,6 +18664,11 @@ react-freeze@^1.0.0: resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d" integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g== +react-image-crop@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.5.tgz#c7abcf9cae28305d253d55d481158a594a937867" + integrity sha512-A/Y/kspOzki1zDL/bSgwWIY1X3CQ9F1QwpdnncWLBVAktnKfAZDIQnWmjXzuzEjZHDMsBlArytIcPBVi6DNklg== + "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"