Merge branch 'piotrpalek-fix-banner-cropper' into main

zio/stable
Paul Frazee 2024-05-06 15:49:52 -07:00
commit 2ca4b74955
7 changed files with 122 additions and 64 deletions

View File

@ -1,8 +1,9 @@
import { import {
Image as RNImage,
openCamera as openCameraFn, openCamera as openCameraFn,
openCropper as openCropperFn, openCropper as openCropperFn,
Image as RNImage,
} from 'react-native-image-crop-picker' } from 'react-native-image-crop-picker'
import {CameraOpts, CropperOptions} from './types' import {CameraOpts, CropperOptions} from './types'
export {openPicker} from './picker.shared' export {openPicker} from './picker.shared'

View File

@ -1,7 +1,8 @@
/// <reference lib="dom" /> /// <reference lib="dom" />
import {CameraOpts, CropperOptions} from './types'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {CameraOpts, CropperOptions} from './types'
export {openPicker} from './picker.shared' export {openPicker} from './picker.shared'
import {unstable__openModal} from '#/state/modals' import {unstable__openModal} from '#/state/modals'
@ -16,6 +17,10 @@ export async function openCropper(opts: CropperOptions): Promise<RNImage> {
unstable__openModal({ unstable__openModal({
name: 'crop-image', name: 'crop-image',
uri: opts.path, uri: opts.path,
dimensions:
opts.height && opts.width
? {width: opts.width, height: opts.height}
: undefined,
onSelect: (img?: RNImage) => { onSelect: (img?: RNImage) => {
if (img) { if (img) {
resolve(img) resolve(img)

View File

@ -47,6 +47,7 @@ export interface EditImageModal {
export interface CropImageModal { export interface CropImageModal {
name: 'crop-image' name: 'crop-image'
uri: string uri: string
dimensions?: {width: number; height: number}
onSelect: (img?: RNImage) => void onSelect: (img?: RNImage) => void
} }

View File

@ -14,11 +14,13 @@ import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util' import {getDataUriSize} from 'lib/media/util'
import {gradients, s} from 'lib/styles' import {gradients, s} from 'lib/styles'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {calculateDimensions} from './cropImageUtil'
enum AspectRatio { enum AspectRatio {
Square = 'square', Square = 'square',
Wide = 'wide', Wide = 'wide',
Tall = 'tall', Tall = 'tall',
Custom = 'custom',
} }
const DIMS: Record<string, Dimensions> = { const DIMS: Record<string, Dimensions> = {
@ -31,17 +33,24 @@ export const snapPoints = ['0%']
export function Component({ export function Component({
uri, uri,
dimensions,
onSelect, onSelect,
}: { }: {
uri: string uri: string
dimensions?: Dimensions
onSelect: (img?: RNImage) => void onSelect: (img?: RNImage) => void
}) { }) {
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square) const defaultAspectStyle = dimensions
? AspectRatio.Custom
: AspectRatio.Square
const [as, setAs] = React.useState<AspectRatio>(defaultAspectStyle)
const [scale, setScale] = React.useState<number>(1) const [scale, setScale] = React.useState<number>(1)
const editorRef = React.useRef<ImageEditor>(null) 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 doSetAs = (v: AspectRatio) => () => setAs(v)
@ -57,8 +66,8 @@ export function Component({
path: dataUri, path: dataUri,
mime: 'image/jpeg', mime: 'image/jpeg',
size: getDataUriSize(dataUri), size: getDataUriSize(dataUri),
width: DIMS[as].width, width: imageEditorWidth,
height: DIMS[as].height, height: imageEditorHeight,
}) })
} else { } else {
onSelect(undefined) onSelect(undefined)
@ -73,7 +82,18 @@ export function Component({
cropperStyle = styles.cropperWide cropperStyle = styles.cropperWide
} else if (as === AspectRatio.Tall) { } else if (as === AspectRatio.Tall) {
cropperStyle = styles.cropperTall cropperStyle = styles.cropperTall
} else if (as === AspectRatio.Custom) {
const cropperDimensions = calculateDimensions(
550,
imageEditorHeight,
imageEditorWidth,
)
cropperStyle = {
width: cropperDimensions.width,
height: cropperDimensions.height,
}
} }
return ( return (
<View> <View>
<View style={[styles.cropper, pal.borderDark, cropperStyle]}> <View style={[styles.cropper, pal.borderDark, cropperStyle]}>
@ -81,8 +101,8 @@ export function Component({
ref={editorRef} ref={editorRef}
style={styles.imageEditor} style={styles.imageEditor}
image={uri} image={uri}
width={DIMS[as].width} width={imageEditorWidth}
height={DIMS[as].height} height={imageEditorHeight}
scale={scale} scale={scale}
border={0} border={0}
/> />
@ -97,36 +117,40 @@ export function Component({
maximumValue={3} maximumValue={3}
containerStyle={styles.slider} containerStyle={styles.slider}
/> />
<TouchableOpacity {as === AspectRatio.Custom ? null : (
onPress={doSetAs(AspectRatio.Wide)} <>
accessibilityRole="button" <TouchableOpacity
accessibilityLabel={_(msg`Wide`)} onPress={doSetAs(AspectRatio.Wide)}
accessibilityHint={_(msg`Sets image aspect ratio to wide`)}> accessibilityRole="button"
<RectWideIcon accessibilityLabel={_(msg`Wide`)}
size={24} accessibilityHint={_(msg`Sets image aspect ratio to wide`)}>
style={as === AspectRatio.Wide ? s.blue3 : pal.text} <RectWideIcon
/> size={24}
</TouchableOpacity> style={as === AspectRatio.Wide ? s.blue3 : pal.text}
<TouchableOpacity />
onPress={doSetAs(AspectRatio.Tall)} </TouchableOpacity>
accessibilityRole="button" <TouchableOpacity
accessibilityLabel={_(msg`Tall`)} onPress={doSetAs(AspectRatio.Tall)}
accessibilityHint={_(msg`Sets image aspect ratio to tall`)}> accessibilityRole="button"
<RectTallIcon accessibilityLabel={_(msg`Tall`)}
size={24} accessibilityHint={_(msg`Sets image aspect ratio to tall`)}>
style={as === AspectRatio.Tall ? s.blue3 : pal.text} <RectTallIcon
/> size={24}
</TouchableOpacity> style={as === AspectRatio.Tall ? s.blue3 : pal.text}
<TouchableOpacity />
onPress={doSetAs(AspectRatio.Square)} </TouchableOpacity>
accessibilityRole="button" <TouchableOpacity
accessibilityLabel={_(msg`Square`)} onPress={doSetAs(AspectRatio.Square)}
accessibilityHint={_(msg`Sets image aspect ratio to square`)}> accessibilityRole="button"
<SquareIcon accessibilityLabel={_(msg`Square`)}
size={24} accessibilityHint={_(msg`Sets image aspect ratio to square`)}>
style={as === AspectRatio.Square ? s.blue3 : pal.text} <SquareIcon
/> size={24}
</TouchableOpacity> style={as === AspectRatio.Square ? s.blue3 : pal.text}
/>
</TouchableOpacity>
</>
)}
</View> </View>
<View style={styles.btns}> <View style={styles.btns}>
<TouchableOpacity <TouchableOpacity

View File

@ -0,0 +1,13 @@
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,
}
}

View File

@ -8,6 +8,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import { import {
useCameraPermission, useCameraPermission,
@ -282,15 +283,21 @@ let EditableUserAvatar = ({
return return
} }
const croppedImage = await openCropper({ try {
mediaType: 'photo', const croppedImage = await openCropper({
cropperCircleOverlay: true, mediaType: 'photo',
height: item.height, cropperCircleOverlay: true,
width: item.width, height: item.height,
path: item.path, width: item.width,
}) path: item.path,
})
onSelectNewAvatar(croppedImage) onSelectNewAvatar(croppedImage)
} catch (e: any) {
if (!String(e).includes('Canceled')) {
logger.error('Failed to crop banner', {error: e})
}
}
}, [onSelectNewAvatar, requestPhotoAccessIfNeeded]) }, [onSelectNewAvatar, requestPhotoAccessIfNeeded])
const onRemoveAvatar = React.useCallback(() => { const onRemoveAvatar = React.useCallback(() => {

View File

@ -1,29 +1,30 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {ModerationUI} from '@atproto/api' import {Image as RNImage} from 'react-native-image-crop-picker'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {useLingui} from '@lingui/react' import {ModerationUI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {usePalette} from 'lib/hooks/usePalette'
import {
useCameraPermission,
usePhotoLibraryPermission,
} from 'lib/hooks/usePermissions'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {useTheme as useAlfTheme, tokens} from '#/alf'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
import {
usePhotoLibraryPermission,
useCameraPermission,
} from 'lib/hooks/usePermissions'
import {usePalette} from 'lib/hooks/usePalette'
import {isAndroid, isNative} from 'platform/detection' import {isAndroid, isNative} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {EventStopper} from 'view/com/util/EventStopper' import {EventStopper} from 'view/com/util/EventStopper'
import * as Menu from '#/components/Menu' import {tokens, useTheme as useAlfTheme} from '#/alf'
import { import {
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
Camera_Stroke2_Corner0_Rounded as Camera, Camera_Stroke2_Corner0_Rounded as Camera,
} from '#/components/icons/Camera' } from '#/components/icons/Camera'
import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import * as Menu from '#/components/Menu'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
export function UserBanner({ export function UserBanner({
type, type,
@ -64,14 +65,20 @@ export function UserBanner({
return return
} }
onSelectNewBanner?.( try {
await openCropper({ onSelectNewBanner?.(
mediaType: 'photo', await openCropper({
path: items[0].path, mediaType: 'photo',
width: 3000, path: items[0].path,
height: 1000, width: 3000,
}), height: 1000,
) }),
)
} catch (e: any) {
if (!String(e).includes('Canceled')) {
logger.error('Failed to crop banner', {error: e})
}
}
}, [onSelectNewBanner, requestPhotoAccessIfNeeded]) }, [onSelectNewBanner, requestPhotoAccessIfNeeded])
const onRemoveBanner = React.useCallback(() => { const onRemoveBanner = React.useCallback(() => {