feat(scene): rotate video previews like images
Reuse previewRotationDeg for video scenes across editor, control, presentation and overlays via ContainedVideo layout parity. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import type {
|
||||
SceneId,
|
||||
} from '../../shared/types';
|
||||
import { AppLogo } from '../shared/branding/AppLogo';
|
||||
import { ContainedVideo } from '../shared/ContainedVideo';
|
||||
import { getDndApi } from '../shared/dndApi';
|
||||
import { RotatedImage } from '../shared/RotatedImage';
|
||||
import { Button, Input } from '../shared/ui/controls';
|
||||
@@ -2696,14 +2697,16 @@ function SceneInspector({
|
||||
</div>
|
||||
) : previewUrl && previewAssetType === 'video' ? (
|
||||
<div className={styles.previewFill}>
|
||||
<video
|
||||
src={previewUrl}
|
||||
<ContainedVideo
|
||||
url={previewUrl}
|
||||
rotationDeg={previewRotationDeg}
|
||||
mode="cover"
|
||||
muted
|
||||
playsInline
|
||||
autoPlay={previewVideoAutostart}
|
||||
loop={previewVideoLoop}
|
||||
preload="metadata"
|
||||
className={styles.videoCover}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -2744,7 +2747,7 @@ function SceneInspector({
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
{previewAssetId && previewAssetType === 'image' ? (
|
||||
{previewAssetId && (previewAssetType === 'image' || previewAssetType === 'video') ? (
|
||||
<>
|
||||
<div className={styles.spacer6} />
|
||||
<Button
|
||||
@@ -2989,23 +2992,44 @@ function SceneListCard({
|
||||
</div>
|
||||
) : previewUrl && scene.previewAssetType === 'video' ? (
|
||||
<div className={styles.sceneThumbInner}>
|
||||
<video
|
||||
src={previewUrl}
|
||||
muted
|
||||
playsInline
|
||||
preload="metadata"
|
||||
draggable={false}
|
||||
className={styles.sceneThumbVideo}
|
||||
onLoadedData={(e) => {
|
||||
const v = e.currentTarget;
|
||||
try {
|
||||
v.currentTime = 0;
|
||||
v.pause();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{scene.previewRotationDeg === 0 ? (
|
||||
<video
|
||||
src={previewUrl}
|
||||
muted
|
||||
playsInline
|
||||
preload="metadata"
|
||||
draggable={false}
|
||||
className={styles.sceneThumbVideo}
|
||||
onLoadedData={(e) => {
|
||||
const v = e.currentTarget;
|
||||
try {
|
||||
v.currentTime = 0;
|
||||
v.pause();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ContainedVideo
|
||||
url={previewUrl}
|
||||
rotationDeg={scene.previewRotationDeg}
|
||||
mode="cover"
|
||||
muted
|
||||
playsInline
|
||||
preload="metadata"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onLoadedData={(e) => {
|
||||
const v = e.currentTarget;
|
||||
try {
|
||||
v.currentTime = 0;
|
||||
v.pause();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.sceneThumbEmptyInner} aria-hidden />
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
isSideStoryEdge,
|
||||
} from '../../../shared/graph/sceneGraphLineage';
|
||||
import type { AssetId, GraphNodeId, SceneGraphEdge, SceneGraphNode, SceneId } from '../../../shared/types';
|
||||
import { ContainedVideo } from '../../shared/ContainedVideo';
|
||||
import { RotatedImage } from '../../shared/RotatedImage';
|
||||
import { EllipsisText } from '../../shared/ui/EllipsisText';
|
||||
import ellipsisStyles from '../../shared/ui/ellipsisText.module.css';
|
||||
@@ -260,22 +261,45 @@ function SceneCardNode({ data }: NodeProps<SceneCardData>) {
|
||||
)}
|
||||
</div>
|
||||
) : previewUrl && data.previewAssetType === 'video' ? (
|
||||
<video
|
||||
src={previewUrl}
|
||||
muted
|
||||
playsInline
|
||||
preload="metadata"
|
||||
className={styles.videoCover}
|
||||
onLoadedData={(e) => {
|
||||
const v = e.currentTarget;
|
||||
try {
|
||||
v.currentTime = 0;
|
||||
v.pause();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={styles.previewFill}>
|
||||
{data.previewRotationDeg === 0 ? (
|
||||
<video
|
||||
src={previewUrl}
|
||||
muted
|
||||
playsInline
|
||||
preload="metadata"
|
||||
className={styles.videoCover}
|
||||
onLoadedData={(e) => {
|
||||
const v = e.currentTarget;
|
||||
try {
|
||||
v.currentTime = 0;
|
||||
v.pause();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ContainedVideo
|
||||
url={previewUrl}
|
||||
rotationDeg={data.previewRotationDeg}
|
||||
mode="cover"
|
||||
muted
|
||||
playsInline
|
||||
preload="metadata"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onLoadedData={(e) => {
|
||||
const v = e.currentTarget;
|
||||
try {
|
||||
v.currentTime = 0;
|
||||
v.pause();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.previewPlaceholder} aria-hidden />
|
||||
)}
|
||||
|
||||
@@ -180,7 +180,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
|
||||
'help.section.sceneProps.title': 'Свойства сцены',
|
||||
'help.section.sceneProps.body':
|
||||
'Выберите сцену в списке слева — справа откроются её свойства.\n\n«Название сцены» — для мастера. «Описание» — заметки мастера с форматированием: рядом с подписью нажмите карандаш, откроется редактор (жирный, курсив, заголовки, списки, ссылки). Под подписью видно фрагмент текста или «описание отсутствует», если поле пустое. Во время сессии описание открывается с пульта в отдельном окне (см. «Пульт управления»), а не в блоке «Сюжетная линия».\n\nЕсли у сцены на карте есть карточка с синей меткой «ПОБОЧНАЯ», появится поле «Название побочной линии».\n\nКартинка или видео для игроков:\n\n1) В «Превью сцены» нажмите «Загрузить» или «Изменить».\n\n2) Выберите файл (PNG, JPG, WebP, GIF, видео и др.).\n\n3) Для картинки можно «Повернуть» (шаг 90°). «Очистить» — убрать превью.\n\n4) Для картинки и видео можно включить «Затемнить сцену»: при показе игроки сначала увидят кадр в полной темноте, а вы снимете её с пульта «Кистью Открытия» (см. «Эффекты»).\n\n5) Кнопка «Редактор сцены» доступна и для картинки, и для видео — сетка, ловушки и неигровые токены поверх превью (см. «Редактор сцены», «Генератор сетки», «Ловушки», «Неигровые токены»).\n\nДля видео включите «Автостарт», если ролик должен сам начаться на экране игроков, и при необходимости «Цикл».\n\n«Аудио сцены» — музыка и звуки этого эпизода. «Загрузить» добавляет файлы. У каждого трека: «Авто» (играть при входе в сцену) и «Цикл». Корзина удаляет трек.\n\nСвязи между сценами задаются на карте, а не здесь — см. «Граф сцен».',
|
||||
'Выберите сцену в списке слева — справа откроются её свойства.\n\n«Название сцены» — для мастера. «Описание» — заметки мастера с форматированием: рядом с подписью нажмите карандаш, откроется редактор (жирный, курсив, заголовки, списки, ссылки). Под подписью видно фрагмент текста или «описание отсутствует», если поле пустое. Во время сессии описание открывается с пульта в отдельном окне (см. «Пульт управления»), а не в блоке «Сюжетная линия».\n\nЕсли у сцены на карте есть карточка с синей меткой «ПОБОЧНАЯ», появится поле «Название побочной линии».\n\nКартинка или видео для игроков:\n\n1) В «Превью сцены» нажмите «Загрузить» или «Изменить».\n\n2) Выберите файл (PNG, JPG, WebP, GIF, видео и др.).\n\n3) Для картинки и видео можно «Повернуть» (шаг 90°). «Очистить» — убрать превью.\n\n4) Для картинки и видео можно включить «Затемнить сцену»: при показе игроки сначала увидят кадр в полной темноте, а вы снимете её с пульта «Кистью Открытия» (см. «Эффекты»).\n\n5) Кнопка «Редактор сцены» доступна и для картинки, и для видео — сетка, ловушки и неигровые токены поверх превью (см. «Редактор сцены», «Генератор сетки», «Ловушки», «Неигровые токены»).\n\nДля видео включите «Автостарт», если ролик должен сам начаться на экране игроков, и при необходимости «Цикл».\n\n«Аудио сцены» — музыка и звуки этого эпизода. «Загрузить» добавляет файлы. У каждого трека: «Авто» (играть при входе в сцену) и «Цикл». Корзина удаляет трек.\n\nСвязи между сценами задаются на карте, а не здесь — см. «Граф сцен».',
|
||||
|
||||
'help.section.sceneEditor.title': 'Редактор сцены',
|
||||
'help.section.sceneEditor.body':
|
||||
@@ -235,7 +235,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
|
||||
'help.section.presentation.title': 'Экран презентации',
|
||||
'help.section.presentation.body':
|
||||
'«Презентация» — то, что видят игроки: картинка сцены (с учётом поворота из редактора) или видео по вашим настройкам.\n\nЭффекты с пульта накладываются поверх. Меню и кнопки мастера здесь не показываются — клики по карте, ловушкам и токенам для игроков недоступны.\n\nЕсли у сцены включено «Затемнить сцену», игроки сначала видят полностью чёрный экран. Мастер открывает карту «Кистью Открытия» и при необходимости снова закрывает участки «Кистью Закрытия» на пульте.\n\nПри смене сцены с пульта картинка обновляется сама. Перенесите окно на экран для игроков и при необходимости спрячьте панель задач.\n\nПустой или тёмный экран — скорее всего, у сцены нет превью. Добавьте картинку в свойствах сцены в редакторе.',
|
||||
'«Презентация» — то, что видят игроки: картинка или видео сцены (с учётом поворота из редактора) по вашим настройкам.\n\nЭффекты с пульта накладываются поверх. Меню и кнопки мастера здесь не показываются — клики по карте, ловушкам и токенам для игроков недоступны.\n\nЕсли у сцены включено «Затемнить сцену», игроки сначала видят полностью чёрный экран. Мастер открывает карту «Кистью Открытия» и при необходимости снова закрывает участки «Кистью Закрытия» на пульте.\n\nПри смене сцены с пульта картинка обновляется сама. Перенесите окно на экран для игроков и при необходимости спрячьте панель задач.\n\nПустой или тёмный экран — скорее всего, у сцены нет превью. Добавьте картинку в свойствах сцены в редакторе.',
|
||||
|
||||
'help.section.importExport.title': 'Импорт и экспорт',
|
||||
'help.section.importExport.body':
|
||||
@@ -800,7 +800,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
|
||||
'help.section.sceneProps.title': 'Scene properties',
|
||||
'help.section.sceneProps.body':
|
||||
'Select a scene in the left list — its properties open on the right.\n\nScene title is for the GM. Description is GM notes with formatting: click the pencil next to the label to open the editor (bold, italic, headings, lists, links). Below the label you see a text preview, or “no description” when empty. During a session, open the description from the control panel in a separate window (see Control panel) — it is not shown inside the Storyline list.\n\nIf the scene has a graph card with a blue SIDE badge, a Side storyline title field appears.\n\nImage or video for players:\n\n1) Under Scene preview, click Upload or Change.\n\n2) Pick a file (PNG, JPG, WebP, GIF, video, etc.).\n\n3) For images, use Rotate (90° steps). Clear removes the preview.\n\n4) For images and video, enable Darken scene so players start in full darkness and you reveal the frame with the Opening brush on the control panel (see Effects).\n\n5) Scene editor works for both images and video — battle grid, traps, and non-player tokens over the preview (see Scene editor, Grid generator, Traps, and Non-player tokens).\n\nFor video, enable Autostart if the clip should start on its own on the player screen, and Loop if needed.\n\nScene audio — music and sounds for this episode. Upload adds files. Per track: Auto (play when entering the scene) and Loop. The trash icon removes a track.\n\nLinks between scenes are drawn on the map, not here — see Scene graph.',
|
||||
'Select a scene in the left list — its properties open on the right.\n\nScene title is for the GM. Description is GM notes with formatting: click the pencil next to the label to open the editor (bold, italic, headings, lists, links). Below the label you see a text preview, or “no description” when empty. During a session, open the description from the control panel in a separate window (see Control panel) — it is not shown inside the Storyline list.\n\nIf the scene has a graph card with a blue SIDE badge, a Side storyline title field appears.\n\nImage or video for players:\n\n1) Under Scene preview, click Upload or Change.\n\n2) Pick a file (PNG, JPG, WebP, GIF, video, etc.).\n\n3) For images and video, use Rotate (90° steps). Clear removes the preview.\n\n4) For images and video, enable Darken scene so players start in full darkness and you reveal the frame with the Opening brush on the control panel (see Effects).\n\n5) Scene editor works for both images and video — battle grid, traps, and non-player tokens over the preview (see Scene editor, Grid generator, Traps, and Non-player tokens).\n\nFor video, enable Autostart if the clip should start on its own on the player screen, and Loop if needed.\n\nScene audio — music and sounds for this episode. Upload adds files. Per track: Auto (play when entering the scene) and Loop. The trash icon removes a track.\n\nLinks between scenes are drawn on the map, not here — see Scene graph.',
|
||||
|
||||
'help.section.sceneEditor.title': 'Scene editor',
|
||||
'help.section.sceneEditor.body':
|
||||
@@ -855,7 +855,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
|
||||
'help.section.presentation.title': 'Presentation screen',
|
||||
'help.section.presentation.body':
|
||||
'Presentation is what players see: the scene image (with rotation from the editor) or video according to your settings.\n\nEffects from the control panel draw on top. There are no GM menus or buttons here — players cannot click the map, traps, or tokens.\n\nIf Darken scene is enabled, players first see a fully black screen. The GM reveals the map with the Opening brush and can cover areas again with the Closing brush on the control panel.\n\nWhen you switch scenes from the control panel, the image updates automatically. Move the window to the display players watch and hide the taskbar if needed.\n\nA blank or dark screen usually means the scene has no preview — add one in scene properties in the editor.',
|
||||
'Presentation is what players see: the scene image or video (with rotation from the editor) according to your settings.\n\nEffects from the control panel draw on top. There are no GM menus or buttons here — players cannot click the map, traps, or tokens.\n\nIf Darken scene is enabled, players first see a fully black screen. The GM reveals the map with the Opening brush and can cover areas again with the Closing brush on the control panel.\n\nWhen you switch scenes from the control panel, the image updates automatically. Move the window to the display players watch and hide the taskbar if needed.\n\nA blank or dark screen usually means the scene has no preview — add one in scene properties in the editor.',
|
||||
|
||||
'help.section.importExport.title': 'Import and export',
|
||||
'help.section.importExport.body':
|
||||
|
||||
Reference in New Issue
Block a user