569 lines
20 KiB
JavaScript
569 lines
20 KiB
JavaScript
// js/main.js (ESM com import absoluto de socket.js + ROOM_NAME local)
|
|
|
|
import { appState } from "./state.js";
|
|
import {
|
|
updateTransportLoop,
|
|
restartAudioEditorIfPlaying,
|
|
} from "./audio/audio_audio.js";
|
|
import { initializeAudioContext } from "./audio.js";
|
|
import { handleFileLoad, generateMmpFile, BLANK_PROJECT_XML } 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 { loadProjectFromServer } from "./file.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";
|
|
|
|
import { renderActivePatternToBlob } from "./pattern/pattern_audio.js"; // <-- ADICIONE ESTA LINHA
|
|
import { showToast } from "./ui.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;
|
|
|
|
const PROJECT_NAME = new URLSearchParams(window.location.search).get("project");
|
|
|
|
if (PROJECT_NAME) {
|
|
// O nome do projeto deve corresponder ao arquivo no servidor, por ex: "mmp/nome-do-seu-projeto-salvo.mmp"
|
|
// O arquivo 'file.js' já espera que loadProjectFromServer receba apenas o nome
|
|
// do arquivo dentro da pasta 'mmp/' (ex: 'nome-do-projeto.mmp').
|
|
console.log(`[MAIN] Carregando projeto do servidor: ${PROJECT_NAME}`);
|
|
// Adicione a extensão se ela não estiver no link
|
|
const filename =
|
|
PROJECT_NAME.endsWith(".mmp") || PROJECT_NAME.endsWith(".mmpz")
|
|
? PROJECT_NAME
|
|
: `${PROJECT_NAME}.mmp`;
|
|
|
|
// Chama a função de file.js para carregar (que já envia a ação 'LOAD_PROJECT')
|
|
loadProjectFromServer(filename);
|
|
}
|
|
|
|
// ✅ 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");
|
|
// Configuração do botão de Gravação
|
|
const recordBtn = document.getElementById('record-btn');
|
|
|
|
if (recordBtn) {
|
|
recordBtn.addEventListener('click', async () => {
|
|
// PASSO CRÍTICO: O navegador exige isso antes de gravar
|
|
if (Tone.context.state !== 'running') {
|
|
await Tone.start();
|
|
console.log("Audio Context iniciado via clique.");
|
|
}
|
|
|
|
// Chama a função que você criou no recording.js
|
|
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");
|
|
}
|
|
});
|
|
|
|
//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", () => {
|
|
// Pega o novo índice (ex: 0, 1, 2...) do seletor
|
|
const newPatternIndex = parseInt(globalPatternSelector.value, 10);
|
|
|
|
// --- CORREÇÃO DE LÓGICA (não precisamos mais do activeTrackId) ---
|
|
// A ação agora é global e afeta TODAS as tracks.
|
|
if (isNaN(newPatternIndex)) {
|
|
console.warn("Não é possível trocar pattern: índice inválido.");
|
|
return;
|
|
}
|
|
|
|
// Envia a ação para todos (incluindo você)
|
|
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) {
|
|
//
|
|
// 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: "LOAD_PROJECT", xml: BLANK_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();
|
|
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\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";
|
|
}
|
|
|
|
renderAll();
|
|
updateToolButtons();
|
|
});
|