first commit
This commit is contained in:
254
src/App.tsx
Normal file
254
src/App.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Layout, { type ToolId } from './components/Layout';
|
||||
import FileLoader from './components/FileLoader';
|
||||
import PageList from './components/PageList';
|
||||
import ActionsPanel from './components/ActionsPanel';
|
||||
import ReorderPanel from './components/ReorderPanel';
|
||||
import type { PdfFile, SplitResult } from './pdf/pdfTypes';
|
||||
import {
|
||||
loadPdfFromFile,
|
||||
splitIntoSinglePages,
|
||||
extractRange,
|
||||
exportReordered,
|
||||
} from './pdf/pdfService';
|
||||
import {
|
||||
generateThumbnails,
|
||||
generateThumbnailsWithRotations,
|
||||
} from './pdf/pdfThumbnailService';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [activeTool, setActiveTool] = useState<ToolId>('split');
|
||||
const [pdf, setPdf] = useState<PdfFile | null>(null);
|
||||
const [selectedPages, setSelectedPages] = useState<number[]>([]);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [splitResults, setSplitResults] = useState<SplitResult[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [baseThumbnails, setBaseThumbnails] = useState<string[] | null>(null);
|
||||
const [reorderThumbnails, setReorderThumbnails] = useState<string[] | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [rangeUrl, setRangeUrl] = useState<string | null>(null);
|
||||
const [rangeFilename, setRangeFilename] = useState<string | null>(null);
|
||||
|
||||
const [reorderUrl, setReorderUrl] = useState<string | null>(null);
|
||||
const [reorderFilename, setReorderFilename] = useState<string | null>(null);
|
||||
|
||||
const [rotations, setRotations] = useState<Record<number, number>>({});
|
||||
|
||||
const handleFileLoaded = async (file: File) => {
|
||||
setError(null);
|
||||
setSplitResults([]);
|
||||
setSelectedPages([]);
|
||||
setRangeUrl(null);
|
||||
setRangeFilename(null);
|
||||
setReorderUrl(null);
|
||||
setReorderFilename(null);
|
||||
setBaseThumbnails(null);
|
||||
setReorderThumbnails(null);
|
||||
setRotations({});
|
||||
setIsBusy(true);
|
||||
try {
|
||||
const loaded = await loadPdfFromFile(file);
|
||||
setPdf(loaded);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError('Failed to load PDF (see console).');
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!pdf) {
|
||||
setBaseThumbnails(null);
|
||||
setReorderThumbnails(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const thumbs = await generateThumbnails(pdf.arrayBuffer);
|
||||
if (!cancelled) {
|
||||
setBaseThumbnails(thumbs);
|
||||
setReorderThumbnails(thumbs);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (!cancelled) {
|
||||
setError('Failed to generate thumbnails (see console).');
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pdf]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pdf) {
|
||||
setReorderThumbnails(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const thumbs = await generateThumbnailsWithRotations(
|
||||
pdf.arrayBuffer,
|
||||
rotations
|
||||
);
|
||||
if (!cancelled) {
|
||||
setReorderThumbnails(thumbs);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (!cancelled) {
|
||||
setError('Failed to generate rotated thumbnails (see console).');
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pdf, rotations]);
|
||||
|
||||
const togglePageSelection = (index: number) => {
|
||||
setSelectedPages((prev) =>
|
||||
prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSplit = async () => {
|
||||
if (!pdf) return;
|
||||
setError(null);
|
||||
setIsBusy(true);
|
||||
try {
|
||||
const result = await splitIntoSinglePages(pdf);
|
||||
setSplitResults(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError('Error while splitting PDF (see console).');
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtractRange = async (from: number, to: number) => {
|
||||
if (!pdf) return;
|
||||
setError(null);
|
||||
setIsBusy(true);
|
||||
|
||||
if (rangeUrl) {
|
||||
URL.revokeObjectURL(rangeUrl);
|
||||
setRangeUrl(null);
|
||||
setRangeFilename(null);
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await extractRange(pdf, { from, to });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const base = pdf.name.replace(/\.pdf$/i, '');
|
||||
const filename = `${base}_pages_${from}-${to}.pdf`;
|
||||
setRangeUrl(url);
|
||||
setRangeFilename(filename);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError('Error while extracting range (see console).');
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportReordered = async (order: number[]) => {
|
||||
if (!pdf) return;
|
||||
setError(null);
|
||||
setIsBusy(true);
|
||||
|
||||
if (reorderUrl) {
|
||||
URL.revokeObjectURL(reorderUrl);
|
||||
setReorderUrl(null);
|
||||
setReorderFilename(null);
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await exportReordered(pdf, order, rotations);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const base = pdf.name.replace(/\.pdf$/i, '');
|
||||
const filename = `${base}_reordered.pdf`;
|
||||
setReorderUrl(url);
|
||||
setReorderFilename(filename);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError('Error while exporting reordered PDF (see console).');
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRotatePage = (pageIndex: number) => {
|
||||
setRotations((prev) => {
|
||||
const current = prev[pageIndex] ?? 0;
|
||||
const next = (current + 90) % 360;
|
||||
return { ...prev, [pageIndex]: next };
|
||||
});
|
||||
};
|
||||
|
||||
const hasPdf = !!pdf;
|
||||
const pageCount = pdf?.pageCount ?? 0;
|
||||
|
||||
return (
|
||||
<Layout activeTool={activeTool} onToolChange={setActiveTool}>
|
||||
<FileLoader pdf={pdf} onFileLoaded={handleFileLoaded} />
|
||||
|
||||
{activeTool === 'split' && pdf && (
|
||||
<>
|
||||
<PageList
|
||||
pageCount={pageCount}
|
||||
selectedPages={selectedPages}
|
||||
onTogglePage={togglePageSelection}
|
||||
thumbnails={baseThumbnails}
|
||||
/>
|
||||
<ActionsPanel
|
||||
hasPdf={hasPdf}
|
||||
onSplit={handleSplit}
|
||||
onExtractRange={handleExtractRange}
|
||||
isBusy={isBusy}
|
||||
splitResults={splitResults}
|
||||
rangeDownloadUrl={rangeUrl}
|
||||
rangeFilename={rangeFilename}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTool === 'reorder' && (
|
||||
<ReorderPanel
|
||||
pageCount={pageCount}
|
||||
thumbnails={reorderThumbnails}
|
||||
isBusy={isBusy}
|
||||
hasPdf={hasPdf}
|
||||
rotations={rotations}
|
||||
onRotate={handleRotatePage}
|
||||
onExportReordered={handleExportReordered}
|
||||
reorderDownloadUrl={reorderUrl}
|
||||
reorderFilename={reorderFilename}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className="card"
|
||||
style={{ border: '1px solid #fecaca', background: '#fef2f2' }}
|
||||
>
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user