c0c99a9672
The server still never sees a photo, so the model has to run in the page. Real-ESRGAN x4v3 ships as a 4.9MB ONNX in public/models and is loaded lazily on the first export that actually needs it; the wasm runtime is copied next to CanvasKit at build time and stays lazily fetched, cached for 30 days. Vite is told onnxruntime-web is external-wasm so no 28MB asset lands in the bundle. UNCHANGED keeps the old path and the tier cap; 2K/4K/custom upscale only when the request is larger than the photo being edited, otherwise they resize down. Guests keep UNCHANGED and 2K. Tiling is 256px with an 8px overlap, so memory follows the target size rather than four times it.
87 lines
3.2 KiB
TypeScript
87 lines
3.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { useI18n } from '../i18n/I18nProvider';
|
|
|
|
// Export size. The photo on the stage has a longest edge; the three fixed
|
|
// answers name the sizes people ask for by name (2048/4096), the fourth takes a
|
|
// number, and UNCHANGED is what the button used to do on its own. Anything
|
|
// bigger than the photo is invented by the model (engine/superRes) before the
|
|
// render, so the file really carries the pixels the user asked for.
|
|
const CUSTOM_MAX = 8192;
|
|
|
|
export function ExportModal({
|
|
longest,
|
|
maxDim,
|
|
onPick,
|
|
onClose,
|
|
}: {
|
|
/** Longest edge of the photo as it stands, crop included. */
|
|
longest: number;
|
|
/** The tier's own ceiling, or null for none. */
|
|
maxDim: number | null;
|
|
onPick: (target: number | null) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const { t } = useI18n();
|
|
const [custom, setCustom] = useState(String(Math.min(CUSTOM_MAX, Math.max(1024, longest * 2))));
|
|
|
|
const pick = (n: number | null) => onPick(n !== null && maxDim !== null ? Math.min(n, maxDim) : n);
|
|
const customValue = Number(custom);
|
|
const customOk = Number.isFinite(customValue) && customValue >= 1 && customValue <= CUSTOM_MAX;
|
|
|
|
return (
|
|
<div className="modal-backdrop" onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
|
|
<div className="modal" data-key="export-modal">
|
|
<h2>{t('export.title')}</h2>
|
|
<p className="hint" data-key="export-hint">
|
|
{t('export.hint', { px: longest })}
|
|
</p>
|
|
<button type="button" className="btn" data-key="export-same" onClick={() => pick(null)}>
|
|
{t('export.same')}
|
|
</button>
|
|
<button type="button" className="btn" data-key="export-2k" onClick={() => pick(2048)}>
|
|
{t('export.2k')}
|
|
</button>
|
|
{/* A capped account cannot be handed a size its tier refuses, so the two
|
|
sizes over the ceiling are not offered at all. */}
|
|
{maxDim === null || maxDim > 2048 ? (
|
|
<>
|
|
<button type="button" className="btn" data-key="export-4k" onClick={() => pick(4096)}>
|
|
{t('export.4k')}
|
|
</button>
|
|
<div className="export-custom" data-key="export-custom">
|
|
<label className="hint" htmlFor="export-custom-px">
|
|
{t('export.customLabel')}
|
|
</label>
|
|
<input
|
|
id="export-custom-px"
|
|
className="field"
|
|
type="number"
|
|
min={1}
|
|
max={CUSTOM_MAX}
|
|
value={custom}
|
|
onChange={(e) => setCustom(e.target.value)}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="btn"
|
|
data-key="export-custom-go"
|
|
disabled={!customOk}
|
|
onClick={() => customOk && pick(Math.round(customValue))}
|
|
>
|
|
{t('export.custom')}
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<p className="hint" data-key="export-cap">
|
|
{t('export.cap', { px: maxDim })}
|
|
</p>
|
|
)}
|
|
<button type="button" className="btn ghost" data-key="export-cancel" onClick={onClose}>
|
|
{t('clear.cancel')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|