"use client"; import { useState } from "react"; import { HexColorPicker } from "react-colorful"; import { Minus, Sun, Mic, Zap, Keyboard, ArrowUpDown, Heart, MessageCircle, Share2, Music } from "lucide-react"; import type { StyleConfig } from "@/lib/api"; const FONTS = [ "Noto Sans Thai", "Noto Sans SC", "Noto Sans", "Impact", "Montserrat", "Oswald", "Anton", "Bebas Neue", ]; const ANIMATIONS = [ { id: "none", Icon: Minus, en: "None", th: "ไม่มี", zh: "无" }, { id: "fade", Icon: Sun, en: "Fade", th: "Fade", zh: "淡入" }, { id: "karaoke", Icon: Mic, en: "Karaoke", th: "Karaoke", zh: "卡拉OK" }, { id: "pop", Icon: Zap, en: "Pop", th: "Pop", zh: "弹出" }, { id: "typewriter", Icon: Keyboard, en: "Typewriter", th: "Typewriter",zh: "打字机" }, { id: "bounce", Icon: ArrowUpDown, en: "Bounce", th: "Bounce", zh: "弹跳" }, ]; const ALIGNMENTS = [ { val: 7, label: "↖" }, { val: 8, label: "↑" }, { val: 9, label: "↗" }, { val: 4, label: "←" }, { val: 5, label: "·" }, { val: 6, label: "→" }, { val: 1, label: "↙" }, { val: 2, label: "↓" }, { val: 3, label: "↘" }, ]; const SAMPLE_TEXTS: Record = { thai: "นี่คือตัวอย่างซับไตเติ้ล", english: "This is a sample subtitle", chinese: "这是字幕示例", japanese: "これはサンプル字幕です", korean: "이것은 샘플 자막입니다", default: "Sample subtitle preview", }; const L = { en: { font: "Font", size: "Size", outline: "Outline", shadow: "Shadow", colors: "Colors", primary: "Text", secondary: "Karaoke", outlineC: "Border", shadowC: "Shadow", word: "Word", sentence: "Sentence", animation: "Animation", animPreview: "Animation preview", position: "Position", close: "Close", }, th: { font: "ฟ้อนต์", size: "ขนาด", outline: "ขอบ", shadow: "เงา", colors: "สี", primary: "ข้อความ", secondary: "Karaoke", outlineC: "ขอบ", shadowC: "เงา", word: "คำ", sentence: "ประโยค", animation: "แอนิเมชัน", animPreview: "ตัวอย่างแอนิเมชัน", position: "ตำแหน่ง", close: "ปิด", }, zh: { font: "字体", size: "大小", outline: "描边", shadow: "阴影", colors: "颜色", primary: "文字", secondary: "卡拉OK", outlineC: "描边", shadowC: "阴影", word: "逐字", sentence: "句子", animation: "动画", animPreview: "动画预览", position: "位置", close: "关闭", }, } as const; type Lang = keyof typeof L; interface Props { config: StyleConfig; onChange: (c: Partial) => void; subtitleLanguage: string; uiLang?: Lang; } /* ── TikTok phone preview ── */ function TikTokPreview({ config, subtitleLanguage }: { config: StyleConfig; subtitleLanguage: string }) { const text = SAMPLE_TEXTS[subtitleLanguage] ?? SAMPLE_TEXTS.default; const alignment = config.alignment ?? 2; const isBottom = alignment <= 3; const isTop = alignment >= 7; const isLeft = [1, 4, 7].includes(alignment); const isRight = [3, 6, 9].includes(alignment); const posStyle: React.CSSProperties = { position: "absolute", zIndex: 10, maxWidth: "76%", wordBreak: "break-word", textAlign: isLeft ? "left" : isRight ? "right" : "center", }; if (isBottom) { posStyle.bottom = "18%"; posStyle.left = isLeft ? "4%" : isRight ? undefined : "12%"; posStyle.right = isRight ? "14%" : isLeft ? undefined : "12%"; } else if (isTop) { posStyle.top = "12%"; posStyle.left = isLeft ? "4%" : isRight ? undefined : "12%"; posStyle.right = isRight ? "14%" : isLeft ? undefined : "12%"; } else { posStyle.top = "50%"; posStyle.transform = "translateY(-50%)"; posStyle.left = isLeft ? "4%" : isRight ? undefined : "12%"; posStyle.right = isRight ? "14%" : isLeft ? undefined : "12%"; } const s = 0.34; const fontSize = Math.max(10, (config.font_size ?? 52) * s); const outlineW = (config.outline_size ?? 2.5) * s; const textShadow = outlineW > 0 ? `${outlineW}px ${outlineW}px 0 ${config.outline_color ?? "#000"}, -${outlineW}px ${outlineW}px 0 ${config.outline_color ?? "#000"}, ${outlineW}px -${outlineW}px 0 ${config.outline_color ?? "#000"}, -${outlineW}px -${outlineW}px 0 ${config.outline_color ?? "#000"}` : "none"; const textStyle: React.CSSProperties = { fontFamily: `"${config.font_family ?? "Noto Sans"}", sans-serif`, fontSize: `${fontSize}px`, fontWeight: config.bold ? "bold" : "normal", fontStyle: config.italic ? "italic" : "normal", textDecoration: config.underline ? "underline" : "none", color: config.primary_color ?? "#FFFFFF", textShadow, lineHeight: 1.25, }; return (
{[{ Icon: Heart, count: "12.4K" }, { Icon: MessageCircle, count: "892" }, { Icon: Share2, count: "Share" }].map(({ Icon, count }, i) => (
{count}
))}
@elevnclip_ai
#highlight #tiktok #ai #amd
{text}
PREVIEW
); } /* ── Animation CSS keyframes ── */ const ANIM_CSS = ` @keyframes elevn-fade { 0%,100%{opacity:0} 30%,70%{opacity:1} } @keyframes elevn-pop { 0%,100%{transform:scale(0.5);opacity:0} 40%{transform:scale(1.12)} 50%,80%{transform:scale(1);opacity:1} } @keyframes elevn-bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-9px)} } @keyframes elevn-type { from{clip-path:inset(0 100% 0 0)} to{clip-path:inset(0 0% 0 0)} } `; function AnimPreview({ animation, config, text }: { animation: string; config: StyleConfig; text: string }) { const s = 0.38; const fontSize = Math.max(12, (config.font_size ?? 52) * s); const outlineW = (config.outline_size ?? 2.5) * s; const textShadow = outlineW > 0 ? `${outlineW}px ${outlineW}px 0 ${config.outline_color ?? "#000"}, -${outlineW}px ${outlineW}px 0 ${config.outline_color ?? "#000"}, ${outlineW}px -${outlineW}px 0 ${config.outline_color ?? "#000"}, -${outlineW}px -${outlineW}px 0 ${config.outline_color ?? "#000"}` : "none"; const animMap: Record = { fade: "elevn-fade 2s ease-in-out infinite", pop: "elevn-pop 1.8s ease-in-out infinite", bounce: "elevn-bounce 1s ease-in-out infinite", typewriter: "elevn-type 2.5s steps(20) infinite", }; const baseStyle: React.CSSProperties = { fontFamily: `"${config.font_family ?? "Noto Sans"}", sans-serif`, fontSize: `${fontSize}px`, fontWeight: config.bold ? "bold" : "normal", fontStyle: config.italic ? "italic" : "normal", color: config.primary_color ?? "#fff", textShadow, display: "inline-block", whiteSpace: "nowrap", animation: animMap[animation] ?? "none", }; return ( <>
{animation === "karaoke" ? ( {text} ) : ( {text} )}
); } export default function SubtitleDesigner({ config, onChange, subtitleLanguage, uiLang = "en" }: Props) { const [colorPicker, setColorPicker] = useState(null); const lbl = L[uiLang]; const sampleText = SAMPLE_TEXTS[subtitleLanguage] ?? SAMPLE_TEXTS.default; const colorFields = [ { key: "primary_color", label: lbl.primary }, { key: "secondary_color", label: lbl.secondary }, { key: "outline_color", label: lbl.outlineC }, { key: "shadow_color", label: lbl.shadowC }, ]; return (
{/* Col 1 — TikTok preview */}
{/* Col 2 — Typography controls */}
{/* Font */}
{/* Size */}
onChange({ font_size: +e.target.value })} className="w-full mt-1 accent-violet-500" />
{/* Outline */}
onChange({ outline_size: +e.target.value })} className="w-full mt-1 accent-violet-500" />
{/* Shadow */}
onChange({ shadow_size: +e.target.value })} className="w-full mt-1 accent-violet-500" />
{/* B/I/U */}
{[ { key: "bold", label: "B", cls: "font-bold" }, { key: "italic", label: "I", cls: "italic" }, { key: "underline", label: "U", cls: "underline" }, ].map(({ key, label, cls }) => ( ))}
{/* Display mode */}
{[{ id: "word", label: lbl.word }, { id: "sentence", label: lbl.sentence }].map((m) => ( ))}
{/* Col 3 — Colors, Animation, Position */}
{/* Colors */}
{colorFields.map(({ key, label }) => (
{colorPicker === key && (
)[key] ?? "#ffffff"} onChange={(c) => onChange({ [key]: c })} />
)}
))}
{/* Animation preview + picker combined */}
{ANIMATIONS.map(({ id, Icon, en, th, zh }) => { const animLabel = uiLang === "th" ? th : uiLang === "zh" ? zh : en; return ( ); })}
{/* Position */}
{ALIGNMENTS.map((a) => ( ))}
onChange({ margin_v: +e.target.value })} className="w-full accent-violet-500" />
); }