From ba7e579986654e96d7e5399adffcfbbe27b9d187 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Fri, 11 Sep 2026 09:08:37 +0700 Subject: [PATCH] feat(panel): RESET shows a star while the look differs from a clean start --- App.tsx | 29 +++++++++++++++++++++++++++++ src/components/AdjustmentPanel.tsx | 9 ++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/App.tsx b/App.tsx index 4c63b93..2c66dbf 100644 --- a/App.tsx +++ b/App.tsx @@ -28,6 +28,14 @@ import { PARAM_DEFS } from './src/utils/paramDefs'; // @ts-ignore import './global.css'; +// Shallow value diff over the union of both objects' keys: undefined and a +// missing key both read as "no value", so an old session without the newer +// optional knobs is not reported as an edit. +const differs = (a: T, b: T) => { + const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set; + return [...keys].some((k) => (a[k] ?? null) !== (b[k] ?? null)); +}; + const SESSION_KEY = 'saved_session'; const STARTUP_MODE_KEY = 'startup_mode'; // Camera/device settings that survive restarts regardless of startup mode @@ -523,8 +531,28 @@ export default function App() { setWmRotation(0); setPhotoRotation(0); setWallLandscape(false); + setCropRatio('none'); + setCropRect(DEFAULT_CROP_RECT); }; + // RESET lights up while the look differs from what handleResetAll puts back — + // every clause here has a matching setter there, or the star never goes out. + // The clean-start values: the startup sim, neutral knobs, no frame, no crop, + // both marks at their defaults, GPS stamp off, nothing rotated. + const resetDirty = + (selectedRecipe?.id ?? 'sim-provia') !== 'sim-provia' || + differs(adjustments, DEFAULT_ADJUSTMENTS) || + selectedFrame !== 'none' || + differs(customWm, DEFAULT_CUSTOM_WM) || + differs(customWmApplied, DEFAULT_CUSTOM_WM) || + differs(gpsWm, DEFAULT_GPS_WM) || + useGeotag || + wmRotation !== 0 || + photoRotation !== 0 || + wallLandscape || + cropRatio !== 'none' || + differs(cropRect, DEFAULT_CROP_RECT); + const handleUpdateAdjustments = (updates: Partial) => { // Storm guard, generic across every param: same-value writes (slider // micro-jitter / repeated pointer-move events) still build a fresh @@ -1276,6 +1304,7 @@ export default function App() { currentRecipeId={selectedRecipe?.id || ''} adjustments={adjustments} onResetAll={handleResetAll} + resetDirty={resetDirty} selectedFrame={selectedFrame} cropRatio={cropRatio} onUpdateCropRatio={setCropRatio} diff --git a/src/components/AdjustmentPanel.tsx b/src/components/AdjustmentPanel.tsx index b61555e..73bcb6f 100644 --- a/src/components/AdjustmentPanel.tsx +++ b/src/components/AdjustmentPanel.tsx @@ -41,6 +41,9 @@ interface AdjustmentPanelProps { // RESET: the app's own defaults, from any tab — the startup look, every // parameter, the frame and the watermarks. onResetAll: () => void; + // True while the look differs from a clean start: the RESET chip says so + // ("RESET *", amber) instead of silently doing nothing visible. + resetDirty: boolean; selectedFrame: FrameId; // FRAME tab CROP. Only meaningful on the library's plain frames (the camera // composes through its ratio band, polaroid/wall through their own window), @@ -179,6 +182,7 @@ export default function AdjustmentPanel({ currentRecipeId, adjustments, onResetAll, + resetDirty, selectedFrame, cropRatio, onUpdateCropRatio, @@ -327,8 +331,11 @@ export default function AdjustmentPanel({ // one can sit off-screen just when it is wanted. const resetChip = (): ChipDef => ({ key: 'reset-all', - label: 'RESET', + // Trailing star + amber text: there is a change to undo (the chip is + // otherwise a plain neutral RESET that puts the code back to a clean start). + label: resetDirty ? 'RESET *' : 'RESET', active: false, + amberValue: resetDirty, onPress: onResetAll, });