Revamp image editor (#5462)
* new image editor * Rm react-avatar-editor --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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, {PercentCrop} from 'react-image-crop'
|
||||
|
||||
import {
|
||||
ImageSource,
|
||||
ImageTransformation,
|
||||
manipulateImage,
|
||||
} from '#/state/gallery'
|
||||
import {atoms as a} 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 (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<EditImageInner key={props.image.source.id} {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => {
|
||||
const {_} = useLingui()
|
||||
|
||||
const source = image.source
|
||||
|
||||
const initialCrop = getInitialCrop(source, image.manips)
|
||||
const [crop, setCrop] = React.useState(initialCrop)
|
||||
|
||||
const isEmpty = !crop || (crop.width || crop.height) === 0
|
||||
const isNew = initialCrop ? true : !isEmpty
|
||||
|
||||
const onPressSubmit = React.useCallback(async () => {
|
||||
const result = await manipulateImage(image, {
|
||||
crop:
|
||||
crop && (crop.width || crop.height) !== 0
|
||||
? {
|
||||
originX: (crop.x * source.width) / 100,
|
||||
originY: (crop.y * source.height) / 100,
|
||||
width: (crop.width * source.width) / 100,
|
||||
height: (crop.height * source.height) / 100,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
onChange(result)
|
||||
control.close()
|
||||
}, [crop, image, source, control, onChange])
|
||||
|
||||
return (
|
||||
<Dialog.Inner label={_(msg`Edit image`)}>
|
||||
<Dialog.Close />
|
||||
|
||||
<Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_sm]}>
|
||||
<Trans>Edit image</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.align_center]}>
|
||||
<ReactCrop
|
||||
crop={crop}
|
||||
onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)}
|
||||
className="ReactCrop--no-animate">
|
||||
<img src={source.path} style={{maxHeight: `50vh`}} />
|
||||
</ReactCrop>
|
||||
</View>
|
||||
|
||||
<View style={[a.mt_md, a.gap_md]}>
|
||||
<Button
|
||||
disabled={!isNew}
|
||||
label={_(msg`Save`)}
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={onPressSubmit}>
|
||||
<ButtonText>
|
||||
<Trans>Save</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</Dialog.Inner>
|
||||
)
|
||||
}
|
||||
|
||||
const getInitialCrop = (
|
||||
source: ImageSource,
|
||||
manips: ImageTransformation | undefined,
|
||||
): PercentCrop | undefined => {
|
||||
const initialArea = manips?.crop
|
||||
|
||||
if (initialArea) {
|
||||
return {
|
||||
unit: '%',
|
||||
x: (initialArea.originX / source.width) * 100,
|
||||
y: (initialArea.originY / source.height) * 100,
|
||||
width: (initialArea.width / source.width) * 100,
|
||||
height: (initialArea.height / source.height) * 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
|
||||
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
|
||||
@@ -144,12 +145,15 @@ const GalleryItem = ({
|
||||
const t = useTheme()
|
||||
|
||||
const altTextControl = Dialog.useDialogControl()
|
||||
const editControl = Dialog.useDialogControl()
|
||||
|
||||
const onImageEdit = () => {
|
||||
if (isNative) {
|
||||
cropImage(image).then(next => {
|
||||
onChange(next)
|
||||
})
|
||||
} else {
|
||||
editControl.open()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,21 +189,15 @@ const GalleryItem = ({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={imageControlsStyle}>
|
||||
{isNative && (
|
||||
<TouchableOpacity
|
||||
testID="editPhotoButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Edit image`)}
|
||||
accessibilityHint=""
|
||||
onPress={onImageEdit}
|
||||
style={styles.imageControl}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={{color: colors.white}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
testID="editPhotoButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Edit image`)}
|
||||
accessibilityHint=""
|
||||
onPress={onImageEdit}
|
||||
style={styles.imageControl}>
|
||||
<FontAwesomeIcon icon="pen" size={12} style={{color: colors.white}} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="removePhotoButton"
|
||||
accessibilityRole="button"
|
||||
@@ -237,6 +235,12 @@ const GalleryItem = ({
|
||||
image={image}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<EditImageDialog
|
||||
control={editControl}
|
||||
image={image}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user