2025-05-15 20:07:33 +01:00
|
|
|
import React, { useState } from "react";
|
2025-05-29 17:26:32 +01:00
|
|
|
import { useTranslation } from "react-i18next";
|
2025-06-27 18:00:35 +01:00
|
|
|
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Loader, Alert } from "@mantine/core";
|
2025-06-05 11:12:39 +01:00
|
|
|
import { FileWithUrl } from "../types/file";
|
|
|
|
import { fileStorage } from "../services/fileStorage";
|
2025-06-27 18:00:35 +01:00
|
|
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
2025-05-15 20:07:33 +01:00
|
|
|
|
2025-05-21 21:47:44 +01:00
|
|
|
export interface CompressProps {
|
2025-06-05 11:12:39 +01:00
|
|
|
files?: FileWithUrl[];
|
2025-05-21 21:47:44 +01:00
|
|
|
setDownloadUrl?: (url: string) => void;
|
|
|
|
setLoading?: (loading: boolean) => void;
|
2025-06-05 11:12:39 +01:00
|
|
|
params?: {
|
|
|
|
compressionLevel: number;
|
|
|
|
grayscale: boolean;
|
|
|
|
removeMetadata: boolean;
|
|
|
|
expectedSize: string;
|
|
|
|
aggressive: boolean;
|
|
|
|
};
|
|
|
|
updateParams?: (newParams: Partial<CompressProps["params"]>) => void;
|
2025-05-21 21:47:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const CompressPdfPanel: React.FC<CompressProps> = ({
|
|
|
|
files = [],
|
|
|
|
setDownloadUrl,
|
|
|
|
setLoading,
|
2025-06-05 11:12:39 +01:00
|
|
|
params = {
|
|
|
|
compressionLevel: 5,
|
|
|
|
grayscale: false,
|
|
|
|
removeMetadata: false,
|
|
|
|
expectedSize: "",
|
|
|
|
aggressive: false,
|
|
|
|
},
|
|
|
|
updateParams,
|
2025-05-21 21:47:44 +01:00
|
|
|
}) => {
|
2025-05-29 17:26:32 +01:00
|
|
|
const { t } = useTranslation();
|
2025-05-27 19:22:26 +01:00
|
|
|
|
2025-05-21 21:47:44 +01:00
|
|
|
const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
|
|
|
|
const [localLoading, setLocalLoading] = useState<boolean>(false);
|
2025-06-27 18:00:35 +01:00
|
|
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf");
|
2025-05-15 20:07:33 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
const {
|
|
|
|
compressionLevel,
|
|
|
|
grayscale,
|
|
|
|
removeMetadata,
|
|
|
|
expectedSize,
|
|
|
|
aggressive,
|
|
|
|
} = params;
|
|
|
|
|
2025-05-15 20:07:33 +01:00
|
|
|
// Update selection state if files prop changes
|
|
|
|
React.useEffect(() => {
|
|
|
|
setSelected(files.map(() => false));
|
|
|
|
}, [files]);
|
|
|
|
|
2025-05-21 21:47:44 +01:00
|
|
|
const handleCheckbox = (idx: number) => {
|
2025-05-15 20:07:33 +01:00
|
|
|
setSelected(sel => sel.map((v, i) => (i === idx ? !v : v)));
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleCompress = async () => {
|
|
|
|
const selectedFiles = files.filter((_, i) => selected[i]);
|
|
|
|
if (selectedFiles.length === 0) return;
|
|
|
|
setLocalLoading(true);
|
|
|
|
setLoading?.(true);
|
|
|
|
|
|
|
|
try {
|
2025-06-05 11:12:39 +01:00
|
|
|
const formData = new FormData();
|
|
|
|
|
|
|
|
// Handle IndexedDB files
|
|
|
|
for (const file of selectedFiles) {
|
|
|
|
if (!file.id) {
|
|
|
|
continue; // Skip files without an id
|
|
|
|
}
|
|
|
|
const storedFile = await fileStorage.getFile(file.id);
|
|
|
|
if (storedFile) {
|
|
|
|
const blob = new Blob([storedFile.data], { type: storedFile.type });
|
|
|
|
const actualFile = new File([blob], storedFile.name, {
|
|
|
|
type: storedFile.type,
|
|
|
|
lastModified: storedFile.lastModified
|
|
|
|
});
|
|
|
|
formData.append("fileInput", actualFile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
formData.append("compressionLevel", compressionLevel.toString());
|
|
|
|
formData.append("grayscale", grayscale.toString());
|
|
|
|
formData.append("removeMetadata", removeMetadata.toString());
|
|
|
|
formData.append("aggressive", aggressive.toString());
|
|
|
|
if (expectedSize) formData.append("expectedSize", expectedSize);
|
|
|
|
|
2025-05-15 20:07:33 +01:00
|
|
|
const res = await fetch("/api/v1/general/compress-pdf", {
|
|
|
|
method: "POST",
|
|
|
|
body: formData,
|
|
|
|
});
|
|
|
|
const blob = await res.blob();
|
2025-05-21 21:47:44 +01:00
|
|
|
setDownloadUrl?.(URL.createObjectURL(blob));
|
2025-06-05 11:12:39 +01:00
|
|
|
} catch (error) {
|
|
|
|
console.error('Compression failed:', error);
|
2025-05-15 20:07:33 +01:00
|
|
|
} finally {
|
|
|
|
setLocalLoading(false);
|
|
|
|
setLoading?.(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-06-27 18:00:35 +01:00
|
|
|
if (endpointLoading) {
|
|
|
|
return (
|
|
|
|
<Stack align="center" justify="center" h={200}>
|
|
|
|
<Loader size="md" />
|
|
|
|
<Text size="sm" c="dimmed">{t("loading", "Loading...")}</Text>
|
|
|
|
</Stack>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (endpointEnabled === false) {
|
|
|
|
return (
|
|
|
|
<Stack align="center" justify="center" h={200}>
|
|
|
|
<Alert color="red" title={t("error._value", "Error")} variant="light">
|
|
|
|
{t("endpointDisabled", "This feature is currently disabled.")}
|
|
|
|
</Alert>
|
|
|
|
</Stack>
|
|
|
|
);
|
|
|
|
}
|
2025-05-27 19:22:26 +01:00
|
|
|
|
2025-05-15 20:07:33 +01:00
|
|
|
return (
|
|
|
|
<Stack>
|
2025-05-29 17:26:32 +01:00
|
|
|
<Text fw={500} mb={4}>{t("multiPdfDropPrompt", "Select files to compress:")}</Text>
|
2025-05-21 21:47:44 +01:00
|
|
|
<Stack gap={4}>
|
2025-05-29 17:26:32 +01:00
|
|
|
{files.length === 0 && <Text c="dimmed" size="sm">{t("noFileSelected")}</Text>}
|
2025-05-15 20:07:33 +01:00
|
|
|
{files.map((file, idx) => (
|
|
|
|
<Checkbox
|
|
|
|
key={file.name + idx}
|
|
|
|
label={file.name}
|
|
|
|
checked={selected[idx] || false}
|
|
|
|
onChange={() => handleCheckbox(idx)}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</Stack>
|
2025-05-21 21:47:44 +01:00
|
|
|
<Stack gap={4} mb={14}>
|
2025-05-29 17:26:32 +01:00
|
|
|
<Text size="sm" style={{ minWidth: 140 }}>{t("compress.selectText.2", "Compression Level")}</Text>
|
2025-05-15 20:07:33 +01:00
|
|
|
<Slider
|
|
|
|
min={1}
|
|
|
|
max={9}
|
|
|
|
step={1}
|
|
|
|
value={compressionLevel}
|
2025-06-05 11:12:39 +01:00
|
|
|
onChange={(value) => updateParams?.({ compressionLevel: value })}
|
2025-05-15 20:07:33 +01:00
|
|
|
marks={[
|
|
|
|
{ value: 1, label: "1" },
|
|
|
|
{ value: 5, label: "5" },
|
|
|
|
{ value: 9, label: "9" },
|
|
|
|
]}
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
/>
|
2025-05-21 21:47:44 +01:00
|
|
|
</Stack>
|
2025-05-15 20:07:33 +01:00
|
|
|
<Checkbox
|
2025-05-29 17:26:32 +01:00
|
|
|
label={t("compress.grayscale.label", "Convert images to grayscale")}
|
2025-05-15 20:07:33 +01:00
|
|
|
checked={grayscale}
|
2025-06-05 11:12:39 +01:00
|
|
|
onChange={e => updateParams?.({ grayscale: e.currentTarget.checked })}
|
2025-05-15 20:07:33 +01:00
|
|
|
/>
|
|
|
|
<Checkbox
|
2025-05-29 17:26:32 +01:00
|
|
|
label={t("removeMetadata.submit", "Remove PDF metadata")}
|
2025-05-15 20:07:33 +01:00
|
|
|
checked={removeMetadata}
|
2025-06-05 11:12:39 +01:00
|
|
|
onChange={e => updateParams?.({ removeMetadata: e.currentTarget.checked })}
|
2025-05-15 20:07:33 +01:00
|
|
|
/>
|
|
|
|
<Checkbox
|
2025-05-29 17:26:32 +01:00
|
|
|
label={t("compress.selectText.1.1", "Aggressive compression (may reduce quality)")}
|
2025-05-15 20:07:33 +01:00
|
|
|
checked={aggressive}
|
2025-06-05 11:12:39 +01:00
|
|
|
onChange={e => updateParams?.({ aggressive: e.currentTarget.checked })}
|
2025-05-15 20:07:33 +01:00
|
|
|
/>
|
|
|
|
<TextInput
|
2025-05-29 17:26:32 +01:00
|
|
|
label={t("compress.selectText.5", "Expected output size")}
|
|
|
|
placeholder={t("compress.selectText.5", "e.g. 25MB, 10.8MB, 25KB")}
|
2025-05-15 20:07:33 +01:00
|
|
|
value={expectedSize}
|
2025-06-05 11:12:39 +01:00
|
|
|
onChange={e => updateParams?.({ expectedSize: e.currentTarget.value })}
|
2025-05-15 20:07:33 +01:00
|
|
|
/>
|
|
|
|
<Button
|
|
|
|
onClick={handleCompress}
|
|
|
|
loading={localLoading}
|
|
|
|
disabled={selected.every(v => !v)}
|
|
|
|
fullWidth
|
|
|
|
mt="md"
|
|
|
|
>
|
2025-05-29 17:26:32 +01:00
|
|
|
{t("compress.submit", "Compress")} {t("pdfPrompt", "PDF")}{selected.filter(Boolean).length > 1 ? "s" : ""}
|
2025-05-15 20:07:33 +01:00
|
|
|
</Button>
|
|
|
|
</Stack>
|
|
|
|
);
|
2025-05-21 21:47:44 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
export default CompressPdfPanel;
|