From d7a8e0681e2d1e73f2863c7ea291db7cd927f175 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Thu, 10 Sep 2026 09:58:50 +0700 Subject: [PATCH] Add CANCEL to the watermark editor, scope the mark's touch layer to that editor, and stop a library frame following the user onto the camera The custom watermark captured EVERY image touch whenever the chip was ON with text, in both modes: after switching from a library photo to the camera there was no way to give the image back to tap-to-focus, and a frame picked for the still rode along onto the live preview (and into the capture). - AdjustPanel: CANCEL chip next to ACCEPT. It drops the un-accepted draft and restores the last ACCEPTED mark (none if nothing was accepted). - Viewfinder: draws the mark whenever the chip is ON with text, but only lets it CAPTURE touches while its editor is open (new wmEditing prop). A double-tap ON the mark still re-opens that editor from the library viewer. - App: ACCEPT and CANCEL now close the editor (that closes the placement layer with it). Switching mode closes the deck panels, and leaves the frame behind: camera mode has no FRAME picker to undo one. Verified on device (9a6a7277): camera tap locks AE/AF with the watermark left ON; library polaroid no longer appears on the live preview after the switch; CANCEL reverts the chip to CUSTOM WATERMARK OFF and closes the panel; ACCEPT closes it and the next capture embeds the mark (amber text found at 0.5/0.375 of the 3000x4000 export), while a capture before ACCEPT carries none. --- App.tsx | 51 +++++++++++++++++++++++++----- src/components/AdjustmentPanel.tsx | 12 +++++++ src/components/Viewfinder.tsx | 37 ++++++++++++++++++---- 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/App.tsx b/App.tsx index 350f923..8182a8a 100644 --- a/App.tsx +++ b/App.tsx @@ -97,6 +97,26 @@ export default function App() { setCustomWm(wm); if (!wm.enabled) setCustomWmApplied({ ...DEFAULT_CUSTOM_WM }); }; + // Leaving the watermark editor = closing its panel. The mark keeps capturing + // image touches ONLY while that panel is open (Viewfinder's wmArmed), so this + // is what hands the photo back to tap-to-focus / pinch / double-tap zoom. + const closeWatermarkEditor = () => { + setOpenTab(null); + setOpenParamKey(null); + }; + // ACCEPT: the draft becomes the mark the export burns in, then the editor + // closes so the image stops being a placement surface. + const handleAcceptCustomWm = () => { + setCustomWmApplied({ ...customWm }); + closeWatermarkEditor(); + }; + // CANCEL: drop every un-accepted edit and fall back to the last ACCEPTED mark + // (none at all when nothing was ever accepted), then close exactly like + // ACCEPT — the escape hatch a running session previously lacked. + const handleCancelCustomWm = () => { + setCustomWm({ ...customWmApplied }); + closeWatermarkEditor(); + }; // Composition aspect-ratio (FULL / 4:3 / 3:2 upright) + RAW DNG sidecar. const [aspectRatio, setAspectRatio] = useState('full'); const [rawEnabled, setRawEnabled] = useState(false); @@ -816,15 +836,20 @@ export default function App() { // Switching back to camera: drop GPS that belongs to a library photo, so the // live view (and any capture) never stamps a stale location. const handleSetMode = (next: 'camera' | 'library') => { + if (next === mode) return; if (next === 'camera' && gpsInfo && gpsInfo.source !== 'device') { setGpsInfo(null); } - // FRAME only exists in library mode — close the panel when switching back - // to camera so a dead frame chip row never lingers over the live preview. - if (next === 'camera' && openTab === 'frame') { - setOpenTab(null); - setOpenParamKey(null); - } + // The deck panels belong to one mode only (FRAME exists in library alone, + // and the WATERMARK panel is the watermark's edit session), so every switch + // closes them: no frame chip row over the live preview, and no watermark + // placement layer left eating the camera's tap-to-focus. + setOpenTab(null); + setOpenParamKey(null); + // A frame was picked for the still it sits on; camera mode has no FRAME + // picker to undo it, so it must never follow the user onto the live camera + // (and into the capture, which burns the frame in). + if (next === 'camera') setSelectedFrame('none'); setMode(next); }; @@ -975,8 +1000,17 @@ export default function App() { meteringAdjustments={meteringAdj} imageAdjustTarget={imageAdjustTarget} customWm={customWm} + // The mark only captures image touches while its panel is open, so a + // watermark left ON cannot block tap-to-focus or pinch/zoom. + wmEditing={openTab === 'watermark'} onWmPositionChange={(x, y) => setCustomWm((p) => ({ ...p, x, y }))} - onWmEdit={() => setWmEditNonce((n) => n + 1)} + onWmEdit={() => { + // Double-tap on the mark re-opens its editor (and with it the + // placement layer), then focuses the input. + setOpenTab('watermark'); + setOpenParamKey(null); + setWmEditNonce((n) => n + 1); + }} /> @@ -1019,7 +1053,8 @@ export default function App() { onDeleteRecipe={handleDeleteRecipe} customWm={customWm} onUpdateCustomWm={handleUpdateCustomWm} - onAcceptCustomWm={() => setCustomWmApplied({ ...customWm })} + onAcceptCustomWm={handleAcceptCustomWm} + onCancelCustomWm={handleCancelCustomWm} wmEditNonce={wmEditNonce} /> diff --git a/src/components/AdjustmentPanel.tsx b/src/components/AdjustmentPanel.tsx index e245dcf..316e3ac 100644 --- a/src/components/AdjustmentPanel.tsx +++ b/src/components/AdjustmentPanel.tsx @@ -58,6 +58,10 @@ interface AdjustmentPanelProps { // ACCEPT: copy the live draft into the mark the export burns in. Edits stay a // draft (viewfinder preview only) until this is pressed. onAcceptCustomWm: () => void; + // CANCEL: throw the un-accepted draft away — back to the mark that was last + // ACCEPTED (or none at all) — and close the editor. The way out of a running + // watermark edit that previously did not exist. + onCancelCustomWm: () => void; // Bumped by a double-tap on the mark: put the caret back in the text input. wmEditNonce: number; } @@ -139,6 +143,7 @@ export default function AdjustmentPanel({ customWm, onUpdateCustomWm, onAcceptCustomWm, + onCancelCustomWm, wmEditNonce, onToggleRotation, }: AdjustmentPanelProps) { @@ -526,6 +531,13 @@ export default function AdjustmentPanel({ active: true, onPress: onAcceptCustomWm, })} + {customWm.enabled && + renderChip({ + key: 'custom-wm-cancel', + label: 'CANCEL', + active: false, + onPress: onCancelCustomWm, + })} {customWm.enabled && renderChip(groupChip('wmColor'))} {customWm.enabled && renderChip(groupChip('wmSize'))} {customWm.enabled && renderChip(groupChip('wmFont'))} diff --git a/src/components/Viewfinder.tsx b/src/components/Viewfinder.tsx index 3dd2467..3f737a7 100644 --- a/src/components/Viewfinder.tsx +++ b/src/components/Viewfinder.tsx @@ -103,6 +103,12 @@ interface ViewfinderProps { y: number; }; onWmPositionChange?: (x: number, y: number) => void; + // True while the WATERMARK panel is open — that panel IS the mark's edit + // session. The mark draws whenever the CUSTOM chip is ON with text, but it + // only CAPTURES image touches (placement drags in camera mode, click-through + // to focus / zoom) while this is set, so a watermark left ON can never + // swallow the camera's tap-to-focus or the library's pinch/double-tap. + wmEditing?: boolean; } export interface ViewfinderHandle { @@ -186,6 +192,7 @@ const Viewfinder = forwardRef(function Viewfi customWm = { enabled: false, text: '', x: 0.5, y: 0.5 }, onWmPositionChange, onWmEdit, + wmEditing = false, photoRotation = 0, }: ViewfinderProps, ref @@ -1018,11 +1025,10 @@ const Viewfinder = forwardRef(function Viewfi // Armed = CUSTOM chip ON + non-empty text + live position callback. While // armed, touches on the image place/move the text (see the wm branches in the // camera + library gesture handlers); this is the only way to position it. - const wmArmed = !!( - customWm.enabled && - customWm.text.trim().length > 0 && - onWmPositionChange - ); + // Drawn in both modes whenever the chip is ON with text; TOUCHED only while + // the WATERMARK panel is open (wmEditing) — see the prop comment. + const wmVisible = !!(customWm.enabled && customWm.text.trim().length > 0); + const wmArmed = !!(wmVisible && wmEditing && onWmPositionChange); const [wmDragPos, setWmDragPos] = useState<{ x: number; y: number } | null>(null); // Photo/window area the 0..1 fractions map onto — identical rule to // renderGPSWatermark so both marks share the user-visible coordinate space. @@ -1077,7 +1083,7 @@ const Viewfinder = forwardRef(function Viewfi // Bounding box of the drawn mark (same anchor maths as renderCustomWatermark) // so a double-tap ON the text re-opens the editor instead of moving it. const wmBox = () => { - if (!wmArmed || !wmFont) return null; + if (!wmVisible || !wmFont) return null; const area = wmArea(); const fracX = wmDragPos?.x ?? customWm.x; const fracY = wmDragPos?.y ?? customWm.y; @@ -1162,6 +1168,23 @@ const Viewfinder = forwardRef(function Viewfi if (wasPinch) return; // a double-tap is always a single finger const x = e.nativeEvent.locationX; const y = e.nativeEvent.locationY; + // Mark drawn but its editor CLOSED: a double-tap ON the mark re-opens the + // editor (and the placement layer with it). A single tap is NOT consumed — + // this block records nothing, so the viewer rules below still run and the + // photo keeps pinch / double-tap zoom / pan. + if (!wmArmed && wmVisible && libZoom.s <= 1.01) { + const prev = lastTapRef.current; + if ( + prev && + Date.now() - prev.t < 350 && + Math.hypot(x - prev.x, y - prev.y) < 50 && + wmBoxHit(wmBox(), x, y) + ) { + lastTapRef.current = null; + onWmEdit?.(); + return; + } + } // Watermark armed: a tap places + commits the text and skips the double-tap // zoom bookkeeping (double-tap zoom is disabled while placing). if (wmArmed && libZoom.s <= 1.01) { @@ -1344,7 +1367,7 @@ const Viewfinder = forwardRef(function Viewfi }; const renderCustomWatermark = () => { - if (!wmArmed || !wmFont) return null; + if (!wmVisible || !wmFont) return null; const area = wmArea(); const fracX = wmDragPos?.x ?? customWm.x; const fracY = wmDragPos?.y ?? customWm.y;