// js/ui.js import { appState, toggleStepState, updateTrackSample, updateTrackVolume, updateTrackPan, } from "./state.js"; import { playSample, stopPlayback } from "./audio.js"; import { getTotalSteps } from "./utils.js"; import { loadProjectFromServer } from "./file.js"; let samplePathMap = {}; const globalPatternSelector = document.getElementById('global-pattern-selector'); if (globalPatternSelector) { globalPatternSelector.addEventListener('change', () => { // A linha stopPlayback() foi REMOVIDA daqui, permitindo a troca em tempo real. appState.activePatternIndex = parseInt(globalPatternSelector.value, 10); const firstTrack = appState.tracks[0]; if (firstTrack) { const activePattern = firstTrack.patterns[appState.activePatternIndex]; if (activePattern) { const stepsPerBar = 16; const requiredBars = Math.ceil(activePattern.steps.length / stepsPerBar); document.getElementById("bars-input").value = requiredBars > 0 ? requiredBars : 1; } } redrawSequencer(); }); } export function updateGlobalPatternSelector() { if (!globalPatternSelector) return; const referenceTrack = appState.tracks[0]; globalPatternSelector.innerHTML = ''; if (referenceTrack && referenceTrack.patterns.length > 0) { referenceTrack.patterns.forEach((pattern, index) => { const option = document.createElement('option'); option.value = index; option.textContent = pattern.name; globalPatternSelector.appendChild(option); }); globalPatternSelector.selectedIndex = appState.activePatternIndex; globalPatternSelector.disabled = false; } else { const option = document.createElement('option'); option.textContent = 'Sem patterns'; globalPatternSelector.appendChild(option); globalPatternSelector.disabled = true; } } export function handleSampleUpload(file) { const validExtensions = ['.wav', '.flac', '.ogg', '.mp3']; const fileExtension = '.' + file.name.split('.').pop().toLowerCase(); if (!validExtensions.includes(fileExtension)) { alert("Formato de arquivo inválido. Por favor, envie .wav, .flac, .ogg, ou .mp3."); return; } const reader = new FileReader(); reader.onload = (e) => { const dataURL = e.target.result; const browserContent = document.getElementById("browser-content"); const list = browserContent.querySelector("ul"); if (list) { const li = document.createElement("li"); li.innerHTML = ` ${file.name}`; li.setAttribute("draggable", true); li.addEventListener("click", (event) => { event.stopPropagation(); playSample(dataURL, null); }); li.addEventListener("dragstart", (event) => { event.dataTransfer.setData("text/plain", dataURL); event.dataTransfer.effectAllowed = "copy"; }); list.prepend(li); } }; reader.readAsDataURL(file); } export function getSamplePathMap() { return samplePathMap; } function buildSamplePathMap(tree, currentPath) { for (const key in tree) { if (key === "_isFile") continue; const node = tree[key]; const newPath = `${currentPath}/${key}`; if (node._isFile) { samplePathMap[key] = newPath; } else { buildSamplePathMap(node, newPath); } } } export function renderApp() { const trackContainer = document.getElementById("track-container"); trackContainer.innerHTML = ""; appState.tracks.forEach((trackData) => { const trackLane = document.createElement("div"); trackLane.className = "track-lane"; trackLane.dataset.trackId = trackData.id; if (trackData.id === appState.activeTrackId) { trackLane.classList.add('active-track'); } trackLane.innerHTML = `
${trackData.name}
VOL
PAN
`; trackLane.addEventListener('click', () => { if (appState.activeTrackId === trackData.id) return; // A linha stopPlayback() também foi REMOVIDA daqui appState.activeTrackId = trackData.id; document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track')); trackLane.classList.add('active-track'); updateGlobalPatternSelector(); // Apenas redesenha a UI, sem parar a música const activeTrack = appState.tracks.find(t => t.id === appState.activeTrackId); if (activeTrack) { const activePattern = activeTrack.patterns[activeTrack.activePatternIndex]; if (activePattern) { const stepsPerBar = 16; const requiredBars = Math.ceil(activePattern.steps.length / stepsPerBar); document.getElementById("bars-input").value = requiredBars > 0 ? requiredBars : 1; redrawSequencer(); } } }); trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); }); trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over")); trackLane.addEventListener("drop", (e) => { e.preventDefault(); trackLane.classList.remove("drag-over"); const filePath = e.dataTransfer.getData("text/plain"); if (filePath) { updateTrackSample(trackData.id, filePath); } }); trackContainer.appendChild(trackLane); const volumeKnob = trackLane.querySelector(".knob[data-control='volume']"); addKnobInteraction(volumeKnob); updateKnobVisual(volumeKnob, "volume"); const panKnob = trackLane.querySelector(".knob[data-control='pan']"); addKnobInteraction(panKnob); updateKnobVisual(panKnob, "pan"); }); updateGlobalPatternSelector(); redrawSequencer(); } export function redrawSequencer() { const totalGridSteps = getTotalSteps(); document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => { let sequencerContainer = wrapper.querySelector(".step-sequencer"); if (!sequencerContainer) { sequencerContainer = document.createElement("div"); sequencerContainer.className = "step-sequencer"; wrapper.appendChild(sequencerContainer); } const parentTrackElement = wrapper.closest(".track-lane"); const trackId = parentTrackElement.dataset.trackId; const trackData = appState.tracks.find((t) => t.id == trackId); if (!trackData || !trackData.patterns || trackData.patterns.length === 0) { sequencerContainer.innerHTML = ""; return; } const activePattern = trackData.patterns[appState.activePatternIndex]; if (!activePattern) { sequencerContainer.innerHTML = ""; return; } const patternSteps = activePattern.steps; sequencerContainer.innerHTML = ""; for (let i = 0; i < totalGridSteps; i++) { const stepWrapper = document.createElement("div"); stepWrapper.className = "step-wrapper"; const stepElement = document.createElement("div"); stepElement.className = "step"; if (patternSteps[i] === true) { stepElement.classList.add("active"); } stepElement.addEventListener("click", () => { toggleStepState(trackData.id, i); stepElement.classList.toggle("active"); if (trackData && trackData.samplePath) { playSample(trackData.samplePath, trackData.id); } }); const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4; const groupIndex = Math.floor(i / beatsPerBar); if (groupIndex % 2 === 0) { stepElement.classList.add("step-dark"); } const stepsPerBar = 16; if (i > 0 && i % stepsPerBar === 0) { const marker = document.createElement("div"); marker.className = "step-marker"; marker.textContent = Math.floor(i / stepsPerBar) + 1; stepWrapper.appendChild(marker); } stepWrapper.appendChild(stepElement); sequencerContainer.appendChild(stepWrapper); } }); } function addKnobInteraction(knobElement) { const controlType = knobElement.dataset.control; knobElement.addEventListener("mousedown", (e) => { if (e.button === 1) { e.preventDefault(); const trackId = knobElement.dataset.trackId; const defaultValue = controlType === "volume" ? 0.8 : 0.0; if (controlType === "volume") { updateTrackVolume(trackId, defaultValue); } else { updateTrackPan(trackId, defaultValue); } updateKnobVisual(knobElement, controlType); } }); knobElement.addEventListener("mousedown", (e) => { if (e.button !== 0) return; e.preventDefault(); const trackId = knobElement.dataset.trackId; const track = appState.tracks.find((t) => t.id == trackId); if (!track) return; const startY = e.clientY; const startValue = controlType === "volume" ? track.volume : track.pan; document.body.classList.add("knob-dragging"); function onMouseMove(moveEvent) { const deltaY = startY - moveEvent.clientY; const sensitivity = controlType === "volume" ? 150 : 200; const newValue = startValue + deltaY / sensitivity; if (controlType === "volume") { updateTrackVolume(trackId, newValue); } else { updateTrackPan(trackId, newValue); } updateKnobVisual(knobElement, controlType); } function onMouseUp() { document.body.classList.remove("knob-dragging"); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); } document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }); knobElement.addEventListener("wheel", (e) => { e.preventDefault(); const trackId = knobElement.dataset.trackId; const track = appState.tracks.find((t) => t.id == trackId); if (!track) return; const step = 0.05; const direction = e.deltaY < 0 ? 1 : -1; if (controlType === "volume") { const newValue = track.volume + direction * step; updateTrackVolume(trackId, newValue); } else { const newValue = track.pan + direction * step; updateTrackPan(trackId, newValue); } updateKnobVisual(knobElement, controlType); }); } function updateKnobVisual(knobElement, controlType) { const trackId = knobElement.dataset.trackId; const track = appState.tracks.find((t) => t.id == trackId); if (!track) return; const indicator = knobElement.querySelector(".knob-indicator"); if (!indicator) return; const minAngle = -135; const maxAngle = 135; let percentage = 0.5; let title = ""; if (controlType === "volume") { const value = track.volume; const clampedValue = Math.max(0, Math.min(1.5, value)); percentage = clampedValue / 1.5; title = `Volume: ${Math.round(clampedValue * 100)}%`; } else { const value = track.pan; const clampedValue = Math.max(-1, Math.min(1, value)); percentage = (clampedValue + 1) / 2; const panDisplay = Math.round(clampedValue * 100); title = `Pan: ${ panDisplay === 0 ? "Centro" : panDisplay < 0 ? `${-panDisplay} L` : `${panDisplay} R` }`; } const angle = minAngle + percentage * (maxAngle - minAngle); indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`; knobElement.title = title; } export function highlightStep(stepIndex, isActive) { if (stepIndex < 0) return; document.querySelectorAll(".track-lane").forEach((track) => { const stepWrapper = track.querySelector( `.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})` ); if (stepWrapper) { const stepElement = stepWrapper.querySelector(".step"); if (stepElement) { stepElement.classList.toggle("playing", isActive); } } }); } export async function loadAndRenderSampleBrowser() { const browserContent = document.getElementById("browser-content"); try { // --- CORREÇÃO AQUI --- // Adiciona um timestamp à URL para evitar que o navegador use uma versão antiga (em cache) do arquivo. const response = await fetch(`metadata/samples-manifest.json?v=${Date.now()}`); // --- FIM DA CORREÇÃO --- if (!response.ok) { throw new Error("Arquivo samples-manifest.json não encontrado."); } const fileTree = await response.json(); samplePathMap = {}; buildSamplePathMap(fileTree, "src/samples"); renderFileTree(fileTree, browserContent, "src/samples"); } catch (error) { console.error("Erro ao carregar samples:", error); browserContent.innerHTML = `

${error.message}

`; } } function renderFileTree(tree, parentElement, currentPath) { parentElement.innerHTML = ""; const ul = document.createElement("ul"); const sortedKeys = Object.keys(tree).sort((a, b) => { const aIsFile = tree[a]._isFile; const bIsFile = tree[b]._isFile; if (aIsFile === bIsFile) return a.localeCompare(b); return aIsFile ? 1 : -1; }); for (const key of sortedKeys) { if (key === '_isFile') continue; const node = tree[key]; const li = document.createElement("li"); const newPath = `${currentPath}/${key}`; if (node._isFile) { li.innerHTML = ` ${key}`; li.setAttribute("draggable", true); li.addEventListener("click", (e) => { e.stopPropagation(); playSample(newPath, null); }); li.addEventListener("dragstart", (e) => { e.dataTransfer.setData("text/plain", newPath); e.dataTransfer.effectAllowed = "copy"; }); ul.appendChild(li); } else { li.className = "directory"; li.innerHTML = ` ${key}`; const nestedUl = document.createElement("ul"); renderFileTree(node, nestedUl, newPath); li.appendChild(nestedUl); li.addEventListener("click", (e) => { e.stopPropagation(); li.classList.toggle("open"); }); ul.appendChild(li); } } parentElement.appendChild(ul); } export async function showOpenProjectModal() { const openProjectModal = document.getElementById("open-project-modal"); const serverProjectsList = document.getElementById("server-projects-list"); serverProjectsList.innerHTML = "

Carregando...

"; openProjectModal.classList.add("visible"); try { const response = await fetch("metadata/mmp-manifest.json"); if (!response.ok) throw new Error("Arquivo mmp-manifest.json não encontrado."); const projects = await response.json(); serverProjectsList.innerHTML = ""; if (projects.length === 0) { serverProjectsList.innerHTML = '

Nenhum projeto encontrado no servidor.

'; } projects.forEach((projectName) => { const projectItem = document.createElement("div"); projectItem.className = "project-item"; projectItem.textContent = projectName; projectItem.addEventListener("click", async () => { const success = await loadProjectFromServer(projectName); if (success) { closeOpenProjectModal(); } }); serverProjectsList.appendChild(projectItem); }); } catch (error) { console.error("Erro ao carregar lista de projetos:", error); serverProjectsList.innerHTML = `

${error.message}

`; } } export function closeOpenProjectModal() { const openProjectModal = document.getElementById("open-project-modal"); openProjectModal.classList.remove("visible"); }