ALF lightbox

This commit is contained in:
Samuel Newman
2024-11-12 22:37:51 +00:00
parent 6471e809aa
commit f89fb4c6cf
4 changed files with 140 additions and 149 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 361 B

+41 -1
View File
@@ -27,6 +27,18 @@ export const atoms = {
right: 0, right: 0,
bottom: 0, bottom: 0,
}, },
top_0: {
top: 0,
},
left_0: {
left: 0,
},
right_0: {
right: 0,
},
bottom_0: {
bottom: 0,
},
z_10: { z_10: {
zIndex: 10, zIndex: 10,
}, },
@@ -48,14 +60,20 @@ export const atoms = {
}, },
/* /*
* Width * Height / Width
*/ */
w_full: { w_full: {
width: '100%', width: '100%',
}, },
max_w_full: {
maxWidth: '100%',
},
h_full: { h_full: {
height: '100%', height: '100%',
}, },
max_h_full: {
maxHeight: '100%',
},
h_full_vh: web({ h_full_vh: web({
height: '100vh', height: '100vh',
}), }),
@@ -878,6 +896,9 @@ export const atoms = {
pointer_events_auto: { pointer_events_auto: {
pointerEvents: 'auto', pointerEvents: 'auto',
}, },
pointer_events_box_none: {
pointerEvents: 'box-none',
},
user_select_none: { user_select_none: {
userSelect: 'none', userSelect: 'none',
}, },
@@ -935,4 +956,23 @@ export const atoms = {
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
transitionDuration: '100ms', transitionDuration: '100ms',
}), }),
/*
* Opacity
*/
opacity_0: {
opacity: 0,
},
opacity_25: {
opacity: 0.25,
},
opacity_50: {
opacity: 0.5,
},
opacity_75: {
opacity: 0.75,
},
opacity_100: {
opacity: 1,
},
} as const } as const
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const FloppyDisk_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z',
})
+93 -148
View File
@@ -9,13 +9,7 @@
// https://github.com/jobtoday/react-native-image-viewing // https://github.com/jobtoday/react-native-image-viewing
import React, {useCallback, useState} from 'react' import React, {useCallback, useState} from 'react'
import { import {LayoutAnimation, PixelRatio, Platform, View} from 'react-native'
LayoutAnimation,
PixelRatio,
Platform,
StyleSheet,
View,
} from 'react-native'
import {Gesture} from 'react-native-gesture-handler' import {Gesture} from 'react-native-gesture-handler'
import PagerView from 'react-native-pager-view' import PagerView from 'react-native-pager-view'
import Animated, { import Animated, {
@@ -39,16 +33,18 @@ import {
useSafeAreaFrame, useSafeAreaFrame,
useSafeAreaInsets, useSafeAreaInsets,
} from 'react-native-safe-area-context' } from 'react-native-safe-area-context'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro'
import {Trans} from '@lingui/macro' import {useLingui} from '@lingui/react'
import {Dimensions} from '#/lib/media/types' import {Dimensions} from '#/lib/media/types'
import {colors, s} from '#/lib/styles'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
import {Lightbox} from '#/state/lightbox' import {Lightbox} from '#/state/lightbox'
import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, ThemeProvider, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox'
import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk'
import {Text} from '#/components/Typography'
import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army'
import {ImageSource, Transform} from './@types' import {ImageSource, Transform} from './@types'
import ImageDefaultHeader from './components/ImageDefaultHeader' import ImageDefaultHeader from './components/ImageDefaultHeader'
@@ -124,28 +120,34 @@ export default function ImageViewRoot({
}, [onRequestClose, openProgress]) }, [onRequestClose, openProgress])
return ( return (
// Keep it always mounted to avoid flicker on the first frame. <ThemeProvider theme="dark">
<SafeAreaView {/* Keep it always mounted to avoid flicker on the first frame. */}
style={[styles.screen, !activeLightbox && styles.screenHidden]} <SafeAreaView
edges={EDGES} style={[
aria-modal a.absolute,
accessibilityViewIsModal a.inset_0,
aria-hidden={!activeLightbox}> !activeLightbox && [a.opacity_0, a.pointer_events_none],
<Animated.View ref={ref} style={{flex: 1}} collapsable={false}> ]}
{activeLightbox && ( edges={EDGES}
<ImageView aria-modal
key={activeLightbox.id} accessibilityViewIsModal
lightbox={activeLightbox} aria-hidden={!activeLightbox}>
onRequestClose={onRequestClose} <Animated.View ref={ref} style={[a.flex_1]} collapsable={false}>
onPressSave={onPressSave} {activeLightbox && (
onPressShare={onPressShare} <ImageView
onFlyAway={onFlyAway} key={activeLightbox.id}
safeAreaRef={ref} lightbox={activeLightbox}
openProgress={openProgress} onRequestClose={onRequestClose}
/> onPressSave={onPressSave}
)} onPressShare={onPressShare}
</Animated.View> onFlyAway={onFlyAway}
</SafeAreaView> safeAreaRef={ref}
openProgress={openProgress}
/>
)}
</Animated.View>
</SafeAreaView>
</ThemeProvider>
) )
} }
@@ -208,11 +210,7 @@ function ImageView({
show && openProgress.value === 1 ? 1 : 0, show && openProgress.value === 1 ? 1 : 0,
FAST_SPRING, FAST_SPRING,
), ),
transform: [ transform: [{translateY: withClampedSpring(show ? 0 : -30, FAST_SPRING)}],
{
translateY: withClampedSpring(show ? 0 : -30, FAST_SPRING),
},
],
} }
}) })
const animatedFooterStyle = useAnimatedStyle(() => { const animatedFooterStyle = useAnimatedStyle(() => {
@@ -224,11 +222,7 @@ function ImageView({
show && openProgress.value === 1 ? 1 : 0, show && openProgress.value === 1 ? 1 : 0,
FAST_SPRING, FAST_SPRING,
), ),
transform: [ transform: [{translateY: withClampedSpring(show ? 0 : 30, FAST_SPRING)}],
{
translateY: withClampedSpring(show ? 0 : 30, FAST_SPRING),
},
],
} }
}) })
@@ -261,9 +255,14 @@ function ImageView({
) )
return ( return (
<Animated.View style={[styles.container, containerStyle]}> <Animated.View style={[a.flex_1, containerStyle]}>
<Animated.View <Animated.View
style={[styles.backdrop, backdropStyle]} style={[
a.absolute,
a.inset_0,
{backgroundColor: '#000'},
backdropStyle,
]}
renderToHardwareTextureAndroid renderToHardwareTextureAndroid
/> />
<PagerView <PagerView
@@ -277,7 +276,7 @@ function ImageView({
setIsDragging(e.nativeEvent.pageScrollState !== 'idle') setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
}} }}
overdrag={true} overdrag={true}
style={styles.pager}> style={[a.flex_1]}>
{images.map((imageSrc, i) => ( {images.map((imageSrc, i) => (
<View key={imageSrc.uri}> <View key={imageSrc.uri}>
<LightboxImage <LightboxImage
@@ -297,7 +296,14 @@ function ImageView({
</View> </View>
))} ))}
</PagerView> </PagerView>
<View style={styles.controls}> <View
style={[
a.absolute,
a.inset_0,
a.gap_xl,
a.z_10,
a.pointer_events_box_none,
]}>
<Animated.View <Animated.View
style={animatedHeaderStyle} style={animatedHeaderStyle}
renderToHardwareTextureAndroid> renderToHardwareTextureAndroid>
@@ -486,11 +492,20 @@ function LightboxFooter({
onPressSave: (uri: string) => void onPressSave: (uri: string) => void
onPressShare: (uri: string) => void onPressShare: (uri: string) => void
}) { }) {
const {_} = useLingui()
const t = useTheme()
const {alt: altText, uri} = images[index] const {alt: altText, uri} = images[index]
const isMomentumScrolling = React.useRef(false) const isMomentumScrolling = React.useRef(false)
return ( return (
<ScrollView <ScrollView
style={styles.footerScrollView} style={[
a.flex_1,
a.max_h_full,
a.w_full,
a.absolute,
a.bottom_0,
{backgroundColor: '#000d'},
]}
scrollEnabled={isAltExpanded} scrollEnabled={isAltExpanded}
onMomentumScrollBegin={() => { onMomentumScrollBegin={() => {
isMomentumScrolling.current = true isMomentumScrolling.current = true
@@ -498,15 +513,12 @@ function LightboxFooter({
onMomentumScrollEnd={() => { onMomentumScrollEnd={() => {
isMomentumScrolling.current = false isMomentumScrolling.current = false
}} }}
contentContainerStyle={{ contentContainerStyle={[a.py_md, a.px_2xl]}>
paddingVertical: 12,
paddingHorizontal: 24,
}}>
<SafeAreaView edges={['bottom']}> <SafeAreaView edges={['bottom']}>
{altText ? ( {altText ? (
<View accessibilityRole="button" style={styles.footerText}> <View accessibilityRole="button" style={[isIOS ? a.pb_xl : a.pb_lg]}>
<Text <Text
style={[s.gray3]} style={[t.atoms.text_contrast_high]}
numberOfLines={isAltExpanded ? undefined : 3} numberOfLines={isAltExpanded ? undefined : 3}
selectable selectable
onPress={() => { onPress={() => {
@@ -524,104 +536,37 @@ function LightboxFooter({
</Text> </Text>
</View> </View>
) : null} ) : null}
<View style={styles.footerBtns}> <ThemeProvider theme="light">
<Button <View style={[a.w_full, a.flex_row, a.justify_center, a.gap_sm]}>
type="primary-outline" <Button
style={styles.footerBtn} label={_(msg`Save`)}
onPress={() => onPressSave(uri)}> color="secondary_inverted"
<FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> variant="solid"
<Text type="xl" style={s.white}> size="small"
<Trans context="action">Save</Trans> onPress={() => onPressSave(uri)}>
</Text> <ButtonIcon icon={SaveIcon} />
</Button> <ButtonText>
<Button <Trans context="action">Save</Trans>
type="primary-outline" </ButtonText>
style={styles.footerBtn} </Button>
onPress={() => onPressShare(uri)}> <Button
<FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> label={_(msg`Share`)}
<Text type="xl" style={s.white}> color="secondary_inverted"
<Trans context="action">Share</Trans> variant="solid"
</Text> size="small"
</Button> onPress={() => onPressShare(uri)}>
</View> <ButtonIcon icon={ShareIcon} />
<ButtonText>
<Trans context="action">Share</Trans>
</ButtonText>
</Button>
</View>
</ThemeProvider>
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
) )
} }
const styles = StyleSheet.create({
screen: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
screenHidden: {
opacity: 0,
pointerEvents: 'none',
},
container: {
flex: 1,
},
backdrop: {
backgroundColor: '#000',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
controls: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
gap: 20,
zIndex: 1,
pointerEvents: 'box-none',
},
pager: {
flex: 1,
},
header: {
position: 'absolute',
width: '100%',
top: 0,
pointerEvents: 'box-none',
},
footer: {
position: 'absolute',
width: '100%',
maxHeight: '100%',
bottom: 0,
},
footerScrollView: {
backgroundColor: '#000d',
flex: 1,
position: 'absolute',
bottom: 0,
width: '100%',
maxHeight: '100%',
},
footerText: {
paddingBottom: isIOS ? 20 : 16,
},
footerBtns: {
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
},
footerBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
backgroundColor: 'transparent',
borderColor: colors.white,
},
})
function interpolatePx( function interpolatePx(
px: number, px: number,
inputRange: readonly number[], inputRange: readonly number[],