Padronização da interface e atualização do sync entre clientes

This commit is contained in:
JotaChina committed 2025-11-27 22:02:02 -03:00
1 parent 3f714c88de
commit 7d9d2b0f63
10 files changed
+796 -377

No files matched your search

+164 -57
View File
@@ -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);
}