feat: full studio build -- light theme, canvas thumbnails, i18n (fa/en)
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
DEFAULT_SCENE_ACCENT_COLOR,
|
||||
DEFAULT_SCENE_BACKGROUND_COLOR,
|
||||
} from "@/lib/studio-color-palettes";
|
||||
import type { Layer, Scene, SceneTransition } from "@/lib/studio-types";
|
||||
import { DEFAULT_SCENE_DURATION } from "@/lib/studio-types";
|
||||
|
||||
const SCENE_TRANSITIONS: SceneTransition[] = [
|
||||
"none",
|
||||
"fade",
|
||||
"slide-left",
|
||||
"zoom",
|
||||
];
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function parseLayer(value: unknown): Layer | null {
|
||||
if (!isRecord(value)) return null;
|
||||
if (typeof value.id !== "string" || typeof value.type !== "string") return null;
|
||||
if (
|
||||
typeof value.x !== "number" ||
|
||||
typeof value.y !== "number" ||
|
||||
typeof value.width !== "number" ||
|
||||
typeof value.height !== "number"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: value.id,
|
||||
type: value.type as Layer["type"],
|
||||
name: typeof value.name === "string" ? value.name : undefined,
|
||||
visible: typeof value.visible === "boolean" ? value.visible : undefined,
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
width: value.width,
|
||||
height: value.height,
|
||||
rotation: typeof value.rotation === "number" ? value.rotation : 0,
|
||||
opacity: typeof value.opacity === "number" ? value.opacity : 1,
|
||||
zIndex: typeof value.zIndex === "number" ? value.zIndex : 0,
|
||||
props: isRecord(value.props) ? value.props : {},
|
||||
};
|
||||
}
|
||||
|
||||
function parseScene(value: unknown): Scene | null {
|
||||
if (!isRecord(value)) return null;
|
||||
if (typeof value.id !== "string" || typeof value.name !== "string") return null;
|
||||
if (!Array.isArray(value.layers)) return null;
|
||||
|
||||
const layers = value.layers
|
||||
.map(parseLayer)
|
||||
.filter((layer): layer is Layer => layer !== null);
|
||||
|
||||
const transitionType =
|
||||
typeof value.transitionType === "string" &&
|
||||
SCENE_TRANSITIONS.includes(value.transitionType as SceneTransition)
|
||||
? (value.transitionType as SceneTransition)
|
||||
: "none";
|
||||
|
||||
return {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
duration:
|
||||
typeof value.duration === "number" ? value.duration : DEFAULT_SCENE_DURATION,
|
||||
layers,
|
||||
transitionType,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseVideoScenes(value: unknown): Scene[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map(parseScene).filter((scene): scene is Scene => scene !== null);
|
||||
}
|
||||
|
||||
export function isVideoSceneDataEmpty(sceneData: Record<string, unknown>): boolean {
|
||||
const scenes = parseVideoScenes(sceneData.scenes);
|
||||
return scenes.length === 0;
|
||||
}
|
||||
|
||||
/** Omit generated thumbnails — they are re-created in the editor */
|
||||
export function scenesForPersistence(scenes: Scene[]): Scene[] {
|
||||
return scenes.map((scene) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- strip before save
|
||||
const { thumbnailUrl, ...rest } = scene;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
|
||||
export interface VideoPersistedSceneData {
|
||||
scenes: Scene[];
|
||||
activeSceneId: string;
|
||||
currentTime?: number;
|
||||
pxPerSecond?: number;
|
||||
audioFileName?: string | null;
|
||||
audioSrc?: string | null;
|
||||
audioVolume?: number;
|
||||
sceneBackgroundColor?: string;
|
||||
sceneAccentColor?: string;
|
||||
}
|
||||
|
||||
export function buildVideoSceneDataPayload(
|
||||
input: VideoPersistedSceneData
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
scenes: scenesForPersistence(input.scenes),
|
||||
activeSceneId: input.activeSceneId,
|
||||
currentTime: input.currentTime,
|
||||
pxPerSecond: input.pxPerSecond,
|
||||
audioFileName: input.audioFileName,
|
||||
audioSrc: input.audioSrc,
|
||||
audioVolume: input.audioVolume,
|
||||
sceneBackgroundColor: input.sceneBackgroundColor,
|
||||
sceneAccentColor: input.sceneAccentColor,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseVideoSceneData(
|
||||
sceneData: Record<string, unknown>
|
||||
): VideoPersistedSceneData | null {
|
||||
const scenes = parseVideoScenes(sceneData.scenes);
|
||||
if (scenes.length === 0) return null;
|
||||
|
||||
const activeSceneId =
|
||||
typeof sceneData.activeSceneId === "string" &&
|
||||
scenes.some((scene) => scene.id === sceneData.activeSceneId)
|
||||
? sceneData.activeSceneId
|
||||
: scenes[0].id;
|
||||
|
||||
return {
|
||||
scenes,
|
||||
activeSceneId,
|
||||
currentTime:
|
||||
typeof sceneData.currentTime === "number" ? sceneData.currentTime : 0,
|
||||
pxPerSecond:
|
||||
typeof sceneData.pxPerSecond === "number" ? sceneData.pxPerSecond : undefined,
|
||||
audioFileName:
|
||||
typeof sceneData.audioFileName === "string" ? sceneData.audioFileName : null,
|
||||
audioSrc: typeof sceneData.audioSrc === "string" ? sceneData.audioSrc : null,
|
||||
audioVolume:
|
||||
typeof sceneData.audioVolume === "number" ? sceneData.audioVolume : 100,
|
||||
sceneBackgroundColor:
|
||||
typeof sceneData.sceneBackgroundColor === "string"
|
||||
? sceneData.sceneBackgroundColor
|
||||
: DEFAULT_SCENE_BACKGROUND_COLOR,
|
||||
sceneAccentColor:
|
||||
typeof sceneData.sceneAccentColor === "string"
|
||||
? sceneData.sceneAccentColor
|
||||
: DEFAULT_SCENE_ACCENT_COLOR,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user