diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-01-27 15:51:24 -0600 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2023-01-27 15:51:24 -0600 |
commit | 7916b26aadb7e003728d9dc653ab8b8deabf4076 (patch) | |
tree | 507d24512fd71c67d4fe49af4ae5f8746444cceb /src/view/com/util | |
parent | 0673129b2018c9db0f7c3fc3e2c3214150efcfb8 (diff) | |
download | voidsky-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.tsx | 25 | ||||
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 39 | ||||
-rw-r--r-- | src/view/com/util/images/ImageCropPicker.tsx | 6 | ||||
-rw-r--r-- | src/view/com/util/images/ImageCropPicker.web.tsx | 32 | ||||
-rw-r--r-- | src/view/com/util/images/image-crop-picker/ImageCropPicker.tsx | 92 | ||||
-rw-r--r-- | src/view/com/util/images/image-crop-picker/ImageCropPicker.web.tsx | 75 | ||||
-rw-r--r-- | src/view/com/util/images/image-crop-picker/types.ts | 31 |
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 +} |