Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 197 lines 5.4 kB view raw
1import 'react-image-crop/dist/ReactCrop.css' 2 3import {useCallback, useImperativeHandle, useRef, useState} from 'react' 4import {View} from 'react-native' 5import {msg, Trans} from '@lingui/macro' 6import {useLingui} from '@lingui/react' 7import ReactCrop, {type PercentCrop} from 'react-image-crop' 8 9import { 10 type ImageSource, 11 type ImageTransformation, 12 manipulateImage, 13} from '#/state/gallery' 14import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 15import {atoms as a, useTheme} from '#/alf' 16import {Button, ButtonIcon, ButtonText} from '#/components/Button' 17import * as Dialog from '#/components/Dialog' 18import {Loader} from '#/components/Loader' 19import {type EditImageDialogProps} from './EditImageDialog' 20 21export function EditImageDialog(props: EditImageDialogProps) { 22 return ( 23 <Dialog.Outer control={props.control} webOptions={{alignCenter: true}}> 24 <Dialog.Handle /> 25 <DialogInner {...props} /> 26 </Dialog.Outer> 27 ) 28} 29 30function DialogInner({ 31 control, 32 image, 33 onChange, 34 circularCrop, 35 aspectRatio, 36}: EditImageDialogProps) { 37 const {_} = useLingui() 38 const [pending, setPending] = useState(false) 39 const ref = useRef<{save: () => Promise<void>}>(null) 40 41 const enableSquareButtons = useEnableSquareButtons() 42 43 const cancelButton = useCallback( 44 () => ( 45 <Button 46 label={_(msg`Cancel`)} 47 disabled={pending} 48 onPress={() => control.close()} 49 size="small" 50 color="primary" 51 variant="ghost" 52 style={enableSquareButtons ? [a.rounded_sm] : [a.rounded_full]} 53 testID="cropImageCancelBtn"> 54 <ButtonText style={[a.text_md]}> 55 <Trans>Cancel</Trans> 56 </ButtonText> 57 </Button> 58 ), 59 [control, _, pending, enableSquareButtons], 60 ) 61 62 const saveButton = useCallback( 63 () => ( 64 <Button 65 label={_(msg`Save`)} 66 onPress={async () => { 67 setPending(true) 68 await ref.current?.save() 69 setPending(false) 70 }} 71 disabled={pending} 72 size="small" 73 color="primary" 74 variant="ghost" 75 style={enableSquareButtons ? [a.rounded_sm] : [a.rounded_full]} 76 testID="cropImageSaveBtn"> 77 <ButtonText style={[a.text_md]}> 78 <Trans>Save</Trans> 79 </ButtonText> 80 {pending && <ButtonIcon icon={Loader} />} 81 </Button> 82 ), 83 [_, pending, enableSquareButtons], 84 ) 85 86 return ( 87 <Dialog.Inner 88 label={_(msg`Edit image`)} 89 header={ 90 <Dialog.Header renderLeft={cancelButton} renderRight={saveButton}> 91 <Dialog.HeaderText> 92 <Trans>Edit image</Trans> 93 </Dialog.HeaderText> 94 </Dialog.Header> 95 }> 96 {image && ( 97 <EditImageInner 98 saveRef={ref} 99 key={image.source.id} 100 image={image} 101 onChange={onChange} 102 circularCrop={circularCrop} 103 aspectRatio={aspectRatio} 104 /> 105 )} 106 </Dialog.Inner> 107 ) 108} 109 110function EditImageInner({ 111 image, 112 onChange, 113 saveRef, 114 circularCrop = false, 115 aspectRatio, 116}: Required<Pick<EditImageDialogProps, 'image'>> & 117 Omit<EditImageDialogProps, 'control' | 'image'> & { 118 saveRef: React.RefObject<{save: () => Promise<void>} | null> 119 }) { 120 const t = useTheme() 121 const [isDragging, setIsDragging] = useState(false) 122 const control = Dialog.useDialogContext() 123 124 const source = image.source 125 126 const initialCrop = getInitialCrop(source, image.manips) 127 const [crop, setCrop] = useState(initialCrop) 128 129 const onPressSubmit = useCallback(async () => { 130 const result = await manipulateImage(image, { 131 crop: 132 crop && (crop.width || crop.height) !== 0 133 ? { 134 originX: (crop.x * source.width) / 100, 135 originY: (crop.y * source.height) / 100, 136 width: (crop.width * source.width) / 100, 137 height: (crop.height * source.height) / 100, 138 } 139 : undefined, 140 }) 141 142 control.close(() => { 143 onChange(result) 144 }) 145 }, [crop, image, source, control, onChange]) 146 147 useImperativeHandle( 148 saveRef, 149 () => ({ 150 save: onPressSubmit, 151 }), 152 [onPressSubmit], 153 ) 154 155 return ( 156 <View 157 style={[ 158 a.mx_auto, 159 a.border, 160 t.atoms.border_contrast_low, 161 a.rounded_xs, 162 a.overflow_hidden, 163 a.align_center, 164 ]}> 165 <ReactCrop 166 crop={crop} 167 aspect={aspectRatio} 168 circularCrop={circularCrop} 169 onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)} 170 className="ReactCrop--no-animate" 171 onDragStart={() => setIsDragging(true)} 172 onDragEnd={() => setIsDragging(false)}> 173 <img src={source.path} style={{maxHeight: `50vh`}} /> 174 </ReactCrop> 175 {/* Eat clicks when dragging, otherwise mousing up over the backdrop 176 causes the dialog to close */} 177 {isDragging && <View style={[a.fixed, a.inset_0]} />} 178 </View> 179 ) 180} 181 182const getInitialCrop = ( 183 source: ImageSource, 184 manips: ImageTransformation | undefined, 185): PercentCrop | undefined => { 186 const initialArea = manips?.crop 187 188 if (initialArea) { 189 return { 190 unit: '%', 191 x: (initialArea.originX / source.width) * 100, 192 y: (initialArea.originY / source.height) * 100, 193 width: (initialArea.width / source.width) * 100, 194 height: (initialArea.height / source.height) * 100, 195 } 196 } 197}