diff --git a/src/lib/media/picker.tsx b/src/lib/media/picker.tsx
index bf531c98..37e01e67 100644
--- a/src/lib/media/picker.tsx
+++ b/src/lib/media/picker.tsx
@@ -1,8 +1,9 @@
import {
+ Image as RNImage,
openCamera as openCameraFn,
openCropper as openCropperFn,
- Image as RNImage,
} from 'react-native-image-crop-picker'
+
import {CameraOpts, CropperOptions} from './types'
export {openPicker} from './picker.shared'
diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx
index 995a0c95..8782e145 100644
--- a/src/lib/media/picker.web.tsx
+++ b/src/lib/media/picker.web.tsx
@@ -1,7 +1,8 @@
///
-import {CameraOpts, CropperOptions} from './types'
import {Image as RNImage} from 'react-native-image-crop-picker'
+
+import {CameraOpts, CropperOptions} from './types'
export {openPicker} from './picker.shared'
import {unstable__openModal} from '#/state/modals'
@@ -16,6 +17,10 @@ export async function openCropper(opts: CropperOptions): Promise {
unstable__openModal({
name: 'crop-image',
uri: opts.path,
+ dimensions:
+ opts.height && opts.width
+ ? {width: opts.width, height: opts.height}
+ : undefined,
onSelect: (img?: RNImage) => {
if (img) {
resolve(img)
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 0f61a971..cf82bcd0 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -47,6 +47,7 @@ export interface EditImageModal {
export interface CropImageModal {
name: 'crop-image'
uri: string
+ dimensions?: {width: number; height: number}
onSelect: (img?: RNImage) => void
}
diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx
index 79ff5a02..10cae2f1 100644
--- a/src/view/com/modals/crop-image/CropImage.web.tsx
+++ b/src/view/com/modals/crop-image/CropImage.web.tsx
@@ -14,11 +14,13 @@ import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util'
import {gradients, s} from 'lib/styles'
import {Text} from 'view/com/util/text/Text'
+import {calculateDimensions} from './cropImageUtil'
enum AspectRatio {
Square = 'square',
Wide = 'wide',
Tall = 'tall',
+ Custom = 'custom',
}
const DIMS: Record = {
@@ -31,17 +33,24 @@ export const snapPoints = ['0%']
export function Component({
uri,
+ dimensions,
onSelect,
}: {
uri: string
+ dimensions?: Dimensions
onSelect: (img?: RNImage) => void
}) {
const {closeModal} = useModalControls()
const pal = usePalette('default')
const {_} = useLingui()
- const [as, setAs] = React.useState(AspectRatio.Square)
+ const defaultAspectStyle = dimensions
+ ? AspectRatio.Custom
+ : AspectRatio.Square
+ const [as, setAs] = React.useState(defaultAspectStyle)
const [scale, setScale] = React.useState(1)
const editorRef = React.useRef(null)
+ const imageEditorWidth = dimensions ? dimensions.width : DIMS[as].width
+ const imageEditorHeight = dimensions ? dimensions.height : DIMS[as].height
const doSetAs = (v: AspectRatio) => () => setAs(v)
@@ -57,8 +66,8 @@ export function Component({
path: dataUri,
mime: 'image/jpeg',
size: getDataUriSize(dataUri),
- width: DIMS[as].width,
- height: DIMS[as].height,
+ width: imageEditorWidth,
+ height: imageEditorHeight,
})
} else {
onSelect(undefined)
@@ -73,7 +82,18 @@ export function Component({
cropperStyle = styles.cropperWide
} else if (as === AspectRatio.Tall) {
cropperStyle = styles.cropperTall
+ } else if (as === AspectRatio.Custom) {
+ const cropperDimensions = calculateDimensions(
+ 550,
+ imageEditorHeight,
+ imageEditorWidth,
+ )
+ cropperStyle = {
+ width: cropperDimensions.width,
+ height: cropperDimensions.height,
+ }
}
+
return (
@@ -81,8 +101,8 @@ export function Component({
ref={editorRef}
style={styles.imageEditor}
image={uri}
- width={DIMS[as].width}
- height={DIMS[as].height}
+ width={imageEditorWidth}
+ height={imageEditorHeight}
scale={scale}
border={0}
/>
@@ -97,36 +117,40 @@ export function Component({
maximumValue={3}
containerStyle={styles.slider}
/>
-
-
-
-
-
-
-
-
-
+ {as === AspectRatio.Custom ? null : (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
{
+ const aspectRatio = originalWidth / originalHeight
+ const newHeight = maxWidth / aspectRatio
+ const newWidth = maxWidth
+ return {
+ width: newWidth,
+ height: newHeight,
+ }
+}
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 118e2ce2..45327669 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -8,6 +8,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
+import {logger} from '#/logger'
import {usePalette} from 'lib/hooks/usePalette'
import {
useCameraPermission,
@@ -282,15 +283,21 @@ let EditableUserAvatar = ({
return
}
- const croppedImage = await openCropper({
- mediaType: 'photo',
- cropperCircleOverlay: true,
- height: item.height,
- width: item.width,
- path: item.path,
- })
+ try {
+ const croppedImage = await openCropper({
+ mediaType: 'photo',
+ cropperCircleOverlay: true,
+ height: item.height,
+ 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])
const onRemoveAvatar = React.useCallback(() => {
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index 4d73b853..93ea3275 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -1,29 +1,30 @@
import React from 'react'
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 {useLingui} from '@lingui/react'
+import {ModerationUI} from '@atproto/api'
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 {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 {Image as RNImage} from 'react-native-image-crop-picker'
import {EventStopper} from 'view/com/util/EventStopper'
-import * as Menu from '#/components/Menu'
+import {tokens, useTheme as useAlfTheme} from '#/alf'
import {
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
Camera_Stroke2_Corner0_Rounded as Camera,
} from '#/components/icons/Camera'
import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
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({
type,
@@ -64,14 +65,20 @@ export function UserBanner({
return
}
- onSelectNewBanner?.(
- await openCropper({
- mediaType: 'photo',
- path: items[0].path,
- width: 3000,
- height: 1000,
- }),
- )
+ try {
+ onSelectNewBanner?.(
+ await openCropper({
+ mediaType: 'photo',
+ path: items[0].path,
+ width: 3000,
+ height: 1000,
+ }),
+ )
+ } catch (e: any) {
+ if (!String(e).includes('Canceled')) {
+ logger.error('Failed to crop banner', {error: e})
+ }
+ }
}, [onSelectNewBanner, requestPhotoAccessIfNeeded])
const onRemoveBanner = React.useCallback(() => {