From c6aca219a6f639a510518d3bfc8947342d0d870f Mon Sep 17 00:00:00 2001 From: Jacob Brooks Date: Fri, 10 Apr 2026 10:10:57 -0400 Subject: [PATCH 1/2] feat: save/load project state (.trp files) - File menu in Electron with Save/Open (Ctrl+S/O) - Browser fallback via download/file-input - Serializes both panels: images, polygons, textures, atlas settings - Fix polygon vertex reorder on load (skipReorder parameter) Co-Authored-By: Claude Opus 4.6 (1M context) --- index.html | 1 + main.js | 38 ++++++++++++- scripts/leftPanelManager.js | 102 +++++++++++++++++++++++++++++++++++ scripts/main.js | 11 ++++ scripts/polygonManager.js | 23 +++++++- scripts/rightPanelManager.js | 58 ++++++++++++++++++++ scripts/saveManager.js | 74 +++++++++++++++++++++++++ scripts/shortcutsManager.js | 12 +++++ 8 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 scripts/saveManager.js diff --git a/index.html b/index.html index 39bc353..965f024 100644 --- a/index.html +++ b/index.html @@ -82,6 +82,7 @@ + diff --git a/main.js b/main.js index e96e7d7..c0cba90 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,5 @@ -const { app, BrowserWindow, dialog, ipcMain } = require('electron'); +const { app, BrowserWindow, dialog, ipcMain, Menu } = require('electron'); +const fs = require('fs'); const { autoUpdater } = require('electron-updater'); const path = require('path'); @@ -17,6 +18,22 @@ function createWindow() { mainWindow.maximize(); mainWindow.loadFile('index.html'); + const menuTemplate = [ + { + label: 'File', + submenu: [ + { label: 'Save Project', accelerator: 'CmdOrCtrl+S', click: () => mainWindow.webContents.send('menu-save-project') }, + { label: 'Open Project', accelerator: 'CmdOrCtrl+O', click: () => mainWindow.webContents.send('menu-open-project') }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + { role: 'editMenu' }, + { role: 'viewMenu' }, + { role: 'windowMenu' } + ]; + Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate)); + // Check for updates after window is ready mainWindow.webContents.once('did-finish-load', () => { console.log('Window loaded, checking for updates...'); @@ -107,6 +124,25 @@ ipcMain.handle('get-app-version', () => { return app.getVersion(); }); +ipcMain.handle('save-project', async (event, jsonData) => { + const { filePath } = await dialog.showSaveDialog(mainWindow, { + title: 'Save Project', defaultPath: 'project.trp', + filters: [{ name: 'Texture Ripper Project', extensions: ['trp'] }] + }); + if (filePath) { fs.writeFileSync(filePath, jsonData, 'utf8'); return true; } + return false; +}); + +ipcMain.handle('open-project', async () => { + const { filePaths } = await dialog.showOpenDialog(mainWindow, { + title: 'Open Project', + filters: [{ name: 'Texture Ripper Project', extensions: ['trp'] }], + properties: ['openFile'] + }); + if (filePaths && filePaths.length > 0) return fs.readFileSync(filePaths[0], 'utf8'); + return null; +}); + app.on('ready', createWindow); app.on('window-all-closed', () => { diff --git a/scripts/leftPanelManager.js b/scripts/leftPanelManager.js index a666e4c..b0b7529 100644 --- a/scripts/leftPanelManager.js +++ b/scripts/leftPanelManager.js @@ -358,6 +358,108 @@ const LeftPanelManager = { PanZoomManager.initPanning(stage); PanZoomManager.initZooming(stage); + // Save/Load API + window.leftPanel = { + getState: () => { + const images = bgImages.map(img => ({ + dataURL: SaveManager.imageToDataURL(img), + x: img.x(), + y: img.y(), + width: img.width(), + height: img.height(), + scaleX: img.scaleX(), + scaleY: img.scaleY(), + rotation: img.rotation() + })); + + const polygons = []; + polygonLayer.find('.group').forEach(group => { + const verts = group.vertices.map(v => ({ x: v.x, y: v.y })); + const mids = group.midpoints.map(m => ({ x: m.x, y: m.y, locked: m.locked })); + polygons.push({ + id: group._id, + x: group.x(), + y: group.y(), + vertices: verts, + midpoints: mids + }); + }); + + return { images, polygons }; + }, + + loadState: (state) => { + // Clear existing + bgImages.forEach(img => img.destroy()); + bgImages.length = 0; + polygonLayer.find('.group').forEach(g => g.destroy()); + bgLayer.batchDraw(); + polygonLayer.batchDraw(); + + // Restore images + if (state.images) { + state.images.forEach(imgData => { + const img = new Image(); + img.onload = () => { + const konvaImg = new Konva.Image({ + x: imgData.x, + y: imgData.y, + image: img, + width: imgData.width, + height: imgData.height, + scaleX: imgData.scaleX || 1, + scaleY: imgData.scaleY || 1, + rotation: imgData.rotation || 0, + draggable: !imagesLocked + }); + bgLayer.add(konvaImg); + bgImages.push(konvaImg); + bgLayer.batchDraw(); + }; + img.src = imgData.dataURL; + }); + } + + // Restore polygons + if (state.polygons) { + state.polygons.forEach(polyData => { + const group = PolygonManager.createPolygonGroup( + stage, polygonLayer, polyData.vertices, dirtyPolygons, true + ); + group.position({ x: polyData.x || 0, y: polyData.y || 0 }); + + // Restore midpoints + if (polyData.midpoints) { + polyData.midpoints.forEach((m, i) => { + if (group.midpoints[i]) { + group.midpoints[i].x = m.x; + group.midpoints[i].y = m.y; + group.midpoints[i].locked = m.locked || false; + } + }); + // Update visual midpoint positions + group.find('.midpoint').forEach((mp, i) => { + if (group.midpoints[i]) { + mp.position({ x: group.midpoints[i].x, y: group.midpoints[i].y }); + } + }); + // Redraw polygon and grid with restored midpoints + PolygonManager.drawCurvedPolygon(group, group.vertices, group.midpoints); + GridManager.drawGrid(group, group.vertices, group.midpoints); + const updatedPoints = PolygonManager.computeDragSurfacePoints(group.vertices, group.midpoints); + PolygonManager.updateDragSurface(group, updatedPoints); + } + + // Reassign ID if saved + if (polyData.id) group._id = polyData.id; + + dirtyPolygons.add(group._id); + }); + polygonLayer.batchDraw(); + } + } + }; + return stage; } }; \ No newline at end of file diff --git a/scripts/main.js b/scripts/main.js index bcaac40..773af77 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -38,6 +38,10 @@ document.addEventListener('DOMContentLoaded', () => { RightPanelManager.autoPackTextures(stageRight, false); }); + // Save/Load project functions + window.saveProject = () => SaveManager.save(stageLeft, stageRight); + window.loadProject = () => SaveManager.load(stageLeft, stageRight); + // Export button document.getElementById('exportRight').addEventListener('click', () => { const exportWidth = parseInt(document.getElementById('rightWidth').value); @@ -138,6 +142,13 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('requestFeature').addEventListener('click', () => { openExternalURL('https://github.com/raycastly/texture-ripper/issues/new?template=feature_request.yml'); }); + + // Listen for Electron menu events (File > Save/Open) + if (isElectron()) { + const { ipcRenderer } = require('electron'); + ipcRenderer.on('menu-save-project', () => window.saveProject()); + ipcRenderer.on('menu-open-project', () => window.loadProject()); + } }); diff --git a/scripts/polygonManager.js b/scripts/polygonManager.js index aa56e29..cba26ac 100644 --- a/scripts/polygonManager.js +++ b/scripts/polygonManager.js @@ -1,7 +1,26 @@ // ==================== POLYGON MANAGEMENT ==================== const PolygonManager = { // Unified polygon creation function - createPolygonGroup: (stage, layer, points = null, dirtyPolygons = null) => { + computeDragSurfacePoints: (vertices, midpoints) => { + const points = []; + for (let i = 0; i < vertices.length; i++) { + const nextIdx = (i + 1) % vertices.length; + const P0 = vertices[i]; + const P2 = vertices[nextIdx]; + const M = midpoints[i]; + const numSamples = 10; + for (let j = 0; j <= numSamples; j++) { + const t = j / numSamples; + const mt = 1 - t; + const x = mt*mt*P0.x + 2*mt*t*M.x + t*t*P2.x; + const y = mt*mt*P0.y + 2*mt*t*M.y + t*t*P2.y; + points.push(x, y); + } + } + return points; + }, + + createPolygonGroup: (stage, layer, points = null, dirtyPolygons = null, skipReorder = false) => { const group = new Konva.Group({ draggable: true, name: 'group', @@ -15,7 +34,7 @@ const PolygonManager = { let vertices; if (points && points.length === 4) { // REORDER vertices to ensure consistent order for both polygon types - vertices = Utils.reorderPolygonVertices(points); + vertices = skipReorder ? points.map(p => ({x: p.x, y: p.y})) : Utils.reorderPolygonVertices(points); } else { // Create default rectangle centered on stage const stageCenterX = stage.width() / 2; diff --git a/scripts/rightPanelManager.js b/scripts/rightPanelManager.js index d9c8cff..f1f9837 100644 --- a/scripts/rightPanelManager.js +++ b/scripts/rightPanelManager.js @@ -367,6 +367,64 @@ const RightPanelManager = { } }, + getState: () => { + const textures = []; + Object.keys(tiedRects).forEach(groupId => { + const img = tiedRects[groupId]; + if (img) { + textures.push({ + groupId: groupId, + dataURL: SaveManager.imageToDataURL(img), + x: img.x(), + y: img.y(), + width: img.width(), + height: img.height(), + scaleX: img.scaleX(), + scaleY: img.scaleY(), + rotation: img.rotation() + }); + } + }); + return { textures }; + }, + + loadState: (state) => { + // Clear existing textures + Object.keys(tiedRects).forEach(id => { + if (tiedRects[id]) tiedRects[id].destroy(); + delete tiedRects[id]; + }); + imageLayer.batchDraw(); + + if (state.textures) { + state.textures.forEach(texData => { + const img = new Image(); + img.onload = () => { + const konvaImg = new Konva.Image({ + x: texData.x, + y: texData.y, + image: img, + width: texData.width, + height: texData.height, + scaleX: texData.scaleX || 1, + scaleY: texData.scaleY || 1, + rotation: texData.rotation || 0, + id: `rect_${texData.groupId}`, + draggable: true + }); + + konvaImg.on('dragmove', snapping.handleDragging); + konvaImg.on('dragend', snapping.handleDragEnd); + + imageLayer.add(konvaImg); + tiedRects[texData.groupId] = konvaImg; + imageLayer.batchDraw(); + }; + img.src = texData.dataURL; + }); + } + }, + updateBackground: () => { const width = parseInt(document.getElementById('rightWidth').value); const height = parseInt(document.getElementById('rightHeight').value); diff --git a/scripts/saveManager.js b/scripts/saveManager.js new file mode 100644 index 0000000..97301ab --- /dev/null +++ b/scripts/saveManager.js @@ -0,0 +1,74 @@ +// ==================== SAVE/LOAD MANAGER ==================== +const SaveManager = { + imageToDataURL(konvaImg) { + const canvas = document.createElement('canvas'); + const htmlImg = konvaImg.image(); + canvas.width = htmlImg.naturalWidth || htmlImg.width; + canvas.height = htmlImg.naturalHeight || htmlImg.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(htmlImg, 0, 0); + return canvas.toDataURL('image/png'); + }, + + save(stageLeft, stageRight) { + const state = { + version: 1, + atlas: { + width: parseInt(document.getElementById('rightWidth').value), + height: parseInt(document.getElementById('rightHeight').value), + transparent: document.getElementById('exportTransparent').checked + }, + leftPanel: window.leftPanel ? window.leftPanel.getState() : { images: [], polygons: [] }, + rightPanel: window.rightPanel && window.rightPanel.getState ? window.rightPanel.getState() : { textures: [] } + }; + const json = JSON.stringify(state); + if (isElectron()) { + const { ipcRenderer } = require('electron'); + ipcRenderer.invoke('save-project', json).then(saved => { + if (saved) FeedbackManager.show('Project saved!'); + }); + } else { + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = 'project.trp'; a.click(); + URL.revokeObjectURL(url); + FeedbackManager.show('Project saved!'); + } + }, + + load(stageLeft, stageRight) { + const self = this; + function handleData(json) { + try { self.deserialize(JSON.parse(json), stageLeft, stageRight); } + catch (e) { FeedbackManager.show('Failed to load project'); console.error('Load error:', e); } + } + if (isElectron()) { + const { ipcRenderer } = require('electron'); + ipcRenderer.invoke('open-project').then(data => { if (data) handleData(data); }); + } else { + const input = document.createElement('input'); + input.type = 'file'; input.accept = '.trp,.json'; + input.onchange = (e) => { + const file = e.target.files[0]; if (!file) return; + const reader = new FileReader(); + reader.onload = (evt) => handleData(evt.target.result); + reader.readAsText(file); + }; + input.click(); + } + }, + + deserialize(state, stageLeft, stageRight) { + if (state.version !== 1) { FeedbackManager.show('Unsupported project file version'); return; } + document.getElementById('rightWidth').value = state.atlas.width; + document.getElementById('rightHeight').value = state.atlas.height; + document.getElementById('exportTransparent').checked = state.atlas.transparent; + stageRight.bgRect.width(state.atlas.width); + stageRight.bgRect.height(state.atlas.height); + RightPanelManager.toggleTransparency(stageRight, state.atlas.transparent); + if (window.leftPanel && window.leftPanel.loadState) window.leftPanel.loadState(state.leftPanel); + if (window.rightPanel && window.rightPanel.loadState) window.rightPanel.loadState(state.rightPanel); + FeedbackManager.show('Project loaded!'); + } +}; diff --git a/scripts/shortcutsManager.js b/scripts/shortcutsManager.js index c9c39c6..6177371 100644 --- a/scripts/shortcutsManager.js +++ b/scripts/shortcutsManager.js @@ -1,5 +1,17 @@ document.addEventListener('keydown', (e) => { + // Save/Load project + if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') { + e.preventDefault(); + if (window.saveProject) window.saveProject(); + return; + } + if ((e.ctrlKey || e.metaKey) && e.code === 'KeyO') { + e.preventDefault(); + if (window.loadProject) window.loadProject(); + return; + } + // Drawing mode toggle if (e.code === CONFIG.SHORTCUTS.toggleDrawingMode) { document.getElementById('toggleDrawingMode').click(); From d6824767d43067dcd9c92810b571cb8c4ae56e40 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 10 Apr 2026 15:54:53 -0400 Subject: [PATCH 2/2] fix: resolve save/load failures with large projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move file I/O from main process IPC to renderer process (fs module) to avoid Electron IPC size limits on large payloads - Show dialog first, then serialize — prevents UI hang before dialog - Use JPEG for source images in save files to avoid V8 string length limit (RangeError) when serializing many full-resolution PNGs - Keep PNG for extracted textures (transparency support) - Add fallback to naturalWidth/naturalHeight when loading textures without explicit dimensions - Add spinner modal overlay during save/load operations - Add error handling with user-visible feedback on failure Co-Authored-By: Claude Opus 4.6 (1M context) --- main.js | 9 ++--- scripts/feedbackManager.js | 73 +++++++++++++++++++++++++++++++++++- scripts/leftPanelManager.js | 2 +- scripts/rightPanelManager.js | 12 ++++-- scripts/saveManager.js | 71 ++++++++++++++++++++++++++++------- 5 files changed, 142 insertions(+), 25 deletions(-) diff --git a/main.js b/main.js index c0cba90..985eddb 100644 --- a/main.js +++ b/main.js @@ -124,22 +124,21 @@ ipcMain.handle('get-app-version', () => { return app.getVersion(); }); -ipcMain.handle('save-project', async (event, jsonData) => { +ipcMain.handle('save-project-dialog', async () => { const { filePath } = await dialog.showSaveDialog(mainWindow, { title: 'Save Project', defaultPath: 'project.trp', filters: [{ name: 'Texture Ripper Project', extensions: ['trp'] }] }); - if (filePath) { fs.writeFileSync(filePath, jsonData, 'utf8'); return true; } - return false; + return filePath || null; }); -ipcMain.handle('open-project', async () => { +ipcMain.handle('open-project-dialog', async () => { const { filePaths } = await dialog.showOpenDialog(mainWindow, { title: 'Open Project', filters: [{ name: 'Texture Ripper Project', extensions: ['trp'] }], properties: ['openFile'] }); - if (filePaths && filePaths.length > 0) return fs.readFileSync(filePaths[0], 'utf8'); + if (filePaths && filePaths.length > 0) return filePaths[0]; return null; }); diff --git a/scripts/feedbackManager.js b/scripts/feedbackManager.js index f771f46..24231f9 100644 --- a/scripts/feedbackManager.js +++ b/scripts/feedbackManager.js @@ -1,5 +1,6 @@ const FeedbackManager = (() => { let feedbackEl = null; + let spinnerOverlay = null; function init() { if (!feedbackEl) { @@ -15,7 +16,7 @@ const FeedbackManager = (() => { background: 'rgba(0,0,0,0.85)', color: '#fff', borderRadius: '6px', - zIndex: 9999, + zIndex: 10000, fontFamily: 'sans-serif', fontSize: '16px', pointerEvents: 'none', @@ -29,6 +30,62 @@ const FeedbackManager = (() => { } } + function initSpinner() { + if (!spinnerOverlay) { + spinnerOverlay = document.createElement('div'); + Object.assign(spinnerOverlay.style, { + position: 'fixed', + top: '0', left: '0', width: '100%', height: '100%', + background: 'rgba(0,0,0,0.5)', + display: 'none', + justifyContent: 'center', + alignItems: 'center', + zIndex: 9999 + }); + + const box = document.createElement('div'); + Object.assign(box.style, { + background: 'rgba(0,0,0,0.85)', + borderRadius: '8px', + padding: '24px 32px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '12px' + }); + + const spinner = document.createElement('div'); + Object.assign(spinner.style, { + width: '32px', height: '32px', + border: '3px solid rgba(255,255,255,0.2)', + borderTop: '3px solid #fff', + borderRadius: '50%', + animation: 'feedback-spin 0.8s linear infinite' + }); + + const label = document.createElement('div'); + label.className = 'spinner-label'; + Object.assign(label.style, { + color: '#fff', + fontFamily: 'sans-serif', + fontSize: '14px' + }); + + // Add keyframes + if (!document.getElementById('feedback-spin-style')) { + const style = document.createElement('style'); + style.id = 'feedback-spin-style'; + style.textContent = '@keyframes feedback-spin { to { transform: rotate(360deg); } }'; + document.head.appendChild(style); + } + + box.appendChild(spinner); + box.appendChild(label); + spinnerOverlay.appendChild(box); + document.body.appendChild(spinnerOverlay); + } + } + function show(message, options = {}) { init(); @@ -47,5 +104,17 @@ const FeedbackManager = (() => { }, duration); } - return { show }; + function showSpinner(message = 'Please wait...') { + initSpinner(); + spinnerOverlay.querySelector('.spinner-label').textContent = message; + spinnerOverlay.style.display = 'flex'; + } + + function hideSpinner() { + if (spinnerOverlay) { + spinnerOverlay.style.display = 'none'; + } + } + + return { show, showSpinner, hideSpinner }; })(); diff --git a/scripts/leftPanelManager.js b/scripts/leftPanelManager.js index b0b7529..03d2aa2 100644 --- a/scripts/leftPanelManager.js +++ b/scripts/leftPanelManager.js @@ -362,7 +362,7 @@ const LeftPanelManager = { window.leftPanel = { getState: () => { const images = bgImages.map(img => ({ - dataURL: SaveManager.imageToDataURL(img), + dataURL: SaveManager.imageToDataURL(img, 'image/jpeg', 0.92), x: img.x(), y: img.y(), width: img.width(), diff --git a/scripts/rightPanelManager.js b/scripts/rightPanelManager.js index f1f9837..9f0b158 100644 --- a/scripts/rightPanelManager.js +++ b/scripts/rightPanelManager.js @@ -396,16 +396,19 @@ const RightPanelManager = { }); imageLayer.batchDraw(); + console.log(`Loading ${state.textures ? state.textures.length : 0} textures`); if (state.textures) { - state.textures.forEach(texData => { + state.textures.forEach((texData, i) => { + console.log(`Texture ${i}: groupId=${texData.groupId}, hasDataURL=${!!texData.dataURL}, dataURLLen=${texData.dataURL ? texData.dataURL.length : 0}, w=${texData.width}, h=${texData.height}`); const img = new Image(); img.onload = () => { + console.log(`Texture ${i} image loaded: ${img.naturalWidth}x${img.naturalHeight}`); const konvaImg = new Konva.Image({ x: texData.x, y: texData.y, image: img, - width: texData.width, - height: texData.height, + width: texData.width || img.naturalWidth, + height: texData.height || img.naturalHeight, scaleX: texData.scaleX || 1, scaleY: texData.scaleY || 1, rotation: texData.rotation || 0, @@ -420,6 +423,9 @@ const RightPanelManager = { tiedRects[texData.groupId] = konvaImg; imageLayer.batchDraw(); }; + img.onerror = (err) => { + console.error(`Texture ${i} failed to load:`, err); + }; img.src = texData.dataURL; }); } diff --git a/scripts/saveManager.js b/scripts/saveManager.js index 97301ab..190f2dd 100644 --- a/scripts/saveManager.js +++ b/scripts/saveManager.js @@ -1,17 +1,17 @@ // ==================== SAVE/LOAD MANAGER ==================== const SaveManager = { - imageToDataURL(konvaImg) { + imageToDataURL(konvaImg, format = 'image/png', quality = 0.92) { const canvas = document.createElement('canvas'); const htmlImg = konvaImg.image(); canvas.width = htmlImg.naturalWidth || htmlImg.width; canvas.height = htmlImg.naturalHeight || htmlImg.height; const ctx = canvas.getContext('2d'); ctx.drawImage(htmlImg, 0, 0); - return canvas.toDataURL('image/png'); + return canvas.toDataURL(format, quality); }, - save(stageLeft, stageRight) { - const state = { + _buildState(stageLeft, stageRight) { + return { version: 1, atlas: { width: parseInt(document.getElementById('rightWidth').value), @@ -21,13 +21,33 @@ const SaveManager = { leftPanel: window.leftPanel ? window.leftPanel.getState() : { images: [], polygons: [] }, rightPanel: window.rightPanel && window.rightPanel.getState ? window.rightPanel.getState() : { textures: [] } }; - const json = JSON.stringify(state); + }, + + save(stageLeft, stageRight) { if (isElectron()) { const { ipcRenderer } = require('electron'); - ipcRenderer.invoke('save-project', json).then(saved => { - if (saved) FeedbackManager.show('Project saved!'); + const fs = require('fs'); + ipcRenderer.invoke('save-project-dialog').then(filePath => { + if (filePath) { + FeedbackManager.showSpinner('Saving project...'); + // Defer to let the spinner render before heavy sync work + setTimeout(() => { + try { + const state = this._buildState(stageLeft, stageRight); + const json = JSON.stringify(state); + fs.writeFileSync(filePath, json, 'utf8'); + FeedbackManager.hideSpinner(); + FeedbackManager.show('Project saved!'); + } catch (err) { + FeedbackManager.hideSpinner(); + FeedbackManager.show('Failed to save project', { bgColor: 'rgba(180,0,0,0.85)' }); + console.error('Save error:', err); + } + }, 50); + } }); } else { + const json = JSON.stringify(this._buildState(stageLeft, stageRight)); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -39,20 +59,44 @@ const SaveManager = { load(stageLeft, stageRight) { const self = this; - function handleData(json) { - try { self.deserialize(JSON.parse(json), stageLeft, stageRight); } - catch (e) { FeedbackManager.show('Failed to load project'); console.error('Load error:', e); } - } if (isElectron()) { const { ipcRenderer } = require('electron'); - ipcRenderer.invoke('open-project').then(data => { if (data) handleData(data); }); + const fs = require('fs'); + ipcRenderer.invoke('open-project-dialog').then(filePath => { + if (filePath) { + FeedbackManager.showSpinner('Loading project...'); + setTimeout(() => { + try { + const json = fs.readFileSync(filePath, 'utf8'); + self.deserialize(JSON.parse(json), stageLeft, stageRight); + FeedbackManager.hideSpinner(); + FeedbackManager.show('Project loaded!'); + } catch (e) { + FeedbackManager.hideSpinner(); + FeedbackManager.show('Failed to load project', { bgColor: 'rgba(180,0,0,0.85)' }); + console.error('Load error:', e); + } + }, 50); + } + }); } else { const input = document.createElement('input'); input.type = 'file'; input.accept = '.trp,.json'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; + FeedbackManager.showSpinner('Loading project...'); const reader = new FileReader(); - reader.onload = (evt) => handleData(evt.target.result); + reader.onload = (evt) => { + try { + self.deserialize(JSON.parse(evt.target.result), stageLeft, stageRight); + FeedbackManager.hideSpinner(); + FeedbackManager.show('Project loaded!'); + } catch (e) { + FeedbackManager.hideSpinner(); + FeedbackManager.show('Failed to load project', { bgColor: 'rgba(180,0,0,0.85)' }); + console.error('Load error:', e); + } + }; reader.readAsText(file); }; input.click(); @@ -69,6 +113,5 @@ const SaveManager = { RightPanelManager.toggleTransparency(stageRight, state.atlas.transparent); if (window.leftPanel && window.leftPanel.loadState) window.leftPanel.loadState(state.leftPanel); if (window.rightPanel && window.rightPanel.loadState) window.rightPanel.loadState(state.rightPanel); - FeedbackManager.show('Project loaded!'); } };