first commit

This commit is contained in:
2025-11-26 18:02:58 +01:00
commit baacb7cfac
15 changed files with 1269 additions and 0 deletions

254
src/App.tsx Normal file
View 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;