react-native-gesture-image-viewer 2.1.3 → 2.2.0

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 (55) hide show
  1. package/lib/module/GestureTrigger.js +22 -5
  2. package/lib/module/GestureTrigger.js.map +1 -1
  3. package/lib/module/GestureViewer.js +50 -40
  4. package/lib/module/GestureViewer.js.map +1 -1
  5. package/lib/module/GestureViewerManager.js +26 -6
  6. package/lib/module/GestureViewerManager.js.map +1 -1
  7. package/lib/module/GestureViewerRegistry.js +52 -8
  8. package/lib/module/GestureViewerRegistry.js.map +1 -1
  9. package/lib/module/useGestureViewer.js +149 -113
  10. package/lib/module/useGestureViewer.js.map +1 -1
  11. package/lib/module/useGestureViewerPaging.js +82 -0
  12. package/lib/module/useGestureViewerPaging.js.map +1 -0
  13. package/lib/module/useGestureViewerPaging.types.js +4 -0
  14. package/lib/module/useGestureViewerPaging.types.js.map +1 -0
  15. package/lib/module/useGestureViewerPaging.web.js +254 -0
  16. package/lib/module/useGestureViewerPaging.web.js.map +1 -0
  17. package/lib/module/utils/index.js +9 -0
  18. package/lib/module/utils/index.js.map +1 -1
  19. package/lib/module/utils/tapZoom.js +34 -0
  20. package/lib/module/utils/tapZoom.js.map +1 -0
  21. package/lib/module/utils/webScroll.js +70 -0
  22. package/lib/module/utils/webScroll.js.map +1 -0
  23. package/lib/typescript/src/GestureTrigger.d.ts +9 -3
  24. package/lib/typescript/src/GestureTrigger.d.ts.map +1 -1
  25. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  26. package/lib/typescript/src/GestureViewerManager.d.ts +5 -0
  27. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  28. package/lib/typescript/src/GestureViewerRegistry.d.ts +11 -4
  29. package/lib/typescript/src/GestureViewerRegistry.d.ts.map +1 -1
  30. package/lib/typescript/src/useGestureViewer.d.ts +4 -3
  31. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  32. package/lib/typescript/src/useGestureViewerPaging.d.ts +3 -0
  33. package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -0
  34. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +33 -0
  35. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -0
  36. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +3 -0
  37. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -0
  38. package/lib/typescript/src/utils/index.d.ts +7 -0
  39. package/lib/typescript/src/utils/index.d.ts.map +1 -1
  40. package/lib/typescript/src/utils/tapZoom.d.ts +12 -0
  41. package/lib/typescript/src/utils/tapZoom.d.ts.map +1 -0
  42. package/lib/typescript/src/utils/webScroll.d.ts +18 -0
  43. package/lib/typescript/src/utils/webScroll.d.ts.map +1 -0
  44. package/package.json +1 -1
  45. package/src/GestureTrigger.tsx +31 -6
  46. package/src/GestureViewer.tsx +39 -15
  47. package/src/GestureViewerManager.ts +29 -6
  48. package/src/GestureViewerRegistry.ts +67 -8
  49. package/src/useGestureViewer.ts +174 -165
  50. package/src/useGestureViewerPaging.ts +128 -0
  51. package/src/useGestureViewerPaging.types.ts +35 -0
  52. package/src/useGestureViewerPaging.web.ts +410 -0
  53. package/src/utils/index.ts +17 -1
  54. package/src/utils/tapZoom.ts +44 -0
  55. package/src/utils/webScroll.ts +96 -0
@@ -0,0 +1,410 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
3
+
4
+ import type {
5
+ UseGestureViewerPagingArgs,
6
+ UseGestureViewerPagingResult,
7
+ } from './useGestureViewerPaging.types';
8
+ import { applyTapZoomAtPoint } from './utils/tapZoom';
9
+ import {
10
+ getWebAutoPlayTargetPhysicalIndex,
11
+ getWebScrollPhysicalIndex,
12
+ resolveWebScrollFinalState,
13
+ } from './utils/webScroll';
14
+
15
+ type WebScrollActor = 'idle' | 'user' | 'autoplay';
16
+
17
+ type WebScrollRuntime = {
18
+ actor: WebScrollActor;
19
+ isAutoplayPausedByUser: boolean;
20
+ lastSettledPhysicalIndex: number;
21
+ latestOffsetX: number;
22
+ latestRawPhysicalIndex: number;
23
+ lastProgrammaticScrollVersion: number;
24
+ settleTimer: ReturnType<typeof setTimeout> | null;
25
+ resumeAutoplayTimer: ReturnType<typeof setTimeout> | null;
26
+ };
27
+
28
+ export function useGestureViewerPaging({
29
+ adjustedInitialIndex,
30
+ autoPlay,
31
+ autoPlayInterval,
32
+ currentIndex,
33
+ dataLength,
34
+ enableDoubleTapZoom,
35
+ enableHorizontalSwipe,
36
+ enableLoop,
37
+ height,
38
+ isRotated,
39
+ isZoomed,
40
+ itemSpacing,
41
+ manager,
42
+ maxZoomScale,
43
+ scale,
44
+ scrollTo,
45
+ syncCurrentIndex,
46
+ syncPendingIndex,
47
+ translateX,
48
+ translateY,
49
+ width,
50
+ }: UseGestureViewerPagingArgs): UseGestureViewerPagingResult {
51
+ const webScrollRuntimeRef = useRef<WebScrollRuntime>({
52
+ actor: 'idle',
53
+ isAutoplayPausedByUser: false,
54
+ lastSettledPhysicalIndex: adjustedInitialIndex,
55
+ latestOffsetX: adjustedInitialIndex * (width + itemSpacing),
56
+ latestRawPhysicalIndex: adjustedInitialIndex,
57
+ lastProgrammaticScrollVersion: 0,
58
+ settleTimer: null,
59
+ resumeAutoplayTimer: null,
60
+ });
61
+
62
+ const clearWebSettleTimer = useCallback(() => {
63
+ const runtime = webScrollRuntimeRef.current;
64
+
65
+ if (runtime.settleTimer) {
66
+ clearTimeout(runtime.settleTimer);
67
+ runtime.settleTimer = null;
68
+ }
69
+ }, []);
70
+
71
+ const clearWebAutoplayResumeTimer = useCallback(() => {
72
+ const runtime = webScrollRuntimeRef.current;
73
+
74
+ if (runtime.resumeAutoplayTimer) {
75
+ clearTimeout(runtime.resumeAutoplayTimer);
76
+ runtime.resumeAutoplayTimer = null;
77
+ }
78
+ }, []);
79
+
80
+ const scheduleWebAutoplayResume = useCallback(() => {
81
+ const runtime = webScrollRuntimeRef.current;
82
+
83
+ clearWebAutoplayResumeTimer();
84
+ runtime.resumeAutoplayTimer = setTimeout(() => {
85
+ runtime.isAutoplayPausedByUser = false;
86
+ runtime.resumeAutoplayTimer = null;
87
+ }, 800);
88
+ }, [clearWebAutoplayResumeTimer]);
89
+
90
+ const pauseWebAutoplayWithoutPagingInteraction = useCallback(() => {
91
+ const runtime = webScrollRuntimeRef.current;
92
+
93
+ runtime.isAutoplayPausedByUser = true;
94
+ runtime.actor = 'idle';
95
+ clearWebAutoplayResumeTimer();
96
+ }, [clearWebAutoplayResumeTimer]);
97
+
98
+ const beginWebUserInteraction = useCallback(
99
+ (force = false) => {
100
+ const runtime = webScrollRuntimeRef.current;
101
+
102
+ if (!force && runtime.actor === 'autoplay') {
103
+ return;
104
+ }
105
+
106
+ runtime.actor = 'user';
107
+ runtime.isAutoplayPausedByUser = true;
108
+ clearWebAutoplayResumeTimer();
109
+ },
110
+ [clearWebAutoplayResumeTimer],
111
+ );
112
+
113
+ const beginWebAutoplayScroll = useCallback(() => {
114
+ const runtime = webScrollRuntimeRef.current;
115
+
116
+ runtime.actor = 'autoplay';
117
+ runtime.isAutoplayPausedByUser = false;
118
+ runtime.lastProgrammaticScrollVersion = manager?.getProgrammaticScrollVersion() ?? 0;
119
+ clearWebAutoplayResumeTimer();
120
+ }, [clearWebAutoplayResumeTimer, manager]);
121
+
122
+ const syncProgrammaticActorFromManager = useCallback(() => {
123
+ const runtime = webScrollRuntimeRef.current;
124
+ const nextVersion = manager?.getProgrammaticScrollVersion() ?? 0;
125
+
126
+ if (nextVersion === runtime.lastProgrammaticScrollVersion) {
127
+ return;
128
+ }
129
+
130
+ runtime.actor = 'autoplay';
131
+ runtime.isAutoplayPausedByUser = false;
132
+ runtime.lastProgrammaticScrollVersion = nextVersion;
133
+ clearWebAutoplayResumeTimer();
134
+ }, [clearWebAutoplayResumeTimer, manager]);
135
+
136
+ const finalizeWebScroll = useCallback(
137
+ (offsetX: number, source: 'scroll' | 'momentum' = 'scroll') => {
138
+ if (!enableHorizontalSwipe || dataLength <= 0) {
139
+ return;
140
+ }
141
+
142
+ const runtime = webScrollRuntimeRef.current;
143
+ const settledByActor = runtime.actor;
144
+
145
+ clearWebSettleTimer();
146
+ runtime.latestOffsetX = offsetX;
147
+
148
+ const settledState = resolveWebScrollFinalState({
149
+ dataLength,
150
+ enableLoop,
151
+ lastSettledPhysicalIndex: runtime.lastSettledPhysicalIndex,
152
+ offsetX,
153
+ pageWidth: width + itemSpacing,
154
+ });
155
+
156
+ if (!settledState) {
157
+ return;
158
+ }
159
+
160
+ const { logicalIndex, rawPhysicalIndex, settledPhysicalIndex } = settledState;
161
+
162
+ if (source === 'scroll' && runtime.latestRawPhysicalIndex !== rawPhysicalIndex) {
163
+ return;
164
+ }
165
+
166
+ if (rawPhysicalIndex !== settledPhysicalIndex) {
167
+ const crossedLoopBoundary =
168
+ enableLoop &&
169
+ dataLength > 1 &&
170
+ (rawPhysicalIndex === 0 || rawPhysicalIndex === dataLength + 1);
171
+
172
+ scrollTo(settledPhysicalIndex, !crossedLoopBoundary);
173
+ }
174
+
175
+ runtime.lastSettledPhysicalIndex = settledPhysicalIndex;
176
+ runtime.latestOffsetX = settledPhysicalIndex * (width + itemSpacing);
177
+ runtime.actor = 'idle';
178
+ manager?.cancelPendingLoopTransition();
179
+ syncCurrentIndex(logicalIndex);
180
+
181
+ if (settledByActor === 'user') {
182
+ scheduleWebAutoplayResume();
183
+ }
184
+ },
185
+ [
186
+ clearWebSettleTimer,
187
+ dataLength,
188
+ enableHorizontalSwipe,
189
+ enableLoop,
190
+ itemSpacing,
191
+ manager,
192
+ scheduleWebAutoplayResume,
193
+ scrollTo,
194
+ syncCurrentIndex,
195
+ width,
196
+ ],
197
+ );
198
+
199
+ const scheduleWebScrollSettle = useCallback(
200
+ (offsetX: number) => {
201
+ const runtime = webScrollRuntimeRef.current;
202
+
203
+ runtime.latestOffsetX = offsetX;
204
+ runtime.latestRawPhysicalIndex = getWebScrollPhysicalIndex(offsetX, width + itemSpacing);
205
+ clearWebSettleTimer();
206
+ runtime.settleTimer = setTimeout(() => {
207
+ finalizeWebScroll(runtime.latestOffsetX, 'scroll');
208
+ }, 180);
209
+ },
210
+ [clearWebSettleTimer, finalizeWebScroll, itemSpacing, width],
211
+ );
212
+
213
+ useEffect(() => {
214
+ const runtime = webScrollRuntimeRef.current;
215
+
216
+ runtime.actor = 'idle';
217
+ runtime.isAutoplayPausedByUser = false;
218
+ runtime.lastSettledPhysicalIndex = adjustedInitialIndex;
219
+ runtime.latestOffsetX = adjustedInitialIndex * (width + itemSpacing);
220
+ runtime.latestRawPhysicalIndex = adjustedInitialIndex;
221
+ runtime.lastProgrammaticScrollVersion = manager?.getProgrammaticScrollVersion() ?? 0;
222
+ clearWebSettleTimer();
223
+ clearWebAutoplayResumeTimer();
224
+ }, [
225
+ adjustedInitialIndex,
226
+ clearWebAutoplayResumeTimer,
227
+ clearWebSettleTimer,
228
+ itemSpacing,
229
+ manager,
230
+ width,
231
+ ]);
232
+
233
+ useEffect(() => {
234
+ return () => {
235
+ clearWebSettleTimer();
236
+ clearWebAutoplayResumeTimer();
237
+ };
238
+ }, [clearWebAutoplayResumeTimer, clearWebSettleTimer]);
239
+
240
+ useEffect(() => {
241
+ if (
242
+ !autoPlay ||
243
+ dataLength <= 1 ||
244
+ isZoomed ||
245
+ isRotated ||
246
+ (!enableLoop && currentIndex === dataLength - 1)
247
+ ) {
248
+ return;
249
+ }
250
+
251
+ const intervalMs = Math.max(250, Math.floor(autoPlayInterval || 0));
252
+
253
+ if (!Number.isFinite(intervalMs)) {
254
+ return;
255
+ }
256
+
257
+ const interval = setInterval(() => {
258
+ if (isZoomed || isRotated) {
259
+ return;
260
+ }
261
+
262
+ const runtime = webScrollRuntimeRef.current;
263
+
264
+ if (runtime.actor !== 'idle' || runtime.isAutoplayPausedByUser || runtime.settleTimer) {
265
+ return;
266
+ }
267
+
268
+ const targetPhysicalIndex = getWebAutoPlayTargetPhysicalIndex({
269
+ currentIndex,
270
+ dataLength,
271
+ enableLoop,
272
+ });
273
+
274
+ if (targetPhysicalIndex === null) {
275
+ return;
276
+ }
277
+
278
+ beginWebAutoplayScroll();
279
+ runtime.latestOffsetX = targetPhysicalIndex * (width + itemSpacing);
280
+ scrollTo(targetPhysicalIndex, true);
281
+ scheduleWebScrollSettle(runtime.latestOffsetX);
282
+ }, intervalMs);
283
+
284
+ return () => clearInterval(interval);
285
+ }, [
286
+ autoPlay,
287
+ autoPlayInterval,
288
+ beginWebAutoplayScroll,
289
+ currentIndex,
290
+ dataLength,
291
+ enableLoop,
292
+ isRotated,
293
+ isZoomed,
294
+ itemSpacing,
295
+ scheduleWebScrollSettle,
296
+ scrollTo,
297
+ width,
298
+ ]);
299
+
300
+ const onScroll = useCallback(
301
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
302
+ if (!enableHorizontalSwipe) {
303
+ return;
304
+ }
305
+
306
+ const offsetX = event.nativeEvent.contentOffset.x;
307
+ const runtime = webScrollRuntimeRef.current;
308
+ const pendingState = resolveWebScrollFinalState({
309
+ dataLength,
310
+ enableLoop,
311
+ lastSettledPhysicalIndex: runtime.lastSettledPhysicalIndex,
312
+ offsetX,
313
+ pageWidth: width + itemSpacing,
314
+ });
315
+
316
+ if (pendingState) {
317
+ syncPendingIndex(pendingState.logicalIndex);
318
+ }
319
+
320
+ syncProgrammaticActorFromManager();
321
+ beginWebUserInteraction();
322
+ scheduleWebScrollSettle(offsetX);
323
+ },
324
+ [
325
+ beginWebUserInteraction,
326
+ dataLength,
327
+ enableHorizontalSwipe,
328
+ enableLoop,
329
+ itemSpacing,
330
+ scheduleWebScrollSettle,
331
+ syncPendingIndex,
332
+ syncProgrammaticActorFromManager,
333
+ width,
334
+ ],
335
+ );
336
+
337
+ const onMomentumScrollEnd = useCallback(
338
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
339
+ if (!enableHorizontalSwipe) {
340
+ return;
341
+ }
342
+
343
+ finalizeWebScroll(event.nativeEvent.contentOffset.x, 'momentum');
344
+ },
345
+ [enableHorizontalSwipe, finalizeWebScroll],
346
+ );
347
+
348
+ const onScrollBeginDrag = useCallback(() => {
349
+ beginWebUserInteraction(true);
350
+ clearWebSettleTimer();
351
+ manager?.handleScrollBeginDrag();
352
+ }, [beginWebUserInteraction, clearWebSettleTimer, manager]);
353
+
354
+ const onWebDoubleClick = useCallback(
355
+ (event: any) => {
356
+ if (!enableDoubleTapZoom || event?.nativeEvent?.detail !== 2) {
357
+ return;
358
+ }
359
+
360
+ pauseWebAutoplayWithoutPagingInteraction();
361
+ scheduleWebAutoplayResume();
362
+
363
+ const nativeEvent = event?.nativeEvent;
364
+ const eventTarget = event?.currentTarget ?? nativeEvent?.currentTarget ?? nativeEvent?.target;
365
+ let locationX = nativeEvent?.locationX;
366
+ let locationY = nativeEvent?.locationY;
367
+
368
+ if (
369
+ (!Number.isFinite(locationX) || !Number.isFinite(locationY)) &&
370
+ typeof eventTarget?.getBoundingClientRect === 'function' &&
371
+ Number.isFinite(nativeEvent?.clientX) &&
372
+ Number.isFinite(nativeEvent?.clientY)
373
+ ) {
374
+ const rect = eventTarget.getBoundingClientRect();
375
+
376
+ locationX = nativeEvent.clientX - rect.left;
377
+ locationY = nativeEvent.clientY - rect.top;
378
+ }
379
+
380
+ applyTapZoomAtPoint({
381
+ x: Number.isFinite(locationX) ? locationX : width / 2,
382
+ y: Number.isFinite(locationY) ? locationY : height / 2,
383
+ width,
384
+ height,
385
+ maxZoomScale,
386
+ scale,
387
+ translateX,
388
+ translateY,
389
+ });
390
+ },
391
+ [
392
+ enableDoubleTapZoom,
393
+ height,
394
+ maxZoomScale,
395
+ pauseWebAutoplayWithoutPagingInteraction,
396
+ scheduleWebAutoplayResume,
397
+ scale,
398
+ translateX,
399
+ translateY,
400
+ width,
401
+ ],
402
+ );
403
+
404
+ return {
405
+ onMomentumScrollEnd,
406
+ onScroll,
407
+ onScrollBeginDrag,
408
+ onWebDoubleClick,
409
+ };
410
+ }
@@ -1,4 +1,8 @@
1
- import { FlatList as RNFlatList, ScrollView as RNScrollView } from 'react-native';
1
+ import {
2
+ FlatList as RNFlatList,
3
+ ScrollView as RNScrollView,
4
+ type PlatformOSType,
5
+ } from 'react-native';
2
6
  import {
3
7
  FlatList as GestureFlatList,
4
8
  ScrollView as GestureScrollView,
@@ -31,6 +35,18 @@ export const isFlashListLike = (component: React.ComponentType<any>): boolean =>
31
35
  return component?.displayName === 'FlashList' || component?.name === 'FlashList';
32
36
  };
33
37
 
38
+ /**
39
+ * iOS needs the extra Native gesture wrapper so the dismiss pan can resolve before the scrollable claims touches.
40
+ * We intentionally skip Android because the same require-to-fail relation regressed horizontal paging there,
41
+ * and we also avoid wrapping RNGH-provided scrollables to prevent double-applying Gesture.Native() to components that already participate in RNGH.
42
+ */
43
+ export const shouldUseNativeScrollGesture = (
44
+ platformOS: PlatformOSType,
45
+ component: React.ComponentType<any>,
46
+ ): boolean => {
47
+ return platformOS === 'ios' && component !== GestureScrollView && component !== GestureFlatList;
48
+ };
49
+
34
50
  export const createBoundsConstraint =
35
51
  ({ width, height }: { width: number; height: number }) =>
36
52
  ({
@@ -0,0 +1,44 @@
1
+ import type { SharedValue } from 'react-native-reanimated';
2
+ import { Easing, withTiming } from 'react-native-reanimated';
3
+
4
+ export const applyTapZoomAtPoint = ({
5
+ x,
6
+ y,
7
+ width,
8
+ height,
9
+ maxZoomScale,
10
+ scale,
11
+ translateX,
12
+ translateY,
13
+ }: {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ maxZoomScale: number;
19
+ scale: SharedValue<number>;
20
+ translateX: SharedValue<number>;
21
+ translateY: SharedValue<number>;
22
+ }) => {
23
+ 'worklet';
24
+
25
+ const nextScale = scale.value > 1 ? 1 : maxZoomScale;
26
+ const timingConfig = {
27
+ duration: 300,
28
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
29
+ };
30
+
31
+ if (nextScale > 1) {
32
+ const centerX = x - width / 2;
33
+ const centerY = y - height / 2;
34
+
35
+ // NOTE 확대로 밀려난 거리만큼 반대로 이동해서 탭 지점을 제자리에 유지
36
+ translateX.value = withTiming(-centerX * (nextScale - 1), timingConfig);
37
+ translateY.value = withTiming(-centerY * (nextScale - 1), timingConfig);
38
+ } else {
39
+ translateX.value = withTiming(0, timingConfig);
40
+ translateY.value = withTiming(0, timingConfig);
41
+ }
42
+
43
+ scale.value = withTiming(nextScale, timingConfig);
44
+ };
@@ -0,0 +1,96 @@
1
+ export const getWebScrollPhysicalIndex = (offsetX: number, pageWidth: number): number => {
2
+ if (!Number.isFinite(offsetX) || !Number.isFinite(pageWidth) || pageWidth <= 0) {
3
+ return 0;
4
+ }
5
+
6
+ return Math.max(0, Math.round(offsetX / pageWidth));
7
+ };
8
+
9
+ export const resolveWebScrollFinalState = ({
10
+ dataLength,
11
+ enableLoop,
12
+ lastSettledPhysicalIndex,
13
+ offsetX,
14
+ pageWidth,
15
+ }: {
16
+ offsetX: number;
17
+ pageWidth: number;
18
+ lastSettledPhysicalIndex: number;
19
+ dataLength: number;
20
+ enableLoop: boolean;
21
+ }): { logicalIndex: number; rawPhysicalIndex: number; settledPhysicalIndex: number } | null => {
22
+ if (
23
+ dataLength <= 0 ||
24
+ !Number.isFinite(offsetX) ||
25
+ !Number.isFinite(pageWidth) ||
26
+ pageWidth <= 0
27
+ ) {
28
+ return null;
29
+ }
30
+
31
+ const maxPhysicalIndex = enableLoop && dataLength > 1 ? dataLength + 1 : dataLength - 1;
32
+ const rawPhysicalIndex = getWebScrollPhysicalIndex(offsetX, pageWidth);
33
+
34
+ if (enableLoop && dataLength > 1) {
35
+ const isWrappingBackward = lastSettledPhysicalIndex === 1 && rawPhysicalIndex >= dataLength;
36
+ const isWrappingForward = lastSettledPhysicalIndex === dataLength && rawPhysicalIndex <= 1;
37
+
38
+ if (isWrappingBackward || rawPhysicalIndex === 0) {
39
+ return {
40
+ logicalIndex: dataLength - 1,
41
+ rawPhysicalIndex,
42
+ settledPhysicalIndex: dataLength,
43
+ };
44
+ }
45
+
46
+ if (isWrappingForward || rawPhysicalIndex >= dataLength + 1) {
47
+ return {
48
+ logicalIndex: 0,
49
+ rawPhysicalIndex,
50
+ settledPhysicalIndex: 1,
51
+ };
52
+ }
53
+ }
54
+
55
+ const targetPhysicalIndex = Math.max(0, Math.min(maxPhysicalIndex, rawPhysicalIndex));
56
+
57
+ if (enableLoop && dataLength > 1) {
58
+ return {
59
+ logicalIndex: targetPhysicalIndex - 1,
60
+ rawPhysicalIndex,
61
+ settledPhysicalIndex: targetPhysicalIndex,
62
+ };
63
+ }
64
+
65
+ return {
66
+ logicalIndex: targetPhysicalIndex,
67
+ rawPhysicalIndex,
68
+ settledPhysicalIndex: targetPhysicalIndex,
69
+ };
70
+ };
71
+
72
+ export const getWebAutoPlayTargetPhysicalIndex = ({
73
+ currentIndex,
74
+ dataLength,
75
+ enableLoop,
76
+ }: {
77
+ currentIndex: number;
78
+ dataLength: number;
79
+ enableLoop: boolean;
80
+ }): number | null => {
81
+ if (dataLength <= 1) {
82
+ return null;
83
+ }
84
+
85
+ if (enableLoop && currentIndex === dataLength - 1) {
86
+ return dataLength + 1;
87
+ }
88
+
89
+ const nextIndex = currentIndex + 1;
90
+
91
+ if (nextIndex >= dataLength) {
92
+ return null;
93
+ }
94
+
95
+ return enableLoop ? nextIndex + 1 : nextIndex;
96
+ };