initial commit
This commit is contained in:
105
frontend/src/components/MediaViewer/MediaViewer.tsx
Normal file
105
frontend/src/components/MediaViewer/MediaViewer.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, BrowseEntry, MediaItem } from "../../api/client";
|
||||
import TagPanel from "../TagPanel/TagPanel";
|
||||
|
||||
interface Props {
|
||||
mediaId: number;
|
||||
siblings: BrowseEntry[];
|
||||
onClose: () => void;
|
||||
onNavigate: (id: number) => void;
|
||||
}
|
||||
|
||||
export default function MediaViewer({ mediaId, siblings, onClose, onNavigate }: Props) {
|
||||
const { data: item } = useQuery<MediaItem>({
|
||||
queryKey: ["media", mediaId],
|
||||
queryFn: () => api.media.get(mediaId),
|
||||
});
|
||||
|
||||
const mediaSiblings = siblings.filter((e) => e.media_item_id != null);
|
||||
const currentIndex = mediaSiblings.findIndex((e) => e.media_item_id === mediaId);
|
||||
const prevId = currentIndex > 0 ? mediaSiblings[currentIndex - 1].media_item_id : null;
|
||||
const nextId = currentIndex < mediaSiblings.length - 1 ? mediaSiblings[currentIndex + 1].media_item_id : null;
|
||||
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
if (e.key === "ArrowLeft" && prevId) onNavigate(prevId);
|
||||
if (e.key === "ArrowRight" && nextId) onNavigate(nextId);
|
||||
}
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [prevId, nextId, onClose, onNavigate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: "fixed", inset: 0, background: "rgba(0,0,0,0.85)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
display: "flex", gap: 16, background: "#1a1a1a", borderRadius: 8,
|
||||
padding: 16, maxWidth: "95vw", maxHeight: "95vh", overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{/* Prev */}
|
||||
<button
|
||||
onClick={() => prevId && onNavigate(prevId)}
|
||||
disabled={!prevId}
|
||||
style={{ alignSelf: "center", fontSize: 24, background: "none", border: "none", color: prevId ? "#fff" : "#444", cursor: prevId ? "pointer" : "default" }}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
|
||||
{/* Media */}
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }}>
|
||||
{item?.filename && (
|
||||
<div style={{ color: "#ccc", fontSize: 13 }}>{item.filename}</div>
|
||||
)}
|
||||
{item?.media_type === "image" && (
|
||||
<img
|
||||
src={api.media.fileUrl(mediaId)}
|
||||
alt={item.filename}
|
||||
style={{ maxWidth: "70vw", maxHeight: "80vh", objectFit: "contain" }}
|
||||
/>
|
||||
)}
|
||||
{item?.media_type === "video" && (
|
||||
<video
|
||||
src={api.media.fileUrl(mediaId)}
|
||||
controls
|
||||
style={{ maxWidth: "70vw", maxHeight: "80vh" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Next */}
|
||||
<button
|
||||
onClick={() => nextId && onNavigate(nextId)}
|
||||
disabled={!nextId}
|
||||
style={{ alignSelf: "center", fontSize: 24, background: "none", border: "none", color: nextId ? "#fff" : "#444", cursor: nextId ? "pointer" : "default" }}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
|
||||
{/* Tag panel */}
|
||||
{item && (
|
||||
<div style={{ color: "#fff", borderLeft: "1px solid #333", paddingLeft: 16, minWidth: 200 }}>
|
||||
<TagPanel item={item} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Close */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{ position: "absolute", top: 16, right: 16, background: "none", border: "none", color: "#fff", fontSize: 20, cursor: "pointer" }}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user