mmpSearch/assets/js/creations/ui.js

462 lines
16 KiB
JavaScript

// 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 = `<i class="fa-solid fa-file-audio"></i> ${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 = `
<div class="track-info">
<i class="fa-solid fa-gear"></i>
<div class="track-mute"></div>
<span class="track-name">${trackData.name}</span>
</div>
<div class="track-controls">
<div class="knob-container">
<div class="knob" data-control="volume" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
<span>VOL</span>
</div>
<div class="knob-container">
<div class="knob" data-control="pan" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
<span>PAN</span>
</div>
</div>
<div class="step-sequencer-wrapper"></div>
`;
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 = `<p style="color:var(--accent-red); padding: 10px;">${error.message}</p>`;
}
}
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 = `<i class="fa-solid fa-file-audio"></i> ${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 = `<i class="fa-solid fa-folder"></i> ${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 = "<p>Carregando...</p>";
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 =
'<p style="color:var(--text-dark);">Nenhum projeto encontrado no servidor.</p>';
}
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 = `<p style="color:var(--accent-red);">${error.message}</p>`;
}
}
export function closeOpenProjectModal() {
const openProjectModal = document.getElementById("open-project-modal");
openProjectModal.classList.remove("visible");
}