feat(frame): move the watermark controls into a FRAME sub-panel

This commit is contained in:
2026-09-11 10:00:10 +07:00
parent ba971f136c
commit 4131c9f401
3 changed files with 90 additions and 67 deletions
+6 -3
View File
@@ -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<string | null>(null);
// FRAME → WATERMARK sub-panel is open (the rail's WATERMARK tab is gone).
const [wmPanelOpen, setWmPanelOpen] = useState(false);
const [libraryImageUri, setLibraryImageUri] = useState<string | null>(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}
/>
</View>
)}
<ToolRail activeTab={openTab} onToggle={handleToggleTab} mode={mode} />
<ToolRail activeTab={openTab} onToggle={handleToggleTab} />
<CameraControls
lastPhotoUri={lastPhotoUri}
+79 -56
View File
@@ -16,7 +16,7 @@ import RecipeCreateModal from './RecipeCreateModal';
// Tapping a parameter chip toggles its slider row; chips stay visible.
// Darkroom-flavoured: amber = active/edited, red = destructive, mono labels.
type TabId = 'recipes' | 'favorites' | 'iq' | 'wb' | 'filters' | 'frame' | 'watermark';
type TabId = 'recipes' | 'favorites' | 'iq' | 'wb' | 'filters' | 'frame';
// Choice-group keys that open a strip above the chip row: color chrome groups
// (wb tab) and the D.RANGE selector (iq tab).
type GroupKey =
@@ -31,7 +31,9 @@ type GroupKey =
| 'gpsColor'
| 'gpsName'
| 'gpsTime'
| 'crop';
| 'crop'
// Not a strip: the WATERMARK row REPLACES the frame chips (sub-panel).
| 'wm';
interface AdjustmentPanelProps {
activeTab: TabId;
@@ -111,6 +113,9 @@ interface AdjustmentPanelProps {
// the GPS lines and the custom text, in the preview and in the export.
wmRotation: number;
onRotateWm: () => 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<GroupKey, { label: string; off: string; value: string; options: { v: string; d: string }[]; onPick: (v: string) => void }> = {
const groupDefs: Record<Exclude<GroupKey, 'wm'>, { 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<GroupKey, 'wm'>): 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 (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ alignItems: 'center', paddingHorizontal: 8, paddingVertical: 8 }}
>
{renderChip({
key: 'wm-back',
label: '< FRAME',
active: false,
onPress: () => setOpenGroup(null),
})}
{wmChips().map(renderChip)}
</ScrollView>
);
}
return (
<ScrollView
horizontal
@@ -719,55 +790,7 @@ export default function AdjustmentPanel({
active: wallLandscape,
onPress: onToggleWallLandscape,
})}
</ScrollView>
);
case 'watermark':
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ alignItems: 'center', paddingHorizontal: 8, paddingVertical: 8 }}
>
{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())}
</ScrollView>
);
@@ -820,7 +843,7 @@ export default function AdjustmentPanel({
</View>
</View>
)}
{openGroup && (
{openGroup && openGroup !== 'wm' && (
<View className="border-b border-zinc-800/60">
<ScrollView
horizontal
@@ -872,7 +895,7 @@ export default function AdjustmentPanel({
</ScrollView>
</View>
)}
{activeTab === 'watermark' && customWm.enabled && (
{activeTab === 'frame' && openGroup === 'wm' && customWm.enabled && (
<View className="border-b border-amber-500/30 bg-amber-500/5 px-3 py-2">
<TextInput
ref={wmInputRef}
+5 -8
View File
@@ -2,14 +2,11 @@ import React from 'react';
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import * as Haptics from 'expo-haptics';
export type TabId = 'recipes' | 'favorites' | 'iq' | 'wb' | 'filters' | 'frame' | 'watermark';
export type TabId = 'recipes' | 'favorites' | 'iq' | 'wb' | 'filters' | 'frame';
interface ToolRailProps {
activeTab: TabId | null;
onToggle: (tab: TabId) => 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 (
<View className="py-1.5">
<ScrollView