Padronização da interface e atualização do sync entre clientes
This commit is contained in:
1 parent
3f714c88de
commit
7d9d2b0f63
10 files changed
+796
-377
No files matched your search
+164
-57
@@ -8,6 +8,7 @@ import {
|
||||
addTrackToState,
|
||||
removeLastTrackFromState,
|
||||
updateTrackSample,
|
||||
removeTrackById,
|
||||
} from "./pattern/pattern_state.js";
|
||||
import {
|
||||
addAudioTrackLane,
|
||||
@@ -37,13 +38,13 @@ import {
|
||||
parseMmpContent,
|
||||
handleLocalProjectReset,
|
||||
syncPatternStateToServer,
|
||||
BLANK_PROJECT_XML,
|
||||
generateXmlFromStateExported,
|
||||
} from "./file.js";
|
||||
|
||||
import { renderAll, showToast } from "./ui.js"; // showToast()
|
||||
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
|
||||
import { PORT_SOCK } from "./config.js";
|
||||
import { DEFAULT_PROJECT_XML } from "./utils.js"
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Gera um ID único otimista (ex: "track_1678886401000_abc123")
|
||||
@@ -200,17 +201,12 @@ socket.on("load_project_state", async (projectXml) => {
|
||||
// 🔥 CORREÇÃO: Força a restauração da sessão LOCAL logo após carregar o XML
|
||||
// Isso garante que suas alterações locais (BPM, steps) "ganhem" do servidor
|
||||
if (window.ROOM_NAME) {
|
||||
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
|
||||
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
const hasLocalAudio = parsed.audioSnapshot?.clips?.length > 0 ||
|
||||
parsed.audioSnapshot?.tracks?.length > 0;
|
||||
if (hasLocalAudio) {
|
||||
console.log("Re-aplicando sessão local sobre o XML do servidor...");
|
||||
// Se existe 'raw', confiamos que é o estado mais recente do usuário.
|
||||
console.log("Re-aplicando sessão local (mesmo se vazia)...");
|
||||
await loadStateFromSession();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
renderAll();
|
||||
@@ -513,6 +509,44 @@ async function handleActionBroadcast(action) {
|
||||
showToast(`▶ ${who} Play bases`, "info");
|
||||
break;
|
||||
}
|
||||
case "UPDATE_PATTERN_NOTES": {
|
||||
const {
|
||||
trackIndex: ti,
|
||||
patternIndex: pi,
|
||||
notes,
|
||||
steps: incomingSteps,
|
||||
} = action;
|
||||
|
||||
const t = appState.pattern.tracks[ti];
|
||||
if (!t) break;
|
||||
|
||||
if (!t.patterns[pi]) t.patterns[pi] = { steps: [], notes: [] };
|
||||
|
||||
t.patterns[pi].notes = Array.isArray(notes) ? notes : [];
|
||||
|
||||
if (Array.isArray(incomingSteps)) {
|
||||
t.patterns[pi].steps = incomingSteps;
|
||||
}
|
||||
|
||||
try {
|
||||
schedulePatternRerender();
|
||||
} catch (e) {
|
||||
console.warn("Erro no render UPDATE_PATTERN_NOTES", e);
|
||||
}
|
||||
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({
|
||||
type: "SYNC_PATTERN_STATE",
|
||||
xml,
|
||||
});
|
||||
}
|
||||
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case "STOP_PLAYBACK": {
|
||||
setTimeout(stopPlayback, delayMs);
|
||||
const who = actorOf(action);
|
||||
@@ -551,9 +585,6 @@ async function handleActionBroadcast(action) {
|
||||
);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// 👇 INÍCIO DA CORREÇÃO (Handlers Sincronia de Loop/Seek/SyncMode)
|
||||
// =================================================================
|
||||
case "SET_LOOP_STATE": {
|
||||
const changed =
|
||||
appState.global.isLoopActive !== !!action.isLoopActive ||
|
||||
@@ -614,9 +645,6 @@ async function handleActionBroadcast(action) {
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
// =================================================================
|
||||
// 👆 FIM DA CORREÇÃO
|
||||
// =================================================================
|
||||
|
||||
// Estado Global
|
||||
case "LOAD_PROJECT": //
|
||||
@@ -647,17 +675,22 @@ async function handleActionBroadcast(action) {
|
||||
isLoadingProject = false;
|
||||
break;
|
||||
|
||||
case "SYNC_PATTERN_STATE": //
|
||||
// Esta ação agora só será recebida de *outros* usuários,
|
||||
// ou quando o servidor enviar, não de você mesmo.
|
||||
try {
|
||||
await parseMmpContent(action.xml); //
|
||||
renderAll();
|
||||
saveStateToSession(); //
|
||||
console.log("Socket: Pattern state sincronizado.");
|
||||
} catch (e) {
|
||||
console.error("Erro SYNC_PATTERN_STATE:", e);
|
||||
showToast("❌ Erro sync pattern", "error");
|
||||
case "SYNC_PATTERN_STATE":
|
||||
// 🔥 CORREÇÃO CRÍTICA:
|
||||
// Esta ação serve apenas para o SERVIDOR atualizar o arquivo .json no disco.
|
||||
// Os clientes online NÃO devem recarregar o XML, pois eles já atualizaram
|
||||
// o estado via ações atômicas (TOGGLE_NOTE, ADD_TRACK, etc).
|
||||
// Recarregar aqui causa o "resetProjectState" que mata o áudio.
|
||||
|
||||
console.log("Socket: XML salvo no servidor (Sync silencioso).");
|
||||
|
||||
// Se quiser garantir, salvamos apenas na sessão local do navegador
|
||||
// sem mexer na engine de áudio ou na tela.
|
||||
if (action.xml) {
|
||||
// Atualiza apenas a string do XML na memória global para referência futura
|
||||
const parser = new DOMParser();
|
||||
appState.global.originalXmlDoc = parser.parseFromString(action.xml, "application/xml");
|
||||
saveStateToSession();
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -713,17 +746,55 @@ async function handleActionBroadcast(action) {
|
||||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
showToast(`🥁 Faixa add por ${who}`, "info");
|
||||
// Salva o estado localmente também!
|
||||
|
||||
// 🔥 CORREÇÃO: Avisa o servidor que o XML mudou!
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
}
|
||||
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_TRACK_BY_ID": {
|
||||
const { trackId } = action;
|
||||
const removed = removeTrackById(trackId); // Tenta remover pelo ID seguro
|
||||
|
||||
if (removed) {
|
||||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
showToast(`❌ Faixa removida por ${who}`, "warning");
|
||||
|
||||
// Sincroniza o XML com o servidor para persistir a remoção
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
// Importante: gerar o XML atualizado (sem a faixa)
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
}
|
||||
saveStateToSession();
|
||||
} else {
|
||||
console.warn(`Tentativa de remover track inexistente/fantasma: ${trackId}`);
|
||||
// Não faz nada, protegendo as faixas locais!
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_LAST_TRACK": {
|
||||
removeLastTrackFromState();
|
||||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
showToast(`❌ Faixa remov. por ${who}`, "warning");
|
||||
// Salva o estado localmente também!
|
||||
|
||||
// 🔥 CORREÇÃO: Avisa o servidor que o XML mudou (removeu o TripleOscillator, por exemplo)
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
}
|
||||
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
@@ -751,6 +822,31 @@ async function handleActionBroadcast(action) {
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_LAST_AUDIO_LANE": {
|
||||
// 1. Verifica se tem tracks
|
||||
if (appState.audio.tracks.length === 0) break;
|
||||
|
||||
// 2. Pega a última track
|
||||
const lastTrack = appState.audio.tracks.pop();
|
||||
|
||||
// 3. Remove os clips associados a essa track (Limpeza)
|
||||
if (lastTrack && lastTrack.id) {
|
||||
// Filtra mantendo apenas os clips que NÃO pertencem à track removida
|
||||
appState.audio.clips = appState.audio.clips.filter(clip => clip.trackId !== lastTrack.id);
|
||||
}
|
||||
|
||||
// 4. Atualiza a tela
|
||||
renderAll();
|
||||
|
||||
// 5. Notifica
|
||||
const who = actorOf(action);
|
||||
showToast(`🗑️ Pista de áudio removida por ${who}`, "error");
|
||||
|
||||
// 6. Salva sessão
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_AUDIO_CLIP":
|
||||
if (removeAudioClip(action.clipId)) {
|
||||
appState.global.selectedClipId = null;
|
||||
@@ -887,47 +983,32 @@ async function handleActionBroadcast(action) {
|
||||
|
||||
// Snapshots
|
||||
case "AUDIO_SNAPSHOT_REQUEST": {
|
||||
const clips = appState.audio?.clips?.length || 0,
|
||||
tracks = appState.audio?.tracks?.length || 0;
|
||||
const iHave = clips > 0 || tracks > 0;
|
||||
if (!iHave || isFromSelf) break;
|
||||
if (!window.__lastSnapshotSentAt) window.__lastSnapshotSentAt = 0;
|
||||
const now = Date.now();
|
||||
if (now - window.__lastSnapshotSentAt < 1500) break;
|
||||
window.__lastSnapshotSentAt = now;
|
||||
try {
|
||||
const snap = getAudioSnapshot();
|
||||
sendAction({
|
||||
type: "AUDIO_SNAPSHOT",
|
||||
snapshot: snap,
|
||||
__target: action.__senderId,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Erro AUDIO_SNAPSHOT_REQUEST:", e);
|
||||
}
|
||||
// Salva o estado localmente também!
|
||||
saveStateToSession();
|
||||
// 🛑 BLOQUEADO: Agora o Servidor (backend) é quem manda o snapshot autoritativo.
|
||||
// Se deixarmos os clientes responderem, eles vão enviar estados antigos ("zumbis").
|
||||
console.log("Socket: Ignorando pedido de snapshot (Server Authoritative Mode).");
|
||||
break;
|
||||
}
|
||||
|
||||
case "AUDIO_SNAPSHOT": {
|
||||
if (action.__target && action.__target !== socket.id) break;
|
||||
if (appState.global.justReset) {
|
||||
console.warn("Socket: Snapshot de áudio ignorado (justReset=true).");
|
||||
break; // Ignora o snapshot
|
||||
}
|
||||
const hasClips = (appState.audio?.clips?.length || 0) > 0;
|
||||
if (hasClips) break;
|
||||
|
||||
// Removemos a verificação "if (hasClips) break" que impedia
|
||||
// de carregar um estado vazio se o local já tivesse algo (como as faixas zumbis).
|
||||
|
||||
try {
|
||||
console.log("Socket: Aplicando Snapshot de Áudio Autoritativo...");
|
||||
await applyAudioSnapshot(action.snapshot);
|
||||
renderAll();
|
||||
|
||||
const who = actorOf(action);
|
||||
showToast(`🔁 Sync áudio por ${who}`, "success");
|
||||
// Só mostra toast se realmente tiver conteúdo, para não spammar na entrada
|
||||
if ((action.snapshot?.tracks?.length || 0) > 0) {
|
||||
showToast(`🔁 Sync áudio por ${who}`, "success");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Erro AUDIO_SNAPSHOT:", e);
|
||||
showToast("❌ Erro sync áudio", "error");
|
||||
}
|
||||
// Salva o estado localmente também!
|
||||
// Salva o estado localmente (mesmo que seja vazio!)
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
@@ -1016,6 +1097,32 @@ async function handleActionBroadcast(action) {
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_AUDIO_LANE_BY_ID": { //
|
||||
const { trackId } = action;
|
||||
|
||||
// 1. Encontra o índice da faixa com esse ID específico
|
||||
const trackIndex = appState.audio.tracks.findIndex(t => t.id === trackId);
|
||||
|
||||
if (trackIndex !== -1) {
|
||||
// 2. Remove a faixa exata do array local
|
||||
appState.audio.tracks.splice(trackIndex, 1);
|
||||
|
||||
// 3. Remove os clips associados a este ID (Limpeza local)
|
||||
appState.audio.clips = appState.audio.clips.filter(c => c.trackId !== trackId);
|
||||
|
||||
renderAll(); // Atualiza a tela
|
||||
|
||||
const who = actorOf(action);
|
||||
// showToast(`🗑️ Pista removida por ${who}`, "warning");
|
||||
|
||||
// 4. Salva a sessão para garantir persistência no F5 local
|
||||
saveStateToSession();
|
||||
} else {
|
||||
console.warn(`Tentativa de remover track inexistente: ${trackId}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Ação desconhecida:", action.type);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user