diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index c0d2b18..c787a30 100644 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -501,4 +501,5 @@ function downloadFile(content, fileName) { URL.revokeObjectURL(url); } -export { BLANK_PROJECT_XML }; \ No newline at end of file +export { generateXmlFromState as generateXmlFromStateExported }; +export { BLANK_PROJECT_XML }; // Mantenha o que já estava \ No newline at end of file diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index a37601f..31ded2e 100644 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -1,6 +1,5 @@ -// js/main.js (ESM com import absoluto de socket.js + ROOM_NAME local) - -import { appState } from "./state.js"; +// js/main.js +import { appState, loadStateFromSession } from "./state.js"; // <--- Importe loadStateFromSession import { updateTransportLoop, restartAudioEditorIfPlaying, @@ -17,44 +16,55 @@ 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 { renderActivePatternToBlob } 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 -// 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`; +// --- LÓGICA DE INICIALIZAÇÃO --- - // Chama a função de file.js para carregar (que já envia a ação 'LOAD_PROJECT') - loadProjectFromServer(filename); -} +// Função autoinvocada assíncrona para gerenciar o carregamento inicial +(async function initApp() { + + let restored = false; -// ✅ NOVO: se tem sala na URL, entra já na sala (independe do áudio) + // 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(); + } -if (ROOM_NAME) { - // entra na sala para receber estado/broadcasts imediatamente - 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); + } + +})(); // Fim do initApp -// 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"); @@ -115,16 +125,13 @@ document.addEventListener("DOMContentLoaded", () => { 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 { + } else { console.error("Botão de gravação (#record-btn) não encontrado no DOM."); } @@ -564,6 +571,8 @@ document.addEventListener("DOMContentLoaded", () => { // if (syncModeBtn) syncModeBtn.style.display = "none"; } + loadAndRenderSampleBrowser(); + renderAll(); updateToolButtons(); }); diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index fcd8ce7..2b5cedd 100644 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -1,7 +1,9 @@ // js/state.js import { initializePatternState } from "./pattern/pattern_state.js"; -import { audioState, initializeAudioState } from "./audio/audio_state.js"; +import { audioState, initializeAudioState, getAudioSnapshot, applyAudioSnapshot } from "./audio/audio_state.js"; import { DEFAULT_VOLUME, DEFAULT_PAN } from "./config.js"; +import { generateMmpFile } from "./file.js"; // Vamos usar uma adaptação disto ou criar um helper +import { parseMmpContent } from "./file.js"; // Importante para restaurar o XML // Estado global da aplicação const globalState = { @@ -25,21 +27,24 @@ const globalState = { clipboard: null, lastRulerClickTime: 0, justReset: false, + // Configs Globais que devem persistir + bpm: 140, + compassoA: 4, + compassoB: 4, + bars: 1, + syncMode: "global" // Adicionado para persistir a escolha }; -// --- ADICIONE ESTE BLOCO --- -// Define o ESTADO INICIAL para o pattern module const patternState = { tracks: [], activeTrackId: null, activePatternIndex: 0, }; -// --- FIM DA ADIÇÃO --- // Combina todos os estados em um único objeto namespaced export let appState = { global: globalState, - pattern: patternState, // <-- AGORA 'patternState' está definido + pattern: patternState, audio: audioState, }; window.appState = appState; @@ -47,7 +52,6 @@ window.appState = appState; export function resetProjectState() { console.log("Executando resetProjectState completo..."); - // 1. Reseta o estado global para os padrões Object.assign(appState.global, { sliceToolActive: false, isPlaying: false, @@ -71,61 +75,117 @@ export function resetProjectState() { justReset: false, }); - // 2. Reseta o estado do pattern Object.assign(appState.pattern, { tracks: [], activeTrackId: null, activePatternIndex: 0, }); - // 3. Reseta o estado de áudio (A PARTE QUE FALTAVA) - // Isso limpa appState.audio.clips e appState.audio.tracks - initializeAudioState(); // + initializeAudioState(); } +// --- NOVA IMPLEMENTAÇÃO DE PERSISTÊNCIA --- + +/** + * Helper: Gera a string XML do estado atual. + * Precisamos "emprestar" a lógica do file.js sem baixar o arquivo. + * A maneira mais limpa é importar generateXmlFromState de file.js, + * mas como ela não é exportada lá, vamos usar uma estratégia via DOM + * ou mover a lógica. Para simplificar sem quebrar o file.js, + * vamos salvar apenas o "essencial" do patternState se o XML falhar, + * mas o ideal é garantir que o XML seja gerado. + * * NOTA: Para que isso funcione perfeitamente, certifique-se de exportar + * 'generateXmlFromState' no seu file.js ou usar a lógica abaixo. + */ +import { generateXmlFromStateExported } from "./file.js"; // Vamos assumir que você vai exportar isso no file.js + export function saveStateToSession() { + // Só salva se estiver em uma sala (contexto colaborativo/online) if (!window.ROOM_NAME) return; - // 1. Crie uma versão "limpa" dos tracks, contendo APENAS dados - const cleanTracks = appState.pattern.tracks.map((track) => { - // Este novo objeto só contém dados que o JSON entende - return { - id: track.id, - name: track.name, - samplePath: track.samplePath, - patterns: track.patterns, // Isto é seguro (arrays de arrays/booleanos) - activePatternIndex: track.activePatternIndex, - volume: track.volume, // O número (0-1) - pan: track.pan, // O número (-1 to 1) - instrumentName: track.instrumentName, - instrumentXml: track.instrumentXml, - // *** OBJETOS TONE.JS EXCLUÍDOS ***: - // track.volumeNode, track.pannerNode, track.player - }; - }); - - // 2. Construa o objeto de estado final para salvar - const stateToSave = { - pattern: { - ...appState.pattern, // Copia outras propriedades (ex: activeTrackId) - tracks: cleanTracks, // Usa a nossa versão limpa dos tracks - }, - global: { - bpm: document.getElementById("bpm-input").value, - compassoA: document.getElementById("compasso-a-input").value, - compassoB: document.getElementById("compasso-b-input").value, - bars: document.getElementById("bars-input").value, - }, - }; - - // 3. Agora o stringify vai funcionar try { - const roomName = window.ROOM_NAME || "default_room"; - sessionStorage.setItem( - `temp_state_${roomName}`, - JSON.stringify(stateToSave) - ); + // 1. Snapshot do Áudio (inclui blobs Base64 das gravações) + const audioSnap = getAudioSnapshot(); + + // 2. Snapshot do Pattern (XML) + // Se não conseguirmos o XML exato, salvamos os dados brutos do pattern + let xmlContent = null; + try { + // Tenta usar a função exportada (veja nota abaixo sobre file.js) + if(typeof generateXmlFromStateExported === 'function') { + xmlContent = generateXmlFromStateExported(); + } + } catch(e) { + console.warn("Não foi possível gerar XML para backup:", e); + } + + const stateToSave = { + timestamp: Date.now(), + xml: xmlContent, + // Fallback: Salva o pattern bruto se o XML falhar + patternRaw: !xmlContent ? appState.pattern : null, + audio: audioSnap, + global: { + bpm: appState.global.bpm, // Pega do state, que deve estar syncado com o DOM + compassoA: appState.global.compassoA, + compassoB: appState.global.compassoB, + bars: appState.global.bars, + syncMode: appState.global.syncMode + } + }; + + const roomName = window.ROOM_NAME; + sessionStorage.setItem(`mmp_backup_${roomName}`, JSON.stringify(stateToSave)); + // console.log("Estado salvo na sessão local (F5 safe)."); + } catch (e) { - console.error("Falha ao salvar estado na sessão:", e); + console.error("Falha crítica ao salvar sessão (Quota Excedida?):", e); } } + +export async function loadStateFromSession() { + const roomName = window.ROOM_NAME; + if (!roomName) return false; + + try { + const raw = sessionStorage.getItem(`mmp_backup_${roomName}`); + if (!raw) return false; + + const backup = JSON.parse(raw); + console.log(`[Session] Recuperando backup de ${new Date(backup.timestamp).toLocaleTimeString()}`); + + // 1. Restaura Globais + if (backup.global) { + appState.global.bpm = backup.global.bpm; + appState.global.compassoA = backup.global.compassoA; + appState.global.compassoB = backup.global.compassoB; + appState.global.bars = backup.global.bars; + appState.global.syncMode = backup.global.syncMode || "global"; + + // Atualiza DOM imediatamente para evitar desincronia visual + const bpmInput = document.getElementById("bpm-input"); + if(bpmInput) bpmInput.value = backup.global.bpm; + // ... (outros inputs se necessário, mas o renderAll cuida da maioria) + } + + // 2. Restaura Pattern (XML é preferível) + if (backup.xml) { + await parseMmpContent(backup.xml); + } else if (backup.patternRaw) { + // Fallback simples (menos robusto que XML) + console.warn("Restaurando de Pattern Raw (backup antigo/incompleto)"); + Object.assign(appState.pattern, backup.patternRaw); + } + + // 3. Restaura Áudio (Clipes + Gravações) + if (backup.audio) { + await applyAudioSnapshot(backup.audio); + } + + return true; // Sucesso + + } catch (e) { + console.error("Erro ao restaurar sessão:", e); + return false; + } +} \ No newline at end of file