diff --git a/src/layoutmanager.js b/src/layoutmanager.js index 87884fd..7ab1e91 100644 --- a/src/layoutmanager.js +++ b/src/layoutmanager.js @@ -104,7 +104,48 @@ function createPlaceholderPane(paneName) { * @returns {Object} Layout definition object */ export function serializeLayout(rootElement) { - const layoutNode = serializeLayoutNode(rootElement.firstChild); + if (!rootElement.firstChild) { + throw new Error("No layout to serialize"); + } + + console.log('[serializeLayout] rootElement has', rootElement.children.length, 'children:'); + for (let i = 0; i < rootElement.children.length; i++) { + console.log(` [${i}]:`, rootElement.children[i].className); + } + + let layoutNode; + + // Check if rootElement itself acts as a grid (has 2 children from a split) + if (rootElement.children.length === 2) { + // rootElement is acting as a grid container + // Check if it has grid attributes + const isHorizontal = rootElement.classList.contains("horizontal-grid"); + const isVertical = rootElement.classList.contains("vertical-grid"); + const percent = parseFloat(rootElement.getAttribute("lb-percent")) || 50; + + if (isHorizontal || isVertical) { + console.log('[serializeLayout] rootElement is a grid, serializing both children'); + layoutNode = { + type: isHorizontal ? "horizontal-grid" : "vertical-grid", + percent: percent, + children: [ + serializeLayoutNode(rootElement.children[0]), + serializeLayoutNode(rootElement.children[1]) + ] + }; + } else { + // No grid classes, but has 2 children - this shouldn't happen but handle it + console.warn('[serializeLayout] rootElement has 2 children but no grid classes, serializing first child only'); + layoutNode = serializeLayoutNode(rootElement.firstChild); + } + } else { + // Single child, serialize it directly + console.log('[serializeLayout] Starting from element:', rootElement.firstChild.className); + layoutNode = serializeLayoutNode(rootElement.firstChild); + } + + console.log('[serializeLayout] Serialized layout:', JSON.stringify(layoutNode, null, 2)); + return { name: "Custom Layout", description: "User-created layout", @@ -116,26 +157,39 @@ export function serializeLayout(rootElement) { * Recursively serializes a layout node * @private */ -function serializeLayoutNode(element) { +function serializeLayoutNode(element, depth = 0) { + const indent = ' '.repeat(depth); + console.log(`${indent}[serializeLayoutNode depth=${depth}] element:`, element.className, 'children:', element.children.length); + if (!element) { throw new Error("Cannot serialize null element"); } - // Check if this is a pane - if (element.classList.contains("pane") && !element.classList.contains("horizontal-grid") && !element.classList.contains("vertical-grid")) { - // Extract pane name from the element (stored in data attribute or class) - const paneName = element.getAttribute("data-pane-name") || "stage"; + // Check if this is a pane (has data-pane-name attribute) + // This check must come first, as panes may also have grid classes for internal layout + if (element.hasAttribute("data-pane-name")) { + // The data-pane-name is kebab-case, but we need to save the camelCase key + // that matches the panes object keys, not the name property + const dataName = element.getAttribute("data-pane-name"); + + // Convert kebab-case to camelCase (e.g., "timeline-v2" -> "timelineV2") + const camelCaseName = dataName.replace(/-([a-z0-9])/g, (g) => g[1].toUpperCase()); + + console.log(`${indent} -> Found pane: ${camelCaseName}`); + return { type: "pane", - name: paneName + name: camelCaseName }; } - // Check if this is a grid + // Check if this is a grid (split pane structure) if (element.classList.contains("horizontal-grid") || element.classList.contains("vertical-grid")) { const isHorizontal = element.classList.contains("horizontal-grid"); const percent = parseFloat(element.getAttribute("lb-percent")) || 50; + console.log(`${indent} -> Found ${isHorizontal ? 'horizontal' : 'vertical'} grid with ${percent}% split`); + if (element.children.length !== 2) { throw new Error("Grid must have exactly 2 children"); } @@ -144,15 +198,24 @@ function serializeLayoutNode(element) { type: isHorizontal ? "horizontal-grid" : "vertical-grid", percent: percent, children: [ - serializeLayoutNode(element.children[0]), - serializeLayoutNode(element.children[1]) + serializeLayoutNode(element.children[0], depth + 1), + serializeLayoutNode(element.children[1], depth + 1) ] }; } + // Check if this is a panecontainer wrapper - recurse into it + if (element.classList.contains("panecontainer")) { + console.log(`${indent} -> Found panecontainer, recursing into child`); + if (element.children.length === 1) { + return serializeLayoutNode(element.children[0], depth); + } + } + // If element has only one child, recurse into it if (element.children.length === 1) { - return serializeLayoutNode(element.children[0]); + console.log(`${indent} -> Element has 1 child, recursing`); + return serializeLayoutNode(element.children[0], depth); } throw new Error(`Cannot serialize element: ${element.className}`); diff --git a/src/main.js b/src/main.js index f5b8608..cebda20 100644 --- a/src/main.js +++ b/src/main.js @@ -112,7 +112,7 @@ import { actions, initializeActions, updateAutomationName } from "./actions/inde // Layout system import { defaultLayouts, getLayout, getLayoutNames } from "./layouts.js"; -import { buildLayout, loadLayoutByKeyOrName, saveCustomLayout } from "./layoutmanager.js"; +import { buildLayout, loadLayoutByKeyOrName, saveCustomLayout, serializeLayout } from "./layoutmanager.js"; const { writeTextFile: writeTextFile, @@ -1669,11 +1669,15 @@ async function _save(path) { } } + // Serialize current layout structure (panes, splits, sizes) + const serializedLayout = serializeLayout(rootPane); + const fileData = { version: "2.0.0", width: config.fileWidth, height: config.fileHeight, fps: config.framerate, + layoutState: serializedLayout, // Save current layout structure actions: undoStack, json: root.toJSON(), // Audio pool at the end for human readability @@ -2094,6 +2098,37 @@ async function _open(path, returnJson = false) { context.activeObject.activeLayer = context.activeObject.layers[0]; } + // Restore layout if saved and preference is enabled + console.log('[JS] Layout restoration check:', { + restoreLayoutFromFile: config.restoreLayoutFromFile, + hasLayoutState: !!file.layoutState, + layoutState: file.layoutState + }); + + if (config.restoreLayoutFromFile && file.layoutState) { + try { + console.log('[JS] Restoring saved layout:', file.layoutState); + // Clear existing layout + while (rootPane.firstChild) { + rootPane.removeChild(rootPane.firstChild); + } + layoutElements.length = 0; + canvases.length = 0; + + // Build layout from saved state + buildLayout(rootPane, file.layoutState, panes, createPane, splitPane); + + // Update UI after layout change + updateAll(); + updateUI(); + console.log('[JS] Layout restored successfully'); + } catch (error) { + console.error('[JS] Failed to restore layout, using default:', error); + } + } else { + console.log('[JS] Skipping layout restoration'); + } + // Restore audio tracks and clips to the Rust backend // The fromJSON method only creates JavaScript objects, // but doesn't initialize them in the audio engine @@ -4994,15 +5029,25 @@ async function startup() { } }); + console.log('[startup] window.openedFiles:', window.openedFiles); + console.log('[startup] config.reopenLastSession:', config.reopenLastSession); + console.log('[startup] config.recentFiles:', config.recentFiles); + + // Always update start screen data so it's ready when needed + await updateStartScreen(config); + if (!window.openedFiles?.length) { if (config.reopenLastSession && config.recentFiles?.length) { + console.log('[startup] Reopening last session:', config.recentFiles[0]); document.body.style.cursor = "wait" setTimeout(()=>_open(config.recentFiles[0]), 10) } else { - // Show start screen instead of new file dialog - await updateStartScreen(config); + console.log('[startup] Showing start screen'); + // Show start screen showStartScreen(); } + } else { + console.log('[startup] Files already opened, skipping start screen'); } } @@ -6441,6 +6486,11 @@ async function renderMenu() { action: actions.selectNone.create, accelerator: getShortcut("selectNone"), }, + { + text: "Preferences", + enabled: true, + action: showPreferencesDialog, + }, ], }); @@ -10231,6 +10281,100 @@ function showSavePresetDialog(container) { }); } +// Show preferences dialog +function showPreferencesDialog() { + const dialog = document.createElement('div'); + dialog.className = 'modal-overlay'; + dialog.innerHTML = ` + + `; + + document.body.appendChild(dialog); + + // Focus first input + setTimeout(() => dialog.querySelector('#pref-bpm')?.focus(), 100); + + // Handle cancel + dialog.querySelector('.btn-cancel').addEventListener('click', () => { + dialog.remove(); + }); + + // Handle save + dialog.querySelector('#preferences-form').addEventListener('submit', async (e) => { + e.preventDefault(); + + // Update config values + config.bpm = parseInt(dialog.querySelector('#pref-bpm').value); + config.framerate = parseInt(dialog.querySelector('#pref-framerate').value); + config.fileWidth = parseInt(dialog.querySelector('#pref-width').value); + config.fileHeight = parseInt(dialog.querySelector('#pref-height').value); + config.scrollSpeed = parseFloat(dialog.querySelector('#pref-scroll-speed').value); + config.reopenLastSession = dialog.querySelector('#pref-reopen-session').checked; + config.restoreLayoutFromFile = dialog.querySelector('#pref-restore-layout').checked; + config.debug = dialog.querySelector('#pref-debug').checked; + + // Save config to localStorage + await saveConfig(); + + dialog.remove(); + + console.log('Preferences saved:', config); + }); + + // Close on background click + dialog.addEventListener('click', (e) => { + if (e.target === dialog) { + dialog.remove(); + } + }); +} + // Helper function to convert MIDI note number to note name function midiToNoteName(midiNote) { const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; diff --git a/src/startscreen.js b/src/startscreen.js index f35e939..8d5ffdb 100644 --- a/src/startscreen.js +++ b/src/startscreen.js @@ -14,6 +14,7 @@ export function createStartScreen(callback) { startScreenContainer = document.createElement('div'); startScreenContainer.id = 'startScreen'; startScreenContainer.className = 'start-screen'; + startScreenContainer.style.display = 'none'; // Hidden by default // Create welcome title const title = document.createElement('h1'); @@ -170,6 +171,8 @@ function createFocusCard(focus) { export async function updateStartScreen(config) { if (!startScreenContainer) return; + console.log('[updateStartScreen] config.recentFiles:', config.recentFiles); + // Update last session const lastSessionDiv = document.getElementById('lastSessionFile'); if (lastSessionDiv) { diff --git a/src/state.js b/src/state.js index 112fb94..0e05465 100644 --- a/src/state.js +++ b/src/state.js @@ -125,9 +125,27 @@ export async function loadConfig() { // Merge loaded config with defaults Object.assign(config, deepMerge({ ...config }, loaded)); + // Ensure recentFiles is always an array (fix legacy string format) + let needsResave = false; + if (typeof config.recentFiles === 'string') { + config.recentFiles = config.recentFiles.split(',').filter(f => f.length > 0); + needsResave = true; + } else if (!Array.isArray(config.recentFiles)) { + config.recentFiles = []; + needsResave = true; + } + // Make config accessible to widgets via context context.config = config; + console.log('[loadConfig] Loaded config.recentFiles:', config.recentFiles); + + // Re-save config if we had to fix the format + if (needsResave) { + console.log('[loadConfig] Re-saving config to fix array format'); + await saveConfig(); + } + return config; } catch (error) { console.log("Error loading config, using defaults:", error); @@ -154,6 +172,7 @@ export async function addRecentFile(filePath) { filePath, ...config.recentFiles.filter(file => file !== filePath) ].slice(0, 10); + console.log('[addRecentFile] Added file, recentFiles now:', config.recentFiles); await saveConfig(); } diff --git a/src/styles.css b/src/styles.css index bb5b90d..b31d04d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1992,7 +1992,8 @@ button { font-weight: 500; } -.form-group input, +.form-group input[type="text"], +.form-group input[type="number"], .form-group textarea { width: 100%; background: #1e1e1e; @@ -2005,6 +2006,19 @@ button { box-sizing: border-box; } +.form-group input[type="checkbox"] { + width: auto; + margin-right: 8px; + cursor: pointer; +} + +.form-group label:has(input[type="checkbox"]) { + display: flex; + align-items: center; + cursor: pointer; + color: #ddd; +} + .form-group input:focus, .form-group textarea:focus { outline: none; @@ -2219,7 +2233,7 @@ button { font-size: 1.1em; padding: 12px; background: var(--surface-light); - border: 1px solid var(--border-light); + border: 1px solid var(--shadow); border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: all 0.2s; @@ -2267,7 +2281,7 @@ button { width: 180px; padding: 24px; background: var(--surface-light); - border: 2px solid var(--border-light); + border: 2px solid var(--shadow); border-radius: 12px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); cursor: pointer; @@ -2290,7 +2304,7 @@ button { display: flex; align-items: center; justify-content: center; - border: 3px solid var(--button-hover); + border: 3px solid var(--shadow); border-radius: 8px; background: var(--surface); }