@one-am/react-native-simple-image-slider 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +74 -0
  3. package/lib/commonjs/@types/styled.d.js +4 -0
  4. package/lib/commonjs/@types/styled.d.js.map +1 -0
  5. package/lib/commonjs/BaseSimpleImageSlider.js +194 -0
  6. package/lib/commonjs/BaseSimpleImageSlider.js.map +1 -0
  7. package/lib/commonjs/FullScreenImageSlider.js +126 -0
  8. package/lib/commonjs/FullScreenImageSlider.js.map +1 -0
  9. package/lib/commonjs/PageCounter.js +39 -0
  10. package/lib/commonjs/PageCounter.js.map +1 -0
  11. package/lib/commonjs/PinchToZoom.js +173 -0
  12. package/lib/commonjs/PinchToZoom.js.map +1 -0
  13. package/lib/commonjs/SimpleImageSlider.js +59 -0
  14. package/lib/commonjs/SimpleImageSlider.js.map +1 -0
  15. package/lib/commonjs/StyledComponentsThemeProvider.js +51 -0
  16. package/lib/commonjs/StyledComponentsThemeProvider.js.map +1 -0
  17. package/lib/commonjs/index.js +35 -0
  18. package/lib/commonjs/index.js.map +1 -0
  19. package/lib/commonjs/utils/clamp.js +13 -0
  20. package/lib/commonjs/utils/clamp.js.map +1 -0
  21. package/lib/commonjs/utils/renderProp.js +17 -0
  22. package/lib/commonjs/utils/renderProp.js.map +1 -0
  23. package/lib/module/@types/styled.d.js +2 -0
  24. package/lib/module/@types/styled.d.js.map +1 -0
  25. package/lib/module/BaseSimpleImageSlider.js +185 -0
  26. package/lib/module/BaseSimpleImageSlider.js.map +1 -0
  27. package/lib/module/FullScreenImageSlider.js +117 -0
  28. package/lib/module/FullScreenImageSlider.js.map +1 -0
  29. package/lib/module/PageCounter.js +31 -0
  30. package/lib/module/PageCounter.js.map +1 -0
  31. package/lib/module/PinchToZoom.js +165 -0
  32. package/lib/module/PinchToZoom.js.map +1 -0
  33. package/lib/module/SimpleImageSlider.js +50 -0
  34. package/lib/module/SimpleImageSlider.js.map +1 -0
  35. package/lib/module/StyledComponentsThemeProvider.js +43 -0
  36. package/lib/module/StyledComponentsThemeProvider.js.map +1 -0
  37. package/lib/module/index.js +6 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/utils/clamp.js +6 -0
  40. package/lib/module/utils/clamp.js.map +1 -0
  41. package/lib/module/utils/renderProp.js +9 -0
  42. package/lib/module/utils/renderProp.js.map +1 -0
  43. package/lib/typescript/src/BaseSimpleImageSlider.d.ts +33 -0
  44. package/lib/typescript/src/BaseSimpleImageSlider.d.ts.map +1 -0
  45. package/lib/typescript/src/FullScreenImageSlider.d.ts +15 -0
  46. package/lib/typescript/src/FullScreenImageSlider.d.ts.map +1 -0
  47. package/lib/typescript/src/PageCounter.d.ts +10 -0
  48. package/lib/typescript/src/PageCounter.d.ts.map +1 -0
  49. package/lib/typescript/src/PinchToZoom.d.ts +18 -0
  50. package/lib/typescript/src/PinchToZoom.d.ts.map +1 -0
  51. package/lib/typescript/src/SimpleImageSlider.d.ts +11 -0
  52. package/lib/typescript/src/SimpleImageSlider.d.ts.map +1 -0
  53. package/lib/typescript/src/StyledComponentsThemeProvider.d.ts +3 -0
  54. package/lib/typescript/src/StyledComponentsThemeProvider.d.ts.map +1 -0
  55. package/lib/typescript/src/index.d.ts +6 -0
  56. package/lib/typescript/src/index.d.ts.map +1 -0
  57. package/lib/typescript/src/utils/clamp.d.ts +2 -0
  58. package/lib/typescript/src/utils/clamp.d.ts.map +1 -0
  59. package/lib/typescript/src/utils/renderProp.d.ts +4 -0
  60. package/lib/typescript/src/utils/renderProp.d.ts.map +1 -0
  61. package/package.json +170 -0
  62. package/src/@types/styled.d.ts +39 -0
  63. package/src/BaseSimpleImageSlider.tsx +281 -0
  64. package/src/FullScreenImageSlider.tsx +164 -0
  65. package/src/PageCounter.tsx +39 -0
  66. package/src/PinchToZoom.tsx +323 -0
  67. package/src/SimpleImageSlider.tsx +72 -0
  68. package/src/StyledComponentsThemeProvider.tsx +48 -0
  69. package/src/index.tsx +14 -0
  70. package/src/utils/clamp.ts +4 -0
  71. package/src/utils/renderProp.tsx +22 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,6BAA6B,MAAM,iCAAiC,CAAC;AAC5E,OAAO,mBAAmB,EAAE,EAAE,KAAK,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAC/F,OAAO,iBAAiB,EAAE,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,qBAAqB,EAAE,EAAE,KAAK,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAEjG,OAAO,EACH,6BAA6B,IAAI,8BAA8B,EAC/D,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,qBAAqB,EACrB,KAAK,0BAA0B,GAClC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const clamp: (value: number, min: number, max: number) => number;
2
+ //# sourceMappingURL=clamp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clamp.d.ts","sourceRoot":"","sources":["../../../../src/utils/clamp.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,UAAW,MAAM,OAAO,MAAM,OAAO,MAAM,WAG5D,CAAC"}
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export type RenderProp = React.ComponentType<unknown> | React.ReactElement | string | undefined | null;
3
+ export default function renderProp(Component: RenderProp): string | React.JSX.Element | null;
4
+ //# sourceMappingURL=renderProp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderProp.d.ts","sourceRoot":"","sources":["../../../../src/utils/renderProp.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyB,MAAM,OAAO,CAAC;AAE9C,MAAM,MAAM,UAAU,GAChB,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,GAC5B,KAAK,CAAC,YAAY,GAClB,MAAM,GACN,SAAS,GACT,IAAI,CAAC;AAEX,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,SAAS,EAAE,UAAU,qCAYvD"}
package/package.json ADDED
@@ -0,0 +1,170 @@
1
+ {
2
+ "name": "@one-am/react-native-simple-image-slider",
3
+ "version": "0.2.2",
4
+ "description": "A simple and performant image slider made with FlashList. Includes a full screen gallery component with gesture support.",
5
+ "main": "lib/commonjs/index",
6
+ "module": "lib/module/index",
7
+ "types": "lib/typescript/src/index.d.ts",
8
+ "react-native": "src/index",
9
+ "source": "src/index",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "android",
14
+ "ios",
15
+ "cpp",
16
+ "*.podspec",
17
+ "!ios/build",
18
+ "!android/build",
19
+ "!android/gradle",
20
+ "!android/gradlew",
21
+ "!android/gradlew.bat",
22
+ "!android/local.properties",
23
+ "!**/__tests__",
24
+ "!**/__fixtures__",
25
+ "!**/__mocks__",
26
+ "!**/.*"
27
+ ],
28
+ "scripts": {
29
+ "example": "yarn workspace @one-am/react-native-simple-image-slider-example",
30
+ "test": "jest",
31
+ "typecheck": "tsc --noEmit",
32
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
33
+ "clean": "del-cli lib",
34
+ "prepare": "bob build",
35
+ "release": "release-it"
36
+ },
37
+ "keywords": [
38
+ "react-native",
39
+ "ios",
40
+ "android"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/one-am-it/react-native-simple-image-slider.git"
45
+ },
46
+ "author": "one-am <info@oneam.it> (https://github.com/one-am-it)",
47
+ "license": "MIT",
48
+ "bugs": {
49
+ "url": "https://github.com/one-am-it/react-native-simple-image-slider/issues"
50
+ },
51
+ "homepage": "https://github.com/one-am-it/react-native-simple-image-slider#readme",
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org/",
54
+ "access": "public"
55
+ },
56
+ "devDependencies": {
57
+ "@commitlint/config-conventional": "^17.0.2",
58
+ "@evilmartians/lefthook": "^1.5.0",
59
+ "@react-native/eslint-config": "^0.73.1",
60
+ "@release-it/conventional-changelog": "^5.0.0",
61
+ "@shopify/flash-list": "^1.6.3",
62
+ "@types/jest": "^29.5.5",
63
+ "@types/react": "^18.2.44",
64
+ "@typescript-eslint/eslint-plugin": "^5.62.0",
65
+ "@typescript-eslint/parser": "^5.62.0",
66
+ "babel-plugin-styled-components": "^2.1.4",
67
+ "commitlint": "^17.0.2",
68
+ "del-cli": "^5.1.0",
69
+ "eslint": "^8.56.0",
70
+ "eslint-config-prettier": "^9.1.0",
71
+ "eslint-config-standard-with-typescript": "^37.0.0",
72
+ "eslint-plugin-import": "^2.29.1",
73
+ "eslint-plugin-n": "^16.6.2",
74
+ "eslint-plugin-prettier": "^5.0.1",
75
+ "eslint-plugin-promise": "^6.1.1",
76
+ "eslint-plugin-react": "^7.33.2",
77
+ "eslint-plugin-react-hooks": "^4.6.0",
78
+ "eslint-plugin-react-native": "^4.1.0",
79
+ "expo-haptics": "^12.8.1",
80
+ "expo-image": "^1.10.6",
81
+ "expo-status-bar": "^1.11.1",
82
+ "jest": "^29.7.0",
83
+ "prettier": "^3.2.5",
84
+ "react": "18.2.0",
85
+ "react-native": "0.73.5",
86
+ "react-native-builder-bob": "^0.20.0",
87
+ "react-native-gesture-handler": "^2.15.0",
88
+ "react-native-reanimated": "^3.3.0",
89
+ "react-native-safe-area-context": "^4.9.0",
90
+ "react-native-svg": "^15.1.0",
91
+ "release-it": "^15.0.0",
92
+ "styled-components": "^6.1.8",
93
+ "tabler-icons-react-native": "^2.42.1",
94
+ "typescript": "^5.2.2",
95
+ "typescript-plugin-styled-components": "^3.0.0"
96
+ },
97
+ "resolutions": {
98
+ "@types/react": "^18.2.44",
99
+ "styled-components": "^6"
100
+ },
101
+ "peerDependencies": {
102
+ "@babel/core": "^7.24.0",
103
+ "@babel/runtime": "^7.24.0",
104
+ "expo-haptics": "*",
105
+ "expo-image": "*",
106
+ "expo-status-bar": "*",
107
+ "react": "*",
108
+ "react-dom": "*",
109
+ "react-native": "*",
110
+ "react-native-gesture-handler": "^2.15.0",
111
+ "react-native-reanimated": "~3.6.2",
112
+ "react-native-safe-area-context": "*",
113
+ "styled-components": "^6.1.8",
114
+ "tabler-icons-react-native": "*",
115
+ "react-native-svg": "*"
116
+ },
117
+ "workspaces": [
118
+ "example"
119
+ ],
120
+ "packageManager": "yarn@3.6.1",
121
+ "jest": {
122
+ "preset": "react-native",
123
+ "modulePathIgnorePatterns": [
124
+ "<rootDir>/example/node_modules",
125
+ "<rootDir>/lib/"
126
+ ]
127
+ },
128
+ "commitlint": {
129
+ "extends": [
130
+ "@commitlint/config-conventional"
131
+ ]
132
+ },
133
+ "release-it": {
134
+ "git": {
135
+ "commitMessage": "chore: release ${version}",
136
+ "tagName": "v${version}"
137
+ },
138
+ "npm": {
139
+ "publish": true
140
+ },
141
+ "github": {
142
+ "release": true
143
+ },
144
+ "gitlab": {
145
+ "release": true
146
+ },
147
+ "plugins": {
148
+ "@release-it/conventional-changelog": {
149
+ "preset": "angular"
150
+ }
151
+ }
152
+ },
153
+ "react-native-builder-bob": {
154
+ "source": "src",
155
+ "output": "lib",
156
+ "targets": [
157
+ "commonjs",
158
+ "module",
159
+ [
160
+ "typescript",
161
+ {
162
+ "project": "tsconfig.build.json"
163
+ }
164
+ ]
165
+ ]
166
+ },
167
+ "dependencies": {
168
+ "merge-refs": "^1.2.2"
169
+ }
170
+ }
@@ -0,0 +1,39 @@
1
+ import 'styled-components/native';
2
+
3
+ export type StyledComponentsTheme = {
4
+ colors: {
5
+ pageCounterBackground: string;
6
+ pageCounterBorder: string;
7
+ fullScreenCloseButton: string;
8
+ descriptionContainerBorder: string;
9
+ };
10
+ styles: {
11
+ spacing: {
12
+ xxs: number;
13
+ xs: number;
14
+ s: number;
15
+ m: number;
16
+ l: number;
17
+ xl: number;
18
+ };
19
+ borderRadius: {
20
+ xs: number;
21
+ s: number;
22
+ m: number;
23
+ l: number;
24
+ xl: number;
25
+ };
26
+ borderWidth: {
27
+ xs: number;
28
+ s: number;
29
+ m: number;
30
+ l: number;
31
+ xl: number;
32
+ };
33
+ };
34
+ };
35
+
36
+ declare module 'styled-components/native' {
37
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
38
+ export interface DefaultTheme extends StyledComponentsTheme {}
39
+ }
@@ -0,0 +1,281 @@
1
+ import React, {
2
+ forwardRef,
3
+ type ReactElement,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import { FlashList, type ListRenderItemInfo } from '@shopify/flash-list';
11
+ import mergeRefs from 'merge-refs';
12
+ import { Image, type ImageProps } from 'expo-image';
13
+ import { Pressable, type StyleProp, type ViewStyle } from 'react-native';
14
+ import type ViewToken from '@shopify/flash-list/src/viewability/ViewToken';
15
+ import styled from 'styled-components/native';
16
+ import PageCounter from './PageCounter';
17
+ import PinchToZoom from './PinchToZoom';
18
+ import { type SharedValue } from 'react-native-reanimated';
19
+ import { GestureHandlerRootView, ScrollView } from 'react-native-gesture-handler';
20
+ import renderProp, { type RenderProp } from './utils/renderProp';
21
+
22
+ export type SimpleImageSliderItem = ImageProps & {
23
+ key: string;
24
+ };
25
+
26
+ export type BaseSimpleImageSliderProps = {
27
+ data: SimpleImageSliderItem[] | undefined;
28
+ style?: StyleProp<ViewStyle>;
29
+ imageWidth?: number;
30
+ imageHeight?: number;
31
+ imageAspectRatio?: number;
32
+ onItemPress?: (item: SimpleImageSliderItem, index: number) => void;
33
+ maxPreviewItems?: number;
34
+ showPageCounter?: boolean;
35
+ pageCounterPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
36
+ renderPageCounter?: (currentPage: number, totalPages: number) => ReactElement;
37
+ TopRightComponent?: RenderProp;
38
+ TopLeftComponent?: RenderProp;
39
+ BottomRightComponent?: RenderProp;
40
+ BottomLeftComponent?: RenderProp;
41
+ indexOverride?: number;
42
+ onViewableItemChange?: (index: number) => void;
43
+ enablePinchToZoom?: boolean;
44
+ onPinchToZoomTranslationChange?: (
45
+ x: SharedValue<number>,
46
+ y: SharedValue<number>,
47
+ scale: SharedValue<number>
48
+ ) => void;
49
+ onPinchToZoomRequestClose?: () => void;
50
+ };
51
+
52
+ const StyledAbsoluteComponentContainer = styled.View<{
53
+ position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
54
+ }>`
55
+ z-index: 1000;
56
+ position: absolute;
57
+ bottom: ${({ position, theme }) =>
58
+ position === 'bottom-left' || position === 'bottom-right'
59
+ ? `${theme.styles.spacing.m}px`
60
+ : undefined};
61
+ top: ${({ position, theme }) =>
62
+ position === 'top-left' || position === 'top-right'
63
+ ? `${theme.styles.spacing.m}px`
64
+ : undefined};
65
+ left: ${({ position, theme }) =>
66
+ position === 'top-left' || position === 'bottom-left'
67
+ ? `${theme.styles.spacing.m}px`
68
+ : undefined};
69
+ right: ${({ position, theme }) =>
70
+ position === 'top-right' || position === 'bottom-right'
71
+ ? `${theme.styles.spacing.m}px`
72
+ : undefined};
73
+ `;
74
+
75
+ const StyledPageCounter = styled(PageCounter)<{
76
+ position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
77
+ }>`
78
+ z-index: 1000;
79
+ position: absolute;
80
+ bottom: ${({ position, theme }) =>
81
+ position === 'bottom-left' || position === 'bottom-right'
82
+ ? `${theme.styles.spacing.m}px`
83
+ : undefined};
84
+ top: ${({ position, theme }) =>
85
+ position === 'top-left' || position === 'top-right'
86
+ ? `${theme.styles.spacing.m}px`
87
+ : undefined};
88
+ left: ${({ position, theme }) =>
89
+ position === 'top-left' || position === 'bottom-left'
90
+ ? `${theme.styles.spacing.m}px`
91
+ : undefined};
92
+ right: ${({ position, theme }) =>
93
+ position === 'top-right' || position === 'bottom-right'
94
+ ? `${theme.styles.spacing.m}px`
95
+ : undefined};
96
+ `;
97
+
98
+ const StyledContainer = styled(GestureHandlerRootView)<{ aspectRatio?: number }>`
99
+ aspect-ratio: ${({ aspectRatio }) => aspectRatio ?? 4 / 3};
100
+ width: 100%;
101
+ `;
102
+
103
+ const StyledImage = styled(Image)<
104
+ ImageProps & {
105
+ imageWidth?: number;
106
+ imageHeight?: number;
107
+ imageAspectRatio?: number;
108
+ }
109
+ >`
110
+ width: ${({ imageWidth }) => (imageWidth ? `${imageWidth}px` : '100%')};
111
+ height: ${({ imageHeight }) => (imageHeight ? `${imageHeight}px` : '100%')};
112
+ aspect-ratio: ${({ imageAspectRatio }) => (imageAspectRatio ? imageAspectRatio : 4 / 3)};
113
+ `;
114
+
115
+ const StyledPinchToZoom = styled(PinchToZoom)`
116
+ z-index: 1000;
117
+ `;
118
+
119
+ const BaseListImageSlider = forwardRef<
120
+ FlashList<SimpleImageSliderItem>,
121
+ BaseSimpleImageSliderProps
122
+ >(function BaseListImageSlider(
123
+ {
124
+ data,
125
+ style,
126
+ imageWidth,
127
+ imageHeight,
128
+ imageAspectRatio,
129
+ onItemPress,
130
+ maxPreviewItems,
131
+ showPageCounter = true,
132
+ pageCounterPosition = 'bottom-left',
133
+ renderPageCounter,
134
+ TopRightComponent,
135
+ TopLeftComponent,
136
+ BottomRightComponent,
137
+ BottomLeftComponent,
138
+ indexOverride,
139
+ onViewableItemChange,
140
+ enablePinchToZoom = false,
141
+ onPinchToZoomTranslationChange,
142
+ onPinchToZoomRequestClose,
143
+ },
144
+ ref
145
+ ) {
146
+ const listRef = useRef<FlashList<SimpleImageSliderItem>>(null);
147
+ const [currentItem, setCurrentItem] = useState(0);
148
+
149
+ const slicedData = useMemo(
150
+ () => (maxPreviewItems !== undefined ? data?.slice(0, maxPreviewItems) ?? [] : data ?? []),
151
+ [data, maxPreviewItems]
152
+ );
153
+
154
+ useEffect(() => {
155
+ setCurrentItem(indexOverride ?? 0);
156
+
157
+ listRef.current?.scrollToIndex({ index: indexOverride ?? 0, animated: false });
158
+ }, [indexOverride, slicedData]);
159
+
160
+ const keyExtractor = useCallback((item: SimpleImageSliderItem) => item.key, []);
161
+
162
+ const renderItem = useCallback(
163
+ ({ item, index }: ListRenderItemInfo<SimpleImageSliderItem>) => {
164
+ return (
165
+ <Pressable onPress={() => onItemPress?.(item, index)}>
166
+ <StyledImage
167
+ transition={200}
168
+ placeholder={item.placeholder}
169
+ placeholderContentFit={'cover'}
170
+ imageWidth={imageWidth}
171
+ imageHeight={imageHeight}
172
+ imageAspectRatio={imageAspectRatio}
173
+ recyclingKey={item.key}
174
+ source={item.source}
175
+ contentFit={'cover'}
176
+ contentPosition={'center'}
177
+ />
178
+ </Pressable>
179
+ );
180
+ },
181
+ [imageAspectRatio, imageHeight, imageWidth, onItemPress]
182
+ );
183
+
184
+ const onViewableItemsChanged = useCallback(
185
+ ({ viewableItems }: { viewableItems: ViewToken[]; changed: ViewToken[] }) => {
186
+ const newIndex = viewableItems[0]?.index;
187
+ if (newIndex !== undefined && newIndex !== null) {
188
+ setCurrentItem(newIndex);
189
+ onViewableItemChange?.(newIndex);
190
+ }
191
+ },
192
+ [onViewableItemChange]
193
+ );
194
+
195
+ const [scrollEnabled, setScrollEnabled] = useState(true);
196
+
197
+ const onScaleChange = useCallback(() => {
198
+ setScrollEnabled(false);
199
+ }, []);
200
+
201
+ const onScaleReset = useCallback(() => {
202
+ setScrollEnabled(true);
203
+ }, []);
204
+
205
+ const list = (
206
+ <FlashList
207
+ renderScrollComponent={ScrollView}
208
+ scrollEnabled={scrollEnabled}
209
+ disableScrollViewPanResponder={enablePinchToZoom ? !scrollEnabled : false}
210
+ ref={mergeRefs(ref, listRef)}
211
+ initialScrollIndex={indexOverride ?? currentItem ?? 0}
212
+ onViewableItemsChanged={onViewableItemsChanged}
213
+ viewabilityConfig={{
214
+ itemVisiblePercentThreshold: 55,
215
+ }}
216
+ decelerationRate={'fast'}
217
+ pagingEnabled={true}
218
+ showsHorizontalScrollIndicator={false}
219
+ showsVerticalScrollIndicator={false}
220
+ horizontal={true}
221
+ keyExtractor={keyExtractor}
222
+ renderItem={renderItem}
223
+ data={slicedData}
224
+ estimatedItemSize={
225
+ imageWidth ?? (imageHeight ? imageHeight * (imageAspectRatio ?? 4 / 3) : 350)
226
+ }
227
+ />
228
+ );
229
+
230
+ return (
231
+ <StyledContainer aspectRatio={imageAspectRatio} style={style}>
232
+ {enablePinchToZoom ? (
233
+ <StyledPinchToZoom
234
+ onRequestClose={onPinchToZoomRequestClose}
235
+ onTranslationChange={onPinchToZoomTranslationChange}
236
+ onScaleChange={onScaleChange}
237
+ onScaleReset={onScaleReset}
238
+ maximumZoomScale={5}
239
+ minimumZoomScale={1}
240
+ >
241
+ {list}
242
+ </StyledPinchToZoom>
243
+ ) : (
244
+ list
245
+ )}
246
+ {showPageCounter ? (
247
+ renderPageCounter ? (
248
+ renderPageCounter(currentItem + 1, slicedData.length)
249
+ ) : (
250
+ <StyledPageCounter
251
+ position={pageCounterPosition}
252
+ totalPages={slicedData.length}
253
+ currentPage={currentItem + 1}
254
+ />
255
+ )
256
+ ) : null}
257
+ {TopRightComponent ? (
258
+ <StyledAbsoluteComponentContainer position={'top-right'}>
259
+ {renderProp(TopRightComponent)}
260
+ </StyledAbsoluteComponentContainer>
261
+ ) : null}
262
+ {TopLeftComponent ? (
263
+ <StyledAbsoluteComponentContainer position={'top-left'}>
264
+ {renderProp(TopLeftComponent)}
265
+ </StyledAbsoluteComponentContainer>
266
+ ) : null}
267
+ {BottomRightComponent ? (
268
+ <StyledAbsoluteComponentContainer position={'bottom-right'}>
269
+ {renderProp(BottomRightComponent)}
270
+ </StyledAbsoluteComponentContainer>
271
+ ) : null}
272
+ {BottomLeftComponent ? (
273
+ <StyledAbsoluteComponentContainer position={'bottom-left'}>
274
+ {renderProp(BottomLeftComponent)}
275
+ </StyledAbsoluteComponentContainer>
276
+ ) : null}
277
+ </StyledContainer>
278
+ );
279
+ });
280
+
281
+ export default BaseListImageSlider;
@@ -0,0 +1,164 @@
1
+ import React, {
2
+ forwardRef,
3
+ type ReactNode,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ } from 'react';
9
+ import { Modal, type ScaledSize, StyleSheet, useWindowDimensions } from 'react-native';
10
+ import { IconX } from 'tabler-icons-react-native';
11
+ import Animated, {
12
+ runOnJS,
13
+ type SharedValue,
14
+ useAnimatedStyle,
15
+ useSharedValue,
16
+ withTiming,
17
+ } from 'react-native-reanimated';
18
+ import { setStatusBarStyle } from 'expo-status-bar';
19
+ import styled, { type DefaultTheme, useTheme } from 'styled-components/native';
20
+ import { FlashList } from '@shopify/flash-list';
21
+ import BaseListImageSlider, {
22
+ type BaseSimpleImageSliderProps,
23
+ type SimpleImageSliderItem,
24
+ } from './BaseSimpleImageSlider';
25
+ import { type EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context';
26
+
27
+ export type FullScreenImageSliderProps = BaseSimpleImageSliderProps & {
28
+ open?: boolean;
29
+ onRequestClose?: () => void;
30
+ renderDescription?: (index: number) => ReactNode;
31
+ };
32
+
33
+ const StyledDescriptionContainer = styled.View`
34
+ position: absolute;
35
+ border-top-width: 1px;
36
+ border-top-color: ${({ theme }) => theme.colors.descriptionContainerBorder};
37
+ width: 100%;
38
+ padding-top: ${({ theme }) => theme.styles.spacing.l}px;
39
+ `;
40
+
41
+ const StyledModalCloseButton = styled.TouchableOpacity`
42
+ position: absolute;
43
+ z-index: 1000;
44
+ `;
45
+
46
+ const StyledModalContentContainer = styled(Animated.View)`
47
+ align-items: center;
48
+ justify-content: center;
49
+ gap: ${({ theme }) => theme.styles.spacing.m}px;
50
+ `;
51
+
52
+ const FullScreenImageSlider = forwardRef<
53
+ FlashList<SimpleImageSliderItem>,
54
+ FullScreenImageSliderProps
55
+ >(function FullScreenImageSlider(
56
+ { open, onRequestClose, renderDescription, onViewableItemChange, ...props },
57
+ ref
58
+ ) {
59
+ const windowDimensions = useWindowDimensions();
60
+ const theme = useTheme();
61
+ const safeAreaInsets = useSafeAreaInsets();
62
+ const styles = useMemo(
63
+ () => makeStyles(theme, safeAreaInsets, windowDimensions),
64
+ [theme, safeAreaInsets, windowDimensions]
65
+ );
66
+
67
+ const [internalIndex, setInternalIndex] = useState<number>(0);
68
+
69
+ const internalOnViewableItemChange = useCallback(
70
+ (index: number) => {
71
+ setInternalIndex(index);
72
+ onViewableItemChange?.(index);
73
+ },
74
+ [onViewableItemChange]
75
+ );
76
+
77
+ useEffect(() => {
78
+ if (open) {
79
+ setStatusBarStyle('light');
80
+ } else {
81
+ setStatusBarStyle('dark');
82
+ }
83
+ }, [open]);
84
+
85
+ const backgroundOpacity = useSharedValue(0);
86
+
87
+ const modalContentStyle = useAnimatedStyle(() => {
88
+ return {
89
+ backgroundColor: `rgba(0, 0, 0, ${backgroundOpacity.value})`,
90
+ };
91
+ }, []);
92
+
93
+ const onPinchToZoomTranslationChange = useCallback(
94
+ (x: SharedValue<number>, y: SharedValue<number>, scale: SharedValue<number>) => {
95
+ if (scale.value <= 1) {
96
+ if (x.value === 0 && y.value === 0) {
97
+ runOnJS(setStatusBarStyle)('light');
98
+ backgroundOpacity.value = withTiming(1);
99
+ } else {
100
+ runOnJS(setStatusBarStyle)('dark');
101
+ backgroundOpacity.value = withTiming(0);
102
+ }
103
+ } else {
104
+ backgroundOpacity.value = 1;
105
+ }
106
+ },
107
+ [backgroundOpacity]
108
+ );
109
+
110
+ return (
111
+ <Modal
112
+ animationType="fade"
113
+ onRequestClose={onRequestClose}
114
+ transparent={true}
115
+ visible={open}
116
+ >
117
+ <StyledModalContentContainer style={[styles.modalContent, modalContentStyle]}>
118
+ <StyledModalCloseButton style={styles.closeButton} onPress={onRequestClose}>
119
+ <IconX color={theme.colors.fullScreenCloseButton} />
120
+ </StyledModalCloseButton>
121
+ <BaseListImageSlider
122
+ enablePinchToZoom={true}
123
+ onPinchToZoomTranslationChange={onPinchToZoomTranslationChange}
124
+ onPinchToZoomRequestClose={onRequestClose}
125
+ {...props}
126
+ onViewableItemChange={internalOnViewableItemChange}
127
+ showPageCounter={false}
128
+ imageWidth={windowDimensions.width}
129
+ ref={ref}
130
+ />
131
+
132
+ {renderDescription ? (
133
+ <StyledDescriptionContainer style={styles.descriptionContainer}>
134
+ {renderDescription(internalIndex)}
135
+ </StyledDescriptionContainer>
136
+ ) : null}
137
+ </StyledModalContentContainer>
138
+ </Modal>
139
+ );
140
+ });
141
+
142
+ export default FullScreenImageSlider;
143
+
144
+ const makeStyles = (
145
+ theme: DefaultTheme,
146
+ safeAreaInsets: EdgeInsets,
147
+ windowDimensions: ScaledSize
148
+ ) => {
149
+ return StyleSheet.create({
150
+ closeButton: {
151
+ top: safeAreaInsets.top,
152
+ right: safeAreaInsets.right + theme.styles.spacing.l,
153
+ },
154
+ modalContent: {
155
+ height: windowDimensions.height,
156
+ width: windowDimensions.width,
157
+ },
158
+ descriptionContainer: {
159
+ bottom: safeAreaInsets.bottom + 100,
160
+ paddingLeft: safeAreaInsets.left + theme.styles.spacing.l,
161
+ paddingRight: safeAreaInsets.right + theme.styles.spacing.l,
162
+ },
163
+ });
164
+ };