Zirnavis / static /js /editor.js
Opera15's picture
Update static/js/editor.js
8896936 verified
Raw
History Blame Contribute Delete
101 kB
// متغیرهای سراسری برای نگهداری کانفیگ استایل
let STYLE_CONFIGS = {};
let STYLE_TEMPLATES = {}; // <--- این خط اضافه شد
// لیست سیاه استایل‌ها برای هر کدام از انیمیشن‌ها (تعریف در ابتدا برای دسترسی سراسری ایمن)
const BLACKLIST_FADE_UNREAD = ['alpha_gradient', 'instagram_box', 'blue_cloud', 'falling_words', 'falling_karaoke'];
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'];
// تابع برای دریافت استایل‌ها از سرور
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';
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 = '<i class="fa-solid fa-plus"></i>';
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 = '<i class="fa-solid fa-arrow-turn-down" style="transform: rotate(90deg) scaleX(-1);"></i>';
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 += `<span style="${baseStyle} ${opacityStyle}
background: ${boxColor};
color: ${textColor} !important;
border-radius: ${state.st.radius}px;
box-shadow: ${customShadow};
padding: ${py}px ${px}px;
margin: 0 3px;
position: relative;
z-index: 100;
white-space: nowrap;">${w.word}</span>`;
} else {
html += `<span style="${baseStyle} ${opacityStyle}
color: ${textColor} !important;
text-shadow: none;
padding: ${py}px 0px;
margin: 0 3px;
position: relative;
z-index: 1;">${w.word}</span>`;
}
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br>"; }
});
tEl.innerHTML = html;
}
else if (state.st.name === 'falling_words' && seg.words) {
if (tEl.dataset.currentSegIndex != idx || tEl.innerHTML.trim() === "") {
tEl.dataset.currentSegIndex = idx;
let html = "";
seg.words.forEach((w, i) => {
const fwColor = (state.st.styleColors && state.st.styleColors['falling_words']) ? state.st.styleColors['falling_words'] : '#ffffff';
let styleExtra = w.color ? `color:${w.color} !important;` : `color:${fwColor} !important;`;
let baseStyle = `display: inline-block; vertical-align: middle; margin: 0px 3px; transition: all 0.3s ease-out; opacity: 0; transform: translateY(-60px);`;
html += `<span class="falling-item" data-widx="${i}" style="${baseStyle} ${styleExtra}">${w.word}</span> `;
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br><br>"; }
});
tEl.innerHTML = html;
void tEl.offsetWidth;
}
const baseFwColor = (state.st.styleColors && state.st.styleColors['falling_words']) ? state.st.styleColors['falling_words'] : '#ffffff';
const activeFwColor = (!isActOn) ? baseFwColor : ((state.st.styleActiveColors && state.st.styleActiveColors['falling_words']) ? state.st.styleActiveColors['falling_words'] : '#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; }
}
if (currentTime >= w.start) {
span.style.opacity = "1"; span.style.transform = "translateY(0)";
} else {
span.style.opacity = "0"; span.style.transform = "translateY(-60px)";
}
}
});
}
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 += `<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 (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 = `<span style="opacity:0; pointer-events:none;">${w.word}</span><span style="position:absolute; right:0; white-space:nowrap;">${typedText}</span>`;
}
// قرار دادن رنگ نهایی (متن) برای رنگ ابر (Cloud) تا با هم کاملاً ست شوند
let cloudHtml = `<span style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: 1; color: ${finalColor}; -webkit-text-stroke: 20px ${finalColor}; filter: blur(8px); opacity: 0.9; pointer-events: none;">${contentHTML}</span>`;
let mainHtml = `<span style="position: relative; z-index: 2; color: ${finalColor}; -webkit-text-stroke: 6px #ffffff; paint-order: stroke fill;">${contentHTML}</span>`;
// ترکیب لایه‌ها با فونت بولد
html += `<span style="position: relative; display: inline-block; margin: 0 5px; font-weight: 900; line-height: 1.1; letter-spacing: -0.5px; ${extraStyle}">${cloudHtml}${mainHtml}</span>`;
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br><br>"; }
});
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 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 = `<span style="opacity:0; pointer-events:none;">${w.word}</span><span style="position:absolute; right:0; white-space:nowrap;">${typedText}</span>`;
}
// فقط متن با دورخط ضخیم سفید، بدون المان ابر پشت سر آن
let mainHtml = `<span style="position: relative; z-index: 2; color: ${finalColor}; -webkit-text-stroke: 6px #ffffff; paint-order: stroke fill;">${contentHTML}</span>`;
html += `<span style="position: relative; display: inline-block; margin: 0 5px; font-weight: 900; line-height: 1.1; letter-spacing: -0.5px; ${extraStyle}">${mainHtml}</span>`;
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br><br>"; }
});
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 += `<span style="display: inline-block; margin: 0 8px; color: ${finalColor}; opacity: ${opacityStyle}; font-weight: 900; transition: color 0.1s; ${transformStyle}">${w.word}</span>`;
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br><br>"; }
});
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 += `<span class="fk-item" data-widx="${i}" style="display: inline-block; vertical-align: middle; margin: 0 5px; position: relative; z-index: 1; font-family: inherit;">` +
// لایه کادر: زیر متن قرار دارد (z-index: -1). بدون سایه. انیمیشن opacity افکت انتقال رنگ (آمپول) را می‌سازد
`<span class="fk-bg" style="position: absolute; top: 0; bottom: 0; left: -${px}px; right: -${px}px; border-radius: ${radius}px; opacity: 0; transition: opacity 0.25s linear; z-index: -1; box-shadow: none;"></span>` +
// لایه متن: از بالا می‌افتد و روی کادر می‌نشیند
`<span class="fk-text" style="display: inline-block; padding: ${py}px 0; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); opacity: 0; transform: translateY(-45px); font-family: inherit;">${w.word}</span>` +
`</span>`;
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { html += "<br>"; }
});
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 (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 = `<span style="opacity:0; pointer-events:none;">${w.word}</span><span style="position:absolute; right:0; top:0; white-space:nowrap;">${typedText}</span>`;
}
let br = ((state.st.name === 'simple_bar' || state.st.name === 'dark_edges') && (i + 1) % 5 === 0 && i !== seg.words.length - 1) ? "<br>" : " ";
return `<span style="position:relative; color:${color} !important; ${opacityStyle} transition: color 0.1s, opacity 0.1s; display:inline-block;">${contentHTML}</span>${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'].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 = `<span style="opacity:0; pointer-events:none;">${w.word}</span><span style="position:absolute; right:0; top:0; white-space:nowrap;">${typedText}</span>`;
styleExtra += " position:relative; display:inline-block;";
}
html += `<span style="${styleExtra}">${contentHTML}</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() {
// بازنشانی خودکار انیمیشن‌های غیرمجاز متناسب با استایل فعلی
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'];
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') {
const fwBg = (state.st.styleBgColors && state.st.styleBgColors['falling_words']) ? state.st.styleBgColors['falling_words'] : '#F77D57';
tEl.style.backgroundColor = fwBg;
tEl.style.color = state.st.col;
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)) {
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') {
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';
tEl.style.color = customC; 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';
}
}
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 (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';
}
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();
}
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<b?6:0);break; case g: h=(b-r)/d+2;break; case b: h=(r-g)/d+4;break;} h*=60;} return {h,s:s*100,v:v*100}; }
function parseColorStringToState(colorStr) { colorStr = colorStr.trim(); let r=255, g=255, b=255, a=100; if(colorStr.startsWith('#')) { let rgb = hexToRgb(colorStr); r=rgb.r; g=rgb.g; b=rgb.b; } else if(colorStr.startsWith('rgb')) { let parts = colorStr.match(/[\d.]+/g); if(parts && parts.length>=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 = `<div class="saved-circle" style="background-color:${hex}"></div><div class="mini-delete-btn"><i class="fa-solid fa-xmark"></i></div>`; 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'];
const stylesWithSecondaryColor = ['alpha_gradient', 'music_player', 'instagram_box', 'karaoke_static', 'dark_edges', 'falling_words', 'falling_karaoke'];
if (stylesWithCustomColor.includes(styleId)) {
const textIcon = '<svg viewBox="0 0 24 24"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>';
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 = `
<div class="settings-left-part">
<div class="settings-icon-box">${textIcon}</div>
<span class="settings-label">رنگ متن اختصاصی</span>
</div>
<div class="color-ring-container">
<div id="indicator-custom-color" class="color-circle-inner" style="background-color:${currentColor};"></div>
</div>
`;
container.appendChild(row);
if (stylesWithSecondaryColor.includes(styleId)) {
const bgIcon = '<svg viewBox="0 0 24 24" fill="var(--primary)"><path d="M12 22C6.49 22 2 17.51 2 12S6.49 2 12 2s10 4.04 10 9c0 3.31-2.69 6-6 6h-1.77c-.28 0-.5.22-.5.5 0 .12.05.23.13.33.41.47.64 1.06.64 1.67 0 1.38-1.12 2.5-2.5 2.5zm0-18c-4.41 0-8 3.59-8 8s3.59 8 8 8c.28 0 .5-.22.5-.5 0-.16-.08-.28-.14-.35-.41-.46-.63-1.05-.63-1.65 0-1.38 1.12-2.5 2.5-2.5H16c2.21 0 4-1.79 4-4 0-3.86-3.59-7-8-7z"/><circle cx="6.5" cy="11.5" r="1.5"/><circle cx="9.5" cy="7.5" r="1.5"/><circle cx="14.5" cy="7.5" r="1.5"/><circle cx="17.5" cy="11.5" r="1.5"/></svg>';
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 = `
<div class="settings-left-part">
<div class="settings-icon-box">${bgIcon}</div>
<span class="settings-label">رنگ کادر</span>
</div>
<div class="color-ring-container">
<div id="indicator-alpha-text" class="color-circle-inner" style="background-color:${secondColor};"></div>
</div>
`;
container.appendChild(row2);
}
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'];
if (stylesWithActiveColor.includes(styleId)) {
const activeIcon = '<svg viewBox="0 0 24 24" fill="var(--primary)"><path d="M7 2v11h3v9l7-12h-4l4-8z"/></svg>';
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 = `
<div class="setting-header" onclick="toggleActiveColor(this)">
<div class="settings-left-part">
<div class="settings-icon-box">${activeIcon}</div>
<span class="settings-label">رنگ کلمه فعال</span>
</div>
<div class="premium-toggle"></div>
</div>
<div class="dropdown-wrapper">
<div class="dropdown-inner">
<div class="dropdown-content">
<span style="color: #ccc; font-size: 0.85rem; font-weight: bold;">انتخاب رنگ:</span>
<div class="color-ring-container" onclick="openColorPicker('style_active_custom', event)">
<div id="indicator-active-color" class="color-circle-inner" style="background-color:${activeColor};"></div>
</div>
</div>
</div>
</div>`;
container.appendChild(row3);
if (styleId === 'instagram_box' || styleId === 'simple_bar') {
const boxIcon = '<svg viewBox="0 0 24 24" fill="var(--primary)"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"/></svg>';
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 = `<div class="settings-left-part"><div class="settings-icon-box">${boxIcon}</div><span class="settings-label">رنگ کادر اصلی</span></div><div class="color-ring-container"><div id="indicator-mainbox-color" class="color-circle-inner" style="background-color:${mainBoxColor};"></div></div>`;
container.appendChild(rowBox);
}
}
if (styleId === 'music_player' || styleId === 'simple_bar') {
const progressIcon = '<svg viewBox="0 0 24 24" fill="var(--primary)"><path d="M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12s4.48 10 10 10 10-4.48 10-10zm-2 0h-4c0-2.21-1.79-4-4-4s-4 1.79-4 4H4c0-4.42 3.58-8 8-8s8 3.58 8 8z"/></svg>';
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 = `<div class="settings-left-part"><div class="settings-icon-box">${progressIcon}</div><span class="settings-label">رنگ نوار پیشرفت</span></div><div class="color-ring-container"><div id="indicator-progress-color" class="color-circle-inner" style="background-color:${progressColor};"></div></div>`;
container.appendChild(row4);
}
} else {
container.innerHTML = '<div style="text-align:center; padding:20px; color:#555;">تنظیمات خاصی برای این استایل وجود ندارد.</div>';
}
}
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();
};