tooot/src/screens/Shared/Compose/EditAttachment.tsx

353 lines
11 KiB
TypeScript
Raw Normal View History

2020-12-06 12:52:29 +01:00
import { useNavigation } from '@react-navigation/native'
2020-12-06 16:06:38 +01:00
import React, {
Dispatch,
useCallback,
useEffect,
useRef,
useState
} from 'react'
2020-12-06 12:52:29 +01:00
import {
2020-12-06 16:06:38 +01:00
Alert,
2020-12-06 12:52:29 +01:00
Animated,
Dimensions,
Image,
KeyboardAvoidingView,
ScrollView,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import Svg, { Circle, G, Path } from 'react-native-svg'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
2020-12-13 14:04:25 +01:00
import { HeaderLeft, HeaderRight } from '@components/Header'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
2020-12-06 12:52:29 +01:00
import { PanGestureHandler } from 'react-native-gesture-handler'
2020-12-13 14:04:25 +01:00
import { PostAction } from '@screens/Shared/Compose'
import client from '@api/client'
import AttachmentVideo from '@components/Timelines/Timeline/Shared/Attachment/AttachmentVideo'
2020-12-06 12:52:29 +01:00
const Stack = createNativeStackNavigator()
export interface Props {
route: {
params: {
attachment: Mastodon.Attachment & { local_url: string }
2020-12-07 12:31:40 +01:00
composeDispatch: Dispatch<PostAction>
2020-12-06 12:52:29 +01:00
}
}
}
const ComposeEditAttachment: React.FC<Props> = ({
route: {
2020-12-07 12:31:40 +01:00
params: { attachment, composeDispatch }
2020-12-06 12:52:29 +01:00
}
}) => {
const navigation = useNavigation()
2020-12-06 16:06:38 +01:00
2020-12-06 12:52:29 +01:00
const { theme } = useTheme()
2020-12-06 16:06:38 +01:00
const [altText, setAltText] = useState<string | undefined>(
attachment.description
)
const focus = useRef({ x: 0, y: 0 })
2020-12-06 12:52:29 +01:00
2020-12-06 16:06:38 +01:00
useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', () => {
let needUpdate = false
if (altText) {
attachment.description = altText
needUpdate = true
}
2020-12-06 21:42:19 +01:00
if (attachment.type === 'image') {
if (focus.current.x !== 0 || focus.current.y !== 0) {
attachment.meta!.focus = {
x: focus.current.x > 1 ? 1 : focus.current.x,
y: focus.current.y > 1 ? 1 : focus.current.y
}
needUpdate = true
2020-12-06 16:06:38 +01:00
}
}
if (needUpdate) {
2020-12-07 12:31:40 +01:00
composeDispatch({ type: 'attachmentEdit', payload: attachment })
2020-12-06 16:06:38 +01:00
}
})
2020-12-06 12:52:29 +01:00
2020-12-06 16:06:38 +01:00
return unsubscribe
}, [navigation, altText])
2020-12-06 21:42:19 +01:00
const videoPlayback = useCallback(() => {
return (
<AttachmentVideo
media_attachments={[attachment as Mastodon.AttachmentVideo]}
width={Dimensions.get('screen').width}
/>
)
}, [])
2020-12-06 16:06:38 +01:00
const imageFocus = useCallback(() => {
2020-12-06 21:42:19 +01:00
const imageDimensionis = {
width: Dimensions.get('screen').width,
height:
Dimensions.get('screen').width /
(attachment as Mastodon.AttachmentImage).meta?.original?.aspect!
}
2020-12-06 16:06:38 +01:00
const panFocus = useRef(
new Animated.ValueXY(
2020-12-06 21:42:19 +01:00
(attachment as Mastodon.AttachmentImage).meta?.focus?.x &&
(attachment as Mastodon.AttachmentImage).meta?.focus?.y
2020-12-06 16:06:38 +01:00
? {
2020-12-06 21:42:19 +01:00
x:
((attachment as Mastodon.AttachmentImage).meta!.focus!.x *
imageDimensionis.width) /
2,
y:
(-(attachment as Mastodon.AttachmentImage).meta!.focus!.y *
imageDimensionis.height) /
2
2020-12-06 16:06:38 +01:00
}
: { x: 0, y: 0 }
)
).current
2020-12-06 12:52:29 +01:00
const panX = panFocus.x.interpolate({
inputRange: [-imageDimensionis.width / 2, imageDimensionis.width / 2],
outputRange: [-imageDimensionis.width / 2, imageDimensionis.width / 2],
extrapolate: 'clamp'
})
const panY = panFocus.y.interpolate({
inputRange: [-imageDimensionis.height / 2, imageDimensionis.height / 2],
outputRange: [-imageDimensionis.height / 2, imageDimensionis.height / 2],
extrapolate: 'clamp'
})
2020-12-06 16:06:38 +01:00
panFocus.addListener(e => {
focus.current = {
x: e.x / (imageDimensionis.width / 2),
y: -e.y / (imageDimensionis.height / 2)
}
})
2020-12-06 12:52:29 +01:00
const handleGesture = Animated.event(
[{ nativeEvent: { translationX: panFocus.x, translationY: panFocus.y } }],
{ useNativeDriver: true }
)
const onHandlerStateChange = useCallback(() => {
panFocus.extractOffset()
}, [])
return (
2020-12-06 16:06:38 +01:00
<>
<View style={{ overflow: 'hidden' }}>
<Image
style={{
width: imageDimensionis.width,
height: imageDimensionis.height
}}
source={{
uri: attachment.local_url || attachment.preview_url
}}
/>
<PanGestureHandler
onGestureEvent={handleGesture}
onHandlerStateChange={onHandlerStateChange}
2020-12-06 12:52:29 +01:00
>
2020-12-06 16:06:38 +01:00
<Animated.View
style={[
{
position: 'absolute',
top: -1000 + imageDimensionis.height / 2,
left: -1000 + imageDimensionis.width / 2,
transform: [{ translateX: panX }, { translateY: panY }]
}
]}
>
<Svg width='2000' height='2000' viewBox='0 0 2000 2000'>
<G>
<G id='Mask'>
<Path
d={
'M2000,0 L2000,2000 L0,2000 L0,0 L2000,0 Z M1000,967 C981.774603,967 967,981.774603 967,1000 C967,1018.2254 981.774603,1033 1000,1033 C1018.2254,1033 1033,1018.2254 1033,1000 C1033,981.774603 1018.2254,967 1000,967 Z'
}
fill={theme.backgroundOverlay}
2020-12-06 12:52:29 +01:00
/>
2020-12-06 16:06:38 +01:00
<G transform='translate(967, 967)'>
<Circle
stroke={theme.background}
strokeWidth='2'
cx='33'
cy='33'
r='33'
/>
<Circle fill={theme.background} cx='33' cy='33' r='2' />
</G>
2020-12-06 12:52:29 +01:00
</G>
</G>
2020-12-06 16:06:38 +01:00
</Svg>
</Animated.View>
</PanGestureHandler>
</View>
<Text style={[styles.imageFocusText, { color: theme.primary }]}>
</Text>
</>
2020-12-06 12:52:29 +01:00
)
}, [])
const altTextInput = useCallback(() => {
return (
2020-12-06 16:06:38 +01:00
<View style={styles.altTextContainer}>
2020-12-06 12:52:29 +01:00
<Text style={[styles.altTextInputHeading, { color: theme.primary }]}>
</Text>
<TextInput
style={[styles.altTextInput, { borderColor: theme.border }]}
autoCapitalize='none'
autoCorrect={false}
maxLength={1500}
multiline
onChangeText={e => setAltText(e)}
placeholder={
'你可以为附件添加文字说明,以便更多人可以查看他们(包括视力障碍或视力受损人士)。\n\n优质的描述应该简洁明了但要准确地描述照片中的内容以便用户理解其含义。'
}
placeholderTextColor={theme.secondary}
scrollEnabled
2020-12-06 16:06:38 +01:00
value={altText}
2020-12-06 12:52:29 +01:00
/>
<Text style={[styles.altTextLength, { color: theme.secondary }]}>
2020-12-06 16:06:38 +01:00
{altText?.length || 0} / 1500
2020-12-06 12:52:29 +01:00
</Text>
2020-12-06 16:06:38 +01:00
</View>
2020-12-06 12:52:29 +01:00
)
2020-12-06 16:06:38 +01:00
}, [altText])
2020-12-06 12:52:29 +01:00
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 1 }} edges={['right', 'bottom', 'left']}>
<Stack.Navigator>
<Stack.Screen
name='Screen-Shared-Compose-EditAttachment-Root'
options={{
title: '编辑附件',
headerLeft: () => (
<HeaderLeft text='取消' onPress={() => navigation.goBack()} />
),
2020-12-06 16:06:38 +01:00
headerRight: () => (
<HeaderRight
text='应用'
onPress={() => {
const formData = new FormData()
if (altText) {
formData.append('description', altText)
}
if (focus.current.x !== 0 || focus.current.y !== 0) {
formData.append(
'focus',
`${focus.current.x},${focus.current.y}`
)
}
client({
method: 'put',
instance: 'local',
url: `media/${attachment.id}`,
...(formData && { body: formData })
})
.then(
res => {
if (res.body.id === attachment.id) {
Alert.alert('修改成功', '', [
{
text: '好的',
onPress: () => {
navigation.goBack()
}
}
])
} else {
Alert.alert('修改失败', '', [
{
text: '返回重试'
}
])
}
},
error => {
Alert.alert('修改失败', error.body, [
{
text: '返回重试'
}
])
}
)
.catch(() => {
Alert.alert('修改失败', '', [
{
text: '返回重试'
}
])
})
}}
/>
)
2020-12-06 12:52:29 +01:00
}}
>
{() => {
switch (attachment.type) {
case 'image':
return (
2020-12-06 16:06:38 +01:00
<ScrollView style={{ flex: 1 }}>
2020-12-06 12:52:29 +01:00
{imageFocus()}
2020-12-06 16:06:38 +01:00
{altTextInput()}
</ScrollView>
2020-12-06 12:52:29 +01:00
)
2020-12-06 21:42:19 +01:00
case 'video':
case 'gifv':
return (
<ScrollView style={{ flex: 1 }}>
{videoPlayback()}
{altTextInput()}
</ScrollView>
)
2020-12-06 12:52:29 +01:00
}
return null
}}
</Stack.Screen>
</Stack.Navigator>
</SafeAreaView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
imageFocusText: {
2020-12-06 16:06:38 +01:00
fontSize: StyleConstants.Font.Size.M,
padding: StyleConstants.Spacing.Global.PagePadding
2020-12-06 12:52:29 +01:00
},
2020-12-06 16:06:38 +01:00
altTextContainer: { padding: StyleConstants.Spacing.Global.PagePadding },
2020-12-06 12:52:29 +01:00
altTextInputHeading: {
fontSize: StyleConstants.Font.Size.M,
2020-12-06 16:06:38 +01:00
fontWeight: StyleConstants.Font.Weight.Bold
2020-12-06 12:52:29 +01:00
},
altTextInput: {
height: 200,
fontSize: StyleConstants.Font.Size.M,
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S,
padding: StyleConstants.Spacing.Global.PagePadding,
paddingTop: StyleConstants.Spacing.S * 1.5,
borderWidth: StyleSheet.hairlineWidth
},
altTextLength: {
textAlign: 'right',
marginRight: StyleConstants.Spacing.S,
fontSize: StyleConstants.Font.Size.S,
marginBottom: StyleConstants.Spacing.M
}
})
export default ComposeEditAttachment