diff --git a/.gitignore b/.gitignore
index 5605f7fb..6ad63f7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ mmp
venv
.bundle
src
+assets/js/creations/server/data
\ No newline at end of file
diff --git a/assets/css/creator.css b/assets/css/creator.css
index bb7331fe..18185720 100644
--- a/assets/css/creator.css
+++ b/assets/css/creator.css
@@ -142,7 +142,39 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
color: var(--text-light); width: 25px; height: 40px; cursor: pointer;
border-radius: 0 4px 4px 0; display: flex; align-items: center; justify-content: center;
}
+/* Botão Padrão Unificado */
+.control-btn {
+ height: 32px;
+ min-width: 32px; /* Garante que botões quadrados fiquem quadrados */
+ padding: 0 10px; /* Padding lateral para botões com texto */
+ border: 1px solid transparent;
+ border-radius: 4px;
+ background-color: #464646; /* Cor base mais neutra */
+ color: var(--text-light);
+ font-size: 14px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+.control-btn:hover {
+ background-color: #5a5a5a;
+ transform: translateY(-1px);
+}
+
+.control-btn:active {
+ transform: translateY(0);
+}
+
+/* Botão Ativo (Toggle) */
+.control-btn.active {
+ background-color: var(--accent-green);
+ color: #fff;
+ border-color: var(--accent-green);
+ box-shadow: 0 0 8px rgba(46, 204, 113, 0.4);
+}
/* =============================================== */
/* ÁREA DE CONTEÚDO E FERRAMENTAS
/* =============================================== */
@@ -161,8 +193,35 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
.info-display { background-color: #1a1c1e; padding: 5px 8px; border-radius: 3px; text-align: center; }
.info-display .label { color: var(--text-dark); font-size: .6rem; text-transform: uppercase; }
.value-input { background: 0 0; border: 0; outline: 0; color: var(--accent-green); font-weight: 700; font-size: 1.4rem; font-family: Courier New, Courier, monospace; text-align: center; padding: 0; width: 55px; }
-.compasso-input { width: 25px; }
-.compasso-separator { color: var(--accent-green); font-weight: 700; font-size: 1.4rem; font-family: Courier New, Courier, monospace; margin: 0 2px; }
+/* --- CORREÇÃO DO COMPASSO --- */
+
+/* Força o container principal a alinhar tudo em uma linha só */
+.interactive-input-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Força cada grupo (botão - input - botão) a ficar em linha */
+.compasso-group {
+ display: flex;
+ align-items: center;
+}
+
+/* Ajuste opcional para a barra separadora ficar bonita */
+.compasso-separator {
+ margin: 0 5px;
+ color: var(--accent-green); /* Garante que fique verde como na imagem */
+ font-weight: bold;
+ font-size: 1.2rem;
+}
+
+/* Garante que os inputs do compasso não fiquem largos demais */
+.compasso-input {
+ width: 25px !important; /* Força a largura pequena */
+ text-align: center;
+ padding: 0;
+}
.value-input::-webkit-outer-spin-button, .value-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.value-input[type=number] { -moz-appearance: textfield; }
.adjust-btn { background: 0 0; border: 0; color: var(--text-dark); font-size: 1rem; font-weight: 700; cursor: pointer; padding: 0 5px; transition: color .2s; line-height: 1; }
@@ -191,7 +250,40 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, .3); overflow: hidden;
display: flex; flex-direction: column;
}
-.editor-header { background-color: var(--bg-toolbar); padding: 4px 10px; font-size: .8rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
+/* Container do Header do Editor */
+.editor-header {
+ display: flex;
+ justify-content: space-between; /* Espalha grupos para esquerda e direita */
+ align-items: center;
+ height: 50px; /* Altura fixa para ambos */
+ background-color: var(--panel-bg);
+ border-bottom: 1px solid var(--border-color);
+ padding: 0 15px;
+ box-sizing: border-box;
+}
+/* Inputs dentro da toolbar (Selects, Numbers) */
+.editor-header select,
+.editor-header input[type="number"] {
+ height: 32px;
+ background-color: #222;
+ border: 1px solid var(--border-color);
+ color: var(--text-light);
+ border-radius: 4px;
+ padding: 0 8px;
+}
+/* Grupos de ferramentas (ex: Transporte, Zoom, Edição) */
+.toolbar-group {
+ display: flex;
+ align-items: center;
+ gap: 8px; /* Espaço uniforme entre botões */
+}
+/* Divisor Vertical Visual (opcional, para separar grupos) */
+.toolbar-divider {
+ width: 1px;
+ height: 24px;
+ background-color: var(--border-color);
+ margin: 0 8px;
+}
.editor-toolbar { background-color: var(--bg-toolbar); padding: 5px 10px; border-bottom: 2px solid var(--border-color); flex-shrink: 0; display: flex; align-items: center; gap: 15px; }
#track-container { overflow-y: auto; overflow-x: hidden; flex-grow: 1; }
@@ -224,6 +316,15 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, .3); overflow: hidden;
display: flex; flex-direction: column; --track-info-width: 255px;
}
+/* CORREÇÃO CSS: Adicionado para garantir layout correto */
+.audio-tracks-wrapper {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow: hidden;
+}
+
#audio-track-container { flex-grow: 1; overflow: auto; }
.audio-track-lane { display: flex; flex-direction: row; align-items: stretch; background-color: var(--bg-editor); border-bottom: 1px solid var(--bg-toolbar); min-height: 90px; box-sizing: border-box; }
.audio-track-lane.drag-over { background-color: #40454d; }
diff --git a/assets/js/creations/audio/audio_ui.js b/assets/js/creations/audio/audio_ui.js
index 6cbfe933..70ce5f49 100644
--- a/assets/js/creations/audio/audio_ui.js
+++ b/assets/js/creations/audio/audio_ui.js
@@ -23,23 +23,43 @@ import { sendAction } from "../socket.js";
export function renderAudioEditor() {
const audioEditor = document.querySelector(".audio-editor");
- const existingTrackContainer = document.getElementById(
- "audio-track-container"
- );
+ const existingTrackContainer = document.getElementById("audio-track-container");
+
if (!audioEditor || !existingTrackContainer) return;
+ // --- CORREÇÃO DO ERRO DOMException ---
+ // Identificamos quem é o pai real do container de tracks (agora é .audio-tracks-wrapper)
+ const tracksParent = existingTrackContainer.parentElement;
+
// --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA ---
- let rulerWrapper = audioEditor.querySelector(".ruler-wrapper");
+ // Buscamos a régua dentro desse pai correto
+ let rulerWrapper = tracksParent.querySelector(".ruler-wrapper");
+
if (!rulerWrapper) {
+ // Se a régua criada pelo JS ainda não existe, cria ela
+ // (Ignora a régua estática do HTML se houver conflito, priorizando esta estrutura)
rulerWrapper = document.createElement("div");
rulerWrapper.className = "ruler-wrapper";
rulerWrapper.innerHTML = `
`;
- audioEditor.insertBefore(rulerWrapper, existingTrackContainer);
+
+ // Insere ANTES do container de tracks, dentro do pai correto
+ tracksParent.insertBefore(rulerWrapper, existingTrackContainer);
}
+ // Remove a régua estática antiga se ela ainda estiver lá atrapalhando (opcional, mas recomendado)
+ const staticRuler = tracksParent.querySelector("#audio-timeline-ruler");
+ if (staticRuler && staticRuler.parentElement === tracksParent) {
+ staticRuler.remove();
+ }
+ // Remove elementos soltos antigos se existirem para evitar duplicação
+ const oldLoopRegion = tracksParent.querySelector("#loop-region");
+ const oldPlayhead = tracksParent.querySelector("#playhead");
+ if(oldLoopRegion) oldLoopRegion.remove();
+ if(oldPlayhead) oldPlayhead.remove();
+
const ruler = rulerWrapper.querySelector(".timeline-ruler");
ruler.innerHTML = "";
@@ -243,7 +263,9 @@ export function renderAudioEditor() {
// Recriação Container Pistas (sem alterações)
const newTrackContainer = existingTrackContainer.cloneNode(false);
- audioEditor.replaceChild(newTrackContainer, existingTrackContainer);
+
+ // Substitui no pai correto (tracksParent), não no audioEditor
+ tracksParent.replaceChild(newTrackContainer, existingTrackContainer);
// Render Pistas (sem alterações)
appState.audio.tracks.forEach((trackData) => {
@@ -725,7 +747,6 @@ export function renderAudioEditor() {
const constrainedLeftPx = Math.max(0, newLeftPx);
let newStartTime = constrainedLeftPx / currentPixelsPerSecond;
newStartTime = quantizeTime(newStartTime);
- // (Correção Bug 4 - remove Number())
updateAudioClipProperties(clipId, {
trackId: newTrackId,
startTimeInSeconds: newStartTime,
@@ -756,13 +777,8 @@ export function renderAudioEditor() {
const clickX = event.clientX - rect.left;
const absoluteX = clickX + scrollLeft;
const newTime = absoluteX / currentPixelsPerSecond;
- // =================================================================
- // 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Pista)
- // =================================================================
+ // Sincronia de Seek na Pista)
sendAction({ type: "SET_SEEK_TIME", seekTime: newTime });
- // seekAudioEditor(newTime); // 👈 Substituído
- // =================================================================
- // 👆 FIM DA CORREÇÃO
};
handleSeek(e); // Aplica no mousedown
const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
@@ -856,7 +872,7 @@ export function resetPlayheadVisual() {
});
}
-// --- INÍCIO DA NOVA FUNÇÃO (Passo 4: A Função de Desenho) ---
+// --- A Função de Desenho) ---
// (Adicionada ao final de audio_ui.js)
/**
@@ -907,5 +923,4 @@ function createPatternViewElement(patternData) {
});
return view;
-}
-// --- FIM DA NOVA FUNÇÃO ---
+}
\ No newline at end of file
diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js
index 5161d3e4..0d5cd60b 100644
--- a/assets/js/creations/file.js
+++ b/assets/js/creations/file.js
@@ -1,4 +1,6 @@
-// js/file.js
+//--------------------------------------------------------------
+// IMPORTS NECESSÁRIOS
+//--------------------------------------------------------------
import { appState, saveStateToSession, resetProjectState, loadStateFromSession } from "./state.js";
import { loadAudioForTrack } from "./pattern/pattern_state.js";
import { renderAll, getSamplePathMap } from "./ui.js";
@@ -7,21 +9,13 @@ import {
initializeAudioContext,
getMainGainNode,
} from "./audio.js";
+import { DEFAULT_PROJECT_XML } from "./utils.js"
import * as Tone from "https://esm.sh/tone";
-
import { sendAction } from "./socket.js";
-const BLANK_PROJECT_XML = `
-
-
-
-
-
-
-
-
-
-`;
+//--------------------------------------------------------------
+//
+//--------------------------------------------------------------
export function handleLocalProjectReset() {
console.log("Recebido comando de reset. Limpando estado local...");
@@ -89,7 +83,7 @@ export async function loadProjectFromServer(fileName) {
export async function parseMmpContent(xmlString) {
resetProjectState();
initializeAudioContext();
- appState.global.justReset = xmlString === BLANK_PROJECT_XML;
+ appState.global.justReset = xmlString === DEFAULT_PROJECT_XML;
const audioContainer = document.getElementById("audio-track-container");
if (audioContainer) {
@@ -305,10 +299,7 @@ export async function parseMmpContent(xmlString) {
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null;
appState.pattern.activePatternIndex = 0;
- // --- SUBSTUIÇÃO DO BLOCO DE RESTAURAÇÃO ---
- // Em vez daquele bloco try/catch gigante, apenas chamamos a função:
loadStateFromSession();
- // ------------------------------------------
await Promise.resolve();
renderAll();
@@ -324,17 +315,19 @@ export function generateMmpFile() {
function generateXmlFromState() {
if (!appState.global.originalXmlDoc) {
- console.warn("Não há XML original. Retornando vazio.");
- return "";
+ console.log("Gerando XML a partir do template em branco...");
+ const parser = new DOMParser();
+ appState.global.originalXmlDoc = parser.parseFromString(DEFAULT_PROJECT_XML, "application/xml");
}
const xmlDoc = appState.global.originalXmlDoc.cloneNode(true);
const head = xmlDoc.querySelector("head");
+
if (head) {
- head.setAttribute("bpm", document.getElementById("bpm-input").value);
- head.setAttribute("num_bars", document.getElementById("bars-input").value);
- head.setAttribute("timesig_numerator", document.getElementById("compasso-a-input").value);
- head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value);
+ head.setAttribute("bpm", document.getElementById("bpm-input").value || 140);
+ head.setAttribute("num_bars", document.getElementById("bars-input").value || 1);
+ head.setAttribute("timesig_numerator", document.getElementById("compasso-a-input").value || 4);
+ head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value || 4);
}
const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer');
@@ -344,7 +337,6 @@ function generateXmlFromState() {
.map((track) => createTrackXml(track))
.join("");
- // Gambiarra para inserir o XML gerado como nós DOM
const tempDoc = new DOMParser().parseFromString(`${tracksXml}`, "application/xml");
Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => {
bbTrackContainer.appendChild(newTrackNode);
@@ -369,21 +361,22 @@ function createTrackXml(track) {
const lmmsVolume = Math.round(track.volume * 100);
const lmmsPan = Math.round(track.pan * 100);
+ // 🔥 PROTEÇÃO: Se não tiver instrumento definido, usa Kicker padrão
+ const instrName = track.instrumentName || "kicker";
+ const instrXml = track.instrumentXml || ``;
+
const patternsXml = track.patterns.map((pattern) => {
let patternNotesXml = "";
- // SE for plugin e tiver notas detalhadas, usa elas
if (track.type === "plugin" && pattern.notes && pattern.notes.length > 0) {
patternNotesXml = pattern.notes.map(note => {
return ``;
}).join("\n ");
}
- // SE for sampler (ou plugin sem notas detalhadas), usa os steps convertidos em notas simples
else {
patternNotesXml = pattern.steps.map((isActive, index) => {
if (isActive) {
const notePos = Math.round(index * ticksPerStep);
- // Key 57 é o padrão do LMMS para samples (Lá)
return ``;
}
return "";
@@ -398,8 +391,8 @@ function createTrackXml(track) {
return `