From 01d833f5206dd0a210b110d949743ca062a61814 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Wed, 4 Mar 2026 17:44:04 +0530 Subject: [PATCH] fix: resolve editor, batch conversion, and startup performance issues - Remove popout preview button (HTML, JS, CSS) - Fix Save/Save As flow for new untitled files - Fix batch conversion menu items (wire show-batch-converter IPC) - Add universal-convert-batch IPC handler for batch file conversion - Lazy-load mermaid, pdfjs-dist, sidebar panels, command palette - Switch highlight.js CSS from CDN to local - Defer CodeMirror language extensions until first use - Add show:false + ready-to-show for faster perceived startup - Install mermaid as local dependency (remove CDN script tag) --- .gitignore | 2 +- package.json | 1 + src/editor/codemirror-setup.js | 27 ++- src/index.html | 7 +- src/main.js | 91 ++++++++++ src/preload.js | 3 + src/renderer.js | 317 ++++++++------------------------- src/styles.css | 28 --- 8 files changed, 188 insertions(+), 288 deletions(-) diff --git a/.gitignore b/.gitignore index b47f6b3..d142a98 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ pdf\ modal.png # Claude/AI development files .claude/ CLAUDE.md -agents.md \ No newline at end of file +agents.mdcoverage/ diff --git a/package.json b/package.json index 6056921..624872c 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "marked": "^17.0.3", "marked-footnote": "^1.4.0", "marked-highlight": "^2.2.3", + "mermaid": "^11.12.3", "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.5.207", "pdfkit": "^0.17.2", diff --git a/src/editor/codemirror-setup.js b/src/editor/codemirror-setup.js index cf4084f..8e25508 100644 --- a/src/editor/codemirror-setup.js +++ b/src/editor/codemirror-setup.js @@ -10,11 +10,8 @@ const { } = require('@codemirror/view'); const { EditorState } = require('@codemirror/state'); const { markdown, markdownLanguage } = require('@codemirror/lang-markdown'); -const { javascript } = require('@codemirror/lang-javascript'); -const { html } = require('@codemirror/lang-html'); -const { css } = require('@codemirror/lang-css'); -const { json } = require('@codemirror/lang-json'); -const { python } = require('@codemirror/lang-python'); +// Language extensions loaded lazily on first use +let _javascript, _html, _css, _json, _python; const { defaultKeymap, history, @@ -108,19 +105,19 @@ function createEditor(parentElement, options = {}) { * @returns {Extension} CodeMirror language extension */ function getLanguageExtension(lang) { - const languages = { - javascript, - js: javascript, - html, - css, - json, - python, - py: python, + const loaders = { + javascript: () => { if (!_javascript) _javascript = require('@codemirror/lang-javascript').javascript; return _javascript(); }, + html: () => { if (!_html) _html = require('@codemirror/lang-html').html; return _html(); }, + css: () => { if (!_css) _css = require('@codemirror/lang-css').css; return _css(); }, + json: () => { if (!_json) _json = require('@codemirror/lang-json').json; return _json(); }, + python: () => { if (!_python) _python = require('@codemirror/lang-python').python; return _python(); }, markdown: () => markdown({ base: markdownLanguage }), }; + loaders.js = loaders.javascript; + loaders.py = loaders.python; - const factory = languages[lang]; - return factory ? factory() : markdown({ base: markdownLanguage }); + const loader = loaders[lang]; + return loader ? loader() : markdown({ base: markdownLanguage }); } module.exports = { createEditor, getLanguageExtension }; diff --git a/src/index.html b/src/index.html index f0ea436..8669518 100644 --- a/src/index.html +++ b/src/index.html @@ -11,9 +11,7 @@ - - - +
@@ -1433,9 +1431,6 @@
-
- -
diff --git a/src/main.js b/src/main.js index 5f3ea9c..bf109ca 100644 --- a/src/main.js +++ b/src/main.js @@ -324,6 +324,7 @@ function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, + show: false, webPreferences: { nodeIntegration: true, contextIsolation: false, @@ -334,6 +335,11 @@ function createWindow() { mainWindow.loadFile(path.join(__dirname, 'index.html')); + // Show window only after content is ready — avoids blank flash + mainWindow.once('ready-to-show', () => { + mainWindow.show(); + }); + createMenu(); mainWindow.on('closed', () => { @@ -1821,6 +1827,91 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file } }); +// Handle universal batch file conversion +ipcMain.on('universal-convert-batch', async (event, { tool, fromFormat, toFormat, inputFolder, outputFolder, includeSubfolders, advancedOptions }) => { + if (!conversionLimiter()) { + mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); + return; + } + try { + const toolAvailable = await checkConverterAvailable(tool); + if (!toolAvailable) { + throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); + } + + // Collect matching files + const files = []; + function collectFiles(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory() && includeSubfolders) { + collectFiles(fullPath); + } else if (entry.isFile() && entry.name.toLowerCase().endsWith(`.${fromFormat}`)) { + files.push(fullPath); + } + } + } + collectFiles(inputFolder); + + if (files.length === 0) { + mainWindow.webContents.send('conversion-complete', { success: false, error: `No .${fromFormat} files found in the selected folder.` }); + return; + } + + let completed = 0; + let failed = 0; + + for (const filePath of files) { + const relativePath = path.relative(inputFolder, filePath); + const outputPath = path.join(outputFolder, relativePath.replace(/\.[^/.]+$/, `.${toFormat}`)); + + // Ensure output subdirectory exists + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + + mainWindow.webContents.send('conversion-status', `Converting ${completed + 1}/${files.length}: ${path.basename(filePath)}`); + + let conversionInfo; + switch (tool) { + case 'libreoffice': conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); break; + case 'imagemagick': conversionInfo = convertWithImageMagick(filePath, outputPath); break; + case 'ffmpeg': conversionInfo = convertWithFFmpeg(filePath, outputPath); break; + case 'pandoc': conversionInfo = convertWithPandoc(filePath, outputPath); break; + default: throw new Error(`Unknown conversion tool: ${tool}`); + } + + await new Promise((resolve) => { + execFile(conversionInfo.command, conversionInfo.args, (error) => { + if (error) { failed++; } else { completed++; } + resolve(); + }); + }); + } + + mainWindow.webContents.send('conversion-complete', { + success: true, + outputPath: outputFolder + }); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Batch Conversion Complete', + message: `Batch conversion finished!`, + detail: `Converted: ${completed}/${files.length} files${failed > 0 ? ` (${failed} failed)` : ''}\nOutput: ${outputFolder}`, + buttons: ['OK'] + }); + } catch (error) { + mainWindow.webContents.send('conversion-complete', { success: false, error: error.message }); + dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Batch Conversion Failed', + message: 'Batch conversion failed', + detail: error.message, + buttons: ['OK'] + }); + } +}); + // LibreOffice conversion - returns {command, args} for execFile (safer than exec) function convertWithLibreOffice(inputFile, outputFormat, outputPath) { const outputDir = path.dirname(outputPath); diff --git a/src/preload.js b/src/preload.js index 9eed3e9..4087d8e 100644 --- a/src/preload.js +++ b/src/preload.js @@ -215,6 +215,9 @@ const ALLOWED_RECEIVE_CHANNELS = [ // Insert content from generator windows 'insert-content', + // Batch converter + 'show-batch-converter', + // v4 menu-triggered events 'load-template-menu', 'toggle-command-palette', diff --git a/src/renderer.js b/src/renderer.js index 5e1f1c6..833b8b5 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -10,15 +10,19 @@ const DOMPurify = require('dompurify'); const hljs = require('highlight.js'); const { createEditor } = require('./editor/codemirror-setup'); const { undo, redo } = require('@codemirror/commands'); -const { SidebarManager } = require('./sidebar/sidebar-manager'); -const { renderTemplatesPanel } = require('./sidebar/templates-panel'); -const { renderExplorerPanel } = require('./sidebar/explorer-panel'); -const { renderGitPanel } = require('./sidebar/git-panel'); -const { renderSnippetsPanel } = require('./sidebar/snippets-panel'); -const { ReplPanel } = require('./repl/repl-panel'); -const { CommandPalette } = require('./command-palette'); -const { PrintPreview } = require('./print-preview'); -const { createWelcomeContent } = require('./welcome'); +// Lazy-loaded modules — defer heavy imports until first use +let _SidebarManager, _renderTemplatesPanel, _renderExplorerPanel, _renderGitPanel, _renderSnippetsPanel; +let _ReplPanel, _CommandPalette, _PrintPreview, _createWelcomeContent; + +function getSidebarManager() { if (!_SidebarManager) _SidebarManager = require('./sidebar/sidebar-manager').SidebarManager; return _SidebarManager; } +function getRenderTemplatesPanel() { if (!_renderTemplatesPanel) _renderTemplatesPanel = require('./sidebar/templates-panel').renderTemplatesPanel; return _renderTemplatesPanel; } +function getRenderExplorerPanel() { if (!_renderExplorerPanel) _renderExplorerPanel = require('./sidebar/explorer-panel').renderExplorerPanel; return _renderExplorerPanel; } +function getRenderGitPanel() { if (!_renderGitPanel) _renderGitPanel = require('./sidebar/git-panel').renderGitPanel; return _renderGitPanel; } +function getRenderSnippetsPanel() { if (!_renderSnippetsPanel) _renderSnippetsPanel = require('./sidebar/snippets-panel').renderSnippetsPanel; return _renderSnippetsPanel; } +function getReplPanel() { if (!_ReplPanel) _ReplPanel = require('./repl/repl-panel').ReplPanel; return _ReplPanel; } +function getCommandPalette() { if (!_CommandPalette) _CommandPalette = require('./command-palette').CommandPalette; return _CommandPalette; } +function getPrintPreview() { if (!_PrintPreview) _PrintPreview = require('./print-preview').PrintPreview; return _PrintPreview; } +function getCreateWelcomeContent() { if (!_createWelcomeContent) _createWelcomeContent = require('./welcome').createWelcomeContent; return _createWelcomeContent; } // Configure marked with highlight extension marked.use(markedHighlight({ @@ -192,9 +196,6 @@ class TabManager {
-
- -
`; @@ -433,38 +434,29 @@ class TabManager { } } - // Render Mermaid diagrams if Mermaid is available - if (window.mermaid) { + // Render Mermaid diagrams — lazy-load mermaid only when needed + const mermaidBlocks = preview.querySelectorAll('pre code.language-mermaid'); + if (mermaidBlocks.length > 0) { try { - // Find all code blocks with language-mermaid class - const mermaidBlocks = preview.querySelectorAll('pre code.language-mermaid'); - mermaidBlocks.forEach((block, index) => { + // Lazy-load mermaid on first use + if (!window.mermaid) { + const mermaidModule = require('mermaid'); + window.mermaid = mermaidModule.default || mermaidModule; + } + + mermaidBlocks.forEach((block) => { const code = block.textContent; const pre = block.parentElement; - - // Create a div for mermaid rendering const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.setAttribute('data-processed', 'true'); mermaidDiv.textContent = code; - - // Replace the pre element with the mermaid div pre.parentElement.replaceChild(mermaidDiv, pre); }); - // Initialize Mermaid with dark theme support const theme = document.body.className.includes('theme-dark') ? 'dark' : 'default'; - mermaid.initialize({ - startOnLoad: false, - theme: theme, - securityLevel: 'loose' - }); - - // Render all mermaid diagrams - mermaid.run({ - querySelector: '.mermaid:not([data-rendered])' - }).then(() => { - // Mark as rendered + window.mermaid.initialize({ startOnLoad: false, theme, securityLevel: 'loose' }); + window.mermaid.run({ querySelector: '.mermaid:not([data-rendered])' }).then(() => { preview.querySelectorAll('.mermaid').forEach(el => { el.setAttribute('data-rendered', 'true'); }); @@ -1147,15 +1139,17 @@ let replPanel; document.addEventListener('DOMContentLoaded', () => { tabManager = new TabManager(); + const ReplPanel = getReplPanel(); replPanel = new ReplPanel(); // Initialize sidebar + const SidebarManager = getSidebarManager(); const sidebarManager = new SidebarManager(); let explorerCurrentDir = null; sidebarManager.registerPanel('explorer', { title: 'Explorer', - render: (container) => renderExplorerPanel(container, { + render: (container) => getRenderExplorerPanel()(container, { listDirectory: (dir) => ipcRenderer.invoke('list-directory', dir), onFileOpen: (filePath) => ipcRenderer.send('open-file-path', filePath), currentDir: explorerCurrentDir, @@ -1163,7 +1157,7 @@ document.addEventListener('DOMContentLoaded', () => { }); sidebarManager.registerPanel('git', { title: 'Git', - render: (container) => renderGitPanel(container, { + render: (container) => getRenderGitPanel()(container, { gitStatus: () => ipcRenderer.invoke('git-status'), gitDiff: (file) => ipcRenderer.invoke('git-diff', { file }), gitStage: (files) => ipcRenderer.invoke('git-stage', { files }), @@ -1173,7 +1167,7 @@ document.addEventListener('DOMContentLoaded', () => { }); sidebarManager.registerPanel('snippets', { title: 'Snippets', - render: (container) => renderSnippetsPanel(container, { + render: (container) => getRenderSnippetsPanel()(container, { getSnippets: () => ipcRenderer.invoke('get-snippets'), saveSnippet: (s) => ipcRenderer.invoke('save-snippet', s), deleteSnippet: (id) => ipcRenderer.invoke('delete-snippet', id), @@ -1182,7 +1176,7 @@ document.addEventListener('DOMContentLoaded', () => { }); sidebarManager.registerPanel('templates', { title: 'Templates', - render: (container) => renderTemplatesPanel(container, async (file) => { + render: (container) => getRenderTemplatesPanel()(container, async (file) => { const templateContent = await ipcRenderer.invoke('load-template', file); if (templateContent) { const content = templateContent.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0]); @@ -1202,7 +1196,7 @@ document.addEventListener('DOMContentLoaded', () => { // Set welcome content in the first tab's preview const recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]'); - const welcomeHtml = createWelcomeContent(recentFiles); + const welcomeHtml = getCreateWelcomeContent()(recentFiles); const tab = tabManager.tabs.get(tabManager.activeTabId); if (tab) { @@ -1287,9 +1281,11 @@ document.addEventListener('DOMContentLoaded', () => { }); // Initialize command palette + const CommandPalette = getCommandPalette(); const commandPalette = new CommandPalette(); // Initialize print preview + const PrintPreview = getPrintPreview(); const printPreview = new PrintPreview(); // Register commands @@ -1297,10 +1293,7 @@ document.addEventListener('DOMContentLoaded', () => { commandPalette.register('Open File', 'Ctrl+O', () => ipcRenderer.send('menu-open')); commandPalette.register('Save', 'Ctrl+S', () => { const content = tabManager.getCurrentContent(); - const filePath = tabManager.getCurrentFilePath(); - if (filePath) { - ipcRenderer.send('save-current-file', content); - } + ipcRenderer.send('save-current-file', content); }); commandPalette.register('Toggle Preview', '', () => { tabManager.isPreviewVisible = !tabManager.isPreviewVisible; @@ -1414,14 +1407,24 @@ ipcRenderer.on('file-opened', (event, data) => { ipcRenderer.on('file-save', () => { const currentContent = tabManager.getCurrentContent(); const currentFilePath = tabManager.getCurrentFilePath(); - if (currentFilePath) { - ipcRenderer.send('save-current-file', currentContent); - } + // send to main process which will save or trigger save-as dialog + ipcRenderer.send('save-current-file', currentContent); }); ipcRenderer.on('get-content-for-save', (event, filePath) => { const currentContent = tabManager.getCurrentContent(); ipcRenderer.send('save-file', { path: filePath, content: currentContent }); + // Update the active tab's file path and title after save-as + const tab = tabManager.tabs.get(tabManager.activeTabId); + if (tab) { + tab.filePath = filePath; + tab.originalContent = currentContent; + tab.isDirty = false; + tab.title = filePath.split(/[/\\]/).pop(); + tabManager.updateTabBar(); + tabManager.updateFilePath(); + tabManager.updateBreadcrumb(); + } }); ipcRenderer.on('get-content-for-spreadsheet', (event, format) => { @@ -1527,7 +1530,8 @@ function openPrintPreviewDialog() { return; } - const printPreviewInstance = new PrintPreview(); + const PrintPreviewClass = getPrintPreview(); + const printPreviewInstance = new PrintPreviewClass(); printPreviewInstance.open(previewContent.innerHTML); } @@ -2001,6 +2005,25 @@ ipcRenderer.on('show-universal-converter-dialog', () => { showUniversalConverterDialog(); }); +// Batch converter menu items - open universal converter with batch mode and correct tool +ipcRenderer.on('show-batch-converter', (event, type) => { + showUniversalConverterDialog(); + // Map batch type to the appropriate tool + const toolMap = { image: 'imagemagick', audio: 'ffmpeg', video: 'ffmpeg', pdf: 'libreoffice' }; + const tool = toolMap[type] || 'libreoffice'; + const toolSelect = document.getElementById('converter-tool'); + if (toolSelect) { + toolSelect.value = tool; + updateConverterFormats(tool); + } + // Enable batch mode + const batchToggle = document.getElementById('converter-batch-mode'); + if (batchToggle) { + batchToggle.checked = true; + batchToggle.dispatchEvent(new Event('change')); + } +}); + ipcRenderer.on('conversion-status', (event, status) => { document.getElementById('converter-status').textContent = status; }); @@ -4048,195 +4071,6 @@ document.addEventListener('mouseup', () => { } }); -// ============================================================================ -// PREVIEW POP-OUT -// ============================================================================ - -let popoutWindow = null; - -function popoutPreview(tabId) { - const previewContent = document.getElementById(`preview-${tabId}`); - - if (!previewContent) { - alert('No preview content to pop out'); - return; - } - - // Close existing popout if open - if (popoutWindow && !popoutWindow.closed) { - popoutWindow.close(); - } - - // Create new window - popoutWindow = window.open('', 'Preview Window', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no'); - - if (!popoutWindow) { - alert('Pop-up blocked! Please allow pop-ups for this application.'); - return; - } - - // Write content to popout window - const htmlContent = ` - - - - Preview - PanConverter - - - -
- ${previewContent.innerHTML} -
- - - - `; - - popoutWindow.document.write(htmlContent); - popoutWindow.document.close(); - - // Setup auto-update - setupPopoutAutoUpdate(tabId); -} - -function getPreviewStyles() { - // Extract relevant preview styles - return ` - h1, h2, h3, h4, h5, h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; - } - h1 { - font-size: 2em; - border-bottom: 1px solid #eaecef; - padding-bottom: 0.3em; - } - h2 { - font-size: 1.5em; - border-bottom: 1px solid #eaecef; - padding-bottom: 0.3em; - } - p { - margin-bottom: 16px; - } - code { - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: #f6f8fa; - border: 1px solid #d0d7de; - border-radius: 6px; - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; - } - pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #f6f8fa; - border: 1px solid #d0d7de; - border-radius: 6px; - margin-bottom: 16px; - } - pre code { - background: transparent; - border: none; - padding: 0; - } - table { - border-collapse: collapse; - margin-bottom: 16px; - width: 100%; - } - table th, table td { - padding: 6px 13px; - border: 1px solid #dfe2e5; - } - table th { - font-weight: 600; - background-color: #f6f8fa; - } - a { - color: #0366d6; - text-decoration: none; - } - a:hover { - text-decoration: underline; - } - blockquote { - padding: 0 1em; - color: #6a737d; - border-left: 0.25em solid #dfe2e5; - margin-bottom: 16px; - } - img { - max-width: 100%; - height: auto; - } - ul, ol { - padding-left: 2em; - margin-bottom: 16px; - } - li { - margin-bottom: 4px; - } - `; -} - -function setupPopoutAutoUpdate(tabId) { - // Monitor for preview changes and update popout - const observer = new MutationObserver(() => { - if (popoutWindow && !popoutWindow.closed) { - const previewContent = document.getElementById(`preview-${tabId}`); - popoutWindow.postMessage({ - type: 'preview-update', - content: previewContent.innerHTML - }, '*'); - } else { - observer.disconnect(); - } - }); - - const previewElement = document.getElementById(`preview-${tabId}`); - if (previewElement) { - observer.observe(previewElement, { - childList: true, - subtree: true, - characterData: true - }); - } -} - -// Pop-out button event listener -document.addEventListener('click', (e) => { - if (e.target.classList.contains('preview-popout-btn')) { - const tabId = e.target.id.replace('preview-popout-', ''); - popoutPreview(tabId); - } -}); // ============================================ // Insert Content from Generator Windows @@ -4267,8 +4101,15 @@ let pdfFilePath = null; let isPdfViewerActive = false; // Track if PDF viewer is currently shown // Initialize PDF.js -const pdfjsLib = require('pdfjs-dist'); -pdfjsLib.GlobalWorkerOptions.workerSrc = require.resolve('pdfjs-dist/build/pdf.worker.mjs'); +// Lazy-load pdfjs-dist only when PDF viewer is needed +let _pdfjsLib; +function getPdfjsLib() { + if (!_pdfjsLib) { + _pdfjsLib = require('pdfjs-dist'); + _pdfjsLib.GlobalWorkerOptions.workerSrc = require.resolve('pdfjs-dist/build/pdf.worker.mjs'); + } + return _pdfjsLib; +} // Open PDF file async function openPdfFile(filePath) { @@ -4292,7 +4133,7 @@ async function openPdfFile(filePath) { // Show loading state document.getElementById('status-text').textContent = 'Loading PDF...'; - const loadingTask = pdfjsLib.getDocument(filePath); + const loadingTask = getPdfjsLib().getDocument(filePath); pdfDoc = await loadingTask.promise; pdfFilePath = filePath; pdfCurrentPage = 1; diff --git a/src/styles.css b/src/styles.css index 639f488..cd2313a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -188,34 +188,6 @@ body { background: #666; } -/* Preview Header */ -.preview-header { - display: flex; - justify-content: flex-end; - align-items: center; - padding: 4px 8px; - background: #f6f8fa; - border-bottom: 1px solid #dfe2e5; - min-height: 32px; -} - -.preview-popout-btn { - background: transparent; - border: none; - border-radius: 3px; - padding: 2px 6px; - cursor: pointer; - font-size: 11px; - color: #8c959f; - opacity: 0.5; - transition: all 0.2s; -} - -.preview-popout-btn:hover { - background: rgba(0, 0, 0, 0.05); - opacity: 1; - color: #24292f; -} .editor-textarea { width: 100%;