diff --git a/assets/css/style.css b/assets/css/style.css index fbc2d92..56571df 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -706,4 +706,63 @@ body.sidebar-hidden .global-toolbar { padding: 1rem 1.5rem; gap: 1rem; } +} + +/* --- ESTILOS PARA O MENU ARQUIVO --- */ + +.file-menu-container { + position: relative; /* Essencial para o posicionamento do dropdown */ +} + +.toolbar-btn { + background-color: var(--background-light); + color: var(--text-light); + border: 1px solid var(--border-color); + border-radius: 3px; + padding: 5px 10px; + cursor: pointer; + font-family: inherit; + font-size: 0.8rem; +} + +.toolbar-btn:hover { + background-color: var(--background-lighter); +} + +.file-menu-dropdown { + position: absolute; + top: 100%; + left: 0; + background-color: var(--background-lighter); + border: 1px solid var(--border-color-dark); + border-radius: 4px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + min-width: 200px; + z-index: 1000; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.file-menu-dropdown.hidden { + display: none; +} + +.file-menu-dropdown a { + color: var(--text-light); + padding: 8px 12px; + text-decoration: none; + display: block; + font-size: 0.9rem; +} + +.file-menu-dropdown a:hover { + background-color: var(--accent-blue); + color: white; +} + +.menu-divider { + height: 1px; + background-color: var(--border-color); + margin: 4px 0; } \ No newline at end of file diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index 612864d..2debcf2 100644 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -19,15 +19,16 @@ import { loadAndRenderSampleBrowser, showOpenProjectModal, closeOpenProjectModal, + handleSampleUpload, // Importa handleSampleUpload, embora não seja mais usado diretamente } from "./ui.js"; import { adjustValue, enforceNumericInput } from "./utils.js"; import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js"; -// --- INICIALIZAÇÃO E EVENTOS FINAIS --- document.addEventListener("DOMContentLoaded", () => { const newProjectBtn = document.getElementById("new-project-btn"); const openMmpBtn = document.getElementById("open-mmp-btn"); const saveMmpBtn = document.getElementById("save-mmp-btn"); + const uploadSampleBtn = document.getElementById("upload-sample-btn"); const addInstrumentBtn = document.getElementById("add-instrument-btn"); const removeInstrumentBtn = document.getElementById("remove-instrument-btn"); const playBtn = document.getElementById("play-btn"); @@ -35,12 +36,12 @@ document.addEventListener("DOMContentLoaded", () => { const rewindBtn = document.getElementById("rewind-btn"); const metronomeBtn = document.getElementById("metronome-btn"); const mmpFileInput = document.getElementById("mmp-file-input"); + const sampleFileInput = document.getElementById("sample-file-input"); const openProjectModal = document.getElementById("open-project-modal"); const openModalCloseBtn = document.getElementById("open-modal-close-btn"); const loadFromComputerBtn = document.getElementById("load-from-computer-btn"); const sidebarToggle = document.getElementById("sidebar-toggle"); const addBarBtn = document.getElementById("add-bar-btn"); - const masterVolumeKnob = document.getElementById("master-volume-knob"); const masterPanKnob = document.getElementById("master-pan-knob"); @@ -52,56 +53,53 @@ document.addEventListener("DOMContentLoaded", () => { return; Object.assign(appState, { tracks: [], + activeTrackId: null, isPlaying: false, playbackIntervalId: null, currentStep: 0, metronomeEnabled: false, originalXmlDoc: null, + currentBeatBasslineName: 'Novo Projeto', masterVolume: DEFAULT_VOLUME, masterPan: DEFAULT_PAN }); - // Reseta os inputs para o padrão document.getElementById('bpm-input').value = 140; document.getElementById('bars-input').value = 1; document.getElementById('compasso-a-input').value = 4; document.getElementById('compasso-b-input').value = 4; + const titleElement = document.getElementById('beat-bassline-title'); + if(titleElement) titleElement.textContent = 'Novo Projeto'; renderApp(); - setupMasterKnobs(); // Re-inicializa os knobs master + setupMasterKnobs(); }); addBarBtn.addEventListener("click", () => { const barsInput = document.getElementById("bars-input"); - if (barsInput) { - adjustValue(barsInput, 1); - } + if (barsInput) adjustValue(barsInput, 1); }); function setupMasterKnobs() { function updateMasterKnobVisual(knobElement, controlType) { 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 = appState.masterVolume; percentage = value / 1.5; title = `Volume Master: ${Math.round(value * 100)}%`; - } else { // pan + } else { const value = appState.masterPan; percentage = (value + 1) / 2; const panDisplay = Math.round(value * 100); title = `Pan Master: ${ 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; } - function addMasterKnobInteraction(knobElement, controlType) { knobElement.addEventListener("wheel", (e) => { e.preventDefault(); @@ -109,56 +107,45 @@ document.addEventListener("DOMContentLoaded", () => { const direction = e.deltaY < 0 ? 1 : -1; if (controlType === "volume") { const newValue = appState.masterVolume + direction * step; - const clampedValue = Math.max(0, Math.min(1.5, newValue)); - appState.masterVolume = clampedValue; - updateMasterVolume(clampedValue); + appState.masterVolume = Math.max(0, Math.min(1.5, newValue)); + updateMasterVolume(appState.masterVolume); } else { const newValue = appState.masterPan + direction * step; - const clampedValue = Math.max(-1, Math.min(1, newValue)); - appState.masterPan = clampedValue; - updateMasterPan(clampedValue); + appState.masterPan = Math.max(-1, Math.min(1, newValue)); + updateMasterPan(appState.masterPan); } updateMasterKnobVisual(knobElement, controlType); }); - knobElement.addEventListener("mousedown", (e) => { if (e.button !== 0) return; e.preventDefault(); - const startY = e.clientY; const startValue = controlType === "volume" ? appState.masterVolume : appState.masterPan; 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") { - const clampedValue = Math.max(0, Math.min(1.5, newValue)); - appState.masterVolume = clampedValue; - updateMasterVolume(clampedValue); + appState.masterVolume = Math.max(0, Math.min(1.5, newValue)); + updateMasterVolume(appState.masterVolume); } else { - const clampedValue = Math.max(-1, Math.min(1, newValue)); - appState.masterPan = clampedValue; - updateMasterPan(clampedValue); + appState.masterPan = Math.max(-1, Math.min(1, newValue)); + updateMasterPan(appState.masterPan); } updateMasterKnobVisual(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); }); } - addMasterKnobInteraction(masterVolumeKnob, "volume"); updateMasterKnobVisual(masterVolumeKnob, "volume"); - addMasterKnobInteraction(masterPanKnob, "pan"); updateMasterKnobVisual(masterPanKnob, "pan"); } @@ -172,6 +159,44 @@ document.addEventListener("DOMContentLoaded", () => { closeOpenProjectModal(); } }); + + uploadSampleBtn.addEventListener("click", () => sampleFileInput.click()); + + // --- INÍCIO DA CORREÇÃO --- + // Lógica de upload de sample para o servidor Flask + sampleFileInput.addEventListener("change", async (event) => { + const file = event.target.files[0]; + if (!file) return; + + const formData = new FormData(); + formData.append("sampleFile", file); + + try { + // ATENÇÃO: Verifique se a URL e a porta estão corretas para o seu servidor Flask + const response = await fetch('http://localhost:5000/upload-sample', { + method: 'POST', + body: formData, + }); + + const result = await response.json(); + + if (response.ok) { + alert("Sample enviado com sucesso!"); + // Recarrega a lista de samples para exibir o novo arquivo + await loadAndRenderSampleBrowser(); + } else { + throw new Error(result.error || "Erro desconhecido no servidor."); + } + + } catch (error) { + console.error("Erro ao enviar o sample:", error); + alert(`Falha no upload: ${error.message}`); + } + + event.target.value = null; // Limpa o input para permitir o mesmo arquivo de novo + }); + // --- FIM DA CORREÇÃO --- + saveMmpBtn.addEventListener("click", generateMmpFile); addInstrumentBtn.addEventListener("click", addTrackToState); removeInstrumentBtn.addEventListener("click", removeLastTrackFromState); @@ -223,7 +248,6 @@ document.addEventListener("DOMContentLoaded", () => { }); }); - // Inicia a aplicação loadAndRenderSampleBrowser(); renderApp(); setupMasterKnobs(); diff --git a/assets/js/creations/ui.js b/assets/js/creations/ui.js index dc2fdd3..a553b17 100644 --- a/assets/js/creations/ui.js +++ b/assets/js/creations/ui.js @@ -6,33 +6,35 @@ import { updateTrackVolume, updateTrackPan, } from "./state.js"; -import { playSample } from "./audio.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'); -globalPatternSelector.addEventListener('change', () => { - // Atualiza o índice GLOBAL - 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; +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(); -}); + 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'); @@ -40,7 +42,6 @@ export function updateGlobalPatternSelector() { option.textContent = pattern.name; globalPatternSelector.appendChild(option); }); - // Usa o índice GLOBAL globalPatternSelector.selectedIndex = appState.activePatternIndex; globalPatternSelector.disabled = false; } else { @@ -51,6 +52,42 @@ export function updateGlobalPatternSelector() { } } +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; } @@ -89,15 +126,11 @@ export function renderApp() {