diff --git a/docker/frontend/src/App.tsx b/docker/frontend/src/App.tsx index 49d9b7f..60d082b 100644 --- a/docker/frontend/src/App.tsx +++ b/docker/frontend/src/App.tsx @@ -675,7 +675,10 @@ export function Workspace() { ); useEffect(() => { - if (!engine || !previewBytes) return; + // While the viewfinder is up it grades the camera's frames with this very + // recipe, so a preview behind it would only compete for the same renderer — + // and the user is watching the live one. It comes back with the camera. + if (!engine || !previewBytes || shooting) return; const token = ++renderToken.current; const timer = setTimeout(async () => { setBusy(t('act.exporting')); @@ -730,7 +733,7 @@ export function Workspace() { }, PREVIEW_DEBOUNCE_MS); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [engine, previewBytes, recipe, frameId, cropEditing, cropLive, cropRect, rotation, straighten, flipH, flipV, useGeotag, gpsInfo, gpsStyle, markOn, markText, markStyle, comparing]); + }, [engine, previewBytes, recipe, frameId, cropEditing, cropLive, cropRect, rotation, straighten, flipH, flipV, useGeotag, gpsInfo, gpsStyle, markOn, markText, markStyle, comparing, shooting]); // The source copy behind previewBytes, re-made only when previewMax moves: // applying a crop upgrades it, CANCEL/UNDO brings the 1600px one back. @@ -3215,7 +3218,12 @@ export function Workspace() { ) : null} - + void loadFile(file)} - onCamera={cameraSupported() ? () => setShooting(true) : undefined} + onCamera={ + // PRO, like the frames and the geotag: the button is there for + // everyone and the click says why when it is not theirs to open. + cameraSupported() ? () => (pro ? setShooting(true) : promptPro()) : undefined + } cropEditing={cropEditing} cropRect={cropRect} cropWH={cropWH} @@ -3318,7 +3330,22 @@ export function Workspace() { /> ) : null } - /> + /> + {shooting && engine ? ( + setShooting(false)} + /> + ) : null} + {naming === 'recipe' ? ( @@ -3369,21 +3396,6 @@ export function Workspace() { /> ) : null} - {shooting && engine ? ( - setShooting(false)} - /> - ) : null} - {choosingExport ? ( +
{/* The stage and the file are one look; the live frame is shown whole, so the user composes against the geometry the export will use. */} diff --git a/docker/frontend/src/ui/ImageStage.tsx b/docker/frontend/src/ui/ImageStage.tsx index 0860101..bf1ef46 100644 --- a/docker/frontend/src/ui/ImageStage.tsx +++ b/docker/frontend/src/ui/ImageStage.tsx @@ -292,7 +292,8 @@ export function ImageStage({ onFile: (file: File) => void; // The other way a picture arrives: shot here, on the device's own camera, and // graded by the same pipeline the export uses (see CameraView). Absent on a - // build with no camera to open. + // build with no camera to open, and PRO — the button carries the badge and the + // click says why when the account is not one that may open it. onCamera?: () => void; // Crop editor: shown over the un-cropped preview until APPLY commits it. cropEditing?: boolean; @@ -1440,6 +1441,7 @@ export function ImageStage({ }} > {t('cam.open')} + PRO ) : null}

{t('drop.demoLimit', { px: 2048 })}

@@ -2000,6 +2002,7 @@ export function ImageStage({ {onCamera ? ( ) : null} {onSaveOriginal ? (