added things in admin

This commit is contained in:
rgrgogu
2026-08-05 20:44:58 +08:00
parent 882dfbe67a
commit a37a4e9a97
17 changed files with 171 additions and 183 deletions
@@ -24,11 +24,18 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
// content.url is redacted (null) server-side for S3 assets — see
// redactS3Url() in controllers/admin/assets.controller.js. Re-resolve
// through media.util.js instead of trusting the persisted url/thumbnail.
const { src, thumbnailUrl, loading } = useAssetPreviewSrc(
{ asset_id: content?.asset_id, storage_provider: content?.storage_provider, file_url: content?.url, thumbnail_url: content?.thumbnail_url },
//
// thumbnail_url is intentionally NOT passed here: resolveAssetSrc()'s fast
// path falls back to thumbnail_url when there's no stream_token, which for
// a video asset with a redacted (null) file_url meant `src` resolved to
// the poster JPEG instead of the video file, and the browser then refused
// to play it ("media resource ... was not suitable"). Omitting it forces
// the async mint-token path, which resolves the real video stream src.
const { src, loading } = useAssetPreviewSrc(
{ asset_id: content?.asset_id, storage_provider: content?.storage_provider, file_url: content?.url },
{ scope: "admin" },
);
const poster = thumbnailUrl ?? content?.thumbnail_url ?? undefined;
const poster = content?.thumbnail_url ?? undefined;
const vidRef = useRef(null);
const wrapRef = useRef(null);
@@ -55,6 +62,13 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
const [mediaLoading, setMediaLoading] = useState(true);
const [hoverProgress, setHoverProgress] = useState(null); // { x, time }
const previewVidRef = useRef(null);
// Fullscreen-only auto-hide for the controls bar, mirroring the client
// player: idle 3s while playing fades the bar out, any mouse movement (or
// a pause) brings it back. Outside fullscreen the bar stays put — it's a
// normal in-flow row there, not an overlay, so hiding it would just leave
// a blank gap.
const [controlsVisible, setControlsVisible] = useState(true);
const hideTimer = useRef(null);
// Reset player when video changes
useEffect(() => {
@@ -104,6 +118,19 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
// ── Controls ──────────────────────────────────────────────────────────────
const resetHideTimer = useCallback(() => {
setControlsVisible(true);
clearTimeout(hideTimer.current);
if (playing && isFullscreen) {
hideTimer.current = setTimeout(() => setControlsVisible(false), 3000);
}
}, [playing, isFullscreen]);
useEffect(() => {
resetHideTimer();
return () => clearTimeout(hideTimer.current);
}, [playing, isFullscreen, resetHideTimer]);
const togglePlay = useCallback(() => {
const v = vidRef.current;
if (!v) return;
@@ -116,7 +143,8 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
const handleAreaClick = useCallback(() => {
wrapRef.current?.focus({ preventScroll: true });
togglePlay();
}, [togglePlay]);
resetHideTimer();
}, [togglePlay, resetHideTimer]);
const restart = () => {
const v = vidRef.current;
@@ -284,6 +312,7 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
ref={wrapRef}
tabIndex={0}
onKeyDown={handleKeyDown}
onMouseMove={isFullscreen ? resetHideTimer : undefined}
className={`overflow-hidden bg-card outline-none focus-visible:ring-2 focus-visible:ring-ring ${isFullscreen ? "fixed inset-0 z-[100] flex flex-col" : "rounded-lg border"}`}
>
@@ -342,12 +371,20 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
)}
</div>
{/* ── Player controls ── */}
<div className="px-3 pt-2.5 pb-3 flex flex-col gap-2">
{/* ── Player controls ──
Fullscreen: absolutely positioned over the bottom of the video
(wrap is `fixed inset-0` there, so it's the containing block)
and fades per controlsVisible. Non-fullscreen: normal in-flow
row below the video, always visible. */}
<div className={
isFullscreen
? `absolute bottom-0 left-0 right-0 z-10 bg-gradient-to-t from-black/80 via-black/40 to-transparent px-4 pt-8 pb-4 flex flex-col gap-2 transition-opacity duration-300 ${controlsVisible ? "opacity-100" : "opacity-0 pointer-events-none"}`
: "px-3 pt-2.5 pb-3 flex flex-col gap-2"
}>
{/* Progress bar */}
<div
className="relative h-1 bg-border rounded-full cursor-pointer"
className={`relative h-1 rounded-full cursor-pointer ${isFullscreen ? "bg-white/30" : "bg-border"}`}
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const pct = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
@@ -360,7 +397,7 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
onMouseLeave={() => setHoverProgress(null)}
>
<div
className="h-full bg-foreground rounded-full transition-[width] duration-100"
className={`h-full rounded-full transition-[width] duration-100 ${isFullscreen ? "bg-white" : "bg-foreground"}`}
style={{ width: `${progress}%` }}
/>
<input
@@ -399,18 +436,18 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
{/* Button row */}
<div className="flex items-center gap-2">
<button onClick={togglePlay} aria-label={playing ? "Pause" : "Play"} className="text-muted-foreground hover:text-foreground transition-colors">
<button onClick={togglePlay} aria-label={playing ? "Pause" : "Play"} className={`transition-colors ${isFullscreen ? "text-white/80 hover:text-white" : "text-muted-foreground hover:text-foreground"}`}>
{playing ? <Pause className="size-4" /> : <Play className="size-4" />}
</button>
<button onClick={restart} aria-label="Restart" className="text-muted-foreground hover:text-foreground transition-colors">
<button onClick={restart} aria-label="Restart" className={`transition-colors ${isFullscreen ? "text-white/80 hover:text-white" : "text-muted-foreground hover:text-foreground"}`}>
<SkipBack className="size-4" />
</button>
<span className="text-xs text-muted-foreground tabular-nums">
<span className={`text-xs tabular-nums ${isFullscreen ? "text-white/70" : "text-muted-foreground"}`}>
{fmtTime(currentTime)} / {fmtTime(totalDuration)}
</span>
<div className="flex items-center gap-1.5 ml-auto">
<button onClick={toggleMute} aria-label="Toggle mute" className="text-muted-foreground hover:text-foreground transition-colors">
<button onClick={toggleMute} aria-label="Toggle mute" className={`transition-colors ${isFullscreen ? "text-white/80 hover:text-white" : "text-muted-foreground hover:text-foreground"}`}>
{muted ? <VolumeX className="size-4" /> : <Volume2 className="size-4" />}
</button>
<input
@@ -418,11 +455,11 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
value={muted ? 0 : volume}
onChange={handleVolumeChange}
aria-label="Volume"
className="w-16 accent-foreground"
className={`w-16 ${isFullscreen ? "accent-white" : "accent-foreground"}`}
/>
</div>
<button onClick={toggleFullscreen} aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"} className="text-muted-foreground hover:text-foreground transition-colors ml-1">
<button onClick={toggleFullscreen} aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"} className={`transition-colors ml-1 ${isFullscreen ? "text-white/80 hover:text-white" : "text-muted-foreground hover:text-foreground"}`}>
{isFullscreen ? <Minimize2 className="size-4" /> : <Maximize2 className="size-4" />}
</button>
</div>
+5 -4
View File
@@ -3,9 +3,10 @@
* Type : Component (Generic)
* Description : Zoom/pan/fit viewer for image and PDF files. Used by both the
* client task-attachment preview (FilePreview.jsx, via blob:
* URLs) and the admin asset preview dialog (AssetPreviewDialog.jsx,
* via direct stream-token URLs) — `src` accepts either, this
* component doesn't care how the URL was produced.
* URLs) and the admin asset view pages (ViewImageAsset.jsx,
* ViewDocumentAsset.jsx, via direct stream-token URLs) —
* `src` accepts either, this component doesn't care how the
* URL was produced.
*
* Supported:
* image/jpeg, image/png → <img> with scroll-zoom + drag-pan
@@ -214,7 +215,7 @@ const PdfViewer = ({ src, fileName }) => {
pdfjsLib.GlobalWorkerOptions.workerSrc =
new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const doc = await pdfjsLib.getDocument(src).promise;
const doc = await pdfjsLib.getDocument({ url: src }).promise;
if (cancelled) return;
setPdfDoc(doc);
setNumPages(doc.numPages);