mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
179 lines
8.7 KiB
React
179 lines
8.7 KiB
React
// modules/admin/pages/assets/ViewVideoAsset.jsx
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { useParams, useNavigate } from "react-router-dom";
|
||
import { ArrowLeft, Lock, Globe } from "lucide-react";
|
||
import api from "@/utils/api.util";
|
||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||
|
||
import { useAssets } from "@/contexts/AdminAssetsContext";
|
||
import { Spinner } from "@/components/ui/spinner";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Separator } from "@/components/ui/separator";
|
||
|
||
function MetaRow({ label, value }) {
|
||
if (!value && value !== 0) return null;
|
||
return (
|
||
<div className="flex items-start gap-3 py-2">
|
||
<span className="text-muted-foreground text-sm w-36 shrink-0">{label}</span>
|
||
<span className="text-sm font-medium break-all">{String(value)}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function formatDuration(seconds) {
|
||
if (!seconds && seconds !== 0) return null;
|
||
const h = Math.floor(seconds / 3600);
|
||
const m = Math.floor((seconds % 3600) / 60);
|
||
const s = Math.floor(seconds % 60);
|
||
return [h > 0 ? String(h).padStart(2, "0") : null, String(m).padStart(2, "0"), String(s).padStart(2, "0")]
|
||
.filter(Boolean)
|
||
.join(":");
|
||
}
|
||
|
||
export default function ViewVideoAsset() {
|
||
const { assetId } = useParams();
|
||
const navigate = useNavigate();
|
||
const { fmtDateTime } = useDateFormat();
|
||
|
||
const { selectedAsset, loading, fetchAsset } = useAssets();
|
||
const [streamUrl, setStreamUrl] = useState(null);
|
||
|
||
useEffect(() => {
|
||
if (assetId) fetchAsset(assetId);
|
||
}, [assetId]);
|
||
|
||
useEffect(() => {
|
||
if (!selectedAsset) return;
|
||
setStreamUrl(null);
|
||
if (selectedAsset.storage_provider !== "s3") {
|
||
setStreamUrl(selectedAsset.file_url ?? null);
|
||
return;
|
||
}
|
||
api.post("/admin/media/token", { asset_id: selectedAsset.asset_id })
|
||
.then(({ data }) => setStreamUrl(
|
||
`${(import.meta.env.VITE_API_URL ?? "http://localhost:3024").replace(/\/$/, "")}/client/media/stream/${data?.data?.token}`
|
||
))
|
||
.catch(() => setStreamUrl(null));
|
||
}, [selectedAsset]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center h-96">
|
||
<Spinner className="size-8" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!selectedAsset) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center h-96 gap-3 text-muted-foreground">
|
||
<p>Asset not found.</p>
|
||
<Button variant="outline" onClick={() => navigate(-1)}>Go Back</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const a = selectedAsset;
|
||
|
||
return (
|
||
<div className="max-w-7xl mx-auto px-4 py-6 space-y-6">
|
||
|
||
{/* ── Header ── */}
|
||
<div className="flex items-center gap-3">
|
||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
</Button>
|
||
<div className="flex-1 min-w-0">
|
||
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
|
||
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||
|
||
{/* ── Video player ── */}
|
||
<div className="lg:col-span-3 space-y-3">
|
||
<div className="rounded-lg border bg-black overflow-hidden aspect-video flex items-center justify-center">
|
||
{streamUrl ? (
|
||
<video
|
||
key={streamUrl}
|
||
controls
|
||
controlsList="nodownload"
|
||
disablePictureInPicture
|
||
onContextMenu={(e) => e.preventDefault()}
|
||
className="w-full h-full"
|
||
poster={a.thumbnail_url ?? undefined}
|
||
>
|
||
<source src={streamUrl} type={a.mime_type ?? "video/mp4"} />
|
||
Your browser does not support the video tag.
|
||
</video>
|
||
) : (
|
||
<p className="text-muted-foreground text-sm">No video source available.</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Thumbnail strip */}
|
||
{a.thumbnail_url && (
|
||
<div className="rounded-lg border overflow-hidden bg-muted/30">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground px-3 py-2">Thumbnail</p>
|
||
<img
|
||
src={a.thumbnail_url}
|
||
alt="Thumbnail"
|
||
className="w-full max-h-40 object-cover"
|
||
draggable={false}
|
||
onContextMenu={(e) => e.preventDefault()}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* ── Metadata panel ── */}
|
||
<div className="lg:col-span-2 space-y-4">
|
||
<div className="rounded-lg border bg-card p-4 space-y-1">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Video Info</p>
|
||
<MetaRow label="Original Name" value={a.original_name} />
|
||
<MetaRow label="Extension" value={a.extension} />
|
||
<MetaRow label="File Size" value={a.file_size ? `${(a.file_size / (1024 * 1024)).toFixed(2)} MB` : null} />
|
||
<MetaRow label="Resolution" value={a.resolution} />
|
||
<MetaRow label="Dimensions" value={a.width && a.height ? `${a.width} × ${a.height}` : null} />
|
||
<MetaRow label="Duration" value={formatDuration(a.duration)} />
|
||
<MetaRow label="Frame Rate" value={a.frame_rate ? `${a.frame_rate} fps` : null} />
|
||
<MetaRow label="Bitrate" value={a.bitrate ? `${a.bitrate} kbps` : null} />
|
||
<MetaRow label="Video Codec" value={a.video_codec} />
|
||
<MetaRow label="Audio Codec" value={a.audio_codec} />
|
||
<Separator className="my-2" />
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Storage</p>
|
||
<MetaRow label="Provider" value={a.storage_provider} />
|
||
<MetaRow label="Bucket" value={a.storage_bucket} />
|
||
<MetaRow label="Key" value={a.storage_key} />
|
||
<Separator className="my-2" />
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Access</p>
|
||
<div className="flex items-center gap-2 py-1">
|
||
{a.is_public
|
||
? <><Globe className="h-3.5 w-3.5 text-green-500" /><Badge variant="outline" className="text-green-600 border-green-400">Public</Badge></>
|
||
: <><Lock className="h-3.5 w-3.5 text-yellow-500" /><Badge variant="outline" className="text-yellow-600 border-yellow-400">Private</Badge></>
|
||
}
|
||
</div>
|
||
<MetaRow label="Access Level" value={a.access_level} />
|
||
<MetaRow label="Owner Type" value={a.owner_type} />
|
||
<MetaRow label="Owner ID" value={a.owner_id} />
|
||
<Separator className="my-2" />
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Timestamps</p>
|
||
<MetaRow label="Created By" value={a.createdBy} />
|
||
<MetaRow label="Created" value={a.createdAt ? fmtDateTime(a.createdAt) : null} />
|
||
<MetaRow label="Modified" value={a.updatedAt ? fmtDateTime(a.updatedAt) : null} />
|
||
</div>
|
||
|
||
{a.description && (
|
||
<div className="rounded-lg border bg-card p-4">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Description</p>
|
||
<p className="text-sm text-foreground">{a.description}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |