mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
added things in admin
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user