react-native-optimized-pdf 1.0.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 +11 -0
- package/.prettierrc.js +10 -0
- package/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +91 -0
- package/README.md +302 -0
- package/ReactNativeOptimizedPdf.podspec +21 -0
- package/android/build.gradle +57 -0
- package/android/proguard-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/reactnativeoptimizedpdf/OptimizedPdfView.kt +499 -0
- package/android/src/main/kotlin/com/reactnativeoptimizedpdf/OptimizedPdfViewManager.kt +68 -0
- package/android/src/main/kotlin/com/reactnativeoptimizedpdf/OptimizedPdfViewPackage.kt +20 -0
- package/docs/android-setup.md +63 -0
- package/index.d.ts +15 -0
- package/index.ts +13 -0
- package/ios/OptimizedPdfView.swift +256 -0
- package/ios/OptimizedPdfViewManager.m +22 -0
- package/ios/OptimizedPdfViewManager.swift +24 -0
- package/ios/TiledPdfPageView.swift +76 -0
- package/package.json +61 -0
- package/src/OptimizedPdfView.tsx +167 -0
- package/src/components/PdfNavigationControls.tsx +123 -0
- package/src/components/PdfOverlays.tsx +46 -0
- package/src/constants.ts +13 -0
- package/src/index.ts +5 -0
- package/src/services/pdfCache.ts +113 -0
- package/src/types/index.ts +112 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
const { View, Text, TouchableOpacity, TextInput, StyleSheet } = require('react-native');
|
|
3
|
+
import type { PdfNavigationControlsProps } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Navigation controls for PDF pages
|
|
7
|
+
* Provides previous/next buttons and page number input
|
|
8
|
+
*/
|
|
9
|
+
export const PdfNavigationControls: React.FC<PdfNavigationControlsProps> = ({
|
|
10
|
+
currentPage,
|
|
11
|
+
totalPages,
|
|
12
|
+
onNextPage,
|
|
13
|
+
onPrevPage,
|
|
14
|
+
onPageChange,
|
|
15
|
+
style,
|
|
16
|
+
}) => {
|
|
17
|
+
const [inputPage, setInputPage] = useState((currentPage + 1).toString());
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setInputPage((currentPage + 1).toString());
|
|
21
|
+
}, [currentPage]);
|
|
22
|
+
|
|
23
|
+
const handleChangePageInput = (text: string) => {
|
|
24
|
+
setInputPage(text);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleEndEditingPageInput = () => {
|
|
28
|
+
const pageNumber = parseInt(inputPage, 10);
|
|
29
|
+
if (!isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= totalPages) {
|
|
30
|
+
onPageChange(pageNumber - 1);
|
|
31
|
+
} else {
|
|
32
|
+
// Reset to current page if invalid
|
|
33
|
+
setInputPage((currentPage + 1).toString());
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isFirstPage = currentPage === 0;
|
|
38
|
+
const isLastPage = currentPage === totalPages - 1;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View style={[styles.container, style]}>
|
|
42
|
+
<TouchableOpacity
|
|
43
|
+
onPress={onPrevPage}
|
|
44
|
+
disabled={isFirstPage}
|
|
45
|
+
style={[styles.navButton, isFirstPage && styles.disabledButton]}
|
|
46
|
+
accessibilityLabel="Previous page"
|
|
47
|
+
accessibilityRole="button"
|
|
48
|
+
>
|
|
49
|
+
<Text style={styles.navButtonText}>{'<'}</Text>
|
|
50
|
+
</TouchableOpacity>
|
|
51
|
+
|
|
52
|
+
<TextInput
|
|
53
|
+
style={styles.pageInput}
|
|
54
|
+
value={inputPage}
|
|
55
|
+
keyboardType="number-pad"
|
|
56
|
+
returnKeyType="done"
|
|
57
|
+
onChangeText={handleChangePageInput}
|
|
58
|
+
onEndEditing={handleEndEditingPageInput}
|
|
59
|
+
accessibilityLabel={`Current page: ${currentPage + 1} of ${totalPages}`}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<Text style={styles.pageInfo}>/ {totalPages}</Text>
|
|
63
|
+
|
|
64
|
+
<TouchableOpacity
|
|
65
|
+
onPress={onNextPage}
|
|
66
|
+
disabled={isLastPage}
|
|
67
|
+
style={[styles.navButton, isLastPage && styles.disabledButton]}
|
|
68
|
+
accessibilityLabel="Next page"
|
|
69
|
+
accessibilityRole="button"
|
|
70
|
+
>
|
|
71
|
+
<Text style={styles.navButtonText}>{'>'}</Text>
|
|
72
|
+
</TouchableOpacity>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const styles = StyleSheet.create({
|
|
78
|
+
container: {
|
|
79
|
+
position: 'absolute',
|
|
80
|
+
bottom: 20,
|
|
81
|
+
left: 0,
|
|
82
|
+
right: 0,
|
|
83
|
+
flexDirection: 'row',
|
|
84
|
+
justifyContent: 'center',
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
},
|
|
87
|
+
navButton: {
|
|
88
|
+
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
89
|
+
padding: 10,
|
|
90
|
+
marginHorizontal: 20,
|
|
91
|
+
borderRadius: 8,
|
|
92
|
+
minWidth: 44,
|
|
93
|
+
minHeight: 44,
|
|
94
|
+
justifyContent: 'center',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
},
|
|
97
|
+
navButtonText: {
|
|
98
|
+
color: '#fff',
|
|
99
|
+
fontSize: 20,
|
|
100
|
+
fontWeight: '600',
|
|
101
|
+
},
|
|
102
|
+
disabledButton: {
|
|
103
|
+
opacity: 0.4,
|
|
104
|
+
},
|
|
105
|
+
pageInfo: {
|
|
106
|
+
color: '#000',
|
|
107
|
+
fontSize: 16,
|
|
108
|
+
fontWeight: '500',
|
|
109
|
+
},
|
|
110
|
+
pageInput: {
|
|
111
|
+
width: 50,
|
|
112
|
+
height: 40,
|
|
113
|
+
borderWidth: 1,
|
|
114
|
+
borderColor: '#000',
|
|
115
|
+
borderRadius: 5,
|
|
116
|
+
textAlign: 'center',
|
|
117
|
+
color: '#000',
|
|
118
|
+
marginHorizontal: 10,
|
|
119
|
+
backgroundColor: '#fff',
|
|
120
|
+
fontWeight: 'bold',
|
|
121
|
+
fontSize: 16,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
const { View, Text, ActivityIndicator, StyleSheet } = require('react-native');
|
|
3
|
+
import type { PdfLoadingOverlayProps, PdfErrorOverlayProps } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loading overlay with progress indicator
|
|
7
|
+
*/
|
|
8
|
+
export const PdfLoadingOverlay: React.FC<PdfLoadingOverlayProps> = ({ progress, style }) => {
|
|
9
|
+
return (
|
|
10
|
+
<View style={[styles.container, style]}>
|
|
11
|
+
<ActivityIndicator size="large" color="#fff" />
|
|
12
|
+
<Text style={styles.progressText}>{progress}%</Text>
|
|
13
|
+
</View>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Error overlay for displaying error messages
|
|
19
|
+
*/
|
|
20
|
+
export const PdfErrorOverlay: React.FC<PdfErrorOverlayProps> = ({ error, style }) => {
|
|
21
|
+
return (
|
|
22
|
+
<View style={[styles.container, style]}>
|
|
23
|
+
<Text style={styles.errorText}>{error}</Text>
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const styles = StyleSheet.create({
|
|
29
|
+
container: {
|
|
30
|
+
flex: 1,
|
|
31
|
+
justifyContent: 'center',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
padding: 20,
|
|
34
|
+
},
|
|
35
|
+
progressText: {
|
|
36
|
+
color: '#fff',
|
|
37
|
+
marginTop: 10,
|
|
38
|
+
fontSize: 16,
|
|
39
|
+
fontWeight: '600',
|
|
40
|
+
},
|
|
41
|
+
errorText: {
|
|
42
|
+
color: '#d32f2f',
|
|
43
|
+
fontSize: 16,
|
|
44
|
+
textAlign: 'center',
|
|
45
|
+
},
|
|
46
|
+
});
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default values and constants for the PDF viewer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_MAXIMUM_ZOOM = 3;
|
|
6
|
+
export const DEFAULT_ENABLE_ANTIALIASING = true;
|
|
7
|
+
export const DEFAULT_SHOW_NAVIGATION_CONTROLS = true;
|
|
8
|
+
|
|
9
|
+
export const ERROR_MESSAGES = {
|
|
10
|
+
DOWNLOAD_FAILED: 'Failed to download PDF',
|
|
11
|
+
LOAD_FAILED: 'Failed to load PDF',
|
|
12
|
+
INVALID_SOURCE: 'Invalid PDF source',
|
|
13
|
+
} as const;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default } from './OptimizedPdfView';
|
|
2
|
+
export { PdfCacheService } from './services/pdfCache';
|
|
3
|
+
export { PdfNavigationControls } from './components/PdfNavigationControls';
|
|
4
|
+
export { PdfLoadingOverlay, PdfErrorOverlay } from './components/PdfOverlays';
|
|
5
|
+
export * from './types';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import RNFS from 'react-native-fs';
|
|
2
|
+
import md5 from 'crypto-js/md5';
|
|
3
|
+
import type { PdfSource } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Service for handling PDF caching and downloads
|
|
7
|
+
*/
|
|
8
|
+
export class PdfCacheService {
|
|
9
|
+
/**
|
|
10
|
+
* Get the local file path for a cached PDF
|
|
11
|
+
*/
|
|
12
|
+
static getCacheFilePath(source: PdfSource): string {
|
|
13
|
+
const fileName = source.cacheFileName || `${md5(source.uri).toString()}.pdf`;
|
|
14
|
+
return `${RNFS.CachesDirectoryPath}/${fileName}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if a cached PDF exists and is still valid
|
|
19
|
+
*/
|
|
20
|
+
static async isCacheValid(source: PdfSource): Promise<boolean> {
|
|
21
|
+
const localPath = this.getCacheFilePath(source);
|
|
22
|
+
const exists = await RNFS.exists(localPath);
|
|
23
|
+
|
|
24
|
+
if (!exists) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If expiration is not set, cache is always valid
|
|
29
|
+
if (!source.expiration || source.expiration <= 0) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if cache has expired
|
|
34
|
+
const stat = await RNFS.stat(localPath);
|
|
35
|
+
const now = Date.now() / 1000;
|
|
36
|
+
return now - stat.mtime < source.expiration;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Download a PDF file and cache it locally
|
|
41
|
+
* @param source PDF source configuration
|
|
42
|
+
* @param onProgress Optional callback for download progress
|
|
43
|
+
* @returns Local file path
|
|
44
|
+
*/
|
|
45
|
+
static async downloadPdf(
|
|
46
|
+
source: PdfSource,
|
|
47
|
+
onProgress?: (percent: number) => void,
|
|
48
|
+
): Promise<string> {
|
|
49
|
+
const localPath = this.getCacheFilePath(source);
|
|
50
|
+
|
|
51
|
+
// Check if we can use cached version
|
|
52
|
+
if (source.cache !== false && (await this.isCacheValid(source))) {
|
|
53
|
+
return localPath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Download the file
|
|
57
|
+
const { promise } = RNFS.downloadFile({
|
|
58
|
+
fromUrl: source.uri,
|
|
59
|
+
toFile: localPath,
|
|
60
|
+
background: false,
|
|
61
|
+
headers: source.headers,
|
|
62
|
+
progressDivider: 1,
|
|
63
|
+
progress: (res) => {
|
|
64
|
+
if (res.contentLength > 0 && onProgress) {
|
|
65
|
+
const percent = Math.floor((res.bytesWritten / res.contentLength) * 100);
|
|
66
|
+
onProgress(percent);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
begin: () => {
|
|
70
|
+
// This callback is required for progress to update properly
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await promise;
|
|
75
|
+
return localPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Clear a specific cached PDF
|
|
80
|
+
*/
|
|
81
|
+
static async clearCache(source: PdfSource): Promise<void> {
|
|
82
|
+
const localPath = this.getCacheFilePath(source);
|
|
83
|
+
const exists = await RNFS.exists(localPath);
|
|
84
|
+
if (exists) {
|
|
85
|
+
await RNFS.unlink(localPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clear all cached PDFs
|
|
91
|
+
*/
|
|
92
|
+
static async clearAllCache(): Promise<void> {
|
|
93
|
+
const files = await RNFS.readDir(RNFS.CachesDirectoryPath);
|
|
94
|
+
const pdfFiles = files.filter((file) => file.name.endsWith('.pdf'));
|
|
95
|
+
await Promise.all(pdfFiles.map((file) => RNFS.unlink(file.path)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get total size of cached PDFs in bytes
|
|
100
|
+
*/
|
|
101
|
+
static async getCacheSize(): Promise<number> {
|
|
102
|
+
const files = await RNFS.readDir(RNFS.CachesDirectoryPath);
|
|
103
|
+
const pdfFiles = files.filter((file) => file.name.endsWith('.pdf'));
|
|
104
|
+
return pdfFiles.reduce((total, file) => total + file.size, 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ensure the file path has the file:// protocol
|
|
109
|
+
*/
|
|
110
|
+
static normalizeFilePath(path: string): string {
|
|
111
|
+
return path.startsWith('file://') ? path : `file://${path}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for PDF source
|
|
5
|
+
*/
|
|
6
|
+
export interface PdfSource {
|
|
7
|
+
/** URI of the PDF file (remote URL or local file path) */
|
|
8
|
+
uri: string;
|
|
9
|
+
/** Whether to cache the PDF file locally. Default: true */
|
|
10
|
+
cache?: boolean;
|
|
11
|
+
/** Custom filename for the cached file. If not provided, uses MD5 hash of URI */
|
|
12
|
+
cacheFileName?: string;
|
|
13
|
+
/** Cache expiration time in seconds. If not set, cache never expires */
|
|
14
|
+
expiration?: number;
|
|
15
|
+
/** HTTP method for download. Default: 'GET' */
|
|
16
|
+
method?: string;
|
|
17
|
+
/** HTTP headers for download request */
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Page dimensions returned by onLoadComplete event
|
|
23
|
+
*/
|
|
24
|
+
export interface PdfPageDimensions {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Error event from native module
|
|
31
|
+
*/
|
|
32
|
+
export interface PdfErrorEvent {
|
|
33
|
+
nativeEvent: {
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Props for OptimizedPdfView component
|
|
40
|
+
*/
|
|
41
|
+
export interface OptimizedPdfViewProps {
|
|
42
|
+
/** PDF source configuration */
|
|
43
|
+
source: PdfSource;
|
|
44
|
+
/** Password for encrypted PDF files */
|
|
45
|
+
password?: string;
|
|
46
|
+
/** Maximum zoom level. Default: 3 */
|
|
47
|
+
maximumZoom?: number;
|
|
48
|
+
/** Enable antialiasing for better rendering quality. Default: true */
|
|
49
|
+
enableAntialiasing?: boolean;
|
|
50
|
+
/** Show built-in navigation controls. Default: true */
|
|
51
|
+
showNavigationControls?: boolean;
|
|
52
|
+
/** Custom style for the container */
|
|
53
|
+
style?: ViewStyle;
|
|
54
|
+
/** Callback when PDF is loaded successfully */
|
|
55
|
+
onLoadComplete?: (currentPage: number, dimensions: PdfPageDimensions) => void;
|
|
56
|
+
/** Callback when an error occurs */
|
|
57
|
+
onError?: (error: PdfErrorEvent) => void;
|
|
58
|
+
/** Callback when page count is available */
|
|
59
|
+
onPageCount?: (numberOfPages: number) => void;
|
|
60
|
+
/** Callback when page changes */
|
|
61
|
+
onPageChange?: (currentPage: number) => void;
|
|
62
|
+
/** Callback when PDF requires a password */
|
|
63
|
+
onPasswordRequired?: () => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Native event for load complete
|
|
68
|
+
*/
|
|
69
|
+
export interface NativeLoadCompleteEvent {
|
|
70
|
+
nativeEvent: {
|
|
71
|
+
currentPage: number;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Native event for page count
|
|
79
|
+
*/
|
|
80
|
+
export interface NativePageCountEvent {
|
|
81
|
+
nativeEvent: {
|
|
82
|
+
numberOfPages: number;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Props for navigation controls component
|
|
88
|
+
*/
|
|
89
|
+
export interface PdfNavigationControlsProps {
|
|
90
|
+
currentPage: number;
|
|
91
|
+
totalPages: number;
|
|
92
|
+
onNextPage: () => void;
|
|
93
|
+
onPrevPage: () => void;
|
|
94
|
+
onPageChange: (page: number) => void;
|
|
95
|
+
style?: ViewStyle;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Props for loading overlay component
|
|
100
|
+
*/
|
|
101
|
+
export interface PdfLoadingOverlayProps {
|
|
102
|
+
progress: number;
|
|
103
|
+
style?: ViewStyle;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Props for error overlay component
|
|
108
|
+
*/
|
|
109
|
+
export interface PdfErrorOverlayProps {
|
|
110
|
+
error: string;
|
|
111
|
+
style?: ViewStyle;
|
|
112
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2017",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["es6"],
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"jsx": "react-native",
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true
|
|
15
|
+
},
|
|
16
|
+
"exclude": ["node_modules", "**/__tests__/**"]
|
|
17
|
+
}
|