fix(library): fix Olympus ORF raw color conversion and add custom photo context menu modal

This commit is contained in:
2026-10-05 18:22:22 +07:00
parent fbed70b9da
commit c486397388
5 changed files with 130 additions and 11 deletions
+118 -5
View File
@@ -228,6 +228,7 @@ export function Library() {
// one more folder; `root` means it landed on a row at the top level — the head
// of a tree — which also carries folding the whole tree.
const [menu, setMenu] = useState<{ folder: LibraryFolder | null; root?: boolean; rel?: string; node?: string; x: number; y: number } | null>(null);
const [photoMenu, setPhotoMenu] = useState<{ photo: LibraryPhoto; x: number; y: number } | null>(null);
// A reading of this roll that is still waiting its turn. The menu is where
// that wait is called off, so it has to know one exists.
const queued = menu?.folder ? scanQueue().some((j) => j.folder === menu.folder!.name) : false;
@@ -574,13 +575,17 @@ export function Library() {
// behind a full-screen scrim: a scrim would eat the click that opens the next
// folder, which is half of how this screen is used.
useEffect(() => {
if (!menu) return;
if (!menu && !photoMenu) return;
const away = (e: Event) => {
if (e.target instanceof Element && e.target.closest('.lib-menu')) return;
setMenu(null);
setPhotoMenu(null);
};
const key = (e: KeyboardEvent) => {
if (e.key === 'Escape') setMenu(null);
if (e.key === 'Escape') {
setMenu(null);
setPhotoMenu(null);
}
};
document.addEventListener('pointerdown', away);
document.addEventListener('keydown', key);
@@ -588,7 +593,7 @@ export function Library() {
document.removeEventListener('pointerdown', away);
document.removeEventListener('keydown', key);
};
}, [menu]);
}, [menu, photoMenu]);
// A reading asked for here is a request, not a reading: one walk runs at a time,
// and a folder asked for while another is being read waits its turn — which is
@@ -1517,7 +1522,19 @@ export function Library() {
<div className="adm-grid" ref={wall.ref}>
{wall.above > 0 ? <span className="adm-wall-gap" style={{ height: wall.above }} aria-hidden="true" /> : null}
{painted.map((photo) => (
<article className="adm-card" key={photo.id} data-key={`lib-photo-${photo.id}`}>
<article
className="adm-card"
key={photo.id}
data-key={`lib-photo-${photo.id}`}
onContextMenu={(e) => {
e.preventDefault();
setPhotoMenu({
photo,
x: Math.min(e.clientX, window.innerWidth - 220),
y: Math.min(e.clientY, window.innerHeight - 160),
});
}}
>
{tile(photo, () => void open(photo))}
<div className="adm-meta">
<b>{photo.name}</b>
@@ -1545,11 +1562,31 @@ export function Library() {
src={stageUrl || urls[frame.id]!}
alt=""
onClick={() => void open(frame)}
onContextMenu={(e) => {
e.preventDefault();
setPhotoMenu({
photo: frame,
x: Math.min(e.clientX, window.innerWidth - 220),
y: Math.min(e.clientY, window.innerHeight - 160),
});
}}
title={t('lib.open')}
style={zoom === 1 ? undefined : { transformOrigin: zoomAt, transform: `scale(${zoom})` }}
/>
) : (
<div className="lib-noimg" onClick={() => void open(frame)} title={t('lib.open')}>
<div
className="lib-noimg"
onClick={() => void open(frame)}
onContextMenu={(e) => {
e.preventDefault();
setPhotoMenu({
photo: frame,
x: Math.min(e.clientX, window.innerWidth - 220),
y: Math.min(e.clientY, window.innerHeight - 160),
});
}}
title={t('lib.open')}
>
RAW
</div>
)}
@@ -1626,6 +1663,14 @@ export function Library() {
data-key={`lib-thumb-${photo.id}`}
title={photo.name}
onClick={() => setUp(photo.id)}
onContextMenu={(e) => {
e.preventDefault();
setPhotoMenu({
photo,
x: Math.min(e.clientX, window.innerWidth - 220),
y: Math.min(e.clientY, window.innerHeight - 160),
});
}}
>
{tile(photo)}
<span className="adm-thumb-cap">{photo.name}</span>
@@ -1797,6 +1842,74 @@ export function Library() {
)}
</div>
) : null}
{photoMenu ? (
<div
className="lib-menu"
data-key="lib-photo-menu"
style={{ left: photoMenu.x, top: photoMenu.y }}
onContextMenu={(e) => e.preventDefault()}
>
<button
type="button"
className="lib-menu-item"
data-key="lib-photo-open-explorer"
onClick={async () => {
const photo = photoMenu.photo;
setPhotoMenu(null);
const folderPath = photo.dir ? `${photo.folder}/${photo.dir}` : photo.folder;
const fullPath = `${folderPath}/${photo.name}`;
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(fullPath);
setNote(t('lib.copiedPath', { path: fullPath }));
}
} catch {}
}}
>
<span className="lib-menu-icon" aria-hidden="true">📁</span>
{t('lib.openExplorer')}
</button>
<button
type="button"
className="lib-menu-item"
data-key="lib-photo-open-location"
onClick={() => {
const photo = photoMenu.photo;
setPhotoMenu(null);
const targetNode = photo.dir ? `${photo.folder}/${photo.dir}` : photo.folder;
setNode(targetNode);
setUp(photo.id);
const parts = targetNode.split('/');
setExpanded((prev) => {
const next = new Set(prev);
for (let i = 1; i <= parts.length; i++) {
next.add(parts.slice(0, i).join('/'));
}
return next;
});
}}
>
<span className="lib-menu-icon" aria-hidden="true">🎯</span>
{t('lib.openLocation')}
</button>
<button
type="button"
className="lib-menu-item"
data-key="lib-photo-open-studio"
onClick={() => {
const photo = photoMenu.photo;
setPhotoMenu(null);
void open(photo);
}}
>
<span className="lib-menu-icon" aria-hidden="true">🎨</span>
{t('lib.openStudio')}
</button>
</div>
) : null}
</div>
);
}
+4 -6
View File
@@ -441,13 +441,11 @@ function invert3x3(m: number[][]): number[][] | null {
function getCamToSrgbMatrix(cd: any): number[][] {
if (cd?.rgb_cam && Array.isArray(cd.rgb_cam) && cd.rgb_cam.length >= 3) {
const m = [
[cd.rgb_cam[0][0], cd.rgb_cam[0][1], cd.rgb_cam[0][2]],
[cd.rgb_cam[1][0], cd.rgb_cam[1][1], cd.rgb_cam[1][2]],
[cd.rgb_cam[2][0], cd.rgb_cam[2][1], cd.rgb_cam[2][2]],
return [
[cd.rgb_cam[0][0] || 0, cd.rgb_cam[0][1] || 0, cd.rgb_cam[0][2] || 0],
[cd.rgb_cam[1][0] || 0, cd.rgb_cam[1][1] || 0, cd.rgb_cam[1][2] || 0],
[cd.rgb_cam[2][0] || 0, cd.rgb_cam[2][1] || 0, cd.rgb_cam[2][2] || 0],
];
const inv = invert3x3(m);
if (inv) return inv;
}
if (cd?.cam_xyz && Array.isArray(cd.cam_xyz) && cd.cam_xyz.length >= 3) {
+4
View File
@@ -258,6 +258,10 @@ export const en: Dict = {
'lib.subsHide': 'THIS FOLDER ONLY',
'lib.subsHint': 'Frames in subfolders: the whole branch under the open folder, or only what that folder holds itself.',
'lib.open': 'Open in the studio',
'lib.openExplorer': 'Open in window explorer',
'lib.openLocation': 'Open at location',
'lib.openStudio': 'Open in studio',
'lib.copiedPath': 'Copied location to clipboard: {path}',
'lib.edited': 'edited',
'lib.size': '{mb} MB',
'lib.count': '{n} photos',
+4
View File
@@ -271,6 +271,10 @@ export const vi = {
'lib.subsHint':
'Ảnh trong thư mục con: hiện cả nhánh dưới thư mục đang chọn, hoặc chỉ ảnh của chính thư mục đó.',
'lib.open': 'Mở trong studio',
'lib.openExplorer': 'Open in window explorer',
'lib.openLocation': 'Open at location',
'lib.openStudio': 'Open in studio',
'lib.copiedPath': 'Đã sao chép đường dẫn: {path}',
'lib.edited': 'đã chỉnh',
'lib.size': '{mb} MB',
'lib.count': '{n} ảnh',
Binary file not shown.