DNDGamePlayer: Electron редактор сцен, презентация, упаковка electron-builder
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { computeTimeSec } from '../../main/video/videoPlaybackStore';
|
||||
import type { SessionState } from '../../shared/ipc/contracts';
|
||||
|
||||
import { PixiEffectsOverlay } from './effects/PxiEffectsOverlay';
|
||||
import { useEffectsState } from './effects/useEffectsState';
|
||||
import styles from './PresentationView.module.css';
|
||||
import { RotatedImage } from './RotatedImage';
|
||||
import { useAssetUrl } from './useAssetImageUrl';
|
||||
import { useVideoPlaybackState } from './video/useVideoPlaybackState';
|
||||
|
||||
export type PresentationViewProps = {
|
||||
session: SessionState | null;
|
||||
/** Если true — показываем укороченный заголовок/оверлей для предпросмотра. */
|
||||
compact?: boolean;
|
||||
showTitle?: boolean;
|
||||
showEffects?: boolean;
|
||||
};
|
||||
|
||||
export function PresentationView({
|
||||
session,
|
||||
compact = false,
|
||||
showTitle = true,
|
||||
showEffects = true,
|
||||
}: PresentationViewProps) {
|
||||
const [fxState] = useEffectsState();
|
||||
const [vp] = useVideoPlaybackState();
|
||||
const videoElRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [contentRect, setContentRect] = React.useState<{ x: number; y: number; w: number; h: number } | null>(
|
||||
null,
|
||||
);
|
||||
const scene =
|
||||
session?.project && session.currentSceneId ? session.project.scenes[session.currentSceneId] : undefined;
|
||||
const previewUrl = useAssetUrl(scene?.previewAssetId ?? null);
|
||||
const rot = scene?.previewRotationDeg ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
const el = videoElRef.current;
|
||||
if (!el) return;
|
||||
if (!vp) return;
|
||||
if (!scene?.previewAssetId) return;
|
||||
if (scene.previewAssetType !== 'video') return;
|
||||
if (vp.targetAssetId !== scene.previewAssetId) return;
|
||||
|
||||
el.playbackRate = vp.playbackRate;
|
||||
const desired = computeTimeSec(vp, vp.serverNowMs);
|
||||
if (Number.isFinite(desired) && Math.abs(el.currentTime - desired) > 0.35) {
|
||||
el.currentTime = Math.max(0, desired);
|
||||
}
|
||||
if (vp.playing) {
|
||||
void el.play().catch(() => undefined);
|
||||
} else {
|
||||
el.pause();
|
||||
}
|
||||
}, [scene?.previewAssetId, scene?.previewAssetType, vp]);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{previewUrl && scene?.previewAssetType === 'image' ? (
|
||||
<div className={styles.fill}>
|
||||
<RotatedImage
|
||||
url={previewUrl}
|
||||
rotationDeg={rot}
|
||||
mode="contain"
|
||||
onContentRectChange={setContentRect}
|
||||
/>
|
||||
</div>
|
||||
) : previewUrl && scene?.previewAssetType === 'video' ? (
|
||||
<video
|
||||
ref={videoElRef}
|
||||
className={styles.video}
|
||||
src={previewUrl}
|
||||
muted
|
||||
playsInline
|
||||
loop={false}
|
||||
preload="auto"
|
||||
onError={() => {
|
||||
// noop: status surfaced in control app; keep presentation clean
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.placeholderBg} />
|
||||
)}
|
||||
<div className={styles.vignette} />
|
||||
{showEffects && scene?.previewAssetType !== 'video' ? (
|
||||
<PixiEffectsOverlay
|
||||
state={fxState}
|
||||
viewport={
|
||||
contentRect
|
||||
? { x: contentRect.x, y: contentRect.y, w: contentRect.w, h: contentRect.h }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{showTitle ? (
|
||||
<div className={styles.titleWrap}>
|
||||
<div className={compact ? styles.titleCompact : styles.titleFull}>
|
||||
{scene?.title ?? 'Выберите сцену в редакторе'}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user