// js/audio/audio_ui.js import { appState } from "../state.js"; import { addAudioClipToTimeline, updateAudioClipProperties, sliceAudioClip, removeAudioClip, } from "./audio_state.js"; import { seekAudioEditor, restartAudioEditorIfPlaying, updateTransportLoop, } from "./audio_audio.js"; import { drawWaveform } from "../waveform.js"; import { PIXELS_PER_BAR, PIXELS_PER_STEP, ZOOM_LEVELS } from "../config.js"; import { getPixelsPerSecond, quantizeTime, getBeatsPerBar, getSecondsPerStep, } from "../utils.js"; import { sendAction } from "../socket.js"; export function renderAudioEditor() { const audioEditor = document.querySelector(".audio-editor"); const existingTrackContainer = document.getElementById( "audio-track-container" ); if (!audioEditor || !existingTrackContainer) return; // --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA --- let rulerWrapper = audioEditor.querySelector(".ruler-wrapper"); if (!rulerWrapper) { rulerWrapper = document.createElement("div"); rulerWrapper.className = "ruler-wrapper"; rulerWrapper.innerHTML = `
`; audioEditor.insertBefore(rulerWrapper, existingTrackContainer); } const ruler = rulerWrapper.querySelector(".timeline-ruler"); ruler.innerHTML = ""; const pixelsPerSecond = getPixelsPerSecond(); let maxTime = appState.global.loopEndTime; appState.audio.clips.forEach((clip) => { const endTime = (clip.startTimeInSeconds || 0) + (clip.durationInSeconds || 0); if (endTime > maxTime) maxTime = endTime; }); const containerWidth = existingTrackContainer.offsetWidth; const contentWidth = maxTime * pixelsPerSecond; const totalWidth = Math.max(contentWidth, containerWidth, 2000); ruler.style.width = `${totalWidth}px`; const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex]; const beatsPerBar = getBeatsPerBar(); const stepWidthPx = PIXELS_PER_STEP * zoomFactor; const beatWidthPx = stepWidthPx * 4; const barWidthPx = beatWidthPx * beatsPerBar; if (barWidthPx > 0) { const numberOfBars = Math.ceil(totalWidth / barWidthPx); for (let i = 1; i <= numberOfBars; i++) { const marker = document.createElement("div"); marker.className = "ruler-marker"; marker.textContent = i; marker.style.left = `${(i - 1) * barWidthPx}px`; ruler.appendChild(marker); } } const loopRegion = document.createElement("div"); loopRegion.id = "loop-region"; loopRegion.style.left = `${ appState.global.loopStartTime * pixelsPerSecond }px`; loopRegion.style.width = `${ (appState.global.loopEndTime - appState.global.loopStartTime) * pixelsPerSecond }px`; loopRegion.innerHTML = `
`; loopRegion.classList.toggle("visible", appState.global.isLoopActive); ruler.appendChild(loopRegion); // --- LISTENER DA RÉGUA (MODIFICADO para enviar Ações de Loop/Seek) --- const newRuler = ruler.cloneNode(true); ruler.parentNode.replaceChild(newRuler, ruler); newRuler.addEventListener("mousedown", (e) => { // Esconde menus document.getElementById("timeline-context-menu").style.display = "none"; document.getElementById("ruler-context-menu").style.display = "none"; const currentPixelsPerSecond = getPixelsPerSecond(); const loopHandle = e.target.closest(".loop-handle"); const loopRegionBody = e.target.closest("#loop-region:not(.loop-handle)"); // Drag Handle Loop if (loopHandle) { e.preventDefault(); e.stopPropagation(); const handleType = loopHandle.classList.contains("left") ? "left" : "right"; const initialMouseX = e.clientX; const initialStart = appState.global.loopStartTime; const initialEnd = appState.global.loopEndTime; const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - initialMouseX; const deltaTime = deltaX / currentPixelsPerSecond; let newStart = appState.global.loopStartTime; let newEnd = appState.global.loopEndTime; if (handleType === "left") { newStart = Math.max(0, initialStart + deltaTime); newStart = Math.min(newStart, appState.global.loopEndTime - 0.1); appState.global.loopStartTime = newStart; } else { newEnd = Math.max( appState.global.loopStartTime + 0.1, initialEnd + deltaTime ); appState.global.loopEndTime = newEnd; } updateTransportLoop(); const loopRegionEl = newRuler.querySelector("#loop-region"); if (loopRegionEl) { loopRegionEl.style.left = `${newStart * currentPixelsPerSecond}px`; loopRegionEl.style.width = `${ (newEnd - newStart) * currentPixelsPerSecond }px`; } }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); // ================================================================= // 👇 INÍCIO DA CORREÇÃO (Sincronia de Loop Drag Handle) // ================================================================= sendAction({ type: "SET_LOOP_STATE", isLoopActive: appState.global.isLoopActive, loopStartTime: appState.global.loopStartTime, loopEndTime: appState.global.loopEndTime, }); // renderAudioEditor(); // Removido // ================================================================= // 👆 FIM DA CORREÇÃO }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); return; } // Drag Body Loop if (loopRegionBody) { e.preventDefault(); e.stopPropagation(); const initialMouseX = e.clientX; const initialStart = appState.global.loopStartTime; const initialEnd = appState.global.loopEndTime; const initialDuration = initialEnd - initialStart; const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - initialMouseX; const deltaTime = deltaX / currentPixelsPerSecond; let newStart = Math.max(0, initialStart + deltaTime); let newEnd = newStart + initialDuration; appState.global.loopStartTime = newStart; appState.global.loopEndTime = newEnd; updateTransportLoop(); const loopRegionEl = newRuler.querySelector("#loop-region"); if (loopRegionEl) loopRegionEl.style.left = `${newStart * currentPixelsPerSecond}px`; }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); // ================================================================= // 👇 INÍCIO DA CORREÇÃO (Sincronia de Loop Drag Body) // ================================================================= sendAction({ type: "SET_LOOP_STATE", isLoopActive: appState.global.isLoopActive, loopStartTime: appState.global.loopStartTime, loopEndTime: appState.global.loopEndTime, }); // renderAudioEditor(); // Removido // ================================================================= // 👆 FIM DA CORREÇÃO }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); return; } // Seek na Régua e.preventDefault(); const handleSeek = (event) => { const rect = newRuler.getBoundingClientRect(); const scrollLeft = newRuler.scrollLeft; const clickX = event.clientX - rect.left; const absoluteX = clickX + scrollLeft; const newTime = absoluteX / currentPixelsPerSecond; // ================================================================= // 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Régua) // ================================================================= sendAction({ type: "SET_SEEK_TIME", seekTime: newTime }); // seekAudioEditor(newTime); // 👈 Substituído // ================================================================= // 👆 FIM DA CORREÇÃO }; handleSeek(e); // Aplica no mousedown const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); const onMouseUpSeek = () => { document.removeEventListener("mousemove", onMouseMoveSeek); document.removeEventListener("mouseup", onMouseUpSeek); }; document.addEventListener("mousemove", onMouseMoveSeek); document.addEventListener("mouseup", onMouseUpSeek); }); // Menu Contexto Régua (sem alterações) newRuler.addEventListener("contextmenu", (e) => { e.preventDefault(); document.getElementById("timeline-context-menu").style.display = "none"; const menu = document.getElementById("ruler-context-menu"); const currentPixelsPerSecond = getPixelsPerSecond(); const rect = newRuler.getBoundingClientRect(); const scrollLeft = newRuler.scrollLeft; const clickX = e.clientX - rect.left; const absoluteX = clickX + scrollLeft; const clickTime = absoluteX / currentPixelsPerSecond; appState.global.lastRulerClickTime = clickTime; menu.style.display = "block"; menu.style.left = `${e.clientX}px`; menu.style.top = `${e.clientY}px`; }); // Recriação Container Pistas (sem alterações) const newTrackContainer = existingTrackContainer.cloneNode(false); audioEditor.replaceChild(newTrackContainer, existingTrackContainer); // Render Pistas (sem alterações) appState.audio.tracks.forEach((trackData) => { const audioTrackLane = document.createElement("div"); audioTrackLane.className = "audio-track-lane"; audioTrackLane.dataset.trackId = trackData.id; audioTrackLane.innerHTML = `
${trackData.name}
VOL
PAN
`; newTrackContainer.appendChild(audioTrackLane); const timelineContainer = audioTrackLane.querySelector( ".timeline-container" ); timelineContainer.addEventListener("dragover", (e) => { e.preventDefault(); audioTrackLane.classList.add("drag-over"); }); timelineContainer.addEventListener("dragleave", () => audioTrackLane.classList.remove("drag-over") ); timelineContainer.addEventListener("drop", (e) => { e.preventDefault(); audioTrackLane.classList.remove("drag-over"); const filePath = e.dataTransfer.getData("text/plain"); if (!filePath) return; const rect = timelineContainer.getBoundingClientRect(); const dropX = e.clientX - rect.left + timelineContainer.scrollLeft; let startTimeInSeconds = dropX / pixelsPerSecond; startTimeInSeconds = quantizeTime(startTimeInSeconds); if ( !trackData.id || startTimeInSeconds == null || isNaN(startTimeInSeconds) ) { console.error("Drop inválido. Ignorando.", { id: trackData.id, time: startTimeInSeconds, }); return; } const clipId = crypto?.randomUUID?.() || `clip_${Date.now()}_${Math.floor(Math.random() * 1e6)}`; addAudioClipToTimeline( filePath, trackData.id, startTimeInSeconds, clipId ); try { sendAction({ type: "ADD_AUDIO_CLIP", filePath, trackId: trackData.id, startTimeInSeconds, clipId, name: String(filePath).split(/[\\/]/).pop(), }); } catch (err) { console.warn("[SYNC] Falha ao emitir ADD_AUDIO_CLIP", err); } }); const grid = timelineContainer.querySelector(".spectrogram-view-grid"); grid.style.setProperty("--step-width", `${stepWidthPx}px`); grid.style.setProperty("--beat-width", `${beatWidthPx}px`); grid.style.setProperty("--bar-width", `${barWidthPx}px`); }); // Render Clips (sem alterações) appState.audio.clips.forEach((clip) => { const parentGrid = newTrackContainer.querySelector( `.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid` ); if (!parentGrid) return; const clipElement = document.createElement("div"); clipElement.className = "timeline-clip"; clipElement.dataset.clipId = clip.id; if (clip.id === appState.global.selectedClipId) clipElement.classList.add("selected"); if (appState.global.clipboard?.cutSourceId === clip.id) clipElement.classList.add("cut"); clipElement.style.left = `${ (clip.startTimeInSeconds || 0) * pixelsPerSecond }px`; clipElement.style.width = `${ (clip.durationInSeconds || 0) * pixelsPerSecond }px`; let pitchStr = clip.pitch > 0 ? `+${clip.pitch.toFixed(1)}` : `${clip.pitch.toFixed(1)}`; if (clip.pitch === 0) pitchStr = ""; clipElement.innerHTML = `
${clip.name} ${pitchStr}
`; parentGrid.appendChild(clipElement); if (clip.buffer) { const canvas = clipElement.querySelector(".waveform-canvas-clip"); const canvasWidth = (clip.durationInSeconds || 0) * pixelsPerSecond; if (canvasWidth > 0) { canvas.width = canvasWidth; canvas.height = 40; const audioBuffer = clip.buffer; const isStretched = clip.pitch !== 0; const sourceOffset = isStretched ? 0 : clip.offset || 0; const sourceDuration = isStretched ? clip.originalDuration : clip.durationInSeconds; drawWaveform( canvas, audioBuffer, "var(--accent-green)", sourceOffset, sourceDuration ); } } clipElement.addEventListener("wheel", (e) => { e.preventDefault(); const clipToUpdate = appState.audio.clips.find( (c) => c.id == clipElement.dataset.clipId ); if (!clipToUpdate) return; const direction = e.deltaY < 0 ? 1 : -1; let newPitch = clipToUpdate.pitch + direction; newPitch = Math.max(-24, Math.min(24, newPitch)); updateAudioClipProperties(clipToUpdate.id, { pitch: newPitch }); try { sendAction({ type: "UPDATE_AUDIO_CLIP", clipId: clipToUpdate.id, props: { pitch: newPitch }, }); } catch (err) { console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (wheel)", err); } renderAudioEditor(); restartAudioEditorIfPlaying(); }); }); // Sync Scroll (sem alterações) newTrackContainer.addEventListener("scroll", () => { const scrollPos = newTrackContainer.scrollLeft; const mainRuler = document.querySelector(".timeline-ruler"); if (mainRuler && mainRuler.scrollLeft !== scrollPos) { mainRuler.scrollLeft = scrollPos; } }); // Event Listener Principal (mousedown no container de pistas) newTrackContainer.addEventListener("mousedown", (e) => { // Esconde menus document.getElementById("timeline-context-menu").style.display = "none"; document.getElementById("ruler-context-menu").style.display = "none"; const clipElement = e.target.closest(".timeline-clip"); // Desseleciona se clicar fora if (!clipElement && e.button !== 2) { if (appState.global.selectedClipId) { appState.global.selectedClipId = null; newTrackContainer .querySelectorAll(".timeline-clip.selected") .forEach((c) => c.classList.remove("selected")); } } const currentPixelsPerSecond = getPixelsPerSecond(); const handle = e.target.closest(".clip-resize-handle"); // Slice Tool if (appState.global.sliceToolActive && clipElement) { e.preventDefault(); e.stopPropagation(); const clipId = clipElement.dataset.clipId; const timelineContainer = clipElement.closest(".timeline-container"); const rect = timelineContainer.getBoundingClientRect(); const clickX = e.clientX - rect.left; const absoluteX = clickX + timelineContainer.scrollLeft; let sliceTimeInTimeline = absoluteX / currentPixelsPerSecond; sliceTimeInTimeline = quantizeTime(sliceTimeInTimeline); sliceAudioClip(clipId, sliceTimeInTimeline); try { sendAction({ type: "UPDATE_AUDIO_CLIP", clipId, props: { __operation: "slice", sliceTimeInTimeline }, }); } catch (err) { console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (slice)", err); } renderAudioEditor(); return; } // Resize Handle if (handle) { e.preventDefault(); e.stopPropagation(); const clipId = clipElement.dataset.clipId; const clip = appState.audio.clips.find((c) => c.id == clipId); if (!clip || !clip.buffer) return; const handleType = handle.classList.contains("left") ? "left" : "right"; const initialMouseX = e.clientX; const secondsPerStep = getSecondsPerStep(); const initialLeftPx = clipElement.offsetLeft; const initialWidthPx = clipElement.offsetWidth; const initialStartTime = clip.startTimeInSeconds; const initialDuration = clip.durationInSeconds; const initialOffset = clip.offset || 0; const initialOriginalDuration = clip.originalDuration || clip.buffer.duration; const bufferStartTime = initialStartTime - initialOffset; const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - initialMouseX; // Trim Mode if (appState.global.resizeMode === "trim") { if (handleType === "right") { let newWidthPx = initialWidthPx + deltaX; let newDuration = newWidthPx / currentPixelsPerSecond; let newEndTime = quantizeTime(initialStartTime + newDuration); newEndTime = Math.max( initialStartTime + secondsPerStep, newEndTime ); const maxEndTime = bufferStartTime + initialOriginalDuration; newEndTime = Math.min(newEndTime, maxEndTime); clipElement.style.width = `${ (newEndTime - initialStartTime) * currentPixelsPerSecond }px`; } else if (handleType === "left") { let newLeftPx = initialLeftPx + deltaX; let newStartTime = newLeftPx / currentPixelsPerSecond; newStartTime = quantizeTime(newStartTime); const minStartTime = initialStartTime + initialDuration - secondsPerStep; newStartTime = Math.min(newStartTime, minStartTime); newStartTime = Math.max(bufferStartTime, newStartTime); const newLeftFinalPx = newStartTime * currentPixelsPerSecond; const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond; clipElement.style.left = `${newLeftFinalPx}px`; clipElement.style.width = `${newWidthFinalPx}px`; } } // Stretch Mode else if (appState.global.resizeMode === "stretch") { if (handleType === "right") { let newWidthPx = initialWidthPx + deltaX; let newDuration = newWidthPx / currentPixelsPerSecond; let newEndTime = quantizeTime(initialStartTime + newDuration); newEndTime = Math.max( initialStartTime + secondsPerStep, newEndTime ); clipElement.style.width = `${ (newEndTime - initialStartTime) * currentPixelsPerSecond }px`; } else if (handleType === "left") { let newLeftPx = initialLeftPx + deltaX; let newStartTime = newLeftPx / currentPixelsPerSecond; newStartTime = quantizeTime(newStartTime); const minStartTime = initialStartTime + initialDuration - secondsPerStep; newStartTime = Math.min(newStartTime, minStartTime); const newLeftFinalPx = newStartTime * currentPixelsPerSecond; const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond; clipElement.style.left = `${newLeftFinalPx}px`; clipElement.style.width = `${newWidthFinalPx}px`; } } }; const onMouseUp = (upEvent) => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); const finalLeftPx = clipElement.offsetLeft; const finalWidthPx = clipElement.offsetWidth; const newStartTime = finalLeftPx / currentPixelsPerSecond; const newDuration = finalWidthPx / currentPixelsPerSecond; // Trim Mode if (appState.global.resizeMode === "trim") { const newOffset = newStartTime - bufferStartTime; if (handleType === "right") { updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: 0, }); try { sendAction({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: 0 }, }); } catch (err) { console.warn( "[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (trim-right)", err ); } } else if (handleType === "left") { updateAudioClipProperties(clipId, { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, offset: newOffset, pitch: 0, }); try { sendAction({ type: "UPDATE_AUDIO_CLIP", clipId, props: { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, offset: newOffset, pitch: 0, }, }); } catch (err) { console.warn( "[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (trim-left)", err ); } } } // Stretch Mode else if (appState.global.resizeMode === "stretch") { const newPlaybackRate = initialOriginalDuration / newDuration; const newPitch = 12 * Math.log2(newPlaybackRate); if (handleType === "right") { updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: newPitch, offset: 0, }); try { sendAction({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: newPitch, offset: 0, }, }); } catch (err) { console.warn( "[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (stretch-right)", err ); } } else if (handleType === "left") { updateAudioClipProperties(clipId, { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, pitch: newPitch, offset: 0, }); try { sendAction({ type: "UPDATE_AUDIO_CLIP", clipId, props: { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, pitch: newPitch, offset: 0, }, }); } catch (err) { console.warn( "[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (stretch-left)", err ); } } } restartAudioEditorIfPlaying(); renderAudioEditor(); }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); return; } // Drag Clip if (clipElement) { const clipId = clipElement.dataset.clipId; if (appState.global.selectedClipId !== clipId) { appState.global.selectedClipId = clipId; newTrackContainer .querySelectorAll(".timeline-clip.selected") .forEach((c) => c.classList.remove("selected")); clipElement.classList.add("selected"); } e.preventDefault(); const clickOffsetInClip = e.clientX - clipElement.getBoundingClientRect().left; clipElement.classList.add("dragging"); let lastOverLane = clipElement.closest(".audio-track-lane"); const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - e.clientX; clipElement.style.transform = `translateX(${deltaX}px)`; const overElement = document.elementFromPoint( moveEvent.clientX, moveEvent.clientY ); const overLane = overElement ? overElement.closest(".audio-track-lane") : null; if (overLane && overLane !== lastOverLane) { if (lastOverLane) lastOverLane.classList.remove("drag-over"); overLane.classList.add("drag-over"); lastOverLane = overLane; } }; const onMouseUp = (upEvent) => { clipElement.classList.remove("dragging"); if (lastOverLane) lastOverLane.classList.remove("drag-over"); clipElement.style.transform = ""; document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); const finalLane = lastOverLane; if (!finalLane) return; const newTrackId = finalLane.dataset.trackId; // (é uma string) const timelineContainer = finalLane.querySelector( ".timeline-container" ); const wrapperRect = timelineContainer.getBoundingClientRect(); const newLeftPx = upEvent.clientX - wrapperRect.left - clickOffsetInClip + timelineContainer.scrollLeft; const constrainedLeftPx = Math.max(0, newLeftPx); let newStartTime = constrainedLeftPx / currentPixelsPerSecond; newStartTime = quantizeTime(newStartTime); // (Correção Bug 4 - remove Number()) updateAudioClipProperties(clipId, { trackId: newTrackId, startTimeInSeconds: newStartTime, }); try { sendAction({ type: "UPDATE_AUDIO_CLIP", clipId, props: { trackId: newTrackId, startTimeInSeconds: newStartTime }, }); } catch (err) { console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (move)", err); } renderAudioEditor(); }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); return; } // Seek na Pista const timelineContainer = e.target.closest(".timeline-container"); if (timelineContainer) { e.preventDefault(); const handleSeek = (event) => { const rect = timelineContainer.getBoundingClientRect(); const scrollLeft = timelineContainer.scrollLeft; const clickX = event.clientX - rect.left; const absoluteX = clickX + scrollLeft; const newTime = absoluteX / currentPixelsPerSecond; // ================================================================= // 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Pista) // ================================================================= sendAction({ type: "SET_SEEK_TIME", seekTime: newTime }); // seekAudioEditor(newTime); // 👈 Substituído // ================================================================= // 👆 FIM DA CORREÇÃO }; handleSeek(e); // Aplica no mousedown const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); const onMouseUpSeek = () => { document.removeEventListener("mousemove", onMouseMoveSeek); document.removeEventListener("mouseup", onMouseUpSeek); }; document.addEventListener("mousemove", onMouseMoveSeek); document.addEventListener("mouseup", onMouseUpSeek); } }); // Menu Contexto Pista (sem alterações) newTrackContainer.addEventListener("contextmenu", (e) => { e.preventDefault(); document.getElementById("ruler-context-menu").style.display = "none"; const menu = document.getElementById("timeline-context-menu"); if (!menu) return; const clipElement = e.target.closest(".timeline-clip"); const copyItem = document.getElementById("copy-clip"); const cutItem = document.getElementById("cut-clip"); const pasteItem = document.getElementById("paste-clip"); const deleteItem = document.getElementById("delete-clip"); const canPaste = appState.global.clipboard?.type === "audio"; pasteItem.style.display = canPaste ? "block" : "none"; if (clipElement) { const clipId = clipElement.dataset.clipId; if (appState.global.selectedClipId !== clipId) { appState.global.selectedClipId = clipId; newTrackContainer .querySelectorAll(".timeline-clip.selected") .forEach((c) => c.classList.remove("selected")); clipElement.classList.add("selected"); } copyItem.style.display = "block"; cutItem.style.display = "block"; deleteItem.style.display = "block"; menu.style.display = "block"; menu.style.left = `${e.clientX}px`; menu.style.top = `${e.clientY}px`; if (!deleteItem.__synced) { deleteItem.__synced = true; deleteItem.addEventListener("click", () => { const id = appState.global.selectedClipId; if (!id) return; const ok = removeAudioClip(id); try { sendAction({ type: "REMOVE_AUDIO_CLIP", clipId: id }); } catch (err) { console.warn("[SYNC] Falha ao emitir REMOVE_AUDIO_CLIP", err); } if (ok) renderAudioEditor(); menu.style.display = "none"; }); } } else { copyItem.style.display = "none"; cutItem.style.display = "none"; deleteItem.style.display = "none"; if (canPaste) { menu.style.display = "block"; menu.style.left = `${e.clientX}px`; menu.style.top = `${e.clientY}px`; } else { menu.style.display = "none"; } } }); } // Funções de UI (sem alterações) export function updateAudioEditorUI() { const playBtn = document.getElementById("audio-editor-play-btn"); if (!playBtn) return; if (appState.global.isAudioEditorPlaying) { playBtn.classList.remove("fa-play"); playBtn.classList.add("fa-pause"); } else { playBtn.classList.remove("fa-pause"); playBtn.classList.add("fa-play"); } } export function updatePlayheadVisual(pixels) { document.querySelectorAll(".audio-track-lane .playhead").forEach((ph) => { ph.style.left = `${pixels}px`; }); } export function resetPlayheadVisual() { document.querySelectorAll(".audio-track-lane .playhead").forEach((ph) => { ph.style.left = "0px"; }); }