232 lines
7.3 KiB
TypeScript
232 lines
7.3 KiB
TypeScript
import type { CSSProperties } from "react";
|
|
import type { VideoBlockProps } from "@/features/editor/state/editorStore";
|
|
|
|
export type VideoPlatform = "youtube" | "vimeo" | "html5";
|
|
|
|
export type VideoPlatformInput = VideoPlatform | "auto" | null | undefined;
|
|
|
|
// 비디오 sourceUrl 을 정규화한다: null/undefined 를 빈 문자열로 만들고 양 끝 공백을 제거한다.
|
|
export function normalizeVideoSourceUrl(source: string | null | undefined): string {
|
|
if (!source) {
|
|
return "";
|
|
}
|
|
|
|
return source.trim();
|
|
}
|
|
|
|
// platform 값과 URL 을 기반으로 최종 비디오 플랫폼을 결정한다.
|
|
// - platform 이 auto 가 아니면 해당 값을 우선 사용한다.
|
|
// - auto 인 경우 URL 을 검사해 youtube/vimeo/html5 를 판별한다.
|
|
export function resolveVideoPlatform(rawUrl: string, platform: VideoPlatformInput): VideoPlatform {
|
|
if (platform && platform !== "auto") {
|
|
return platform;
|
|
}
|
|
|
|
const normalized = normalizeVideoSourceUrl(rawUrl);
|
|
const lower = normalized.toLowerCase();
|
|
|
|
if (lower.includes("youtube.com") || lower.includes("youtu.be")) {
|
|
return "youtube";
|
|
}
|
|
|
|
if (lower.includes("vimeo.com")) {
|
|
return "vimeo";
|
|
}
|
|
|
|
return "html5";
|
|
}
|
|
|
|
export type BuildEmbedUrlOptions = {
|
|
enableVimeoEmbed?: boolean;
|
|
};
|
|
|
|
// 플랫폼과 URL 을 기반으로 최종 embed URL 을 생성한다.
|
|
// - YouTube: 항상 watch/short/youtu.be 형태에서 embed URL 로 변환.
|
|
// - Vimeo: enableVimeoEmbed 가 true 인 경우에만 player.vimeo.com 기반 embed URL 로 변환.
|
|
// - HTML5: 항상 정규화된 원본 URL 을 그대로 사용.
|
|
export function buildVideoEmbedUrl(
|
|
rawUrl: string,
|
|
platform: VideoPlatform,
|
|
options?: BuildEmbedUrlOptions,
|
|
): string {
|
|
const normalized = normalizeVideoSourceUrl(rawUrl);
|
|
|
|
if (platform === "youtube") {
|
|
const idMatch =
|
|
normalized.match(/[?&]v=([^&]+)/) ||
|
|
normalized.match(/youtu\.be\/([^?&]+)/) ||
|
|
normalized.match(/youtube\.com\/shorts\/([^?&]+)/);
|
|
const videoId = idMatch && idMatch[1] ? idMatch[1] : "";
|
|
if (videoId) {
|
|
return `https://www.youtube.com/embed/${videoId}`;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
if (platform === "vimeo" && options?.enableVimeoEmbed) {
|
|
const idMatch = normalized.match(/vimeo\.com\/(\d+)/);
|
|
const videoId = idMatch && idMatch[1] ? idMatch[1] : "";
|
|
if (videoId) {
|
|
return `https://player.vimeo.com/video/${videoId}`;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
export interface VideoExportEscapers {
|
|
escapeAttr: (value: string) => string;
|
|
}
|
|
|
|
export interface VideoExportTokens {
|
|
wrapperStyleParts: string[];
|
|
videoStyleParts: string[];
|
|
outerStyleParts: string[];
|
|
aspectClass: string;
|
|
}
|
|
|
|
export function computeVideoExportTokens(
|
|
props: VideoBlockProps,
|
|
escapers: VideoExportEscapers,
|
|
): VideoExportTokens {
|
|
const { escapeAttr } = escapers;
|
|
|
|
const aspect = props.aspectRatio ?? "16:9";
|
|
const aspectClass =
|
|
aspect === "4:3" ? " pb-video-wrapper--4by3" : aspect === "1:1" ? " pb-video-wrapper--1by1" : "";
|
|
|
|
const wrapperStyleParts: string[] = [];
|
|
const videoStyleParts: string[] = [];
|
|
|
|
const widthMode = props.widthMode ?? "auto";
|
|
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
|
const widthEm = pxToEm(props.widthPx);
|
|
wrapperStyleParts.push(`width:${widthEm}`);
|
|
videoStyleParts.push(`width:${widthEm}`);
|
|
} else if (widthMode === "full") {
|
|
wrapperStyleParts.push("width:100%");
|
|
videoStyleParts.push("width:100%");
|
|
}
|
|
|
|
if (typeof props.backgroundColorCustom === "string" && props.backgroundColorCustom.trim() !== "") {
|
|
wrapperStyleParts.push(`background-color:${escapeAttr(props.backgroundColorCustom.trim())}`);
|
|
}
|
|
|
|
if (typeof props.cardPaddingPx === "number" && props.cardPaddingPx >= 0) {
|
|
wrapperStyleParts.push(`padding:${pxToEm(props.cardPaddingPx)}`);
|
|
}
|
|
|
|
if (typeof props.borderRadiusPx === "number" && props.borderRadiusPx >= 0) {
|
|
wrapperStyleParts.push(`border-radius:${pxToEm(props.borderRadiusPx)}`);
|
|
}
|
|
|
|
const align =
|
|
props.align === "left" ? "flex-start" : props.align === "right" ? "flex-end" : "center";
|
|
const outerStyleParts = ["display:flex", `justify-content:${align}`];
|
|
|
|
return {
|
|
wrapperStyleParts,
|
|
videoStyleParts,
|
|
outerStyleParts,
|
|
aspectClass,
|
|
};
|
|
}
|
|
|
|
export interface VideoPublicTokens {
|
|
wrapperStyle: CSSProperties;
|
|
videoStyle: CSSProperties;
|
|
justifyClass: string;
|
|
aspectClass: string;
|
|
}
|
|
|
|
const pxToEm = (px: number, base = 16) => `${px / base}em`;
|
|
|
|
export function computeVideoPublicTokens(props: VideoBlockProps): VideoPublicTokens {
|
|
const aspect = props.aspectRatio ?? "16:9";
|
|
const aspectClass =
|
|
aspect === "4:3" ? " pb-video-wrapper--4by3" : aspect === "1:1" ? " pb-video-wrapper--1by1" : "";
|
|
|
|
const wrapperStyle: CSSProperties = {};
|
|
const videoStyle: CSSProperties = {};
|
|
|
|
const widthMode = props.widthMode ?? "auto";
|
|
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
|
const widthEm = pxToEm(props.widthPx);
|
|
wrapperStyle.width = widthEm;
|
|
videoStyle.width = widthEm;
|
|
} else if (widthMode === "full") {
|
|
wrapperStyle.width = "100%";
|
|
videoStyle.width = "100%";
|
|
}
|
|
|
|
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
|
|
wrapperStyle.backgroundColor = props.backgroundColorCustom.trim();
|
|
}
|
|
|
|
if (typeof (props as any).cardPaddingPx === "number" && (props as any).cardPaddingPx >= 0) {
|
|
wrapperStyle.padding = pxToEm((props as any).cardPaddingPx as number);
|
|
}
|
|
|
|
if (typeof (props as any).borderRadiusPx === "number" && (props as any).borderRadiusPx >= 0) {
|
|
wrapperStyle.borderRadius = pxToEm((props as any).borderRadiusPx as number);
|
|
}
|
|
|
|
const align = props.align ?? "center";
|
|
const justifyClass =
|
|
align === "left" ? "justify-start" : align === "right" ? "justify-end" : "justify-center";
|
|
|
|
return {
|
|
wrapperStyle,
|
|
videoStyle,
|
|
justifyClass,
|
|
aspectClass,
|
|
};
|
|
}
|
|
|
|
export interface VideoEditorTokens {
|
|
wrapperStyle: CSSProperties;
|
|
videoStyle: CSSProperties;
|
|
justifyClass: string;
|
|
aspectClass: string;
|
|
}
|
|
|
|
export function computeVideoEditorTokens(props: VideoBlockProps): VideoEditorTokens {
|
|
const aspect = props.aspectRatio ?? "16:9";
|
|
const aspectClass =
|
|
aspect === "4:3" ? " pb-video-wrapper--4by3" : aspect === "1:1" ? " pb-video-wrapper--1by1" : "";
|
|
|
|
const wrapperStyle: CSSProperties = {};
|
|
const videoStyle: CSSProperties = {};
|
|
|
|
const widthMode = props.widthMode ?? "auto";
|
|
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
|
wrapperStyle.width = `${props.widthPx}px`;
|
|
videoStyle.width = `${props.widthPx}px`;
|
|
} else if (widthMode === "full") {
|
|
wrapperStyle.width = "100%";
|
|
videoStyle.width = "100%";
|
|
}
|
|
|
|
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
|
|
wrapperStyle.backgroundColor = props.backgroundColorCustom.trim();
|
|
}
|
|
if (typeof props.cardPaddingPx === "number" && props.cardPaddingPx >= 0) {
|
|
wrapperStyle.padding = `${props.cardPaddingPx}px`;
|
|
}
|
|
if (typeof props.borderRadiusPx === "number" && props.borderRadiusPx >= 0) {
|
|
wrapperStyle.borderRadius = `${props.borderRadiusPx}px`;
|
|
}
|
|
|
|
const align = props.align ?? "center";
|
|
const justifyClass =
|
|
align === "left" ? "justify-start" : align === "right" ? "justify-end" : "justify-center";
|
|
|
|
return {
|
|
wrapperStyle,
|
|
videoStyle,
|
|
justifyClass,
|
|
aspectClass,
|
|
};
|
|
}
|