web: the histogram opens top-left, and CLEAR asks before it forgets
The histogram used to park itself in the top-right corner on the first paint; it now starts at the top-left of the photo and is dragged from there, the way the rest of the overlay is. Nothing else changed in it — same drag, same clamping, same resize. The stage also gains a CLEAR button, sitting before the picker button, which is now OPEN PHOTO. CLEAR takes the photo off the stage, but not before asking: SAVE PHOTO files it first and only then clears, EXPORT IMAGE writes the JPEG and then clears, CLEAR WITHOUT SAVING drops it there and then, and CANCEL leaves everything alone. Saving from that modal resumes the clear once the file has really landed — a guest, a capped account or a cancelled name prompt never loses the frame. Clearing forgets the working photo (source, preview, GPS, ISO, and the IndexedDB copy session.ts now deletes), while the look, the crop and the undo history stay put, so the next photo opens on the same settings the way replacing a photo already did.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { useI18n } from '../i18n/I18nProvider';
|
||||
|
||||
// CLEAR takes the photo off the stage, and the work standing on it goes with
|
||||
// it: the look lives in the session, not in the picture. So the button asks
|
||||
// first, and the two ways of keeping something are one tap each — file the
|
||||
// frame in MY PHOTOS so it can be picked up again, or put a JPEG in the user's
|
||||
// hands. Neither is forced: CLEAR WITHOUT SAVING is a plain discard, and
|
||||
// closing the dialog does nothing at all.
|
||||
export function ClearPhotoModal({
|
||||
onSave,
|
||||
onExport,
|
||||
onDiscard,
|
||||
onClose,
|
||||
}: {
|
||||
onSave: () => void;
|
||||
onExport: () => void;
|
||||
onDiscard: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<div className="modal" data-key="clear-modal">
|
||||
<h2>{t('clear.title')}</h2>
|
||||
<p className="hint" data-key="clear-hint">
|
||||
{t('clear.hint')}
|
||||
</p>
|
||||
<button type="button" className="btn" data-key="clear-save" onClick={onSave}>
|
||||
{t('clear.save')}
|
||||
</button>
|
||||
<button type="button" className="btn" data-key="clear-export" onClick={onExport}>
|
||||
{t('clear.export')}
|
||||
</button>
|
||||
<button type="button" className="btn adm-danger" data-key="clear-discard" onClick={onDiscard}>
|
||||
{t('clear.discard')}
|
||||
</button>
|
||||
<button type="button" className="btn ghost" data-key="clear-cancel" onClick={onClose}>
|
||||
{t('clear.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ function curve(bins: number[], max: number, close: boolean): string {
|
||||
export function Histogram({ url, onClose }: { url: string; onClose: () => void }) {
|
||||
const { t } = useI18n();
|
||||
const [data, setData] = useState<Histogram | null>(null);
|
||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
|
||||
const [pos, setPos] = useState<{ x: number; y: number }>({ x: 12, y: 12 });
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const boxRef = useRef<HTMLDivElement>(null);
|
||||
const dragRef = useRef<{ px: number; py: number; x: number; y: number } | null>(null);
|
||||
@@ -98,16 +98,6 @@ export function Histogram({ url, onClose }: { url: string; onClose: () => void }
|
||||
return { x: Math.min(Math.max(8, x), hi(b.width, m.width)), y: Math.min(Math.max(8, y), hi(b.height, m.height)) };
|
||||
}, []);
|
||||
|
||||
// First paint: park it in the top-right corner of the photo.
|
||||
useEffect(() => {
|
||||
if (pos) return;
|
||||
const wrap = boxRef.current?.parentElement;
|
||||
const box = boxRef.current;
|
||||
if (!wrap || !box) return;
|
||||
const b = wrap.getBoundingClientRect();
|
||||
setPos({ x: Math.max(8, b.width - box.getBoundingClientRect().width - 12), y: 12 });
|
||||
}, [pos]);
|
||||
|
||||
// The photo's own box is what the frame is parked against, and it shrinks
|
||||
// when a column opens beside the stage — not only when the window does. So
|
||||
// the frame watches that box, not the window, and is pulled back inside it.
|
||||
@@ -151,12 +141,15 @@ export function Histogram({ url, onClose }: { url: string; onClose: () => void }
|
||||
// that starts to hide the shadows.
|
||||
const max = Math.max(1, ...(data ? [...data.lum, ...data.r, ...data.g, ...data.b] : [1]));
|
||||
|
||||
// The shape is parked in the photo's top-left corner as soon as it appears —
|
||||
// that is where the eye already is when a frame lands — and the user drags it
|
||||
// wherever they want it from there.
|
||||
return (
|
||||
<div
|
||||
className={`hist${dragging ? ' dragging' : ''}`}
|
||||
data-key="histogram"
|
||||
ref={boxRef}
|
||||
style={{ left: pos?.x ?? 12, top: pos?.y ?? 12, visibility: pos ? 'visible' : 'hidden' }}
|
||||
style={{ left: pos.x, top: pos.y }}
|
||||
>
|
||||
<div
|
||||
className="hist-head"
|
||||
|
||||
@@ -66,6 +66,9 @@ export function ImageStage({
|
||||
onStageZoom,
|
||||
canCompare = false,
|
||||
onSaveOriginal,
|
||||
// Takes the photo off the stage. What is on it goes first — the confirmation
|
||||
// offers to file the frame or export it, so nothing is lost by accident.
|
||||
onClear,
|
||||
picking = false,
|
||||
onPickColor,
|
||||
pickPanel = null,
|
||||
@@ -96,6 +99,7 @@ export function ImageStage({
|
||||
// Puts the untouched photo file itself in the owner's hands — on a phone that
|
||||
// is the share sheet, whose "Save Image" is what reaches the photo library.
|
||||
onSaveOriginal?: () => void;
|
||||
onClear?: () => void;
|
||||
// Eyedropper mode: clicks on the photo report the colour underneath instead of
|
||||
// panning it. The HSL mixer's PICK chip owns this flag.
|
||||
picking?: boolean;
|
||||
@@ -831,7 +835,12 @@ export function ImageStage({
|
||||
|
||||
{previewUrl ? (
|
||||
<div className="toolbar">
|
||||
<button type="button" className="btn" onClick={() => inputRef.current?.click()}>
|
||||
{onClear ? (
|
||||
<button type="button" className="btn" data-key="clear-photo" onClick={onClear}>
|
||||
{t('drop.clear')}
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" className="btn" data-key="open-photo" onClick={() => inputRef.current?.click()}>
|
||||
{t('drop.replace')}
|
||||
</button>
|
||||
{onSaveOriginal ? (
|
||||
|
||||
Reference in New Issue
Block a user