plugin v1.0.1
Deploy / Deploy (push) Successful in 1m15s
Details
Deploy / Deploy (push) Successful in 1m15s
Details
This commit is contained in:
parent
fb9dccda3a
commit
ebaa0f81bf
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue