441 lines
16 KiB
JavaScript
441 lines
16 KiB
JavaScript
// js/main.js (ESM com import absoluto de socket.js + ROOM_NAME local)
|
|
|
|
import { appState, resetProjectState } from "./state.js";
|
|
import {
|
|
updateTransportLoop,
|
|
restartAudioEditorIfPlaying,
|
|
} from "./audio/audio_audio.js";
|
|
import { initializeAudioContext } from "./audio.js";
|
|
import { handleFileLoad, generateMmpFile } from "./file.js";
|
|
import {
|
|
renderAll,
|
|
loadAndRenderSampleBrowser,
|
|
showOpenProjectModal,
|
|
closeOpenProjectModal,
|
|
} from "./ui.js";
|
|
import { renderAudioEditor } from "./audio/audio_ui.js";
|
|
import { adjustValue, enforceNumericInput } from "./utils.js";
|
|
import { ZOOM_LEVELS } from "./config.js";
|
|
|
|
// ⚠️ IMPORT ABSOLUTO para evitar 404/text/html quando a página estiver em /creation/ ou fora dela.
|
|
// Ajuste o prefixo abaixo para o caminho real onde seus assets vivem no servidor:
|
|
import { sendAction, joinRoom, setUserName } from "./socket.js";
|
|
|
|
// Descobre a sala pela URL (local ao main.js) e expõe no window para debug
|
|
const ROOM_NAME = new URLSearchParams(window.location.search).get("room");
|
|
window.ROOM_NAME = ROOM_NAME;
|
|
|
|
// ✅ NOVO: se tem sala na URL, entra já na sala (independe do áudio)
|
|
|
|
if (ROOM_NAME) {
|
|
// entra na sala para receber estado/broadcasts imediatamente
|
|
joinRoom();
|
|
}
|
|
|
|
// Função util para alternar estado dos botões de ferramenta
|
|
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");
|
|
|
|
// =================================================================
|
|
// 👇 INÍCIO DA CORREÇÃO (Botão de Sincronia - Agora envia Ação)
|
|
// =================================================================
|
|
const syncModeBtn = document.getElementById("sync-mode-btn"); //
|
|
if (syncModeBtn) {
|
|
//
|
|
// Define o estado inicial (global por padrão)
|
|
appState.global.syncMode = "global"; //
|
|
syncModeBtn.classList.add("active"); //
|
|
syncModeBtn.textContent = "Global"; //
|
|
|
|
syncModeBtn.addEventListener("click", () => {
|
|
//
|
|
// 1. Determina qual será o *novo* modo
|
|
const newMode =
|
|
appState.global.syncMode === "global" ? "local" : "global"; //
|
|
|
|
// 2. Envia a ação para sincronizar. O handleActionBroadcast
|
|
// cuidará de atualizar o appState, o botão e mostrar o toast.
|
|
sendAction({
|
|
type: "SET_SYNC_MODE",
|
|
mode: newMode,
|
|
});
|
|
|
|
// Lógica antiga removida daqui (movida para o handler)
|
|
/*
|
|
const isNowLocal = appState.global.syncMode === "global";
|
|
appState.global.syncMode = isNowLocal ? "local" : "global";
|
|
syncModeBtn.classList.toggle("active", !isNowLocal);
|
|
syncModeBtn.textContent = isNowLocal ? "Local" : "Global";
|
|
showToast( `🎧 Modo de Playback: ${isNowLocal ? "Local" : "Global"}`, "info" );
|
|
*/
|
|
});
|
|
|
|
// Esconde o botão se não estiver em uma sala (lógica movida do socket.js)
|
|
if (!ROOM_NAME) {
|
|
//
|
|
//syncModeBtn.style.display = 'none'; // REMOVIDO PARA TESTE VISUAL
|
|
}
|
|
}
|
|
// =================================================================
|
|
// 👆 FIM DA CORREÇÃO
|
|
// =================================================================
|
|
|
|
// 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";
|
|
}
|
|
});
|
|
|
|
// 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: "RESET_PROJECT" });
|
|
});
|
|
|
|
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();
|
|
sendAction({ type: "REMOVE_LAST_TRACK" });
|
|
});
|
|
|
|
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);
|
|
if (
|
|
appState.global.isPlaying &&
|
|
(event.target.id.startsWith("compasso-") ||
|
|
event.target.id === "bars-input")
|
|
) {
|
|
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 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) {
|
|
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" });
|
|
});
|
|
}
|
|
|
|
// 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\nCopie 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";
|
|
}
|
|
|
|
renderAll();
|
|
updateToolButtons();
|
|
});
|