From 4131c9f40133fd0fc4f350b73bc7e0a6406f4c37 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Fri, 11 Sep 2026 10:00:10 +0700 Subject: [PATCH] feat(frame): move the watermark controls into a FRAME sub-panel --- App.tsx | 9 +- src/components/AdjustmentPanel.tsx | 135 +++++++++++++++++------------ src/components/ToolRail.tsx | 13 ++- 3 files changed, 90 insertions(+), 67 deletions(-) diff --git a/App.tsx b/App.tsx index 21c23b4..8cbac35 100644 --- a/App.tsx +++ b/App.tsx @@ -189,6 +189,8 @@ export default function App() { // local to AdjustmentPanel) because an open row arms drag-anywhere-on-the- // image adjustment in the Viewfinder. Cleared when the panel/tab closes. const [openParamKey, setOpenParamKey] = useState(null); + // FRAME → WATERMARK sub-panel is open (the rail's WATERMARK tab is gone). + const [wmPanelOpen, setWmPanelOpen] = useState(false); const [libraryImageUri, setLibraryImageUri] = useState(null); // Session photo history (camera captures + library exports). The camera @@ -1273,12 +1275,12 @@ export default function App() { wallLandscape={wallLandscape} // 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'} + wmEditing={openTab === 'frame' && wmPanelOpen} onWmPositionChange={(x, y) => setCustomWm((p) => ({ ...p, x, y }))} onWmEdit={() => { // Double-tap on the mark re-opens its editor (and with it the // placement layer), then focuses the input. - setOpenTab('watermark'); + setOpenTab('frame'); setOpenParamKey(null); setWmEditNonce((n) => n + 1); }} @@ -1356,11 +1358,12 @@ export default function App() { wmEditNonce={wmEditNonce} wmRotation={wmRotation} onRotateWm={() => setWmRotation((r) => (r + 90) % 360)} + onWmPanelChange={setWmPanelOpen} /> )} - + void; + // FRAME → WATERMARK sub-panel is open: App needs this to arm the mark's + // drag/pinch layer on the image (the old `activeTab === 'watermark'`). + onWmPanelChange: (open: boolean) => void; } interface ChipDef { @@ -222,6 +227,7 @@ export default function AdjustmentPanel({ onToggleRotation, wmRotation, onRotateWm, + onWmPanelChange, }: AdjustmentPanelProps) { // Double-tap on the mark in the viewfinder bumps wmEditNonce -> focus the // input again so the text can be retyped after the keyboard was dismissed. @@ -252,6 +258,11 @@ export default function AdjustmentPanel({ setShowRecipes(false); setCreateVisible(false); }, [activeTab]); + // The WATERMARK sub-panel is what arms the mark's drag layer on the image: + // one effect on the strip state keeps App's flag true exactly while it shows. + useEffect(() => { + onWmPanelChange(openGroup === 'wm'); + }, [openGroup]); // CROP is a plain-frame tool: the moment the selected frame drops it the chip // unmounts, so the ratio strip must close with it (no orphan CROP row). useEffect(() => { @@ -411,7 +422,6 @@ export default function AdjustmentPanel({ ), filters: toRows(PARAM_DEFS.filters), frame: [], - watermark: [], }; // TEMP chip's value: the remembered preset while the engine pair still @@ -477,7 +487,7 @@ export default function AdjustmentPanel({ ...[...names].sort().map((n) => ({ v: n, d: n })), ]; }, []); - const groupDefs: Record void }> = { + const groupDefs: Record, { label: string; off: string; value: string; options: { v: string; d: string }[]; onPick: (v: string) => void }> = { cx: { label: 'COLOR CHROME', off: 'none', value: a.colorChrome || 'none', options: chromeOptions, onPick: (v) => onUpdateAdjustments({ colorChrome: v as ColorAdjustments['colorChrome'] }), @@ -568,7 +578,7 @@ export default function AdjustmentPanel({ onPick: (v) => onUpdateGpsWm({ ...gpsWm, showTime: v === 'on' }), }, }; - const groupChip = (g: GroupKey): ChipDef => ({ + const groupChip = (g: Exclude): ChipDef => ({ key: `grp-${g}`, label: groupDefs[g].label, active: openGroup === g, @@ -581,6 +591,49 @@ export default function AdjustmentPanel({ label: wbTempLabel(), }); + // WATERMARK is a sub-panel of FRAME now (the rail tab is gone): its chip swaps + // the frame chips for the watermark controls, behind a breadcrumb back. + const wmOn = useGeotag || customWm.enabled; + const wmChip = (): ChipDef => ({ + key: 'grp-wm', + label: wmOn ? 'WATERMARK ON' : 'WATERMARK', + active: openGroup === 'wm', + amberValue: openGroup !== 'wm' && wmOn, + onPress: () => toggleGroup('wm'), + }); + const wmChips = (): ChipDef[] => [ + { + key: 'gps', + label: useGeotag ? 'GPS WATERMARK ON' : 'GPS WATERMARK OFF', + active: useGeotag, + onPress: () => onToggleGeotag(!useGeotag), + }, + ...(useGeotag ? [groupChip('gpsColor'), groupChip('gpsName'), groupChip('gpsTime')] : []), + { + key: 'custom-wm', + label: customWm.enabled ? 'CUSTOM WATERMARK ON' : 'CUSTOM WATERMARK OFF', + active: customWm.enabled, + onPress: () => onUpdateCustomWm({ ...customWm, enabled: !customWm.enabled }), + }, + ...(wmOn + ? [{ + key: 'wm-rotate', + label: `ROTATE ${wmRotation}°`, + active: wmRotation !== 0, + onPress: onRotateWm, + }] + : []), + ...(customWm.enabled + ? [ + { key: 'custom-wm-accept', label: 'ACCEPT', active: true, onPress: onAcceptCustomWm }, + { key: 'custom-wm-cancel', label: 'CANCEL', active: false, onPress: onCancelCustomWm }, + groupChip('wmColor'), + groupChip('wmSize'), + groupChip('wmFont'), + ] + : []), + ]; + // ---- per-tab chip rows (bottom, above the tool rail) ----------------------- const chipsRow = (() => { switch (activeTab) { @@ -693,6 +746,24 @@ export default function AdjustmentPanel({ ); case 'frame': + // FRAME → WATERMARK: the sub-panel row (breadcrumb back to the frames). + if (openGroup === 'wm') { + return ( + + {renderChip({ + key: 'wm-back', + label: '< FRAME', + active: false, + onPress: () => setOpenGroup(null), + })} + {wmChips().map(renderChip)} + + ); + } return ( - ); - - case 'watermark': - return ( - - {renderChip({ - key: 'gps', - label: useGeotag ? 'GPS WATERMARK ON' : 'GPS WATERMARK OFF', - active: useGeotag, - onPress: () => onToggleGeotag(!useGeotag), - })} - {useGeotag && renderChip(groupChip('gpsColor'))} - {useGeotag && renderChip(groupChip('gpsName'))} - {useGeotag && renderChip(groupChip('gpsTime'))} - {renderChip({ - key: 'custom-wm', - label: customWm.enabled ? 'CUSTOM WATERMARK ON' : 'CUSTOM WATERMARK OFF', - active: customWm.enabled, - onPress: () => onUpdateCustomWm({ ...customWm, enabled: !customWm.enabled }), - })} - {(useGeotag || customWm.enabled) && - renderChip({ - key: 'wm-rotate', - label: `ROTATE ${wmRotation}°`, - active: wmRotation !== 0, - onPress: onRotateWm, - })} - {customWm.enabled && - renderChip({ - key: 'custom-wm-accept', - label: 'ACCEPT', - 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'))} + {renderChip(wmChip())} ); @@ -820,7 +843,7 @@ export default function AdjustmentPanel({ )} - {openGroup && ( + {openGroup && openGroup !== 'wm' && ( )} - {activeTab === 'watermark' && customWm.enabled && ( + {activeTab === 'frame' && openGroup === 'wm' && customWm.enabled && ( void; - // FRAME only exists in library mode (camera shows every frame WYSIWYG through - // the viewfinder overlays); WATERMARK is available in both modes. - mode: 'camera' | 'library'; } const TOOLS: { id: TabId; label: string }[] = [ @@ -19,12 +16,12 @@ const TOOLS: { id: TabId; label: string }[] = [ { id: 'wb', label: 'WB' }, { id: 'filters', label: 'FX' }, { id: 'frame', label: 'FRAME' }, - { id: 'watermark', label: 'WATERMARK' }, ]; -export default function ToolRail({ activeTab, onToggle, mode }: ToolRailProps) { - // FRAME (frame picker) belongs to library stills only: camera mode hides it. - const tools = TOOLS.filter((t) => t.id !== 'frame' || mode === 'library'); +export default function ToolRail({ activeTab, onToggle }: ToolRailProps) { + // FRAME is on in BOTH modes: the camera composes through the frame's window + // WYSIWYG (and WATERMARK now lives inside this tab). + const tools = TOOLS; return (