Add swipe gestures to the lightbox
This commit is contained in:
parent
3a44a1cfdc
commit
3aded6887d
8 changed files with 387 additions and 40 deletions
|
@ -51,18 +51,56 @@ export class ServerInputModal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProfileImageLightbox {
|
interface LightboxModel {
|
||||||
|
canSwipeLeft: boolean
|
||||||
|
canSwipeRight: boolean
|
||||||
|
onSwipeLeft: () => void
|
||||||
|
onSwipeRight: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProfileImageLightbox implements LightboxModel {
|
||||||
name = 'profile-image'
|
name = 'profile-image'
|
||||||
|
canSwipeLeft = false
|
||||||
|
canSwipeRight = false
|
||||||
constructor(public profileView: ProfileViewModel) {
|
constructor(public profileView: ProfileViewModel) {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
}
|
}
|
||||||
|
onSwipeLeft() {}
|
||||||
|
onSwipeRight() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageLightbox {
|
export class ImageLightbox implements LightboxModel {
|
||||||
name = 'image'
|
name = 'image'
|
||||||
|
canSwipeLeft = true
|
||||||
|
canSwipeRight = true
|
||||||
constructor(public uri: string) {
|
constructor(public uri: string) {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
}
|
}
|
||||||
|
onSwipeLeft() {}
|
||||||
|
onSwipeRight() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImagesLightbox implements LightboxModel {
|
||||||
|
name = 'images'
|
||||||
|
get canSwipeLeft() {
|
||||||
|
return this.index > 0
|
||||||
|
}
|
||||||
|
get canSwipeRight() {
|
||||||
|
return this.index < this.uris.length - 1
|
||||||
|
}
|
||||||
|
constructor(public uris: string[], public index: number) {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
onSwipeLeft() {
|
||||||
|
if (this.canSwipeLeft) {
|
||||||
|
this.index = this.index - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSwipeRight() {
|
||||||
|
if (this.canSwipeRight) {
|
||||||
|
this.index = this.index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComposerOptsPostRef {
|
export interface ComposerOptsPostRef {
|
||||||
|
@ -91,7 +129,11 @@ export class ShellUiModel {
|
||||||
| ServerInputModal
|
| ServerInputModal
|
||||||
| undefined
|
| undefined
|
||||||
isLightboxActive = false
|
isLightboxActive = false
|
||||||
activeLightbox: ProfileImageLightbox | ImageLightbox | undefined
|
activeLightbox:
|
||||||
|
| ProfileImageLightbox
|
||||||
|
| ImageLightbox
|
||||||
|
| ImagesLightbox
|
||||||
|
| undefined
|
||||||
isComposerActive = false
|
isComposerActive = false
|
||||||
composerOpts: ComposerOpts | undefined
|
composerOpts: ComposerOpts | undefined
|
||||||
|
|
||||||
|
@ -123,7 +165,9 @@ export class ShellUiModel {
|
||||||
this.activeModal = undefined
|
this.activeModal = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
openLightbox(lightbox: ProfileImageLightbox | ImageLightbox) {
|
openLightbox(
|
||||||
|
lightbox: ProfileImageLightbox | ImageLightbox | ImagesLightbox,
|
||||||
|
) {
|
||||||
this.isLightboxActive = true
|
this.isLightboxActive = true
|
||||||
this.activeLightbox = lightbox
|
this.activeLightbox = lightbox
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {Image, StyleSheet, useWindowDimensions, View} from 'react-native'
|
||||||
export function Component({uri}: {uri: string}) {
|
export function Component({uri}: {uri: string}) {
|
||||||
const winDim = useWindowDimensions()
|
const winDim = useWindowDimensions()
|
||||||
const top = winDim.height / 2 - (winDim.width - 40) / 2 - 100
|
const top = winDim.height / 2 - (winDim.width - 40) / 2 - 100
|
||||||
console.log(uri)
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, {top}]}>
|
<View style={[styles.container, {top}]}>
|
||||||
<Image style={styles.image} source={{uri}} />
|
<Image style={styles.image} source={{uri}} />
|
||||||
|
|
35
src/view/com/lightbox/Images.tsx
Normal file
35
src/view/com/lightbox/Images.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {Image, StyleSheet, useWindowDimensions, View} from 'react-native'
|
||||||
|
|
||||||
|
export function Component({uris, index}: {uris: string[]; index: number}) {
|
||||||
|
const winDim = useWindowDimensions()
|
||||||
|
const left = index * winDim.width * -1
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, {left}]}>
|
||||||
|
{uris.map((uri, i) => (
|
||||||
|
<Image
|
||||||
|
key={i}
|
||||||
|
style={[styles.image, {left: i * winDim.width}]}
|
||||||
|
source={{uri}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 200,
|
||||||
|
left: 0,
|
||||||
|
resizeMode: 'contain',
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: 1,
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,20 +1,41 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {
|
||||||
|
Animated,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
useWindowDimensions,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {Swipe, Dir} from '../util/gestures/Swipe'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
||||||
|
|
||||||
import * as models from '../../../state/models/shell-ui'
|
import * as models from '../../../state/models/shell-ui'
|
||||||
|
|
||||||
import * as ProfileImageLightbox from './ProfileImage'
|
import * as ProfileImageLightbox from './ProfileImage'
|
||||||
import * as ImageLightbox from './Image'
|
import * as ImageLightbox from './Image'
|
||||||
|
import * as ImagesLightbox from './Images'
|
||||||
|
|
||||||
export const Lightbox = observer(function Lightbox() {
|
export const Lightbox = observer(function Lightbox() {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const winDim = useWindowDimensions()
|
||||||
|
const panX = useAnimatedValue(0)
|
||||||
|
const panY = useAnimatedValue(0)
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
store.shell.closeLightbox()
|
store.shell.closeLightbox()
|
||||||
}
|
}
|
||||||
|
const onSwipeEnd = (dir: Dir) => {
|
||||||
|
if (dir === Dir.Up || dir === Dir.Down) {
|
||||||
|
onClose()
|
||||||
|
} else if (dir === Dir.Left) {
|
||||||
|
store.shell.activeLightbox?.onSwipeLeft()
|
||||||
|
} else if (dir === Dir.Right) {
|
||||||
|
store.shell.activeLightbox?.onSwipeRight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!store.shell.isLightboxActive) {
|
if (!store.shell.isLightboxActive) {
|
||||||
return <View />
|
return <View />
|
||||||
|
@ -33,18 +54,49 @@ export const Lightbox = observer(function Lightbox() {
|
||||||
{...(store.shell.activeLightbox as models.ImageLightbox)}
|
{...(store.shell.activeLightbox as models.ImageLightbox)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
} else if (store.shell.activeLightbox?.name === 'images') {
|
||||||
|
element = (
|
||||||
|
<ImagesLightbox.Component
|
||||||
|
{...(store.shell.activeLightbox as models.ImagesLightbox)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const translateX = Animated.multiply(panX, winDim.width * -1)
|
||||||
|
const translateY = Animated.multiply(panY, winDim.height * -1)
|
||||||
|
const swipeTransform = {transform: [{translateX}, {translateY}]}
|
||||||
|
const swipeOpacity = {
|
||||||
|
opacity: panY.interpolate({
|
||||||
|
inputRange: [-1, 0, 1],
|
||||||
|
outputRange: [0, 1, 0],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View style={StyleSheet.absoluteFill}>
|
||||||
<TouchableOpacity style={styles.bg} onPress={onClose} />
|
<Swipe
|
||||||
<TouchableOpacity style={styles.xIcon} onPress={onClose}>
|
panX={panX}
|
||||||
<FontAwesomeIcon icon="x" size={24} style={{color: '#fff'}} />
|
panY={panY}
|
||||||
</TouchableOpacity>
|
swipeEnabled
|
||||||
{element}
|
canSwipeLeft={store.shell.activeLightbox.canSwipeLeft}
|
||||||
</>
|
canSwipeRight={store.shell.activeLightbox.canSwipeRight}
|
||||||
|
canSwipeUp
|
||||||
|
canSwipeDown
|
||||||
|
hasPriority
|
||||||
|
onSwipeEnd={onSwipeEnd}>
|
||||||
|
<TouchableWithoutFeedback onPress={onClose}>
|
||||||
|
<Animated.View style={[styles.bg, swipeOpacity]} />
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
<TouchableWithoutFeedback onPress={onClose}>
|
||||||
|
<View style={styles.xIcon}>
|
||||||
|
<FontAwesomeIcon icon="x" size={24} style={{color: '#fff'}} />
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
<Animated.View style={swipeTransform}>{element}</Animated.View>
|
||||||
|
</Swipe>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
import React, {useEffect, useState} from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
Image,
|
|
||||||
ImageStyle,
|
ImageStyle,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
Text,
|
Text,
|
||||||
TouchableWithoutFeedback,
|
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {
|
|
||||||
Record as PostRecord,
|
|
||||||
Entity,
|
|
||||||
} from '../../../third-party/api/src/client/types/app/bsky/feed/post'
|
|
||||||
import * as AppBskyEmbedImages from '../../../third-party/api/src/client/types/app/bsky/embed/images'
|
import * as AppBskyEmbedImages from '../../../third-party/api/src/client/types/app/bsky/embed/images'
|
||||||
import * as AppBskyEmbedExternal from '../../../third-party/api/src/client/types/app/bsky/embed/external'
|
import * as AppBskyEmbedExternal from '../../../third-party/api/src/client/types/app/bsky/embed/external'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {LinkMeta, getLikelyType, LikelyType} from '../../../lib/link-meta'
|
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
import {AutoSizedImage} from './images/AutoSizedImage'
|
import {AutoSizedImage} from './images/AutoSizedImage'
|
||||||
|
import {ImagesLightbox} from '../../../state/models/shell-ui'
|
||||||
|
import {useStores} from '../../../state'
|
||||||
|
|
||||||
type Embed =
|
type Embed =
|
||||||
| AppBskyEmbedImages.Presented
|
| AppBskyEmbedImages.Presented
|
||||||
|
@ -33,14 +27,19 @@ export function PostEmbeds({
|
||||||
embed?: Embed
|
embed?: Embed
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
|
const store = useStores()
|
||||||
if (embed?.$type === 'app.bsky.embed.images#presented') {
|
if (embed?.$type === 'app.bsky.embed.images#presented') {
|
||||||
const imgEmbed = embed as AppBskyEmbedImages.Presented
|
const imgEmbed = embed as AppBskyEmbedImages.Presented
|
||||||
if (imgEmbed.images.length > 0) {
|
if (imgEmbed.images.length > 0) {
|
||||||
|
const uris = imgEmbed.images.map(img => img.fullsize)
|
||||||
|
const openLightbox = (index: number) => {
|
||||||
|
store.shell.openLightbox(new ImagesLightbox(uris, index))
|
||||||
|
}
|
||||||
const Thumb = ({i, style}: {i: number; style: StyleProp<ImageStyle>}) => (
|
const Thumb = ({i, style}: {i: number; style: StyleProp<ImageStyle>}) => (
|
||||||
<AutoSizedImage
|
<AutoSizedImage
|
||||||
style={style}
|
style={style}
|
||||||
uri={imgEmbed.images[i].thumb}
|
uri={imgEmbed.images[i].thumb}
|
||||||
fullSizeUri={imgEmbed.images[i].fullsize}
|
onPress={() => openLightbox(i)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
if (imgEmbed.images.length === 4) {
|
if (imgEmbed.images.length === 4) {
|
||||||
|
|
|
@ -72,11 +72,6 @@ export function HorzSwipe({
|
||||||
setDir(0)
|
setDir(0)
|
||||||
onSwipeStart?.()
|
onSwipeStart?.()
|
||||||
|
|
||||||
// TODO
|
|
||||||
// if (keyboardDismissMode === 'on-drag') {
|
|
||||||
// Keyboard.dismiss()
|
|
||||||
// }
|
|
||||||
|
|
||||||
panX.stopAnimation()
|
panX.stopAnimation()
|
||||||
// @ts-expect-error: _value is private, but docs use it as well
|
// @ts-expect-error: _value is private, but docs use it as well
|
||||||
panX.setOffset(panX._value)
|
panX.setOffset(panX._value)
|
||||||
|
|
232
src/view/com/util/gestures/Swipe.tsx
Normal file
232
src/view/com/util/gestures/Swipe.tsx
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
import React, {useState} from 'react'
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
GestureResponderEvent,
|
||||||
|
I18nManager,
|
||||||
|
PanResponder,
|
||||||
|
PanResponderGestureState,
|
||||||
|
useWindowDimensions,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {clamp} from 'lodash'
|
||||||
|
|
||||||
|
export enum Dir {
|
||||||
|
None,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
panX: Animated.Value
|
||||||
|
panY: Animated.Value
|
||||||
|
canSwipeLeft?: boolean
|
||||||
|
canSwipeRight?: boolean
|
||||||
|
canSwipeUp?: boolean
|
||||||
|
canSwipeDown?: boolean
|
||||||
|
swipeEnabled?: boolean
|
||||||
|
hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
|
||||||
|
horzDistThresholdDivisor?: number
|
||||||
|
vertDistThresholdDivisor?: number
|
||||||
|
useNativeDriver?: boolean
|
||||||
|
onSwipeStart?: () => void
|
||||||
|
onSwipeStartDirection?: (dir: Dir) => void
|
||||||
|
onSwipeEnd?: (dir: Dir) => void
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Swipe({
|
||||||
|
panX,
|
||||||
|
panY,
|
||||||
|
canSwipeLeft = false,
|
||||||
|
canSwipeRight = false,
|
||||||
|
canSwipeUp = false,
|
||||||
|
canSwipeDown = false,
|
||||||
|
swipeEnabled = true,
|
||||||
|
hasPriority = false,
|
||||||
|
horzDistThresholdDivisor = 1.75,
|
||||||
|
vertDistThresholdDivisor = 1.75,
|
||||||
|
useNativeDriver = false,
|
||||||
|
onSwipeStart,
|
||||||
|
onSwipeStartDirection,
|
||||||
|
onSwipeEnd,
|
||||||
|
children,
|
||||||
|
}: Props) {
|
||||||
|
const winDim = useWindowDimensions()
|
||||||
|
const [dir, setDir] = useState<Dir>(Dir.None)
|
||||||
|
|
||||||
|
const swipeVelocityThreshold = 35
|
||||||
|
const swipeHorzDistanceThreshold = winDim.width / horzDistThresholdDivisor
|
||||||
|
const swipeVertDistanceThreshold = winDim.height / vertDistThresholdDivisor
|
||||||
|
|
||||||
|
const isMovingHorizontally = (
|
||||||
|
_: GestureResponderEvent,
|
||||||
|
gestureState: PanResponderGestureState,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) &&
|
||||||
|
Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const isMovingVertically = (
|
||||||
|
_: GestureResponderEvent,
|
||||||
|
gestureState: PanResponderGestureState,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
Math.abs(gestureState.dy) > Math.abs(gestureState.dx * 1.25) &&
|
||||||
|
Math.abs(gestureState.vy) > Math.abs(gestureState.vx * 1.25)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canDir = (d: Dir) => {
|
||||||
|
if (d === Dir.Left) return canSwipeLeft
|
||||||
|
if (d === Dir.Right) return canSwipeRight
|
||||||
|
if (d === Dir.Up) return canSwipeUp
|
||||||
|
if (d === Dir.Down) return canSwipeDown
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const isHorz = (d: Dir) => d === Dir.Left || d === Dir.Right
|
||||||
|
const isVert = (d: Dir) => d === Dir.Up || d === Dir.Down
|
||||||
|
|
||||||
|
const canMoveScreen = (
|
||||||
|
event: GestureResponderEvent,
|
||||||
|
gestureState: PanResponderGestureState,
|
||||||
|
) => {
|
||||||
|
if (swipeEnabled === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
||||||
|
const dy = gestureState.dy
|
||||||
|
const willHandle =
|
||||||
|
(isMovingHorizontally(event, gestureState) &&
|
||||||
|
((dx > 0 && canSwipeLeft) || (dx < 0 && canSwipeRight))) ||
|
||||||
|
(isMovingVertically(event, gestureState) &&
|
||||||
|
((dy > 0 && canSwipeUp) || (dy < 0 && canSwipeDown)))
|
||||||
|
return willHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
const startGesture = () => {
|
||||||
|
setDir(Dir.None)
|
||||||
|
onSwipeStart?.()
|
||||||
|
|
||||||
|
panX.stopAnimation()
|
||||||
|
// @ts-expect-error: _value is private, but docs use it as well
|
||||||
|
panX.setOffset(panX._value)
|
||||||
|
panY.stopAnimation()
|
||||||
|
// @ts-expect-error: _value is private, but docs use it as well
|
||||||
|
panY.setOffset(panY._value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const respondToGesture = (
|
||||||
|
_: GestureResponderEvent,
|
||||||
|
gestureState: PanResponderGestureState,
|
||||||
|
) => {
|
||||||
|
const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
||||||
|
const dy = gestureState.dy
|
||||||
|
|
||||||
|
let newDir = Dir.None
|
||||||
|
if (dir === Dir.None) {
|
||||||
|
// establish if the user is swiping horz or vert
|
||||||
|
if (Math.abs(dx) > Math.abs(dy)) {
|
||||||
|
newDir = dx > 0 ? Dir.Left : Dir.Right
|
||||||
|
} else {
|
||||||
|
newDir = dy > 0 ? Dir.Up : Dir.Down
|
||||||
|
}
|
||||||
|
} else if (isHorz(dir)) {
|
||||||
|
// direction update
|
||||||
|
newDir = dx > 0 ? Dir.Left : Dir.Right
|
||||||
|
} else if (isVert(dir)) {
|
||||||
|
// direction update
|
||||||
|
newDir = dy > 0 ? Dir.Up : Dir.Down
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHorz(newDir)) {
|
||||||
|
panX.setValue(
|
||||||
|
clamp(
|
||||||
|
dx / swipeHorzDistanceThreshold,
|
||||||
|
canSwipeRight ? -1 : 0,
|
||||||
|
canSwipeLeft ? 1 : 0,
|
||||||
|
) * -1,
|
||||||
|
)
|
||||||
|
panY.setValue(0)
|
||||||
|
} else if (isVert(newDir)) {
|
||||||
|
panY.setValue(
|
||||||
|
clamp(
|
||||||
|
dy / swipeVertDistanceThreshold,
|
||||||
|
canSwipeDown ? -1 : 0,
|
||||||
|
canSwipeUp ? 1 : 0,
|
||||||
|
) * -1,
|
||||||
|
)
|
||||||
|
panX.setValue(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canDir(newDir)) {
|
||||||
|
newDir = Dir.None
|
||||||
|
}
|
||||||
|
if (newDir !== dir) {
|
||||||
|
setDir(newDir)
|
||||||
|
onSwipeStartDirection?.(newDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finishGesture = (
|
||||||
|
_: GestureResponderEvent,
|
||||||
|
gestureState: PanResponderGestureState,
|
||||||
|
) => {
|
||||||
|
const finish = (finalDir: dir) => () => {
|
||||||
|
if (finalDir !== Dir.None) {
|
||||||
|
onSwipeEnd?.(finalDir)
|
||||||
|
}
|
||||||
|
setDir(Dir.None)
|
||||||
|
panX.flattenOffset()
|
||||||
|
panX.setValue(0)
|
||||||
|
panY.flattenOffset()
|
||||||
|
panY.setValue(0)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isHorz(dir) &&
|
||||||
|
(Math.abs(gestureState.dx) > swipeHorzDistanceThreshold / 4 ||
|
||||||
|
Math.abs(gestureState.vx) > swipeVelocityThreshold)
|
||||||
|
) {
|
||||||
|
Animated.timing(panX, {
|
||||||
|
toValue: dir === Dir.Left ? -1 : 1,
|
||||||
|
duration: 100,
|
||||||
|
useNativeDriver,
|
||||||
|
}).start(finish(dir))
|
||||||
|
} else if (
|
||||||
|
isVert(dir) &&
|
||||||
|
(Math.abs(gestureState.dy) > swipeVertDistanceThreshold / 8 ||
|
||||||
|
Math.abs(gestureState.vy) > swipeVelocityThreshold)
|
||||||
|
) {
|
||||||
|
Animated.timing(panY, {
|
||||||
|
toValue: dir === Dir.Up ? -1 : 1,
|
||||||
|
duration: 100,
|
||||||
|
useNativeDriver,
|
||||||
|
}).start(finish(dir))
|
||||||
|
} else {
|
||||||
|
onSwipeEnd?.(Dir.None)
|
||||||
|
Animated.timing(panX, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 100,
|
||||||
|
useNativeDriver,
|
||||||
|
}).start(finish(Dir.None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const panResponder = PanResponder.create({
|
||||||
|
onMoveShouldSetPanResponder: canMoveScreen,
|
||||||
|
onPanResponderGrant: startGesture,
|
||||||
|
onPanResponderMove: respondToGesture,
|
||||||
|
onPanResponderTerminate: finishGesture,
|
||||||
|
onPanResponderRelease: finishGesture,
|
||||||
|
onPanResponderTerminationRequest: () => !hasPriority,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View {...panResponder.panHandlers} style={{flex: 1}}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -9,8 +9,6 @@ import {
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {ImageLightbox} from '../../../../state/models/shell-ui'
|
|
||||||
import {useStores} from '../../../../state'
|
|
||||||
import {colors} from '../../../lib/styles'
|
import {colors} from '../../../lib/styles'
|
||||||
|
|
||||||
const MAX_HEIGHT = 300
|
const MAX_HEIGHT = 300
|
||||||
|
@ -22,14 +20,13 @@ interface Dim {
|
||||||
|
|
||||||
export function AutoSizedImage({
|
export function AutoSizedImage({
|
||||||
uri,
|
uri,
|
||||||
fullSizeUri,
|
onPress,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
uri: string
|
uri: string
|
||||||
fullSizeUri?: string
|
onPress?: () => void
|
||||||
style: StyleProp<ImageStyle>
|
style: StyleProp<ImageStyle>
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
|
||||||
const [error, setError] = useState<string | undefined>()
|
const [error, setError] = useState<string | undefined>()
|
||||||
const [imgInfo, setImgInfo] = useState<Dim | undefined>()
|
const [imgInfo, setImgInfo] = useState<Dim | undefined>()
|
||||||
const [containerInfo, setContainerInfo] = useState<Dim | undefined>()
|
const [containerInfo, setContainerInfo] = useState<Dim | undefined>()
|
||||||
|
@ -74,15 +71,9 @@ export function AutoSizedImage({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPressImage = () => {
|
|
||||||
if (fullSizeUri) {
|
|
||||||
store.shell.openLightbox(new ImageLightbox(fullSizeUri))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={style}>
|
<View style={style}>
|
||||||
<TouchableWithoutFeedback onPress={onPressImage}>
|
<TouchableWithoutFeedback onPress={onPress}>
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={[styles.container, styles.errorContainer]}>
|
<View style={[styles.container, styles.errorContainer]}>
|
||||||
<Text style={styles.error}>{error}</Text>
|
<Text style={styles.error}>{error}</Text>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue