Merge branch 'main' into hailey/79fab
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import React from 'react'
|
||||
import type React from 'react'
|
||||
|
||||
import {ComposerImage} from '#/state/gallery'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {type ComposerImage} from '#/state/gallery'
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
|
||||
export type EditImageDialogProps = {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
image: ComposerImage
|
||||
image?: ComposerImage
|
||||
onChange: (next: ComposerImage) => void
|
||||
aspectRatio?: number
|
||||
circularCrop?: boolean
|
||||
}
|
||||
|
||||
export const EditImageDialog = ({}: EditImageDialogProps): React.ReactNode => {
|
||||
|
||||
@@ -1,43 +1,130 @@
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
|
||||
import React from 'react'
|
||||
import {useCallback, useImperativeHandle, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import ReactCrop, {PercentCrop} from 'react-image-crop'
|
||||
import ReactCrop, {type PercentCrop} from 'react-image-crop'
|
||||
|
||||
import {
|
||||
ImageSource,
|
||||
ImageTransformation,
|
||||
type ImageSource,
|
||||
type ImageTransformation,
|
||||
manipulateImage,
|
||||
} from '#/state/gallery'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {EditImageDialogProps} from './EditImageDialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {type EditImageDialogProps} from './EditImageDialog'
|
||||
|
||||
export const EditImageDialog = (props: EditImageDialogProps) => {
|
||||
export function EditImageDialog(props: EditImageDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
<EditImageInner key={props.image.source.id} {...props} />
|
||||
<DialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => {
|
||||
function DialogInner({
|
||||
control,
|
||||
image,
|
||||
onChange,
|
||||
circularCrop,
|
||||
aspectRatio,
|
||||
}: EditImageDialogProps) {
|
||||
const {_} = useLingui()
|
||||
const [pending, setPending] = useState(false)
|
||||
const ref = useRef<{save: () => Promise<void>}>(null)
|
||||
|
||||
const cancelButton = useCallback(
|
||||
() => (
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
disabled={pending}
|
||||
onPress={() => control.close()}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
style={[a.rounded_full]}
|
||||
testID="cropImageCancelBtn">
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
),
|
||||
[control, _, pending],
|
||||
)
|
||||
|
||||
const saveButton = useCallback(
|
||||
() => (
|
||||
<Button
|
||||
label={_(msg`Save`)}
|
||||
onPress={async () => {
|
||||
setPending(true)
|
||||
await ref.current?.save()
|
||||
setPending(false)
|
||||
}}
|
||||
disabled={pending}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
style={[a.rounded_full]}
|
||||
testID="cropImageSaveBtn">
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Save</Trans>
|
||||
</ButtonText>
|
||||
{pending && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
),
|
||||
[_, pending],
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Inner
|
||||
label={_(msg`Edit image`)}
|
||||
header={
|
||||
<Dialog.Header renderLeft={cancelButton} renderRight={saveButton}>
|
||||
<Dialog.HeaderText>
|
||||
<Trans>Edit image</Trans>
|
||||
</Dialog.HeaderText>
|
||||
</Dialog.Header>
|
||||
}>
|
||||
{image && (
|
||||
<EditImageInner
|
||||
saveRef={ref}
|
||||
key={image.source.id}
|
||||
image={image}
|
||||
onChange={onChange}
|
||||
circularCrop={circularCrop}
|
||||
aspectRatio={aspectRatio}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Inner>
|
||||
)
|
||||
}
|
||||
|
||||
function EditImageInner({
|
||||
image,
|
||||
onChange,
|
||||
saveRef,
|
||||
circularCrop = false,
|
||||
aspectRatio,
|
||||
}: Required<Pick<EditImageDialogProps, 'image'>> &
|
||||
Omit<EditImageDialogProps, 'control' | 'image'> & {
|
||||
saveRef: React.RefObject<{save: () => Promise<void>}>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const {_} = useLingui()
|
||||
const control = Dialog.useDialogContext()
|
||||
|
||||
const source = image.source
|
||||
|
||||
const initialCrop = getInitialCrop(source, image.manips)
|
||||
const [crop, setCrop] = React.useState(initialCrop)
|
||||
const [crop, setCrop] = useState(initialCrop)
|
||||
|
||||
const isEmpty = !crop || (crop.width || crop.height) === 0
|
||||
const isNew = initialCrop ? true : !isEmpty
|
||||
|
||||
const onPressSubmit = React.useCallback(async () => {
|
||||
const onPressSubmit = useCallback(async () => {
|
||||
const result = await manipulateImage(image, {
|
||||
crop:
|
||||
crop && (crop.width || crop.height) !== 0
|
||||
@@ -50,41 +137,43 @@ const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => {
|
||||
: undefined,
|
||||
})
|
||||
|
||||
onChange(result)
|
||||
control.close()
|
||||
control.close(() => {
|
||||
onChange(result)
|
||||
})
|
||||
}, [crop, image, source, control, onChange])
|
||||
|
||||
useImperativeHandle(
|
||||
saveRef,
|
||||
() => ({
|
||||
save: onPressSubmit,
|
||||
}),
|
||||
[onPressSubmit],
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Inner label={_(msg`Edit image`)}>
|
||||
<Dialog.Close />
|
||||
|
||||
<Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_sm]}>
|
||||
<Trans>Edit image</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.align_center]}>
|
||||
<ReactCrop
|
||||
crop={crop}
|
||||
onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)}
|
||||
className="ReactCrop--no-animate">
|
||||
<img src={source.path} style={{maxHeight: `50vh`}} />
|
||||
</ReactCrop>
|
||||
</View>
|
||||
|
||||
<View style={[a.mt_md, a.gap_md]}>
|
||||
<Button
|
||||
disabled={!isNew}
|
||||
label={_(msg`Save`)}
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={onPressSubmit}>
|
||||
<ButtonText>
|
||||
<Trans>Save</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</Dialog.Inner>
|
||||
<View
|
||||
style={[
|
||||
a.mx_auto,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
a.rounded_xs,
|
||||
a.overflow_hidden,
|
||||
a.align_center,
|
||||
]}>
|
||||
<ReactCrop
|
||||
crop={crop}
|
||||
aspect={aspectRatio}
|
||||
circularCrop={circularCrop}
|
||||
onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)}
|
||||
className="ReactCrop--no-animate"
|
||||
onDragStart={() => setIsDragging(true)}
|
||||
onDragEnd={() => setIsDragging(false)}>
|
||||
<img src={source.path} style={{maxHeight: `50vh`}} />
|
||||
</ReactCrop>
|
||||
{/* Eat clicks when dragging, otherwise mousing up over the backdrop
|
||||
causes the dialog to close */}
|
||||
{isDragging && <View style={[a.fixed, a.inset_0]} />}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import RNPickerSelect from 'react-native-picker-select'
|
||||
import {Keyboard, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -240,19 +239,21 @@ function SubtitleFileRow({
|
||||
numberOfLines={1}>
|
||||
{file.name}
|
||||
</Text>
|
||||
<RNPickerSelect
|
||||
placeholder={{
|
||||
label: _(msg`Select language...`),
|
||||
value: '',
|
||||
}}
|
||||
<select
|
||||
value={language}
|
||||
onValueChange={handleValueChange}
|
||||
items={otherLanguages.map(lang => ({
|
||||
label: `${lang.name} (${langCode(lang)})`,
|
||||
value: langCode(lang),
|
||||
}))}
|
||||
style={{viewContainer: {maxWidth: 200, flex: 1}}}
|
||||
/>
|
||||
onChange={evt => handleValueChange(evt.target.value)}
|
||||
style={{maxWidth: 200, flex: 1}}>
|
||||
<option value="" disabled selected hidden>
|
||||
{/* eslint-disable-next-line bsky-internal/avoid-unwrapped-text */}
|
||||
<Trans>Select language...</Trans>
|
||||
</option>
|
||||
{otherLanguages.map(lang => (
|
||||
<option key={langCode(lang)} value={langCode(lang)}>
|
||||
{/* eslint-disable-next-line bsky-internal/avoid-unwrapped-text */}
|
||||
{`${lang.name} (${langCode(lang)})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user