rn-pdf-king 0.1.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.
- package/.eslintrc.js +5 -0
- package/README.md +148 -0
- package/android/build.gradle +55 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/rnpdfking/PdfKing.kt +693 -0
- package/android/src/main/java/expo/modules/rnpdfking/RnPdfKingModule.kt +163 -0
- package/android/src/main/java/expo/modules/rnpdfking/RnPdfKingView.kt +184 -0
- package/build/PdfDocument.d.ts +19 -0
- package/build/PdfDocument.d.ts.map +1 -0
- package/build/PdfDocument.js +81 -0
- package/build/PdfDocument.js.map +1 -0
- package/build/PdfPage.d.ts +24 -0
- package/build/PdfPage.d.ts.map +1 -0
- package/build/PdfPage.js +13 -0
- package/build/PdfPage.js.map +1 -0
- package/build/RnPdfKing.types.d.ts +48 -0
- package/build/RnPdfKing.types.d.ts.map +1 -0
- package/build/RnPdfKing.types.js +2 -0
- package/build/RnPdfKing.types.js.map +1 -0
- package/build/RnPdfKingModule.d.ts +13 -0
- package/build/RnPdfKingModule.d.ts.map +1 -0
- package/build/RnPdfKingModule.js +4 -0
- package/build/RnPdfKingModule.js.map +1 -0
- package/build/RnPdfKingModule.web.d.ts +13 -0
- package/build/RnPdfKingModule.web.d.ts.map +1 -0
- package/build/RnPdfKingModule.web.js +21 -0
- package/build/RnPdfKingModule.web.js.map +1 -0
- package/build/RnPdfKingView.d.ts +4 -0
- package/build/RnPdfKingView.d.ts.map +1 -0
- package/build/RnPdfKingView.js +7 -0
- package/build/RnPdfKingView.js.map +1 -0
- package/build/RnPdfKingView.web.d.ts +4 -0
- package/build/RnPdfKingView.web.d.ts.map +1 -0
- package/build/RnPdfKingView.web.js +7 -0
- package/build/RnPdfKingView.web.js.map +1 -0
- package/build/ZoomableList.d.ts +37 -0
- package/build/ZoomableList.d.ts.map +1 -0
- package/build/ZoomableList.js +289 -0
- package/build/ZoomableList.js.map +1 -0
- package/build/ZoomablePage.d.ts +10 -0
- package/build/ZoomablePage.d.ts.map +1 -0
- package/build/ZoomablePage.js +15 -0
- package/build/ZoomablePage.js.map +1 -0
- package/build/ZoomablePdfPage.d.ts +10 -0
- package/build/ZoomablePdfPage.d.ts.map +1 -0
- package/build/ZoomablePdfPage.js +17 -0
- package/build/ZoomablePdfPage.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +10 -0
- package/build/index.js.map +1 -0
- package/build/zoom/constants.d.ts +36 -0
- package/build/zoom/constants.d.ts.map +1 -0
- package/build/zoom/constants.js +36 -0
- package/build/zoom/constants.js.map +1 -0
- package/build/zoom/index.d.ts +255 -0
- package/build/zoom/index.d.ts.map +1 -0
- package/build/zoom/index.js +783 -0
- package/build/zoom/index.js.map +1 -0
- package/build/zoom/utils.d.ts +55 -0
- package/build/zoom/utils.d.ts.map +1 -0
- package/build/zoom/utils.js +66 -0
- package/build/zoom/utils.js.map +1 -0
- package/bun.lock +2217 -0
- package/expo-module.config.json +9 -0
- package/ios/RnPdfKing.podspec +29 -0
- package/ios/RnPdfKingModule.swift +48 -0
- package/ios/RnPdfKingView.swift +38 -0
- package/package.json +45 -0
- package/src/PdfDocument.tsx +115 -0
- package/src/PdfPage.tsx +57 -0
- package/src/RnPdfKing.types.ts +32 -0
- package/src/RnPdfKingModule.ts +15 -0
- package/src/RnPdfKingModule.web.ts +24 -0
- package/src/RnPdfKingView.tsx +11 -0
- package/src/RnPdfKingView.web.tsx +15 -0
- package/src/ZoomableList.tsx +438 -0
- package/src/ZoomablePage.tsx +31 -0
- package/src/ZoomablePdfPage.tsx +34 -0
- package/src/index.ts +9 -0
- package/src/zoom/constants.ts +40 -0
- package/src/zoom/index.tsx +1267 -0
- package/src/zoom/utils.ts +96 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useMemo,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
StyleProp,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
View,
|
|
12
|
+
LayoutChangeEvent,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
Text,
|
|
15
|
+
} from "react-native";
|
|
16
|
+
import { GestureDetector, Gesture } from "react-native-gesture-handler";
|
|
17
|
+
import Animated, {
|
|
18
|
+
runOnJS,
|
|
19
|
+
useAnimatedRef,
|
|
20
|
+
useAnimatedStyle,
|
|
21
|
+
useDerivedValue,
|
|
22
|
+
useSharedValue,
|
|
23
|
+
scrollTo,
|
|
24
|
+
withTiming,
|
|
25
|
+
withDelay,
|
|
26
|
+
} from "react-native-reanimated";
|
|
27
|
+
import { useZoomGesture, UseZoomGestureProps } from "./zoom/index";
|
|
28
|
+
import {
|
|
29
|
+
FlashList,
|
|
30
|
+
FlashListProps,
|
|
31
|
+
ListRenderItemInfo,
|
|
32
|
+
} from "@shopify/flash-list";
|
|
33
|
+
|
|
34
|
+
const AnimatedFlashList = Animated.createAnimatedComponent(FlashList as any) as any;
|
|
35
|
+
|
|
36
|
+
const PAGE_SLIDER_PADDING_Y = 16;
|
|
37
|
+
const PAGE_SLIDER_THUMB_SIZE = 28;
|
|
38
|
+
|
|
39
|
+
// Context for ZoomableList state
|
|
40
|
+
interface ZoomableListContextType {
|
|
41
|
+
isZoomed: boolean;
|
|
42
|
+
isPanning: boolean;
|
|
43
|
+
isPinching: boolean;
|
|
44
|
+
width: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ZoomableListContext = createContext<ZoomableListContextType | undefined>(
|
|
48
|
+
undefined,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const useZoomableList = () => {
|
|
52
|
+
const context = useContext(ZoomableListContext);
|
|
53
|
+
if (!context) {
|
|
54
|
+
throw new Error("useZoomableList must be used within a ZoomableList");
|
|
55
|
+
}
|
|
56
|
+
return context;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export interface ZoomableListProps<T> extends Omit<
|
|
60
|
+
FlashListProps<T>,
|
|
61
|
+
"renderItem"
|
|
62
|
+
> {
|
|
63
|
+
renderItem: (
|
|
64
|
+
info: ListRenderItemInfo<T> & { width: number },
|
|
65
|
+
) => React.ReactElement | null;
|
|
66
|
+
style?: StyleProp<ViewStyle>;
|
|
67
|
+
zoomProps?: Omit<UseZoomGestureProps, "parentAnimatedScrollRef">;
|
|
68
|
+
onZoomChange?: (scale: number) => void;
|
|
69
|
+
onZoomStateChange?: (isZoomed: boolean) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Google Drive-style page scrubber on the right side.
|
|
72
|
+
* Works best for "page" lists where each item is a page.
|
|
73
|
+
*/
|
|
74
|
+
pageSliderEnabled?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Label renderer for the bubble. Defaults to "current/total".
|
|
77
|
+
* current is 1-based.
|
|
78
|
+
*/
|
|
79
|
+
pageSliderLabel?: (current: number, total: number) => string;
|
|
80
|
+
/**
|
|
81
|
+
* Logo/Icon for the slider thumb.
|
|
82
|
+
*/
|
|
83
|
+
pageSliderLogo?: React.ReactNode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function ZoomableList<T>(props: ZoomableListProps<T>) {
|
|
87
|
+
const {
|
|
88
|
+
style,
|
|
89
|
+
zoomProps,
|
|
90
|
+
onZoomChange,
|
|
91
|
+
onZoomStateChange,
|
|
92
|
+
renderItem,
|
|
93
|
+
pageSliderEnabled = false,
|
|
94
|
+
pageSliderLabel,
|
|
95
|
+
pageSliderLogo,
|
|
96
|
+
...flashListProps
|
|
97
|
+
} = props;
|
|
98
|
+
|
|
99
|
+
const [width, setWidth] = useState(0);
|
|
100
|
+
const [isZoomed, setIsZoomed] = useState(false);
|
|
101
|
+
const [isPanning, setIsPanning] = useState(false);
|
|
102
|
+
const [isPinching, setIsPinching] = useState(false);
|
|
103
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
104
|
+
const [isScrubbing, setIsScrubbing] = useState(false);
|
|
105
|
+
const [scrubIndex, setScrubIndex] = useState(0);
|
|
106
|
+
const [trackHeight, setTrackHeight] = useState(0);
|
|
107
|
+
const [hasScrolled, setHasScrolled] = useState(false);
|
|
108
|
+
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
const listRef = useAnimatedRef<FlashList<T>>();
|
|
111
|
+
|
|
112
|
+
const {
|
|
113
|
+
zoomGesture,
|
|
114
|
+
contentContainerAnimatedStyle,
|
|
115
|
+
onLayout,
|
|
116
|
+
onLayoutContent,
|
|
117
|
+
onScroll,
|
|
118
|
+
} = useZoomGesture({
|
|
119
|
+
disableVerticalPan: true,
|
|
120
|
+
parentAnimatedScrollRef: listRef,
|
|
121
|
+
...zoomProps,
|
|
122
|
+
onPanningStarted: () => {
|
|
123
|
+
setIsPanning(true);
|
|
124
|
+
zoomProps?.onPanningStarted?.();
|
|
125
|
+
},
|
|
126
|
+
onPanningEnd: () => {
|
|
127
|
+
setIsPanning(false);
|
|
128
|
+
zoomProps?.onPanningEnd?.();
|
|
129
|
+
},
|
|
130
|
+
onPinchingStarted: () => {
|
|
131
|
+
setIsPinching(true);
|
|
132
|
+
zoomProps?.onPinchingStarted?.();
|
|
133
|
+
},
|
|
134
|
+
onPinchingStopped: () => {
|
|
135
|
+
setIsPinching(false);
|
|
136
|
+
zoomProps?.onPinchingStopped?.();
|
|
137
|
+
},
|
|
138
|
+
onZoomChange: (s: number) => {
|
|
139
|
+
const newIsZoomed = s > 1.05; // Small buffer
|
|
140
|
+
if (newIsZoomed !== isZoomed) {
|
|
141
|
+
setIsZoomed(newIsZoomed);
|
|
142
|
+
onZoomStateChange?.(newIsZoomed);
|
|
143
|
+
}
|
|
144
|
+
onZoomChange?.(s);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const handleLayout = (e: LayoutChangeEvent) => {
|
|
149
|
+
if (e.nativeEvent.layout.width !== width) {
|
|
150
|
+
setWidth(e.nativeEvent.layout.width);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const totalCount = flashListProps.data?.length ?? 0;
|
|
155
|
+
const sliderEnabled = pageSliderEnabled && totalCount > 1;
|
|
156
|
+
const bubbleText = (
|
|
157
|
+
pageSliderLabel ?? ((cur: number, total: number) => `${cur}/${total}`)
|
|
158
|
+
)((isScrubbing ? scrubIndex : currentIndex) + 1, totalCount);
|
|
159
|
+
|
|
160
|
+
const onViewableItemsChanged = useRef(
|
|
161
|
+
({ viewableItems }: { viewableItems: Array<{ index: number | null }> }) => {
|
|
162
|
+
const first = viewableItems?.find(
|
|
163
|
+
(v) => typeof v.index === "number" && v.index !== null,
|
|
164
|
+
);
|
|
165
|
+
if (first?.index != null) setCurrentIndex(first.index);
|
|
166
|
+
},
|
|
167
|
+
).current;
|
|
168
|
+
|
|
169
|
+
const viewabilityConfig = useMemo(
|
|
170
|
+
() => ({
|
|
171
|
+
itemVisiblePercentThreshold: 55,
|
|
172
|
+
minimumViewTime: 60,
|
|
173
|
+
}),
|
|
174
|
+
[],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const thumbY = useSharedValue(0);
|
|
178
|
+
const bubbleOpacity = useSharedValue(0);
|
|
179
|
+
const contentHeight = useSharedValue(0);
|
|
180
|
+
const viewportHeight = useSharedValue(0);
|
|
181
|
+
|
|
182
|
+
const handleScroll = (e: any) => {
|
|
183
|
+
// Keep zoom gesture bookkeeping intact
|
|
184
|
+
onScroll?.(e);
|
|
185
|
+
// Preserve consumer scroll handler if provided
|
|
186
|
+
(flashListProps as any).onScroll?.(e);
|
|
187
|
+
|
|
188
|
+
const ne = e?.nativeEvent;
|
|
189
|
+
if (ne) {
|
|
190
|
+
contentHeight.value = ne.contentSize?.height ?? 0;
|
|
191
|
+
viewportHeight.value = ne.layoutMeasurement?.height ?? 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!sliderEnabled || trackHeight <= 0) return;
|
|
195
|
+
|
|
196
|
+
if (!isScrubbing) {
|
|
197
|
+
// Show bubble while panning/scrolling and hide after 1s of inactivity
|
|
198
|
+
bubbleOpacity.value = 1;
|
|
199
|
+
bubbleOpacity.value = withDelay(1000, withTiming(0, { duration: 300 }));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const offsetY = ne?.contentOffset?.y ?? 0;
|
|
203
|
+
const contentH = ne?.contentSize?.height ?? 0;
|
|
204
|
+
const viewportH = ne?.layoutMeasurement?.height ?? 0;
|
|
205
|
+
const scrollable = Math.max(1, contentH - viewportH);
|
|
206
|
+
const ratio = Math.max(0, Math.min(1, offsetY / scrollable));
|
|
207
|
+
thumbY.value = ratio * trackHeight;
|
|
208
|
+
if (!hasScrolled) setHasScrolled(true);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
useDerivedValue(() => {
|
|
212
|
+
if (
|
|
213
|
+
!sliderEnabled ||
|
|
214
|
+
isScrubbing ||
|
|
215
|
+
hasScrolled ||
|
|
216
|
+
trackHeight <= 0 ||
|
|
217
|
+
totalCount <= 1
|
|
218
|
+
)
|
|
219
|
+
return;
|
|
220
|
+
const ratio = currentIndex / (totalCount - 1);
|
|
221
|
+
thumbY.value = ratio * trackHeight;
|
|
222
|
+
}, [
|
|
223
|
+
currentIndex,
|
|
224
|
+
isScrubbing,
|
|
225
|
+
sliderEnabled,
|
|
226
|
+
totalCount,
|
|
227
|
+
trackHeight,
|
|
228
|
+
hasScrolled,
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const updateScrubFromY = (y: number) => {
|
|
232
|
+
if (!sliderEnabled || trackHeight <= 0) return;
|
|
233
|
+
// Gesture y is relative to the track view; normalize into usable range.
|
|
234
|
+
const normalized = y - PAGE_SLIDER_PADDING_Y;
|
|
235
|
+
const clamped = Math.max(0, Math.min(trackHeight, normalized));
|
|
236
|
+
const ratio = trackHeight > 0 ? clamped / trackHeight : 0;
|
|
237
|
+
const idx = Math.max(
|
|
238
|
+
0,
|
|
239
|
+
Math.min(totalCount - 1, Math.round(ratio * (totalCount - 1))),
|
|
240
|
+
);
|
|
241
|
+
setScrubIndex(idx);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const panGesture = useMemo(() => {
|
|
245
|
+
return Gesture.Pan()
|
|
246
|
+
.manualActivation(true)
|
|
247
|
+
.onTouchesDown((_e, manager) => {
|
|
248
|
+
manager.activate();
|
|
249
|
+
})
|
|
250
|
+
.enabled(sliderEnabled && !isZoomed && !isPanning && !isPinching)
|
|
251
|
+
.onBegin(() => {
|
|
252
|
+
bubbleOpacity.value = 1;
|
|
253
|
+
runOnJS(setIsScrubbing)(true);
|
|
254
|
+
})
|
|
255
|
+
.onUpdate((e) => {
|
|
256
|
+
// keep thumbY in the same normalized coordinate space as trackHeight
|
|
257
|
+
const y = Math.max(
|
|
258
|
+
0,
|
|
259
|
+
Math.min(trackHeight, e.y - PAGE_SLIDER_PADDING_Y),
|
|
260
|
+
);
|
|
261
|
+
thumbY.value = y;
|
|
262
|
+
|
|
263
|
+
if (trackHeight > 0) {
|
|
264
|
+
const ratio = y / trackHeight;
|
|
265
|
+
const scrollableHeight = contentHeight.value - viewportHeight.value;
|
|
266
|
+
if (scrollableHeight > 0) {
|
|
267
|
+
scrollTo(listRef, 0, ratio * scrollableHeight, true);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
runOnJS(updateScrubFromY)(e.y);
|
|
272
|
+
})
|
|
273
|
+
.onEnd(() => {
|
|
274
|
+
bubbleOpacity.value = withTiming(0, { duration: 300 });
|
|
275
|
+
runOnJS(setIsScrubbing)(false);
|
|
276
|
+
})
|
|
277
|
+
.onFinalize(() => {
|
|
278
|
+
bubbleOpacity.value = withTiming(0, { duration: 300 });
|
|
279
|
+
runOnJS(setIsScrubbing)(false);
|
|
280
|
+
});
|
|
281
|
+
}, [
|
|
282
|
+
bubbleOpacity,
|
|
283
|
+
isPanning,
|
|
284
|
+
isPinching,
|
|
285
|
+
isZoomed,
|
|
286
|
+
sliderEnabled,
|
|
287
|
+
thumbY,
|
|
288
|
+
trackHeight,
|
|
289
|
+
totalCount,
|
|
290
|
+
contentHeight,
|
|
291
|
+
viewportHeight,
|
|
292
|
+
listRef,
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
296
|
+
const y =
|
|
297
|
+
trackHeight > 0 ? Math.max(0, Math.min(trackHeight, thumbY.value)) : 0;
|
|
298
|
+
return {
|
|
299
|
+
transform: [{ translateY: y + PAGE_SLIDER_PADDING_Y }],
|
|
300
|
+
};
|
|
301
|
+
}, [trackHeight]);
|
|
302
|
+
|
|
303
|
+
const bubbleStyle = useAnimatedStyle(() => {
|
|
304
|
+
const y =
|
|
305
|
+
trackHeight > 0 ? Math.max(0, Math.min(trackHeight, thumbY.value)) : 0;
|
|
306
|
+
return {
|
|
307
|
+
opacity: bubbleOpacity.value,
|
|
308
|
+
transform: [{ translateY: y + PAGE_SLIDER_PADDING_Y }],
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<ZoomableListContext.Provider
|
|
314
|
+
value={{ isZoomed, isPanning, isPinching, width }}
|
|
315
|
+
>
|
|
316
|
+
<View style={[styles.container, style]} onLayout={handleLayout}>
|
|
317
|
+
<GestureDetector gesture={zoomGesture}>
|
|
318
|
+
<View style={{ flex: 1 }} onLayout={onLayout} collapsable={false}>
|
|
319
|
+
<Animated.View
|
|
320
|
+
style={[contentContainerAnimatedStyle, { flex: 1 }]}
|
|
321
|
+
onLayout={onLayoutContent}
|
|
322
|
+
>
|
|
323
|
+
<AnimatedFlashList
|
|
324
|
+
ref={listRef}
|
|
325
|
+
scrollEnabled={!isZoomed && !isPanning && !isPinching}
|
|
326
|
+
scrollEventThrottle={16}
|
|
327
|
+
renderItem={(info: ListRenderItemInfo<T>) => renderItem({ ...info, width })}
|
|
328
|
+
{...flashListProps}
|
|
329
|
+
onScroll={handleScroll}
|
|
330
|
+
onViewableItemsChanged={
|
|
331
|
+
sliderEnabled
|
|
332
|
+
? (onViewableItemsChanged as any)
|
|
333
|
+
: flashListProps.onViewableItemsChanged
|
|
334
|
+
}
|
|
335
|
+
viewabilityConfig={
|
|
336
|
+
sliderEnabled
|
|
337
|
+
? (viewabilityConfig as any)
|
|
338
|
+
: flashListProps.viewabilityConfig
|
|
339
|
+
}
|
|
340
|
+
/>
|
|
341
|
+
</Animated.View>
|
|
342
|
+
</View>
|
|
343
|
+
</GestureDetector>
|
|
344
|
+
|
|
345
|
+
{sliderEnabled ? (
|
|
346
|
+
<View pointerEvents="box-none" style={styles.pageSliderOverlay}>
|
|
347
|
+
<View style={styles.pageSliderGestureContainer}>
|
|
348
|
+
<GestureDetector gesture={panGesture}>
|
|
349
|
+
<View
|
|
350
|
+
style={styles.pageSliderTrack}
|
|
351
|
+
collapsable={false}
|
|
352
|
+
onLayout={(e) => {
|
|
353
|
+
const h = e.nativeEvent.layout.height;
|
|
354
|
+
const usable = Math.max(
|
|
355
|
+
0,
|
|
356
|
+
h - PAGE_SLIDER_PADDING_Y * 2 - PAGE_SLIDER_THUMB_SIZE,
|
|
357
|
+
);
|
|
358
|
+
setTrackHeight(usable);
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
<Animated.View style={[styles.pageSliderBubble, bubbleStyle]}>
|
|
362
|
+
<Text style={styles.pageSliderBubbleText}>
|
|
363
|
+
{bubbleText}
|
|
364
|
+
</Text>
|
|
365
|
+
</Animated.View>
|
|
366
|
+
<Animated.View style={[styles.pageSliderThumb, thumbStyle]}>
|
|
367
|
+
{pageSliderLogo}
|
|
368
|
+
</Animated.View>
|
|
369
|
+
</View>
|
|
370
|
+
</GestureDetector>
|
|
371
|
+
</View>
|
|
372
|
+
</View>
|
|
373
|
+
) : null}
|
|
374
|
+
</View>
|
|
375
|
+
</ZoomableListContext.Provider>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const styles = StyleSheet.create({
|
|
380
|
+
container: {
|
|
381
|
+
flex: 1,
|
|
382
|
+
overflow: "hidden",
|
|
383
|
+
},
|
|
384
|
+
pageSliderOverlay: {
|
|
385
|
+
position: "absolute",
|
|
386
|
+
right: 0,
|
|
387
|
+
top: 0,
|
|
388
|
+
bottom: 0,
|
|
389
|
+
width: 10,
|
|
390
|
+
alignItems: "flex-end",
|
|
391
|
+
justifyContent: "flex-start",
|
|
392
|
+
},
|
|
393
|
+
pageSliderGestureContainer: {
|
|
394
|
+
flex: 1,
|
|
395
|
+
alignSelf: "stretch",
|
|
396
|
+
alignItems: "flex-end",
|
|
397
|
+
},
|
|
398
|
+
pageSliderTrack: {
|
|
399
|
+
flex: 1,
|
|
400
|
+
width: 10,
|
|
401
|
+
alignItems: "flex-end",
|
|
402
|
+
justifyContent: "flex-start",
|
|
403
|
+
paddingVertical: 16,
|
|
404
|
+
},
|
|
405
|
+
pageSliderThumb: {
|
|
406
|
+
position: "absolute",
|
|
407
|
+
right: 2,
|
|
408
|
+
width: 38,
|
|
409
|
+
height: 38,
|
|
410
|
+
borderRadius: 999,
|
|
411
|
+
backgroundColor: "rgba(255,255,255,0.92)",
|
|
412
|
+
borderWidth: 1,
|
|
413
|
+
borderColor: "rgba(0,0,0,0.12)",
|
|
414
|
+
shadowColor: "#000",
|
|
415
|
+
shadowOpacity: 0.15,
|
|
416
|
+
shadowRadius: 6,
|
|
417
|
+
shadowOffset: { width: 0, height: 2 },
|
|
418
|
+
elevation: 3,
|
|
419
|
+
alignItems: "center",
|
|
420
|
+
justifyContent: "center",
|
|
421
|
+
},
|
|
422
|
+
pageSliderBubble: {
|
|
423
|
+
position: "absolute",
|
|
424
|
+
right: 38,
|
|
425
|
+
minWidth: 80,
|
|
426
|
+
paddingHorizontal: 8,
|
|
427
|
+
paddingVertical: 10,
|
|
428
|
+
borderRadius: 999,
|
|
429
|
+
backgroundColor: "rgba(0,0,0,0.82)",
|
|
430
|
+
alignItems: "center",
|
|
431
|
+
justifyContent: "center",
|
|
432
|
+
},
|
|
433
|
+
pageSliderBubbleText: {
|
|
434
|
+
color: "#fff",
|
|
435
|
+
fontSize: 12,
|
|
436
|
+
fontWeight: "600",
|
|
437
|
+
},
|
|
438
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface ZoomablePageProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ZoomablePage: React.FC<ZoomablePageProps> = ({
|
|
12
|
+
children,
|
|
13
|
+
width,
|
|
14
|
+
height,
|
|
15
|
+
style,
|
|
16
|
+
}) => {
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
style={[
|
|
20
|
+
{
|
|
21
|
+
width,
|
|
22
|
+
height,
|
|
23
|
+
overflow: 'hidden',
|
|
24
|
+
},
|
|
25
|
+
style,
|
|
26
|
+
]}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { PdfPage, PdfPageProps } from './PdfPage';
|
|
4
|
+
import { ZoomablePage } from './ZoomablePage';
|
|
5
|
+
import { useZoomableList } from './ZoomableList';
|
|
6
|
+
|
|
7
|
+
export interface ZoomablePdfPageProps extends PdfPageProps {
|
|
8
|
+
width?: number;
|
|
9
|
+
height: number;
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ZoomablePdfPage: React.FC<ZoomablePdfPageProps> = (props) => {
|
|
14
|
+
const { width: propWidth, height, style, selectionEnabled, ...pdfProps } = props;
|
|
15
|
+
const { isPanning, isPinching, width: contextWidth } = useZoomableList();
|
|
16
|
+
|
|
17
|
+
const width = propWidth ?? contextWidth;
|
|
18
|
+
const isInteracting = isPanning || isPinching;
|
|
19
|
+
const shouldEnableSelection = selectionEnabled !== undefined
|
|
20
|
+
? (selectionEnabled && !isInteracting)
|
|
21
|
+
: !isInteracting;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ZoomablePage width={width} height={height} style={style}>
|
|
25
|
+
<PdfPage
|
|
26
|
+
width={width}
|
|
27
|
+
height={height}
|
|
28
|
+
style={{ width, height }}
|
|
29
|
+
selectionEnabled={shouldEnableSelection}
|
|
30
|
+
{...pdfProps}
|
|
31
|
+
/>
|
|
32
|
+
</ZoomablePage>
|
|
33
|
+
);
|
|
34
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Reexport the native module. On web, it will be resolved to RnPdfKingModule.web.ts
|
|
2
|
+
// and on native platforms to RnPdfKingModule.ts
|
|
3
|
+
export { default } from './RnPdfKingModule';
|
|
4
|
+
export * from './RnPdfKing.types';
|
|
5
|
+
export * from './PdfDocument';
|
|
6
|
+
export * from './PdfPage';
|
|
7
|
+
export * from './ZoomableList';
|
|
8
|
+
export * from './ZoomablePage';
|
|
9
|
+
export * from './ZoomablePdfPage';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default zoom scale constraints
|
|
3
|
+
* MAX_SCALE: maximum allowed zoom level
|
|
4
|
+
*/
|
|
5
|
+
export const MAX_SCALE = 4
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Double-tap zoom scale (Apple Photos uses 2x)
|
|
9
|
+
*/
|
|
10
|
+
export const DOUBLE_TAP_SCALE = 2
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Animation configuration constants
|
|
14
|
+
*/
|
|
15
|
+
export const ANIMATION_DURATION = 350
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gesture detection thresholds
|
|
19
|
+
*/
|
|
20
|
+
export const TAP_MAX_DELTA = 25
|
|
21
|
+
export const PAN_DEBOUNCE_MS = 10
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimum number of pointers for pan gesture
|
|
25
|
+
*/
|
|
26
|
+
export const MIN_PAN_POINTERS = 2
|
|
27
|
+
export const MAX_PAN_POINTERS = 2
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Grouped zoom configuration
|
|
31
|
+
*/
|
|
32
|
+
export const ZOOM_CONFIG = {
|
|
33
|
+
MAX_SCALE,
|
|
34
|
+
DOUBLE_TAP_SCALE,
|
|
35
|
+
ANIMATION_DURATION,
|
|
36
|
+
TAP_MAX_DELTA,
|
|
37
|
+
PAN_DEBOUNCE_MS,
|
|
38
|
+
MIN_PAN_POINTERS,
|
|
39
|
+
MAX_PAN_POINTERS,
|
|
40
|
+
} as const
|