change for host
This commit is contained in:
141
.gitignore
vendored
Normal file
141
.gitignore
vendored
Normal 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
1542
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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">
|
||||||
|
|||||||
@@ -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 0–359
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user