merge files inital commit
This commit is contained in:
@@ -3,14 +3,14 @@ import type { PdfFile } from '../pdf/pdfTypes';
|
||||
|
||||
interface FileLoaderProps {
|
||||
pdf: PdfFile | null;
|
||||
onFileLoaded: (file: File) => void;
|
||||
onFilesLoaded: (files: File[]) => void;
|
||||
}
|
||||
|
||||
const FileLoader: React.FC<FileLoaderProps> = ({ pdf, onFileLoaded }) => {
|
||||
const FileLoader: React.FC<FileLoaderProps> = ({ pdf, onFilesLoaded }) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
onFileLoaded(file);
|
||||
const files = Array.from(e.target.files ?? []);
|
||||
if (files.length > 0) {
|
||||
onFilesLoaded(files);
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
@@ -18,8 +18,16 @@ const FileLoader: React.FC<FileLoaderProps> = ({ pdf, onFileLoaded }) => {
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>1. Load PDF</h2>
|
||||
<p>Select a PDF file. Processing happens entirely in your browser.</p>
|
||||
<input type="file" accept="application/pdf" onChange={handleChange} />
|
||||
<p>
|
||||
Select one PDF to open it directly, or select several PDFs to place them
|
||||
in the merge queue. Processing happens entirely in your browser.
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
multiple
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
{pdf && (
|
||||
<div style={{ marginTop: '0.75rem', fontSize: '0.9rem' }}>
|
||||
|
||||
@@ -34,7 +34,7 @@ const shortcuts = [
|
||||
const tutorialSteps = [
|
||||
{
|
||||
title: '1. Open a PDF or load a workspace',
|
||||
body: 'Start by selecting a local PDF file. If you saved workspaces before, you can restore one from browser storage instead.',
|
||||
body: 'Start by selecting a local PDF file. Select several PDFs to open the merge queue, or restore a saved workspace from browser storage.',
|
||||
},
|
||||
{
|
||||
title: '2. Arrange pages visually',
|
||||
@@ -50,7 +50,7 @@ const tutorialSteps = [
|
||||
},
|
||||
{
|
||||
title: '5. Split and download results',
|
||||
body: 'Splitting creates individual one-page PDF downloads and a ZIP archive that contains all generated page files.',
|
||||
body: 'Splitting creates individual one-page PDF downloads and a ZIP archive that contains all generated page files. For merging, review the incoming PDF queue, reorder it if needed, then replace, append, or insert the queued PDFs.',
|
||||
},
|
||||
{
|
||||
title: '6. Save your workspace or export a PDF',
|
||||
@@ -102,7 +102,7 @@ const HelpDialog: React.FC<HelpDialogProps> = ({ open, onClose }) => {
|
||||
<h2 id="help-dialog-title">Help & tutorial</h2>
|
||||
<p>
|
||||
PDF Workbench is a browser-only page workspace. Use it to quickly
|
||||
rearrange, split, merge, rotate, duplicate, and export PDFs
|
||||
rearrange, split, queue-merge, rotate, duplicate, and export PDFs
|
||||
without uploading documents to a server.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
232
src/components/MergeQueuePanel.tsx
Normal file
232
src/components/MergeQueuePanel.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React from 'react';
|
||||
import type { MergeQueueItem, MergeMode } from '../merge/mergeTypes';
|
||||
|
||||
interface MergeQueuePanelProps {
|
||||
items: MergeQueueItem[];
|
||||
hasCurrentPdf: boolean;
|
||||
currentPdfName: string | null;
|
||||
currentPageCount: number;
|
||||
mergeMode: MergeMode;
|
||||
mergeInsertAt: string;
|
||||
isBusy: boolean;
|
||||
canMerge: boolean;
|
||||
isLoading: boolean;
|
||||
hasErrors: boolean;
|
||||
onMergeModeChange: (mode: MergeMode) => void;
|
||||
onMergeInsertAtChange: (value: string) => void;
|
||||
onMoveUp: (itemId: string) => void;
|
||||
onMoveDown: (itemId: string) => void;
|
||||
onRemove: (itemId: string) => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
function formatFileSize(size: number): string {
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
const MergeQueuePanel: React.FC<MergeQueuePanelProps> = ({
|
||||
items,
|
||||
hasCurrentPdf,
|
||||
currentPdfName,
|
||||
currentPageCount,
|
||||
mergeMode,
|
||||
mergeInsertAt,
|
||||
isBusy,
|
||||
canMerge,
|
||||
isLoading,
|
||||
hasErrors,
|
||||
onMergeModeChange,
|
||||
onMergeInsertAtChange,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onRemove,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const queuedPageCount = items.reduce(
|
||||
(sum, item) => sum + (item.pageCount ?? 0),
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card merge-queue-card"
|
||||
style={{ border: '1px solid #bfdbfe', background: '#eff6ff' }}
|
||||
>
|
||||
<h2>Merge PDF queue</h2>
|
||||
<p style={{ fontSize: '0.85rem', color: '#374151' }}>
|
||||
Add several PDFs, reorder the queue, then merge them into a new unsaved
|
||||
workspace. Processing still happens entirely in your browser.
|
||||
</p>
|
||||
|
||||
{hasCurrentPdf ? (
|
||||
<p style={{ fontSize: '0.85rem', color: '#374151' }}>
|
||||
Current workspace:{' '}
|
||||
<strong>{currentPdfName ?? 'Untitled document'}</strong> with{' '}
|
||||
{currentPageCount} {currentPageCount === 1 ? 'page' : 'pages'}.
|
||||
</p>
|
||||
) : (
|
||||
<p style={{ fontSize: '0.85rem', color: '#374151' }}>
|
||||
No current workspace is open. The queue will become a new workspace.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="merge-queue-list" aria-label="PDF merge queue">
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id} className="merge-queue-item">
|
||||
<div className="merge-queue-order">#{index + 1}</div>
|
||||
<div className="merge-queue-details">
|
||||
<strong>{item.name}</strong>
|
||||
<span>
|
||||
{formatFileSize(item.size)} ·{' '}
|
||||
{item.status === 'ready' && item.pageCount != null
|
||||
? `${item.pageCount} ${item.pageCount === 1 ? 'page' : 'pages'}`
|
||||
: item.status === 'loading'
|
||||
? 'Loading…'
|
||||
: (item.error ?? 'Error')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="merge-queue-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={() => onMoveUp(item.id)}
|
||||
disabled={isBusy || index === 0}
|
||||
title="Move up"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={() => onMoveDown(item.id)}
|
||||
disabled={isBusy || index === items.length - 1}
|
||||
title="Move down"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={() => onRemove(item.id)}
|
||||
disabled={isBusy}
|
||||
title="Remove from queue"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '0.85rem', color: '#374151' }}>
|
||||
Queue total:{' '}
|
||||
<strong>
|
||||
{items.length} {items.length === 1 ? 'PDF' : 'PDFs'}
|
||||
</strong>
|
||||
{queuedPageCount > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
· {queuedPageCount} {queuedPageCount === 1 ? 'page' : 'pages'}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="merge-mode-group">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="mergeMode"
|
||||
value="overwrite"
|
||||
checked={mergeMode === 'overwrite'}
|
||||
onChange={() => onMergeModeChange('overwrite')}
|
||||
disabled={isBusy}
|
||||
/>
|
||||
<span>
|
||||
{hasCurrentPdf ? 'Replace current document' : 'Create from queue'}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="mergeMode"
|
||||
value="append"
|
||||
checked={mergeMode === 'append'}
|
||||
onChange={() => onMergeModeChange('append')}
|
||||
disabled={isBusy || !hasCurrentPdf}
|
||||
/>
|
||||
<span>Append queue after current workspace</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="mergeMode"
|
||||
value="insertAt"
|
||||
checked={mergeMode === 'insertAt'}
|
||||
onChange={() => onMergeModeChange('insertAt')}
|
||||
disabled={isBusy || !hasCurrentPdf}
|
||||
/>
|
||||
<span>
|
||||
Insert queue starting at position{' '}
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={currentPageCount + 1}
|
||||
value={mergeInsertAt}
|
||||
onChange={(e) => onMergeInsertAtChange(e.target.value)}
|
||||
disabled={isBusy || !hasCurrentPdf || mergeMode !== 'insertAt'}
|
||||
style={{
|
||||
width: '4rem',
|
||||
padding: '0.15rem 0.3rem',
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
/>{' '}
|
||||
<span style={{ color: '#6b7280' }}>
|
||||
(1 = before first page, {currentPageCount + 1} = after last page)
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{mergeMode === 'overwrite' && hasCurrentPdf && (
|
||||
<p className="merge-warning">
|
||||
Replace mode discards the current in-memory workspace after the merge.
|
||||
Save it first if you want to keep the current state separately.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hasErrors && (
|
||||
<p className="merge-warning">
|
||||
One or more queued files could not be loaded. Remove failed items
|
||||
before merging.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="button-row" style={{ marginTop: '0.75rem' }}>
|
||||
<button
|
||||
className="secondary"
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={isBusy}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="primary"
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={isBusy || isLoading || !canMerge || hasErrors}
|
||||
>
|
||||
{isBusy ? 'Working…' : isLoading ? 'Loading PDFs…' : 'Merge queue'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MergeQueuePanel;
|
||||
Reference in New Issue
Block a user