320 lines
11 KiB
JavaScript
320 lines
11 KiB
JavaScript
// js/ui.js
|
|
import {
|
|
appState,
|
|
toggleStepState,
|
|
updateTrackSample,
|
|
updateTrackVolume,
|
|
updateTrackPan,
|
|
} from "./state.js";
|
|
import { playSample } from "./audio.js";
|
|
import { getTotalSteps } from "./utils.js";
|
|
import { loadProjectFromServer } from "./file.js"; // (CORREÇÃO) Importa a função que faltava
|
|
|
|
// RENDERIZAÇÃO PRINCIPAL
|
|
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;
|
|
|
|
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"></div>
|
|
`;
|
|
|
|
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");
|
|
});
|
|
redrawSequencer();
|
|
}
|
|
|
|
export function redrawSequencer() {
|
|
const beatsPerBar =
|
|
parseInt(document.getElementById("compasso-a-input").value, 10) || 4;
|
|
const totalSteps = getTotalSteps();
|
|
document.querySelectorAll(".step-sequencer").forEach((container) => {
|
|
const parentTrackElement = container.closest(".track-lane");
|
|
const trackId = parentTrackElement.dataset.trackId;
|
|
const trackData = appState.tracks.find((t) => t.id == trackId);
|
|
if (trackData && trackData.steps.length !== totalSteps) {
|
|
const newStepsState = new Array(totalSteps).fill(false);
|
|
for (let i = 0; i < Math.min(trackData.steps.length, totalSteps); i++) {
|
|
newStepsState[i] = trackData.steps[i];
|
|
}
|
|
trackData.steps = newStepsState;
|
|
}
|
|
container.innerHTML = "";
|
|
for (let i = 0; i < totalSteps; i++) {
|
|
const stepWrapper = document.createElement("div");
|
|
stepWrapper.className = "step-wrapper";
|
|
const stepElement = document.createElement("div");
|
|
stepElement.className = "step";
|
|
if (trackData && trackData.steps[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 groupIndex = Math.floor(i / beatsPerBar);
|
|
if (groupIndex % 2 === 0) {
|
|
stepElement.classList.add("step-dark");
|
|
}
|
|
if (i > 0 && i % beatsPerBar === 0) {
|
|
const marker = document.createElement("div");
|
|
marker.className = "step-marker";
|
|
marker.textContent = i;
|
|
stepWrapper.appendChild(marker);
|
|
}
|
|
stepWrapper.appendChild(stepElement);
|
|
container.appendChild(stepWrapper);
|
|
}
|
|
});
|
|
}
|
|
|
|
// LÓGICA DE INTERAÇÃO DOS KNOBS
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
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);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// LÓGICA DA SIDEBAR E MODAL
|
|
export async function loadAndRenderSampleBrowser() {
|
|
const browserContent = document.getElementById("browser-content");
|
|
try {
|
|
const response = await fetch("metadata/samples-manifest.json");
|
|
if (!response.ok) {
|
|
throw new Error("Arquivo samples-manifest.json não encontrado.");
|
|
}
|
|
const fileTree = await response.json();
|
|
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) {
|
|
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");
|
|
}
|