mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-19 02:22:11 +01:00
Overlay PDF tool (#4620)
# Description of Changes - Added the OverlayPDF tool --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -125,6 +125,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
radius="md"
|
||||
className="overflow-hidden p-0"
|
||||
withCloseButton={false}
|
||||
zIndex={1100}
|
||||
styles={{
|
||||
content: {
|
||||
position: 'relative',
|
||||
|
||||
@@ -125,6 +125,7 @@ const FilePickerModal = ({
|
||||
title={t("fileUpload.selectFromStorage", "Select Files from Storage")}
|
||||
size="lg"
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
zIndex={1100}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{storedFiles.length === 0 ? (
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
.fileListContainer {
|
||||
max-height: 16.25rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fileItem {
|
||||
border: 1px solid var(--mantine-color-gray-3);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fileNameContainer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fileName {
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.countLabel {
|
||||
width: 140px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fileGroup {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { Stack, Text, Group, Select, SegmentedControl, NumberInput, Button, ActionIcon, Divider } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type OverlayPdfsParameters, type OverlayMode } from '../../../hooks/tools/overlayPdfs/useOverlayPdfsParameters';
|
||||
import LocalIcon from '../../shared/LocalIcon';
|
||||
import { useFilesModalContext } from '../../../contexts/FilesModalContext';
|
||||
import styles from './OverlayPdfsSettings.module.css';
|
||||
|
||||
interface OverlayPdfsSettingsProps {
|
||||
parameters: OverlayPdfsParameters;
|
||||
onParameterChange: <K extends keyof OverlayPdfsParameters>(key: K, value: OverlayPdfsParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function OverlayPdfsSettings({ parameters, onParameterChange, disabled = false }: OverlayPdfsSettingsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
|
||||
const handleOverlayFilesChange = (files: File[]) => {
|
||||
onParameterChange('overlayFiles', files);
|
||||
// Reset counts to match number of files if in FixedRepeatOverlay
|
||||
if (parameters.overlayMode === 'FixedRepeatOverlay') {
|
||||
const nextCounts = files.map((_, i) => parameters.counts[i] && parameters.counts[i] > 0 ? parameters.counts[i] : 1);
|
||||
onParameterChange('counts', nextCounts);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModeChange = (mode: OverlayMode) => {
|
||||
onParameterChange('overlayMode', mode);
|
||||
if (mode !== 'FixedRepeatOverlay') {
|
||||
onParameterChange('counts', []);
|
||||
} else if (parameters.overlayFiles?.length > 0) {
|
||||
onParameterChange('counts', parameters.overlayFiles.map((_, i) => parameters.counts[i] && parameters.counts[i] > 0 ? parameters.counts[i] : 1));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenOverlayFilesModal = () => {
|
||||
if (disabled) return;
|
||||
openFilesModal({
|
||||
customHandler: (files: File[]) => {
|
||||
handleOverlayFilesChange([...(parameters.overlayFiles || []), ...files]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>{t('overlay-pdfs.mode.label', 'Overlay Mode')}</Text>
|
||||
<Select
|
||||
data={[
|
||||
{ value: 'SequentialOverlay', label: t('overlay-pdfs.mode.sequential', 'Sequential Overlay') },
|
||||
{ value: 'InterleavedOverlay', label: t('overlay-pdfs.mode.interleaved', 'Interleaved Overlay') },
|
||||
{ value: 'FixedRepeatOverlay', label: t('overlay-pdfs.mode.fixedRepeat', 'Fixed Repeat Overlay') },
|
||||
]}
|
||||
value={parameters.overlayMode}
|
||||
onChange={(v) => handleModeChange((v || 'SequentialOverlay') as OverlayMode)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>{t('overlay-pdfs.position.label', 'Overlay Position')}</Text>
|
||||
<SegmentedControl
|
||||
value={String(parameters.overlayPosition)}
|
||||
onChange={(v) => onParameterChange('overlayPosition', (v === '1' ? 1 : 0) as 0 | 1)}
|
||||
data={[
|
||||
{ label: t('overlay-pdfs.position.foreground', 'Foreground'), value: '0' },
|
||||
{ label: t('overlay-pdfs.position.background', 'Background'), value: '1' },
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{parameters.overlayMode === 'FixedRepeatOverlay' && (
|
||||
<>
|
||||
<Divider />
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>{t('overlay-pdfs.counts.label', 'Overlay Counts')}</Text>
|
||||
{parameters.overlayFiles?.length > 0 ? (
|
||||
<Stack gap="xs">
|
||||
{parameters.overlayFiles.map((_, index) => (
|
||||
<Group key={index} gap="xs" wrap="nowrap">
|
||||
<Text size="sm" className={styles.countLabel}>
|
||||
{t('overlay-pdfs.counts.item', 'Count for file')} {index + 1}
|
||||
</Text>
|
||||
<NumberInput
|
||||
min={1}
|
||||
step={1}
|
||||
value={parameters.counts[index] ?? 1}
|
||||
onChange={(value) => {
|
||||
const next = [...(parameters.counts || [])];
|
||||
next[index] = Number(value) || 1;
|
||||
onParameterChange('counts', next);
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('overlay-pdfs.counts.noFiles', 'Add overlay files to configure counts')}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>{t('overlay-pdfs.overlayFiles.label', 'Overlay Files')}</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
onClick={handleOpenOverlayFilesModal}
|
||||
disabled={disabled}
|
||||
leftSection={<LocalIcon icon="add" width="14" height="14" />}
|
||||
fullWidth
|
||||
>
|
||||
{parameters.overlayFiles?.length > 0
|
||||
? t('overlay-pdfs.overlayFiles.addMore', 'Add more PDFs...')
|
||||
: t('overlay-pdfs.overlayFiles.placeholder', 'Choose PDF(s)...')}
|
||||
</Button>
|
||||
|
||||
{parameters.overlayFiles?.length > 0 && (() => {
|
||||
return (
|
||||
<div className={styles.fileListContainer}>
|
||||
<Stack gap="xs">
|
||||
{parameters.overlayFiles.map((file, index) => (
|
||||
<Group
|
||||
key={index}
|
||||
justify="space-between"
|
||||
p="xs"
|
||||
className={styles.fileItem}
|
||||
>
|
||||
<Group gap="xs" className={styles.fileGroup}>
|
||||
<div className={styles.fileNameContainer}>
|
||||
<div
|
||||
className={styles.fileName}
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
</div>
|
||||
</div>
|
||||
<Text size="xs" c="dimmed" className={styles.fileSize}>
|
||||
({(file.size / 1024).toFixed(1)} KB)
|
||||
</Text>
|
||||
</Group>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
className={styles.removeButton}
|
||||
onClick={() => {
|
||||
const next = (parameters.overlayFiles || []).filter((_, i) => i !== index);
|
||||
handleOverlayFilesChange(next);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="14" height="14" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
56
frontend/src/components/tooltips/useOverlayPdfsTips.ts
Normal file
56
frontend/src/components/tooltips/useOverlayPdfsTips.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useOverlayPdfsTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t('overlay-pdfs.tooltip.header.title', 'Overlay PDFs Overview')
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t('overlay-pdfs.tooltip.description.title', 'Description'),
|
||||
description: t(
|
||||
'overlay-pdfs.tooltip.description.text',
|
||||
'Combine a base PDF with one or more overlay PDFs. Overlays can be applied page-by-page in different modes and placed in the foreground or background.'
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('overlay-pdfs.tooltip.mode.title', 'Overlay Mode'),
|
||||
description: t(
|
||||
'overlay-pdfs.tooltip.mode.text',
|
||||
'Choose how to distribute overlay pages across the base PDF pages.'
|
||||
),
|
||||
bullets: [
|
||||
t('overlay-pdfs.tooltip.mode.sequential', 'Sequential Overlay: Use pages from the first overlay PDF until it ends, then move to the next.'),
|
||||
t('overlay-pdfs.tooltip.mode.interleaved', 'Interleaved Overlay: Take one page from each overlay in turn.'),
|
||||
t('overlay-pdfs.tooltip.mode.fixedRepeat', 'Fixed Repeat Overlay: Take a set number of pages from each overlay before moving to the next. Use Counts to set the numbers.')
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('overlay-pdfs.tooltip.position.title', 'Overlay Position'),
|
||||
description: t(
|
||||
'overlay-pdfs.tooltip.position.text',
|
||||
'Foreground places the overlay on top of the page. Background places it behind.'
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('overlay-pdfs.tooltip.overlayFiles.title', 'Overlay Files'),
|
||||
description: t(
|
||||
'overlay-pdfs.tooltip.overlayFiles.text',
|
||||
'Select one or more PDFs to overlay on the base. The order of these files affects how pages are applied in Sequential and Fixed Repeat modes.'
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('overlay-pdfs.tooltip.counts.title', 'Counts (Fixed Repeat only)'),
|
||||
description: t(
|
||||
'overlay-pdfs.tooltip.counts.text',
|
||||
'Provide a positive number for each overlay file showing how many pages to take before moving to the next. Required when mode is Fixed Repeat.'
|
||||
)
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user