From e2055dfb7842c15eb1cda847c74b24bf1f88d3b9 Mon Sep 17 00:00:00 2001 From: Ollie H Date: Mon, 15 May 2023 14:54:14 -0700 Subject: [PATCH] Image editor mobile layout update (#613) * Image editor mobile layout update * Minor viewport fix --- src/state/models/media/gallery.ts | 8 +- src/state/models/media/image.ts | 92 ++++---- src/view/com/modals/EditImage.tsx | 375 +++++++++++++----------------- 3 files changed, 219 insertions(+), 256 deletions(-) diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts index 86bf8a31..89f0d012 100644 --- a/src/state/models/media/gallery.ts +++ b/src/state/models/media/gallery.ts @@ -52,16 +52,14 @@ export class GalleryModel { } async edit(image: ImageModel) { - if (!isNative) { + if (isNative) { + this.crop(image) + } else { this.rootStore.shell.openModal({ name: 'edit-image', image, gallery: this, }) - - return - } else { - this.crop(image) } } diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index ff464a5a..6edf88d9 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -13,12 +13,12 @@ import {compressAndResizeImageForPost} from 'lib/media/manip' // Cases to consider: ExternalEmbed export interface ImageManipulationAttributes { + aspectRatio?: '4:3' | '1:1' | '3:4' | 'None' rotate?: number scale?: number position?: Position flipHorizontal?: boolean flipVertical?: boolean - aspectRatio?: '4:3' | '1:1' | '3:4' | 'None' } export class ImageModel implements RNImage { @@ -34,14 +34,14 @@ export class ImageModel implements RNImage { scaledHeight: number = POST_IMG_MAX.height // Web manipulation - aspectRatio?: ImageManipulationAttributes['aspectRatio'] - position?: Position = undefined - prev?: RNImage = undefined - rotation?: number = 0 - scale?: number = 1 - flipHorizontal?: boolean = false - flipVertical?: boolean = false - + prev?: RNImage + attributes: ImageManipulationAttributes = { + aspectRatio: '1:1', + scale: 1, + flipHorizontal: false, + flipVertical: false, + rotate: 0, + } prevAttributes: ImageManipulationAttributes = {} constructor(public rootStore: RootStoreModel, image: RNImage) { @@ -65,6 +65,25 @@ export class ImageModel implements RNImage { // : MAX_IMAGE_SIZE_IN_BYTES / this.size // } + setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) { + this.attributes.aspectRatio = aspectRatio + } + + setRotate(degrees: number) { + this.attributes.rotate = degrees + this.manipulate({}) + } + + flipVertical() { + this.attributes.flipVertical = !this.attributes.flipVertical + this.manipulate({}) + } + + flipHorizontal() { + this.attributes.flipHorizontal = !this.attributes.flipHorizontal + this.manipulate({}) + } + get ratioMultipliers() { return { '4:3': 4 / 3, @@ -162,33 +181,19 @@ export class ImageModel implements RNImage { crop?: ActionCrop['crop'] } & ImageManipulationAttributes, ) { - const {aspectRatio, crop, flipHorizontal, flipVertical, rotate, scale} = - attributes + const {aspectRatio, crop, position, scale} = attributes const modifiers = [] - if (flipHorizontal !== undefined) { - this.flipHorizontal = flipHorizontal - } - - if (flipVertical !== undefined) { - this.flipVertical = flipVertical - } - - if (this.flipHorizontal) { + if (this.attributes.flipHorizontal) { modifiers.push({flip: FlipType.Horizontal}) } - if (this.flipVertical) { + if (this.attributes.flipVertical) { modifiers.push({flip: FlipType.Vertical}) } - // TODO: Fix rotation -- currently not functional - if (rotate !== undefined) { - this.rotation = rotate - } - - if (this.rotation !== undefined) { - modifiers.push({rotate: this.rotation}) + if (this.attributes.rotate !== undefined) { + modifiers.push({rotate: this.attributes.rotate}) } if (crop !== undefined) { @@ -203,18 +208,21 @@ export class ImageModel implements RNImage { } if (scale !== undefined) { - this.scale = scale + this.attributes.scale = scale + } + + if (position !== undefined) { + this.attributes.position = position } if (aspectRatio !== undefined) { - this.aspectRatio = aspectRatio + this.attributes.aspectRatio = aspectRatio } - const ratioMultiplier = this.ratioMultipliers[this.aspectRatio ?? '1:1'] + const ratioMultiplier = + this.ratioMultipliers[this.attributes.aspectRatio ?? '1:1'] - // TODO: Ollie - should support up to 2000 but smaller images that scale - // up need an updated compression factor calculation. Use 1000 for now. - const MAX_SIDE = 1000 + const MAX_SIDE = 2000 const result = await ImageManipulator.manipulateAsync( this.path, @@ -223,7 +231,7 @@ export class ImageModel implements RNImage { {resize: ratioMultiplier > 1 ? {width: MAX_SIDE} : {height: MAX_SIDE}}, ], { - compress: 0.7, // TODO: revisit compression calculation + compress: 0.9, format: SaveFormat.JPEG, }, ) @@ -238,16 +246,12 @@ export class ImageModel implements RNImage { }) } + resetCompressed() { + this.manipulate({}) + } + previous() { this.compressed = this.prev - - const {flipHorizontal, flipVertical, rotate, position, scale} = - this.prevAttributes - - this.scale = scale - this.rotation = rotate - this.flipHorizontal = flipHorizontal - this.flipVertical = flipVertical - this.position = position + this.attributes = this.prevAttributes } } diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx index 4a5d9bfd..eab472a7 100644 --- a/src/view/com/modals/EditImage.tsx +++ b/src/view/com/modals/EditImage.tsx @@ -18,148 +18,114 @@ import {Slider} from '@miblanchard/react-native-slider' import {MaterialIcons} from '@expo/vector-icons' import {observer} from 'mobx-react-lite' import {getKeys} from 'lib/type-assertions' +import {isDesktopWeb} from 'platform/detection' export const snapPoints = ['80%'] +const RATIOS = { + '4:3': { + Icon: RectWideIcon, + }, + '1:1': { + Icon: SquareIcon, + }, + '3:4': { + Icon: RectTallIcon, + }, + None: { + label: 'None', + Icon: MaterialIcons, + name: 'do-not-disturb-alt', + }, +} as const + +type AspectRatio = keyof typeof RATIOS + interface Props { image: ImageModel gallery: GalleryModel } -// This is only used for desktop web export const Component = observer(function ({image, gallery}: Props) { const pal = usePalette('default') - const store = useStores() - const {shell} = store const theme = useTheme() - const winDim = useWindowDimensions() + const store = useStores() + const windowDimensions = useWindowDimensions() - const [altText, setAltText] = useState(image.altText) - const [aspectRatio, setAspectRatio] = useState( - image.aspectRatio ?? 'None', - ) - const [flipHorizontal, setFlipHorizontal] = useState( - image.flipHorizontal ?? false, - ) - const [flipVertical, setFlipVertical] = useState( - image.flipVertical ?? false, - ) + const { + aspectRatio, + // rotate = 0 + } = image.attributes - // TODO: doesn't seem to be working correctly with crop - // const [rotation, setRotation] = useState(image.rotation ?? 0) - const [scale, setScale] = useState(image.scale ?? 1) - const [position, setPosition] = useState() - const [isEditing, setIsEditing] = useState(false) const editorRef = useRef(null) - - const imgEditorStyles = useMemo(() => { - const dim = Math.min(425, winDim.width - 24) - return {width: dim, height: dim} - }, [winDim.width]) - - const manipulationAttributes = useMemo( - () => ({ - // TODO: doesn't seem to be working correctly with crop - // ...(rotation !== undefined ? {rotate: rotation} : {}), - ...(flipHorizontal !== undefined ? {flipHorizontal} : {}), - ...(flipVertical !== undefined ? {flipVertical} : {}), - }), - [flipHorizontal, flipVertical], + const [scale, setScale] = useState(image.attributes.scale ?? 1) + const [position, setPosition] = useState( + image.attributes.position, ) - - useEffect(() => { - const manipulateImage = async () => { - await image.manipulate(manipulationAttributes) - } - - manipulateImage() - }, [image, manipulationAttributes]) - - const ratios = useMemo( - () => - ({ - '4:3': { - hint: 'Sets image aspect ratio to wide', - Icon: RectWideIcon, - }, - '1:1': { - hint: 'Sets image aspect ratio to square', - Icon: SquareIcon, - }, - '3:4': { - hint: 'Sets image aspect ratio to tall', - Icon: RectTallIcon, - }, - None: { - label: 'None', - hint: 'Sets image aspect ratio to tall', - Icon: MaterialIcons, - name: 'do-not-disturb-alt', - }, - } as const), - [], - ) - - type AspectRatio = keyof typeof ratios + const [altText, setAltText] = useState('') const onFlipHorizontal = useCallback(() => { - setFlipHorizontal(!flipHorizontal) - image.manipulate({flipHorizontal}) - }, [flipHorizontal, image]) + image.flipHorizontal() + }, [image]) const onFlipVertical = useCallback(() => { - setFlipVertical(!flipVertical) - image.manipulate({flipVertical}) - }, [flipVertical, image]) + image.flipVertical() + }, [image]) + + // const onSetRotate = useCallback( + // (direction: 'left' | 'right') => { + // const rotation = (rotate + 90 * (direction === 'left' ? -1 : 1)) % 360 + // image.setRotate(rotation) + // }, + // [rotate, image], + // ) + + const onSetRatio = useCallback( + (ratio: AspectRatio) => { + image.setRatio(ratio) + }, + [image], + ) const adjustments = useMemo( - () => - [ - // { - // name: 'rotate-left', - // label: 'Rotate left', - // hint: 'Rotate image left', - // onPress: () => { - // const rotate = (rotation - 90) % 360 - // setRotation(rotate) - // image.manipulate({rotate}) - // }, - // }, - // { - // name: 'rotate-right', - // label: 'Rotate right', - // hint: 'Rotate image right', - // onPress: () => { - // const rotate = (rotation + 90) % 360 - // setRotation(rotate) - // image.manipulate({rotate}) - // }, - // }, - { - name: 'flip', - label: 'Flip horizontal', - hint: 'Flip image horizontally', - onPress: onFlipHorizontal, - }, - { - name: 'flip', - label: 'Flip vertically', - hint: 'Flip image vertically', - onPress: onFlipVertical, - }, - ] as const, + () => [ + // { + // name: 'rotate-left' as const, + // label: 'Rotate left', + // onPress: () => { + // onSetRotate('left') + // }, + // }, + // { + // name: 'rotate-right' as const, + // label: 'Rotate right', + // onPress: () => { + // onSetRotate('right') + // }, + // }, + { + name: 'flip' as const, + label: 'Flip horizontal', + onPress: onFlipHorizontal, + }, + { + name: 'flip' as const, + label: 'Flip vertically', + onPress: onFlipVertical, + }, + ], [onFlipHorizontal, onFlipVertical], ) useEffect(() => { image.prev = image.compressed - setIsEditing(true) + image.prevAttributes = image.attributes + image.resetCompressed() }, [image]) const onCloseModal = useCallback(() => { - shell.closeModal() - setIsEditing(false) - }, [shell]) + store.shell.closeModal() + }, [store.shell]) const onPressCancel = useCallback(async () => { await gallery.previous(image) @@ -184,25 +150,12 @@ export const Component = observer(function ({image, gallery}: Props) { ...(position !== undefined ? {position} : {}), } : {}), - ...manipulationAttributes, - aspectRatio, }) - image.prevAttributes = manipulationAttributes + image.prev = image.compressed + image.prevAttributes = image.attributes onCloseModal() - }, [ - altText, - aspectRatio, - image, - manipulationAttributes, - position, - scale, - onCloseModal, - ]) - - const onPressRatio = useCallback((as: AspectRatio) => { - setAspectRatio(as) - }, []) + }, [altText, image, position, scale, onCloseModal]) const getLabelIconSize = useCallback((as: AspectRatio) => { switch (as) { @@ -220,40 +173,55 @@ export const Component = observer(function ({image, gallery}: Props) { return null } - const {width, height} = image.getDisplayDimensions( - aspectRatio, - imgEditorStyles.width, - ) + const computedWidth = + windowDimensions.width > 500 ? 410 : windowDimensions.width - 80 + const sideLength = isDesktopWeb ? 300 : computedWidth + + const dimensions = image.getDisplayDimensions(aspectRatio, sideLength) + const imgContainerStyles = {width: sideLength, height: sideLength} + + const imgControlStyles = { + alignItems: 'center' as const, + flexDirection: isDesktopWeb ? ('row' as const) : ('column' as const), + gap: isDesktopWeb ? 5 : 0, + } return ( Edit image - - - + + + + + + setScale(Array.isArray(v) ? v[0] : v) + } + minimumValue={1} + maximumValue={3} /> - - setScale(Array.isArray(v) ? v[0] : v) - } - minimumValue={1} - maximumValue={3} - /> - - - {getKeys(ratios).map(ratio => { - const {hint, Icon, ...props} = ratios[ratio] + + {isDesktopWeb ? ( + + Ratios + + ) : null} + + {getKeys(RATIOS).map(ratio => { + const {Icon, ...props} = RATIOS[ratio] const labelIconSize = getLabelIconSize(ratio) const isSelected = aspectRatio === ratio @@ -261,10 +229,10 @@ export const Component = observer(function ({image, gallery}: Props) { { - onPressRatio(ratio) + onSetRatio(ratio) }} accessibilityLabel={ratio} - accessibilityHint={hint}> + accessibilityHint=""> - - - {adjustments.map(({label, hint, name, onPress}) => ( + {isDesktopWeb ? ( + + Transformations + + ) : null} + + {adjustments.map(({label, name, onPress}) => ( - + + + Accessibility + setAltText(enforceLen(text, MAX_ALT_TEXT))} - placeholder="Image description" - placeholderTextColor={pal.colors.textLight} - accessibilityLabel="Image alt text" - accessibilityHint="Sets image alt text for screenreaders" - accessibilityLabelledBy="imageAltText" + accessibilityLabel="Alt text" + accessibilityHint="" + accessibilityLabelledBy="alt-text" /> @@ -345,30 +318,16 @@ export const Component = observer(function ({image, gallery}: Props) { const styles = StyleSheet.create({ container: { gap: 18, - paddingVertical: 18, - paddingHorizontal: 12, + paddingHorizontal: isDesktopWeb ? undefined : 16, height: '100%', width: '100%', }, - gap18: { - gap: 18, - }, - + subsection: {marginTop: 12}, + gap18: {gap: 18}, title: { fontWeight: 'bold', fontSize: 24, }, - - textArea: { - borderWidth: 1, - borderRadius: 6, - paddingTop: 10, - paddingHorizontal: 12, - fontSize: 16, - height: 100, - textAlignVertical: 'top', - }, - btns: { flexDirection: 'row', alignItems: 'center', @@ -379,28 +338,12 @@ const styles = StyleSheet.create({ paddingVertical: 8, paddingHorizontal: 24, }, - - verticalSep: { - borderLeftWidth: 1, - }, - - imgControls: { - flexDirection: 'row', - gap: 5, - }, imgControl: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: 40, }, - flipVertical: { - transform: [{rotate: '90deg'}], - }, - flipBtn: { - paddingHorizontal: 4, - paddingVertical: 8, - }, imgEditor: { maxWidth: '100%', }, @@ -408,11 +351,29 @@ const styles = StyleSheet.create({ display: 'flex', alignItems: 'center', justifyContent: 'center', - height: 425, - width: 425, borderWidth: 1, - borderRadius: 8, borderStyle: 'solid', - overflow: 'hidden', + marginBottom: 4, + }, + flipVertical: { + transform: [{rotate: '90deg'}], + }, + flipBtn: { + paddingHorizontal: 4, + paddingVertical: 8, + }, + textArea: { + borderWidth: 1, + borderRadius: 6, + paddingTop: 10, + paddingHorizontal: 12, + fontSize: 16, + height: 100, + textAlignVertical: 'top', + maxHeight: isDesktopWeb ? undefined : 50, + }, + bottomSection: { + borderTopWidth: 1, + paddingTop: 18, }, })