diff options
Diffstat (limited to 'src/view/com/composer/photos/EditImageDialog.web.tsx')
-rw-r--r-- | src/view/com/composer/photos/EditImageDialog.web.tsx | 185 |
1 files changed, 137 insertions, 48 deletions
diff --git a/src/view/com/composer/photos/EditImageDialog.web.tsx b/src/view/com/composer/photos/EditImageDialog.web.tsx index ebe528abc..9a170736a 100644 --- a/src/view/com/composer/photos/EditImageDialog.web.tsx +++ b/src/view/com/composer/photos/EditImageDialog.web.tsx @@ -1,43 +1,130 @@ import 'react-image-crop/dist/ReactCrop.css' -import React from 'react' +import {useCallback, useImperativeHandle, useRef, useState} 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 ReactCrop, {type PercentCrop} from 'react-image-crop' import { - ImageSource, - ImageTransformation, + type ImageSource, + type ImageTransformation, manipulateImage, } from '#/state/gallery' -import {atoms as a} from '#/alf' -import {Button, ButtonText} from '#/components/Button' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {Text} from '#/components/Typography' -import {EditImageDialogProps} from './EditImageDialog' +import {Loader} from '#/components/Loader' +import {type EditImageDialogProps} from './EditImageDialog' -export const EditImageDialog = (props: EditImageDialogProps) => { +export function EditImageDialog(props: EditImageDialogProps) { return ( <Dialog.Outer control={props.control}> <Dialog.Handle /> - <EditImageInner key={props.image.source.id} {...props} /> + <DialogInner {...props} /> </Dialog.Outer> ) } -const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => { +function DialogInner({ + control, + image, + onChange, + circularCrop, + aspectRatio, +}: EditImageDialogProps) { const {_} = useLingui() + const [pending, setPending] = useState(false) + const ref = useRef<{save: () => Promise<void>}>(null) + + const cancelButton = useCallback( + () => ( + <Button + label={_(msg`Cancel`)} + disabled={pending} + onPress={() => control.close()} + size="small" + color="primary" + variant="ghost" + style={[a.rounded_full]} + testID="cropImageCancelBtn"> + <ButtonText style={[a.text_md]}> + <Trans>Cancel</Trans> + </ButtonText> + </Button> + ), + [control, _, pending], + ) + + const saveButton = useCallback( + () => ( + <Button + label={_(msg`Save`)} + onPress={async () => { + setPending(true) + await ref.current?.save() + setPending(false) + }} + disabled={pending} + size="small" + color="primary" + variant="ghost" + style={[a.rounded_full]} + testID="cropImageSaveBtn"> + <ButtonText style={[a.text_md]}> + <Trans>Save</Trans> + </ButtonText> + {pending && <ButtonIcon icon={Loader} />} + </Button> + ), + [_, pending], + ) + + return ( + <Dialog.Inner + label={_(msg`Edit image`)} + header={ + <Dialog.Header renderLeft={cancelButton} renderRight={saveButton}> + <Dialog.HeaderText> + <Trans>Edit image</Trans> + </Dialog.HeaderText> + </Dialog.Header> + }> + {image && ( + <EditImageInner + saveRef={ref} + key={image.source.id} + image={image} + onChange={onChange} + circularCrop={circularCrop} + aspectRatio={aspectRatio} + /> + )} + </Dialog.Inner> + ) +} + +function EditImageInner({ + image, + onChange, + saveRef, + circularCrop = false, + aspectRatio, +}: Required<Pick<EditImageDialogProps, 'image'>> & + Omit<EditImageDialogProps, 'control' | 'image'> & { + saveRef: React.RefObject<{save: () => Promise<void>}> + }) { + const t = useTheme() + const [isDragging, setIsDragging] = useState(false) + const {_} = useLingui() + const control = Dialog.useDialogContext() 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 [crop, setCrop] = useState(initialCrop) - const onPressSubmit = React.useCallback(async () => { + const onPressSubmit = useCallback(async () => { const result = await manipulateImage(image, { crop: crop && (crop.width || crop.height) !== 0 @@ -50,41 +137,43 @@ const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => { : undefined, }) - onChange(result) - control.close() + control.close(() => { + onChange(result) + }) }, [crop, image, source, control, onChange]) + useImperativeHandle( + saveRef, + () => ({ + save: onPressSubmit, + }), + [onPressSubmit], + ) + 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> + <View + style={[ + a.mx_auto, + a.border, + t.atoms.border_contrast_low, + a.rounded_xs, + a.overflow_hidden, + a.align_center, + ]}> + <ReactCrop + crop={crop} + aspect={aspectRatio} + circularCrop={circularCrop} + onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)} + className="ReactCrop--no-animate" + onDragStart={() => setIsDragging(true)} + onDragEnd={() => setIsDragging(false)}> + <img src={source.path} style={{maxHeight: `50vh`}} /> + </ReactCrop> + {/* Eat clicks when dragging, otherwise mousing up over the backdrop + causes the dialog to close */} + {isDragging && <View style={[a.fixed, a.inset_0]} />} + </View> ) } |