233 lines
6.9 KiB
TypeScript
233 lines
6.9 KiB
TypeScript
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;
|