plugin v1.0.1
Deploy / Deploy (push) Successful in 1m15s Details

This commit is contained in:
JotaChina 2025-11-23 17:33:22 -03:00
parent fb9dccda3a
commit ebaa0f81bf
5 changed files with 165 additions and 83 deletions

View File

@ -501,4 +501,5 @@ function downloadFile(content, fileName) {
URL.revokeObjectURL(url);
}
export { BLANK_PROJECT_XML };
export { generateXmlFromState as generateXmlFromStateExported };
export { BLANK_PROJECT_XML }; // Mantenha o que já estava

View File

@ -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();
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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 , 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;
}
}