Spaces:
Paused
Paused
| // متغیرهای دیتابیس | |
| 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 => ` | |
| <div class="lang-row ${l.code === selectedLanguage ? 'active' : ''}" onclick="selectLanguage('${l.code}')"> | |
| <span class="lang-flag">${l.flag}</span> | |
| <span class="lang-name">${l.label}</span> | |
| ${l.code === selectedLanguage ? '<i class="fa-solid fa-circle-check lang-check"></i>' : ''} | |
| </div> | |
| `).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 += `<span style="${baseStyle} background: ${boxColor}; color: ${textColor} !important; border-radius: ${state.st.radius}px; box-shadow: 0 0 10px rgba(0,0,0,0.3); padding: ${py}px ${px}px; margin: -${py}px calc(${GAP}px - ${px}px); position: relative; z-index: 100; white-space: nowrap;">${w.word}</span>`; | |
| } else { | |
| html += `<span style="${baseStyle} color: ${textColor} !important; text-shadow: none; margin: 0 ${GAP}px; padding: 0; position: relative; z-index: 1;">${w.word}</span>`; | |
| } | |
| if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br><br>"; } | |
| }); | |
| 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 += `<span style="${baseStyle} opacity:1; ${styleExtra}">${w.word}</span> `; | |
| else html += `<span style="${baseStyle} opacity:0; ${styleExtra}">${w.word}</span> `; | |
| if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br><br>"; } | |
| }); | |
| 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 += `<span style="${styleExtra}">${w.word}</span> `; | |
| if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br><br>"; } | |
| }); | |
| 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'; | |
| } | |
| } | |
| } |