mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(web): add target file size controls to image-to-pdf settings
This commit is contained in:
@@ -111,6 +111,13 @@ export function ImageToPdfSettings() {
|
|||||||
const [orientation, setOrientation] = useState<"portrait" | "landscape">("portrait");
|
const [orientation, setOrientation] = useState<"portrait" | "landscape">("portrait");
|
||||||
const [margin, setMargin] = useState(20);
|
const [margin, setMargin] = useState(20);
|
||||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||||
|
const [targetSizeValue, setTargetSizeValue] = useState<string>("");
|
||||||
|
const [targetSizeUnit, setTargetSizeUnit] = useState<"KB" | "MB">("MB");
|
||||||
|
const [compressionResult, setCompressionResult] = useState<{
|
||||||
|
targetMet: boolean;
|
||||||
|
jpegQuality: number;
|
||||||
|
targetRequested: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Local processing state so the ProgressCard renders reliably
|
// Local processing state so the ProgressCard renders reliably
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -151,6 +158,7 @@ export function ImageToPdfSettings() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setDownloadUrl(null);
|
setDownloadUrl(null);
|
||||||
|
setCompressionResult(null);
|
||||||
setProgress({ phase: "uploading", percent: 0, elapsed: 0 });
|
setProgress({ phase: "uploading", percent: 0, elapsed: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +171,14 @@ export function ImageToPdfSettings() {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
}
|
}
|
||||||
formData.append("settings", JSON.stringify({ pageSize, orientation, margin }));
|
const settings: Record<string, unknown> = { pageSize, orientation, margin };
|
||||||
|
if (targetSizeValue.trim() !== "") {
|
||||||
|
const numVal = Number.parseFloat(targetSizeValue);
|
||||||
|
if (!Number.isNaN(numVal) && numVal > 0) {
|
||||||
|
settings.targetSize = { value: numVal, unit: targetSizeUnit };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.append("settings", JSON.stringify(settings));
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhrRef.current = xhr;
|
xhrRef.current = xhr;
|
||||||
@@ -194,6 +209,7 @@ export function ImageToPdfSettings() {
|
|||||||
try {
|
try {
|
||||||
const result = JSON.parse(xhr.responseText);
|
const result = JSON.parse(xhr.responseText);
|
||||||
setDownloadUrl(result.downloadUrl);
|
setDownloadUrl(result.downloadUrl);
|
||||||
|
setCompressionResult(result.compression ?? null);
|
||||||
setProgress((prev) => ({ ...prev, phase: "complete", percent: 100 }));
|
setProgress((prev) => ({ ...prev, phase: "complete", percent: 100 }));
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to parse server response");
|
setError("Failed to parse server response");
|
||||||
@@ -224,7 +240,16 @@ export function ImageToPdfSettings() {
|
|||||||
xhr.setRequestHeader(key, value);
|
xhr.setRequestHeader(key, value);
|
||||||
});
|
});
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
}, [files, pageSize, orientation, margin, setProcessing, setError]);
|
}, [
|
||||||
|
files,
|
||||||
|
pageSize,
|
||||||
|
orientation,
|
||||||
|
margin,
|
||||||
|
targetSizeValue,
|
||||||
|
targetSizeUnit,
|
||||||
|
setProcessing,
|
||||||
|
setError,
|
||||||
|
]);
|
||||||
|
|
||||||
const hasFiles = files.length > 0;
|
const hasFiles = files.length > 0;
|
||||||
|
|
||||||
@@ -290,6 +315,35 @@ export function ImageToPdfSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="image-to-pdf-target-size" className="text-xs text-muted-foreground">
|
||||||
|
Target file size (optional)
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2 mt-0.5">
|
||||||
|
<input
|
||||||
|
id="image-to-pdf-target-size"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step="any"
|
||||||
|
placeholder="e.g. 2"
|
||||||
|
value={targetSizeValue}
|
||||||
|
onChange={(e) => setTargetSizeValue(e.target.value)}
|
||||||
|
className="flex-1 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||||
|
data-testid="image-to-pdf-target-size-value"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={targetSizeUnit}
|
||||||
|
onChange={(e) => setTargetSizeUnit(e.target.value as "KB" | "MB")}
|
||||||
|
className="px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||||
|
data-testid="image-to-pdf-target-size-unit"
|
||||||
|
>
|
||||||
|
<option value="KB">KB</option>
|
||||||
|
<option value="MB">MB</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-1">Leave empty for maximum quality</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PdfPagePreview
|
<PdfPagePreview
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
@@ -335,6 +389,23 @@ export function ImageToPdfSettings() {
|
|||||||
Download PDF
|
Download PDF
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{compressionResult && (
|
||||||
|
<div
|
||||||
|
data-testid="image-to-pdf-compression-result"
|
||||||
|
className={`text-xs px-3 py-2 rounded ${
|
||||||
|
compressionResult.targetMet
|
||||||
|
? "bg-green-500/10 text-green-600"
|
||||||
|
: "bg-amber-500/10 text-amber-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{compressionResult.targetMet ? (
|
||||||
|
<span>Target met (JPEG quality: {compressionResult.jpegQuality}%)</span>
|
||||||
|
) : (
|
||||||
|
<span>Could not reach target. File compressed to minimum quality.</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user