From e2055dfb7842c15eb1cda847c74b24bf1f88d3b9 Mon Sep 17 00:00:00 2001
From: Ollie H <renahlee@outlook.com>
Date: Mon, 15 May 2023 14:54:14 -0700
Subject: Image editor mobile layout update (#613)

* Image editor mobile layout update

* Minor viewport fix
---
 src/view/com/modals/EditImage.tsx | 375 +++++++++++++++++---------------------
 1 file changed, 168 insertions(+), 207 deletions(-)

(limited to 'src/view/com/modals/EditImage.tsx')

diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx
index 4a5d9bfde..eab472a78 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<AspectRatio>(
-    image.aspectRatio ?? 'None',
-  )
-  const [flipHorizontal, setFlipHorizontal] = useState<boolean>(
-    image.flipHorizontal ?? false,
-  )
-  const [flipVertical, setFlipVertical] = useState<boolean>(
-    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<number>(image.scale ?? 1)
-  const [position, setPosition] = useState<Position>()
-  const [isEditing, setIsEditing] = useState(false)
   const editorRef = useRef<ImageEditor>(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],
-  )
-
-  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),
-    [],
+  const [scale, setScale] = useState<number>(image.attributes.scale ?? 1)
+  const [position, setPosition] = useState<Position | undefined>(
+    image.attributes.position,
   )
-
-  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 (
     <View testID="editImageModal" style={[pal.view, styles.container, s.flex1]}>
       <Text style={[styles.title, pal.text]}>Edit image</Text>
-      <View>
-        <View style={[styles.imgContainer, imgEditorStyles, pal.borderDark]}>
-          <ImageEditor
-            ref={editorRef}
-            style={styles.imgEditor}
-            image={isEditing ? image.compressed.path : image.path}
-            width={width}
-            height={height}
-            scale={scale}
-            border={0}
-            position={position}
-            onPositionChange={setPosition}
+      <View style={[styles.gap18, s.flexRow]}>
+        <View>
+          <View
+            style={[styles.imgContainer, pal.borderDark, imgContainerStyles]}>
+            <ImageEditor
+              ref={editorRef}
+              style={styles.imgEditor}
+              image={image.compressed.path}
+              scale={scale}
+              border={0}
+              position={position}
+              onPositionChange={setPosition}
+              {...dimensions}
+            />
+          </View>
+          <Slider
+            value={scale}
+            onValueChange={(v: number | number[]) =>
+              setScale(Array.isArray(v) ? v[0] : v)
+            }
+            minimumValue={1}
+            maximumValue={3}
           />
         </View>
-        <Slider
-          value={scale}
-          onValueChange={(v: number | number[]) =>
-            setScale(Array.isArray(v) ? v[0] : v)
-          }
-          minimumValue={1}
-          maximumValue={3}
-        />
-        <View style={[s.flexRow, styles.gap18]}>
-          <View style={styles.imgControls}>
-            {getKeys(ratios).map(ratio => {
-              const {hint, Icon, ...props} = ratios[ratio]
+        <View>
+          {isDesktopWeb ? (
+            <Text type="sm-bold" style={pal.text}>
+              Ratios
+            </Text>
+          ) : null}
+          <View style={imgControlStyles}>
+            {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) {
                 <Pressable
                   key={ratio}
                   onPress={() => {
-                    onPressRatio(ratio)
+                    onSetRatio(ratio)
                   }}
                   accessibilityLabel={ratio}
-                  accessibilityHint={hint}>
+                  accessibilityHint="">
                   <Icon
                     size={labelIconSize}
                     style={[styles.imgControl, isSelected ? s.blue3 : pal.text]}
@@ -281,18 +249,22 @@ export const Component = observer(function ({image, gallery}: Props) {
               )
             })}
           </View>
-          <View style={[styles.verticalSep, pal.border]} />
-          <View style={styles.imgControls}>
-            {adjustments.map(({label, hint, name, onPress}) => (
+          {isDesktopWeb ? (
+            <Text type="sm-bold" style={[pal.text, styles.subsection]}>
+              Transformations
+            </Text>
+          ) : null}
+          <View style={imgControlStyles}>
+            {adjustments.map(({label, name, onPress}) => (
               <Pressable
                 key={label}
                 onPress={onPress}
                 accessibilityLabel={label}
-                accessibilityHint={hint}
+                accessibilityHint=""
                 style={styles.flipBtn}>
                 <MaterialIcons
                   name={name}
-                  size={label.startsWith('Flip') ? 22 : 24}
+                  size={label?.startsWith('Flip') ? 22 : 24}
                   style={[
                     pal.text,
                     label === 'Flip vertically'
@@ -305,7 +277,10 @@ export const Component = observer(function ({image, gallery}: Props) {
           </View>
         </View>
       </View>
-      <View style={[styles.gap18]}>
+      <View style={[styles.gap18, styles.bottomSection, pal.border]}>
+        <Text type="sm-bold" style={pal.text} nativeID="alt-text">
+          Accessibility
+        </Text>
         <TextInput
           testID="altTextImageInput"
           style={[styles.textArea, pal.border, pal.text]}
@@ -313,11 +288,9 @@ export const Component = observer(function ({image, gallery}: Props) {
           multiline
           value={altText}
           onChangeText={text => 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"
         />
       </View>
       <View style={styles.btns}>
@@ -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,
   },
 })
-- 
cgit 1.4.1

