diff --git "a/static/js/editor.js" "b/static/js/editor.js" new file mode 100644--- /dev/null +++ "b/static/js/editor.js" @@ -0,0 +1,1772 @@ +// متغیرهای سراسری برای نگهداری کانفیگ استایل +let STYLE_CONFIGS = {}; +let STYLE_TEMPLATES = {}; // <--- این خط اضافه شد + +// لیست سیاه استایل‌ها برای هر کدام از انیمیشن‌ها (تعریف در ابتدا برای دسترسی سراسری ایمن) +const BLACKLIST_FADE_UNREAD = ['alpha_gradient', 'instagram_box', 'blue_cloud', 'falling_words', 'falling_karaoke', 'falling_plain']; +const BLACKLIST_FADE_SURROUNDING = ['blue_cloud', 'falling_words', 'falling_karaoke']; +const BLACKLIST_TYPEWRITER = ['alpha_gradient', 'karaoke_static', 'instagram_box', 'auto_director', 'spring_popup', 'falling_words', 'falling_karaoke', 'falling_plain']; + +// تابع برای دریافت استایل‌ها از سرور +async function fetchStylesFromServer() { + try { + const r = await fetch('/api/styles'); + const data = await r.json(); + STYLE_CONFIGS = data.styles; + STYLE_TEMPLATES = data.templates; // <-- این خط اضافه می‌شود + console.log("Styles loaded:", Object.keys(STYLE_CONFIGS)); + console.log("Templates loaded:", Object.keys(STYLE_TEMPLATES)); // <-- این خط برای تست است + } catch (e) { + console.error("Failed to load styles:", e); + } +} + +fetchStylesFromServer(); + +// ========================== +// --- Editor Core Logic --- +// ========================== + +let pickerState = { h: 0, s: 0, v: 100, a: 100 }; +let currentTarget = null; +let savedColors = []; +let isDraggingSpectrum = false; +let currentDeleteIndex = -1; +let toastTimeout; +let trimMinLimit = 0; +let trimMaxLimit = 0; +let trimViewDuration = 0; +let activeDragHandle = null; + +function updateRange(input, labelId, unit = '%') { + const label = document.getElementById(labelId); + if (label) label.innerText = input.value + unit; + const min = parseFloat(input.min); + const max = parseFloat(input.max); + const val = parseFloat(input.value); + const percentage = ((val - min) / (max - min)) * 100; + input.style.backgroundSize = percentage + '% 100%'; +} + +function syncUIWithState() { + let fzPercent = ((state.st.fz - 10) / 140) * 100; + const fzInput = document.getElementById('fz'); + if(fzInput) { fzInput.value = Math.round(Math.max(0, Math.min(100, fzPercent))); updateRange(fzInput, 'lbl-size'); } + + let yPercent = (state.st.y / 1200) * 100; + const posInput = document.getElementById('pos'); + if(posInput) { posInput.value = Math.round(Math.max(0, Math.min(100, yPercent))); updateRange(posInput, 'lbl-y'); } + + let currentX = state.st.x || 0; + let xPercent = (currentX / 500) * 100; + const posXInput = document.getElementById('posX'); + if(posXInput) { posXInput.value = Math.round(Math.max(-100, Math.min(100, xPercent))); updateRange(posXInput, 'lbl-x'); } + + const radiusInput = document.getElementById('radius'); + if(radiusInput) { radiusInput.value = state.st.radius || 16; updateRange(radiusInput, 'lbl-radius', 'px'); } + + const paddingXInput = document.getElementById('paddingX'); + if(paddingXInput) { paddingXInput.value = state.st.paddingX || 20; updateRange(paddingXInput, 'lbl-paddingX', 'px'); } + + const paddingYInput = document.getElementById('paddingY'); + if(paddingYInput) { paddingYInput.value = state.st.paddingY || 10; updateRange(paddingYInput, 'lbl-paddingY', 'px'); } +} + +function openProject(projectId) { + currentProjectId = projectId; + const tx = db.transaction("projects", "readonly"); + const store = tx.objectStore("projects"); + const req = store.get(projectId); + + req.onsuccess = (e) => { + const p = e.target.result; + if (!p) { alert('پروژه یافت نشد'); loadHome(); return; } + + state = p.state || {}; + if (!state.st) state.st = {}; + if (state.st.radius === undefined) state.st.radius = 16; + if (state.st.paddingX === undefined) state.st.paddingX = 20; + if (state.st.paddingY === undefined) state.st.paddingY = 10; + if (state.st.fz === undefined) state.st.fz = 45; + if (!state.st.name) state.st.name = 'classic'; + if (!state.language) state.language = 'fa'; // پروژه‌های قدیمی‌تر، پیش‌فرض فارسی + + applyEditorTextDirection(state.language); + applyDemoLanguage(state.language); + + document.getElementById('currentProjectTitle').innerText = p.name; + + try { + const videoURL = URL.createObjectURL(p.videoBlob); + v.src = videoURL; + } catch(err) { + console.error("Error creating video URL:", err); + alert("خطا در بارگذاری ویدیو."); + return; + } + + updateColorPreviewButtons(); + syncUIWithState(); + + // تیک زدن دکمه فونت فعال به صورت هوشمند برای تمام فونت‌ها + document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); + const activeFontBtn = Array.from(document.querySelectorAll('.font-btn')).find(btn => btn.getAttribute('onclick').includes(`'${state.st.f}'`)); + if (activeFontBtn) activeFontBtn.classList.add('ticked'); + + // 1. حذف تیک از همه کارت‌ها + document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected')); + + // 2. پیدا کردن مستقیم کارتی که در onclick خود، نام استایل فعلی را دارد + const activeCard = document.querySelector(`.style-card[onclick*="'${state.st.name}'"]`); + + // 3. تیک زدن همان یک کارت + if (activeCard) { + activeCard.classList.add('selected'); + } + + document.getElementById('homeScreen').style.display = 'none'; + document.getElementById('editorScreen').style.display = 'flex'; + + initSavedColors(); + const palette = generateGridPalette(); + renderGridWithData(palette); + renderSegList(); + renderSettings(state.st.name); + + if (v.readyState >= 1) { fit(); upd(); } + else { + v.onloadeddata = () => { + fit(); upd(); + if(!p.thumbnail && v.duration > 1) { + v.currentTime = 1.0; + let captured = false; + v.addEventListener('seeked', function cap() { + if(captured) return; captured = true; saveProjectToDB(); v.currentTime = 0; + }, {once:true}); + } else if (!p.thumbnail) { saveProjectToDB(); } + }; + } + }; + + req.onerror = (e) => { console.error("Error opening project:", e); alert("خطا در باز کردن پروژه"); loadHome(); }; +} + +function goHome() { if(v) v.pause(); saveProjectToDB(); loadHome(); } + +function fit() { + const vw = (state.w && state.w > 0) ? state.w : v.videoWidth; + const vh = (state.h && state.h > 0) ? state.h : v.videoHeight; + if(!vw || !v) return; + if(!state.w || state.w === 0) { state.w = vw; state.h = vh; } + const ws = document.getElementById('workspace'); + const availableHeight = window.innerHeight * 0.6; + const scale = Math.min((ws.clientWidth - 40) / vw, availableHeight / vh); + const c = document.getElementById('videoContainer'); + c.style.width = vw + 'px'; + c.style.height = vh + 'px'; + document.getElementById('scaler').style.transform = `scale(${scale})`; + ws.style.height = (vh * scale + 40) + 'px'; +} + +function activateCustomStyle() { + if(state.st.name !== 'progressive_write') state.st.name = 'classic'; + document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected')); + const selector = state.st.name === 'progressive_write' ? '.style-card:nth-child(2)' : '.style-card:first-child'; + const targetCard = document.querySelector(`.custom-style-container ${selector}`); + if(targetCard) targetCard.classList.add('selected'); +} + +function renderSegList() { + saveProjectToDB(); + const timeline = document.getElementById('timelineScroll'); + const spacers = timeline.querySelectorAll('.spacer'); + timeline.innerHTML = ''; + timeline.appendChild(spacers[0]); + if(!state.segs) state.segs = []; + + state.segs.forEach((seg, sIdx) => { + if (!seg.words || seg.words.length === 0) { + const wordsArr = seg.text.trim().split(/\s+/).filter(w => w.length > 0); + const duration = seg.end - seg.start; + const timePerWord = duration / Math.max(1, wordsArr.length); + let wStart = seg.start; + seg.words = wordsArr.map((wStr, i) => { + let wEnd = wStart + timePerWord; + if (i === wordsArr.length - 1) wEnd = seg.end; + return { word: wStr, start: parseFloat(wStart.toFixed(2)), end: parseFloat(wEnd.toFixed(2)) }; + }); + } + + seg.words.forEach((w, wIdx) => { + // ساخت رپر (Wrapper) برای قرار دادن کلمه و دکمه (+) کنار هم + const wrapper = document.createElement('div'); + wrapper.style.display = 'inline-flex'; + wrapper.style.alignItems = 'center'; + + const el = document.createElement('div'); + // اعمال کلاس برای کلمات دستی اضافه شده (زیرکلمات) + el.className = 'word-chip' + (w.isManual ? ' manual-word' : ''); + el.innerText = w.word; + + const uid = `${sIdx}-${wIdx}`; + + // ساخت دکمه + به صورت جداگانه + const addBtn = document.createElement('div'); + addBtn.className = 'add-word-btn'; + addBtn.innerHTML = ''; + addBtn.onclick = (e) => { + e.stopPropagation(); + openAddWordModal(sIdx, wIdx); + }; + + // ترت��ب قرارگیری (چون استایل timeline rtl هست، دکمه در ظاهر سمت چپ میفته) + wrapper.appendChild(el); + wrapper.appendChild(addBtn); + + el.id = `w-${uid}`; + if (activeWordId === uid) el.classList.add('active'); + + el.onclick = (e) => { e.stopPropagation(); highlightWord(sIdx, wIdx, true); }; + timeline.appendChild(wrapper); + }); + + if (sIdx < state.segs.length - 1) { + const nl = document.createElement('div'); + nl.className = 'newline-indicator'; + nl.innerHTML = ''; + timeline.appendChild(nl); + } + }); + timeline.appendChild(spacers[1]); + updateSplitButton(); +} + +function highlightWord(sIdx, wIdx, showToolbar) { + activeWordId = `${sIdx}-${wIdx}`; + v.pause(); + togglePlayIcon(false); + if(!state.segs[sIdx] || !state.segs[sIdx].words[wIdx]) return; + const word = state.segs[sIdx].words[wIdx]; + // قرار دادن زمان ویدیو روی نقطه وسط کلمه جهت جلوگیری از تداخل با بازه کلمه قبل + v.currentTime = (word.start + word.end) / 2; + manualOverride = true; + updateOverlayContent(v.currentTime); + + // پاک کردن کلاس اکتیو از همه کلمات + document.querySelectorAll('.word-chip').forEach(c => c.classList.remove('active')); + + const el = document.getElementById(`w-${activeWordId}`); + if(el) { + el.classList.add('active'); // اضافه کردن کلاس اکتیو به کلمه کلیک شده + el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); + } + if(showToolbar) document.getElementById('toolbar').classList.add('show'); + updateSplitButton(); +} + +function initTrimmerUI(startT, endT, minLimit, maxLimit) { + trimMinLimit = minLimit; trimMaxLimit = maxLimit; trimViewDuration = trimMaxLimit - trimMinLimit; + if(trimViewDuration <= 0.001) trimViewDuration = 1; + updateTrimmerVisuals(); + const track = document.getElementById('trackActive'); if(track) track.style.display = 'none'; + const handleL = document.getElementById('handleLeft'); const handleR = document.getElementById('handleRight'); + const startDrag = (e, type) => { + if(e.cancelable) e.preventDefault(); e.stopPropagation(); + activeDragHandle = type; + e.target.style.transform = "scale(1.5)"; e.target.style.zIndex = "100"; + document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', endDrag); + document.addEventListener('touchmove', onDrag, {passive: false}); document.addEventListener('touchend', endDrag); + }; + if(handleL) { handleL.onmousedown = (e) => startDrag(e, 'left'); handleL.ontouchstart = (e) => startDrag(e, 'left'); handleL.style.position = 'absolute'; } + if(handleR) { handleR.onmousedown = (e) => startDrag(e, 'right'); handleR.ontouchstart = (e) => startDrag(e, 'right'); handleR.style.position = 'absolute'; } +} + +function onDrag(e) { + if (!activeDragHandle) return; + if(e.cancelable) e.preventDefault(); e.stopPropagation(); + const strip = document.getElementById('timelineStrip'); if (!strip) return; + const rect = strip.getBoundingClientRect(); + let clientX = (e.touches && e.touches.length > 0) ? e.touches[0].clientX : e.clientX; + let percentFromLeft = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); + let percentRTL = 1 - percentFromLeft; + let newTime = Math.round((trimMinLimit + (percentRTL * trimViewDuration)) * 100) / 100; + if (activeDragHandle === 'right') { + if (newTime < trimMinLimit) newTime = trimMinLimit; + if (newTime >= tempEndTime - 0.05) newTime = tempEndTime - 0.05; + tempStartTime = newTime; + } else { + if (newTime > trimMaxLimit) newTime = trimMaxLimit; + if (newTime <= tempStartTime + 0.05) newTime = tempStartTime + 0.05; + tempEndTime = newTime; + } + updateTrimmerVisuals(); +} + +function endDrag(e) { + if(!activeDragHandle) return; + const handleL = document.getElementById('handleLeft'); const handleR = document.getElementById('handleRight'); + if(handleL) { handleL.style.transform = "scale(1)"; handleL.style.zIndex = "10"; } + if(handleR) { handleR.style.transform = "scale(1)"; handleR.style.zIndex = "10"; } + activeDragHandle = null; + document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', endDrag); + document.removeEventListener('touchmove', onDrag); document.removeEventListener('touchend', endDrag); +} + +function updateTrimmerVisuals() { + const tStartDisp = document.getElementById('trimStartDisp'); const tEndDisp = document.getElementById('trimEndDisp'); + if(tStartDisp) tStartDisp.innerText = formatTimeMs(tempStartTime); + if(tEndDisp) tEndDisp.innerText = formatTimeMs(tempEndTime); + const percentStart = ((tempStartTime - trimMinLimit) / trimViewDuration) * 100; + const percentEnd = ((trimMaxLimit - tempEndTime) / trimViewDuration) * 100; + const handleL = document.getElementById('handleLeft'); const handleR = document.getElementById('handleRight'); + if(handleR) { handleR.style.right = `${percentStart}%`; handleR.style.left = 'auto'; } + if(handleL) { handleL.style.left = `${percentEnd}%`; handleL.style.right = 'auto'; } +} + +function formatTimeMs(t) { + let m = Math.floor(t / 60); let s = Math.floor(t % 60); let ms = Math.round((t - Math.floor(t)) * 100); + return `${m < 10 ? '0'+m : m}:${s < 10 ? '0'+s : s}.${ms < 10 ? '0'+ms : ms}`; +} + +function playPreviewWord() { + if ((tempStartTime === undefined) || (tempEndTime === undefined)) return; + if(previewInterval) clearInterval(previewInterval); + v.pause(); v.currentTime = tempStartTime; + const icon1 = document.getElementById('btnPreviewPlay')?.querySelector('i'); + if(icon1) icon1.className = "fa-solid fa-pause"; + const icon2 = document.getElementById('btnTimeSheetPlayIcon'); + if(icon2) icon2.className = "fa-solid fa-pause"; + v.play().then(() => { + previewInterval = setInterval(() => { + if(v.currentTime >= tempEndTime) { + v.pause(); clearInterval(previewInterval); + if(icon1) icon1.className = "fa-solid fa-play"; + if(icon2) icon2.className = "fa-solid fa-play"; + v.currentTime = tempEndTime; + } + }, 15); + }).catch(e => console.log("Play interrupted", e)); +} + +function saveText() { + if (!activeWordId) return; + const [sIdx, wIdx] = activeWordId.split('-').map(Number); + const val = document.getElementById('textInput').value.trim(); + if (val) { + state.segs[sIdx].words[wIdx].word = val; + state.segs[sIdx].text = state.segs[sIdx].words.map(w => w.word).join(' '); + renderSegList(); highlightWord(sIdx, wIdx, true); + } + closeAllSheets(); +} + +function confirmTimeChanges() { + if (!activeWordId) return; + const [sIdx, wIdx] = activeWordId.split('-').map(Number); + const seg = state.segs[sIdx]; + seg.words[wIdx].start = tempStartTime; + seg.words[wIdx].end = tempEndTime; + + // مرتب سازی کلمات و چسباندن دقیق زمان‌های ابتدا و انتهای سطر به کلمات باقی‌مانده + seg.words.sort((a, b) => a.start - b.start); + seg.start = seg.words[0].start; + seg.end = seg.words[seg.words.length - 1].end; + + closeAllSheets(); renderSegList(); highlightWord(sIdx, wIdx, true); +} +function cancelTimeChanges() { closeAllSheets(); } + +function confirmDeleteWord() { + if (!activeWordId) return; + const [sIdx, wIdx] = activeWordId.split('-').map(Number); + const seg = state.segs[sIdx]; + seg.words.splice(wIdx, 1); + + if (seg.words.length === 0) { + state.segs.splice(sIdx, 1); + activeWordId = null; + document.getElementById('toolbar').classList.remove('show'); + } + else { + seg.text = seg.words.map(w => w.word).join(' '); + // چسباندن دقیق زمان‌های ابتدا و انتهای سطر به اولین و آخرین کلمه موجود + seg.start = seg.words[0].start; + seg.end = seg.words[seg.words.length - 1].end; + activeWordId = null; + document.getElementById('toolbar').classList.remove('show'); + } + closeAllSheets(); renderSegList(); +} + +function toggleSplit() { + if (!activeWordId) return; + const [sIdx, wIdx] = activeWordId.split('-').map(Number); + if (wIdx === 0 && sIdx > 0) { + const prevSeg = state.segs[sIdx - 1]; const currSeg = state.segs[sIdx]; + prevSeg.words = prevSeg.words.concat(currSeg.words); + // همگام‌سازی زمان‌های سطر ترکیب‌شده + prevSeg.words.sort((a, b) => a.start - b.start); + prevSeg.start = prevSeg.words[0].start; + prevSeg.end = prevSeg.words[prevSeg.words.length - 1].end; + prevSeg.text = prevSeg.words.map(w => w.word).join(' '); + state.segs.splice(sIdx, 1); + const newWIdx = prevSeg.words.length - currSeg.words.length; + renderSegList(); highlightWord(sIdx - 1, newWIdx, true); + } else { + const seg = state.segs[sIdx]; + const wordsFirstHalf = seg.words.slice(0, wIdx); + const wordsSecondHalf = seg.words.slice(wIdx); + + seg.words = wordsFirstHalf; + seg.text = seg.words.map(w => w.word).join(' '); + // همگام‌سازی زمان سطر اول + seg.start = wordsFirstHalf[0].start; + seg.end = wordsFirstHalf[wordsFirstHalf.length-1].end; + + const newSeg = { + id: Date.now().toString() + "_" + Math.floor(Math.random() * 1000), + text: wordsSecondHalf.map(w => w.word).join(' '), + start: wordsSecondHalf[0].start, + end: wordsSecondHalf[wordsSecondHalf.length-1].end, + words: wordsSecondHalf, + isHidden: false + }; + state.segs.splice(sIdx + 1, 0, newSeg); + renderSegList(); highlightWord(sIdx + 1, 0, true); + } +} + +function updateSplitButton() { + const btn = document.getElementById('btnSplit'); if(!activeWordId) { btn.classList.remove('active-state'); return; } + const [sIdx, wIdx] = activeWordId.split('-').map(Number); + if (wIdx === 0 && sIdx > 0) { btn.classList.add('active-state'); btn.style.transform = "rotate(180deg)"; } else { btn.classList.remove('active-state'); btn.style.transform = ""; } +} + +function togglePlay() { if(v.paused) { v.play(); togglePlayIcon(true); } else { v.pause(); togglePlayIcon(false); } } +function togglePlayIcon(isPlaying) { const overlay = document.getElementById('playOverlay'); overlay.className = isPlaying ? 'playing' : ''; } + +function updateOverlayContent(currentTime) { + // --- حرکت نوار قرمز برای استایل موزیک --- + if ((state.st.name === 'music_player' || state.st.name === 'simple_bar') && v && v.duration) { + const prog = (1 - (currentTime / v.duration)) * 100; + tEl.style.setProperty('--progress', `${prog.toFixed(2)}%`); + } + + if (!state.segs) { + tEl.style.opacity = 0; + return; + } + + // پیدا کردن ردیف زمان‌بندی که زمان فعلی دقیقاً داخل آن است + const idx = state.segs.findIndex(s => currentTime >= s.start && currentTime <= s.end); + + if(idx !== -1) { + const isActOn = (state.st.styleActiveWordToggles && state.st.styleActiveWordToggles[state.st.name]) !== false; + 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 limit = (['auto_director', 'karaoke_static', 'alpha_gradient', 'instagram_box'].includes(state.st.name) && i < seg.words.length - 1) ? seg.words[i+1].start : w.end; let isActive = (currentTime >= w.start && currentTime < limit); + + const bgColors = state.st.styleBgColors || {}; + let boxColor = bgColors[state.st.name] || '#A020F0'; + const activeWordCol = (state.st.styleActiveColors && state.st.styleActiveColors[state.st.name]) ? state.st.styleActiveColors[state.st.name] : '#ffffff'; + + let textColor = '#ffffff'; + let customShadow = '0 0 10px rgba(0,0,0,0.3)'; + + if (state.st.name === 'karaoke_static') { + const customText = (state.st.styleColors && state.st.styleColors['karaoke_static']) ? state.st.styleColors['karaoke_static'] : '#ffffff'; + textColor = (isActive && isActOn) ? activeWordCol : customText; + } else if (state.st.name === 'auto_director') { + boxColor = (i % 2 === 0 ? '#00D7FF' : '#FF0080'); + const customText = (state.st.styleColors && state.st.styleColors['auto_director']) ? state.st.styleColors['auto_director'] : '#ffffff'; + textColor = (isActive && isActOn) ? activeWordCol : customText; + } else if (state.st.name === 'instagram_box') { + const bgColors = state.st.styleBgColors || {}; + boxColor = bgColors['instagram_box'] || '#1a1a1a'; + const instaColor = (state.st.styleColors && state.st.styleColors['instagram_box']) ? state.st.styleColors['instagram_box'] : '#000000'; + if (isActive) { textColor = isActOn ? activeWordCol : '#ffffff'; } else { textColor = instaColor; } + } else if (state.st.name === 'alpha_gradient') { + const alphaBg = (state.st.styleBgColors && state.st.styleBgColors['alpha_gradient']) ? state.st.styleBgColors['alpha_gradient'] : '#7c4dff'; + const rgb = hexToRgb(alphaBg); + const factor = 0.35; + const r2 = Math.round(rgb.r + (255 - rgb.r) * factor); + const g2 = Math.round(rgb.g + (255 - rgb.g) * factor); + const b2 = Math.round(rgb.b + (255 - rgb.b) * factor); + boxColor = `linear-gradient(135deg, ${alphaBg}, rgb(${r2}, ${g2}, ${b2}))`; + customShadow = `0 4px 12px rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.4)`; + let mainColor = (state.st.styleColors && state.st.styleColors['alpha_gradient']) ? state.st.styleColors['alpha_gradient'] : '#ffffff'; + if (isActive && isActOn) { textColor = activeWordCol; } else { textColor = mainColor; } + } else if (state.st.name === 'auto_director') { + const bgColors = state.st.styleBgColors || {}; + const customHighlight = bgColors['auto_director'] || '#00D7FF'; + boxColor = (i % 2 === 0 ? customHighlight : '#FF0080'); + } + + if (w.color) textColor = w.color; + + let baseStyle = "display: inline-block; vertical-align: middle; transition: all 0.1s; font-family: inherit;"; + let opacityStyle = ""; + if (!isActive) { + if (currentTime < w.start) { + if (state.st.name === 'alpha_gradient' || state.st.name === 'instagram_box') { + opacityStyle = "opacity: 0.35;"; + } else if (state.st.fadeUnread !== false || state.st.fadeSurrounding) { + opacityStyle = "opacity: 0.35;"; + } + } else if (currentTime >= limit && state.st.fadeSurrounding) { + opacityStyle = "opacity: 0.35;"; + } + } + + let py = state.st.paddingY; + let px = state.st.paddingX; + + if(isActive) { + html += `${w.word}`; + } else { + html += `${w.word}`; + } + + if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "
"; } + }); + tEl.innerHTML = html; + } + else if ((state.st.name === 'falling_words' || state.st.name === 'falling_plain') && seg.words) { + if (tEl.dataset.currentSegIndex != idx || tEl.innerHTML.trim() === "" || tEl.dataset.currentStyle !== state.st.name) { + tEl.dataset.currentSegIndex = idx; + tEl.dataset.currentStyle = state.st.name; + let html = ""; + seg.words.forEach((w, i) => { + const styleKey = state.st.name; + const fwColor = (state.st.styleColors && state.st.styleColors[styleKey]) ? state.st.styleColors[styleKey] : '#ffffff'; + let styleExtra = w.color ? `color:${w.color} !important;` : `color:${fwColor} !important;`; + let baseStyle = `display: inline-block; vertical-align: middle; margin: 0px 5px; transition: all 0.3s ease-out; opacity: 0; transform: translateY(-45px);`; + html += `${w.word} `; + if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "

"; } + }); + tEl.innerHTML = html; + void tEl.offsetWidth; + } + + const styleKey = state.st.name; + const baseFwColor = (state.st.styleColors && state.st.styleColors[styleKey]) ? state.st.styleColors[styleKey] : '#ffffff'; + const activeFwColor = (!isActOn) ? baseFwColor : ((state.st.styleActiveColors && state.st.styleActiveColors[styleKey]) ? state.st.styleActiveColors[styleKey] : '#FFEB3B'); + + const spans = tEl.querySelectorAll('.falling-item'); + spans.forEach((span) => { + const i = parseInt(span.dataset.widx); + const w = seg.words[i]; + if (w) { + if (w.color) { span.style.color = w.color; } + else { + if (currentTime >= w.start && currentTime < w.end) { span.style.color = activeFwColor; } + else { span.style.color = baseFwColor; } + } + + let opacityStyle = "1"; + if (currentTime < w.start) { + opacityStyle = "0"; // کلمات جلو تحت هیچ شرایطی نشان داده نمی��شوند (همواره مخفی) + } else if (currentTime >= w.end && state.st.fadeSurrounding) { + opacityStyle = "0.35"; // کلمات عقب در صورت فعال بودن دکمه کمرنگ می‌شوند + } + + span.style.opacity = opacityStyle; + + if (currentTime >= w.start) { + span.style.transform = "translateY(0)"; + } else { + span.style.transform = "translateY(-45px)"; + } + } + }); + } + else if (state.st.name === 'progressive_write' && seg.words) { + let html = ""; + let activeColor = (state.st.styleActiveColors && state.st.styleActiveColors['progressive_write']) ? state.st.styleActiveColors['progressive_write'] : '#ffffff'; + + seg.words.forEach((w, i) => { + let styleExtra = ""; + let baseStyle = "display: inline-block; vertical-align: middle; margin: 0px 3px;"; + let isWordActive = (currentTime >= w.start && currentTime < w.end); + + if (w.color) { styleExtra = `color:${w.color} !important;`; } + else if (isWordActive && isActOn) { styleExtra = `color:${activeColor} !important;`; } + + 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 (state.st.name === 'blue_cloud' && seg.words) { + let html = ""; + // گرفتن رنگ پایه انتخاب شده توسط کاربر (پیش‌فرض آبی روشن) + let customBaseColor = (state.st.styleColors && state.st.styleColors['blue_cloud']) ? state.st.styleColors['blue_cloud'] : '#00b4ff'; + // تعیین رنگ کلمه فعال (پیش‌فرض همان رنگ متن است) + let activeColor = (state.st.styleActiveColors && state.st.styleActiveColors['blue_cloud']) ? state.st.styleActiveColors['blue_cloud'] : customBaseColor; + + seg.words.forEach((w, i) => { + let isWordActive = (currentTime >= w.start && currentTime < w.end); + + // اگر کلمه فعال باشد رنگ فعال، وگرنه رنگ پایه + let finalColor = w.color ? w.color : (isWordActive && isActOn ? activeColor : customBaseColor); + + // --- منطق تایپ شدن --- + let contentHTML = w.word; + let extraStyle = ""; + let isTypewriter = state.st.typewriter; + if (currentTime < w.start) { + if (isTypewriter) extraStyle = "opacity: 0;"; + } else if (isTypewriter && isWordActive) { + let charLen = w.word.length; + let progress = (currentTime - w.start) / (w.end - w.start); + let visibleChars = Math.floor(progress * charLen) + 1; + if (visibleChars > charLen) visibleChars = charLen; + let typedText = w.word.substring(0, visibleChars); + contentHTML = `${w.word}${typedText}`; + } + + // قرار دادن رنگ نهایی (متن) برای رنگ ابر (Cloud) تا با هم کاملاً ست شوند + let cloudHtml = `${contentHTML}`; + let mainHtml = `${contentHTML}`; + + // ترکیب لایه‌ها با فونت بولد + html += `${cloudHtml}${mainHtml}`; + + if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "

"; } + }); + tEl.innerHTML = html.trim(); + } + /* ----- پایان استایل ابر ملایم آبی ----- */ + /* ----- شروع استایل آبی دورسفید (بدون ابر) ----- */ + else if (state.st.name === 'blue_outline' && seg.words) { + let html = ""; + let customBaseColor = (state.st.styleColors && state.st.styleColors['blue_outline']) ? state.st.styleColors['blue_outline'] : '#0000FF'; + let activeColor = (state.st.styleActiveColors && state.st.styleActiveColors['blue_outline']) ? state.st.styleActiveColors['blue_outline'] : customBaseColor; + let customStroke = (state.st.styleBgColors && state.st.styleBgColors['blue_outline_stroke']) ? state.st.styleBgColors['blue_outline_stroke'] : '#ffffff'; + let isTypewriter = state.st.typewriter; // <--- این خط را اضافه کنید + + seg.words.forEach((w, i) => { + let isWordActive = (currentTime >= w.start && currentTime < w.end); + let finalColor = w.color ? w.color : (isWordActive && isActOn ? activeColor : customBaseColor); + + // --- منطق تایپ شدن و کمرنگ شدن کلمات --- + let contentHTML = w.word; + let extraStyle = ""; + if (currentTime < w.start) { + if (isTypewriter) { + extraStyle = "opacity: 0;"; + } else if (state.st.fadeUnread !== false || state.st.fadeSurrounding) { + extraStyle = "opacity: 0.35; transition: opacity 0.1s;"; + } + } else if (currentTime >= w.end && state.st.fadeSurrounding) { + extraStyle = "opacity: 0.35; transition: opacity 0.1s;"; + } else if (isTypewriter && isWordActive) { + let charLen = w.word.length; + let progress = (currentTime - w.start) / (w.end - w.start); + let visibleChars = Math.floor(progress * charLen) + 1; + if (visibleChars > charLen) visibleChars = charLen; + let typedText = w.word.substring(0, visibleChars); + contentHTML = `${w.word}${typedText}`; + } + + // فقط متن با دورخط ضخیم (کاهش ۳۰ درصدی حاشیه در ویرایشگر)، بدون المان ابر پشت سر آن + let mainHtml = `${contentHTML}`; + + html += `${mainHtml}`; + + if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "

"; } + }); + tEl.innerHTML = html.trim(); + } + /* ----- پایان استایل آبی دورسفید ----- */ + /* ----- شروع استایل پاپ‌آپ فنری ----- */ + else if (state.st.name === 'spring_popup' && seg.words) { + let html = ""; + + // طبق درخواست شما: هر دو رنگ پایه و فعال به صورت پیش‌فرض کاملا سفید هستند + let customBaseColor = (state.st.styleColors && state.st.styleColors['spring_popup']) ? state.st.styleColors['spring_popup'] : '#ffffff'; + let activeColor = (state.st.styleActiveColors && state.st.styleActiveColors['spring_popup']) ? state.st.styleActiveColors['spring_popup'] : '#ffffff'; + + seg.words.forEach((w, i) => { + let isWordActive = (currentTime >= w.start && currentTime < w.end); + let finalColor = w.color ? w.color : (isWordActive && isActOn ? activeColor : customBaseColor); + + // پیش‌فرض شفافیت ۱ است و کلمات ساده شفاف نیستند (مگر دکمه‌های پنل روشن باشند) + let opacityStyle = "1"; + if (!isWordActive) { + if (currentTime < w.start && (state.st.fadeUnread !== false || state.st.fadeSurrounding)) { + opacityStyle = "0.35"; + } else if (currentTime >= w.end && state.st.fadeSurrounding) { + opacityStyle = "0.35"; + } + } + + let scale = 1.0; + + if (isWordActive) { + let timePassed = currentTime - w.start; + let animDur = 0.25; + if (timePassed < animDur) { + let t = timePassed / animDur; + let c1 = 1.70158; + let c3 = c1 + 1; + let progress = 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); + scale = 1.0 + (progress * 0.2); // بزرگ‌نمایی نرم از 1 به 1.2 + } else { + scale = 1.2; + } + } + + // تغییر ۱: مارجین ثابت (margin: 0 8px) باعث می‌شود کلمات مجاور اصلا تکان نخورند! + // تغییر ۲: transform-origin: center bottom باعث می‌شود کلمه فقط از سمت بالا قد بکشد و پایین آن ثابت بماند. + let transformStyle = `transform: scale(${scale}); transform-origin: center bottom;`; + + html += `${w.word}`; + + if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "

"; } + }); + tEl.innerHTML = html.trim(); + } + /* ----- پایان استایل پاپ‌آپ فنری ----- */ + /* ----- شروع استایل کارائوکه ریزان ----- */ + else if (state.st.name === 'falling_karaoke' && seg.words) { + // جلوگیری از رندر مجدد و خراب شدن انیمیشن‌ها + if (tEl.dataset.currentSegIndex != idx || !tEl.querySelector('.fk-item') || tEl.dataset.currentStyle !== 'falling_karaoke') { + tEl.dataset.currentSegIndex = idx; + tEl.dataset.currentStyle = 'falling_karaoke'; + + let html = ""; + // کادر متحرک کلی حذف شد. به جای آن افکت آمپول روی کادر هر کلمه پیاده می‌شود + + seg.words.forEach((w, i) => { + let py = state.st.paddingY || 10; + let px = state.st.paddingX || 20; + let radius = state.st.radius || 16; + + // ساختار جدید: لایه زیرین (کادر ثابت که با افکت آمپول روشن/خاموش می‌شود) + لایه متن (که از بالا می‌افتد) + html += `` + + // لایه کادر: زیر متن قرار دارد (z-index: -1). بدون سایه. انیمیشن opacity افکت انتقال رنگ (آمپول) را می‌سازد + `` + + // لایه متن: از بالا می‌افتد و روی کادر می‌نشیند + `${w.word}` + + ``; + if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "
"; } + }); + tEl.innerHTML = html; + void tEl.offsetHeight; // مرورگر را مجبور به ثبت نقطه شروع (-45px) می‌کند تا انیمیشن کلمه اول هم اجرا شود + } + + const activeBg = (state.st.styleBgColors && state.st.styleBgColors['falling_karaoke']) ? state.st.styleBgColors['falling_karaoke'] : '#F77D57'; + const activeTextColor = (state.st.styleActiveColors && state.st.styleActiveColors['falling_karaoke']) ? state.st.styleActiveColors['falling_karaoke'] : '#ffffff'; + const pastTextColor = (state.st.styleColors && state.st.styleColors['falling_karaoke']) ? state.st.styleColors['falling_karaoke'] : '#ffffff'; + let isActOn = (state.st.styleActiveWordToggles && state.st.styleActiveWordToggles['falling_karaoke']) !== false; + + const spans = tEl.querySelectorAll('.fk-item'); + + spans.forEach((span) => { + const i = parseInt(span.dataset.widx); + const w = seg.words[i]; + if (w) { + const textSpan = span.querySelector('.fk-text'); + const bgSpan = span.querySelector('.fk-bg'); + let isWordActive = (currentTime >= w.start && currentTime < w.end); + + // اعمال زنده (Real-time) تغییرات اسلایدرها (پدینگ و شعاع) + bgSpan.style.borderRadius = `${state.st.radius || 16}px`; + bgSpan.style.left = `-${state.st.paddingX || 20}px`; + bgSpan.style.right = `-${state.st.paddingX || 20}px`; + textSpan.style.padding = `${state.st.paddingY || 10}px 0`; + + // ۱. فرود کلمه از بالا روی کادر + if (currentTime >= w.start) { + textSpan.style.opacity = "1"; + textSpan.style.transform = "translateY(0)"; + } else { + textSpan.style.opacity = "0"; + textSpan.style.transform = "translateY(-45px)"; + } + + // ۲. رنگ متن + let textColor = (isWordActive && isActOn) ? activeTextColor : pastTextColor; + if (w.color) textColor = w.color; + textSpan.style.color = textColor; + + // ۳. افکت آمپول (Crossfade): رنگ به کادر این کلمه تزریق می‌شود و از کلمه قبلی همزمان کشیده می‌شود + bgSpan.style.background = activeBg; + if (isWordActive) { + bgSpan.style.opacity = "1"; + } else { + bgSpan.style.opacity = "0"; + } + } + }); + } + /* ----- پایان استایل کارائوکه ریزان ----- */ + else if (state.st.name === 'ai_dynamic' && seg.words) { + const design = state.st.ai_design || {}; + const entry = design.entry || 'fall'; + const exitType = design.exit || 'fade'; + const timing = design.timing || 'smooth'; + const stagger = (typeof design.stagger === 'number') ? design.stagger : 0.04; + const glow = design.glow || 'none'; + const glowColor = design.glowColor || design.shapeColor || '#7C4DFF'; + const activeColor = design.activeColor || state.st.col || '#FFD700'; + const primaryColor = design.primaryColor || state.st.col || '#ffffff'; + const ANIM_DUR = 0.3, EXIT_DUR = 0.3; + + let html = ""; + seg.words.forEach((w, i) => { + const extraDelay = stagger * i; + let dx = 0, dy = 0, alpha = 0; + const isActive = (currentTime >= w.start && currentTime < w.end); + + if (currentTime >= w.start) { + const timePassed = Math.max(0, (currentTime - w.start) - extraDelay); + const progress = Math.min(1, timePassed / ANIM_DUR); + const eased = aiEase(timing, progress); + if (entry === 'fall') dy = -70 * (1 - eased); + else if (entry === 'rise') dy = 65 * (1 - eased); + else if (entry === 'pop') dy = -18 * (1 - eased); + else if (entry === 'slide_left') dx = 85 * (1 - eased); + else if (entry === 'slide_right') dx = -85 * (1 - eased); + alpha = Math.max(0, Math.min(1, eased)); + + if (currentTime >= w.end && exitType !== 'none') { + const timeAfter = currentTime - w.end; + const exitProgress = Math.min(1, timeAfter / EXIT_DUR); + const exitEased = aiEase('smooth', exitProgress); + if (exitType === 'fade') alpha = Math.max(0.4, 1 - exitEased * 0.6); + else if (exitType === 'shrink') { alpha = Math.max(0.35, 1 - exitEased * 0.65); dy += 6 * exitEased; } + else if (exitType === 'fall') { dy += 25 * exitEased; alpha = Math.max(0, 1 - exitEased); } + else if (exitType === 'rise') { dy -= 25 * exitEased; alpha = Math.max(0, 1 - exitEased); } + } + } + + let color = isActive ? activeColor : primaryColor; + if (w.color) color = w.color; + + const fsz = state.st.fz || 60; + let glowStyle = ''; + const glowBlur = Math.round(fsz * 0.08); + if (glow === 'soft') glowStyle = `text-shadow: 0 0 ${glowBlur}px ${glowColor};`; + else if (glow === 'strong') glowStyle = `text-shadow: 0 0 ${glowBlur}px ${glowColor}, 0 0 ${glowBlur * 2}px ${glowColor}, 0 0 ${glowBlur * 3}px ${glowColor};`; + + html += `${w.word}`; + if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) html += "

"; + }); + tEl.innerHTML = html; + } + else if (STYLE_TEMPLATES && STYLE_TEMPLATES[state.st.name]) { + const template = STYLE_TEMPLATES[state.st.name]; + const fullText = seg.words.map((w, i) => { + let isActive = (currentTime >= w.start && currentTime <= w.end); + let defaultActive = (state.st.name === 'music_player') ? 'inherit' : (state.st.name === 'dark_edges' ? '#FFEB3B' : '#ffffff'); + let activeColor = (state.st.name !== 'music_player' && !isActOn) ? 'inherit' : ((state.st.styleActiveColors && state.st.styleActiveColors[state.st.name]) ? state.st.styleActiveColors[state.st.name] : defaultActive); + let color = w.color ? w.color : (isActive ? activeColor : 'inherit'); + + let isTypewriter = state.st.typewriter && ['simple_bar', 'dark_edges', 'music_player'].includes(state.st.name); + let opacityStyle = ""; + let contentHTML = w.word; + + if (currentTime < w.start) { + opacityStyle = isTypewriter ? "opacity: 0;" : ((state.st.fadeUnread !== false || state.st.fadeSurrounding) ? "opacity: 0.35;" : ""); + } else if (currentTime >= w.end && state.st.fadeSurrounding) { + opacityStyle = "opacity: 0.35;"; + } else if (isTypewriter && isActive) { + let charLen = w.word.length; + let progress = (currentTime - w.start) / (w.end - w.start); + let visibleChars = Math.floor(progress * charLen) + 1; + if (visibleChars > charLen) visibleChars = charLen; + let typedText = w.word.substring(0, visibleChars); + contentHTML = `${w.word}${typedText}`; + } + + let br = ((state.st.name === 'simple_bar' || state.st.name === 'dark_edges') && (i + 1) % 5 === 0 && i !== seg.words.length - 1) ? "
" : " "; + return `${contentHTML}${br}`; + }).join(''); + + const totalDuration = v.duration || 1; + const progressRatio = currentTime / totalDuration; + const progressPercent = (1 - progressRatio) * 100; + + tEl.style.setProperty('--progress', `${progressPercent.toFixed(2)}%`); + + if (tEl.dataset.activeStyle === state.st.name && tEl.querySelector('div')) { + const textSpan = tEl.querySelector('.music-text-inner') || (state.st.name === 'simple_bar' ? tEl.lastElementChild.lastElementChild : tEl.querySelector('div')) || tEl.lastElementChild; + + if (state.st.name === 'dark_edges') { + textSpan.style.padding = `${state.st.paddingY}px ${state.st.paddingX}px`; + textSpan.style.borderRadius = `${state.st.radius}px`; + } + + if (textSpan && textSpan.innerHTML !== fullText) { + textSpan.innerHTML = fullText; + if (state.st.name === 'music_player' && tEl.dataset.lastText !== seg.text) { + tEl.dataset.lastText = seg.text; + const sColors = state.st.styleColors || {}; + if (sColors['music_player']) textSpan.style.color = sColors['music_player']; + textSpan.style.transition = 'none'; + textSpan.style.transform = 'translateY(80px)'; + textSpan.style.opacity = '0'; + void textSpan.offsetHeight; + textSpan.style.transition = 'transform 0.6s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.6s'; + textSpan.style.transform = 'translateY(0)'; + textSpan.style.opacity = '1'; + } + } + } else { + const initialHTML = template + .replace(/{{WORD}}/g, fullText) + .replace(/{{PAD_X}}/g, state.st.paddingX || 20) + .replace(/{{PAD_Y}}/g, state.st.paddingY || 10) + .replace(/{{RADIUS}}/g, state.st.radius || 50); + tEl.innerHTML = initialHTML; + tEl.dataset.activeStyle = state.st.name; + } + + if (state.st.name === 'music_player') { + const sColors = state.st.styleColors || {}; + const bgColors = state.st.styleBgColors || {}; + const customBg = bgColors['music_player'] || '#ffffff'; + + const ts = tEl.querySelector('.music-text-inner'); + if(ts && sColors['music_player']) ts.style.color = sColors['music_player']; + + const box = tEl.querySelector('.music-text-box'); + if(box) box.style.backgroundColor = customBg; + + const bars = tEl.querySelectorAll('.v-bar'); + if(bars.length > 0) bars.forEach(b => b.style.backgroundColor = customBg); + + const progBar = tEl.querySelector('.progress-border-background'); + if(progBar) { + const progColor = bgColors['music_player_progress'] || '#d32f2f'; + progBar.style.backgroundColor = progColor; + progBar.style.boxShadow = `0 0.1em 0.3em ${progColor}`; + } + + const vBars = tEl.querySelectorAll('.v-bar'); + const playState = v.paused ? 'paused' : 'running'; + vBars.forEach(b => b.style.animationPlayState = playState); + } + + if (state.st.name === 'simple_bar') { + const sColors = state.st.styleColors || {}; + const bgColors = state.st.styleBgColors || {}; + const textDiv = tEl.firstElementChild ? tEl.firstElementChild.lastElementChild : null; + + if (textDiv) { + if (sColors['simple_bar']) textDiv.style.color = sColors['simple_bar']; + const mainBoxKey = state.st.name + '_main_box'; + if (bgColors[mainBoxKey]) textDiv.style.backgroundColor = bgColors[mainBoxKey]; + } + const progressBar = tEl.querySelector('div > div:nth-child(1) > div:nth-child(2)'); + const progKey = state.st.name + '_progress'; + if (progressBar && bgColors[progKey]) { + const newCol = bgColors[progKey]; + progressBar.style.backgroundColor = newCol; + progressBar.style.boxShadow = `0 0 8px ${newCol}`; + } + } + + if (state.st.name === 'dark_edges') { + const sColors = state.st.styleColors || {}; + const bgColors = state.st.styleBgColors || {}; + if (sColors['dark_edges']) tEl.firstElementChild.style.color = sColors['dark_edges']; + if (bgColors['dark_edges']) tEl.firstElementChild.style.backgroundColor = bgColors['dark_edges']; + } + + const wrapper = tEl.querySelector('.music-wrapper'); + const textBox = tEl.querySelector('.music-text-box'); + const textInner = tEl.querySelector('.music-text-inner'); + + if (wrapper && textBox && textInner) { + textInner.style.transform = 'none'; + const contentWidth = textInner.scrollWidth; + const containerWidth = textBox.clientWidth - 10; + if (contentWidth > containerWidth) { + const scale = containerWidth / contentWidth; + textInner.style.transform = `scale(${scale})`; + } + } + } + else { + if (seg.words && seg.words.length > 0) { + let html = ""; + let activeWordColor = '#ffffff'; + if (state.st.styleActiveColors && state.st.styleActiveColors[state.st.name]) { + activeWordColor = state.st.styleActiveColors[state.st.name]; + } + + seg.words.forEach((w, i) => { + let styleExtra = ""; + let isWordActive = (currentTime >= w.start && currentTime < w.end); + + if (w.color) { + styleExtra = `color:${w.color} !important;`; + } else if (isWordActive && isActOn) { + styleExtra = `color:${activeWordColor} !important; transition: color 0.1s;`; + } + + let isTypewriter = state.st.typewriter && ['plain_white', 'white_outline', 'classic', 'red_outline_box'].includes(state.st.name); + let contentHTML = w.word; + + if (currentTime < w.start) { + styleExtra += isTypewriter ? ` opacity: 0;` : ((state.st.fadeUnread !== false || state.st.fadeSurrounding) ? ` opacity: 0.35; transition: opacity 0.1s;` : ""); + } else if (currentTime >= w.end && state.st.fadeSurrounding) { + styleExtra += ` opacity: 0.35; transition: opacity 0.1s;`; + } else if (isTypewriter && isWordActive) { + let charLen = w.word.length; + let progress = (currentTime - w.start) / (w.end - w.start); + let visibleChars = Math.floor(progress * charLen) + 1; + if (visibleChars > charLen) visibleChars = charLen; + + let typedText = w.word.substring(0, visibleChars); + + contentHTML = `${w.word}${typedText}`; + styleExtra += " position:relative; display:inline-block;"; + } + + html += `${contentHTML} `; + + 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() { + // بازنشانی خودکار انیمیشن‌های غیرمجاز متناسب با استایل فعلی + if (typeof BLACKLIST_FADE_UNREAD !== 'undefined') { + if (BLACKLIST_FADE_UNREAD.includes(state.st.name)) state.st.fadeUnread = false; + if (BLACKLIST_FADE_SURROUNDING.includes(state.st.name)) state.st.fadeSurrounding = false; + if (BLACKLIST_TYPEWRITER.includes(state.st.name)) state.st.typewriter = false; + + // همگام‌سازی بصری دکمه‌ها در منوی انیمیشن تا دکمه‌ها بلافاصله روی صفحه خاموش (خاکستری) شوند + const unreadCard = document.querySelector('#sub-opacity .settings-row-toggle'); + if (unreadCard) unreadCard.classList.toggle('active', state.st.fadeUnread !== false); + + const surroundingCard = document.getElementById('surroundingToggleCard'); + if (surroundingCard) surroundingCard.classList.toggle('active', state.st.fadeSurrounding === true); + + const typeCard = document.getElementById('typewriterToggleCard'); + if (typeCard) typeCard.classList.toggle('active', state.st.typewriter === true); + } + 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'; + if(state.st.f === 'amiri') font = 'Amiri'; + if(state.st.f === 'sarbaz') font = 'Sarbaz'; + if(state.st.f === 'nastaliq') font = 'IranNastaliq'; + if(state.st.f === 'vazir-thin') font = 'VazirThin'; + if(state.st.f === 'mada-thin') font = 'MadaThin'; + if(state.st.f === 'aref-bold') font = 'ArefBold'; + if(state.st.f === 'dastnevis') font = 'Dastnevis'; + if(state.st.f === 'entazar') font = 'Entazar'; + if(state.st.f === 'kamran') font = 'Kamran'; + if(state.st.f === 'gharib') font = 'Gharib'; + if(state.st.f === 'pinar') font = 'Pinar'; + if(state.st.f === 'hasti') font = 'Hasti'; + + tEl.style.fontFamily = font; + tEl.style.fontWeight = (state.st.f === 'vazir-thin' || state.st.f === 'mada-thin') ? '100' : 'bold'; + tEl.style.fontSize = state.st.fz + 'px'; + + const compactStyles = ['falling_words', 'classic', 'progressive_write', 'plain_white', 'white_outline', 'spring_popup', 'red_outline_box', 'blue_outline', 'blue_cloud', 'falling_plain']; + + if (compactStyles.includes(state.st.name)) { + tEl.style.lineHeight = '0.8'; + } else { + tEl.style.lineHeight = '1.2'; + } + + 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', 'falling_karaoke']; + + if (state.st.name === 'instagram_box') { + const iBgColors = state.st.styleBgColors || {}; + const mainBoxKey = state.st.name + '_main_box'; + tEl.style.backgroundColor = iBgColors[mainBoxKey] || '#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 (state.st.name === 'falling_words' || state.st.name === 'red_outline_box') { + const fwBg = (state.st.styleBgColors && state.st.styleBgColors[state.st.name]) ? state.st.styleBgColors[state.st.name] : '#F77D57'; + tEl.style.backgroundColor = fwBg; + + // خواندن رنگ از حافظه اختصاصی استایل‌ها به جای متغیر عمومی + const customColor = (state.st.styleColors && state.st.styleColors[state.st.name]) ? state.st.styleColors[state.st.name] : '#ffffff'; + tEl.style.color = customColor; + + if (state.st.name === 'red_outline_box') { + const strokeWidth = Math.max(2, (state.st.fz / 12) * 1.5); + const customStroke = (state.st.styleBgColors && state.st.styleBgColors[state.st.name + '_stroke']) ? state.st.styleBgColors[state.st.name + '_stroke'] : '#ff0000'; + tEl.style.webkitTextStroke = `${strokeWidth}px ${customStroke}`; + tEl.style.paintOrder = 'stroke fill'; + tEl.style.webkitPaintOrder = 'stroke fill'; + } else { + tEl.style.webkitTextStroke = '0px'; + } + + tEl.style.textShadow = 'none'; + tEl.style.padding = `${state.st.paddingY}px ${state.st.paddingX}px`; + tEl.style.borderRadius = `${state.st.radius}px`; + + } else if(karaokeStyles.includes(state.st.name) || state.st.name === 'ai_dynamic') { + tEl.style.backgroundColor = 'transparent'; tEl.style.color = '#FFFFFF'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; + } else if (state.st.name === 'music_player' || state.st.name === 'simple_bar' || state.st.name === 'dark_edges' || state.st.name === 'blue_cloud' || state.st.name === 'blue_outline' || state.st.name === 'spring_popup' || state.st.name === 'falling_plain') { + tEl.style.background = 'none'; tEl.style.boxShadow = 'none'; tEl.style.padding = '0'; tEl.style.textShadow = 'none'; + } else if (state.st.name === 'plain_white' || state.st.name === 'white_outline') { + const customC = (state.st.styleColors && state.st.styleColors[state.st.name]) ? state.st.styleColors[state.st.name] : '#FFFFFF'; + const customStroke = (state.st.styleBgColors && state.st.styleBgColors[state.st.name + '_stroke']) ? state.st.styleBgColors[state.st.name + '_stroke'] : '#000000'; + tEl.style.color = customC; tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = (state.st.name === 'white_outline') ? `${Math.max(2.5, (state.st.fz / 4.5) * 0.85)}px ${customStroke}` : '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'; + } + } + updateOverlayContent(v.currentTime); +} + +function resegmentByWordCount(count) { + let allWords = []; + state.segs.forEach(s => { if(s.words) allWords.push(...s.words); }); + if(allWords.length === 0) return; + let newSegs = []; + for (let i = 0; i < allWords.length; i += count) { + let chunk = allWords.slice(i, i + count); + let start = chunk[0].start; + let end = chunk[chunk.length - 1].end; + let text = chunk.map(w => w.word).join(' '); + newSegs.push({ id: Date.now() + i, start: start, end: end, text: text, words: chunk, isHidden: false }); + } + state.segs = newSegs; + renderSegList(); + upd(); + saveProjectToDB(); + closeAllSheets(); + showToast(`هر جمله شامل ${count} کلمه شد`, "fa-solid fa-list-ol"); +} + +function updateColorPreviewButtons() { + const mainBtn = document.getElementById('preview-main-btn'); + if(mainBtn && state.st.col) mainBtn.style.backgroundColor = state.st.col; + const bgBtn = document.getElementById('preview-bg-btn'); + if(bgBtn && state.st.bg) bgBtn.style.backgroundColor = state.st.bg; +} + +function setStylePreset(name, el, skipModeSet = false) { + state.st.name = name; + document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected')); + if(el) el.classList.add('selected'); + + // پاکسازی کش رندر برای بروزرسانی و اعمال آنی استایل در لحظه کلیک + if (tEl) { + tEl.dataset.currentStyle = ""; + tEl.dataset.currentSegIndex = ""; + } + + // تعریف متغیر موقت برای نگهداری فونت اصلی کاربر + if (typeof window.lastActiveFontBeforeBlueCloud === 'undefined') { + window.lastActiveFontBeforeBlueCloud = state.st.f || 'pinar'; + } + + // اگر استایل ابر ملایم انتخاب شد، فونت به سرباز تغییر کند + if(name === 'blue_cloud') { + if (state.st.f && state.st.f !== 'sarbaz') { + window.lastActiveFontBeforeBlueCloud = state.st.f; // ذخیره کردن فونت اصلی کاربر + } + state.st.f = 'sarbaz'; + document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); + const activeFontBtn = Array.from(document.querySelectorAll('.font-btn')).find(btn => btn.getAttribute('onclick').includes(`'sarbaz'`)); + if (activeFontBtn) activeFontBtn.classList.add('ticked'); + } else { + // اگر استایل دیگری انتخاب شد و فونت هنوز روی سرباز بود، فونت اصلی کاربر بازیابی شود + if (state.st.f === 'sarbaz') { + state.st.f = window.lastActiveFontBeforeBlueCloud || 'pinar'; + document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); + const activeFontBtn = Array.from(document.querySelectorAll('.font-btn')).find(btn => btn.getAttribute('onclick').includes(`'${state.st.f}'`)); + if (activeFontBtn) activeFontBtn.classList.add('ticked'); + } + } + + if(name === 'karaoke_static' && !skipModeSet) { + const defaultPurple = '#A020F0'; state.st.col = defaultPurple; + document.documentElement.style.setProperty('--static-color', defaultPurple); + } + if(name === 'instagram_box') { + state.st.radius = 16; state.st.paddingY = 10; state.st.paddingX = 20; + } + + // فعال‌سازی خودکار انیمیشن «کمرنگ کردن جلو و عقب» در صورت خاموش بودن هنگام انتخاب پاپ‌آپ فنری + if(name === 'spring_popup') { + if (!state.st.fadeSurrounding) { + state.st.fadeSurrounding = true; + state.st.fadeUnread = false; // غیرفعال کردن انیمیشن یک طرفه جهت جلوگیری از تداخل + } + } + + if((name === 'classic' || name === 'progressive_write' || name === 'falling_words') && !skipModeSet) setMode('solid'); + else if (name === 'plain_white' || name === 'white_outline') { + state.st.col = '#FFFFFF'; state.st.bg = '#000000'; + setMode('outline'); + } + renderSettings(state.st.name); + upd(); +} + +function handleClassicDoubleClick(element) { + event.stopPropagation(); activateCustomStyle(); setStylePreset('classic', element, true); setMode('none'); +} + +function setFont(f, el) { document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); el.classList.add('ticked'); state.st.f = f; upd(); } +function setMode(m) { state.st.type = m; syncModeButtons(); upd(); } + +function openColorPicker(target, ev) { + if (ev) ev.stopPropagation(); + if ((target === 'main' || target === 'bg') && state.st.name !== 'classic' && state.st.name !== 'progressive_write') { + activateCustomStyle(); + setMode('solid'); + if (!document.getElementById('customAccordion').classList.contains('open')) toggleCustomAccordion(); + } + currentTarget = target; + document.getElementById('pickerBackdrop').classList.add('active'); + document.getElementById('colorPickerModal').classList.add('active'); + let hex = '#FFFFFF'; + if(target === 'main') hex = state.st.col; + else if(target === 'bg') hex = state.st.bg; + else if(target === 'static') hex = state.st.col; + else if(target === 'word' && activeWordId) { + const [s, w] = activeWordId.split('-').map(Number); + const wordObj = state.segs[s].words[w]; + hex = wordObj.color || state.st.col; + } + else if(target === 'temp_alpha') { + const defColor = state.st.name === 'falling_words' ? '#F77D57' : '#ffffff'; + hex = (state.st.styleBgColors && state.st.styleBgColors[state.st.name]) ? state.st.styleBgColors[state.st.name] : defColor; + } + else if(target === 'style_custom') { + const savedColors = state.st.styleColors || {}; + hex = savedColors[state.st.name] || '#ffffff'; + } + else if(target === 'style_active_custom') { + const savedColors = state.st.styleActiveColors || {}; + hex = savedColors[state.st.name] || '#ffffff'; + } + else if(target === 'style_progress') { + const savedBgColors = state.st.styleBgColors || {}; + hex = savedBgColors[state.st.name + '_progress'] || '#d32f2f'; + } + else if(target === 'style_main_box') { + const savedBgColors = state.st.styleBgColors || {}; + hex = savedBgColors[state.st.name + '_main_box'] || '#ffffff'; + } + else if(target === 'style_stroke') { + const savedBgColors = state.st.styleBgColors || {}; + let defaultStroke = '#ff0000'; + if (state.st.name === 'white_outline') defaultStroke = '#000000'; + else if (state.st.name === 'blue_outline') defaultStroke = '#ffffff'; + hex = savedBgColors[state.st.name + '_stroke'] || defaultStroke; + } + let parsed = parseColorStringToState(hex || '#FFFFFF'); + pickerState = parsed; + if (target === 'bg' && pickerState.v === 0) { + pickerState.v = 100; + pickerState.s = 0; + } + switchTab('spectrum'); + syncAllUI(); + syncGridSelectedFromCurrentColor(); +} +function closePicker() { document.getElementById('colorPickerModal').classList.remove('active'); document.getElementById('pickerBackdrop').classList.remove('active'); } +function saveAndClosePicker() { + const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v); + let colorStr = (pickerState.a >= 100) ? rgbToHex(rgb.r, rgb.g, rgb.b) : `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${pickerState.a/100})`; + if (currentTarget === 'main') state.st.col = colorStr; + else if (currentTarget === 'bg') state.st.bg = colorStr; + else if (currentTarget === 'static') { state.st.col = colorStr; document.documentElement.style.setProperty('--static-color', colorStr); } + else if (currentTarget === 'word' && activeWordId) { + const [s, w] = activeWordId.split('-').map(Number); + state.segs[s].words[w].color = colorStr; + updateOverlayContent(v.currentTime); + } + else if (currentTarget === 'temp_alpha') { + if (!state.st.styleBgColors) state.st.styleBgColors = {}; + state.st.styleBgColors[state.st.name] = colorStr; + const ind = document.getElementById('indicator-alpha-text'); + if(ind) ind.style.backgroundColor = colorStr; + updateOverlayContent(v.currentTime); + } + else if (currentTarget === 'style_custom') { + if (!state.st.styleColors) state.st.styleColors = {}; + state.st.styleColors[state.st.name] = colorStr; + + const ind = document.getElementById('indicator-custom-color'); + if(ind) ind.style.backgroundColor = colorStr; + updateOverlayContent(v.currentTime); + } + else if (currentTarget === 'style_active_custom') { + if (!state.st.styleActiveColors) state.st.styleActiveColors = {}; + state.st.styleActiveColors[state.st.name] = colorStr; + const ind = document.getElementById('indicator-active-color'); + if(ind) ind.style.backgroundColor = colorStr; + updateOverlayContent(v.currentTime); + } + else if (currentTarget === 'style_progress') { + if (!state.st.styleBgColors) state.st.styleBgColors = {}; + state.st.styleBgColors[state.st.name + '_progress'] = colorStr; + const ind = document.getElementById('indicator-progress-color'); + if(ind) ind.style.backgroundColor = colorStr; + updateOverlayContent(v.currentTime); + } + else if (currentTarget === 'style_main_box') { + if (!state.st.styleBgColors) state.st.styleBgColors = {}; + state.st.styleBgColors[state.st.name + '_main_box'] = colorStr; + const ind = document.getElementById('indicator-mainbox-color'); + if(ind) ind.style.backgroundColor = colorStr; + upd(); + } + else if (currentTarget === 'style_stroke') { + if (!state.st.styleBgColors) state.st.styleBgColors = {}; + state.st.styleBgColors[state.st.name + '_stroke'] = colorStr; + const ind = document.getElementById('indicator-stroke-color'); + if(ind) ind.style.backgroundColor = colorStr; + upd(); + } + upd(); closePicker(); +} +const spectrumAreaP = document.getElementById('spectrumArea'); const spectrumHandleP = document.getElementById('spectrumHandle'); const slBrightP = document.getElementById('brightness-slider'); const slAlphaP = document.getElementById('alpha-slider'); const previewBoxP = document.getElementById('largeColorPreview'); const gridContP = document.getElementById('gridContainer'); +function switchTab(name) { document.querySelectorAll('.picker-tab').forEach(t => t.classList.remove('active')); document.getElementById('tab-' + name).classList.add('active'); document.querySelectorAll('.view-section').forEach(v => v.classList.remove('active-view')); document.getElementById('view-' + name).classList.add('active-view'); const brRow = document.getElementById('brightness-row'); if(brRow) brRow.style.display = (name === 'manual') ? 'none' : 'flex'; syncAllUI(); if(name === 'grid') syncGridSelectedFromCurrentColor(); } +function handleSpectrum(e) { if(!spectrumAreaP) return; const rect = spectrumAreaP.getBoundingClientRect(); let x = Math.max(0, Math.min(e.clientX - rect.left, rect.width)); let y = Math.max(0, Math.min(e.clientY - rect.top, rect.height)); spectrumHandleP.style.left = x + 'px'; spectrumHandleP.style.top = y + 'px'; pickerState.h = (x / rect.width) * 360; pickerState.s = 100 - ((y / rect.height) * 100); syncAllUI(); syncGridSelectedFromCurrentColor(); } +if(spectrumAreaP) { spectrumAreaP.addEventListener('mousedown', e => { isDraggingSpectrum = true; handleSpectrum(e); }); window.addEventListener('mousemove', e => { if(isDraggingSpectrum) handleSpectrum(e); }); window.addEventListener('mouseup', () => isDraggingSpectrum = false); spectrumAreaP.addEventListener('touchstart', e => { isDraggingSpectrum = true; handleSpectrum(e.touches[0]); }, { passive:false }); window.addEventListener('touchmove', e => { if(isDraggingSpectrum) { e.preventDefault(); handleSpectrum(e.touches[0]); } }, { passive:false }); window.addEventListener('touchend', () => isDraggingSpectrum = false); } +function updateBrightness() { pickerState.v = parseInt(slBrightP.value, 10); syncAllUI(); syncGridSelectedFromCurrentColor(); } +function updateAlpha() { pickerState.a = parseInt(slAlphaP.value, 10); syncAllUI(); } +function updateRGBFromSliders() { const r = parseInt(document.getElementById('slider-r').value, 10); const g = parseInt(document.getElementById('slider-g').value, 10); const b = parseInt(document.getElementById('slider-b').value, 10); updateFromRGB(r, g, b); } +function updateRGBFromInputs() { const r = Math.min(255, Math.max(0, parseInt(document.getElementById('input-r').value, 10) || 0)); const g = Math.min(255, Math.max(0, parseInt(document.getElementById('input-g').value, 10) || 0)); const b = Math.min(255, Math.max(0, parseInt(document.getElementById('input-b').value, 10) || 0)); updateFromRGB(r, g, b); } +function updateFromRGB(r, g, b) { const hsv = rgbToHsv(r, g, b); pickerState.h = hsv.h; pickerState.s = hsv.s; pickerState.v = hsv.v; syncAllUI(); syncGridSelectedFromCurrentColor(); } +function updateFromHexInput() { let val = document.getElementById('input-hex').value.trim(); if(val.startsWith('#')) val = val.substring(1); if(/^[0-9A-Fa-f]{6}$/.test(val) || /^[0-9A-Fa-f]{3}$/.test(val)) { const rgb = hexToRgb('#'+val); const hsv = rgbToHsv(rgb.r,rgb.g,rgb.b); pickerState.h = hsv.h; pickerState.s = hsv.s; pickerState.v = hsv.v; pickerState.a = 100; syncAllUI(); syncGridSelectedFromCurrentColor(); } } +function syncAllUI() { const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v); const hex = rgbToHex(rgb.r, rgb.g, rgb.b); const rgba = `rgba(${rgb.r},${rgb.g},${rgb.b},${pickerState.a/100})`; const slR = document.getElementById('slider-r'), inR = document.getElementById('input-r'); const slG = document.getElementById('slider-g'), inG = document.getElementById('input-g'); const slB = document.getElementById('slider-b'), inB = document.getElementById('input-b'); const inHex = document.getElementById('input-hex'); slR.value = inR.value = rgb.r; slG.value = inG.value = rgb.g; slB.value = inB.value = rgb.b; if(document.activeElement !== inHex) inHex.value = hex.replace('#','').toUpperCase(); slR.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(0,${rgb.g},${rgb.b}), rgb(255,${rgb.g},${rgb.b}))`); slG.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(${rgb.r},0,${rgb.b}), rgb(${rgb.r},255,${rgb.b}))`); slB.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(${rgb.r},${rgb.g},0), rgb(${rgb.r},${rgb.g},255))`); slBrightP.value = Math.round(pickerState.v); document.getElementById('disp-brightness').innerText = Math.round(pickerState.v); const brightColor = hsvToRgb(pickerState.h, pickerState.s, 100); slBrightP.style.setProperty('--track-bg', `linear-gradient(90deg, #000000, rgb(${brightColor.r},${brightColor.g},${brightColor.b}))`); slAlphaP.value = Math.round(pickerState.a); document.getElementById('disp-alpha').innerText = Math.round(pickerState.a); slAlphaP.style.setProperty('--track-bg', `linear-gradient(90deg, rgba(${rgb.r},${rgb.g},${rgb.b},0), rgba(${rgb.r},${rgb.g},${rgb.b},1))`); if(previewBoxP) { previewBoxP.style.background = `linear-gradient(0deg, ${rgba}, ${rgba}), conic-gradient(#dedede 0.25turn, #ffffff 0.25turn 0.5turn, #dedede 0.5turn 0.75turn, #ffffff 0.75turn) top left / 16px 16px`; } if(!isDraggingSpectrum && spectrumHandleP) { spectrumHandleP.style.left = (pickerState.h / 360 * 100) + '%'; spectrumHandleP.style.top = (100 - pickerState.s) + '%'; } } +function rgbToHex(r,g,b) { r=Math.round(r);g=Math.round(g);b=Math.round(b); return "#" + ((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1).toUpperCase(); } +function hexToRgb(hex) { hex = (hex||'#000').replace('#','').trim(); if(hex.length===3) hex=hex.split('').map(c=>c+c).join(''); if(hex.length!==6) return {r:0,g:0,b:0}; return {r:parseInt(hex.substring(0,2),16),g:parseInt(hex.substring(2,4),16),b:parseInt(hex.substring(4,6),16)}; } +function hsvToRgb(h,s,v){ s/=100;v/=100; let c=v*s; let x=c*(1-Math.abs(((h/60)%2)-1)); let m=v-c; let r=0,g=0,b=0; if(h<60){r=c;g=x;}else if(h<120){r=x;g=c;}else if(h<180){g=c;b=x;}else if(h<240){g=x;b=c;}else if(h<300){r=x;b=c;}else{r=c;b=x;} return {r:Math.round((r+m)*255),g:Math.round((g+m)*255),b:Math.round((b+m)*255)}; } +function rgbToHsv(r,g,b){ r/=255;g/=255;b/=255; let max=Math.max(r,g,b), min=Math.min(r,g,b), d=max-min, h=0, s=max===0?0:d/max, v=max; if(d!==0){ switch(max){ case r: h=(g-b)/d+(g=3) { r = parseFloat(parts[0]); g = parseFloat(parts[1]); b = parseFloat(parts[2]); if(parts.length > 3) a = Math.round(parseFloat(parts[3]) * 100); } } const hsv = rgbToHsv(r,g,b); return { h:hsv.h, s:hsv.s, v:hsv.v, a:a }; } +function initSavedColors() { const s = localStorage.getItem('mySavedColors'); if(s) { try{savedColors=JSON.parse(s)||[];}catch(e){savedColors=[];} } renderSavedColors(); } +function saveCurrentColor() { const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v); const hex = rgbToHex(rgb.r, rgb.g, rgb.b); savedColors.push(hex); localStorage.setItem('mySavedColors', JSON.stringify(savedColors)); renderSavedColors(); showToast("این رنگ ��ر حافظه رنگ‌ها ذخیره شد", "fa-solid fa-save"); } +function renderSavedColors(){ const c = document.getElementById('savedColorsContainer'); if(!c) return; const svBtn = c.querySelector('.btn-save-text'); while(svBtn.nextSibling) svBtn.nextSibling.remove(); savedColors.forEach((hex, idx) => { const w = document.createElement('div'); w.className='saved-color-wrapper'; w.innerHTML = `
`; w.querySelector('.saved-circle').onclick = () => { let parsed = parseColorStringToState(hex); pickerState=parsed; syncAllUI(); syncGridSelectedFromCurrentColor(); }; w.querySelector('.mini-delete-btn').onclick = (e) => { e.stopPropagation(); currentDeleteIndex=idx; document.getElementById('delColorPreview').style.backgroundColor=hex; document.getElementById('deleteModal').classList.add('active'); }; c.appendChild(w); }); } +function showToast(message = "این رنگ در حافظه رنگ‌ها ذخیره شد", iconClass = "fa-solid fa-circle-check") { const t=document.getElementById('toastNotification'); if (!t) return; const span = t.querySelector('span'); const icon = t.querySelector('i'); if(span) span.innerText = message; if(icon) icon.className = iconClass; t.classList.add('show'); clearTimeout(toastTimeout); toastTimeout=setTimeout(()=>t.classList.remove('show'),2200); } +function confirmDelete(){ if(currentDeleteIndex>-1) { savedColors.splice(currentDeleteIndex,1); localStorage.setItem('mySavedColors',JSON.stringify(savedColors)); renderSavedColors(); } closeDeleteModal(); } +function closeDeleteModal(){ document.getElementById('deleteModal').classList.remove('active'); currentDeleteIndex=-1; } +function generateGridPalette() { const colors = ['#FFFFFF','#F2F2F7','#E5E5EA','#D1D1D6','#C7C7CC','#AEAEB2','#8E8E93','#636366','#48484A','#3A3A3C','#2C2C2E','#1C1C1E','#000000','#FF3B30','#FF453A','#FF9500','#FF9F0A','#FFD60A','#FFCC00','#34C759','#30D158','#00C7BE','#64D2FF','#32ADE6','#0A84FF','#007AFF','#5E5CE6','#5856D6','#AF52DE','#BF5AF2','#FF2D55']; for(let i=0;i<=20;i++){ let v=Math.round(i/20*255); colors.push(rgbToHex(v,v,v)); } for(let h=0;h<360;h+=12){ for(let s of [92,72]) for(let v of [100,85,70]) { colors.push(rgbToHex(hsvToRgb(h,s,v).r,hsvToRgb(h,s,v).g,hsvToRgb(h,s,v).b)); } } return [...new Set(colors)]; } +function renderGridWithData(colors){ if(!gridContP) return; gridContP.innerHTML=''; colors.forEach(hex => { const d = document.createElement('div'); d.className='grid-item'; d.style.backgroundColor=hex; d.setAttribute('data-hex', hex.toUpperCase()); d.onclick=()=>{ document.querySelectorAll('.grid-item').forEach(x=>x.classList.remove('selected')); d.classList.add('selected'); let parsed=parseColorStringToState(hex); pickerState=parsed; syncAllUI(); }; gridContP.appendChild(d); }); } +function syncGridSelectedFromCurrentColor() { if(!gridContP) return; const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v); const hex = rgbToHex(rgb.r,rgb.g,rgb.b).toUpperCase(); const items = gridContP.querySelectorAll('.grid-item'); items.forEach(it => { if(it.getAttribute('data-hex')===hex) it.classList.add('selected'); else it.classList.remove('selected'); }); } + +// --- توابع جدید بخش تنظیمات فونت --- +function switchSubTab(tabName, btnEl) { + if (btnEl) { + document.querySelectorAll('.sub-nav-btn').forEach(b => b.classList.remove('active')); + btnEl.classList.add('active'); + } + document.querySelectorAll('.sub-section-content').forEach(c => c.classList.remove('active')); + + if(tabName === 'settings') { + document.getElementById('sub-settings').classList.add('active'); + if(state && state.st) renderSettings(state.st.name); + } else if(tabName === 'opacity') { + document.getElementById('sub-opacity').classList.add('active'); + const unreadCard = document.querySelector('#sub-opacity .settings-row-toggle:nth-child(1)'); + if(unreadCard) unreadCard.classList.toggle('active', state.st.fadeUnread !== false); + const surroundingCard = document.getElementById('surroundingToggleCard'); + if(surroundingCard) surroundingCard.classList.toggle('active', state.st.fadeSurrounding === true); + const typeCard = document.getElementById('typewriterToggleCard'); + if(typeCard) typeCard.classList.toggle('active', state.st.typewriter === true); + } else { + document.getElementById('sub-fonts').classList.add('active'); + } +} + +function renderSettings(styleId) { + const container = document.getElementById('settingsContainerInline'); + if (!container) return; + container.innerHTML = ''; + + const stylesWithCustomColor = ['alpha_gradient', 'music_player', 'instagram_box', 'karaoke_static', 'auto_director', 'plain_white', 'white_outline', 'simple_bar', 'dark_edges', 'falling_words', 'falling_karaoke', 'classic', 'progressive_write', 'blue_cloud', 'blue_outline', 'spring_popup', 'falling_plain', 'red_outline_box']; + const stylesWithSecondaryColor = ['alpha_gradient', 'music_player', 'instagram_box', 'karaoke_static', 'dark_edges', 'falling_words', 'falling_karaoke', 'red_outline_box']; + + if (stylesWithCustomColor.includes(styleId)) { + const textIcon = ''; + const savedColors = state.st.styleColors || {}; + const currentColor = savedColors[styleId] || '#ffffff'; + + const row = document.createElement('div'); + row.className = 'settings-row'; + if (['classic', 'progressive_write'].includes(styleId)) row.style.display = 'none'; + row.onclick = function(e) { openColorPicker('style_custom', e); }; + + row.innerHTML = ` +
+
${textIcon}
+ رنگ متن اختصاصی +
+
+
+
+ `; + container.appendChild(row); + + if (stylesWithSecondaryColor.includes(styleId)) { + const bgIcon = ''; + const savedBgColors = state.st.styleBgColors || {}; + const secondColor = savedBgColors[styleId] || '#ffffff'; + + const row2 = document.createElement('div'); + row2.className = 'settings-row'; + row2.onclick = function(e) { openColorPicker('temp_alpha', e); }; + + row2.innerHTML = ` +
+
${bgIcon}
+ رنگ کادر +
+
+
+
+ `; + container.appendChild(row2); + } + + const stylesWithStrokeColor = ['red_outline_box', 'white_outline', 'blue_outline']; + if (stylesWithStrokeColor.includes(styleId)) { + const strokeIcon = ''; + const savedBgColors = state.st.styleBgColors || {}; + let defaultStroke = '#ff0000'; + if (styleId === 'white_outline') defaultStroke = '#000000'; + else if (styleId === 'blue_outline') defaultStroke = '#ffffff'; + const strokeColor = savedBgColors[styleId + '_stroke'] || defaultStroke; + + const rowStroke = document.createElement('div'); + rowStroke.className = 'settings-row'; + rowStroke.onclick = function(e) { openColorPicker('style_stroke', e); }; + rowStroke.innerHTML = ` +
+
${strokeIcon}
+ رنگ حاشیه متن +
+
+
+
+ `; + container.appendChild(rowStroke); + } + + const stylesWithActiveColor = ['alpha_gradient', 'instagram_box', 'karaoke_static', 'auto_director', 'simple_bar', 'dark_edges', 'falling_words', 'falling_karaoke', 'white_outline', 'plain_white', 'classic', 'progressive_write', 'blue_cloud', 'blue_outline', 'spring_popup', 'falling_plain', 'red_outline_box']; + if (stylesWithActiveColor.includes(styleId)) { + const activeIcon = ''; + const savedActiveColors = state.st.styleActiveColors || {}; + const activeColor = savedActiveColors[styleId] || '#ffffff'; + const row3 = document.createElement('div'); + const activeToggles = state.st.styleActiveWordToggles || {}; + const isToggleActive = activeToggles[styleId] !== false; + row3.className = 'settings-row-toggle' + (isToggleActive ? ' active' : ''); + row3.innerHTML = ` +
+
+
${activeIcon}
+ رنگ کلمه فعال +
+
+
+ `; + container.appendChild(row3); + + if (styleId === 'instagram_box' || styleId === 'simple_bar') { + const boxIcon = ''; + const savedBgColors = state.st.styleBgColors || {}; + const mainBoxColor = savedBgColors[styleId + '_main_box'] || '#ffffff'; + const rowBox = document.createElement('div'); + rowBox.className = 'settings-row'; + rowBox.onclick = function(e) { openColorPicker('style_main_box', e); }; + rowBox.innerHTML = `
${boxIcon}
رنگ کادر اصلی
`; + container.appendChild(rowBox); + } + } + + if (styleId === 'music_player' || styleId === 'simple_bar') { + const progressIcon = ''; + const savedBgColors = state.st.styleBgColors || {}; + const progressColor = savedBgColors[styleId + '_progress'] || '#d32f2f'; + const row4 = document.createElement('div'); + row4.className = 'settings-row'; + row4.onclick = function(e) { openColorPicker('style_progress', e); }; + row4.innerHTML = `
${progressIcon}
رنگ نوار پیشرفت
`; + container.appendChild(row4); + } + + } else { + container.innerHTML = '
تنظیمات خاصی برای این استایل وجود ندارد.
'; + } +} + +window.toggleUnreadOpacity = function(el) { + const currentStyle = state.st.name; + if (BLACKLIST_FADE_UNREAD.includes(currentStyle)) { + showToast("این انیمیشن برای استایل انتخاب شده در دسترس نیست", "fa-solid fa-triangle-exclamation"); + return; + } + const card = el.parentElement; + card.classList.toggle('active'); + state.st.fadeUnread = card.classList.contains('active'); + + if (state.st.fadeUnread) { + state.st.fadeSurrounding = false; + const surroundingCard = document.getElementById('surroundingToggleCard'); + if (surroundingCard) surroundingCard.classList.remove('active'); + } + + upd(); +}; + +window.toggleSurroundingOpacity = function(el) { + const currentStyle = state.st.name; + if (BLACKLIST_FADE_SURROUNDING.includes(currentStyle)) { + showToast("این انیمیشن برای استایل انتخاب شده در دسترس نیست", "fa-solid fa-triangle-exclamation"); + return; + } + const card = el.parentElement; + card.classList.toggle('active'); + state.st.fadeSurrounding = card.classList.contains('active'); + + if (state.st.fadeSurrounding) { + state.st.fadeUnread = false; + const unreadCard = document.querySelector('#sub-opacity .settings-row-toggle:nth-child(1)'); + if (unreadCard) unreadCard.classList.remove('active'); + } + + upd(); +}; + +window.toggleTypewriter = function(el) { + const currentStyle = state.st.name; + if (BLACKLIST_TYPEWRITER.includes(currentStyle)) { + showToast("این انیمیشن برای استایل انتخاب شده در دسترس نیست", "fa-solid fa-triangle-exclamation"); + return; + } + const card = el.parentElement; + card.classList.toggle('active'); + state.st.typewriter = card.classList.contains('active'); + upd(); +}; +window.toggleActiveColor = function(el) { + const card = el.parentElement; + card.classList.toggle('active'); + if (!state.st.styleActiveWordToggles) state.st.styleActiveWordToggles = {}; + state.st.styleActiveWordToggles[state.st.name] = card.classList.contains('active'); + upd(); +}; + +// --- منطق اضافه کردن دستی کلمه جدید و تقسیم زمان --- + +let targetAddWordInfo = null; + +window.openAddWordModal = function(sIdx, wIdx) { + targetAddWordInfo = { sIdx, wIdx }; + const modal = document.getElementById('addWordModal'); + document.getElementById('newWordInput').value = ''; + modal.style.display = 'flex'; + setTimeout(() => modal.classList.add('show'), 10); + setTimeout(() => document.getElementById('newWordInput').focus(), 100); +}; + +window.closeAddWordModal = function() { + const modal = document.getElementById('addWordModal'); + modal.classList.remove('show'); + setTimeout(() => modal.style.display = 'none', 300); + targetAddWordInfo = null; +}; + +window.confirmAddWord = function() { + if (!targetAddWordInfo) return; + const { sIdx, wIdx } = targetAddWordInfo; + const val = document.getElementById('newWordInput').value.trim(); + + if (!val) { + closeAddWordModal(); + return; + } + + const seg = state.segs[sIdx]; + + // 1. اضافه کردن کلمه جدید دقیقاً بعد از کلمه انتخاب شده + const newWord = { + word: val, + start: 0, + end: 0, + isManual: true, // مشخص کننده زیرکلمه دستی + highlight: false, + color: null + }; + + seg.words.splice(wIdx + 1, 0, newWord); + + // 2. محاسبات ریاضی برای تقسیم عادلانه زمان بین کلمات اصلی و دستی + + // الف) پیدا کردن کلمه اصلی (هوش مصنوعی) قبل از این مجموعه کلمات + let origIdx = wIdx; + while (origIdx >= 0 && seg.words[origIdx].isManual) { + origIdx--; + } + if (origIdx < 0) origIdx = 0; // در حالت استثنایی که کلمه اول هم دستی باشد + + // ب) پیدا کردن کلمه اصلی (هوش مصنوعی) بعدی + let nextOrigIdx = wIdx + 2; // wIdx + 1 کلمه جدید است که الان اضافه شد + while (nextOrigIdx < seg.words.length && seg.words[nextOrigIdx].isManual) { + nextOrigIdx++; + } + + // ج) محاسبه کل زمان موجود برای این بلاک + let startTime = seg.words[origIdx].start; + let endTime = (nextOrigIdx < seg.words.length) ? seg.words[nextOrigIdx].start : seg.end; + + // د) تقسیم مساوی زمان بین کلمه اصلی و تمام زیرکلمات جدید + let count = nextOrigIdx - origIdx; + let step = (endTime - startTime) / count; + + for (let k = origIdx; k < nextOrigIdx; k++) { + seg.words[k].start = startTime + (k - origIdx) * step; + seg.words[k].end = seg.words[k].start + step; + } + + // 3. بروزرسانی متن کامل سگمنت + seg.text = seg.words.map(w => w.word).join(' '); + + closeAddWordModal(); + + // 4. رفرش کردن ظاهر و هایلایت کردن کلمه جدید + renderSegList(); + highlightWord(sIdx, wIdx + 1, true); + upd(); +}; \ No newline at end of file