From 94af89356f2e4e08f462370c77e4778d7626ce2f Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Wed, 2 Feb 2022 14:22:07 +0100 Subject: [PATCH] compile --- dist/@types/index.d.ts | 17 ++ dist/@types/index.js | 7 + dist/ImageViewing.d.ts | 35 +++ dist/ImageViewing.js | 86 ++++++ dist/components/ImageDefaultHeader.d.ts | 13 + dist/components/ImageDefaultHeader.js | 39 +++ .../ImageItem/ImageItem.android.d.ts | 20 ++ .../components/ImageItem/ImageItem.android.js | 85 ++++++ dist/components/ImageItem/ImageItem.ios.d.ts | 20 ++ dist/components/ImageItem/ImageItem.ios.js | 79 +++++ dist/components/ImageItem/ImageLoading.d.ts | 9 + dist/components/ImageItem/ImageLoading.js | 30 ++ dist/components/StatusBarManager.d.ts | 4 + dist/components/StatusBarManager.js | 14 + dist/hooks/useAnimatedComponents.d.ts | 14 + dist/hooks/useAnimatedComponents.js | 44 +++ dist/hooks/useDoubleTapToZoom.d.ts | 16 + dist/hooks/useDoubleTapToZoom.js | 49 ++++ dist/hooks/useImageDimensions.d.ts | 10 + dist/hooks/useImageDimensions.js | 64 ++++ dist/hooks/useImageIndexChange.d.ts | 11 + dist/hooks/useImageIndexChange.js | 20 ++ dist/hooks/useImagePrefetch.d.ts | 10 + dist/hooks/useImagePrefetch.js | 21 ++ dist/hooks/usePanResponder.d.ts | 19 ++ dist/hooks/usePanResponder.js | 273 ++++++++++++++++++ dist/hooks/useRequestClose.d.ts | 9 + dist/hooks/useRequestClose.js | 20 ++ dist/index.d.ts | 8 + dist/index.js | 8 + dist/utils.d.ts | 48 +++ dist/utils.js | 101 +++++++ 32 files changed, 1203 insertions(+) create mode 100644 dist/@types/index.d.ts create mode 100644 dist/@types/index.js create mode 100644 dist/ImageViewing.d.ts create mode 100644 dist/ImageViewing.js create mode 100644 dist/components/ImageDefaultHeader.d.ts create mode 100644 dist/components/ImageDefaultHeader.js create mode 100644 dist/components/ImageItem/ImageItem.android.d.ts create mode 100644 dist/components/ImageItem/ImageItem.android.js create mode 100644 dist/components/ImageItem/ImageItem.ios.d.ts create mode 100644 dist/components/ImageItem/ImageItem.ios.js create mode 100644 dist/components/ImageItem/ImageLoading.d.ts create mode 100644 dist/components/ImageItem/ImageLoading.js create mode 100644 dist/components/StatusBarManager.d.ts create mode 100644 dist/components/StatusBarManager.js create mode 100644 dist/hooks/useAnimatedComponents.d.ts create mode 100644 dist/hooks/useAnimatedComponents.js create mode 100644 dist/hooks/useDoubleTapToZoom.d.ts create mode 100644 dist/hooks/useDoubleTapToZoom.js create mode 100644 dist/hooks/useImageDimensions.d.ts create mode 100644 dist/hooks/useImageDimensions.js create mode 100644 dist/hooks/useImageIndexChange.d.ts create mode 100644 dist/hooks/useImageIndexChange.js create mode 100644 dist/hooks/useImagePrefetch.d.ts create mode 100644 dist/hooks/useImagePrefetch.js create mode 100644 dist/hooks/usePanResponder.d.ts create mode 100644 dist/hooks/usePanResponder.js create mode 100644 dist/hooks/useRequestClose.d.ts create mode 100644 dist/hooks/useRequestClose.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/utils.d.ts create mode 100644 dist/utils.js diff --git a/dist/@types/index.d.ts b/dist/@types/index.d.ts new file mode 100644 index 0000000..a8628f6 --- /dev/null +++ b/dist/@types/index.d.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { ImageURISource, ImageRequireSource } from "react-native"; +export declare type Dimensions = { + width: number; + height: number; +}; +export declare type Position = { + x: number; + y: number; +}; +export declare type ImageSource = ImageURISource | ImageRequireSource; diff --git a/dist/@types/index.js b/dist/@types/index.js new file mode 100644 index 0000000..50a575c --- /dev/null +++ b/dist/@types/index.js @@ -0,0 +1,7 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ diff --git a/dist/ImageViewing.d.ts b/dist/ImageViewing.d.ts new file mode 100644 index 0000000..a94d73e --- /dev/null +++ b/dist/ImageViewing.d.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { ComponentType } from "react"; +import { ModalProps } from "react-native"; +import { ImageSource } from "./@types"; +declare type Props = { + images: ImageSource[]; + keyExtractor?: (imageSrc: ImageSource, index: number) => string; + imageIndex: number; + visible: boolean; + onRequestClose: () => void; + onLongPress?: (image: ImageSource) => void; + onImageIndexChange?: (imageIndex: number) => void; + presentationStyle?: ModalProps["presentationStyle"]; + animationType?: ModalProps["animationType"]; + backgroundColor?: string; + swipeToCloseEnabled?: boolean; + doubleTapToZoomEnabled?: boolean; + hideHeaderOnZoom?: boolean; + hideFooterOnZoom?: boolean; + delayLongPress?: number; + HeaderComponent?: ComponentType<{ + imageIndex: number; + }>; + FooterComponent?: ComponentType<{ + imageIndex: number; + }>; +}; +declare const EnhancedImageViewing: (props: Props) => JSX.Element; +export default EnhancedImageViewing; diff --git a/dist/ImageViewing.js b/dist/ImageViewing.js new file mode 100644 index 0000000..6dd03be --- /dev/null +++ b/dist/ImageViewing.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import React, { useCallback, useEffect } from "react"; +import { Animated, Dimensions, StyleSheet, View, VirtualizedList, Modal, } from "react-native"; +import ImageItem from "./components/ImageItem/ImageItem"; +import ImageDefaultHeader from "./components/ImageDefaultHeader"; +import StatusBarManager from "./components/StatusBarManager"; +import useAnimatedComponents from "./hooks/useAnimatedComponents"; +import useImageIndexChange from "./hooks/useImageIndexChange"; +import useRequestClose from "./hooks/useRequestClose"; +const DEFAULT_ANIMATION_TYPE = "fade"; +const DEFAULT_BG_COLOR = "#000"; +const DEFAULT_DELAY_LONG_PRESS = 800; +const SCREEN = Dimensions.get("screen"); +const SCREEN_WIDTH = SCREEN.width; +const DEFAULT_HIDE_HEADER_ON_ZOOM = true; +const DEFAULT_HIDE_FOOTER_ON_ZOOM = true; +function ImageViewing({ images, keyExtractor, imageIndex, visible, onRequestClose, onLongPress = () => { }, onImageIndexChange, animationType = DEFAULT_ANIMATION_TYPE, backgroundColor = DEFAULT_BG_COLOR, presentationStyle, swipeToCloseEnabled, doubleTapToZoomEnabled, delayLongPress = DEFAULT_DELAY_LONG_PRESS, HeaderComponent, FooterComponent, hideHeaderOnZoom = DEFAULT_HIDE_HEADER_ON_ZOOM, hideFooterOnZoom = DEFAULT_HIDE_FOOTER_ON_ZOOM, }) { + const imageList = React.createRef(); + const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose); + const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN); + const [headerTransform, footerTransform, toggleBarsVisible,] = useAnimatedComponents(); + useEffect(() => { + if (onImageIndexChange) { + onImageIndexChange(currentImageIndex); + } + }, [currentImageIndex]); + const onZoom = useCallback((isScaled) => { + var _a, _b; + // @ts-ignore + (_b = (_a = imageList) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.setNativeProps({ scrollEnabled: !isScaled }); + toggleBarsVisible(!isScaled, hideHeaderOnZoom, hideFooterOnZoom); + }, [imageList, hideHeaderOnZoom, hideFooterOnZoom]); + if (!visible) { + return null; + } + return ( + + + + {typeof HeaderComponent !== "undefined" + ? (React.createElement(HeaderComponent, { + imageIndex: currentImageIndex, + })) + : ()} + + images[index]} getItemCount={() => images.length} getItemLayout={(_, index) => ({ + length: SCREEN_WIDTH, + offset: SCREEN_WIDTH * index, + index, + })} renderItem={({ item: imageSrc }) => ()} onMomentumScrollEnd={onScroll} + //@ts-ignore + keyExtractor={(imageSrc, index) => keyExtractor ? keyExtractor(imageSrc, index) : imageSrc.uri || `${imageSrc}`}/> + {typeof FooterComponent !== "undefined" && ( + {React.createElement(FooterComponent, { + imageIndex: currentImageIndex, + })} + )} + + ); +} +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#000", + }, + header: { + position: "absolute", + width: "100%", + zIndex: 1, + top: 0, + }, + footer: { + position: "absolute", + width: "100%", + zIndex: 1, + bottom: 0, + }, +}); +const EnhancedImageViewing = (props) => (); +export default EnhancedImageViewing; diff --git a/dist/components/ImageDefaultHeader.d.ts b/dist/components/ImageDefaultHeader.d.ts new file mode 100644 index 0000000..5f1205d --- /dev/null +++ b/dist/components/ImageDefaultHeader.d.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +/// +declare type Props = { + onRequestClose: () => void; +}; +declare const ImageDefaultHeader: ({ onRequestClose }: Props) => JSX.Element; +export default ImageDefaultHeader; diff --git a/dist/components/ImageDefaultHeader.js b/dist/components/ImageDefaultHeader.js new file mode 100644 index 0000000..1278de0 --- /dev/null +++ b/dist/components/ImageDefaultHeader.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import React from "react"; +import { SafeAreaView, Text, TouchableOpacity, StyleSheet } from "react-native"; +const HIT_SLOP = { top: 16, left: 16, bottom: 16, right: 16 }; +const ImageDefaultHeader = ({ onRequestClose }) => ( + + + + ); +const styles = StyleSheet.create({ + root: { + alignItems: "flex-end", + }, + closeButton: { + marginRight: 8, + marginTop: 8, + width: 45, + height: 45, + alignItems: "center", + justifyContent: "center", + borderRadius: 22.5, + backgroundColor: "#00000077", + }, + closeText: { + lineHeight: 25, + fontSize: 25, + paddingTop: 2, + textAlign: "center", + color: "#FFF", + includeFontPadding: false, + }, +}); +export default ImageDefaultHeader; diff --git a/dist/components/ImageItem/ImageItem.android.d.ts b/dist/components/ImageItem/ImageItem.android.d.ts new file mode 100644 index 0000000..0b1aa96 --- /dev/null +++ b/dist/components/ImageItem/ImageItem.android.d.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import React from "react"; +import { ImageSource } from "../../@types"; +declare type Props = { + imageSrc: ImageSource; + onRequestClose: () => void; + onZoom: (isZoomed: boolean) => void; + onLongPress: (image: ImageSource) => void; + delayLongPress: number; + swipeToCloseEnabled?: boolean; + doubleTapToZoomEnabled?: boolean; +}; +declare const _default: React.MemoExoticComponent<({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled, doubleTapToZoomEnabled, }: Props) => JSX.Element>; +export default _default; diff --git a/dist/components/ImageItem/ImageItem.android.js b/dist/components/ImageItem/ImageItem.android.js new file mode 100644 index 0000000..7099ccb --- /dev/null +++ b/dist/components/ImageItem/ImageItem.android.js @@ -0,0 +1,85 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import React, { useState, useCallback } from "react"; +import { Animated, Dimensions, StyleSheet, } from "react-native"; +import useImageDimensions from "../../hooks/useImageDimensions"; +import usePanResponder from "../../hooks/usePanResponder"; +import { getImageStyles, getImageTransform } from "../../utils"; +import { ImageLoading } from "./ImageLoading"; +const SWIPE_CLOSE_OFFSET = 75; +const SWIPE_CLOSE_VELOCITY = 1.75; +const SCREEN = Dimensions.get("window"); +const SCREEN_WIDTH = SCREEN.width; +const SCREEN_HEIGHT = SCREEN.height; +const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, }) => { + const imageContainer = React.createRef(); + const imageDimensions = useImageDimensions(imageSrc); + const [translate, scale] = getImageTransform(imageDimensions, SCREEN); + const scrollValueY = new Animated.Value(0); + const [isLoaded, setLoadEnd] = useState(false); + const onLoaded = useCallback(() => setLoadEnd(true), []); + const onZoomPerformed = (isZoomed) => { + var _a; + onZoom(isZoomed); + if ((_a = imageContainer) === null || _a === void 0 ? void 0 : _a.current) { + // @ts-ignore + imageContainer.current.setNativeProps({ + scrollEnabled: !isZoomed, + }); + } + }; + const onLongPressHandler = useCallback(() => { + onLongPress(imageSrc); + }, [imageSrc, onLongPress]); + const [panHandlers, scaleValue, translateValue] = usePanResponder({ + initialScale: scale || 1, + initialTranslate: translate || { x: 0, y: 0 }, + onZoom: onZoomPerformed, + doubleTapToZoomEnabled, + onLongPress: onLongPressHandler, + delayLongPress, + }); + const imagesStyles = getImageStyles(imageDimensions, translateValue, scaleValue); + const imageOpacity = scrollValueY.interpolate({ + inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], + outputRange: [0.7, 1, 0.7], + }); + const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity }; + const onScrollEndDrag = ({ nativeEvent, }) => { + var _a, _b, _c, _d, _e, _f; + const velocityY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.velocity) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0)); + const offsetY = (_f = (_e = (_d = nativeEvent) === null || _d === void 0 ? void 0 : _d.contentOffset) === null || _e === void 0 ? void 0 : _e.y, (_f !== null && _f !== void 0 ? _f : 0)); + if ((Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY && + offsetY > SWIPE_CLOSE_OFFSET) || + offsetY > SCREEN_HEIGHT / 2) { + onRequestClose(); + } + }; + const onScroll = ({ nativeEvent, }) => { + var _a, _b, _c; + const offsetY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.contentOffset) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0)); + scrollValueY.setValue(offsetY); + }; + return ( + + {(!isLoaded || !imageDimensions) && } + ); +}; +const styles = StyleSheet.create({ + listItem: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + }, + imageScrollContainer: { + height: SCREEN_HEIGHT * 2, + }, +}); +export default React.memo(ImageItem); diff --git a/dist/components/ImageItem/ImageItem.ios.d.ts b/dist/components/ImageItem/ImageItem.ios.d.ts new file mode 100644 index 0000000..1dd5e0e --- /dev/null +++ b/dist/components/ImageItem/ImageItem.ios.d.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import React from "react"; +import { ImageSource } from "../../@types"; +declare type Props = { + imageSrc: ImageSource; + onRequestClose: () => void; + onZoom: (scaled: boolean) => void; + onLongPress: (image: ImageSource) => void; + delayLongPress: number; + swipeToCloseEnabled?: boolean; + doubleTapToZoomEnabled?: boolean; +}; +declare const _default: React.MemoExoticComponent<({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled, doubleTapToZoomEnabled, }: Props) => JSX.Element>; +export default _default; diff --git a/dist/components/ImageItem/ImageItem.ios.js b/dist/components/ImageItem/ImageItem.ios.js new file mode 100644 index 0000000..c87e88c --- /dev/null +++ b/dist/components/ImageItem/ImageItem.ios.js @@ -0,0 +1,79 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import React, { useCallback, useRef, useState } from "react"; +import { Animated, Dimensions, ScrollView, StyleSheet, View, TouchableWithoutFeedback, } from "react-native"; +import useDoubleTapToZoom from "../../hooks/useDoubleTapToZoom"; +import useImageDimensions from "../../hooks/useImageDimensions"; +import { getImageStyles, getImageTransform } from "../../utils"; +import { ImageLoading } from "./ImageLoading"; +const SWIPE_CLOSE_OFFSET = 75; +const SWIPE_CLOSE_VELOCITY = 1.55; +const SCREEN = Dimensions.get("screen"); +const SCREEN_WIDTH = SCREEN.width; +const SCREEN_HEIGHT = SCREEN.height; +const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, }) => { + const scrollViewRef = useRef(null); + const [loaded, setLoaded] = useState(false); + const [scaled, setScaled] = useState(false); + const imageDimensions = useImageDimensions(imageSrc); + const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN); + const [translate, scale] = getImageTransform(imageDimensions, SCREEN); + const scrollValueY = new Animated.Value(0); + const scaleValue = new Animated.Value(scale || 1); + const translateValue = new Animated.ValueXY(translate); + const maxScale = scale && scale > 0 ? Math.max(1 / scale, 1) : 1; + const imageOpacity = scrollValueY.interpolate({ + inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], + outputRange: [0.5, 1, 0.5], + }); + const imagesStyles = getImageStyles(imageDimensions, translateValue, scaleValue); + const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity }; + const onScrollEndDrag = useCallback(({ nativeEvent }) => { + var _a, _b, _c, _d; + const velocityY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.velocity) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0)); + const scaled = ((_d = nativeEvent) === null || _d === void 0 ? void 0 : _d.zoomScale) > 1; + onZoom(scaled); + setScaled(scaled); + if (!scaled && + swipeToCloseEnabled && + Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) { + onRequestClose(); + } + }, [scaled]); + const onScroll = ({ nativeEvent, }) => { + var _a, _b, _c, _d; + const offsetY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.contentOffset) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0)); + if (((_d = nativeEvent) === null || _d === void 0 ? void 0 : _d.zoomScale) > 1) { + return; + } + scrollValueY.setValue(offsetY); + }; + const onLongPressHandler = useCallback((event) => { + onLongPress(imageSrc); + }, [imageSrc, onLongPress]); + return ( + + {(!loaded || !imageDimensions) && } + + setLoaded(true)}/> + + + ); +}; +const styles = StyleSheet.create({ + listItem: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + }, + imageScrollContainer: { + height: SCREEN_HEIGHT, + }, +}); +export default React.memo(ImageItem); diff --git a/dist/components/ImageItem/ImageLoading.d.ts b/dist/components/ImageItem/ImageLoading.d.ts new file mode 100644 index 0000000..865b895 --- /dev/null +++ b/dist/components/ImageItem/ImageLoading.d.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +/// +export declare const ImageLoading: () => JSX.Element; diff --git a/dist/components/ImageItem/ImageLoading.js b/dist/components/ImageItem/ImageLoading.js new file mode 100644 index 0000000..9770f7d --- /dev/null +++ b/dist/components/ImageItem/ImageLoading.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import React from "react"; +import { ActivityIndicator, Dimensions, StyleSheet, View } from "react-native"; +const SCREEN = Dimensions.get("screen"); +const SCREEN_WIDTH = SCREEN.width; +const SCREEN_HEIGHT = SCREEN.height; +export const ImageLoading = () => ( + + ); +const styles = StyleSheet.create({ + listItem: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + }, + loading: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + alignItems: "center", + justifyContent: "center", + }, + imageScrollContainer: { + height: SCREEN_HEIGHT, + }, +}); diff --git a/dist/components/StatusBarManager.d.ts b/dist/components/StatusBarManager.d.ts new file mode 100644 index 0000000..942c70f --- /dev/null +++ b/dist/components/StatusBarManager.d.ts @@ -0,0 +1,4 @@ +declare const StatusBarManager: ({ presentationStyle, }: { + presentationStyle?: "fullScreen" | "pageSheet" | "formSheet" | "overFullScreen" | undefined; +}) => null; +export default StatusBarManager; diff --git a/dist/components/StatusBarManager.js b/dist/components/StatusBarManager.js new file mode 100644 index 0000000..7677856 --- /dev/null +++ b/dist/components/StatusBarManager.js @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { Platform, StatusBar, } from "react-native"; +const StatusBarManager = ({ presentationStyle, }) => { + if (Platform.OS === "ios" || presentationStyle !== "overFullScreen") { + return null; + } + //Can't get an actual state of app status bar with default RN. Gonna rely on "presentationStyle === overFullScreen" prop and guess application status bar state to be visible in this case. + StatusBar.setHidden(true); + useEffect(() => { + return () => StatusBar.setHidden(false); + }, []); + return null; +}; +export default StatusBarManager; diff --git a/dist/hooks/useAnimatedComponents.d.ts b/dist/hooks/useAnimatedComponents.d.ts new file mode 100644 index 0000000..1f31475 --- /dev/null +++ b/dist/hooks/useAnimatedComponents.d.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { Animated } from "react-native"; +declare const useAnimatedComponents: () => readonly [{ + [key: string]: Animated.Value; +}[], { + [key: string]: Animated.Value; +}[], (isVisible: boolean, hideHeaderOnZoom: boolean, hideFooterOnZoom: boolean) => void]; +export default useAnimatedComponents; diff --git a/dist/hooks/useAnimatedComponents.js b/dist/hooks/useAnimatedComponents.js new file mode 100644 index 0000000..705445f --- /dev/null +++ b/dist/hooks/useAnimatedComponents.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { Animated } from "react-native"; +const INITIAL_POSITION = { x: 0, y: 0 }; +const ANIMATION_CONFIG = { + duration: 200, + useNativeDriver: true, +}; +const useAnimatedComponents = () => { + const headerTranslate = new Animated.ValueXY(INITIAL_POSITION); + const footerTranslate = new Animated.ValueXY(INITIAL_POSITION); + const toggleVisible = (isVisible, hideHeaderOnZoom, hideFooterOnZoom) => { + if (isVisible) { + Animated.parallel([ + Animated.timing(headerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }), + Animated.timing(footerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }), + ]).start(); + } + else { + const hideHeaderAnimation = hideHeaderOnZoom + ? [Animated.timing(headerTranslate.y, { + ...ANIMATION_CONFIG, + toValue: -300, + })] + : []; + const hideFooterAnimation = hideFooterOnZoom + ? [Animated.timing(footerTranslate.y, { + ...ANIMATION_CONFIG, + toValue: 300, + })] + : []; + Animated.parallel([...hideHeaderAnimation, ...hideFooterAnimation]).start(); + } + }; + const headerTransform = headerTranslate.getTranslateTransform(); + const footerTransform = footerTranslate.getTranslateTransform(); + return [headerTransform, footerTransform, toggleVisible]; +}; +export default useAnimatedComponents; diff --git a/dist/hooks/useDoubleTapToZoom.d.ts b/dist/hooks/useDoubleTapToZoom.d.ts new file mode 100644 index 0000000..1df97a3 --- /dev/null +++ b/dist/hooks/useDoubleTapToZoom.d.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import React from "react"; +import { ScrollView, NativeTouchEvent, NativeSyntheticEvent } from "react-native"; +import { Dimensions } from "../@types"; +/** + * This is iOS only. + * Same functionality for Android implemented inside usePanResponder hook. + */ +declare function useDoubleTapToZoom(scrollViewRef: React.RefObject, scaled: boolean, screen: Dimensions): (event: NativeSyntheticEvent) => void; +export default useDoubleTapToZoom; diff --git a/dist/hooks/useDoubleTapToZoom.js b/dist/hooks/useDoubleTapToZoom.js new file mode 100644 index 0000000..112e0b2 --- /dev/null +++ b/dist/hooks/useDoubleTapToZoom.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { useCallback } from "react"; +const DOUBLE_TAP_DELAY = 300; +let lastTapTS = null; +/** + * This is iOS only. + * Same functionality for Android implemented inside usePanResponder hook. + */ +function useDoubleTapToZoom(scrollViewRef, scaled, screen) { + const handleDoubleTap = useCallback((event) => { + var _a, _b, _c; + const nowTS = new Date().getTime(); + const scrollResponderRef = (_b = (_a = scrollViewRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.getScrollResponder(); + if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) { + const { pageX, pageY } = event.nativeEvent; + let targetX = 0; + let targetY = 0; + let targetWidth = screen.width; + let targetHeight = screen.height; + // Zooming in + // TODO: Add more precise calculation of targetX, targetY based on touch + if (!scaled) { + targetX = pageX / 2; + targetY = pageY / 2; + targetWidth = screen.width / 2; + targetHeight = screen.height / 2; + } + // @ts-ignore + (_c = scrollResponderRef) === null || _c === void 0 ? void 0 : _c.scrollResponderZoomTo({ + x: targetX, + y: targetY, + width: targetWidth, + height: targetHeight, + animated: true, + }); + } + else { + lastTapTS = nowTS; + } + }, [scaled]); + return handleDoubleTap; +} +export default useDoubleTapToZoom; diff --git a/dist/hooks/useImageDimensions.d.ts b/dist/hooks/useImageDimensions.d.ts new file mode 100644 index 0000000..5613dca --- /dev/null +++ b/dist/hooks/useImageDimensions.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { Dimensions, ImageSource } from "../@types"; +declare const useImageDimensions: (image: ImageSource) => Dimensions | null; +export default useImageDimensions; diff --git a/dist/hooks/useImageDimensions.js b/dist/hooks/useImageDimensions.js new file mode 100644 index 0000000..d89a3a1 --- /dev/null +++ b/dist/hooks/useImageDimensions.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { useEffect, useState } from "react"; +import { Image } from "react-native"; +import { createCache } from "../utils"; +const CACHE_SIZE = 50; +const imageDimensionsCache = createCache(CACHE_SIZE); +const useImageDimensions = (image) => { + const [dimensions, setDimensions] = useState(null); + const getImageDimensions = (image) => { + return new Promise((resolve) => { + if (typeof image == "number") { + const cacheKey = `${image}`; + let imageDimensions = imageDimensionsCache.get(cacheKey); + if (!imageDimensions) { + const { width, height } = Image.resolveAssetSource(image); + imageDimensions = { width, height }; + imageDimensionsCache.set(cacheKey, imageDimensions); + } + resolve(imageDimensions); + return; + } + // @ts-ignore + if (image.uri) { + const source = image; + const cacheKey = source.uri; + const imageDimensions = imageDimensionsCache.get(cacheKey); + if (imageDimensions) { + resolve(imageDimensions); + } + else { + // @ts-ignore + Image.getSizeWithHeaders(source.uri, source.headers, (width, height) => { + imageDimensionsCache.set(cacheKey, { width, height }); + resolve({ width, height }); + }, () => { + resolve({ width: 0, height: 0 }); + }); + } + } + else { + resolve({ width: 0, height: 0 }); + } + }); + }; + let isImageUnmounted = false; + useEffect(() => { + getImageDimensions(image).then((dimensions) => { + if (!isImageUnmounted) { + setDimensions(dimensions); + } + }); + return () => { + isImageUnmounted = true; + }; + }, [image]); + return dimensions; +}; +export default useImageDimensions; diff --git a/dist/hooks/useImageIndexChange.d.ts b/dist/hooks/useImageIndexChange.d.ts new file mode 100644 index 0000000..badccaf --- /dev/null +++ b/dist/hooks/useImageIndexChange.d.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { NativeSyntheticEvent, NativeScrollEvent } from "react-native"; +import { Dimensions } from "../@types"; +declare const useImageIndexChange: (imageIndex: number, screen: Dimensions) => readonly [number, (event: NativeSyntheticEvent) => void]; +export default useImageIndexChange; diff --git a/dist/hooks/useImageIndexChange.js b/dist/hooks/useImageIndexChange.js new file mode 100644 index 0000000..cb5f0f7 --- /dev/null +++ b/dist/hooks/useImageIndexChange.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { useState } from "react"; +const useImageIndexChange = (imageIndex, screen) => { + const [currentImageIndex, setImageIndex] = useState(imageIndex); + const onScroll = (event) => { + const { nativeEvent: { contentOffset: { x: scrollX }, }, } = event; + if (screen.width) { + const nextIndex = Math.round(scrollX / screen.width); + setImageIndex(nextIndex < 0 ? 0 : nextIndex); + } + }; + return [currentImageIndex, onScroll]; +}; +export default useImageIndexChange; diff --git a/dist/hooks/useImagePrefetch.d.ts b/dist/hooks/useImagePrefetch.d.ts new file mode 100644 index 0000000..7dcef2c --- /dev/null +++ b/dist/hooks/useImagePrefetch.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { ImageSource } from "../@types"; +declare const useImagePrefetch: (images: ImageSource[]) => void; +export default useImagePrefetch; diff --git a/dist/hooks/useImagePrefetch.js b/dist/hooks/useImagePrefetch.js new file mode 100644 index 0000000..de1375d --- /dev/null +++ b/dist/hooks/useImagePrefetch.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { useEffect } from "react"; +import { Image } from "react-native"; +const useImagePrefetch = (images) => { + useEffect(() => { + images.forEach((image) => { + //@ts-ignore + if (image.uri) { + //@ts-ignore + return Image.prefetch(image.uri); + } + }); + }, []); +}; +export default useImagePrefetch; diff --git a/dist/hooks/usePanResponder.d.ts b/dist/hooks/usePanResponder.d.ts new file mode 100644 index 0000000..a4e41f0 --- /dev/null +++ b/dist/hooks/usePanResponder.d.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { Animated, GestureResponderHandlers } from "react-native"; +import { Position } from "../@types"; +declare type Props = { + initialScale: number; + initialTranslate: Position; + onZoom: (isZoomed: boolean) => void; + doubleTapToZoomEnabled: boolean; + onLongPress: () => void; + delayLongPress: number; +}; +declare const usePanResponder: ({ initialScale, initialTranslate, onZoom, doubleTapToZoomEnabled, onLongPress, delayLongPress, }: Props) => readonly [GestureResponderHandlers, Animated.Value, Animated.ValueXY]; +export default usePanResponder; diff --git a/dist/hooks/usePanResponder.js b/dist/hooks/usePanResponder.js new file mode 100644 index 0000000..99dcbc7 --- /dev/null +++ b/dist/hooks/usePanResponder.js @@ -0,0 +1,273 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { useMemo, useEffect } from "react"; +import { Animated, Dimensions, } from "react-native"; +import { createPanResponder, getDistanceBetweenTouches, getImageTranslate, getImageDimensionsByTranslate, } from "../utils"; +const SCREEN = Dimensions.get("window"); +const SCREEN_WIDTH = SCREEN.width; +const SCREEN_HEIGHT = SCREEN.height; +const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT); +const SCALE_MAX = 2; +const DOUBLE_TAP_DELAY = 300; +const OUT_BOUND_MULTIPLIER = 0.75; +const usePanResponder = ({ initialScale, initialTranslate, onZoom, doubleTapToZoomEnabled, onLongPress, delayLongPress, }) => { + let numberInitialTouches = 1; + let initialTouches = []; + let currentScale = initialScale; + let currentTranslate = initialTranslate; + let tmpScale = 0; + let tmpTranslate = null; + let isDoubleTapPerformed = false; + let lastTapTS = null; + let longPressHandlerRef = null; + const meaningfulShift = MIN_DIMENSION * 0.01; + const scaleValue = new Animated.Value(initialScale); + const translateValue = new Animated.ValueXY(initialTranslate); + const imageDimensions = getImageDimensionsByTranslate(initialTranslate, SCREEN); + const getBounds = (scale) => { + const scaledImageDimensions = { + width: imageDimensions.width * scale, + height: imageDimensions.height * scale, + }; + const translateDelta = getImageTranslate(scaledImageDimensions, SCREEN); + const left = initialTranslate.x - translateDelta.x; + const right = left - (scaledImageDimensions.width - SCREEN.width); + const top = initialTranslate.y - translateDelta.y; + const bottom = top - (scaledImageDimensions.height - SCREEN.height); + return [top, left, bottom, right]; + }; + const getTranslateInBounds = (translate, scale) => { + const inBoundTranslate = { x: translate.x, y: translate.y }; + const [topBound, leftBound, bottomBound, rightBound] = getBounds(scale); + if (translate.x > leftBound) { + inBoundTranslate.x = leftBound; + } + else if (translate.x < rightBound) { + inBoundTranslate.x = rightBound; + } + if (translate.y > topBound) { + inBoundTranslate.y = topBound; + } + else if (translate.y < bottomBound) { + inBoundTranslate.y = bottomBound; + } + return inBoundTranslate; + }; + const fitsScreenByWidth = () => imageDimensions.width * currentScale < SCREEN_WIDTH; + const fitsScreenByHeight = () => imageDimensions.height * currentScale < SCREEN_HEIGHT; + useEffect(() => { + scaleValue.addListener(({ value }) => { + if (typeof onZoom === "function") { + onZoom(value !== initialScale); + } + }); + return () => scaleValue.removeAllListeners(); + }); + const cancelLongPressHandle = () => { + longPressHandlerRef && clearTimeout(longPressHandlerRef); + }; + const handlers = { + onGrant: (_, gestureState) => { + numberInitialTouches = gestureState.numberActiveTouches; + if (gestureState.numberActiveTouches > 1) + return; + longPressHandlerRef = setTimeout(onLongPress, delayLongPress); + }, + onStart: (event, gestureState) => { + initialTouches = event.nativeEvent.touches; + numberInitialTouches = gestureState.numberActiveTouches; + if (gestureState.numberActiveTouches > 1) + return; + const tapTS = Date.now(); + // Handle double tap event by calculating diff between first and second taps timestamps + isDoubleTapPerformed = Boolean(lastTapTS && tapTS - lastTapTS < DOUBLE_TAP_DELAY); + if (doubleTapToZoomEnabled && isDoubleTapPerformed) { + const isScaled = currentTranslate.x !== initialTranslate.x; // currentScale !== initialScale; + const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0]; + const targetScale = SCALE_MAX; + const nextScale = isScaled ? initialScale : targetScale; + const nextTranslate = isScaled + ? initialTranslate + : getTranslateInBounds({ + x: initialTranslate.x + + (SCREEN_WIDTH / 2 - touchX) * (targetScale / currentScale), + y: initialTranslate.y + + (SCREEN_HEIGHT / 2 - touchY) * (targetScale / currentScale), + }, targetScale); + onZoom(!isScaled); + Animated.parallel([ + Animated.timing(translateValue.x, { + toValue: nextTranslate.x, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(translateValue.y, { + toValue: nextTranslate.y, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(scaleValue, { + toValue: nextScale, + duration: 300, + useNativeDriver: true, + }), + ], { stopTogether: false }).start(() => { + currentScale = nextScale; + currentTranslate = nextTranslate; + }); + lastTapTS = null; + } + else { + lastTapTS = Date.now(); + } + }, + onMove: (event, gestureState) => { + const { dx, dy } = gestureState; + if (Math.abs(dx) >= meaningfulShift || Math.abs(dy) >= meaningfulShift) { + cancelLongPressHandle(); + } + // Don't need to handle move because double tap in progress (was handled in onStart) + if (doubleTapToZoomEnabled && isDoubleTapPerformed) { + cancelLongPressHandle(); + return; + } + if (numberInitialTouches === 1 && + gestureState.numberActiveTouches === 2) { + numberInitialTouches = 2; + initialTouches = event.nativeEvent.touches; + } + const isTapGesture = numberInitialTouches == 1 && gestureState.numberActiveTouches === 1; + const isPinchGesture = numberInitialTouches === 2 && gestureState.numberActiveTouches === 2; + if (isPinchGesture) { + cancelLongPressHandle(); + const initialDistance = getDistanceBetweenTouches(initialTouches); + const currentDistance = getDistanceBetweenTouches(event.nativeEvent.touches); + let nextScale = (currentDistance / initialDistance) * currentScale; + /** + * In case image is scaling smaller than initial size -> + * slow down this transition by applying OUT_BOUND_MULTIPLIER + */ + if (nextScale < initialScale) { + nextScale = + nextScale + (initialScale - nextScale) * OUT_BOUND_MULTIPLIER; + } + /** + * In case image is scaling down -> move it in direction of initial position + */ + if (currentScale > initialScale && currentScale > nextScale) { + const k = (currentScale - initialScale) / (currentScale - nextScale); + const nextTranslateX = nextScale < initialScale + ? initialTranslate.x + : currentTranslate.x - + (currentTranslate.x - initialTranslate.x) / k; + const nextTranslateY = nextScale < initialScale + ? initialTranslate.y + : currentTranslate.y - + (currentTranslate.y - initialTranslate.y) / k; + translateValue.x.setValue(nextTranslateX); + translateValue.y.setValue(nextTranslateY); + tmpTranslate = { x: nextTranslateX, y: nextTranslateY }; + } + scaleValue.setValue(nextScale); + tmpScale = nextScale; + } + if (isTapGesture && currentScale > initialScale) { + const { x, y } = currentTranslate; + const { dx, dy } = gestureState; + const [topBound, leftBound, bottomBound, rightBound] = getBounds(currentScale); + let nextTranslateX = x + dx; + let nextTranslateY = y + dy; + if (nextTranslateX > leftBound) { + nextTranslateX = + nextTranslateX - + (nextTranslateX - leftBound) * OUT_BOUND_MULTIPLIER; + } + if (nextTranslateX < rightBound) { + nextTranslateX = + nextTranslateX - + (nextTranslateX - rightBound) * OUT_BOUND_MULTIPLIER; + } + if (nextTranslateY > topBound) { + nextTranslateY = + nextTranslateY - (nextTranslateY - topBound) * OUT_BOUND_MULTIPLIER; + } + if (nextTranslateY < bottomBound) { + nextTranslateY = + nextTranslateY - + (nextTranslateY - bottomBound) * OUT_BOUND_MULTIPLIER; + } + if (fitsScreenByWidth()) { + nextTranslateX = x; + } + if (fitsScreenByHeight()) { + nextTranslateY = y; + } + translateValue.x.setValue(nextTranslateX); + translateValue.y.setValue(nextTranslateY); + tmpTranslate = { x: nextTranslateX, y: nextTranslateY }; + } + }, + onRelease: () => { + cancelLongPressHandle(); + if (isDoubleTapPerformed) { + isDoubleTapPerformed = false; + } + if (tmpScale > 0) { + if (tmpScale < initialScale || tmpScale > SCALE_MAX) { + tmpScale = tmpScale < initialScale ? initialScale : SCALE_MAX; + Animated.timing(scaleValue, { + toValue: tmpScale, + duration: 100, + useNativeDriver: true, + }).start(); + } + currentScale = tmpScale; + tmpScale = 0; + } + if (tmpTranslate) { + const { x, y } = tmpTranslate; + const [topBound, leftBound, bottomBound, rightBound] = getBounds(currentScale); + let nextTranslateX = x; + let nextTranslateY = y; + if (!fitsScreenByWidth()) { + if (nextTranslateX > leftBound) { + nextTranslateX = leftBound; + } + else if (nextTranslateX < rightBound) { + nextTranslateX = rightBound; + } + } + if (!fitsScreenByHeight()) { + if (nextTranslateY > topBound) { + nextTranslateY = topBound; + } + else if (nextTranslateY < bottomBound) { + nextTranslateY = bottomBound; + } + } + Animated.parallel([ + Animated.timing(translateValue.x, { + toValue: nextTranslateX, + duration: 100, + useNativeDriver: true, + }), + Animated.timing(translateValue.y, { + toValue: nextTranslateY, + duration: 100, + useNativeDriver: true, + }), + ]).start(); + currentTranslate = { x: nextTranslateX, y: nextTranslateY }; + tmpTranslate = null; + } + }, + }; + const panResponder = useMemo(() => createPanResponder(handlers), [handlers]); + return [panResponder.panHandlers, scaleValue, translateValue]; +}; +export default usePanResponder; diff --git a/dist/hooks/useRequestClose.d.ts b/dist/hooks/useRequestClose.d.ts new file mode 100644 index 0000000..d747f58 --- /dev/null +++ b/dist/hooks/useRequestClose.d.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +declare const useRequestClose: (onRequestClose: () => void) => readonly [number, () => void]; +export default useRequestClose; diff --git a/dist/hooks/useRequestClose.js b/dist/hooks/useRequestClose.js new file mode 100644 index 0000000..0fe2d86 --- /dev/null +++ b/dist/hooks/useRequestClose.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { useState } from "react"; +const useRequestClose = (onRequestClose) => { + const [opacity, setOpacity] = useState(1); + return [ + opacity, + () => { + setOpacity(0); + onRequestClose(); + setTimeout(() => setOpacity(1), 0); + }, + ]; +}; +export default useRequestClose; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..6e58101 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +export { default } from "./ImageViewing"; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..6e58101 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +export { default } from "./ImageViewing"; diff --git a/dist/utils.d.ts b/dist/utils.d.ts new file mode 100644 index 0000000..edeea10 --- /dev/null +++ b/dist/utils.d.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { Animated, GestureResponderEvent, PanResponderGestureState, PanResponderInstance, NativeTouchEvent } from "react-native"; +import { Dimensions, Position } from "./@types"; +declare type CacheStorageItem = { + key: string; + value: any; +}; +export declare const createCache: (cacheSize: number) => { + _storage: CacheStorageItem[]; + get(key: string): any; + set(key: string, value: any): void; +}; +export declare const splitArrayIntoBatches: (arr: any[], batchSize: number) => any[]; +export declare const getImageTransform: (image: Dimensions | null, screen: Dimensions) => readonly [] | readonly [{ + readonly x: number; + readonly y: number; +}, number]; +export declare const getImageStyles: (image: Dimensions | null, translate: Animated.ValueXY, scale?: Animated.Value | undefined) => { + width: number; + height: number; + transform?: undefined; +} | { + width: number; + height: number; + transform: { + [key: string]: Animated.Value; + }[]; +}; +export declare const getImageTranslate: (image: Dimensions, screen: Dimensions) => Position; +export declare const getImageDimensionsByTranslate: (translate: Position, screen: Dimensions) => Dimensions; +export declare const getImageTranslateForScale: (currentTranslate: Position, targetScale: number, screen: Dimensions) => Position; +declare type HandlerType = (event: GestureResponderEvent, state: PanResponderGestureState) => void; +declare type PanResponderProps = { + onGrant: HandlerType; + onStart?: HandlerType; + onMove: HandlerType; + onRelease?: HandlerType; + onTerminate?: HandlerType; +}; +export declare const createPanResponder: ({ onGrant, onStart, onMove, onRelease, onTerminate, }: PanResponderProps) => PanResponderInstance; +export declare const getDistanceBetweenTouches: (touches: NativeTouchEvent[]) => number; +export {}; diff --git a/dist/utils.js b/dist/utils.js new file mode 100644 index 0000000..69aae88 --- /dev/null +++ b/dist/utils.js @@ -0,0 +1,101 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { Animated, PanResponder, } from "react-native"; +export const createCache = (cacheSize) => ({ + _storage: [], + get(key) { + const { value } = this._storage.find(({ key: storageKey }) => storageKey === key) || {}; + return value; + }, + set(key, value) { + if (this._storage.length >= cacheSize) { + this._storage.shift(); + } + this._storage.push({ key, value }); + }, +}); +export const splitArrayIntoBatches = (arr, batchSize) => arr.reduce((result, item) => { + const batch = result.pop() || []; + if (batch.length < batchSize) { + batch.push(item); + result.push(batch); + } + else { + result.push(batch, [item]); + } + return result; +}, []); +export const getImageTransform = (image, screen) => { + var _a, _b; + if (!((_a = image) === null || _a === void 0 ? void 0 : _a.width) || !((_b = image) === null || _b === void 0 ? void 0 : _b.height)) { + return []; + } + const wScale = screen.width / image.width; + const hScale = screen.height / image.height; + const scale = Math.min(wScale, hScale); + const { x, y } = getImageTranslate(image, screen); + return [{ x, y }, scale]; +}; +export const getImageStyles = (image, translate, scale) => { + var _a, _b; + if (!((_a = image) === null || _a === void 0 ? void 0 : _a.width) || !((_b = image) === null || _b === void 0 ? void 0 : _b.height)) { + return { width: 0, height: 0 }; + } + const transform = translate.getTranslateTransform(); + if (scale) { + transform.push({ scale }, { perspective: new Animated.Value(1000) }); + } + return { + width: image.width, + height: image.height, + transform, + }; +}; +export const getImageTranslate = (image, screen) => { + const getTranslateForAxis = (axis) => { + const imageSize = axis === "x" ? image.width : image.height; + const screenSize = axis === "x" ? screen.width : screen.height; + return (screenSize - imageSize) / 2; + }; + return { + x: getTranslateForAxis("x"), + y: getTranslateForAxis("y"), + }; +}; +export const getImageDimensionsByTranslate = (translate, screen) => ({ + width: screen.width - translate.x * 2, + height: screen.height - translate.y * 2, +}); +export const getImageTranslateForScale = (currentTranslate, targetScale, screen) => { + const { width, height } = getImageDimensionsByTranslate(currentTranslate, screen); + const targetImageDimensions = { + width: width * targetScale, + height: height * targetScale, + }; + return getImageTranslate(targetImageDimensions, screen); +}; +export const createPanResponder = ({ onGrant, onStart, onMove, onRelease, onTerminate, }) => PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderGrant: onGrant, + onPanResponderStart: onStart, + onPanResponderMove: onMove, + onPanResponderRelease: onRelease, + onPanResponderTerminate: onTerminate, + onPanResponderTerminationRequest: () => false, + onShouldBlockNativeResponder: () => false, +}); +export const getDistanceBetweenTouches = (touches) => { + const [a, b] = touches; + if (a == null || b == null) { + return 0; + } + return Math.sqrt(Math.pow(a.pageX - b.pageX, 2) + Math.pow(a.pageY - b.pageY, 2)); +};