change for host

This commit is contained in:
2025-11-26 18:40:03 +01:00
parent baacb7cfac
commit abfe6c347a
6 changed files with 1835 additions and 26 deletions

141
.gitignore vendored Normal file
View File

@@ -0,0 +1,141 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
.output
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/

1542
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -25,44 +25,73 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
}) => { }) => {
const [order, setOrder] = useState<number[]>([]); const [order, setOrder] = useState<number[]>([]);
const [draggingIndex, setDraggingIndex] = useState<number | null>(null); const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
const [dropIndex, setDropIndex] = useState<number | null>(null); // slot 0..order.length
useEffect(() => { useEffect(() => {
if (pageCount > 0) { if (pageCount > 0) {
setOrder(Array.from({ length: pageCount }, (_, i) => i)); setOrder(Array.from({ length: pageCount }, (_, i) => i));
} else { } else {
setOrder([]); setOrder([]);
setDraggingIndex(null);
setDropIndex(null);
} }
}, [pageCount]); }, [pageCount]);
const handleDragStart = (index: number) => (e: React.DragEvent) => { const handleDragStart = (index: number) => (e: React.DragEvent) => {
setDraggingIndex(index); setDraggingIndex(index);
setDropIndex(index); // initial assumption: before itself
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
}; // Firefox needs some data
e.dataTransfer.setData('text/plain', String(index));
const handleDragOver = (index: number) => (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleDrop = (index: number) => (e: React.DragEvent) => {
e.preventDefault();
if (draggingIndex === null || draggingIndex === index) return;
setOrder((prev) => {
const updated = [...prev];
const [moved] = updated.splice(draggingIndex, 1);
updated.splice(index, 0, moved);
return updated;
});
setDraggingIndex(null);
}; };
const handleDragEnd = () => { const handleDragEnd = () => {
setDraggingIndex(null); setDraggingIndex(null);
setDropIndex(null);
};
const handleCardDragOver = (cardIndex: number) => (e: React.DragEvent) => {
if (draggingIndex == null) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
const x = e.clientX - rect.left;
// left half => slot BEFORE this card
// right half => slot AFTER this card
const slot = x < rect.width / 2 ? cardIndex : cardIndex + 1;
setDropIndex(slot);
};
const handleEndSlotDragOver = (e: React.DragEvent) => {
if (draggingIndex == null) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setDropIndex(order.length); // slot at the very end
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
if (draggingIndex == null || dropIndex == null) return;
setOrder((prev) => {
const updated = [...prev];
const [moved] = updated.splice(draggingIndex, 1);
const adjustedSlot = dropIndex > draggingIndex ? dropIndex - 1 : dropIndex;
updated.splice(adjustedSlot, 0, moved);
return updated;
});
setDraggingIndex(null);
setDropIndex(null);
}; };
const handleDelete = (visualIndex: number) => () => { const handleDelete = (visualIndex: number) => () => {
setOrder((prev) => prev.filter((_, idx) => idx !== visualIndex)); setOrder((prev) => prev.filter((_, idx) => idx !== visualIndex));
setDraggingIndex(null);
setDropIndex(null);
}; };
const handleRotateClick = (pageIndex: number) => () => { const handleRotateClick = (pageIndex: number) => () => {
@@ -83,12 +112,21 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
); );
} }
const showLeftLine = (cardIndex: number) =>
dropIndex !== null && dropIndex === cardIndex && draggingIndex !== null;
const showRightLine = (cardIndex: number) =>
dropIndex !== null && dropIndex === cardIndex + 1 && draggingIndex !== null;
const showEndLine = () =>
dropIndex !== null && dropIndex === order.length && draggingIndex !== null;
return ( return (
<div className="card"> <div className="card">
<h2>Reorder / delete / rotate</h2> <h2>Reorder / delete / rotate</h2>
<p> <p>
Drag pages to reorder them. Use rotate and delete controls below each Drag pages to reorder them. A vertical blue line shows where the page
thumbnail. All changes stay in memory until you export a new PDF. will be inserted. Use rotate and delete controls below each thumbnail.
</p> </p>
<div <div
@@ -96,8 +134,10 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '0.5rem', gap: '0.5rem',
alignItems: 'flex-start',
marginBottom: '0.75rem', marginBottom: '0.75rem',
}} }}
onDrop={handleDrop}
> >
{order.map((pageIndex, visualIndex) => { {order.map((pageIndex, visualIndex) => {
const thumb = thumbnails?.[pageIndex]; const thumb = thumbnails?.[pageIndex];
@@ -109,10 +149,10 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
key={`${pageIndex}-${visualIndex}`} key={`${pageIndex}-${visualIndex}`}
draggable draggable
onDragStart={handleDragStart(visualIndex)} onDragStart={handleDragStart(visualIndex)}
onDragOver={handleDragOver(visualIndex)}
onDrop={handleDrop(visualIndex)}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDragOver={handleCardDragOver(visualIndex)}
style={{ style={{
position: 'relative',
width: '130px', width: '130px',
padding: '0.4rem', padding: '0.4rem',
borderRadius: '0.5rem', borderRadius: '0.5rem',
@@ -125,6 +165,36 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
cursor: 'grab', cursor: 'grab',
}} }}
> >
{/* left drop indicator */}
{showLeftLine(visualIndex) && (
<div
style={{
position: 'absolute',
left: '-4px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: '#2563eb',
}}
/>
)}
{/* right drop indicator */}
{showRightLine(visualIndex) && (
<div
style={{
position: 'absolute',
right: '-4px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: '#2563eb',
}}
/>
)}
{thumb ? ( {thumb ? (
<img <img
src={thumb} src={thumb}
@@ -194,6 +264,32 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
</div> </div>
); );
})} })}
{/* invisible end slot to allow dropping after the last card */}
<div
onDragOver={handleEndSlotDragOver}
onDrop={handleDrop}
style={{
width: '20px',
height: '120px',
position: 'relative',
alignSelf: 'stretch',
}}
>
{showEndLine() && (
<div
style={{
position: 'absolute',
left: '8px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: '#2563eb',
}}
/>
)}
</div>
</div> </div>
<div className="button-row"> <div className="button-row">

View File

@@ -1,11 +1,27 @@
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url'; import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url';
// pdf.js worker setup for Vite
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(pdfjsLib as any).GlobalWorkerOptions.workerSrc = pdfjsWorker; (pdfjsLib as any).GlobalWorkerOptions.workerSrc = pdfjsWorker;
type RotationsMap = Record<number, number>; type RotationsMap = Record<number, number>; // key: 0-based page index, value: degrees
/**
* Helper: create a *copy* of the data for pdf.js to consume.
* pdf.js may transfer/detach the buffer it receives, so we NEVER
* pass the original ArrayBuffer directly.
*/
function makePdfJsDataCopy(arrayBuffer: ArrayBuffer): Uint8Array {
const src = new Uint8Array(arrayBuffer);
const copy = new Uint8Array(src.byteLength);
copy.set(src);
return copy;
}
/**
* Unrotated thumbnails used e.g. in the Split/Extract view.
*/
export async function generateThumbnails( export async function generateThumbnails(
arrayBuffer: ArrayBuffer, arrayBuffer: ArrayBuffer,
maxHeight = 150 maxHeight = 150
@@ -13,6 +29,9 @@ export async function generateThumbnails(
return generateThumbnailsInternal(arrayBuffer, {}, maxHeight); return generateThumbnailsInternal(arrayBuffer, {}, maxHeight);
} }
/**
* Thumbnails that respect per-page rotations (for the Reorder view).
*/
export async function generateThumbnailsWithRotations( export async function generateThumbnailsWithRotations(
arrayBuffer: ArrayBuffer, arrayBuffer: ArrayBuffer,
rotations: RotationsMap, rotations: RotationsMap,
@@ -26,7 +45,10 @@ async function generateThumbnailsInternal(
rotations: RotationsMap, rotations: RotationsMap,
maxHeight: number maxHeight: number
): Promise<string[]> { ): Promise<string[]> {
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); // IMPORTANT: use a COPY so pdf.js can detach it without breaking future calls
const dataCopy = makePdfJsDataCopy(arrayBuffer);
const loadingTask = pdfjsLib.getDocument({ data: dataCopy });
const pdf = await loadingTask.promise; const pdf = await loadingTask.promise;
const thumbs: string[] = []; const thumbs: string[] = [];
@@ -37,6 +59,7 @@ async function generateThumbnailsInternal(
const scale = maxHeight / viewport.height; const scale = maxHeight / viewport.height;
const scaledViewport = page.getViewport({ scale }); const scaledViewport = page.getViewport({ scale });
// First render unrotated page into a canvas
const baseCanvas = document.createElement('canvas'); const baseCanvas = document.createElement('canvas');
const baseCtx = baseCanvas.getContext('2d'); const baseCtx = baseCanvas.getContext('2d');
if (!baseCtx) { if (!baseCtx) {
@@ -55,13 +78,14 @@ async function generateThumbnailsInternal(
const originalIndex = pageNum - 1; const originalIndex = pageNum - 1;
const rotationDegRaw = rotations[originalIndex] ?? 0; const rotationDegRaw = rotations[originalIndex] ?? 0;
const rotationDeg = ((rotationDegRaw % 360) + 360) % 360; const rotationDeg = ((rotationDegRaw % 360) + 360) % 360; // normalize 0359
if (rotationDeg === 0) { if (rotationDeg === 0) {
thumbs.push(baseCanvas.toDataURL('image/png')); thumbs.push(baseCanvas.toDataURL('image/png'));
continue; continue;
} }
// Re-render onto a second canvas with rotation applied
const rotatedCanvas = document.createElement('canvas'); const rotatedCanvas = document.createElement('canvas');
const rotatedCtx = rotatedCanvas.getContext('2d'); const rotatedCtx = rotatedCanvas.getContext('2d');
if (!rotatedCtx) { if (!rotatedCtx) {
@@ -95,6 +119,7 @@ async function generateThumbnailsInternal(
rotatedCtx.rotate(rad); rotatedCtx.rotate(rad);
break; break;
default: default:
// fallback: no rotation
break; break;
} }

View File

@@ -128,7 +128,7 @@ button.secondary {
.page-pill { .page-pill {
padding: 0.2rem 0.5rem; padding: 0.2rem 0.5rem;
border-radius: 999px; border-radius: 0.5rem;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
font-size: 0.8rem; font-size: 0.8rem;
background: #f9fafb; background: #f9fafb;

View File

@@ -3,4 +3,9 @@ import react from '@vitejs/plugin-react-swc';
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
host: true,
allowedHosts: ['pdftools.add-ideas.de'], // ← ADD THIS
},
}); });