// متغیرهای دیتابیس const DB_NAME = "SubtitleStudioDB"; const DB_VERSION = 1; let db; let currentProjectId = null; let currentActionProjectId = null; let currentActionProjectName = ""; // وضعیت سراسری برنامه let state = { id: null, w: 1080, h: 1920, segs: [], language: 'fa', st: { f: 'pinar', fz: 45, col: '#A020F0', bg: '#000000', type: 'solid', y: 150, x: 0, name: 'alpha_gradient', radius: 24, paddingX: 5, paddingY: 15, useActiveColor: true } }; let curSeg = -1; let manualOverride = false; // --- چندزبانه (Multi-language) --- // لیست پیش‌فرض (در صورت عدم دسترسی به سرور موقع بارگذاری صفحه از همین استفاده می‌شود) let LANGUAGES_LIST = [ { code: 'fa', label: 'فارسی', flag: '🇮🇷', rtl: true }, { code: 'en', label: 'English', flag: '🇺🇸', rtl: false }, { code: 'ar', label: 'العربية', flag: '🇸🇦', rtl: true }, { code: 'tr', label: 'Türkçe', flag: '🇹🇷', rtl: false }, { code: 'es', label: 'Español', flag: '🇪🇸', rtl: false }, { code: 'fr', label: 'Français', flag: '🇫🇷', rtl: false }, { code: 'de', label: 'Deutsch', flag: '🇩🇪', rtl: false }, { code: 'it', label: 'Italiano', flag: '🇮🇹', rtl: false }, { code: 'pt', label: 'Português', flag: '🇵🇹', rtl: false }, { code: 'ur', label: 'اردو', flag: '🇵🇰', rtl: true }, ]; let selectedLanguage = 'fa'; // زبانی که کاربر برای پروژه بعدی انتخاب کرده let pendingSelectedVideoFile = null; // فایل انتخاب‌شده پیش از شروع آپلود function getLangInfo(code) { return LANGUAGES_LIST.find(l => l.code === code) || LANGUAGES_LIST[0]; } function getDefaultFontForLang(code) { if (code === 'fa') return 'pinar'; if (code === 'ar') return 'amiri'; if (code === 'ur') return 'nastaliq'; return 'vazir'; } async function initLanguagesList() { try { const r = await fetch('/api/languages'); if (r.ok) { const d = await r.json(); if (d.languages && d.languages.length > 0) { LANGUAGES_LIST = d.languages; } } } catch (e) { console.warn('Could not fetch languages list, using defaults', e); } renderLanguageList(); updateLanguageButtons(); } function renderLanguageList() { const box = document.getElementById('languageList'); if (!box) return; box.innerHTML = LANGUAGES_LIST.map(l => `
${l.flag} ${l.label} ${l.code === selectedLanguage ? '' : ''}
`).join(''); } function selectLanguage(code) { selectedLanguage = code; renderLanguageList(); updateLanguageButtons(); closeLanguageSheet(); } function updateLanguageButtons() { const info = getLangInfo(selectedLanguage); const homeFlag = document.getElementById('homeLangFlag'); const homeLabel = document.getElementById('homeLangLabel'); if (homeFlag) homeFlag.innerText = info.flag; if (homeLabel) homeLabel.innerText = info.label; const pillFlag = document.getElementById('langPillFlag'); const pillLabel = document.getElementById('langPillLabel'); if (pillFlag) pillFlag.innerText = info.flag; if (pillLabel) pillLabel.innerText = info.label; } function openLanguageSheet() { renderLanguageList(); document.getElementById('languageSheetOverlay').style.display = 'flex'; } function closeLanguageSheet() { document.getElementById('languageSheetOverlay').style.display = 'none'; } function formatFileSize(bytes) { if (!bytes) return '۰ مگابایت'; const mb = bytes / (1024 * 1024); if (mb < 1) return Math.round(bytes / 1024) + ' کیلوبایت'; return mb.toFixed(1) + ' مگابایت'; } // وقتی کاربر ویدیویی را از گالری/فایل‌ها انتخاب می‌کند: بلافاصله آپلود شروع نمی‌شود // بلکه کارت پیش‌نمایش با انتخاب زبان نمایش داده می‌شود تا کاربر تایید کند function onVideoFileChosen() { const inp = document.getElementById('fileIn'); const f = inp.files && inp.files[0]; if (!f) return; pendingSelectedVideoFile = f; document.getElementById('preUploadFileName').innerText = f.name; document.getElementById('preUploadFileSize').innerText = formatFileSize(f.size); updateLanguageButtons(); const overlay = document.getElementById('preUploadOverlay'); overlay.style.display = 'flex'; setTimeout(() => overlay.classList.add('show'), 10); } function cancelPreUpload() { pendingSelectedVideoFile = null; document.getElementById('fileIn').value = ''; const overlay = document.getElementById('preUploadOverlay'); overlay.classList.remove('show'); setTimeout(() => overlay.style.display = 'none', 250); } function confirmStartUpload() { const overlay = document.getElementById('preUploadOverlay'); overlay.classList.remove('show'); setTimeout(() => overlay.style.display = 'none', 250); startUpload(); } // --- ترجمه متن نمونه در قالب‌های آماده (سلام اینجا آلفا است) --- const DEMO_WORDS = { fa: ["سلام", "اینجا", "آلفا", "است"], en: ["Hi", "this", "is", "Alpha"], ar: ["مرحباً", "هنا", "ألفا", "الآن"], tr: ["Merhaba", "burada", "Alfa", "şimdi"], es: ["Hola", "aquí", "Alfa", "ahora"], fr: ["Salut", "ici", "Alpha", "maintenant"], de: ["Hallo", "hier", "Alpha", "jetzt"], it: ["Ciao", "qui", "Alfa", "adesso"], pt: ["Oi", "aqui", "Alfa", "agora"], ur: ["ہیلو", "یہاں", "الفا", "ہے"], }; function applyDemoLanguage(langCode) { const info = getLangInfo(langCode); const words = DEMO_WORDS[langCode] || DEMO_WORDS.fa; const isRtl = info.rtl; // چیدمان راست‌به‌چپ/چپ‌به‌راست کارت‌های پیش‌نمایش قالب‌ها بر اساس زبان پروژه document.querySelectorAll('.demo-row').forEach(row => { row.style.direction = isRtl ? 'rtl' : 'ltr'; }); // کلمه‌های تکی document.querySelectorAll('[data-dw]').forEach(el => { const idx = parseInt(el.getAttribute('data-dw'), 10); if (!isNaN(idx) && words[idx] !== undefined) el.textContent = words[idx]; }); // بازه‌ای از کلمات (مثلاً کلمات ۱ تا ۳ با هم در یک span) document.querySelectorAll('[data-dw-range]').forEach(el => { const range = el.getAttribute('data-dw-range').split('-').map(Number); el.textContent = words.slice(range[0], range[1] + 1).join(' '); }); // عبارت کامل در یک تکه (بدون نیاز به تغییر جهت چون یک متن یکپارچه است) document.querySelectorAll('.demo-full').forEach(el => { el.textContent = words.join(' '); }); } function aiEase(name, p) { p = Math.max(0, Math.min(1, p)); if (name === 'linear') return p; if (name === 'punch') return 1 - Math.pow(1 - p, 3); if (name === 'elastic') { if (p <= 0 || p >= 1) return p; return Math.pow(2, -10 * p) * Math.sin((p * 10 - 0.75) * (2 * Math.PI) / 3) + 1; } // smooth (پیش‌فرض) return 1 - (1 - p) * (1 - p); } function applyEditorTextDirection(langCode) { const info = getLangInfo(langCode); const dir = info.rtl ? 'rtl' : 'ltr'; const activeTextEl = document.getElementById('activeText'); if (activeTextEl) activeTextEl.dir = dir; const timelineEl = document.getElementById('timelineScroll'); if (timelineEl) timelineEl.dir = dir; } // المان‌های رابط کاربری const v = document.getElementById('vid'); const tEl = document.getElementById('activeText'); const toolsContainer = document.getElementById('toolsContainer'); // متغیرهای ویرایش متن let activeWordId = null; // Format: "segIndex-wordIndex" let previewInterval; let tempStartTime = 0; let tempEndTime = 0; // متغیرهای تاچ let initialY = 0, initialX = 0, initialBottom = 0, initialXState = 0, initialDist = 0, initialFontSize = 0, touchMode = null; // --- تابع اصلی نمایش زیرنویس در مرورگر (با منطق جدید) --- function updateOverlayContent(currentTime) { if (!state.segs) return; const idx = state.segs.findIndex(s => currentTime >= s.start && currentTime < s.end); if(idx !== -1) { const seg = state.segs[idx]; if(seg.isHidden) { tEl.style.opacity = 0; } else { tEl.style.opacity = 1; // ========================================================== // === بلوک ۱: منطق قدیمی برای استایل‌های کارائوکه (دست‌نخورده) === // ========================================================== const karaokeStyles = ['auto_director', 'karaoke_static', 'instagram_box', 'alpha_gradient']; if(karaokeStyles.includes(state.st.name) && seg.words) { let html = ""; const GAP = 5; seg.words.forEach((w, i) => { let isActive = (currentTime >= w.start && currentTime <= w.end); let boxColor = state.st.col; let textColor = '#ffffff'; if (state.st.name === 'instagram_box') { boxColor = '#1a1a1a'; if (isActive) { textColor = '#ffffff'; } else if (currentTime > w.end) { textColor = '#000000'; } else { textColor = '#d1d1d6'; } } else if (state.st.name === 'alpha_gradient') { boxColor = 'linear-gradient(135deg, #7c4dff, #b388ff)'; if (isActive) { textColor = '#ffffff'; } else if (currentTime > w.end) { textColor = '#ffffff'; } else { textColor = 'rgba(255,255,255,0.35)'; } } else if (state.st.name === 'auto_director') { boxColor = (i % 2 === 0 ? '#00D7FF' : '#FF0080'); } if (w.color) textColor = w.color; let baseStyle = "display: inline-block; vertical-align: middle; transition: all 0.1s; font-family: inherit;"; if(isActive) { let py = state.st.paddingY; let px = state.st.paddingX; html += `${w.word}`; } else { html += `${w.word}`; } if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "

"; } }); tEl.innerHTML = html; } // ================================================================= // === بلوک ۲: منطق قدیمی برای استایل تایپی (Progressive) (دست‌نخورده) === // ================================================================= else if (state.st.name === 'progressive_write' && seg.words) { let html = ""; seg.words.forEach((w, i) => { let styleExtra = w.color ? `color:${w.color} !important;` : ""; let baseStyle = "display: inline-block; vertical-align: middle; margin: 0px 3px;"; if(currentTime >= w.start) html += `${w.word} `; else html += `${w.word} `; if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "

"; } }); tEl.innerHTML = html.trim(); } // ============================================================================ // === بلوک ۳: منطق جدید و هوشمند برای موزیکال و استایل‌های آینده (کد جدید) === // ============================================================================ else if (STYLE_TEMPLATES && STYLE_TEMPLATES[state.st.name]) { const template = STYLE_TEMPLATES[state.st.name]; // برای استایل‌هایی مثل موزیک پلیر، کل متن سگمنت را جایگزین می‌کنیم const renderedHtml = template.replace(/{{WORD}}/g, seg.text); tEl.innerHTML = renderedHtml; } // ========================================================= // === بلوک ۴: منطق نهایی برای استایل‌های کلاسیک (دست‌نخورده) === // ========================================================= else { if (seg.words && seg.words.length > 0) { let html = ""; seg.words.forEach((w, i) => { let styleExtra = w.color ? `color:${w.color} !important;` : ""; html += `${w.word} `; if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "

"; } }); tEl.innerHTML = html; } else { tEl.innerText = seg.text; } } } } else { tEl.style.opacity = 0; } } function upd() { saveProjectToDB(); const elFz = document.getElementById('fz'); if(elFz) state.st.fz = Math.round(10 + (parseFloat(elFz.value) / 100) * 140); const elPos = document.getElementById('pos'); if(elPos) state.st.y = Math.round((parseFloat(elPos.value) / 100) * 1200); const elPosX = document.getElementById('posX'); if(elPosX) state.st.x = Math.round((parseFloat(elPosX.value) / 100) * 500); const elRadius = document.getElementById('radius'); if(elRadius) state.st.radius = parseInt(elRadius.value, 10); const elPadX = document.getElementById('paddingX'); if(elPadX) state.st.paddingX = parseInt(elPadX.value, 10); const elPadY = document.getElementById('paddingY'); if(elPadY) state.st.paddingY = parseInt(elPadY.value, 10); updateColorPreviewButtons(); let font = 'Vazirmatn'; if(state.st.f === 'lalezar') font = 'Lalezar'; if(state.st.f === 'bangers') font = 'Impact'; if(state.st.f === 'roboto') font = 'Arial'; tEl.style.fontFamily = font; tEl.style.fontSize = state.st.fz + 'px'; tEl.style.lineHeight = '0.8'; tEl.style.bottom = state.st.y + 'px'; tEl.style.textAlign = 'center'; tEl.style.left = '50%'; tEl.style.transform = `translateX(calc(-50% + ${state.st.x || 0}px))`; tEl.style.padding = '0'; tEl.style.borderRadius = '0px'; const karaokeStyles = ['karaoke_static', 'auto_director', 'alpha_gradient']; if (state.st.name === 'instagram_box') { tEl.style.backgroundColor = '#ffffff'; tEl.style.color = '#d1d1d6'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; const finalPaddingTop = (state.st.paddingY || 10) + 4; const finalPaddingBottom = (state.st.paddingY || 10) + 4; const finalPaddingX = (state.st.paddingX || 20) + 15; tEl.style.padding = `${finalPaddingTop}px ${finalPaddingX}px ${finalPaddingBottom}px ${finalPaddingX}px`; tEl.style.borderRadius = `${state.st.radius || 16}px`; } else if(karaokeStyles.includes(state.st.name)) { tEl.style.backgroundColor = 'transparent'; tEl.style.color = '#FFFFFF'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; } else if (state.st.name === 'plain_white' || state.st.name === 'white_outline') { tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = (state.st.name === 'white_outline') ? `${Math.max(3, state.st.fz / 4.5)}px #000000` : '0px'; tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitPaintOrder = 'stroke fill'; tEl.style.textShadow = 'none'; } else { if(!state.st.col) state.st.col = '#FFFFFF'; if(!state.st.bg) state.st.bg = '#000000'; tEl.style.color = state.st.col; tEl.style.padding = `${state.st.paddingY}px ${state.st.paddingX}px`; tEl.style.borderRadius = `${state.st.radius}px`; if(state.st.type === 'solid' || state.st.type === 'transparent') { let bgColor = state.st.bg; if (state.st.type === 'transparent') { if(bgColor.startsWith('#') && bgColor.length === 7) { let r = parseInt(bgColor.substring(1, 3), 16); let g = parseInt(bgColor.substring(3, 5), 16); let b = parseInt(bgColor.substring(5, 7), 16); bgColor = `rgba(${r},${g},${b},0.6)`; } else if(bgColor.startsWith('rgba')) { bgColor = bgColor.replace(/[\d.]+\)$/, '0.6)'); } } tEl.style.backgroundColor = bgColor; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; } else if (state.st.type === 'outline') { tEl.style.backgroundColor = 'transparent'; const s = Math.max(3, state.st.fz / 4.5); tEl.style.webkitTextStroke = `${s}px ${state.st.bg}`; tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitPaintOrder = 'stroke fill'; tEl.style.textShadow = 'none'; } else { tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; } } const boxControlsPanel = document.getElementById('boxControlsPanel'); const currentConfig = STYLE_CONFIGS[state.st.name]; if (boxControlsPanel && currentConfig) { const hasRadius = currentConfig.panels.includes('radius'); boxControlsPanel.style.display = hasRadius ? 'block' : 'none'; const boxPreview = document.getElementById('boxPreview'); if (boxPreview && hasRadius) { boxPreview.style.borderRadius = `${state.st.radius || 16}px`; boxPreview.style.padding = `${state.st.paddingY || 10}px ${state.st.paddingX || 20}px`; let previewBg = '#000'; if(state.st.name === 'instagram_box') previewBg = '#ffffff'; else if(state.st.name === 'alpha_gradient') previewBg = 'linear-gradient(135deg, #7c4dff, #b388ff)'; else if(state.st.bg) previewBg = state.st.bg; boxPreview.style.background = previewBg; boxPreview.style.color = (state.st.name === 'instagram_box') ? '#000' : '#fff'; } } }