import type { Asset } from './shared'; import { useCallback, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router'; import { Archive, ArchiveRestore, ChevronLeft, ChevronRight, Download, Heart, Info, Trash2, Undo2, X, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { ASSET_PARAM, originalUrl, thumbUrl, videoUrl } from './shared'; import { AssetInfoPanel } from './AssetInfoPanel'; import { useAsset } from './usePhotosData'; import { useAssetActions } from './useAssetActions'; // The lightbox. Which asset is open lives in `?asset=`, so it is linkable, survives a reload, and closes // with the browser's back button — the rule from docs/navigation-audit.md applied to the one piece of this // screen that most wants to be a modal with a useState behind it. type AssetViewerProps = { /** Every asset currently on screen, in display order — this is what prev/next walk. */ ids: string[]; token: string | null; /** Trash shows restore/delete-forever instead of archive/delete. */ inTrash?: boolean; }; export const AssetViewer = ({ ids, token, inTrash = false }: AssetViewerProps) => { const [params, setParams] = useSearchParams(); const [showInfo, setShowInfo] = useState(false); const openId = params.get(ASSET_PARAM); const { data: asset } = useAsset(openId); const index = openId ? ids.indexOf(openId) : -1; const prev = index > 0 ? ids[index - 1] : undefined; const next = index >= 0 && index < ids.length - 1 ? ids[index + 1] : undefined; const goTo = useCallback( (id: string | undefined) => { if (!id) return; setParams( (current) => { const nextParams = new URLSearchParams(current); nextParams.set(ASSET_PARAM, id); return nextParams; }, { replace: true }, ); }, [setParams], ); // Replace, not push: stepping through fifty photos shouldn't bury the grid fifty entries deep in history. // Opening the viewer pushed one entry, so one Back still returns to where you were. const close = useCallback(() => { setParams((current) => { const nextParams = new URLSearchParams(current); nextParams.delete(ASSET_PARAM); return nextParams; }); }, [setParams]); useEffect(() => { if (!openId) return; const onKey = (ev: KeyboardEvent) => { if (ev.key === 'Escape') close(); else if (ev.key === 'ArrowLeft') goTo(prev); else if (ev.key === 'ArrowRight') goTo(next); else if (ev.key === 'i') setShowInfo((value) => !value); else return; ev.preventDefault(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [openId, prev, next, close, goTo]); if (!openId) return null; return (
setShowInfo((value) => !value)} onClose={close} onAdvance={() => (next ? goTo(next) : close())} />
goTo(prev)} /> {asset?.type === 'VIDEO' ? (
{showInfo && ( )}
); }; type NavButtonProps = { side: 'left' | 'right'; disabled: boolean; onClick: () => void }; const NavButton = ({ side, disabled, onClick }: NavButtonProps) => ( ); type ViewerHeaderProps = { asset: Asset | undefined; id: string; token: string | null; inTrash: boolean; showInfo: boolean; onToggleInfo: () => void; onClose: () => void; onAdvance: () => void; }; const ViewerHeader = ({ asset, id, token, inTrash, showInfo, onToggleInfo, onClose, onAdvance }: ViewerHeaderProps) => { const { setFavorite, setArchived, trash, restore, deleteForever } = useAssetActions(); const archived = asset?.visibility === 'archive'; // Anything that removes the asset from the current view advances to the next one first — otherwise the // viewer is left pointing at an id that no longer exists in the grid behind it. const removeThen = (run: () => void) => { run(); onAdvance(); }; return (
{asset?.originalFileName} {inTrash ? ( <> ) : ( <> )}
); };