mostly formatting, dependency fix

This commit is contained in:
2026-05-17 02:39:32 +02:00
parent a5dc70aabf
commit cf9a0dd0b7
32 changed files with 837 additions and 836 deletions

View File

@@ -1,19 +1,19 @@
import React, { useCallback, useEffect, useState } from "react";
import Layout from "./components/Layout";
import FileLoader from "./components/FileLoader";
import ReorderPanel from "./components/ReorderPanel";
import ActionsPanel from "./components/ActionsPanel";
import PagePreviewModal from "./components/PagePreviewModal";
import WorkspacePanel from "./components/WorkspacePanel";
import React, { useCallback, useEffect, useState } from 'react';
import Layout from './components/Layout';
import FileLoader from './components/FileLoader';
import ReorderPanel from './components/ReorderPanel';
import ActionsPanel from './components/ActionsPanel';
import PagePreviewModal from './components/PagePreviewModal';
import WorkspacePanel from './components/WorkspacePanel';
import ActionDialog, {
type ActionDialogAction,
} from "./components/ActionDialog";
import HelpDialog from "./components/HelpDialog";
import { PDFDocument } from "pdf-lib";
} from './components/ActionDialog';
import HelpDialog from './components/HelpDialog';
import { PDFDocument } from 'pdf-lib';
import type {
StoredWorkspace,
WorkspaceSummary,
} from "./workspace/workspaceTypes";
} from './workspace/workspaceTypes';
import {
createInitialPageRefs,
createPageRefId,
@@ -22,22 +22,22 @@ import {
defaultWorkspaceNameFromPdfName,
normalizeRotation,
useWorkspaceState,
} from "./workspace/useWorkspaceState";
} from './workspace/useWorkspaceState';
import {
deleteWorkspaceFromIndexedDb,
listWorkspaces,
loadWorkspaceFromIndexedDb,
saveWorkspaceToIndexedDb,
} from "./workspace/workspaceDb";
import type { PageRef, PdfFile } from "./pdf/pdfTypes";
} from './workspace/workspaceDb';
import type { PageRef, PdfFile } from './pdf/pdfTypes';
import {
loadPdfFromFile,
mergePdfFiles,
splitIntoSinglePages,
exportPages,
} from "./pdf/pdfService";
import { usePdfThumbnails } from "./pdf/usePdfThumbnails";
import { usePdfGeneratedOutputs } from "./hooks/usePdfGeneratedOutputs";
} from './pdf/pdfService';
import { usePdfThumbnails } from './pdf/usePdfThumbnails';
import { usePdfGeneratedOutputs } from './hooks/usePdfGeneratedOutputs';
import {
createSelectionPdfName,
createSelectionWorkspaceName,
@@ -50,9 +50,9 @@ function isEditableKeyboardTarget(target: EventTarget | null): boolean {
const tagName = target.tagName.toLowerCase();
return (
target.isContentEditable ||
tagName === "input" ||
tagName === "textarea" ||
tagName === "select"
tagName === 'input' ||
tagName === 'textarea' ||
tagName === 'select'
);
}
@@ -70,18 +70,18 @@ const App: React.FC = () => {
const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
const [activeWorkspaceId, setActiveWorkspaceId] = useState<string | null>(
null,
null
);
const [workspaceName, setWorkspaceName] = useState("");
const [workspaceName, setWorkspaceName] = useState('');
const [previewPageId, setPreviewPageId] = useState<string | null>(null);
const [pendingFile, setPendingFile] = useState<File | null>(null);
const [showMergeOptions, setShowMergeOptions] = useState(false);
const [mergeMode, setMergeMode] = useState<
"overwrite" | "append" | "insertAt"
>("append");
const [mergeInsertAt, setMergeInsertAt] = useState<string>("");
'overwrite' | 'append' | 'insertAt'
>('append');
const [mergeInsertAt, setMergeInsertAt] = useState<string>('');
const {
splitDownloads,
@@ -123,7 +123,7 @@ const App: React.FC = () => {
console.error(thrown);
setError(message);
},
[],
[]
);
const { thumbnails: reorderThumbnails, clearThumbnailCache } =
@@ -154,7 +154,7 @@ const App: React.FC = () => {
setWorkspaces(summaries);
} catch (e) {
console.error(e);
setError("Failed to read saved workspaces from browser storage.");
setError('Failed to read saved workspaces from browser storage.');
}
};
@@ -165,7 +165,7 @@ const App: React.FC = () => {
const resetWorkspaceState = () => {
setPdf(null);
setActiveWorkspaceId(null);
setWorkspaceName("");
setWorkspaceName('');
resetWorkspaceCommandState();
clearGeneratedOutputs();
clearThumbnailCache();
@@ -183,7 +183,7 @@ const App: React.FC = () => {
const workspaceId = activeWorkspaceId ?? createWorkspaceId();
const existing = workspaces.find(
(workspace) => workspace.id === workspaceId,
(workspace) => workspace.id === workspaceId
);
const workspace: StoredWorkspace = {
@@ -221,7 +221,7 @@ const App: React.FC = () => {
} catch (e) {
console.error(e);
setError(
"Failed to save workspace. The browser storage quota may be full.",
'Failed to save workspace. The browser storage quota may be full.'
);
return false;
} finally {
@@ -242,7 +242,7 @@ const App: React.FC = () => {
}
openActionDialog({
title: "Reset workspace?",
title: 'Reset workspace?',
content: (
<>
<p style={{ marginTop: 0 }}>This workspace has unsaved changes.</p>
@@ -253,21 +253,21 @@ const App: React.FC = () => {
),
actions: [
{
label: "Cancel",
variant: "secondary",
label: 'Cancel',
variant: 'secondary',
onClick: closeActionDialog,
},
{
label: "Reset without saving",
variant: "danger",
label: 'Reset without saving',
variant: 'danger',
onClick: () => {
closeActionDialog();
performResetWorkspace();
},
},
{
label: "Save and reset",
variant: "primary",
label: 'Save and reset',
variant: 'primary',
autoFocus: true,
onClick: async () => {
closeActionDialog();
@@ -289,7 +289,7 @@ const App: React.FC = () => {
const loaded = await loadWorkspaceFromIndexedDb(workspaceId);
if (!loaded) {
setError("Workspace not found.");
setError('Workspace not found.');
await refreshWorkspaces();
return;
}
@@ -324,7 +324,7 @@ const App: React.FC = () => {
setWorkspaceName(loaded.workspace.name);
} catch (e) {
console.error(e);
setError("Failed to load workspace from browser storage.");
setError('Failed to load workspace from browser storage.');
} finally {
setIsBusy(false);
}
@@ -332,10 +332,10 @@ const App: React.FC = () => {
const handleDeleteWorkspace = (workspaceId: string) => {
const workspace = workspaces.find((item) => item.id === workspaceId);
const name = workspace?.name ?? "this workspace";
const name = workspace?.name ?? 'this workspace';
openActionDialog({
title: "Delete workspace?",
title: 'Delete workspace?',
content: (
<>
<p style={{ marginTop: 0 }}>
@@ -350,13 +350,13 @@ const App: React.FC = () => {
),
actions: [
{
label: "Cancel",
variant: "secondary",
label: 'Cancel',
variant: 'secondary',
onClick: closeActionDialog,
},
{
label: "Delete workspace",
variant: "danger",
label: 'Delete workspace',
variant: 'danger',
autoFocus: true,
onClick: () => {
closeActionDialog();
@@ -377,14 +377,14 @@ const App: React.FC = () => {
setActiveWorkspaceId(null);
setWorkspaceDirty(true);
setWorkspaceMessage(
"Saved workspace deleted. Current in-memory document remains open.",
'Saved workspace deleted. Current in-memory document remains open.'
);
}
await refreshWorkspaces();
} catch (e) {
console.error(e);
setError("Failed to delete workspace.");
setError('Failed to delete workspace.');
}
};
@@ -411,7 +411,7 @@ const App: React.FC = () => {
setWorkspaceName(defaultWorkspaceNameFromPdfName(loaded.name));
} catch (e) {
console.error(e);
setError("Failed to load PDF (see console).");
setError('Failed to load PDF (see console).');
} finally {
setIsBusy(false);
}
@@ -423,7 +423,7 @@ const App: React.FC = () => {
} else {
setPendingFile(file);
setShowMergeOptions(true);
setMergeMode("append");
setMergeMode('append');
setMergeInsertAt(String(pages.length + 1));
}
};
@@ -436,7 +436,7 @@ const App: React.FC = () => {
const handleMergeConfirm = async () => {
if (!pendingFile) return;
if (!pdf || mergeMode === "overwrite") {
if (!pdf || mergeMode === 'overwrite') {
await loadFileAsNew(pendingFile);
setPendingFile(null);
setShowMergeOptions(false);
@@ -464,12 +464,12 @@ const App: React.FC = () => {
// 3) Determine insert position (0-based)
let insertAt = pages.length; // default: append at end
if (mergeMode === "insertAt") {
if (mergeMode === 'insertAt') {
const parsed = parseInt(mergeInsertAt, 10);
if (Number.isFinite(parsed)) {
insertAt = Math.min(Math.max(parsed - 1, 0), pages.length);
}
} else if (mergeMode === "append") {
} else if (mergeMode === 'append') {
insertAt = pages.length;
}
@@ -495,7 +495,7 @@ const App: React.FC = () => {
setActiveWorkspaceId(null);
} catch (e) {
console.error(e);
setError("Failed to merge PDF (see console).");
setError('Failed to merge PDF (see console).');
} finally {
setIsBusy(false);
setPendingFile(null);
@@ -518,16 +518,16 @@ const App: React.FC = () => {
const handleKeyDown = (e: KeyboardEvent) => {
if (isEditableKeyboardTarget(e.target)) return;
if (e.key === "F1" || e.key === "?") {
if (e.key === 'F1' || e.key === '?') {
e.preventDefault();
setHelpOpen(true);
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
@@ -537,13 +537,13 @@ const App: React.FC = () => {
const afterPages = pages.map((page) =>
page.id === pageId
? { ...page, rotation: (normalizeRotation(page.rotation) + 90) % 360 }
: page,
: page
);
executeWorkspaceCommand(
createWorkspaceCommand({
type: "page.rotate",
label: "Rotated page clockwise",
type: 'page.rotate',
label: 'Rotated page clockwise',
before,
after: {
...before,
@@ -553,7 +553,7 @@ const App: React.FC = () => {
pageId,
degrees: 90,
},
}),
})
);
};
@@ -562,13 +562,13 @@ const App: React.FC = () => {
const afterPages = pages.map((page) =>
page.id === pageId
? { ...page, rotation: (normalizeRotation(page.rotation) + 270) % 360 }
: page,
: page
);
executeWorkspaceCommand(
createWorkspaceCommand({
type: "page.rotate",
label: "Rotated page counterclockwise",
type: 'page.rotate',
label: 'Rotated page counterclockwise',
before,
after: {
...before,
@@ -578,7 +578,7 @@ const App: React.FC = () => {
pageId,
degrees: -90,
},
}),
})
);
};
@@ -586,10 +586,10 @@ const App: React.FC = () => {
const page = pages.find((item) => item.id === pageId);
const visualIndex = page ? pages.indexOf(page) : -1;
const pageLabel =
visualIndex >= 0 ? `page at position ${visualIndex + 1}` : "this page";
visualIndex >= 0 ? `page at position ${visualIndex + 1}` : 'this page';
openActionDialog({
title: "Delete page?",
title: 'Delete page?',
content: (
<p style={{ margin: 0 }}>
Delete <strong>{pageLabel}</strong> from the current workspace?
@@ -597,13 +597,13 @@ const App: React.FC = () => {
),
actions: [
{
label: "Cancel",
variant: "secondary",
label: 'Cancel',
variant: 'secondary',
onClick: closeActionDialog,
},
{
label: "Delete page",
variant: "danger",
label: 'Delete page',
variant: 'danger',
autoFocus: true,
onClick: () => {
closeActionDialog();
@@ -619,8 +619,8 @@ const App: React.FC = () => {
executeWorkspaceCommand(
createWorkspaceCommand({
type: "page.delete",
label: "Deleted page",
type: 'page.delete',
label: 'Deleted page',
before,
after: {
pages: pages.filter((page) => page.id !== pageId),
@@ -630,7 +630,7 @@ const App: React.FC = () => {
details: {
pageId,
},
}),
})
);
};
@@ -639,8 +639,8 @@ const App: React.FC = () => {
executeWorkspaceCommand(
createWorkspaceCommand({
type: "pages.reorder",
label: "Reordered pages",
type: 'pages.reorder',
label: 'Reordered pages',
before,
after: {
...before,
@@ -649,14 +649,14 @@ const App: React.FC = () => {
details: {
pageCount: newPages.length,
},
}),
})
);
};
const handleToggleSelect = (
pageId: string,
visualIndex: number,
e: React.MouseEvent<HTMLButtonElement>,
e: React.MouseEvent<HTMLButtonElement>
) => {
setSelectedPageIds((prev) => {
if (e.shiftKey && lastSelectedVisualIndex !== null && pages.length > 0) {
@@ -731,28 +731,28 @@ const App: React.FC = () => {
openActionDialog({
title:
idsToDelete.length === 1
? "Delete selected page?"
: "Delete selected pages?",
? 'Delete selected page?'
: 'Delete selected pages?',
content: (
<p style={{ margin: 0 }}>
Delete{" "}
Delete{' '}
<strong>
{idsToDelete.length === 1
? "1 selected page"
? '1 selected page'
: `${idsToDelete.length} selected pages`}
</strong>{" "}
</strong>{' '}
from the current workspace?
</p>
),
actions: [
{
label: "Cancel",
variant: "secondary",
label: 'Cancel',
variant: 'secondary',
onClick: closeActionDialog,
},
{
label: idsToDelete.length === 1 ? "Delete page" : "Delete pages",
variant: "danger",
label: idsToDelete.length === 1 ? 'Delete page' : 'Delete pages',
variant: 'danger',
autoFocus: true,
onClick: () => {
closeActionDialog();
@@ -795,10 +795,10 @@ const App: React.FC = () => {
executeWorkspaceCommand(
createWorkspaceCommand({
type: "pages.copy",
type: 'pages.copy',
label:
copiedPages.length === 1
? "Copied page"
? 'Copied page'
: `Copied ${copiedPages.length} pages`,
before,
after: {
@@ -810,7 +810,7 @@ const App: React.FC = () => {
count: copiedPages.length,
insertSlot: clampedSlot,
},
}),
})
);
};
@@ -852,7 +852,7 @@ const App: React.FC = () => {
const key = e.key.toLowerCase();
if ((e.ctrlKey || e.metaKey) && key === "z") {
if ((e.ctrlKey || e.metaKey) && key === 'z') {
e.preventDefault();
if (e.shiftKey) {
handleRedo();
@@ -862,13 +862,13 @@ const App: React.FC = () => {
return;
}
if ((e.ctrlKey || e.metaKey) && key === "y") {
if ((e.ctrlKey || e.metaKey) && key === 'y') {
e.preventDefault();
handleRedo();
return;
}
if ((e.ctrlKey || e.metaKey) && key === "a") {
if ((e.ctrlKey || e.metaKey) && key === 'a') {
e.preventDefault();
setSelectedPageIds(pages.map((page) => page.id));
setLastSelectedVisualIndex(null);
@@ -876,7 +876,7 @@ const App: React.FC = () => {
}
if (
(e.key === "Delete" || e.key === "Backspace") &&
(e.key === 'Delete' || e.key === 'Backspace') &&
selectedPageIds.length > 0
) {
e.preventDefault();
@@ -884,17 +884,17 @@ const App: React.FC = () => {
return;
}
if (e.key === "Escape" && selectedPageIds.length > 0) {
if (e.key === 'Escape' && selectedPageIds.length > 0) {
e.preventDefault();
setSelectedPageIds([]);
setLastSelectedVisualIndex(null);
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener('keydown', handleKeyDown);
};
}, [
hasPdf,
@@ -919,7 +919,7 @@ const App: React.FC = () => {
replaceSplitResults(result);
} catch (e) {
console.error(e);
setError("Error while splitting PDF (see console).");
setError('Error while splitting PDF (see console).');
} finally {
setIsBusy(false);
}
@@ -939,18 +939,17 @@ const App: React.FC = () => {
if (selectedPages.length === 0) return;
const blob = await exportPages(pdf, selectedPages);
const base = pdf.name.replace(/\.pdf$/i, "");
const base = pdf.name.replace(/\.pdf$/i, '');
const filename = `${base}_selected.pdf`;
replaceSubsetResult(blob, filename);
} catch (e) {
console.error(e);
setError("Error while extracting selected pages (see console).");
setError('Error while extracting selected pages (see console).');
} finally {
setIsBusy(false);
}
};
const performOpenSelectionAsWorkspace = async () => {
if (!pdf || selectedPageIds.length === 0) return;
@@ -988,7 +987,7 @@ const App: React.FC = () => {
redoHistory: [],
dirty: true,
message: `Created a new workspace from ${selectedPageCount} selected ${
selectedPageCount === 1 ? "page" : "pages"
selectedPageCount === 1 ? 'page' : 'pages'
}.`,
});
@@ -999,7 +998,7 @@ const App: React.FC = () => {
clearThumbnailCache();
} catch (e) {
console.error(e);
setError("Error while opening selection as a new workspace.");
setError('Error while opening selection as a new workspace.');
} finally {
setIsBusy(false);
}
@@ -1017,13 +1016,13 @@ const App: React.FC = () => {
}
openActionDialog({
title: "Open selection as new workspace?",
title: 'Open selection as new workspace?',
content: (
<>
<p style={{ marginTop: 0 }}>
This will replace the current in-memory workspace with a new
workspace built from {selectedPages.length}{' '}
{selectedPages.length === 1 ? "selected page" : "selected pages"}.
{selectedPages.length === 1 ? 'selected page' : 'selected pages'}.
</p>
<p style={{ marginBottom: 0 }}>
The current workspace has unsaved changes. Do you want to save it
@@ -1033,21 +1032,21 @@ const App: React.FC = () => {
),
actions: [
{
label: "Cancel",
variant: "secondary",
label: 'Cancel',
variant: 'secondary',
onClick: closeActionDialog,
},
{
label: "Open without saving",
variant: "danger",
label: 'Open without saving',
variant: 'danger',
onClick: () => {
closeActionDialog();
void performOpenSelectionAsWorkspace();
},
},
{
label: "Save and open",
variant: "primary",
label: 'Save and open',
variant: 'primary',
autoFocus: true,
onClick: async () => {
closeActionDialog();
@@ -1068,12 +1067,12 @@ const App: React.FC = () => {
try {
const blob = await exportPages(pdf, pages);
const base = pdf.name.replace(/\.pdf$/i, "");
const base = pdf.name.replace(/\.pdf$/i, '');
const filename = `${base}_reordered.pdf`;
replaceExportResult(blob, filename);
} catch (e) {
console.error(e);
setError("Error while exporting reordered PDF (see console).");
setError('Error while exporting reordered PDF (see console).');
} finally {
setIsBusy(false);
}
@@ -1120,62 +1119,62 @@ const App: React.FC = () => {
{showMergeOptions && pendingFile && pdf && pages.length > 0 && (
<div
className="card"
style={{ border: "1px solid #bfdbfe", background: "#eff6ff" }}
style={{ border: '1px solid #bfdbfe', background: '#eff6ff' }}
>
<h2>Open file: merge or replace?</h2>
<p style={{ fontSize: "0.85rem", color: "#374151" }}>
You already have <strong>{pdf.name}</strong> with {pages.length}{" "}
pages open. What should happen with{" "}
<p style={{ fontSize: '0.85rem', color: '#374151' }}>
You already have <strong>{pdf.name}</strong> with {pages.length}{' '}
pages open. What should happen with{' '}
<strong>{pendingFile.name}</strong>?
</p>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "0.5rem",
marginTop: "0.5rem",
fontSize: "0.9rem",
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
marginTop: '0.5rem',
fontSize: '0.9rem',
}}
>
<label
style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}
style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
>
<input
type="radio"
name="mergeMode"
value="overwrite"
checked={mergeMode === "overwrite"}
onChange={() => setMergeMode("overwrite")}
checked={mergeMode === 'overwrite'}
onChange={() => setMergeMode('overwrite')}
/>
<span>Replace current document</span>
</label>
<label
style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}
style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
>
<input
type="radio"
name="mergeMode"
value="append"
checked={mergeMode === "append"}
onChange={() => setMergeMode("append")}
checked={mergeMode === 'append'}
onChange={() => setMergeMode('append')}
/>
<span>Merge and append pages at the end</span>
</label>
<label
style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}
style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
>
<input
type="radio"
name="mergeMode"
value="insertAt"
checked={mergeMode === "insertAt"}
onChange={() => setMergeMode("insertAt")}
checked={mergeMode === 'insertAt'}
onChange={() => setMergeMode('insertAt')}
/>
<span>
Merge and insert starting at position{" "}
Merge and insert starting at position{' '}
<input
type="number"
min={1}
@@ -1183,19 +1182,19 @@ const App: React.FC = () => {
value={mergeInsertAt}
onChange={(e) => setMergeInsertAt(e.target.value)}
style={{
width: "4rem",
padding: "0.15rem 0.3rem",
fontSize: "0.85rem",
width: '4rem',
padding: '0.15rem 0.3rem',
fontSize: '0.85rem',
}}
/>{" "}
<span style={{ color: "#6b7280" }}>
/>{' '}
<span style={{ color: '#6b7280' }}>
(1 = before first page, {pages.length + 1} = after last page)
</span>
</span>
</label>
</div>
<div className="button-row" style={{ marginTop: "0.75rem" }}>
<div className="button-row" style={{ marginTop: '0.75rem' }}>
<button
className="secondary"
type="button"
@@ -1210,7 +1209,7 @@ const App: React.FC = () => {
onClick={handleMergeConfirm}
disabled={isBusy}
>
{isBusy ? "Working…" : "Continue"}
{isBusy ? 'Working…' : 'Continue'}
</button>
</div>
</div>
@@ -1240,7 +1239,7 @@ const App: React.FC = () => {
selectedCount={selectedPageIds.length}
onSplit={handleSplit}
onExtractSelected={handleExtractSelected}
onOpenSelectionAsWorkspace={handleOpenSelectionAsWorkspace}
onOpenSelectionAsWorkspace={handleOpenSelectionAsWorkspace}
onExportReordered={handleExportReordered}
splitDownloads={splitDownloads}
subsetDownload={subsetDownload}
@@ -1250,7 +1249,7 @@ const App: React.FC = () => {
{error && (
<div
className="card"
style={{ border: "1px solid #fecaca", background: "#fef2f2" }}
style={{ border: '1px solid #fecaca', background: '#fef2f2' }}
>
<strong>Error:</strong> {error}
</div>
@@ -1272,7 +1271,7 @@ const App: React.FC = () => {
<ActionDialog
open={actionDialog !== null}
title={actionDialog?.title ?? ""}
title={actionDialog?.title ?? ''}
actions={actionDialog?.actions ?? []}
onClose={closeActionDialog}
>