diff options
Diffstat (limited to 'src/view/com/modals')
-rw-r--r-- | src/view/com/modals/CropImage.web.tsx | 145 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 2 | ||||
-rw-r--r-- | src/view/com/modals/crop-image/CropImage.web.tsx | 228 | ||||
-rw-r--r-- | src/view/com/modals/crop-image/cropImageUtil.ts | 13 |
4 files changed, 146 insertions, 242 deletions
diff --git a/src/view/com/modals/CropImage.web.tsx b/src/view/com/modals/CropImage.web.tsx new file mode 100644 index 000000000..41ca30657 --- /dev/null +++ b/src/view/com/modals/CropImage.web.tsx @@ -0,0 +1,145 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {Image as RNImage} from 'react-native-image-crop-picker' +import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' +import {LinearGradient} from 'expo-linear-gradient' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import ReactCrop, {PercentCrop} from 'react-image-crop' + +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {getDataUriSize} from '#/lib/media/util' +import {gradients, s} from '#/lib/styles' +import {useModalControls} from '#/state/modals' +import {Text} from '#/view/com/util/text/Text' + +export const snapPoints = ['0%'] + +export function Component({ + uri, + aspect, + circular, + onSelect, +}: { + uri: string + aspect?: number + circular?: boolean + onSelect: (img?: RNImage) => void +}) { + const pal = usePalette('default') + const {_} = useLingui() + + const {closeModal} = useModalControls() + const {isMobile} = useWebMediaQueries() + + const imageRef = React.useRef<HTMLImageElement>(null) + const [crop, setCrop] = React.useState<PercentCrop>() + + const isEmpty = !crop || (crop.width || crop.height) === 0 + + const onPressCancel = () => { + onSelect(undefined) + closeModal() + } + const onPressDone = async () => { + const img = imageRef.current! + + const result = await manipulateAsync( + uri, + isEmpty + ? [] + : [ + { + crop: { + originX: (crop.x * img.naturalWidth) / 100, + originY: (crop.y * img.naturalHeight) / 100, + width: (crop.width * img.naturalWidth) / 100, + height: (crop.height * img.naturalHeight) / 100, + }, + }, + ], + { + base64: true, + format: SaveFormat.JPEG, + }, + ) + + onSelect({ + path: result.uri, + mime: 'image/jpeg', + size: result.base64 !== undefined ? getDataUriSize(result.base64) : 0, + width: result.width, + height: result.height, + }) + + closeModal() + } + + return ( + <View> + <View style={[styles.cropper, pal.borderDark]}> + <ReactCrop + aspect={aspect} + crop={crop} + onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)} + circularCrop={circular}> + <img ref={imageRef} src={uri} style={{maxHeight: '75vh'}} /> + </ReactCrop> + </View> + <View style={[styles.btns, isMobile && {paddingHorizontal: 16}]}> + <TouchableOpacity + onPress={onPressCancel} + accessibilityRole="button" + accessibilityLabel={_(msg`Cancel image crop`)} + accessibilityHint={_(msg`Exits image cropping process`)}> + <Text type="xl" style={pal.link}> + <Trans>Cancel</Trans> + </Text> + </TouchableOpacity> + <View style={s.flex1} /> + <TouchableOpacity + onPress={onPressDone} + accessibilityRole="button" + accessibilityLabel={_(msg`Save image crop`)} + accessibilityHint={_(msg`Saves image crop settings`)}> + <LinearGradient + colors={[gradients.blueLight.start, gradients.blueLight.end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[styles.btn]}> + <Text type="xl-medium" style={s.white}> + <Trans>Done</Trans> + </Text> + </LinearGradient> + </TouchableOpacity> + </View> + </View> + ) +} + +const styles = StyleSheet.create({ + cropper: { + marginLeft: 'auto', + marginRight: 'auto', + borderWidth: 1, + borderRadius: 4, + overflow: 'hidden', + alignItems: 'center', + }, + ctrls: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 10, + }, + btns: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 10, + }, + btn: { + borderRadius: 4, + paddingVertical: 8, + paddingHorizontal: 24, + }, +}) diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index c1024751f..a2acc23bb 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -12,7 +12,7 @@ import * as ChangeEmailModal from './ChangeEmail' import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' import * as CreateOrEditListModal from './CreateOrEditList' -import * as CropImageModal from './crop-image/CropImage.web' +import * as CropImageModal from './CropImage.web' import * as DeleteAccountModal from './DeleteAccount' import * as EditProfileModal from './EditProfile' import * as InviteCodesModal from './InviteCodes' diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx deleted file mode 100644 index 10cae2f17..000000000 --- a/src/view/com/modals/crop-image/CropImage.web.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {Image as RNImage} from 'react-native-image-crop-picker' -import {LinearGradient} from 'expo-linear-gradient' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {Slider} from '@miblanchard/react-native-slider' -import ImageEditor from 'react-avatar-editor' - -import {useModalControls} from '#/state/modals' -import {usePalette} from 'lib/hooks/usePalette' -import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons' -import {Dimensions} from 'lib/media/types' -import {getDataUriSize} from 'lib/media/util' -import {gradients, s} from 'lib/styles' -import {Text} from 'view/com/util/text/Text' -import {calculateDimensions} from './cropImageUtil' - -enum AspectRatio { - Square = 'square', - Wide = 'wide', - Tall = 'tall', - Custom = 'custom', -} - -const DIMS: Record<string, Dimensions> = { - [AspectRatio.Square]: {width: 1000, height: 1000}, - [AspectRatio.Wide]: {width: 1000, height: 750}, - [AspectRatio.Tall]: {width: 750, height: 1000}, -} - -export const snapPoints = ['0%'] - -export function Component({ - uri, - dimensions, - onSelect, -}: { - uri: string - dimensions?: Dimensions - onSelect: (img?: RNImage) => void -}) { - const {closeModal} = useModalControls() - const pal = usePalette('default') - const {_} = useLingui() - const defaultAspectStyle = dimensions - ? AspectRatio.Custom - : AspectRatio.Square - const [as, setAs] = React.useState<AspectRatio>(defaultAspectStyle) - const [scale, setScale] = React.useState<number>(1) - const editorRef = React.useRef<ImageEditor>(null) - const imageEditorWidth = dimensions ? dimensions.width : DIMS[as].width - const imageEditorHeight = dimensions ? dimensions.height : DIMS[as].height - - const doSetAs = (v: AspectRatio) => () => setAs(v) - - const onPressCancel = () => { - onSelect(undefined) - closeModal() - } - const onPressDone = () => { - const canvas = editorRef.current?.getImageScaledToCanvas() - if (canvas) { - const dataUri = canvas.toDataURL('image/jpeg') - onSelect({ - path: dataUri, - mime: 'image/jpeg', - size: getDataUriSize(dataUri), - width: imageEditorWidth, - height: imageEditorHeight, - }) - } else { - onSelect(undefined) - } - closeModal() - } - - let cropperStyle - if (as === AspectRatio.Square) { - cropperStyle = styles.cropperSquare - } else if (as === AspectRatio.Wide) { - cropperStyle = styles.cropperWide - } else if (as === AspectRatio.Tall) { - cropperStyle = styles.cropperTall - } else if (as === AspectRatio.Custom) { - const cropperDimensions = calculateDimensions( - 550, - imageEditorHeight, - imageEditorWidth, - ) - cropperStyle = { - width: cropperDimensions.width, - height: cropperDimensions.height, - } - } - - return ( - <View> - <View style={[styles.cropper, pal.borderDark, cropperStyle]}> - <ImageEditor - ref={editorRef} - style={styles.imageEditor} - image={uri} - width={imageEditorWidth} - height={imageEditorHeight} - scale={scale} - border={0} - /> - </View> - <View style={styles.ctrls}> - <Slider - value={scale} - onValueChange={(v: number | number[]) => - setScale(Array.isArray(v) ? v[0] : v) - } - minimumValue={1} - maximumValue={3} - containerStyle={styles.slider} - /> - {as === AspectRatio.Custom ? null : ( - <> - <TouchableOpacity - onPress={doSetAs(AspectRatio.Wide)} - accessibilityRole="button" - accessibilityLabel={_(msg`Wide`)} - accessibilityHint={_(msg`Sets image aspect ratio to wide`)}> - <RectWideIcon - size={24} - style={as === AspectRatio.Wide ? s.blue3 : pal.text} - /> - </TouchableOpacity> - <TouchableOpacity - onPress={doSetAs(AspectRatio.Tall)} - accessibilityRole="button" - accessibilityLabel={_(msg`Tall`)} - accessibilityHint={_(msg`Sets image aspect ratio to tall`)}> - <RectTallIcon - size={24} - style={as === AspectRatio.Tall ? s.blue3 : pal.text} - /> - </TouchableOpacity> - <TouchableOpacity - onPress={doSetAs(AspectRatio.Square)} - accessibilityRole="button" - accessibilityLabel={_(msg`Square`)} - accessibilityHint={_(msg`Sets image aspect ratio to square`)}> - <SquareIcon - size={24} - style={as === AspectRatio.Square ? s.blue3 : pal.text} - /> - </TouchableOpacity> - </> - )} - </View> - <View style={styles.btns}> - <TouchableOpacity - onPress={onPressCancel} - accessibilityRole="button" - accessibilityLabel={_(msg`Cancel image crop`)} - accessibilityHint={_(msg`Exits image cropping process`)}> - <Text type="xl" style={pal.link}> - <Trans>Cancel</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - <TouchableOpacity - onPress={onPressDone} - accessibilityRole="button" - accessibilityLabel={_(msg`Save image crop`)} - accessibilityHint={_(msg`Saves image crop settings`)}> - <LinearGradient - colors={[gradients.blueLight.start, gradients.blueLight.end]} - start={{x: 0, y: 0}} - end={{x: 1, y: 1}} - style={[styles.btn]}> - <Text type="xl-medium" style={s.white}> - <Trans>Done</Trans> - </Text> - </LinearGradient> - </TouchableOpacity> - </View> - </View> - ) -} - -const styles = StyleSheet.create({ - cropper: { - marginLeft: 'auto', - marginRight: 'auto', - borderWidth: 1, - borderRadius: 4, - overflow: 'hidden', - }, - cropperSquare: { - width: 400, - height: 400, - }, - cropperWide: { - width: 400, - height: 300, - }, - cropperTall: { - width: 300, - height: 400, - }, - imageEditor: { - maxWidth: '100%', - }, - ctrls: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 10, - }, - slider: { - flex: 1, - marginRight: 10, - }, - btns: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 10, - }, - btn: { - borderRadius: 4, - paddingVertical: 8, - paddingHorizontal: 24, - }, -}) diff --git a/src/view/com/modals/crop-image/cropImageUtil.ts b/src/view/com/modals/crop-image/cropImageUtil.ts deleted file mode 100644 index 303d15ba5..000000000 --- a/src/view/com/modals/crop-image/cropImageUtil.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const calculateDimensions = ( - maxWidth: number, - originalHeight: number, - originalWidth: number, -) => { - const aspectRatio = originalWidth / originalHeight - const newHeight = maxWidth / aspectRatio - const newWidth = maxWidth - return { - width: newWidth, - height: newHeight, - } -} |