feat(library & studio): add EXIF info bar, portrait frame panning, and multi-photo selection with Panorama/HDR context menu

This commit is contained in:
2026-10-07 10:44:59 +07:00
parent 7b836984fc
commit 3b4c49c41d
9 changed files with 196 additions and 49 deletions
+14 -1
View File
@@ -15,7 +15,7 @@ import { RecipeCreatePanel, type RecipeDraft } from './ui/RecipeCreatePanel';
import { useI18n } from './i18n/I18nProvider';
import { loadEngine, MARK_FACES } from './engine/assets';
import { renderPhoto, type RenderAssets } from './engine/exportEngine';
import { download, fileToBytes, readCapturedAt, readGps, readIso, resizedJpeg, scaledJpeg } from './engine/imageOps';
import { download, fileToBytes, readCapturedAt, readGps, readIso, readSpecs, resizedJpeg, scaledJpeg, type ShotSpecs } from './engine/imageOps';
import { developRaw, isRawName } from './engine/rawDevelop';
import { heicToJpeg, isHeicName } from './engine/heicDevelop';
import { clearRaw, readRaw, storeRaw } from './engine/rawStore';
@@ -383,6 +383,7 @@ export function Workspace() {
// UNDO and every knob still speak about the look the click left behind.
const [preview, setPreview] = useState<Recipe | null>(null);
const [frameId, setFrameId] = useState<FrameId>(() => session.frameId ?? 'none');
const [framePanX, setFramePanX] = useState<number>(() => session.framePanX ?? 0);
const [crop, setCrop] = useState<CropRatio>(() => session.crop ?? 'none');
const [cropRect, setCropRect] = useState<CropRect>(() => session.cropRect ?? DEFAULT_CROP_RECT);
const [cropApplied, setCropApplied] = useState(() => session.cropApplied ?? false);
@@ -440,6 +441,7 @@ export function Workspace() {
const [gps, setGps] = useState<GPSInfo | null>(() => session.gps ?? null);
// ISO off the loaded photo's EXIF; null means the strip prints AUTO.
const [iso, setIso] = useState<number | null>(null);
const [shotSpecs, setShotSpecs] = useState<ShotSpecs | null>(null);
// When the loaded photo was taken, off its own EXIF. The GPS stamp prints this
// and not the clock, whichever way the position was come by: a photo from the
// library can be years old, and the location typed in for it today is still the
@@ -936,6 +938,7 @@ export function Workspace() {
setGpsManual({ lat: '', lng: '' });
setUseGeotag(false);
setIso(null);
setShotSpecs(null);
setError(null);
setBusy(null);
void forgetPhoto();
@@ -1024,6 +1027,7 @@ export function Workspace() {
const tagged = exifBytes ?? bytes;
const found = await readGps(tagged);
const foundIso = await readIso(tagged);
const foundSpecs = await readSpecs(tagged);
// The photo's own date, held apart from its GPS: a frame with a date but no
// position still stamps the date when the position is typed in or asked of
// the device (see the two places the clock is fallen back on, below).
@@ -1053,6 +1057,7 @@ export function Workspace() {
setPreviewBytes(preview);
setGps(found);
setIso(foundIso);
setShotSpecs(foundSpecs);
const def = defaultLook();
restoreLook(def);
setUndo([def]);
@@ -3903,6 +3908,14 @@ export function Workspace() {
cropRect={cropRect}
cropWH={cropWH}
dims={stageDims}
frameId={cropEditing ? 'none' : frameId}
framePanX={framePanX}
onFramePanXChange={(pan) => {
setFramePanX(pan);
setRecipe((prev) => ({ ...prev, framePanX: pan }));
}}
shotSpecs={shotSpecs}
place={place}
onCropRectChange={setCropRect}
onStageZoom={onStageZoom}
canCompare={!!source}