feat(studio): add right-click context menu (Solo mode, Collapse all, Expand all) for DevelopPanels, pin Color Chrome sub-chips, and fix library single view sorting & time range select

This commit is contained in:
2026-10-07 17:35:20 +07:00
parent eb47afc63e
commit 4704e936ba
9 changed files with 587 additions and 190 deletions
+59 -14
View File
@@ -747,7 +747,8 @@ export function Workspace() {
// The photo half of the session, back out of IndexedDB. It waits for the
// account: which file may open depends on the tier, and the check reads it
// from the render — run it too early and a PRO's own RAW is turned away.
const openLibraryPhotoRef = useRef<((id: string) => Promise<void>) | null>(null);
// When the stored session is empty but a RAW is still parked in OPFS, the RAW
// is what "the last photo" means: develop it again rather than open on
// nothing. A frame handed over by the catalogue (`/app?lib=<id>`) wins over
@@ -756,19 +757,22 @@ export function Workspace() {
useEffect(() => {
if (!authReady) return;
let alive = true;
const wanted = new URLSearchParams(window.location.search).get('lib');
if (wanted) openLibraryPhoto(wanted);
else
const checkAndLoad = () => {
const wanted = new URLSearchParams(window.location.search).get('lib');
if (wanted && openLibraryPhotoRef.current) {
void openLibraryPhotoRef.current(wanted);
return true;
}
return false;
};
if (!checkAndLoad()) {
loadPhoto()
.then(async (photo) => {
if (!alive) return;
const show = (row: { name: string; bytes: Uint8Array }) =>
adoptPhoto(row.bytes, row.name, new Blob([row.bytes as BlobPart], { type: 'image/jpeg' }), true);
// A session photo named like a RAW is a develop an older build left
// there — the RAW branch writes none any more — and it carries that
// build's colour, which is how a fixed develop kept opening olive. The
// parked RAW is the copy that is not stale, so it is read first; the
// session's is only shown when nothing is parked to develop.
if (photo && !isRawName(photo.name)) return show(photo);
const parked = await readRaw();
if (!alive) return;
@@ -777,10 +781,17 @@ export function Workspace() {
await adoptPhoto(jpeg, parked.name, new Blob([jpeg as BlobPart], { type: 'image/jpeg' }), true, parked.bytes);
})
.catch(() => undefined);
}
const onPop = () => {
checkAndLoad();
};
window.addEventListener('popstate', onPop);
return () => {
alive = false;
window.removeEventListener('popstate', onPop);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authReady]);
useEffect(() => {
@@ -2091,6 +2102,10 @@ export function Workspace() {
[loadFile, applyRecipe, clearPreview, t]
);
useEffect(() => {
openLibraryPhotoRef.current = openLibraryPhoto;
}, [openLibraryPhoto]);
// What the catalogue remembers about a frame is the look it was left at, filed
// back on every change — debounced, because a slider drag fires this per frame
// of the drag and IndexedDB is on the wrong side of that.
@@ -3359,10 +3374,40 @@ export function Workspace() {
}))}
/>
),
// The two Color Chromes close WB: same switch, the other axis of the two
// tracks above it, so they are read under the pair they are read against —
// and on the tab whose colour they are.
wbFoot: <ChipRow grid chips={chromeChips} />,
wbFoot: (
<div className="dev-box" data-key="dev-box-color-chrome" style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '8px 0' }}>
<div>
<span className="dev-box-title" style={{ marginBottom: '4px', display: 'block', fontSize: '11px', opacity: 0.8 }}>COLOR CHROME</span>
<ChipRow
grid
chips={CHROME_OPTIONS.map((opt) => ({
key: `cx-${opt.v}`,
label: opt.d,
active: (recipe.adjustments.colorChrome || 'none') === opt.v,
onClick: () => {
remember();
setAdjustment({ colorChrome: opt.v as Recipe['adjustments']['colorChrome'] });
},
}))}
/>
</div>
<div>
<span className="dev-box-title" style={{ marginBottom: '4px', display: 'block', fontSize: '11px', opacity: 0.8 }}>CHROME BLUE</span>
<ChipRow
grid
chips={CHROME_OPTIONS.map((opt) => ({
key: `cxb-${opt.v}`,
label: opt.d,
active: (recipe.adjustments.colorChromeBlue || 'none') === opt.v,
onClick: () => {
remember();
setAdjustment({ colorChromeBlue: opt.v as Recipe['adjustments']['colorChromeBlue'] });
},
}))}
/>
</div>
</div>
),
// TONE CURVE closes TONE: the graph on the photo is the shape of the range
// the panel's seven rows are, so it stands under them rather than in a row
// of its own at the top of the tab — and it glows amber once the graph is