Spaces:
Sleeping
Sleeping
| // منطق اصلی ادیتور (نمایش ویدیو، سینک متن، ابزارها) | |
| 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; | |
| document.getElementById('currentProjectTitle').innerText = p.name; | |
| const videoURL = URL.createObjectURL(p.videoBlob); | |
| v.src = videoURL; | |
| document.getElementById('col').value = state.st.col; | |
| document.getElementById('bgCol').value = state.st.bg; | |
| document.getElementById('fz').value = state.st.fz; | |
| document.getElementById('pos').value = state.st.y; | |
| 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 staticCard = Array.from(document.querySelectorAll('.style-card')).find(c => c.textContent.includes("هوشمند (ثابت)")); | |
| if(staticCard) staticCard.classList.add('selected'); | |
| document.getElementById('staticColorPicker').value = state.st.col; | |
| } | |
| document.getElementById('homeScreen').style.display = 'none'; | |
| document.getElementById('editorScreen').style.display = 'flex'; | |
| renderSegList(); | |
| fit(); | |
| upd(); | |
| v.onloadeddata = () => { | |
| fit(); | |
| 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(); } | |
| }; | |
| }; | |
| } | |
| function goHome() { v.pause(); saveProjectToDB(); loadHome(); } | |
| function fit() { | |
| if(!state.w) return; | |
| const ws = document.getElementById('workspace'); | |
| const availableHeight = window.innerHeight * 0.6; | |
| const scale = Math.min((ws.clientWidth - 40) / state.w, availableHeight / state.h); | |
| const c = document.getElementById('videoContainer'); | |
| c.style.width = state.w + 'px'; c.style.height = state.h + 'px'; | |
| document.getElementById('scaler').style.transform = `scale(${scale})`; | |
| ws.style.height = (state.h * 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]); // Start spacer | |
| 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]); // End spacer | |
| updateSplitButton(); | |
| } | |
| function highlightWord(sIdx, wIdx, showToolbar) { | |
| activeWordId = `${sIdx}-${wIdx}`; | |
| v.pause(); | |
| togglePlayIcon(false); | |
| const seg = state.segs[sIdx]; | |
| const word = seg.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(); | |
| } | |
| function playPreviewWord() { | |
| if (!activeWordId) return; | |
| const [sIdx, wIdx] = activeWordId.split('-').map(Number); | |
| const word = state.segs[sIdx].words[wIdx]; | |
| if(previewInterval) clearInterval(previewInterval); | |
| v.pause(); | |
| v.currentTime = word.start; | |
| const icon = document.getElementById('btnPreviewPlay').querySelector('i'); | |
| icon.className = "fa-solid fa-pause"; | |
| v.play(); | |
| previewInterval = setInterval(() => { | |
| if(v.currentTime >= word.end) { | |
| v.pause(); | |
| clearInterval(previewInterval); | |
| icon.className = "fa-solid fa-play"; | |
| if (v.currentTime >= word.end) v.currentTime = word.end - 0.01; | |
| } | |
| }, 20); | |
| } | |
| 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) { | |
| if(type === 'start') { | |
| let v = parseFloat((tempStartTime + am).toFixed(2)); | |
| if(v < 0) v = 0; | |
| if(v >= tempEndTime) v = parseFloat((tempEndTime - 0.1).toFixed(2)); | |
| tempStartTime = v; | |
| document.getElementById('valStart').innerText = fmt(tempStartTime); | |
| } else { | |
| let v = parseFloat((tempEndTime + am).toFixed(2)); | |
| if(v <= tempStartTime) v = parseFloat((tempStartTime + 0.1).toFixed(2)); | |
| tempEndTime = v; | |
| document.getElementById('valEnd').innerText = fmt(tempEndTime); | |
| } | |
| } | |
| 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 confirmDelete() { | |
| 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) { | |
| if (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 = { | |
| 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) { | |
| 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; | |
| if(state.st.name === 'auto_director' && seg.words) { | |
| let html = ""; seg.words.forEach((w, i) => { | |
| let isActive = (currentTime >= w.start && currentTime <= w.end); | |
| let boxColor = (i % 2 === 0) ? '#00D7FF' : '#FF0080'; | |
| if(isActive) html += `<span style="background-color: ${boxColor}; color: #ffffff !important; box-shadow: 0 0 15px ${boxColor}; transform: scale(1.1); display:inline-block; border-radius: 6px; padding: 0 6px; text-shadow:none; font-family: inherit;">${w.word}</span> `; | |
| else html += `<span style="color: #ffffff !important; text-shadow:none; font-family: inherit;">${w.word}</span> `; | |
| }); tEl.innerHTML = html; | |
| } | |
| else if(state.st.name === 'karaoke_static' && seg.words) { | |
| let html = ""; seg.words.forEach(w => { | |
| let isActive = (currentTime >= w.start && currentTime <= w.end); let cls = isActive ? "word-active" : ""; | |
| let styleAttr = ""; if(isActive) { let boxColor = state.st.col; styleAttr = `style="background-color: ${boxColor} !important; color: #fff !important; box-shadow: 0 2px 8px ${boxColor};"`; } | |
| html += `<span class="${cls}" ${styleAttr}>${w.word}</span> `; | |
| }); tEl.innerHTML = html; | |
| } | |
| else if (state.st.name === 'progressive_write' && seg.words) { | |
| let html = ""; seg.words.forEach(w => { if(currentTime >= w.start) html += `<span style="opacity:1">${w.word}</span> `; else html += `<span style="opacity:0">${w.word}</span> `; }); tEl.innerHTML = html.trim(); | |
| } | |
| else { tEl.innerText = seg.text; } | |
| } | |
| } else { tEl.style.opacity = 0; } | |
| } | |
| function upd() { | |
| saveProjectToDB(); | |
| state.st.fz = parseInt(document.getElementById('fz').value); | |
| state.st.y = parseInt(document.getElementById('pos').value); | |
| if (state.st.name !== 'karaoke_static') state.st.col = document.getElementById('col').value; | |
| state.st.bg = document.getElementById('bgCol').value; | |
| 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.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'; tEl.style.borderRadius = '0px'; | |
| if(state.st.name === 'karaoke_static' || state.st.name === 'auto_director') { | |
| tEl.style.backgroundColor = 'transparent'; tEl.style.color = '#FFFFFF'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; | |
| } else if (state.st.name === 'plain_white') { | |
| tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; | |
| } else if (state.st.name === 'white_outline') { | |
| tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; const s = Math.max(3, state.st.fz / 4.5); tEl.style.webkitTextStroke = `${s}px #000000`; tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitPaintOrder = 'stroke fill'; tEl.style.textShadow = 'none'; | |
| } else { | |
| tEl.style.color = state.st.col; | |
| if(state.st.type === 'solid') { tEl.style.backgroundColor = state.st.bg; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; tEl.style.borderRadius = '12px'; } | |
| else if (state.st.type === 'transparent') { | |
| let c = state.st.bg.replace('#', ''); let r = parseInt(c.substring(0, 2), 16); let g = parseInt(c.substring(2, 4), 16); let b = parseInt(c.substring(4, 6), 16); | |
| tEl.style.backgroundColor = `rgba(${r},${g},${b},0.6)`; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; tEl.style.borderRadius = '12px'; | |
| } else if (state.st.type === 'outline') { | |
| tEl.style.backgroundColor = 'transparent'; tEl.style.color = state.st.col; 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'; } | |
| } | |
| } | |
| function openStaticColorPicker(event) { event.stopPropagation(); document.getElementById('staticColorPicker').click(); } | |
| function updateStaticColor(val) { document.documentElement.style.setProperty('--static-color', val); state.st.col = val; document.getElementById('col').value = val; upd(); } | |
| 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); | |
| document.getElementById('staticColorPicker').value = defaultPurple; | |
| document.getElementById('col').value = 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'; | |
| document.getElementById('col').value = '#FFFFFF'; | |
| document.getElementById('bgCol').value = '#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(); } |