Zirnavis21 / static /js /editor.js
Elias207's picture
Update static/js/editor.js
cfc7302 verified
// منطق اصلی ادیتور + Color Picker + Trimmer Logic
// ==========================
// --- 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;
// متغیرهای Trimmer (ویرایش زمان دقیق)
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.radius === undefined) state.st.radius = 16;
if (state.st.paddingX === undefined) state.st.paddingX = 20;
if (state.st.paddingY === undefined) state.st.paddingY = 10;
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'));
if(state.st.f === 'vazir') document.querySelectorAll('.font-btn')[1].classList.add('ticked');
else if(state.st.f === 'lalezar') document.querySelectorAll('.font-btn')[0].classList.add('ticked');
document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected'));
if(state.st.name === 'karaoke_static') {
const cards = document.querySelectorAll('.style-card');
cards.forEach(c => {
if(c.textContent.includes('ثابت') || (c.dataset.styleName === 'karaoke_static')) c.classList.add('selected');
});
}
document.getElementById('homeScreen').style.display = 'none';
document.getElementById('editorScreen').style.display = 'flex';
initSavedColors();
const palette = generateGridPalette();
renderGridWithData(palette);
renderSegList();
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;
// ذخیره ابعاد اصلاح شده در استیت برای جلوگیری از ارسال عدد 0 به سرور
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(['karaoke_static', 'auto_director', 'plain_white', 'white_outline'].includes(state.st.name)) {
state.st.name = 'classic';
document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected'));
const classicCard = document.querySelector('.custom-style-container .style-card:first-child');
if(classicCard) classicCard.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;
let obj = { word: wStr, start: parseFloat(wStart.toFixed(2)), end: parseFloat(wEnd.toFixed(2)) };
wStart = wEnd;
return obj;
});
}
seg.words.forEach((w, wIdx) => {
const el = document.createElement('div');
el.className = 'word-chip';
el.innerText = w.word;
const uid = `${sIdx}-${wIdx}`;
el.id = `w-${uid}`;
if (activeWordId === uid) el.classList.add('active');
el.onclick = (e) => { e.stopPropagation(); highlightWord(sIdx, wIdx, true); };
timeline.appendChild(el);
});
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;
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: 'center', inline: 'center' });
}
if(showToolbar) document.getElementById('toolbar').classList.add('show');
updateSplitButton();
}
// ============================================
// --- LOGIC: Trimmer & Precise Time Playback ---
// ============================================
function initTrimmerUI(startT, endT, minLimit, maxLimit) {
// تنظیم محدودیت‌های سراسری
trimMinLimit = minLimit;
trimMaxLimit = maxLimit;
trimViewDuration = trimMaxLimit - trimMinLimit;
// جلوگیری از خطای تقسیم بر صفر
if(trimViewDuration <= 0.001) trimViewDuration = 1;
// اعمال مقادیر اولیه به UI
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.transition = "transform 0.1s";
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();
// دریافت موقعیت X
let clientX;
if (e.touches && e.touches.length > 0) {
clientX = e.touches[0].clientX;
} else {
clientX = e.clientX;
}
// محاسبه درصد از سمت چپ
let percentFromLeft = (clientX - rect.left) / rect.width;
percentFromLeft = Math.max(0, Math.min(1, percentFromLeft));
// تبدیل به منطق RTL (راست به چپ)
// در این حالت: سمت راست (100% چپ) = شروع بازه (Min)
// سمت چپ (0% چپ) = پایان بازه (Max)
// فرمول معکوس:
let percentRTL = 1 - percentFromLeft;
// محاسبه زمان بر اساس درصد RTL
let newTime = trimMinLimit + (percentRTL * trimViewDuration);
newTime = Math.round(newTime * 100) / 100;
if (activeDragHandle === 'right') {
// دکمه سمت راست = شروع زمان (Start Time)
// نباید کمتر از حد مجاز (Min) باشد
if (newTime < trimMinLimit) newTime = trimMinLimit;
// نباید جلوتر از پایان فعلی برود
if (newTime >= tempEndTime - 0.05) newTime = tempEndTime - 0.05;
tempStartTime = newTime;
} else {
// دکمه سمت چپ = پایان زمان (End Time)
// نباید بیشتر از حد مجاز (Max) باشد
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);
// محاسبه موقعیت دکمه‌ها برای حالت RTL
// دکمه راست (Start): فاصله از لبه راست کادر
const percentStart = ((tempStartTime - trimMinLimit) / trimViewDuration) * 100;
// دکمه چپ (End): فاصله از لبه چپ کادر
// چون محور معکوس است: End Time به Max نزدیک‌تر است که سمت چپ قرار دارد
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 adjustTime(type, am) {
// اصلاح منطق دکمه‌های مثبت و منفی برای حالت RTL
if(type === 'start') {
let v = parseFloat((tempStartTime + am).toFixed(2));
if(v < trimMinLimit) v = trimMinLimit;
if(v >= tempEndTime - 0.05) v = parseFloat((tempEndTime - 0.05).toFixed(2));
tempStartTime = v;
} else {
let v = parseFloat((tempEndTime + am).toFixed(2));
if(v <= tempStartTime + 0.05) v = parseFloat((tempStartTime + 0.05).toFixed(2));
if(v > trimMaxLimit) v = trimMaxLimit;
tempEndTime = v;
}
updateTrimmerVisuals();
}
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;
if (wIdx === 0 && tempStartTime < seg.start) seg.start = tempStartTime;
if (wIdx === seg.words.length - 1 && tempEndTime > seg.end) seg.end = tempEndTime;
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(' '); 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.end = currSeg.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.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.segs) return;
const idx = state.segs.findIndex(s => currentTime >= s.start && currentTime <= s.end);
if(idx !== -1) {
const seg = state.segs[idx];
if(seg.isHidden) { tEl.style.opacity = 0; }
else {
tEl.style.opacity = 1;
const karaokeStyles = ['auto_director', 'karaoke_static'];
if(karaokeStyles.includes(state.st.name) && seg.words) {
let html = "";
// فاصله استاندارد بین کلمات (ثابت)
const GAP = 5;
seg.words.forEach((w, i) => {
let isActive = (currentTime >= w.start && currentTime <= w.end);
let boxColor = (state.st.name === 'auto_director')
? (i % 2 === 0 ? '#00D7FF' : '#FF0080')
: state.st.col;
let textColor = '#ffffff';
if (w.color) textColor = w.color;
// استایل پایه: اینلاین-بلاک برای اعمال پدینگ/مارجین، و تراز عمودی وسط
let baseStyle = "display: inline-block; vertical-align: middle; transition: all 0.1s; font-family: inherit;";
if(isActive) {
// تکنیک حاشیه منفی:
// 1. padding: کادر را بزرگ می‌کند.
// 2. margin منفی: اثر padding را خنثی می‌کند تا بقیه کلمات هل داده نشوند.
// فرمول افقی: فاصله استاندارد (GAP) منهای پدینگ تنظیمی
// فرمول عمودی: منفی پدینگ تنظیمی (برای جلوگیری از پریدن خط بالا/پایین)
let py = state.st.paddingY;
let px = state.st.paddingX;
html += `<span style="${baseStyle}
background-color: ${boxColor};
color: ${textColor} !important;
border-radius: ${state.st.radius}px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
padding: ${py}px ${px}px;
margin: -${py}px calc(${GAP}px - ${px}px);
position: relative;
z-index: 100;
white-space: nowrap;">${w.word}</span>`;
} else {
// کلمات غیرفعال: فقط فاصله استاندارد را دارند
html += `<span style="${baseStyle}
color: ${textColor} !important;
text-shadow: none;
margin: 0 ${GAP}px;
padding: 0;
position: relative;
z-index: 1;">${w.word}</span>`;
}
// شکستن خطوط
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) {
html += "<br><br>";
}
});
tEl.innerHTML = html;
}
else if (state.st.name === 'progressive_write' && seg.words) {
let html = "";
seg.words.forEach((w, i) => {
let styleExtra = "";
if (w.color) styleExtra = `color:${w.color} !important;`;
// تغییر ۱: اضافه کردن دیسپلی اینلاین-بلاک و مارجین برای جلوگیری از چسبیدن
let baseStyle = "display: inline-block; vertical-align: middle; margin: 0px 3px;";
if(currentTime >= w.start) html += `<span style="${baseStyle} opacity:1; ${styleExtra}">${w.word}</span> `;
else html += `<span style="${baseStyle} opacity:0; ${styleExtra}">${w.word}</span> `;
// تغییر ۲: استفاده از دو عدد اینتر (<br><br>) برای ایجاد فاصله بین خطوط بالا و پایین
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) {
html += "<br><br>";
}
});
tEl.innerHTML = html.trim();
}
else {
// For static blocks without word-level highlights in data, mostly fallback or manual override
// We'll rely on text segmentation. If words array exists, use it to break lines
if (seg.words && seg.words.length > 0) {
let html = "";
seg.words.forEach((w, i) => {
let styleExtra = "";
if (w.color) styleExtra = `color:${w.color} !important;`;
html += `<span style="${styleExtra}">${w.word}</span> `;
if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) {
html += "<br><br>";
}
});
tEl.innerHTML = html;
} else {
tEl.innerText = seg.text;
}
}
}
} else { tEl.style.opacity = 0; }
}
function upd() {
saveProjectToDB();
state.st.fz = Math.round(10 + (parseFloat(document.getElementById('fz').value) / 100) * 140);
state.st.y = Math.round((parseFloat(document.getElementById('pos').value) / 100) * 1200);
state.st.x = Math.round((parseFloat(document.getElementById('posX').value) / 100) * 500);
state.st.radius = parseInt(document.getElementById('radius').value, 10);
state.st.paddingX = parseInt(document.getElementById('paddingX').value, 10);
state.st.paddingY = parseInt(document.getElementById('paddingY').value, 10);
updateColorPreviewButtons();
let font = 'Vazirmatn';
if(state.st.f === 'lalezar') font = 'Lalezar'; if(state.st.f === 'bangers') font = 'Impact'; if(state.st.f === 'roboto') font = 'Arial';
tEl.style.fontFamily = font; tEl.style.fontSize = state.st.fz + 'px'; tEl.style.lineHeight = '0.8'; tEl.style.bottom = state.st.y + 'px';
tEl.style.textAlign = 'center'; tEl.style.left = '50%'; tEl.style.transform = `translateX(calc(-50% + ${state.st.x}px))`;
tEl.style.paintOrder = 'normal'; tEl.style.webkitPaintOrder = 'normal';
// Remove fixed padding here because we might handle it per word in karaoke
tEl.style.padding = '0';
tEl.style.borderRadius = '0px';
const karaokeStyles = ['karaoke_static', 'auto_director'];
if(karaokeStyles.includes(state.st.name)) {
tEl.style.backgroundColor = 'transparent'; tEl.style.color = '#FFFFFF'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none';
} else if (state.st.name === 'plain_white' || state.st.name === 'white_outline') {
tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = (state.st.name === 'white_outline') ? `${Math.max(3, state.st.fz / 4.5)}px #000000` : '0px'; tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitPaintOrder = 'stroke fill'; tEl.style.textShadow = 'none';
} else {
if(!state.st.col) state.st.col = '#FFFFFF'; if(!state.st.bg) state.st.bg = '#000000';
tEl.style.color = state.st.col;
// Apply box style to the container
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 boxPreview = document.getElementById('boxPreview');
const boxControlsPanel = document.getElementById('boxControlsPanel');
if (boxControlsPanel) {
const isBoxStyleActive = ['solid', 'transparent'].includes(state.st.type) || ['karaoke_static', 'auto_director'].includes(state.st.name);
boxControlsPanel.style.display = isBoxStyleActive ? 'block' : 'none';
if (isBoxStyleActive && boxPreview) {
boxPreview.style.borderRadius = `${state.st.radius}px`;
boxPreview.style.padding = `${state.st.paddingY}px ${state.st.paddingX}px`;
let previewColor = state.st.name === 'karaoke_static' ? state.st.col : state.st.bg;
boxPreview.style.backgroundColor = previewColor;
}
}
}
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(name === 'karaoke_static' && !skipModeSet) {
const defaultPurple = '#A020F0'; state.st.col = defaultPurple;
document.documentElement.style.setProperty('--static-color', defaultPurple);
}
if(name === 'classic' && !skipModeSet) setMode('solid');
else if (name === 'progressive_write') setMode('none');
else if (name === 'plain_white' || name === 'white_outline') {
state.st.col = '#FFFFFF'; state.st.bg = '#000000';
setMode('outline');
}
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();
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') {
if(activeWordId) {
const [s, w] = activeWordId.split('-').map(Number);
const wordObj = state.segs[s].words[w];
hex = wordObj.color || state.st.col;
} else {
hex = state.st.col;
}
}
let parsed = parseColorStringToState(hex || '#FFFFFF');
pickerState = parsed;
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;
if (pickerState.a >= 100) { colorStr = rgbToHex(rgb.r, rgb.g, rgb.b); } else { colorStr = `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); // آپدیت آنی پیش‌نمایش
}
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'); }); }