809 lines
29 KiB
JavaScript
Executable File
809 lines
29 KiB
JavaScript
Executable File
// js/main.js
|
|
import { appState, loadStateFromSession } from "./state.js";
|
|
import {
|
|
updateTransportLoop,
|
|
restartAudioEditorIfPlaying,
|
|
} from "./audio/audio_audio.js";
|
|
import { initializeAudioContext } from "./audio.js";
|
|
import { handleFileLoad, generateMmpFile, generateXmlFromStateExported, buildRenderPackageBase64 } from "./file.js";
|
|
import {
|
|
renderAll,
|
|
loadAndRenderSampleBrowser,
|
|
showOpenProjectModal,
|
|
closeOpenProjectModal,
|
|
} from "./ui.js";
|
|
import { renderAudioEditor } from "./audio/audio_ui.js";
|
|
import { adjustValue, enforceNumericInput, DEFAULT_PROJECT_XML, secondsToSongTicks, snapSongTicks, LMMS_TICKS_PER_BAR } from "./utils.js";
|
|
import { ZOOM_LEVELS } from "./config.js";
|
|
import { loadProjectFromServer } from "./file.js";
|
|
import { sendAction, joinRoom, setUserName } from "./socket.js";
|
|
import { renderActivePatternToBlob, renderProjectAndDownload } from "./pattern/pattern_audio.js";
|
|
import { showToast } from "./ui.js";
|
|
import { toggleRecording } from "./recording.js"
|
|
import * as Tone from "https://esm.sh/tone"; // Adicione o Tone aqui se não estiver global
|
|
|
|
const ROOM_NAME = new URLSearchParams(window.location.search).get("room");
|
|
window.ROOM_NAME = ROOM_NAME;
|
|
|
|
const PROJECT_NAME = new URLSearchParams(window.location.search).get("project");
|
|
|
|
// --- LÓGICA DE INICIALIZAÇÃO ---
|
|
|
|
// Função autoinvocada assíncrona para gerenciar o carregamento inicial
|
|
(async function initApp() {
|
|
|
|
let restored = false;
|
|
|
|
// 1. Se houver sala, tenta restaurar o backup do F5 primeiro
|
|
if (ROOM_NAME) {
|
|
restored = await loadStateFromSession();
|
|
if (restored) {
|
|
showToast("🔄 Sessão restaurada (F5)", "success");
|
|
// Se restaurou, renderiza tudo e não carrega mais nada
|
|
renderAll();
|
|
updateToolButtons();
|
|
|
|
// Sincroniza o botão de modo (Global/Local) com o estado restaurado
|
|
const syncModeBtn = document.getElementById("sync-mode-btn");
|
|
if (syncModeBtn && appState.global.syncMode) {
|
|
syncModeBtn.textContent = appState.global.syncMode === "global" ? "Global" : "Local";
|
|
syncModeBtn.classList.toggle("active", appState.global.syncMode === "global");
|
|
}
|
|
}
|
|
// Entra na sala socket (mesmo se restaurou, precisa reconectar o socket)
|
|
joinRoom();
|
|
}
|
|
|
|
// 2. Se NÃO restaurou e tem um projeto na URL, carrega do servidor
|
|
if (!restored && PROJECT_NAME) {
|
|
console.log(`[MAIN] Carregando projeto do servidor: ${PROJECT_NAME}`);
|
|
const filename = PROJECT_NAME.endsWith(".mmp") || PROJECT_NAME.endsWith(".mmpz")
|
|
? PROJECT_NAME
|
|
: `${PROJECT_NAME}.mmp`;
|
|
loadProjectFromServer(filename);
|
|
} else if (!restored && !PROJECT_NAME) {
|
|
// CORREÇÃO: Se entrou numa sala vazia sem projeto na URL,
|
|
// inicializa o XML base para não dar erro depois.
|
|
const parser = new DOMParser();
|
|
appState.global.originalXmlDoc = parser.parseFromString(DEFAULT_PROJECT_XML, "application/xml");
|
|
}
|
|
|
|
})(); // Fim do initApp
|
|
|
|
function updateToolButtons() {
|
|
const sliceToolBtn = document.getElementById("slice-tool-btn");
|
|
const trimToolBtn = document.getElementById("resize-tool-trim");
|
|
const stretchToolBtn = document.getElementById("resize-tool-stretch");
|
|
|
|
if (sliceToolBtn)
|
|
sliceToolBtn.classList.toggle("active", appState.global.sliceToolActive);
|
|
if (trimToolBtn)
|
|
trimToolBtn.classList.toggle(
|
|
"active",
|
|
!appState.global.sliceToolActive && appState.global.resizeMode === "trim"
|
|
);
|
|
if (stretchToolBtn)
|
|
stretchToolBtn.classList.toggle(
|
|
"active",
|
|
!appState.global.sliceToolActive &&
|
|
appState.global.resizeMode === "stretch"
|
|
);
|
|
|
|
document.body.classList.toggle(
|
|
"slice-tool-active",
|
|
appState.global.sliceToolActive
|
|
);
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// Botões e elementos
|
|
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");
|
|
const stopBtn = document.getElementById("stop-btn");
|
|
const audioEditorPlayBtn = document.getElementById("audio-editor-play-btn");
|
|
const audioEditorStopBtn = document.getElementById("audio-editor-stop-btn");
|
|
const audioEditorLoopBtn = document.getElementById("audio-editor-loop-btn");
|
|
const addAudioTrackBtn = document.getElementById("add-audio-track-btn");
|
|
const rewindBtn = document.getElementById("rewind-btn");
|
|
const metronomeBtn = document.getElementById("metronome-btn");
|
|
const sliceToolBtn = document.getElementById("slice-tool-btn");
|
|
const resizeToolTrimBtn = document.getElementById("resize-tool-trim");
|
|
const resizeToolStretchBtn = document.getElementById("resize-tool-stretch");
|
|
const createRoomBtn = document.getElementById("create-room-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 zoomInBtn = document.getElementById("zoom-in-btn");
|
|
const zoomOutBtn = document.getElementById("zoom-out-btn");
|
|
const deleteClipBtn = document.getElementById("delete-clip");
|
|
const addPatternBtn = document.getElementById("add-pattern-btn");
|
|
const removePatternBtn = document.getElementById("remove-pattern-btn");
|
|
const downloadPackageBtn = document.getElementById("download-package-btn");
|
|
|
|
|
|
// Render
|
|
const renderAudioBtn = document.getElementById("render-audio-btn");
|
|
|
|
renderAudioBtn?.addEventListener("click", async () => {
|
|
const fmt = (prompt("Formato: wav / mp3 / ogg / flac", "wav") || "").trim().toLowerCase();
|
|
if (!fmt) return;
|
|
|
|
const allowed = new Set(["wav", "mp3", "ogg", "flac"]);
|
|
const chosenFormat = allowed.has(fmt) ? fmt : "wav";
|
|
|
|
const icon = renderAudioBtn.querySelector("i");
|
|
const oldIconClass = icon?.className;
|
|
if (icon) icon.className = "fa-solid fa-spinner fa-spin";
|
|
renderAudioBtn.style.pointerEvents = "none";
|
|
|
|
try {
|
|
showToast("🎛️ Renderizando no LMMS (servidor)...", "info", 4000);
|
|
|
|
const roomName = new URLSearchParams(window.location.search).get("room");
|
|
const baseUrl = `https://${window.location.hostname}:33001`;
|
|
|
|
const body = {
|
|
roomName: roomName || null,
|
|
format: chosenFormat, // ✅ sem shorthand
|
|
name: appState.global?.currentBeatBasslineName || appState.global?.projectName || "projeto",
|
|
};
|
|
const pkg = await buildRenderPackageBase64(body.name);
|
|
body.mmpzBase64 = pkg.mmpzBase64;
|
|
body.mmpzName = pkg.mmpzName;
|
|
|
|
// ✅ Modo Local: manda XML direto (senão dá missing_xml_or_room)
|
|
if (!roomName) {
|
|
body.xml = generateXmlFromStateExported();
|
|
}
|
|
|
|
const resp = await fetch(`${baseUrl}/render`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!resp.ok) throw new Error(await resp.text());
|
|
|
|
const blob = await resp.blob();
|
|
|
|
let filename = `projeto.${chosenFormat}`;
|
|
const cd = resp.headers.get("Content-Disposition");
|
|
const m = cd && /filename="?([^"]+)"?/i.exec(cd);
|
|
if (m?.[1]) filename = m[1];
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
setTimeout(() => URL.revokeObjectURL(url), 1500);
|
|
|
|
showToast("✅ Render concluído!", "success", 3000);
|
|
} catch (e) {
|
|
console.error(e);
|
|
showToast("❌ Falha ao renderizar no LMMS.", "error", 6000);
|
|
alert("Falha ao renderizar no LMMS. Veja o console para detalhes.");
|
|
} finally {
|
|
if (icon && oldIconClass) icon.className = oldIconClass;
|
|
renderAudioBtn.style.pointerEvents = "auto";
|
|
}
|
|
});
|
|
|
|
// Download projeto
|
|
downloadPackageBtn?.addEventListener("click", generateMmpFile);
|
|
|
|
// Configuração do botão de Gravação
|
|
const recordBtn = document.getElementById('record-btn');
|
|
|
|
if (recordBtn) {
|
|
recordBtn.addEventListener('click', async () => {
|
|
if (Tone.context.state !== 'running') {
|
|
await Tone.start();
|
|
console.log("Audio Context iniciado via clique.");
|
|
}
|
|
toggleRecording();
|
|
});
|
|
} else {
|
|
console.error("Botão de gravação (#record-btn) não encontrado no DOM.");
|
|
}
|
|
|
|
//envia pattern pro editor de áudio
|
|
|
|
const bouncePatternBtn = document.getElementById(
|
|
"send-pattern-to-playlist-btn"
|
|
);
|
|
|
|
bouncePatternBtn?.addEventListener("click", async () => {
|
|
// 1. Verifica se existe uma pista de áudio para onde enviar
|
|
const targetTrackId = appState.audio.tracks[0]?.id;
|
|
if (!targetTrackId) {
|
|
showToast(
|
|
"Crie uma Pista de Áudio (no editor de amostras) primeiro!",
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
|
|
showToast("Renderizando pattern...", "info");
|
|
|
|
try {
|
|
// 2. Chama a função de renderização que criamos
|
|
const audioBlob = await renderActivePatternToBlob();
|
|
|
|
// 3. Cria uma URL local para o áudio renderizado
|
|
const audioUrl = URL.createObjectURL(audioBlob);
|
|
|
|
// --- INÍCIO DA NOVA MODIFICAÇÃO (Visualização de Steps) ---
|
|
|
|
// 4. Pega o índice do pattern que foi renderizado
|
|
const activePatternIndex =
|
|
appState.pattern.tracks[0]?.activePatternIndex || 0;
|
|
|
|
// 5. Coleta os dados de steps de CADA trilha para esse pattern
|
|
const patternData = appState.pattern.tracks.map((track) => {
|
|
const pattern = track.patterns[activePatternIndex];
|
|
// Retorna o array de steps, ou um array vazio se não houver
|
|
return pattern && pattern.steps ? pattern.steps : [];
|
|
});
|
|
// --- FIM DA NOVA MODIFICAÇÃO ---
|
|
|
|
// 6. Prepara o nome e ID
|
|
const patternName =
|
|
appState.pattern.tracks[0]?.patterns[activePatternIndex]?.name ||
|
|
"Pattern";
|
|
const clipName = `${patternName} (Bounced).wav`;
|
|
const clipId = `bounced_${Date.now()}`;
|
|
|
|
// 7. Envia a ação (a lógica ADD_AUDIO_CLIP já sabe como carregar o áudio)
|
|
sendAction({
|
|
type: "ADD_AUDIO_CLIP",
|
|
filePath: audioUrl, // O player de áudio sabe ler essa URL 'blob:'
|
|
trackId: targetTrackId, // Envia para a primeira pista de áudio
|
|
startTimeInSeconds: 0, // Coloca no início da timeline
|
|
clipId: clipId,
|
|
name: clipName,
|
|
patternData: patternData, // <-- AQUI ESTÁ A "PARTITURA"
|
|
});
|
|
|
|
showToast("Pattern enviada para a Pista de Áudio!", "success");
|
|
} catch (error) {
|
|
console.error("Erro ao renderizar pattern:", error);
|
|
showToast("Erro ao renderizar pattern", "error");
|
|
}
|
|
});
|
|
|
|
function getNextPatternIndex() {
|
|
const nonBass = (appState.pattern.tracks || []).filter(t => t.type !== "bassline");
|
|
|
|
const maxFromNonBass = nonBass.reduce(
|
|
(m, t) => Math.max(m, (t.patterns?.length || 0) - 1),
|
|
-1
|
|
);
|
|
|
|
const maxFromBass = (appState.pattern.tracks || [])
|
|
.filter(t => t.type === "bassline" && Number.isFinite(Number(t.patternIndex)))
|
|
.reduce((m, t) => Math.max(m, Number(t.patternIndex)), -1);
|
|
|
|
return Math.max(maxFromNonBass, maxFromBass) + 1;
|
|
}
|
|
|
|
addPatternBtn?.addEventListener("click", () => {
|
|
const idx = getNextPatternIndex();
|
|
const defaultName = `Pattern ${idx + 1}`;
|
|
const name = (prompt("Nome do novo pattern:", defaultName) || defaultName).trim();
|
|
|
|
sendAction({ type: "ADD_PATTERN", patternIndex: idx, name, select: true });
|
|
});
|
|
|
|
//Seleção de pattern
|
|
const globalPatternSelector = document.getElementById(
|
|
"global-pattern-selector"
|
|
);
|
|
|
|
// Adiciona o novo "ouvinte" de evento para o seletor de pattern
|
|
globalPatternSelector?.addEventListener("change", () => {
|
|
const raw = globalPatternSelector.value;
|
|
|
|
// ✅ limpar seleção
|
|
if (raw === "") {
|
|
sendAction({ type: "SET_ACTIVE_PATTERN", patternIndex: null });
|
|
return;
|
|
}
|
|
|
|
const newPatternIndex = parseInt(raw, 10);
|
|
if (Number.isNaN(newPatternIndex)) {
|
|
console.warn("Não é possível trocar pattern: índice inválido.");
|
|
return;
|
|
}
|
|
|
|
sendAction({
|
|
type: "SET_ACTIVE_PATTERN",
|
|
patternIndex: newPatternIndex,
|
|
});
|
|
});
|
|
|
|
|
|
// =================================================================
|
|
// 👇 INÍCIO DA CORREÇÃO (Botão de Sincronia - Agora envia Ação)
|
|
// =================================================================
|
|
const syncModeBtn = document.getElementById("sync-mode-btn");
|
|
if (syncModeBtn) {
|
|
// Só define default se ainda não existir
|
|
if (!appState.global.syncMode) {
|
|
appState.global.syncMode = "global";
|
|
}
|
|
|
|
syncModeBtn.classList.toggle("active", appState.global.syncMode === "global");
|
|
syncModeBtn.textContent =
|
|
appState.global.syncMode === "global" ? "Global" : "Local";
|
|
|
|
syncModeBtn.addEventListener("click", () => {
|
|
const newMode =
|
|
appState.global.syncMode === "global" ? "local" : "global";
|
|
|
|
sendAction({
|
|
type: "SET_SYNC_MODE",
|
|
mode: newMode,
|
|
});
|
|
});
|
|
}
|
|
|
|
// Excluir clipe
|
|
if (deleteClipBtn) {
|
|
deleteClipBtn.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
const clipId = appState.global.selectedClipId;
|
|
if (clipId) {
|
|
sendAction({ type: "REMOVE_AUDIO_CLIP", clipId });
|
|
appState.global.selectedClipId = null;
|
|
}
|
|
const menu = document.getElementById("timeline-context-menu");
|
|
if (menu) menu.style.display = "none";
|
|
});
|
|
}
|
|
|
|
// Delete/Backspace
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
|
|
const clipId = appState.global.selectedClipId;
|
|
if ((e.key === "Delete" || e.key === "Backspace") && clipId) {
|
|
e.preventDefault();
|
|
sendAction({ type: "REMOVE_AUDIO_CLIP", clipId });
|
|
appState.global.selectedClipId = null;
|
|
}
|
|
});
|
|
|
|
// Fechar menu contexto
|
|
document.addEventListener("click", (e) => {
|
|
const menu = document.getElementById("timeline-context-menu");
|
|
if (menu && !e.target.closest("#timeline-context-menu")) {
|
|
menu.style.display = "none";
|
|
}
|
|
});
|
|
|
|
// 🔥 CORREÇÃO DO PULO DE TELA (SCROLL JUMP)
|
|
// Adicionamos um listener global para capturar cliques em clips ou links vazios
|
|
document.body.addEventListener("click", (e) => {
|
|
// 1. Verifica links com href="#"
|
|
const targetLink = e.target.closest('a[href="#"]');
|
|
|
|
// 2. Verifica se clicou num clip ou container de clip
|
|
// (Mesmo sendo divs, vamos prevenir o comportamento padrão para garantir que não role)
|
|
const isAudioClip = e.target.closest('.timeline-clip') ||
|
|
e.target.closest('.audio-clip-container') ||
|
|
e.target.closest('.track-info'); // Às vezes clicar no header da track causa isso
|
|
|
|
if (targetLink || isAudioClip) {
|
|
// Impede o navegador de rolar a tela ou mudar o foco bruscamente
|
|
// Nota: O 'logic' do clique (selecionar, arrastar) ainda vai funcionar
|
|
// porque os listeners do audio_ui.js usam 'mousedown', que acontece ANTES do 'click'.
|
|
e.preventDefault();
|
|
}
|
|
}, { passive: false }); // passive: false é necessário para usar preventDefault
|
|
|
|
// Ações principais (broadcast)
|
|
newProjectBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
if (
|
|
(appState.pattern.tracks.length > 0 || appState.audio.clips.length > 0) &&
|
|
!confirm(
|
|
"Você tem certeza? Isso irá resetar o projeto para TODOS na sala."
|
|
)
|
|
)
|
|
return;
|
|
sendAction({ type: "LOAD_PROJECT", xml: DEFAULT_PROJECT_XML });
|
|
});
|
|
|
|
addBarBtn?.addEventListener("click", () => {
|
|
const barsInput = document.getElementById("bars-input");
|
|
if (barsInput) {
|
|
adjustValue(barsInput, 1);
|
|
barsInput.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}
|
|
});
|
|
|
|
openMmpBtn?.addEventListener("click", showOpenProjectModal);
|
|
loadFromComputerBtn?.addEventListener("click", () => mmpFileInput?.click());
|
|
mmpFileInput?.addEventListener("change", (event) => {
|
|
const file = event.target.files[0];
|
|
if (file) handleFileLoad(file).then(() => closeOpenProjectModal());
|
|
});
|
|
uploadSampleBtn?.addEventListener("click", () => sampleFileInput?.click());
|
|
saveMmpBtn?.addEventListener("click", generateMmpFile);
|
|
|
|
addInstrumentBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
sendAction({ type: "ADD_TRACK" });
|
|
});
|
|
|
|
removeInstrumentBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
|
|
// Pega o ID da faixa que está selecionada/ativa na interface
|
|
const activeId = appState.pattern.activeTrackId;
|
|
|
|
if (activeId) {
|
|
// Envia comando seguro por ID
|
|
sendAction({
|
|
type: "REMOVE_TRACK_BY_ID",
|
|
trackId: activeId
|
|
});
|
|
} else {
|
|
// Fallback: se não tiver ID ativo, tenta pegar a última (comportamento antigo, mas seguro localmente)
|
|
const tracks = appState.pattern.tracks;
|
|
if (tracks.length > 0) {
|
|
const lastId = tracks[tracks.length - 1].id;
|
|
sendAction({ type: "REMOVE_TRACK_BY_ID", trackId: lastId });
|
|
}
|
|
}
|
|
});
|
|
|
|
playBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
sendAction({ type: "TOGGLE_PLAYBACK" });
|
|
});
|
|
stopBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
sendAction({ type: "STOP_PLAYBACK" });
|
|
});
|
|
rewindBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
sendAction({ type: "REWIND_PLAYBACK" });
|
|
});
|
|
|
|
metronomeBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
appState.global.metronomeEnabled = !appState.global.metronomeEnabled;
|
|
metronomeBtn.classList.toggle("active", appState.global.metronomeEnabled);
|
|
});
|
|
|
|
// Ferramentas locais
|
|
if (sliceToolBtn) {
|
|
sliceToolBtn.addEventListener("click", () => {
|
|
appState.global.sliceToolActive = !appState.global.sliceToolActive;
|
|
updateToolButtons();
|
|
});
|
|
}
|
|
if (resizeToolTrimBtn) {
|
|
resizeToolTrimBtn.addEventListener("click", () => {
|
|
appState.global.resizeMode = "trim";
|
|
appState.global.sliceToolActive = false;
|
|
updateToolButtons();
|
|
});
|
|
}
|
|
if (resizeToolStretchBtn) {
|
|
resizeToolStretchBtn.addEventListener("click", () => {
|
|
appState.global.resizeMode = "stretch";
|
|
appState.global.sliceToolActive = false;
|
|
updateToolButtons();
|
|
});
|
|
}
|
|
|
|
openModalCloseBtn?.addEventListener("click", closeOpenProjectModal);
|
|
sidebarToggle?.addEventListener("click", () => {
|
|
document.body.classList.toggle("sidebar-hidden");
|
|
const icon = sidebarToggle.querySelector("i");
|
|
if (icon) {
|
|
icon.className = document.body.classList.contains("sidebar-hidden")
|
|
? "fa-solid fa-caret-right"
|
|
: "fa-solid fa-caret-left";
|
|
}
|
|
});
|
|
|
|
const inputs = document.querySelectorAll(".value-input");
|
|
inputs.forEach((input) => {
|
|
input.addEventListener("input", (event) => {
|
|
enforceNumericInput(event);
|
|
const id = event.target.id;
|
|
const affectsTimeline = id === "bars-input" || id.startsWith("compasso-");
|
|
|
|
// ✅ só para se for o usuário digitando/colando (evento real)
|
|
if (appState.global.isPlaying && affectsTimeline && event.isTrusted) {
|
|
sendAction({ type: "STOP_PLAYBACK" });
|
|
}
|
|
});
|
|
|
|
input.addEventListener("change", (event) => {
|
|
const target = event.target;
|
|
if (target.id === "bpm-input") {
|
|
sendAction({ type: "SET_BPM", value: target.value });
|
|
} else if (target.id === "bars-input") {
|
|
sendAction({ type: "SET_BARS", value: target.value });
|
|
} else if (target.id === "compasso-a-input") {
|
|
sendAction({ type: "SET_TIMESIG_A", value: target.value });
|
|
} else if (target.id === "compasso-b-input") {
|
|
sendAction({ type: "SET_TIMESIG_B", value: target.value });
|
|
}
|
|
});
|
|
|
|
input.addEventListener("wheel", (event) => {
|
|
event.preventDefault();
|
|
|
|
const id = event.target.id;
|
|
const affectsTimeline = id === "bars-input" || id.startsWith("compasso-");
|
|
|
|
// ✅ mantém o comportamento atual: mexeu em bars/compasso enquanto toca → para
|
|
if (appState.global.isPlaying && affectsTimeline) {
|
|
sendAction({ type: "STOP_PLAYBACK" });
|
|
}
|
|
|
|
const step = event.deltaY < 0 ? 1 : -1;
|
|
adjustValue(event.target, step);
|
|
event.target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
});
|
|
});
|
|
|
|
const buttons = document.querySelectorAll(".adjust-btn");
|
|
buttons.forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const targetId = button.dataset.target + "-input";
|
|
const targetInput = document.getElementById(targetId);
|
|
const step = parseInt(button.dataset.step, 10) || 1;
|
|
if (targetInput) {
|
|
const id = targetInput.id;
|
|
const affectsTimeline = id === "bars-input" || id.startsWith("compasso-");
|
|
if (appState.global.isPlaying && affectsTimeline) {
|
|
sendAction({ type: "STOP_PLAYBACK" });
|
|
}
|
|
adjustValue(targetInput, step);
|
|
targetInput.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}
|
|
});
|
|
});
|
|
|
|
// Zoom local
|
|
zoomInBtn?.addEventListener("click", () => {
|
|
if (appState.global.zoomLevelIndex < ZOOM_LEVELS.length - 1) {
|
|
appState.global.zoomLevelIndex++;
|
|
renderAll();
|
|
}
|
|
});
|
|
zoomOutBtn?.addEventListener("click", () => {
|
|
if (appState.global.zoomLevelIndex > 0) {
|
|
appState.global.zoomLevelIndex--;
|
|
renderAll();
|
|
}
|
|
});
|
|
|
|
// Editor de Áudio
|
|
audioEditorPlayBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
if (appState.global.isAudioEditorPlaying) {
|
|
sendAction({ type: "STOP_AUDIO_PLAYBACK", rewind: false });
|
|
} else {
|
|
sendAction({
|
|
type: "START_AUDIO_PLAYBACK",
|
|
seekTime: appState.audio.audioEditorSeekTime, // Corrigido
|
|
loopState: {
|
|
isLoopActive: appState.global.isLoopActive,
|
|
loopStartTime: appState.global.loopStartTime,
|
|
loopEndTime: appState.global.loopEndTime,
|
|
},
|
|
});
|
|
}
|
|
});
|
|
audioEditorStopBtn?.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
sendAction({ type: "STOP_AUDIO_PLAYBACK", rewind: true });
|
|
});
|
|
|
|
// Loop Button (agora envia ação)
|
|
audioEditorLoopBtn?.addEventListener("click", () => {
|
|
initializeAudioContext(); // Garante contexto
|
|
const newLoopState = !appState.global.isLoopActive;
|
|
sendAction({
|
|
type: "SET_LOOP_STATE",
|
|
isLoopActive: newLoopState,
|
|
loopStartTime: appState.global.loopStartTime,
|
|
loopEndTime: appState.global.loopEndTime,
|
|
});
|
|
});
|
|
|
|
if (addAudioTrackBtn) {
|
|
addAudioTrackBtn.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
sendAction({ type: "ADD_AUDIO_LANE" });
|
|
});
|
|
}
|
|
|
|
const removeAudioTrackBtn = document.getElementById("remove-audio-track-btn");
|
|
if (removeAudioTrackBtn) {
|
|
removeAudioTrackBtn.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
|
|
// Verifica quantas faixas existem
|
|
const trackCount = appState.audio.tracks.length;
|
|
|
|
if (trackCount > 0) {
|
|
// Pega a ÚLTIMA faixa da lista
|
|
const lastTrack = appState.audio.tracks[trackCount - 1];
|
|
|
|
// Verifica se tem clips nela (segurança)
|
|
const hasClips = appState.audio.clips.some(c => c.trackId === lastTrack.id);
|
|
if (hasClips && !confirm("A última pista contém clips de áudio. Deseja removê-la mesmo assim?")) {
|
|
return;
|
|
}
|
|
|
|
// 🔥 A MUDANÇA CRUCIAL: Envia o ID, e usa o NOME NOVO do comando
|
|
sendAction({
|
|
type: "REMOVE_AUDIO_LANE_BY_ID",
|
|
trackId: lastTrack.id
|
|
});
|
|
} else {
|
|
showToast("Não há pistas de áudio para remover.", "warning");
|
|
}
|
|
});
|
|
}
|
|
|
|
// Navegador de Samples (local)
|
|
loadAndRenderSampleBrowser();
|
|
|
|
const browserContent = document.getElementById("browser-content");
|
|
if (browserContent) {
|
|
browserContent.addEventListener("click", function (event) {
|
|
const folderName = event.target.closest(".folder-name");
|
|
if (folderName) {
|
|
const folderItem = folderName.parentElement;
|
|
folderItem.classList.toggle("open");
|
|
}
|
|
});
|
|
}
|
|
|
|
// Criar sala (gera link com ?room=...)
|
|
if (createRoomBtn) {
|
|
createRoomBtn.addEventListener("click", () => {
|
|
initializeAudioContext();
|
|
const currentParams = new URLSearchParams(window.location.search);
|
|
if (currentParams.has("room")) {
|
|
alert(
|
|
`Você já está na sala: ${currentParams.get(
|
|
"room"
|
|
)}\n\Copie o link da barra de endereços para convidar.`
|
|
);
|
|
return;
|
|
}
|
|
const defaultName = `sessao-${Math.random()
|
|
.toString(36)
|
|
.substring(2, 7)}`;
|
|
const roomName = prompt(
|
|
"Digite um nome para a sala compartilhada:",
|
|
defaultName
|
|
);
|
|
if (!roomName) return;
|
|
const currentUrl = window.location.origin + window.location.pathname;
|
|
const shareableLink = `${currentUrl}?room=${encodeURIComponent(
|
|
roomName
|
|
)}`;
|
|
try {
|
|
navigator.clipboard.writeText(shareableLink);
|
|
alert(
|
|
`Link da sala copiado para a área de transferência!\n\n${shareableLink}\n\nA página será recarregada agora para entrar na nova sala.`
|
|
);
|
|
} catch (err) {
|
|
alert(
|
|
`Link da sala: ${shareableLink}\n\nA página será recarregada agora para entrar na nova sala.`
|
|
);
|
|
}
|
|
window.location.href = shareableLink;
|
|
});
|
|
}
|
|
|
|
// Modal “destravar áudio” + entrar na sala
|
|
const audioUnlockModal = document.getElementById("audio-unlock-modal");
|
|
const audioUnlockBtn = document.getElementById("audio-unlock-btn");
|
|
|
|
if (ROOM_NAME && audioUnlockModal && audioUnlockBtn) {
|
|
audioUnlockModal.style.display = "flex";
|
|
audioUnlockBtn.addEventListener("click", () => {
|
|
const userName = prompt(
|
|
"Qual o seu nome?",
|
|
`Alicer-${Math.floor(Math.random() * 999)}`
|
|
);
|
|
if (!userName) return;
|
|
setUserName(userName);
|
|
initializeAudioContext();
|
|
// joinRoom() já foi chamado no início se ROOM_NAME existe
|
|
audioUnlockModal.style.display = "none";
|
|
});
|
|
} else {
|
|
console.log("Modo local. Áudio será iniciado no primeiro clique.");
|
|
// Comentado para permitir teste visual
|
|
// if (syncModeBtn) syncModeBtn.style.display = "none";
|
|
}
|
|
|
|
// --- FUNÇÕES GLOBAIS DE FOCO NO PATTERN ---
|
|
|
|
window.openPatternEditor = function (basslineTrack) {
|
|
console.log("Focando na Bassline:", basslineTrack.name);
|
|
|
|
appState.pattern.focusedBasslineId = basslineTrack.id;
|
|
|
|
if (Number.isInteger(basslineTrack.patternIndex)) {
|
|
// fonte de verdade: aplica em TODAS as tracks (samplers/plugins)
|
|
sendAction({ type: "SET_ACTIVE_PATTERN", patternIndex: basslineTrack.patternIndex });
|
|
|
|
// opcional: manter selector sincronizado
|
|
const sel = document.getElementById("global-pattern-selector");
|
|
if (sel) sel.value = String(basslineTrack.patternIndex);
|
|
}
|
|
|
|
renderAll();
|
|
showToast(`Editando: ${basslineTrack.name}`, "info");
|
|
};
|
|
|
|
const sendPatternToSongBtn = document.getElementById("send-pattern-to-song-btn");
|
|
sendPatternToSongBtn?.addEventListener("click", () => {
|
|
const bpm = parseFloat(document.getElementById("bpm-input")?.value) || 120;
|
|
|
|
// playhead atual (se estiver tocando, usa logical; senão, seek)
|
|
const sec =
|
|
(appState.audio.audioEditorLogicalTime ?? 0) ||
|
|
(appState.audio.audioEditorSeekTime ?? 0) ||
|
|
0;
|
|
|
|
const patternIndex = appState.pattern.activePatternIndex;
|
|
if (!Number.isInteger(patternIndex)) {
|
|
showToast("Selecione uma pattern antes de enviar.", "error");
|
|
return;
|
|
}
|
|
|
|
let posTicks = secondsToSongTicks(sec, bpm);
|
|
posTicks = snapSongTicks(posTicks, LMMS_TICKS_PER_BAR); // snap por compasso (fica LMMS-like)
|
|
|
|
sendAction({
|
|
type: "ADD_PLAYLIST_PATTERN_CLIP",
|
|
patternIndex,
|
|
pos: posTicks,
|
|
len: LMMS_TICKS_PER_BAR, // 1 compasso default
|
|
clipId: `plc_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
});
|
|
});
|
|
|
|
|
|
window.exitPatternFocus = function() {
|
|
console.log("Saindo do foco da Bassline");
|
|
appState.pattern.focusedBasslineId = null;
|
|
|
|
// ✅ sem pattern selecionada no Song Editor
|
|
appState.pattern.activePatternIndex = null;
|
|
|
|
renderAll();
|
|
}
|
|
|
|
loadAndRenderSampleBrowser();
|
|
|
|
renderAll();
|
|
updateToolButtons();
|
|
});
|