about summary refs log tree commit diff
path: root/src/view/com/util
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-01-27 15:51:24 -0600
committerPaul Frazee <pfrazee@gmail.com>2023-01-27 15:51:24 -0600
commit7916b26aadb7e003728d9dc653ab8b8deabf4076 (patch)
tree507d24512fd71c67d4fe49af4ae5f8746444cceb /src/view/com/util
parent0673129b2018c9db0f7c3fc3e2c3214150efcfb8 (diff)
downloadvoidsky-7916b26aadb7e003728d9dc653ab8b8deabf4076.tar.zst
Break out the web/native image picking code and make some progress on the web version
Diffstat (limited to 'src/view/com/util')
-rw-r--r--src/view/com/util/UserAvatar.tsx25
-rw-r--r--src/view/com/util/UserBanner.tsx39
-rw-r--r--src/view/com/util/images/ImageCropPicker.tsx6
-rw-r--r--src/view/com/util/images/ImageCropPicker.web.tsx32
-rw-r--r--src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx92
-rw-r--r--src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx75
-rw-r--r--src/view/com/util/images/image-crop-picker/types.ts31
7 files changed, 228 insertions, 72 deletions
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index d91607b6c..287d94412 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -6,8 +6,9 @@ import {
   openCamera,
   openCropper,
   openPicker,
-  Image as PickedImage,
-} from './images/ImageCropPicker'
+  PickedMedia,
+} from './images/image-crop-picker/ImageCropPicker'
+import {useStores} from '../../../state'
 import {colors, gradients} from '../../lib/styles'
 
 export function UserAvatar({
@@ -21,8 +22,9 @@ export function UserAvatar({
   handle: string
   displayName: string | undefined
   avatar?: string | null
-  onSelectNewAvatar?: (img: PickedImage) => void
+  onSelectNewAvatar?: (img: PickedMedia) => void
 }) {
+  const store = useStores()
   const initials = getInitials(displayName || handle)
 
   const handleEditAvatar = useCallback(() => {
@@ -30,37 +32,32 @@ export function UserAvatar({
       {
         text: 'Take a new photo',
         onPress: () => {
-          openCamera({
+          openCamera(store, {
             mediaType: 'photo',
-            cropping: true,
             width: 1000,
             height: 1000,
             cropperCircleOverlay: true,
-            forceJpg: true, // ios only
-            compressImageQuality: 1,
           }).then(onSelectNewAvatar)
         },
       },
       {
         text: 'Select from gallery',
         onPress: () => {
-          openPicker({
+          openPicker(store, {
             mediaType: 'photo',
-          }).then(async item => {
-            await openCropper({
+          }).then(async items => {
+            await openCropper(store, {
               mediaType: 'photo',
-              path: item.path,
+              path: items[0].path,
               width: 1000,
               height: 1000,
               cropperCircleOverlay: true,
-              forceJpg: true, // ios only
-              compressImageQuality: 1,
             }).then(onSelectNewAvatar)
           })
         },
       },
     ])
-  }, [onSelectNewAvatar])
+  }, [store, onSelectNewAvatar])
 
   const renderSvg = (svgSize: number, svgInitials: string) => (
     <Svg width={svgSize} height={svgSize} viewBox="0 0 100 100">
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index fe606bc55..d5d6e3aaa 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -2,57 +2,56 @@ import React, {useCallback} from 'react'
 import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
 import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Image as PickedImage} from './images/ImageCropPicker'
 import {colors, gradients} from '../../lib/styles'
-import {openCamera, openCropper, openPicker} from './images/ImageCropPicker'
+import {
+  openCamera,
+  openCropper,
+  openPicker,
+  PickedMedia,
+} from './images/image-crop-picker/ImageCropPicker'
+import {useStores} from '../../../state'
 
 export function UserBanner({
   banner,
   onSelectNewBanner,
 }: {
   banner?: string | null
-  onSelectNewBanner?: (img: PickedImage) => void
+  onSelectNewBanner?: (img: PickedMedia) => void
 }) {
+  const store = useStores()
   const handleEditBanner = useCallback(() => {
     Alert.alert('Select upload method', '', [
       {
         text: 'Take a new photo',
         onPress: () => {
-          openCamera({
+          openCamera(store, {
             mediaType: 'photo',
-            cropping: true,
-            compressImageMaxWidth: 3000,
+            // compressImageMaxWidth: 3000, TODO needed?
             width: 3000,
-            compressImageMaxHeight: 1000,
+            // compressImageMaxHeight: 1000, TODO needed?
             height: 1000,
-            forceJpg: true, // ios only
-            compressImageQuality: 1,
-            includeExif: true,
           }).then(onSelectNewBanner)
         },
       },
       {
         text: 'Select from gallery',
         onPress: () => {
-          openPicker({
+          openPicker(store, {
             mediaType: 'photo',
-          }).then(async item => {
-            await openCropper({
+          }).then(async items => {
+            await openCropper(store, {
               mediaType: 'photo',
-              path: item.path,
-              compressImageMaxWidth: 3000,
+              path: items[0].path,
+              // compressImageMaxWidth: 3000, TODO needed?
               width: 3000,
-              compressImageMaxHeight: 1000,
+              // compressImageMaxHeight: 1000, TODO needed?
               height: 1000,
-              forceJpg: true, // ios only
-              compressImageQuality: 1,
-              includeExif: true,
             }).then(onSelectNewBanner)
           })
         },
       },
     ])
-  }, [onSelectNewBanner])
+  }, [store, onSelectNewBanner])
 
   const renderSvg = () => (
     <Svg width="100%" height="150" viewBox="50 0 200 100">
diff --git a/src/view/com/util/images/ImageCropPicker.tsx b/src/view/com/util/images/ImageCropPicker.tsx
deleted file mode 100644
index 9cd4da9f5..000000000
--- a/src/view/com/util/images/ImageCropPicker.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-export {
-  openPicker,
-  openCamera,
-  openCropper,
-} from 'react-native-image-crop-picker'
-export type {Image} from 'react-native-image-crop-picker'
diff --git a/src/view/com/util/images/ImageCropPicker.web.tsx b/src/view/com/util/images/ImageCropPicker.web.tsx
deleted file mode 100644
index a385e2e93..000000000
--- a/src/view/com/util/images/ImageCropPicker.web.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import type {
-  Image,
-  Video,
-  ImageOrVideo,
-  Options,
-  PossibleArray,
-} from 'react-native-image-crop-picker'
-
-export type {Image} from 'react-native-image-crop-picker'
-
-type MediaType<O> = O extends {mediaType: 'photo'}
-  ? Image
-  : O extends {mediaType: 'video'}
-  ? Video
-  : ImageOrVideo
-
-export async function openPicker<O extends Options>(
-  _options: O,
-): Promise<PossibleArray<O, MediaType<O>>> {
-  // TODO
-  throw new Error('TODO')
-}
-export async function openCamera<O extends Options>(
-  _options: O,
-): Promise<PossibleArray<O, MediaType<O>>> {
-  // TODO
-  throw new Error('TODO')
-}
-export async function openCropper(_options: Options): Promise<Image> {
-  // TODO
-  throw new Error('TODO')
-}
diff --git a/src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx b/src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx
new file mode 100644
index 000000000..ddc9e87fd
--- /dev/null
+++ b/src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx
@@ -0,0 +1,92 @@
+import {
+  openPicker as openPickerFn,
+  openCamera as openCameraFn,
+  openCropper as openCropperFn,
+  ImageOrVideo,
+} from 'react-native-image-crop-picker'
+import {RootStoreModel} from '../../../../../state'
+import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
+export type {PickedMedia} from './types'
+
+/**
+ * NOTE
+ * These methods all include the RootStoreModel as the first param
+ * because the web versions require it. The signatures have to remain
+ * equivalent between the different forms, but the store param is not
+ * used here.
+ * -prf
+ */
+
+export async function openPicker(
+  _store: RootStoreModel,
+  opts: PickerOpts,
+): Promise<PickedMedia[]> {
+  const mediaType = opts.mediaType || 'photo'
+  const items = await openPickerFn({
+    mediaType,
+    multiple: opts.multiple,
+    maxFiles: opts.maxFiles,
+  })
+  const toMedia = (item: ImageOrVideo) => ({
+    mediaType,
+    path: item.path,
+    mime: item.mime,
+    size: item.size,
+    width: item.width,
+    height: item.height,
+  })
+  if (Array.isArray(items)) {
+    return items.map(toMedia)
+  }
+  return [toMedia(items)]
+}
+
+export async function openCamera(
+  _store: RootStoreModel,
+  opts: CameraOpts,
+): Promise<PickedMedia> {
+  const mediaType = opts.mediaType || 'photo'
+  const item = await openCameraFn({
+    mediaType,
+    width: opts.width,
+    height: opts.height,
+    freeStyleCropEnabled: opts.freeStyleCropEnabled,
+    cropperCircleOverlay: opts.cropperCircleOverlay,
+    cropping: true,
+    forceJpg: true, // ios only
+    compressImageQuality: 1.0,
+  })
+  return {
+    mediaType,
+    path: item.path,
+    mime: item.mime,
+    size: item.size,
+    width: item.width,
+    height: item.height,
+  }
+}
+
+export async function openCropper(
+  _store: RootStoreModel,
+  opts: CropperOpts,
+): Promise<PickedMedia> {
+  const mediaType = opts.mediaType || 'photo'
+  const item = await openCropperFn({
+    path: opts.path,
+    mediaType: opts.mediaType || 'photo',
+    width: opts.width,
+    height: opts.height,
+    freeStyleCropEnabled: opts.freeStyleCropEnabled,
+    cropperCircleOverlay: opts.cropperCircleOverlay,
+    forceJpg: true, // ios only
+    compressImageQuality: 1.0,
+  })
+  return {
+    mediaType,
+    path: item.path,
+    mime: item.mime,
+    size: item.size,
+    width: item.width,
+    height: item.height,
+  }
+}
diff --git a/src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx b/src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx
new file mode 100644
index 000000000..a7037f3a4
--- /dev/null
+++ b/src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx
@@ -0,0 +1,75 @@
+/// <reference lib="dom" />
+
+import {CropImageModal} from '../../../../../state/models/shell-ui'
+import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
+export type {PickedMedia} from './types'
+import {RootStoreModel} from '../../../../../state'
+
+interface PickedFile {
+  uri: string
+  path: string
+  size: number
+}
+
+export async function openPicker(
+  store: RootStoreModel,
+  opts: PickerOpts,
+): Promise<PickedMedia[] | PickedMedia> {
+  const res = await selectFile(opts)
+  return new Promise((resolve, reject) => {
+    store.shell.openModal(
+      new CropImageModal(res.uri, (img?: PickedMedia) => {
+        if (img) {
+          resolve(img)
+        } else {
+          reject(new Error('Canceled'))
+        }
+      }),
+    )
+  })
+}
+
+export async function openCamera(
+  _store: RootStoreModel,
+  opts: CameraOpts,
+): Promise<PickedMedia> {
+  const mediaType = opts.mediaType || 'photo'
+  throw new Error('TODO')
+}
+
+export async function openCropper(
+  _store: RootStoreModel,
+  opts: CropperOpts,
+): Promise<PickedMedia> {
+  const mediaType = opts.mediaType || 'photo'
+  throw new Error('TODO')
+}
+
+function selectFile(opts: PickerOpts): Promise<PickedFile> {
+  return new Promise((resolve, reject) => {
+    var input = document.createElement('input')
+    input.type = 'file'
+    input.accept = opts.mediaType === 'photo' ? 'image/*' : '*/*'
+    input.onchange = e => {
+      const target = e.target as HTMLInputElement
+      const file = target?.files?.[0]
+      if (!file) {
+        return reject(new Error('Canceled'))
+      }
+
+      var reader = new FileReader()
+      reader.readAsDataURL(file)
+      reader.onload = readerEvent => {
+        if (!readerEvent.target) {
+          return reject(new Error('Canceled'))
+        }
+        resolve({
+          uri: readerEvent.target.result as string,
+          path: file.name,
+          size: file.size,
+        })
+      }
+    }
+    input.click()
+  })
+}
diff --git a/src/view/com/util/images/image-crop-picker/types.ts b/src/view/com/util/images/image-crop-picker/types.ts
new file mode 100644
index 000000000..3197b4d3e
--- /dev/null
+++ b/src/view/com/util/images/image-crop-picker/types.ts
@@ -0,0 +1,31 @@
+export interface PickerOpts {
+  mediaType?: 'photo'
+  multiple?: boolean
+  maxFiles?: number
+}
+
+export interface CameraOpts {
+  mediaType?: 'photo'
+  width: number
+  height: number
+  freeStyleCropEnabled?: boolean
+  cropperCircleOverlay?: boolean
+}
+
+export interface CropperOpts {
+  path: string
+  mediaType?: 'photo'
+  width: number
+  height: number
+  freeStyleCropEnabled?: boolean
+  cropperCircleOverlay?: boolean
+}
+
+export interface PickedMedia {
+  mediaType: 'photo'
+  path: string
+  mime: string
+  size: number
+  width: number
+  height: number
+}