shareing and pdf format
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -7,11 +7,13 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "LensApp",
|
"name": "LensApp",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.73.0",
|
"react-native": "0.73.0",
|
||||||
"react-native-document-scanner-plugin": "^2.0.4",
|
"react-native-document-scanner-plugin": "^2.0.4",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
|
"react-native-html-to-pdf": "^1.3.0",
|
||||||
"react-native-image-crop-picker": "^0.51.1",
|
"react-native-image-crop-picker": "^0.51.1",
|
||||||
"react-native-share": "^12.2.6"
|
"react-native-share": "^12.2.6"
|
||||||
},
|
},
|
||||||
@@ -10495,6 +10497,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-html-to-pdf": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-html-to-pdf/-/react-native-html-to-pdf-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-UCbQO1k5yeajaJrEF7Wqq8ojdgpxsTbICQvtorXmuO1LkU/a6B3/tZyLes4zXCiq/5nTMTNA/k9m5bYStg5S0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"example"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-image-crop-picker": {
|
"node_modules/react-native-image-crop-picker": {
|
||||||
"version": "0.51.1",
|
"version": "0.51.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.51.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.51.1.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"react-native": "0.73.0",
|
"react-native": "0.73.0",
|
||||||
"react-native-document-scanner-plugin": "^2.0.4",
|
"react-native-document-scanner-plugin": "^2.0.4",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
|
"react-native-html-to-pdf": "^1.3.0",
|
||||||
"react-native-image-crop-picker": "^0.51.1",
|
"react-native-image-crop-picker": "^0.51.1",
|
||||||
"react-native-share": "^12.2.6"
|
"react-native-share": "^12.2.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Share,
|
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Alert,
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import Share from 'react-native-share';
|
||||||
import {ScannedDocument} from '../types';
|
import {ScannedDocument} from '../types';
|
||||||
|
import {generatePdf} from '../utils/generatePdf';
|
||||||
|
|
||||||
const {width: SW} = Dimensions.get('window');
|
const {width: SW} = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -24,30 +26,33 @@ interface Props {
|
|||||||
|
|
||||||
export default function ViewerScreen({document, onBack, onDelete, onRename}: Props) {
|
export default function ViewerScreen({document, onBack, onDelete, onRename}: Props) {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
|
||||||
const shareImage = async () => {
|
const sharePdf = async (pageIndices?: number[]) => {
|
||||||
const page = document.pages[currentIndex];
|
setGenerating(true);
|
||||||
if (!page) return;
|
|
||||||
try {
|
try {
|
||||||
await Share.share({
|
const pages =
|
||||||
title: document.name,
|
pageIndices
|
||||||
url: page.uri,
|
? pageIndices.map(i => document.pages[i]).filter(Boolean)
|
||||||
message: `${document.name} — page ${currentIndex + 1}`,
|
: document.pages;
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// user cancelled
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const shareAll = async () => {
|
const label =
|
||||||
try {
|
pageIndices?.length === 1
|
||||||
await Share.share({
|
? `${document.name} — page ${pageIndices[0] + 1}`
|
||||||
title: document.name,
|
: document.name;
|
||||||
message: `${document.name}: ${document.pages.length} pages`,
|
|
||||||
url: document.pages[0]?.uri,
|
const pdfPath = await generatePdf(pages, label);
|
||||||
|
|
||||||
|
await Share.open({
|
||||||
|
title: label,
|
||||||
|
type: 'application/pdf',
|
||||||
|
url: `file://${pdfPath}`,
|
||||||
|
failOnCancel: false,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
// user cancelled
|
Alert.alert('Share failed', err?.message ?? 'Could not generate PDF.');
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,8 +77,11 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
|||||||
{currentIndex + 1} / {document.pages.length}
|
{currentIndex + 1} / {document.pages.length}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={shareAll} style={styles.headerBtn}>
|
<TouchableOpacity
|
||||||
<Text style={styles.shareText}>Share</Text>
|
onPress={() => sharePdf()}
|
||||||
|
style={styles.headerBtn}
|
||||||
|
disabled={generating}>
|
||||||
|
<Text style={styles.shareText}>Share PDF</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -106,10 +114,20 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.toolbar}>
|
<View style={styles.toolbar}>
|
||||||
<TouchableOpacity style={styles.toolBtn} onPress={shareImage}>
|
<TouchableOpacity
|
||||||
|
style={styles.toolBtn}
|
||||||
|
onPress={() => sharePdf([currentIndex])}
|
||||||
|
disabled={generating}>
|
||||||
<Text style={styles.toolIcon}>⬆</Text>
|
<Text style={styles.toolIcon}>⬆</Text>
|
||||||
<Text style={styles.toolLabel}>Share Page</Text>
|
<Text style={styles.toolLabel}>Share Page</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.toolBtn}
|
||||||
|
onPress={() => sharePdf()}
|
||||||
|
disabled={generating}>
|
||||||
|
<Text style={styles.toolIcon}>📄</Text>
|
||||||
|
<Text style={styles.toolLabel}>Share All</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.toolBtn} onPress={() => onRename(document.name)}>
|
<TouchableOpacity style={styles.toolBtn} onPress={() => onRename(document.name)}>
|
||||||
<Text style={styles.toolIcon}>✎</Text>
|
<Text style={styles.toolIcon}>✎</Text>
|
||||||
<Text style={styles.toolLabel}>Rename</Text>
|
<Text style={styles.toolLabel}>Rename</Text>
|
||||||
@@ -119,6 +137,13 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
|||||||
<Text style={[styles.toolLabel, styles.toolLabelDanger]}>Delete</Text>
|
<Text style={[styles.toolLabel, styles.toolLabelDanger]}>Delete</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{generating && (
|
||||||
|
<View style={styles.loadingOverlay}>
|
||||||
|
<ActivityIndicator size="large" color="#fff" />
|
||||||
|
<Text style={styles.loadingText}>Generating PDF…</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -174,4 +199,12 @@ const styles = StyleSheet.create({
|
|||||||
toolIconDanger: {color: '#ff453a'},
|
toolIconDanger: {color: '#ff453a'},
|
||||||
toolLabel: {fontSize: 11, color: '#007AFF'},
|
toolLabel: {fontSize: 11, color: '#007AFF'},
|
||||||
toolLabelDanger: {color: '#ff453a'},
|
toolLabelDanger: {color: '#ff453a'},
|
||||||
|
loadingOverlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.65)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
loadingText: {color: '#fff', fontSize: 15},
|
||||||
});
|
});
|
||||||
|
|||||||
60
src/utils/generatePdf.ts
Normal file
60
src/utils/generatePdf.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {generatePDF as rnGeneratePDF} from 'react-native-html-to-pdf';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import {ScannedPage} from '../types';
|
||||||
|
|
||||||
|
const PDF_DIR = `${RNFS.DocumentDirectoryPath}/pdfs`;
|
||||||
|
|
||||||
|
async function pageToBase64(uri: string): Promise<string> {
|
||||||
|
const path = uri.startsWith('file://') ? uri.slice(7) : uri;
|
||||||
|
return RNFS.readFile(path, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generatePdf(
|
||||||
|
pages: ScannedPage[],
|
||||||
|
name: string,
|
||||||
|
): Promise<string> {
|
||||||
|
await RNFS.mkdir(PDF_DIR);
|
||||||
|
|
||||||
|
const imageHtml = await Promise.all(
|
||||||
|
pages.map(async p => {
|
||||||
|
const b64 = await pageToBase64(p.uri);
|
||||||
|
return `<div class="page"><img src="data:image/jpeg;base64,${b64}" /></div>`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { background: #fff; }
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
page-break-after: always;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.page:last-child { page-break-after: avoid; }
|
||||||
|
img { width: 100%; height: auto; display: block; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>${imageHtml.join('\n')}</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await rnGeneratePDF({
|
||||||
|
html,
|
||||||
|
fileName: name.replace(/[^a-z0-9_\-]/gi, '_'),
|
||||||
|
directory: PDF_DIR,
|
||||||
|
base64: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.filePath) {
|
||||||
|
throw new Error('PDF generation failed — no output path returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.filePath;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user