/** * MarkdownConverter Renderer Process * @version 4.5.0 */ const { ipcRenderer } = require('electron'); const marked = require('marked'); const { markedHighlight } = require('marked-highlight'); const createDOMPurify = require('dompurify'); const DOMPurify = createDOMPurify(window); const hljs = require('highlight.js'); const { createEditor } = require('./editor/codemirror-setup'); const { undo, redo } = require('@codemirror/commands'); const { showMediaOperationsDialog } = require('./renderer/media-operations-dialog'); const { showDocumentCompareDialog } = require('./renderer/document-compare-dialog'); const { initExportPresets, refreshExportPresets } = require('./renderer/export-presets'); const { csvToMarkdownTable } = require('./utils/csv-to-markdown-table'); /** * Toggle body classes that drive the monospace font + ligatures CSS tokens. * Single source of truth for the live preview/editor font behaviour. * Also pushes the settings into the renderer-wide cache so PrintPreview can read them. * @param {{monospaceFont?: string, monospaceLigatures?: boolean} | null | undefined} settings */ function applyMonospaceClasses(settings) { const s = settings || {}; // Cache for downstream consumers (PrintPreview, future settings UI, etc.) if (typeof window !== 'undefined') { window.__monospaceSettings = { monospaceFont: s.monospaceFont || 'jetbrains-mono', monospaceLigatures: s.monospaceLigatures === true, }; if (window.__printPreview && typeof window.__printPreview.setMonospaceSettings === 'function') { window.__printPreview.setMonospaceSettings(window.__monospaceSettings); } } const body = document.body; if (!body) return; const isFira = s.monospaceFont === 'fira-code'; body.classList.toggle('mono-fira', isFira); body.classList.toggle('mono-jetbrains', !isFira); const ligOn = s.monospaceLigatures === true; body.classList.toggle('mono-ligatures-on', ligOn); body.classList.toggle('mono-ligatures-off', !ligOn); } // Define a fallback window.electronAPI object for when contextIsolation is disabled // and the preload script is not loaded in the main window context. if (typeof window !== 'undefined' && !window.electronAPI) { window.electronAPI = { send: (channel, data) => { ipcRenderer.send(channel, data); }, invoke: async (channel, data) => { return await ipcRenderer.invoke(channel, data); }, on: (channel, callback) => { const subscription = (event, ...args) => callback(...args); ipcRenderer.on(channel, subscription); return () => { ipcRenderer.removeListener(channel, subscription); }; }, once: (channel, callback) => { ipcRenderer.once(channel, (event, ...args) => callback(...args)); }, removeAllListeners: (channel) => { ipcRenderer.removeAllListeners(channel); }, getAppVersion: () => ipcRenderer.invoke('get-app-version'), file: { read: (filePath) => ipcRenderer.invoke('read-file', filePath), write: (filePath, content) => ipcRenderer.invoke('write-file', { path: filePath, content, }), delete: (filePath) => ipcRenderer.invoke('delete-file', filePath), ensureDir: (dirPath) => ipcRenderer.invoke('ensure-directory', dirPath), exists: (filePath) => ipcRenderer.invoke('path-exists', filePath), isDirectory: (filePath) => ipcRenderer.invoke('is-directory', filePath), copy: (source, destination) => ipcRenderer.invoke('copy-path', { source, destination, }), move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination, }), }, }; } // Use window.ModalManager if already set by script tag, otherwise require it. // This prevents "Identifier 'ModalManager' has already been declared" when // both the script tag in index.html and CommonJS require() declare it. let _ModalManager; if (typeof window !== 'undefined' && window.ModalManager) { _ModalManager = window.ModalManager; } else { const result = require('./utils/ModalManager'); _ModalManager = result.ModalManager || result; } // 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; } let _renderOutlinePanel; function getRenderOutlinePanel() { if (!_renderOutlinePanel) _renderOutlinePanel = require('./sidebar/outline-panel').renderOutlinePanel; return _renderOutlinePanel; } 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; } let _ZenMode; function getZenMode() { if (!_ZenMode) _ZenMode = require('./zen-mode').ZenMode; return _ZenMode; } let _showAnalyticsModal; function getShowAnalyticsModal() { if (!_showAnalyticsModal) _showAnalyticsModal = require('./analytics/analytics-panel').showAnalyticsModal; return _showAnalyticsModal; } function ensureToastContainer() { let container = document.getElementById('app-toast-container'); if (!container) { container = document.createElement('div'); container.id = 'app-toast-container'; container.className = 'app-toast-container'; document.body.appendChild(container); } return container; } function notifyUser(message, type = 'info', options = {}) { if (!message) return; const { duration = 3500 } = options; const container = ensureToastContainer(); const toast = document.createElement('div'); toast.className = `app-toast app-toast-${type}`; toast.setAttribute('role', type === 'warning' ? 'alert' : 'status'); toast.textContent = message; container.appendChild(toast); requestAnimationFrame(() => { toast.classList.add('visible'); }); const dismiss = () => { toast.classList.remove('visible'); setTimeout(() => toast.remove(), 180); }; if (duration > 0) { setTimeout(dismiss, duration); } toast.addEventListener('click', dismiss); return toast; } // Configure marked with highlight extension marked.use( markedHighlight({ highlight: function (code, lang) { if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang, }).value; } catch (err) { console.warn('Syntax highlighting failed for language:', lang, err.message); } } return hljs.highlightAuto(code).value; }, }) ); marked.use({ breaks: true, gfm: true, }); // Footnotes support const markedFootnote = require('marked-footnote'); marked.use(markedFootnote()); // Admonitions support (:::note, :::warning, :::tip, :::danger, :::info) marked.use({ extensions: [ { name: 'admonition', level: 'block', start(src) { return src.match(/^:::(note|warning|tip|danger|info)/m)?.index; }, tokenizer(src) { const match = src.match(/^:::(note|warning|tip|danger|info)\s*\n([\s\S]*?)^:::\s*$/m); if (match && match.index === 0) { const admonitionType = match[1]; const text = match[2].trim(); const tokens = []; this.lexer.blockTokens(text, tokens); return { type: 'admonition', raw: match[0], admonitionType, text, tokens, }; } }, renderer(token) { const icons = { note: '\u2139', warning: '\u26A0', tip: '\uD83D\uDCA1', danger: '\uD83D\uDD34', info: '\u2139', }; const icon = icons[token.admonitionType] || '\u2139'; const inner = this.parser.parse(token.tokens || []); return `
${icon} ${token.admonitionType.charAt(0).toUpperCase() + token.admonitionType.slice(1)}
${inner}
`; }, }, ], }); // PlantUML hex encoding for server rendering function plantumlEncode(text) { const hex = Array.from(new TextEncoder().encode(text)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); return '~h' + hex; } // Scopes user custom CSS by prefixing all selectors to protect application UI function scopeCSS(cssText, scopeSelector) { if (!cssText) return ''; return cssText.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|(?=[^{]*{))/g, (match, selector, separator) => { const trimmed = selector.trim(); if ( !trimmed || trimmed.startsWith('@') || trimmed.startsWith(':root') || trimmed.startsWith('from') || trimmed.startsWith('to') || /^\d+%$/.test(trimmed) ) { return match; } return scopeSelector + ' ' + trimmed + (separator || ''); }); } // Module-level reference set by DOMContentLoaded after sidebar registration const outlinePanelContainer = null; // Tab Management class TabManager { constructor() { this.tabs = new Map(); this.activeTabId = 1; this.nextTabId = 2; this.isPreviewVisible = true; this.showLineNumbers = false; this.autoSaveInterval = null; this.autoSaveDelay = 30000; // 30 seconds this.recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]'); this.previewDebounceTimers = new Map(); // Debounce timers per tab this.previewDebounceDelay = 300; // 300ms debounce this.previewCache = new Map(); // Performance optimization render cache // Initialize first tab this.tabs.set(1, { id: 1, title: 'Untitled', content: '', filePath: null, isDirty: false, editorView: null, findMatches: [], currentMatchIndex: -1, type: 'markdown', // 'markdown' or 'pdf' // PDF-specific state pdfDoc: null, pdfCurrentPage: 1, pdfZoomLevel: 1.0, pdfRotation: 0, }); this.setupEventListeners(); this.updateUI(); } setupEventListeners() { // Tab bar events document.getElementById('new-tab-btn').addEventListener('click', () => this.createNewTab()); document.getElementById('tab-bar').addEventListener('click', (e) => { if (e.target.classList.contains('tab-close')) { e.stopPropagation(); const tabId = parseInt(e.target.closest('.tab').dataset.tabId); this.closeTab(tabId); } else if (e.target.closest('.tab')) { const tabId = parseInt(e.target.closest('.tab').dataset.tabId); this.switchToTab(tabId); } }); // Editor events for active tab this.setupEditorEvents(); // Toolbar events this.setupToolbarEvents(); // Find dialog events this.setupFindEvents(); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'n': case 't': e.preventDefault(); this.createNewTab(); break; case 'w': if (this.tabs.size > 1) { e.preventDefault(); this.closeTab(this.activeTabId); } break; case 'Tab': if (this.tabs.size > 1) { e.preventDefault(); this.switchToNextTab(); } break; } } }); } createNewTab(type = 'markdown') { const newTabId = this.nextTabId++; const tab = { id: newTabId, title: type === 'pdf' ? 'PDF Document' : 'Untitled', content: '', filePath: null, isDirty: false, editorView: null, findMatches: [], currentMatchIndex: -1, type: type, // 'markdown' or 'pdf' // PDF-specific state pdfDoc: null, pdfCurrentPage: 1, pdfZoomLevel: 1.0, pdfRotation: 0, }; this.tabs.set(newTabId, tab); this.createTabElements(tab); this.switchToTab(newTabId); this.startAutoSave(); this.updateTabBar(); } createPdfTab(filePath) { const newTabId = this.nextTabId++; const fileName = require('path').basename(filePath); const tab = { id: newTabId, title: fileName, content: '', filePath: filePath, isDirty: false, editorView: null, findMatches: [], currentMatchIndex: -1, type: 'pdf', // PDF-specific state pdfDoc: null, pdfCurrentPage: 1, pdfZoomLevel: 1.0, pdfRotation: 0, }; this.tabs.set(newTabId, tab); this.createPdfTabElements(tab); this.switchToTab(newTabId); this.loadPdfInTab(tab.id, filePath); this.updateTabBar(); return tab.id; } createPdfTabElements(tab) { // Create PDF tab content container const tabContent = document.createElement('div'); tabContent.className = 'tab-content'; tabContent.id = `tab-content-${tab.id}`; tabContent.dataset.tabId = tab.id; tabContent.dataset.tabType = 'pdf'; tabContent.innerHTML = `
/ 1 100%
`; document.querySelector('.editor-container').appendChild(tabContent); // Add event listeners for PDF controls this.setupPdfTabEvents(tab.id); } setupPdfTabEvents(tabId) { const container = document.getElementById(`pdf-container-${tabId}`); if (!container) return; container.addEventListener('click', async (e) => { const action = e.target.dataset.action; if (!action) return; const tab = this.tabs.get(tabId); if (!tab || !tab.pdfDoc) return; switch (action) { case 'prev': if (tab.pdfCurrentPage > 1) { tab.pdfCurrentPage--; await this.renderPdfPageInTab(tabId); } break; case 'next': if (tab.pdfCurrentPage < tab.pdfDoc.numPages) { tab.pdfCurrentPage++; await this.renderPdfPageInTab(tabId); } break; case 'zoom-out': if (tab.pdfZoomLevel > 0.25) { tab.pdfZoomLevel -= 0.25; await this.renderPdfPageInTab(tabId); } break; case 'zoom-in': if (tab.pdfZoomLevel < 4.0) { tab.pdfZoomLevel += 0.25; await this.renderPdfPageInTab(tabId); } break; case 'fit-width': const page = await tab.pdfDoc.getPage(tab.pdfCurrentPage); const viewport = page.getViewport({ scale: 1, rotation: tab.pdfRotation, }); const containerWidth = container.querySelector('.pdf-canvas-container').clientWidth - 40; tab.pdfZoomLevel = containerWidth / viewport.width; await this.renderPdfPageInTab(tabId); break; case 'rotate-left': tab.pdfRotation = (tab.pdfRotation - 90 + 360) % 360; await this.renderPdfPageInTab(tabId); break; case 'rotate-right': tab.pdfRotation = (tab.pdfRotation + 90) % 360; await this.renderPdfPageInTab(tabId); break; } }); // Page input handler const pageInput = document.getElementById(`pdf-page-${tabId}`); if (pageInput) { pageInput.addEventListener('change', async (e) => { const tab = this.tabs.get(tabId); const pageNum = parseInt(e.target.value); if (tab && tab.pdfDoc && pageNum >= 1 && pageNum <= tab.pdfDoc.numPages) { tab.pdfCurrentPage = pageNum; await this.renderPdfPageInTab(tabId); } }); } } async loadPdfInTab(tabId, filePath) { const tab = this.tabs.get(tabId); if (!tab) return; try { document.getElementById('status-text').textContent = 'Loading PDF...'; const loadingTask = getPdfjsLib().getDocument(filePath); tab.pdfDoc = await loadingTask.promise; tab.pdfCurrentPage = 1; tab.pdfZoomLevel = 1.0; tab.pdfRotation = 0; // Update UI document.getElementById(`pdf-total-${tabId}`).textContent = tab.pdfDoc.numPages; document.getElementById(`pdf-page-${tabId}`).value = 1; document.getElementById(`pdf-page-${tabId}`).max = tab.pdfDoc.numPages; document.getElementById(`pdf-zoom-${tabId}`).textContent = '100%'; await this.renderPdfPageInTab(tabId); document.getElementById('status-text').textContent = `PDF: ${tab.title} (${tab.pdfDoc.numPages} pages)`; } catch (error) { console.error('Error loading PDF:', error); document.getElementById('status-text').textContent = 'Error loading PDF'; notifyUser(`Error loading PDF: ${error.message}`, 'warning'); } } async renderPdfPageInTab(tabId) { const tab = this.tabs.get(tabId); if (!tab || !tab.pdfDoc) return; try { const page = await tab.pdfDoc.getPage(tab.pdfCurrentPage); const canvas = document.getElementById(`pdf-canvas-${tabId}`); if (!canvas) return; const ctx = canvas.getContext('2d'); const viewport = page.getViewport({ scale: tab.pdfZoomLevel, rotation: tab.pdfRotation, }); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport: viewport, }).promise; // Update UI document.getElementById(`pdf-page-${tabId}`).value = tab.pdfCurrentPage; document.getElementById(`pdf-zoom-${tabId}`).textContent = Math.round(tab.pdfZoomLevel * 100) + '%'; } catch (error) { console.error('Error rendering PDF page:', error); } } createTabElements(tab) { // Create tab content container const tabContent = document.createElement('div'); tabContent.className = 'tab-content'; tabContent.id = `tab-content-${tab.id}`; tabContent.dataset.tabId = tab.id; tabContent.innerHTML = `
`; document.querySelector('.editor-container').appendChild(tabContent); // Initialize CodeMirror editor const editorContainer = document.getElementById(`editor-cm-${tab.id}`); if (editorContainer) { const isDark = document.body.className.includes('dark'); tab.editorView = createEditor(editorContainer, { content: tab.content, onChange: (newContent) => { tab.content = newContent; tab.isDirty = true; // Dynamically enable/disable Large File Mode on edit if (newContent.length > 1024 * 1024) { if (!tab.largeFileMode) { tab.largeFileMode = true; this.isPreviewVisible = false; this.updatePreviewVisibility(); notifyUser( 'Large content detected (>1MB). Large File Mode enabled to maintain peak responsiveness. Live preview auto-render is disabled.', 'warning' ); } } else { tab.largeFileMode = false; } this.updatePreview(tab.id); this.updateWordCount(); this.updateTabBar(); if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline(); }, onUpdate: (view) => { this.updateCursorPosition(view); if (outlinePanelContainer?._setActiveHeading) outlinePanelContainer._setActiveHeading( view.state.doc.lineAt(view.state.selection.main.head).number ); }, isDark, showLineNumbers: this.showLineNumbers, }); } } switchToTab(tabId) { if (!this.tabs.has(tabId)) return; // Save current tab state before switching if (this.activeTabId && this.tabs.has(this.activeTabId)) { this.saveCurrentTabState(); } this.activeTabId = tabId; this.updateUI(); this.restoreTabState(tabId); this.focusActiveEditor(); this.updateFilePath(); this.updateBreadcrumb(); // Update cursor position for the newly active tab const tabForCursor = this.tabs.get(tabId); if (tabForCursor?.editorView) { this.updateCursorPosition(tabForCursor.editorView); } // Notify main process about current file for exports const tab = this.tabs.get(tabId); ipcRenderer.send('set-current-file', tab?.filePath || null); // Refresh outline panel for new tab content if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline(); } switchToNextTab() { const tabIds = Array.from(this.tabs.keys()); const currentIndex = tabIds.indexOf(this.activeTabId); const nextIndex = (currentIndex + 1) % tabIds.length; this.switchToTab(tabIds[nextIndex]); } closeTab(tabId) { if (this.tabs.size === 1) return; // Don't close the last tab const tab = this.tabs.get(tabId); if (!tab) return; if (tab.isDirty && tab.type === 'markdown') { // Show confirmation dialog for unsaved changes (only for markdown) const result = confirm( 'You have unsaved changes. Do you want to close this tab without saving?' ); if (!result) return; } // Destroy CodeMirror view (for markdown tabs) if (tab?.editorView) { tab.editorView.destroy(); } // Destroy PDF document (for PDF tabs) if (tab?.pdfDoc) { try { tab.pdfDoc.destroy(); } catch (e) { console.warn('Error destroying PDF:', e); } } // Remove tab elements const tabElement = document.querySelector(`.tab[data-tab-id="${tabId}"]`); const tabContent = document.getElementById(`tab-content-${tabId}`); if (tabElement?.classList.contains('tab')) { tabElement.remove(); } if (tabContent) { tabContent.remove(); } this.tabs.delete(tabId); // Switch to another tab if this was active if (this.activeTabId === tabId) { const remainingTabs = Array.from(this.tabs.keys()); this.switchToTab(remainingTabs[0]); } this.updateTabBar(); } updateTabBar() { const tabBar = document.getElementById('tab-bar'); const existingTabs = tabBar.querySelectorAll('.tab'); // Remove all existing tab elements except the new tab button existingTabs.forEach((tab) => tab.remove()); // Add tabs in order const sortedTabs = Array.from(this.tabs.values()).sort((a, b) => a.id - b.id); const newTabBtn = document.getElementById('new-tab-btn'); sortedTabs.forEach((tab) => { const tabElement = document.createElement('div'); const typeClass = tab.type === 'pdf' ? 'pdf-tab' : 'markdown-tab'; tabElement.className = `tab ${typeClass} ${tab.id === this.activeTabId ? 'active' : ''}`; tabElement.dataset.tabId = tab.id; tabElement.dataset.tabType = tab.type || 'markdown'; const title = tab.filePath ? tab.filePath.split('/').pop() : tab.title; const dirtyIndicator = tab.isDirty ? ' •' : ''; const typeIndicator = tab.type === 'pdf' ? '📄 ' : ''; tabElement.innerHTML = ` ${typeIndicator}${title}${dirtyIndicator} `; tabBar.insertBefore(tabElement, newTabBtn); }); } updateUI() { // Show/hide tab contents document.querySelectorAll('.tab-content').forEach((content) => { content.classList.remove('active'); }); const activeContent = document.getElementById(`tab-content-${this.activeTabId}`); if (activeContent) { activeContent.classList.add('active'); } // Get active tab to check type const activeTab = this.tabs.get(this.activeTabId); // Show/hide toolbar based on tab type const toolbar = document.querySelector('.toolbar'); if (toolbar) { if (activeTab?.type === 'pdf') { toolbar.classList.add('hidden'); } else { toolbar.classList.remove('hidden'); } } // Update preview visibility (only for markdown tabs) if (activeTab?.type !== 'pdf') { this.updatePreviewVisibility(); this.updateLineNumbers(); } this.updateTabBar(); } saveCurrentTabState() { const tab = this.tabs.get(this.activeTabId); if (!tab) return; if (tab.editorView) { tab.content = tab.editorView.state.doc.toString(); tab.isDirty = tab.content !== (tab.originalContent || ''); } } restoreTabState(tabId) { const tab = this.tabs.get(tabId); if (!tab) return; if (tab.editorView) { this.setEditorContent(tabId, tab.content); this.updatePreview(tabId, true); // immediate=true for tab switches this.updateWordCount(); } } focusActiveEditor() { const tab = this.tabs.get(this.activeTabId); if (tab?.editorView) { tab.editorView.focus(); } } updatePreview(tabId = this.activeTabId, immediate = false) { const tab = this.tabs.get(tabId); if (!tab || tab.type === 'pdf') return; // Guard against auto-renders in large file mode if (tab.largeFileMode && !immediate) { return; } // Clear existing debounce timer for this tab if (this.previewDebounceTimers.has(tabId)) { clearTimeout(this.previewDebounceTimers.get(tabId)); } // If immediate, render right away (for tab switches, file loads) if (immediate) { this._renderPreview(tabId); return; } // Debounce preview rendering for typing performance this.previewDebounceTimers.set( tabId, setTimeout(() => { this._renderPreview(tabId); this.previewDebounceTimers.delete(tabId); }, this.previewDebounceDelay) ); } _hash(str) { if (!str) return 0; let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; // Convert to 32bit integer } return hash; } _renderPreview(tabId) { const tab = this.tabs.get(tabId); const preview = document.getElementById(`preview-${tabId}`); if (!tab || !preview) return; try { // Check if libraries are available if (!marked || !DOMPurify) { console.error('[_renderPreview] Libraries not available:', { marked: !!marked, DOMPurify: !!DOMPurify, }); preview.innerHTML = '
⚠️
Libraries Not Loaded
Required libraries (marked/DOMPurify) could not be loaded. Please check your installation.
'; return; } // Cache lookup const contentHash = this._hash(tab.content); let sanitizedHtml; if (this.previewCache.has(contentHash)) { sanitizedHtml = this.previewCache.get(contentHash); } else { const html = marked.parse(tab.content); if (html instanceof Promise) { console.error('[_renderPreview] marked.parse returned a Promise! Need to await it.'); return; } sanitizedHtml = DOMPurify.sanitize(html); // Add to cache and maintain cache size limit (100 entries) this.previewCache.set(contentHash, sanitizedHtml); if (this.previewCache.size > 100) { const oldestKey = this.previewCache.keys().next().value; this.previewCache.delete(oldestKey); } } // TOC generation if (sanitizedHtml.includes('[[toc]]') || sanitizedHtml.includes('[TOC]')) { const headingRegex = /]*>(.*?)<\/h[1-6]>/gi; let tocMatch; const toc = []; const tempHtml = sanitizedHtml; while ((tocMatch = headingRegex.exec(tempHtml)) !== null) { const level = parseInt(tocMatch[1]); const text = tocMatch[2].replace(/<[^>]+>/g, ''); const id = text.toLowerCase().replace(/[^\w]+/g, '-'); toc.push({ level, text, id, }); } if (toc.length > 0) { const tocHtml = ''; sanitizedHtml = sanitizedHtml.replace(/\[\[toc\]\]|\[TOC\]/gi, tocHtml); // Add IDs to headings for anchor links toc.forEach((h) => { sanitizedHtml = sanitizedHtml.replace( new RegExp(`(]*)>`, 'i'), `$1 id="${h.id}">` ); }); } } preview.innerHTML = sanitizedHtml; // Render math expressions if KaTeX is available if (window.katex && window.renderMathInElement) { try { window.renderMathInElement(preview, { delimiters: [ { left: '$$', right: '$$', display: true, }, { left: '$', right: '$', display: false, }, { left: '\\[', right: '\\]', display: true, }, { left: '\\(', right: '\\)', display: false, }, ], }); } catch (mathError) { console.warn('Math rendering error:', mathError); } } // Render Mermaid diagrams — lazy-load mermaid only when needed const mermaidBlocks = preview.querySelectorAll('pre code.language-mermaid'); if (mermaidBlocks.length > 0) { try { // 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; const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.setAttribute('data-processed', 'true'); mermaidDiv.textContent = code; pre.parentElement.replaceChild(mermaidDiv, pre); }); const theme = document.body.className.includes('theme-dark') ? 'dark' : 'default'; 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'); }); }); } catch (mermaidError) { console.warn('Mermaid rendering error:', mermaidError); } } // Render PlantUML diagrams const plantumlBlocks = preview.querySelectorAll('pre code.language-plantuml'); plantumlBlocks.forEach((block) => { const code = block.textContent; const pre = block.parentElement; // Encode for PlantUML server using hex encoding const encoded = plantumlEncode(code); const img = document.createElement('img'); img.src = `https://www.plantuml.com/plantuml/svg/${encoded}`; img.alt = 'PlantUML diagram'; img.style.maxWidth = '100%'; img.onerror = () => { img.replaceWith(pre); // Fallback to code block on error }; pre.parentElement.replaceChild(img, pre); }); // Add Run buttons to executable code blocks (REPL) const codeBlocks = preview.querySelectorAll('pre code[class*="language-"]'); codeBlocks.forEach((block) => { const lang = block.className.match(/language-(\w+)/)?.[1]; if (['javascript', 'js', 'python', 'py', 'bash', 'sh'].includes(lang)) { const runBtn = document.createElement('button'); runBtn.className = 'code-run-btn'; runBtn.textContent = '\u25B6 Run'; runBtn.addEventListener('click', async () => { if (replPanel) { replPanel.show(); const result = await ipcRenderer.invoke('execute-code', { code: block.textContent, language: lang, }); replPanel.appendOutput(`[${lang}]`, result); } }); block.parentElement.style.position = 'relative'; block.parentElement.appendChild(runBtn); } }); } catch (error) { console.error('Error rendering preview:', error); preview.innerHTML = '

Error rendering preview. Please check your markdown syntax.

'; } } updatePreviewVisibility() { document.querySelectorAll('.tab-content').forEach((content) => { const previewPane = content.querySelector('.pane:last-child'); const editorPane = content.querySelector('.pane:first-child'); if (this.isPreviewVisible) { previewPane.classList.remove('hidden'); editorPane.classList.remove('full-width'); } else { previewPane.classList.add('hidden'); editorPane.classList.add('full-width'); } }); } updateLineNumbers() { // CodeMirror handles line numbers natively — this is now a no-op. // Line number visibility is controlled via the showLineNumbers option // passed to createEditor(). } updateWordCount() { const tab = this.tabs.get(this.activeTabId); if (!tab) return; const content = tab.content; const words = content.trim() ? content .trim() .split(/\s+/) .filter((word) => word.length > 0).length : 0; const chars = content.length; const wordEl = document.getElementById('word-count'); const charEl = document.getElementById('char-count'); if (wordEl) wordEl.textContent = `Words: ${words}`; if (charEl) charEl.textContent = `Chars: ${chars}`; } updateCursorPosition(view) { if (!view) return; const pos = view.state.selection.main.head; const line = view.state.doc.lineAt(pos); const lineCol = document.getElementById('line-col'); if (lineCol) lineCol.textContent = `Ln ${line.number}, Col ${pos - line.from + 1}`; } updateFilePath() { const tab = this.tabs.get(this.activeTabId); const el = document.getElementById('status-file-path'); if (el && tab) { el.textContent = tab.filePath || 'Untitled'; el.title = tab.filePath || ''; } } updateBreadcrumb() { const tab = this.tabs.get(this.activeTabId); const el = document.getElementById('breadcrumb-path'); if (el && tab) { el.textContent = tab.filePath || 'Untitled'; el.title = tab.filePath || ''; } } setupEditorEvents() { // CodeMirror handles editor events via the onChange callback // passed to createEditor(). No additional event delegation needed. } handleEditorInput(tabId) { // CodeMirror's onChange callback handles content syncing. // This method is kept for any remaining callers but is largely a no-op now. const tab = this.tabs.get(tabId); if (!tab) return; if (tab.editorView) { tab.content = tab.editorView.state.doc.toString(); } tab.isDirty = true; this.updatePreview(tabId); this.updateWordCount(); this.updateTabBar(); } // Auto-save functionality startAutoSave() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); } this.autoSaveInterval = setInterval(() => { this.performAutoSave(); }, this.autoSaveDelay); } stopAutoSave() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); this.autoSaveInterval = null; } } performAutoSave() { const tab = this.tabs.get(this.activeTabId); if (!tab || !tab.filePath || !tab.content) return; // Only auto-save if content has changed since last save if (tab.lastSavedContent !== tab.content) { ipcRenderer.send('save-file', { path: tab.filePath, content: tab.content, }); tab.lastSavedContent = tab.content; // Show brief auto-save indicator this.showAutoSaveIndicator(); } } showAutoSaveIndicator() { const indicator = document.createElement('div'); indicator.textContent = 'Auto-saved'; indicator.className = 'auto-save-indicator'; document.body.appendChild(indicator); setTimeout(() => { indicator.classList.add('fade-out'); setTimeout(() => { if (indicator.parentNode) { indicator.parentNode.removeChild(indicator); } }, 300); }, 1500); } // Recent files functionality addToRecentFiles(filePath) { if (!filePath) return; // Remove if already exists this.recentFiles = this.recentFiles.filter((f) => f !== filePath); // Add to beginning this.recentFiles.unshift(filePath); // Keep only last 10 files this.recentFiles = this.recentFiles.slice(0, 10); // Save to localStorage and sync with main process localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles)); ipcRenderer.send('save-recent-files', this.recentFiles); } getRecentFiles() { return this.recentFiles.filter((file) => { // Check if file still exists (basic check by trying to access it) try { return file && file.length > 0; } catch { return false; } }); } setupToolbarEvents() { // Bold document.getElementById('btn-bold').addEventListener('click', () => { this.wrapSelection('**', '**'); }); // Italic document.getElementById('btn-italic').addEventListener('click', () => { this.wrapSelection('*', '*'); }); // Heading document.getElementById('btn-heading').addEventListener('click', () => { this.insertAtLineStart('## '); }); // Link document.getElementById('btn-link').addEventListener('click', () => { this.wrapSelection('[', '](url)'); }); // Code document.getElementById('btn-code').addEventListener('click', () => { this.wrapSelection('`', '`'); }); // List document.getElementById('btn-list').addEventListener('click', () => { this.insertAtLineStart('- '); }); // Quote document.getElementById('btn-quote').addEventListener('click', () => { this.insertAtLineStart('> '); }); // Table document.getElementById('btn-table').addEventListener('click', () => { this.insertTable(); }); // CSV to Table (converts the selected CSV text in place) document.getElementById('btn-csv-table').addEventListener('click', () => { this.convertSelectionToTable(); }); // Strikethrough document.getElementById('btn-strikethrough').addEventListener('click', () => { this.wrapSelection('~~', '~~'); }); // Code Block document.getElementById('btn-code-block').addEventListener('click', () => { this.insertCodeBlock(); }); // Horizontal Rule document.getElementById('btn-horizontal-rule').addEventListener('click', () => { this.insertHorizontalRule(); }); // Preview toggle document.getElementById('btn-preview-toggle').addEventListener('click', () => { this.isPreviewVisible = !this.isPreviewVisible; this.updatePreviewVisibility(); }); // Line numbers document.getElementById('btn-line-numbers').addEventListener('click', () => { this.showLineNumbers = !this.showLineNumbers; this.updateLineNumbers(); }); } // Helper function to wrap selected text wrapSelection(before, after) { const tab = this.tabs.get(this.activeTabId); if (!tab?.editorView) return; const view = tab.editorView; const { from, to } = view.state.selection.main; const selectedText = view.state.sliceDoc(from, to); const replacement = before + (selectedText || 'text') + after; view.dispatch({ changes: { from, to, insert: replacement, }, }); view.focus(); } // Helper function to insert text at the start of current line insertAtLineStart(prefix) { const tab = this.tabs.get(this.activeTabId); if (!tab?.editorView) return; const view = tab.editorView; const pos = view.state.selection.main.head; const line = view.state.doc.lineAt(pos); view.dispatch({ changes: { from: line.from, insert: prefix, }, }); view.focus(); } // Insert a markdown table insertTable() { const table = '\n| Column 1 | Column 2 | Column 3 |\n' + '|----------|----------|----------|\n' + '| Cell 1 | Cell 2 | Cell 3 |\n' + '| Cell 4 | Cell 5 | Cell 6 |\n'; this.insertAtCursor(table); } // Convert the selected CSV text into a markdown table in place convertSelectionToTable() { const tab = this.tabs.get(this.activeTabId); if (!tab?.editorView) return; const view = tab.editorView; const { from, to } = view.state.selection.main; const selectedText = view.state.sliceDoc(from, to); if (!selectedText.trim()) return; const table = csvToMarkdownTable(selectedText); if (!table) return; view.dispatch({ changes: { from, to, insert: table, }, }); view.focus(); } // Insert a code block insertCodeBlock() { const tab = this.tabs.get(this.activeTabId); if (!tab?.editorView) return; const view = tab.editorView; const { from, to } = view.state.selection.main; const selectedText = view.state.sliceDoc(from, to); const codeBlock = '\n```\n' + (selectedText || 'code here') + '\n```\n'; view.dispatch({ changes: { from, to, insert: codeBlock, }, }); view.focus(); } // Insert a horizontal rule insertHorizontalRule() { this.insertAtCursor('\n\n---\n\n'); } setupFindEvents() { const btnFind = document.getElementById('btn-find'); const btnFindClose = document.getElementById('btn-find-close'); const findInput = document.getElementById('find-input'); const btnFindNext = document.getElementById('btn-find-next'); const btnFindPrev = document.getElementById('btn-find-prev'); const btnReplace = document.getElementById('btn-replace'); const btnReplaceAll = document.getElementById('btn-replace-all'); if ( !btnFind || !btnFindClose || !findInput || !btnFindNext || !btnFindPrev || !btnReplace || !btnReplaceAll ) { console.error('Find dialog elements not found'); return; } // Show find dialog btnFind.addEventListener('click', () => { window.modals.findModal.open(); findInput.focus(); }); // Close find dialog btnFindClose.addEventListener('click', () => { window.modals.findModal.close(); this.clearFindHighlights(); }); // Find input change - update matches findInput.addEventListener('input', () => { this.performFind(); }); // Find next btnFindNext.addEventListener('click', () => { this.findNext(); }); // Find previous btnFindPrev.addEventListener('click', () => { this.findPrevious(); }); // Replace btnReplace.addEventListener('click', () => { this.replaceOne(); }); // Replace all btnReplaceAll.addEventListener('click', () => { this.replaceAll(); }); // Enter key in find input - find next findInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); if (e.shiftKey) { this.findPrevious(); } else { this.findNext(); } } }); } performFind() { const findText = document.getElementById('find-input').value; const tab = this.tabs.get(this.activeTabId); if (!findText || !tab) { this.clearFindHighlights(); const findCount = document.getElementById('find-count'); if (findCount) { findCount.textContent = '0 matches'; } return; } const content = this.getEditorContent(); const matches = []; let index = 0; // Find all matches while ((index = content.indexOf(findText, index)) !== -1) { matches.push(index); index += findText.length; } tab.findMatches = matches; tab.currentMatchIndex = matches.length > 0 ? 0 : -1; // Update match count const findCount = document.getElementById('find-count'); if (findCount) { findCount.textContent = `${matches.length} match${matches.length !== 1 ? 'es' : ''}`; } // Highlight first match if (matches.length > 0) { this.highlightMatch(0); } } findNext() { const tab = this.tabs.get(this.activeTabId); if (!tab || tab.findMatches.length === 0) return; tab.currentMatchIndex = (tab.currentMatchIndex + 1) % tab.findMatches.length; this.highlightMatch(tab.currentMatchIndex); } findPrevious() { const tab = this.tabs.get(this.activeTabId); if (!tab || tab.findMatches.length === 0) return; tab.currentMatchIndex = tab.currentMatchIndex - 1; if (tab.currentMatchIndex < 0) { tab.currentMatchIndex = tab.findMatches.length - 1; } this.highlightMatch(tab.currentMatchIndex); } highlightMatch(matchIndex) { const tab = this.tabs.get(this.activeTabId); const findText = document.getElementById('find-input').value; if (!tab || !tab.editorView || matchIndex < 0 || matchIndex >= tab.findMatches.length) return; const position = tab.findMatches[matchIndex]; const view = tab.editorView; // Select the match in CodeMirror view.dispatch({ selection: { anchor: position, head: position + findText.length, }, scrollIntoView: true, }); // Restore focus to find input const findInput = document.getElementById('find-input'); if (findInput) { setTimeout(() => { findInput.focus(); findInput.setSelectionRange(findInput.value.length, findInput.value.length); }, 10); } // Update match counter const findCount = document.getElementById('find-count'); if (findCount) { findCount.textContent = `Match ${matchIndex + 1} of ${tab.findMatches.length}`; } } replaceOne() { const tab = this.tabs.get(this.activeTabId); const findText = document.getElementById('find-input').value; const replaceText = document.getElementById('replace-input').value; if (!tab || !tab.editorView || tab.findMatches.length === 0 || tab.currentMatchIndex < 0) return; const position = tab.findMatches[tab.currentMatchIndex]; const view = tab.editorView; view.dispatch({ changes: { from: position, to: position + findText.length, insert: replaceText, }, }); tab.content = view.state.doc.toString(); tab.isDirty = true; this.updatePreview(this.activeTabId); this.updateWordCount(); this.updateTabBar(); // Re-perform find to update matches this.performFind(); } replaceAll() { const tab = this.tabs.get(this.activeTabId); const findText = document.getElementById('find-input').value; const replaceText = document.getElementById('replace-input').value; if (!tab || !tab.editorView || !findText || tab.findMatches.length === 0) return; const view = tab.editorView; const replacedCount = tab.findMatches.length; // Build all changes as a single transaction for proper undo support. // Changes must be provided in document order; findMatches is already sorted. const changes = tab.findMatches.map((pos) => ({ from: pos, to: pos + findText.length, insert: replaceText, })); view.dispatch({ changes, }); tab.content = view.state.doc.toString(); tab.isDirty = true; this.updatePreview(this.activeTabId); this.updateWordCount(); this.updateTabBar(); // Update match count document.getElementById('find-count').textContent = `Replaced ${replacedCount} match${replacedCount !== 1 ? 'es' : ''}`; // Re-perform find this.performFind(); } clearFindHighlights() { const tab = this.tabs.get(this.activeTabId); if (tab) { tab.findMatches = []; tab.currentMatchIndex = -1; } } checkForLargeFile(tab, content) { if (content && content.length > 1024 * 1024) { // >1MB tab.largeFileMode = true; this.isPreviewVisible = false; this.updatePreviewVisibility(); notifyUser( 'Large file detected (>1MB). Large File Mode enabled to maintain peak responsiveness. Live preview auto-render is disabled.', 'warning' ); } else { tab.largeFileMode = false; } } // File operations openFile(filePath, content) { let tab = this.tabs.get(this.activeTabId); // Handle both forward and back slashes for cross-platform compatibility const fileName = filePath.split(/[\\/]/).pop(); // If current tab is empty and untitled, reuse it if (!tab.filePath && !tab.isDirty && tab.content === '') { tab.filePath = filePath; tab.title = fileName; tab.content = content; tab.originalContent = content; tab.isDirty = false; this.checkForLargeFile(tab, content); // Update the editor and preview this.setEditorContent(this.activeTabId, content); this.updatePreview(this.activeTabId, true); // immediate=true for initial load this.updateWordCount(); } else { // Create new tab for the file this.createNewTab(); tab = this.tabs.get(this.activeTabId); tab.filePath = filePath; tab.title = fileName; tab.content = content; tab.originalContent = content; tab.isDirty = false; this.checkForLargeFile(tab, content); // Set content in the CodeMirror editor this.setEditorContent(this.activeTabId, content); this.updatePreview(this.activeTabId, true); // immediate=true for initial load this.updateWordCount(); } this.startAutoSave(); this.addToRecentFiles(filePath); this.updateTabBar(); this.updateFilePath(); this.updateBreadcrumb(); this.updateUI(); // Ensure tab content is visible // Notify main process about current file for exports ipcRenderer.send('set-current-file', filePath); } // Get content from active editor getEditorContent(tabId = this.activeTabId) { const tab = this.tabs.get(tabId); if (tab?.editorView) { return tab.editorView.state.doc.toString(); } return tab?.content || ''; } // Set content in editor setEditorContent(tabId, content) { const tab = this.tabs.get(tabId); if (tab?.editorView) { tab.editorView.dispatch({ changes: { from: 0, to: tab.editorView.state.doc.length, insert: content, }, }); } tab.content = content; } // Insert text at cursor position insertAtCursor(text) { const tab = this.tabs.get(this.activeTabId); if (!tab?.editorView) return; const view = tab.editorView; const pos = view.state.selection.main.head; view.dispatch({ changes: { from: pos, insert: text, }, }); view.focus(); } // Get selected text getSelection() { const tab = this.tabs.get(this.activeTabId); if (!tab?.editorView) return ''; const state = tab.editorView.state; return state.sliceDoc(state.selection.main.from, state.selection.main.to); } // Replace selection replaceSelection(text) { const tab = this.tabs.get(this.activeTabId); if (!tab?.editorView) return; const view = tab.editorView; const { from, to } = view.state.selection.main; view.dispatch({ changes: { from, to, insert: text, }, }); view.focus(); } getCurrentContent() { const tab = this.tabs.get(this.activeTabId); return tab ? tab.content : ''; } getCurrentFilePath() { const tab = this.tabs.get(this.activeTabId); return tab ? tab.filePath : null; } } // Initialize tab manager let tabManager; let replPanel; // Shared by the sidebar Templates panel and the "New from Template" menu items: // loads a template file's content into a brand-new tab, substituting {{DATE}}. async function loadTemplateIntoNewTab(file) { const templateContent = await ipcRenderer.invoke('load-template', file); if (templateContent) { const content = templateContent.replace( /\{\{DATE\}\}/g, new Date().toISOString().split('T')[0] ); tabManager.createNewTab(); const tab = tabManager.tabs.get(tabManager.activeTabId); tabManager.setEditorContent(tab.id, content); } } document.addEventListener('DOMContentLoaded', async () => { const ModalManager = _ModalManager; tabManager = new TabManager(); // Load saved Custom Preview CSS if present const savedCSSContent = localStorage.getItem('customPreviewCSSContent'); if (savedCSSContent) { applyCustomPreviewCSS(savedCSSContent); } const ReplPanel = getReplPanel(); replPanel = new ReplPanel(); // View menu: toggle the bottom REPL panel ipcRenderer.on('toggle-bottom-panel', () => { replPanel.toggle(); }); // Populate app version from main process window.electronAPI.getAppVersion().then((version) => { const el = document.getElementById('app-version-display'); if (el) el.textContent = `v${version}`; }); // Initialize ModalManager for all dialogs const findModal = new ModalManager('#find-dialog'); const exportModal = new ModalManager('#export-dialog'); const printPreviewModal = new ModalManager('#print-preview-overlay'); const tableModal = new ModalManager('#table-generator-dialog'); const asciiModal = new ModalManager('#ascii-art-dialog'); const converterModal = new ModalManager('#universal-converter-dialog'); const batchModal = new ModalManager('#batch-dialog'); const pdfEditorModal = new ModalManager('#pdf-editor-dialog'); const headerFooterModal = new ModalManager('#header-footer-dialog'); const fieldPickerModal = new ModalManager('#field-picker-dialog'); const wordTemplateModal = new ModalManager('#word-template-dialog'); // Make modals globally accessible for functions outside this scope window.modals = { findModal, exportModal, printPreviewModal, tableModal, asciiModal, converterModal, batchModal, pdfEditorModal, headerFooterModal, fieldPickerModal, wordTemplateModal, }; // Initialize sidebar const SidebarManager = getSidebarManager(); const sidebarManager = new SidebarManager(); const explorerCurrentDir = null; sidebarManager.registerPanel('explorer', { title: 'Explorer', render: (container) => getRenderExplorerPanel()(container, { listDirectory: (dir) => ipcRenderer.invoke('list-directory', dir), onFileOpen: (filePath) => ipcRenderer.send('open-file-path', filePath), currentDir: explorerCurrentDir, }), }); sidebarManager.registerPanel('git', { title: 'Git', render: (container) => getRenderGitPanel()(container, { gitStatus: () => ipcRenderer.invoke('git-status'), gitDiff: (file) => ipcRenderer.invoke('git-diff', { file, }), gitStage: (files) => ipcRenderer.invoke('git-stage', { files, }), gitCommit: (message) => ipcRenderer.invoke('git-commit', { message, }), gitLog: () => ipcRenderer.invoke('git-log'), gitBranches: () => ipcRenderer.invoke('git-branches'), gitCheckout: (name, isNew) => ipcRenderer.invoke('git-checkout', { name, isNew, }), gitPush: () => ipcRenderer.invoke('git-push'), gitPull: () => ipcRenderer.invoke('git-pull'), }), }); sidebarManager.registerPanel('snippets', { title: 'Snippets', render: (container) => getRenderSnippetsPanel()(container, { getSnippets: () => ipcRenderer.invoke('get-snippets'), saveSnippet: (s) => ipcRenderer.invoke('save-snippet', s), deleteSnippet: (id) => ipcRenderer.invoke('delete-snippet', id), onInsert: (code) => tabManager.insertAtCursor(code), }), }); sidebarManager.registerPanel('templates', { title: 'Templates', render: (container) => getRenderTemplatesPanel()(container, loadTemplateIntoNewTab), }); let outlinePanelContainer = null; sidebarManager.registerPanel('outline', { title: 'Outline', render: (container) => { outlinePanelContainer = container; getRenderOutlinePanel()(container, { getEditorContent: () => tabManager.getEditorContent(), getActiveLine: () => { const tab = tabManager.tabs.get(tabManager.activeTabId); if (tab?.editorView) { const pos = tab.editorView.state.selection.main.head; return tab.editorView.state.doc.lineAt(pos).number; } return 1; }, onHeadingClick: (line) => { const tab = tabManager.tabs.get(tabManager.activeTabId); if (tab?.editorView) { const pos = tab.editorView.state.doc.line(line).from; tab.editorView.dispatch({ selection: { anchor: pos, }, scrollIntoView: true, }); tab.editorView.focus(); } }, }); }, }); // View menu: toggle a sidebar panel by id (explorer/git/snippets/templates/outline) ipcRenderer.on('toggle-sidebar-panel', (event, panelId) => { sidebarManager.togglePanel(panelId); }); // --- Plugin System --- const { PluginLoader } = require('./plugins/plugin-loader'); const { PluginRegistry } = require('./plugins/plugin-registry'); const { EventBus } = require('./plugins/event-bus'); const { SettingsStore } = require('./plugins/settings-store'); const { FormatRegistry } = require('./plugins/format-registry'); const pluginPath = require('path'); const pluginEventBus = new EventBus(); const pluginFormatRegistry = new FormatRegistry(); const pluginSettings = new SettingsStore({ get: (key) => window.electronAPI.invoke('plugin-settings:get', key), set: (key, value) => window.electronAPI.invoke('plugin-settings:set', { key, value, }), }); const statusBarRight = document.querySelector('.status-bar-right'); // Initialize command palette const CommandPalette = getCommandPalette(); const commandPalette = new CommandPalette(); const pluginRegistry = new PluginRegistry({ sidebar: sidebarManager, commands: commandPalette, statusBar: { registerIndicator: (id, opts) => { const el = document.createElement('span'); el.className = 'status-item'; el.id = `plugin-status-${id}`; if (opts.tooltip) el.title = opts.tooltip; el.textContent = opts.text || ''; const sep = document.createElement('span'); sep.className = 'status-separator'; sep.textContent = '|'; if (statusBarRight) { statusBarRight.appendChild(sep); statusBarRight.appendChild(el); } }, }, eventBus: pluginEventBus, settings: pluginSettings, editor: { getContent: () => tabManager.getCurrentContent(), getSelection: () => tabManager.getSelection(), insertAtCursor: (text) => tabManager.insertAtCursor(text), onContentChanged: (cb) => pluginEventBus.on('document:changed', cb), }, ipc: { invoke: (ch, data) => window.electronAPI.invoke(ch, data), on: (ch, cb) => window.electronAPI.on(ch, cb), }, formatRegistry: pluginFormatRegistry, }); const builtInDir = pluginPath.join(__dirname, 'plugins', 'built-in'); const loader = new PluginLoader([builtInDir]); const discovered = loader.discoverPlugins(); discovered.forEach((p) => pluginRegistry.register(p)); // Tell the main process which export formats plugins registered, so the // Export menu (built in main.js, a separate process from this one) can // grow entries for them. Only serializable metadata crosses the IPC // boundary — the handler functions stay here in the renderer, since // that's the only process that actually holds them. ipcRenderer.send( 'plugin-export-formats-registered', pluginFormatRegistry.getAll().map(({ id, label, extension }) => ({ id, label, extension })) ); // Main process resolved a save path (via the Export menu) for a // plugin-registered format; run the plugin's handler with the live // editor content and report back so main can show success/error UI. ipcRenderer.on('run-plugin-export-format', async (event, { id, outputPath } = {}) => { const entry = pluginFormatRegistry.get(id); if (!entry || typeof entry.handler !== 'function') { ipcRenderer.send('plugin-export-format-result', { id, outputPath, success: false, error: `Export format "${id}" is not registered.`, }); return; } try { const content = tabManager.getCurrentContent(); await entry.handler(content, outputPath, {}); ipcRenderer.send('plugin-export-format-result', { id, outputPath, success: true }); } catch (err) { ipcRenderer.send('plugin-export-format-result', { id, outputPath, success: false, error: err && err.message ? err.message : String(err), }); } }); // Initialize Zen Mode const ZenModeClass = getZenMode(); const zenMode = new ZenModeClass(tabManager); // Apply initial monospace font + ligatures from settings // (Task 20 will add the dedicated IPC; until then we read defaults) if (window.electronAPI && typeof window.electronAPI.invoke === 'function') { window.electronAPI .invoke('get-monospace-settings') .then(applyMonospaceClasses) .catch(() => applyMonospaceClasses(null)); } else { applyMonospaceClasses(null); } // Reachable UI control: View > Monospace Font menu (main.js) sends the // requested change here; persist it via the existing setter and apply it // live the same way the initial load above does. ipcRenderer.on('monospace-setting-change', (event, partial) => { if (!window.electronAPI || typeof window.electronAPI.invoke !== 'function') return; window.electronAPI .invoke('set-monospace-settings', partial) .then(applyMonospaceClasses) .catch(() => {}); }); // Welcome tab on startup const hasLaunched = localStorage.getItem('hasLaunchedBefore'); const showWelcome = localStorage.getItem('showWelcomeOnStartup') !== 'false'; if (!hasLaunched || showWelcome) { localStorage.setItem('hasLaunchedBefore', 'true'); // Set welcome content in the first tab's preview const recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]'); const appVersion = await window.electronAPI.getAppVersion().catch(() => ''); const welcomeHtml = getCreateWelcomeContent()(recentFiles, appVersion); const tab = tabManager.tabs.get(tabManager.activeTabId); if (tab) { tab.title = 'Welcome'; tab.content = ''; const preview = document.getElementById(`preview-${tab.id}`); if (preview) { preview.innerHTML = welcomeHtml; // Wire up card actions preview.querySelectorAll('.welcome-card').forEach((card) => { card.addEventListener('click', () => { const action = card.dataset.action; if (action === 'new-file') tabManager.createNewTab(); else if (action === 'open-file') ipcRenderer.send('menu-open'); else if (action === 'open-template') sidebarManager.togglePanel('templates'); else if (action === 'command-palette') { if (typeof commandPalette !== 'undefined') commandPalette.open(); } }); }); // Wire up recent files preview.querySelectorAll('.welcome-recent-item').forEach((item) => { item.addEventListener('click', () => ipcRenderer.send('open-file-path', item.dataset.path) ); }); // Wire up show-on-startup checkbox const checkbox = document.getElementById('show-welcome-startup'); if (checkbox) { checkbox.addEventListener('change', () => { localStorage.setItem('showWelcomeOnStartup', checkbox.checked); }); } } tabManager.updateTabBar(); } } // Image paste handler document.addEventListener('paste', async (e) => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith('image/')) { e.preventDefault(); const blob = item.getAsFile(); const reader = new FileReader(); reader.onload = async () => { const base64 = reader.result.split(',')[1]; const ext = item.type.split('/')[1] === 'png' ? 'png' : 'jpg'; const result = await ipcRenderer.invoke('save-pasted-image', { base64, ext, }); if (result?.relativePath) { tabManager.insertAtCursor(`![image](${result.relativePath})\n`); } }; reader.readAsDataURL(blob); break; } } }); // Drag and drop images const editorContainer = document.querySelector('.editor-container'); editorContainer?.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }); editorContainer?.addEventListener('drop', async (e) => { e.preventDefault(); const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/')); for (const file of files) { const reader = new FileReader(); reader.onload = async () => { const base64 = reader.result.split(',')[1]; const ext = file.name.split('.').pop() || 'png'; const result = await ipcRenderer.invoke('save-pasted-image', { base64, ext, }); if (result?.relativePath) { tabManager.insertAtCursor(`![${file.name}](${result.relativePath})\n`); } }; reader.readAsDataURL(file); } }); // Initialize print preview const PrintPreview = getPrintPreview(); const printPreview = new PrintPreview( window.__monospaceSettings || { monospaceFont: 'jetbrains-mono', monospaceLigatures: false } ); window.__printPreview = printPreview; // Register commands commandPalette.register('New File', 'Ctrl+N', () => tabManager.createNewTab()); commandPalette.register('Open File', 'Ctrl+O', () => ipcRenderer.send('menu-open')); commandPalette.register('Save', 'Ctrl+S', () => { const content = tabManager.getCurrentContent(); ipcRenderer.send('save-current-file', content); }); commandPalette.register('Toggle Preview', '', () => { tabManager.isPreviewVisible = !tabManager.isPreviewVisible; tabManager.updatePreviewVisibility(); }); commandPalette.register('Toggle Line Numbers', '', () => { tabManager.showLineNumbers = !tabManager.showLineNumbers; tabManager.updateLineNumbers(); }); commandPalette.register('Bold', 'Ctrl+B', () => tabManager.wrapSelection('**', '**')); commandPalette.register('Italic', 'Ctrl+I', () => tabManager.wrapSelection('*', '*')); commandPalette.register('Insert Table', '', () => tabManager.insertTable()); commandPalette.register('Insert Code Block', '', () => tabManager.insertCodeBlock()); commandPalette.register('Insert Horizontal Rule', '', () => tabManager.insertHorizontalRule()); commandPalette.register('Find & Replace', 'Ctrl+F', () => { const findDialog = document.getElementById('find-dialog'); if (findDialog) { findDialog.classList.toggle('hidden'); if (!findDialog.classList.contains('hidden')) { document.getElementById('find-input').focus(); } } }); commandPalette.register('Toggle Sidebar: Explorer', 'Ctrl+Shift+E', () => sidebarManager.togglePanel('explorer') ); commandPalette.register('Toggle Sidebar: Git', 'Ctrl+Shift+G', () => sidebarManager.togglePanel('git') ); commandPalette.register('Toggle Sidebar: Snippets', '', () => sidebarManager.togglePanel('snippets') ); commandPalette.register('Toggle Sidebar: Templates', '', () => sidebarManager.togglePanel('templates') ); commandPalette.register('Toggle Sidebar: Outline', 'Ctrl+Shift+O', () => sidebarManager.togglePanel('outline') ); commandPalette.register('Print Preview', 'Ctrl+P', () => { const tab = tabManager.tabs.get(tabManager.activeTabId); const preview = document.getElementById(`preview-${tab.id}`); printPreview.open(preview?.innerHTML || ''); }); commandPalette.register('Export to PDF', '', () => ipcRenderer.send('export', 'pdf')); commandPalette.register('Export to DOCX', '', () => ipcRenderer.send('export', 'docx')); commandPalette.register('Export to HTML', '', () => ipcRenderer.send('export', 'html')); commandPalette.register('New Tab', 'Ctrl+T', () => tabManager.createNewTab()); commandPalette.register('Close Tab', 'Ctrl+W', () => tabManager.closeTab(tabManager.activeTabId)); commandPalette.register('Undo', 'Ctrl+Z', () => { const tab = tabManager.tabs.get(tabManager.activeTabId); if (tab?.editorView) undo(tab.editorView); }); commandPalette.register('Redo', 'Ctrl+Shift+Z', () => { const tab = tabManager.tabs.get(tabManager.activeTabId); if (tab?.editorView) redo(tab.editorView); }); commandPalette.register('Heading 1', '', () => tabManager.insertAtLineStart('# ')); commandPalette.register('Heading 2', '', () => tabManager.insertAtLineStart('## ')); commandPalette.register('Heading 3', '', () => tabManager.insertAtLineStart('### ')); commandPalette.register('Insert Link', '', () => tabManager.wrapSelection('[', '](url)')); commandPalette.register('Insert Image', '', () => tabManager.wrapSelection('![', '](image.jpg)')); commandPalette.register('Toggle Zen Mode', 'F11', () => zenMode.toggle()); commandPalette.register('Writing Analytics', 'Ctrl+Shift+A', () => getShowAnalyticsModal()(tabManager) ); commandPalette.register('Load Custom Preview CSS', '', triggerLoadCustomCSS); commandPalette.register('Clear Custom Preview CSS', '', triggerClearCustomCSS); // View menu: toggle the Command Palette ipcRenderer.on('toggle-command-palette', () => { if (commandPalette.isOpen()) { commandPalette.close(); } else { commandPalette.open(); } }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // Ctrl+Shift+P — Command Palette if (e.ctrlKey && e.shiftKey && e.key === 'P') { e.preventDefault(); commandPalette.open(); } // F11 — Zen Mode if (e.key === 'F11') { e.preventDefault(); zenMode.toggle(); } // Ctrl+Shift+A — Writing Analytics if (e.ctrlKey && e.shiftKey && e.key === 'A') { e.preventDefault(); getShowAnalyticsModal()(tabManager); } // Escape — Exit Zen Mode if (e.key === 'Escape' && zenMode.active) { zenMode.deactivate(); } }); // Initialize CodeMirror for the initial tab (tab 1) const initialEditorContainer = document.getElementById('editor-cm-1'); if (initialEditorContainer) { const tab = tabManager.tabs.get(1); const isDark = document.body.className.includes('dark'); tab.editorView = createEditor(initialEditorContainer, { content: tab.content, onChange: (newContent) => { tab.content = newContent; tab.isDirty = true; tabManager.updatePreview(tab.id); tabManager.updateWordCount(); tabManager.updateTabBar(); if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline(); }, onUpdate: (view) => { tabManager.updateCursorPosition(view); if (outlinePanelContainer?._setActiveHeading) outlinePanelContainer._setActiveHeading( view.state.doc.lineAt(view.state.selection.main.head).number ); }, isDark, showLineNumbers: tabManager.showLineNumbers, }); } // Request current theme ipcRenderer.send('get-theme'); // Also send renderer-ready immediately as backup // This ensures we don't get stuck waiting for theme-changed setTimeout(() => { ipcRenderer.send('renderer-ready'); }, 100); // Set up auto-save interval setInterval(() => { // Auto-save logic for all tabs tabManager.tabs.forEach((tab) => { if (tab.isDirty && tab.filePath) { ipcRenderer.send('save-current-file', { filePath: tab.filePath, content: tab.content, }); } }); }, 30000); }); // IPC event listeners // IPC event listeners ipcRenderer.on('file-new', () => { tabManager.createNewTab(); }); ipcRenderer.on('load-template-menu', (event, file) => { loadTemplateIntoNewTab(file); }); ipcRenderer.on('file-opened', (event, data) => { // Diagnostic: check library state if (tabManager) { tabManager.openFile(data.path, data.content); } else { console.error('[RENDERER] tabManager not initialized!'); } }); ipcRenderer.on('file-save', () => { const currentContent = tabManager.getCurrentContent(); const currentFilePath = tabManager.getCurrentFilePath(); // send to main process which will save or trigger save-as dialog ipcRenderer.send('save-current-file', { filePath: currentFilePath, content: 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) => { const currentContent = tabManager.getCurrentContent(); ipcRenderer.send('export-spreadsheet', { content: currentContent, format, }); }); ipcRenderer.on('toggle-preview', () => { tabManager.isPreviewVisible = !tabManager.isPreviewVisible; tabManager.updatePreviewVisibility(); }); ipcRenderer.on('toggle-find', () => { if (window.modals.findModal.isOpen()) { window.modals.findModal.close(); } else { window.modals.findModal.open(); document.getElementById('find-input').focus(); } }); ipcRenderer.on('theme-changed', (event, theme) => { document.body.className = `theme-${theme}`; // After theme is applied, wait for next frame then signal renderer is ready // This ensures complete UI initialization before files are opened requestAnimationFrame(() => { ipcRenderer.send('renderer-ready'); }); }); // Undo/Redo handlers — delegate to CodeMirror's built-in history ipcRenderer.on('undo', () => { if (tabManager) { const tab = tabManager.tabs.get(tabManager.activeTabId); if (tab?.editorView) { undo(tab.editorView); } } }); ipcRenderer.on('redo', () => { if (tabManager) { const tab = tabManager.tabs.get(tabManager.activeTabId); if (tab?.editorView) { redo(tab.editorView); } } }); // Custom Preview CSS event handlers and trigger helpers function applyCustomPreviewCSS(cssContent) { let styleTag = document.getElementById('custom-preview-style'); if (!styleTag) { styleTag = document.createElement('style'); styleTag.id = 'custom-preview-style'; document.head.appendChild(styleTag); } const scopedCSS = scopeCSS(cssContent, '.preview-content'); styleTag.textContent = scopedCSS; } async function triggerLoadCustomCSS() { try { const result = await window.electronAPI.invoke('select-custom-css'); if (result) { localStorage.setItem('customPreviewCSSPath', result.path); localStorage.setItem('customPreviewCSSContent', result.content); applyCustomPreviewCSS(result.content); notifyUser('Custom preview stylesheet applied successfully.', 'success'); } } catch (err) { console.error('[RENDERER] Error loading custom CSS:', err); notifyUser('Failed to load custom CSS stylesheet.', 'error'); } } function triggerClearCustomCSS() { localStorage.removeItem('customPreviewCSSPath'); localStorage.removeItem('customPreviewCSSContent'); const styleTag = document.getElementById('custom-preview-style'); if (styleTag) { styleTag.remove(); } notifyUser('Custom preview stylesheet cleared.', 'info'); } ipcRenderer.on('load-custom-css', triggerLoadCustomCSS); ipcRenderer.on('clear-custom-css', triggerClearCustomCSS); // Font size adjustment let currentFontSize = parseInt(localStorage.getItem('fontSize')) || 15; function updateFontSizes(size) { const editors = document.querySelectorAll('.cm-editor'); const previews = document.querySelectorAll('#preview, .preview-content'); editors.forEach((editor) => { editor.style.fontSize = `${size}px`; }); previews.forEach((preview) => { preview.style.fontSize = `${size}px`; }); localStorage.setItem('fontSize', size); } // Apply saved font size on load updateFontSizes(currentFontSize); ipcRenderer.on('adjust-font-size', (event, action) => { if (action === 'increase' && currentFontSize < 24) { currentFontSize++; } else if (action === 'decrease' && currentFontSize > 10) { currentFontSize--; } else if (action === 'reset') { currentFontSize = 15; } updateFontSizes(currentFontSize); }); // Print preview request handlers - open print preview dialog ipcRenderer.on('print-preview', () => { openPrintPreviewDialog(); }); ipcRenderer.on('print-preview-styled', () => { openPrintPreviewDialog(); }); function openPrintPreviewDialog() { const activeTabId = tabManager ? tabManager.activeTabId : 1; const previewContent = document.getElementById(`preview-${activeTabId}`); if (!previewContent || !previewContent.innerHTML.trim()) { notifyUser( 'Nothing to print. Create or open a document and keep the preview visible.', 'warning' ); return; } const PrintPreviewClass = getPrintPreview(); const printPreviewInstance = new PrintPreviewClass( window.__monospaceSettings || { monospaceFont: 'jetbrains-mono', monospaceLigatures: false } ); printPreviewInstance.open(previewContent.innerHTML); } // Export Dialog functionality let currentExportFormat = null; ipcRenderer.on('show-export-dialog', (event, format) => { currentExportFormat = format; showExportDialog(format); }); function showExportDialog(format) { const dialog = document.getElementById('export-dialog'); const title = document.getElementById('export-dialog-title'); if (!dialog) { console.error('Export dialog element not found!'); return; } title.textContent = `Export as ${format.toUpperCase()}`; dialog.setAttribute('data-format', format); window.modals.exportModal.open(); // Initialize form values initializeExportForm(format); // Populate the preset dropdown from the main-process store refreshExportPresets(); } function hideExportDialog() { window.modals.exportModal.close(); currentExportFormat = null; } function initializeExportForm(format) { // Reset advanced export toggle to unchecked const advancedToggle = document.getElementById('advanced-export-toggle'); const advancedOptions = document.getElementById('advanced-export-options'); advancedToggle.checked = false; advancedOptions.classList.add('hidden'); // Reset form to defaults document.getElementById('export-template').value = 'default'; document.getElementById('custom-template-path').style.display = 'none'; // Clear metadata fields const metadataFields = document.querySelectorAll('.metadata-field'); metadataFields.forEach((field, index) => { if (index < 4) { // Keep first 4 default fields field.querySelector('.metadata-key').value = ['title', 'author', 'date', 'subject'][index] || ''; field.querySelector('.metadata-value').value = ''; } else { field.remove(); // Remove additional fields } }); // Reset checkboxes and other fields document.getElementById('export-toc').checked = false; document.getElementById('export-number-sections').checked = false; document.getElementById('export-citeproc').checked = false; document.getElementById('export-toc-depth').value = 3; // Toggle and reset PDF-specific fields const pdfOnly = document.querySelector('.pdf-only'); if (pdfOnly) { pdfOnly.style.display = format === 'pdf' ? 'block' : 'none'; } if (format === 'pdf') { document.getElementById('pdf-engine').value = 'xelatex'; document.getElementById('pdf-geometry').value = 'margin=1in'; document.getElementById('custom-geometry').style.display = 'none'; } // Toggle and reset Reveal.js-specific fields const revealjsOnly = document.querySelector('.revealjs-only'); if (revealjsOnly) { revealjsOnly.style.display = format === 'revealjs' ? 'block' : 'none'; } const revealTheme = document.getElementById('reveal-theme'); if (revealTheme) revealTheme.value = 'black'; const revealTransition = document.getElementById('reveal-transition'); if (revealTransition) revealTransition.value = 'slide'; const revealSpeed = document.getElementById('reveal-speed'); if (revealSpeed) revealSpeed.value = 'default'; const revealSlideNumber = document.getElementById('reveal-slide-number'); if (revealSlideNumber) revealSlideNumber.checked = false; const revealControls = document.getElementById('reveal-controls'); if (revealControls) revealControls.checked = true; const revealProgress = document.getElementById('reveal-progress'); if (revealProgress) revealProgress.checked = true; const revealHistory = document.getElementById('reveal-history'); if (revealHistory) revealHistory.checked = true; const revealCenter = document.getElementById('reveal-center'); if (revealCenter) revealCenter.checked = true; // Clear bibliography fields document.getElementById('bibliography-file').value = ''; document.getElementById('csl-file').value = ''; // Request current page settings from main process and apply them ipcRenderer.send('get-page-settings'); } function collectExportOptions() { const advancedMode = document.getElementById('advanced-export-toggle').checked; const options = {}; if (advancedMode) { // Collect advanced options options.template = document.getElementById('export-template').value; options.metadata = {}; options.variables = {}; options.toc = document.getElementById('export-toc').checked; options.tocDepth = document.getElementById('export-toc-depth').value; options.numberSections = document.getElementById('export-number-sections').checked; options.citeproc = document.getElementById('export-citeproc').checked; } else { // Collect basic options only options.template = 'default'; options.metadata = {}; options.variables = {}; options.toc = document.getElementById('basic-toc').checked; options.tocDepth = 3; options.numberSections = document.getElementById('basic-number-sections').checked; options.citeproc = false; } if (advancedMode) { // Collect custom template path if (options.template === 'custom') { options.template = document.getElementById('custom-template-path').value.trim(); } // Collect metadata const metadataFields = document.querySelectorAll('.metadata-field'); metadataFields.forEach((field) => { const key = field.querySelector('.metadata-key').value.trim(); const value = field.querySelector('.metadata-value').value.trim(); if (key && value) { options.metadata[key] = value; } }); // PDF-specific options if (currentExportFormat === 'pdf') { options.pdfEngine = document.getElementById('pdf-engine').value; const geometrySelect = document.getElementById('pdf-geometry'); if (geometrySelect.value === 'custom') { options.geometry = document.getElementById('custom-geometry').value.trim() || 'margin=1in'; } else { options.geometry = geometrySelect.value; } } // Reveal.js-specific options if (currentExportFormat === 'revealjs') { options.revealTheme = document.getElementById('reveal-theme').value; options.revealTransition = document.getElementById('reveal-transition').value; options.revealTransitionSpeed = document.getElementById('reveal-speed').value; options.revealSlideNumber = document.getElementById('reveal-slide-number').checked; options.revealControls = document.getElementById('reveal-controls').checked; options.revealProgress = document.getElementById('reveal-progress').checked; options.revealHistory = document.getElementById('reveal-history').checked; options.revealCenter = document.getElementById('reveal-center').checked; } // Bibliography const bibFile = document.getElementById('bibliography-file').value.trim(); const cslFile = document.getElementById('csl-file').value.trim(); if (bibFile) options.bibliography = bibFile; if (cslFile) options.csl = cslFile; } else { // Basic mode - set default PDF options if needed if (currentExportFormat === 'pdf') { options.pdfEngine = 'xelatex'; options.geometry = 'margin=1in'; } else if (currentExportFormat === 'revealjs') { options.revealTheme = 'black'; options.revealTransition = 'slide'; options.revealTransitionSpeed = 'default'; options.revealSlideNumber = false; options.revealControls = true; options.revealProgress = true; options.revealHistory = true; options.revealCenter = true; } } // Collect page size and orientation (always collected, from basic options) const pageSize = document.getElementById('page-size').value; const pageOrientation = document.getElementById('page-orientation').value; const customWidth = document.getElementById('custom-width').value.trim(); const customHeight = document.getElementById('custom-height').value.trim(); // Send page settings to main process ipcRenderer.send('update-page-settings', { size: pageSize, orientation: pageOrientation, customWidth: customWidth || null, customHeight: customHeight || null, }); return options; } // Export Presets — the preset dropdown / "Save as preset" logic lives in // src/renderer/export-presets.js; presets are persisted by the main process // in settings.json (get-export-presets / save-export-preset / // delete-export-preset invoke channels), replacing the earlier // localStorage-only export profiles. // Event listeners for export dialog document.addEventListener('DOMContentLoaded', () => { // Template selection document.getElementById('export-template').addEventListener('change', (e) => { const customPath = document.getElementById('custom-template-path'); const fileInput = document.getElementById('template-file-input'); if (e.target.value === 'custom') { customPath.style.display = 'block'; fileInput.style.display = 'block'; } else { customPath.style.display = 'none'; fileInput.style.display = 'none'; customPath.value = ''; } }); // Advanced export toggle document.getElementById('advanced-export-toggle').addEventListener('change', (e) => { const advancedOptions = document.getElementById('advanced-export-options'); if (e.target.checked) { advancedOptions.classList.remove('hidden'); // Scroll the advanced options into view after they become visible setTimeout(() => { advancedOptions.scrollIntoView({ behavior: 'smooth', block: 'nearest', }); }, 100); } else { advancedOptions.classList.add('hidden'); } }); // Template file input document.getElementById('template-file-input').addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { document.getElementById('custom-template-path').value = file.path; } }); // PDF geometry selection document.getElementById('pdf-geometry').addEventListener('change', (e) => { const customGeometry = document.getElementById('custom-geometry'); if (e.target.value === 'custom') { customGeometry.style.display = 'block'; } else { customGeometry.style.display = 'none'; } }); // Page size selection - show/hide custom size inputs document.getElementById('page-size').addEventListener('change', (e) => { const customPageSize = document.getElementById('custom-page-size'); if (e.target.value === 'custom') { customPageSize.style.display = 'block'; } else { customPageSize.style.display = 'none'; } }); // Load saved page settings on startup ipcRenderer.send('get-page-settings'); // Handle page settings data (can be called multiple times) ipcRenderer.on('page-settings-data', (event, settings) => { if (settings) { const pageSizeEl = document.getElementById('page-size'); const pageOrientationEl = document.getElementById('page-orientation'); const customPageSizeEl = document.getElementById('custom-page-size'); const customWidthEl = document.getElementById('custom-width'); const customHeightEl = document.getElementById('custom-height'); if (pageSizeEl) pageSizeEl.value = settings.size || 'a4'; if (pageOrientationEl) pageOrientationEl.value = settings.orientation || 'portrait'; if (customWidthEl && settings.customWidth) { customWidthEl.value = settings.customWidth; } if (customHeightEl && settings.customHeight) { customHeightEl.value = settings.customHeight; } // Show custom inputs if size is custom if (customPageSizeEl) { if (settings.size === 'custom') { customPageSizeEl.style.display = 'block'; } else { customPageSizeEl.style.display = 'none'; } } } }); // Export Preset controls (dropdown, per-row delete, save-as-preset) initExportPresets({ notify: notifyUser }); // Add metadata field document.getElementById('add-metadata-field').addEventListener('click', () => { const container = document.querySelector('.metadata-container'); const newField = document.createElement('div'); newField.className = 'metadata-field'; newField.innerHTML = ` `; container.appendChild(newField); }); // Browse bibliography document.getElementById('browse-bibliography').addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.bib,.yaml,.yml,.json'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { document.getElementById('bibliography-file').value = file.path; } }; input.click(); }); // Browse CSL document.getElementById('browse-csl').addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.csl'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { document.getElementById('csl-file').value = file.path; } }; input.click(); }); // Dialog close buttons document.getElementById('export-dialog-close').addEventListener('click', hideExportDialog); document.getElementById('export-cancel').addEventListener('click', hideExportDialog); // Export confirm document.getElementById('export-confirm').addEventListener('click', () => { const options = collectExportOptions(); ipcRenderer.send('export-with-options', { format: currentExportFormat, options: options, }); hideExportDialog(); }); }); // Batch Conversion Dialog functionality let currentBatchOptions = {}; ipcRenderer.on('show-batch-dialog', () => { showBatchDialog(); }); // Universal Converter dialog handlers 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; }); ipcRenderer.on('conversion-complete', (event, result) => { document.getElementById('converter-progress').classList.add('hidden'); if (result.success) { window.modals.converterModal.close(); } }); ipcRenderer.on('batch-progress', (event, progress) => { updateBatchProgress(progress); }); ipcRenderer.on('folder-selected', (event, { type, path }) => { if (type === 'input') { document.getElementById('batch-input-folder').value = path; validateBatchForm(); } else if (type === 'output') { document.getElementById('batch-output-folder').value = path; validateBatchForm(); } else if (type === 'converter-batch-input') { document.getElementById('converter-batch-input-folder').value = path; } else if (type === 'converter-batch-output') { document.getElementById('converter-batch-output-folder').value = path; } }); function showBatchDialog() { window.modals.batchModal.open(); // Reset form document.getElementById('batch-input-folder').value = ''; document.getElementById('batch-output-folder').value = ''; document.getElementById('batch-format').value = 'html'; document.getElementById('batch-include-subfolders').checked = true; document.getElementById('batch-progress').classList.add('hidden'); document.getElementById('batch-start').disabled = true; currentBatchOptions = { template: 'default', metadata: {}, variables: {}, toc: false, tocDepth: 3, numberSections: false, citeproc: false, }; } function hideBatchDialog() { window.modals.batchModal.close(); } function updateBatchProgress(progress) { const progressSection = document.getElementById('batch-progress'); const progressFill = document.getElementById('batch-progress-fill'); const progressText = document.getElementById('batch-progress-text'); const progressCount = document.getElementById('batch-progress-count'); const startButton = document.getElementById('batch-start'); progressSection.classList.remove('hidden'); const percentage = Math.round((progress.completed / progress.total) * 100); progressFill.style.width = `${percentage}%`; if (progress.completed === progress.total) { progressText.textContent = 'Conversion complete!'; if (startButton) startButton.disabled = false; } else { progressText.textContent = `Processing: ${progress.currentFile}`; } progressCount.textContent = `${progress.completed} / ${progress.total}`; } function validateBatchForm() { const inputFolder = document.getElementById('batch-input-folder').value.trim(); const outputFolder = document.getElementById('batch-output-folder').value.trim(); const startButton = document.getElementById('batch-start'); startButton.disabled = !inputFolder || !outputFolder; } // Event listeners for batch dialog document.addEventListener('DOMContentLoaded', () => { // Browse input folder document.getElementById('browse-input-folder').addEventListener('click', () => { ipcRenderer.send('select-folder', 'input'); }); // Browse output folder document.getElementById('browse-output-folder').addEventListener('click', () => { ipcRenderer.send('select-folder', 'output'); }); // Show advanced options document.getElementById('batch-show-options').addEventListener('click', () => { const format = document.getElementById('batch-format').value; currentExportFormat = format; showExportDialog(format); }); // Dialog close buttons document.getElementById('batch-dialog-close').addEventListener('click', hideBatchDialog); document.getElementById('batch-cancel').addEventListener('click', hideBatchDialog); // Start batch conversion document.getElementById('batch-start').addEventListener('click', () => { const inputFolder = document.getElementById('batch-input-folder').value.trim(); const outputFolder = document.getElementById('batch-output-folder').value.trim(); const format = document.getElementById('batch-format').value; if (!inputFolder || !outputFolder) { return; } // Use current export options from advanced dialog if they were set const options = currentBatchOptions; const includeSubfolders = document.getElementById('batch-include-subfolders').checked; // Start batch conversion ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options, includeSubfolders, }); // Show progress document.getElementById('batch-progress').classList.remove('hidden'); document.getElementById('batch-start').disabled = true; }); // Input validation document.getElementById('batch-input-folder').addEventListener('input', validateBatchForm); document.getElementById('batch-output-folder').addEventListener('input', validateBatchForm); }); // Override the export dialog confirm to also save batch options const originalExportConfirm = document.getElementById('export-confirm'); if (originalExportConfirm) { originalExportConfirm.addEventListener('click', () => { // If batch dialog is open, save options for batch conversion if (window.modals.batchModal.isOpen()) { currentBatchOptions = collectExportOptions(); } }); } // Universal File Converter Dialog Functions let converterFilePath = ''; // Format definitions for each converter const converterFormats = { libreoffice: { input: [ { value: 'docx', label: 'Word Document (DOCX)', }, { value: 'doc', label: 'Word 97-2003 (DOC)', }, { value: 'odt', label: 'OpenDocument Text (ODT)', }, { value: 'rtf', label: 'Rich Text Format (RTF)', }, { value: 'txt', label: 'Plain Text (TXT)', }, { value: 'html', label: 'HTML Document', }, { value: 'htm', label: 'HTM Document', }, { value: 'xlsx', label: 'Excel Spreadsheet (XLSX)', }, { value: 'xls', label: 'Excel 97-2003 (XLS)', }, { value: 'ods', label: 'OpenDocument Spreadsheet (ODS)', }, { value: 'csv', label: 'Comma Separated Values (CSV)', }, { value: 'pptx', label: 'PowerPoint (PPTX)', }, { value: 'ppt', label: 'PowerPoint 97-2003 (PPT)', }, { value: 'odp', label: 'OpenDocument Presentation (ODP)', }, ], output: [ { value: 'pdf', label: 'PDF Document', }, { value: 'docx', label: 'Word Document (DOCX)', }, { value: 'doc', label: 'Word 97-2003 (DOC)', }, { value: 'odt', label: 'OpenDocument Text (ODT)', }, { value: 'rtf', label: 'Rich Text Format (RTF)', }, { value: 'txt', label: 'Plain Text (TXT)', }, { value: 'html', label: 'HTML Document', }, { value: 'xlsx', label: 'Excel Spreadsheet (XLSX)', }, { value: 'xls', label: 'Excel 97-2003 (XLS)', }, { value: 'ods', label: 'OpenDocument Spreadsheet (ODS)', }, { value: 'csv', label: 'CSV', }, { value: 'pptx', label: 'PowerPoint (PPTX)', }, { value: 'ppt', label: 'PowerPoint 97-2003 (PPT)', }, { value: 'odp', label: 'OpenDocument Presentation (ODP)', }, ], }, imagemagick: { input: [ { value: 'jpg', label: 'JPEG Image (JPG)', }, { value: 'jpeg', label: 'JPEG Image (JPEG)', }, { value: 'png', label: 'PNG Image', }, { value: 'gif', label: 'GIF Image', }, { value: 'bmp', label: 'Bitmap Image (BMP)', }, { value: 'tiff', label: 'TIFF Image', }, { value: 'tif', label: 'TIF Image', }, { value: 'webp', label: 'WebP Image', }, { value: 'svg', label: 'SVG Vector Image', }, { value: 'ico', label: 'Icon File (ICO)', }, { value: 'psd', label: 'Photoshop (PSD)', }, { value: 'raw', label: 'RAW Image', }, { value: 'cr2', label: 'Canon RAW (CR2)', }, { value: 'nef', label: 'Nikon RAW (NEF)', }, { value: 'heic', label: 'HEIC Image', }, { value: 'avif', label: 'AVIF Image', }, ], output: [ { value: 'jpg', label: 'JPEG Image (JPG)', }, { value: 'png', label: 'PNG Image', }, { value: 'gif', label: 'GIF Image', }, { value: 'bmp', label: 'Bitmap Image (BMP)', }, { value: 'tiff', label: 'TIFF Image', }, { value: 'webp', label: 'WebP Image', }, { value: 'svg', label: 'SVG Vector Image', }, { value: 'ico', label: 'Icon File (ICO)', }, { value: 'pdf', label: 'PDF Document', }, { value: 'eps', label: 'EPS Vector', }, { value: 'ps', label: 'PostScript', }, { value: 'avif', label: 'AVIF Image', }, ], }, ffmpeg: { input: [ { value: 'mp4', label: 'MP4 Video', }, { value: 'avi', label: 'AVI Video', }, { value: 'mov', label: 'MOV Video (QuickTime)', }, { value: 'mkv', label: 'MKV Video (Matroska)', }, { value: 'wmv', label: 'WMV Video (Windows Media)', }, { value: 'flv', label: 'FLV Video (Flash)', }, { value: 'webm', label: 'WebM Video', }, { value: 'mpeg', label: 'MPEG Video', }, { value: 'mpg', label: 'MPG Video', }, { value: 'm4v', label: 'M4V Video', }, { value: 'mp3', label: 'MP3 Audio', }, { value: 'wav', label: 'WAV Audio', }, { value: 'ogg', label: 'OGG Audio', }, { value: 'flac', label: 'FLAC Audio', }, { value: 'aac', label: 'AAC Audio', }, { value: 'm4a', label: 'M4A Audio', }, { value: 'wma', label: 'WMA Audio', }, ], output: [ { value: 'mp4', label: 'MP4 Video', }, { value: 'avi', label: 'AVI Video', }, { value: 'mov', label: 'MOV Video (QuickTime)', }, { value: 'mkv', label: 'MKV Video (Matroska)', }, { value: 'webm', label: 'WebM Video', }, { value: 'mpeg', label: 'MPEG Video', }, { value: 'gif', label: 'Animated GIF', }, { value: 'mp3', label: 'MP3 Audio', }, { value: 'wav', label: 'WAV Audio', }, { value: 'ogg', label: 'OGG Audio', }, { value: 'flac', label: 'FLAC Audio', }, { value: 'aac', label: 'AAC Audio', }, { value: 'm4a', label: 'M4A Audio', }, ], }, pandoc: { input: [ { value: 'md', label: 'Markdown (MD)', }, { value: 'markdown', label: 'Markdown', }, { value: 'html', label: 'HTML Document', }, { value: 'docx', label: 'Word Document (DOCX)', }, { value: 'odt', label: 'OpenDocument Text (ODT)', }, { value: 'rtf', label: 'Rich Text Format (RTF)', }, { value: 'tex', label: 'LaTeX Document', }, { value: 'latex', label: 'LaTeX', }, { value: 'epub', label: 'EPUB eBook', }, { value: 'rst', label: 'reStructuredText (RST)', }, { value: 'textile', label: 'Textile', }, { value: 'org', label: 'Org Mode', }, { value: 'mediawiki', label: 'MediaWiki', }, { value: 'docbook', label: 'DocBook XML', }, ], output: [ { value: 'html', label: 'HTML Document', }, { value: 'pdf', label: 'PDF Document', }, { value: 'docx', label: 'Word Document (DOCX)', }, { value: 'odt', label: 'OpenDocument Text (ODT)', }, { value: 'rtf', label: 'Rich Text Format (RTF)', }, { value: 'epub', label: 'EPUB eBook', }, { value: 'latex', label: 'LaTeX Document', }, { value: 'md', label: 'Markdown (MD)', }, { value: 'rst', label: 'reStructuredText (RST)', }, { value: 'textile', label: 'Textile', }, { value: 'org', label: 'Org Mode', }, { value: 'mediawiki', label: 'MediaWiki', }, { value: 'docbook', label: 'DocBook XML', }, { value: 'pptx', label: 'PowerPoint (PPTX)', }, ], }, }; function showUniversalConverterDialog() { window.modals.converterModal.open(); converterFilePath = ''; document.getElementById('converter-file-path').value = ''; document.getElementById('converter-tool').value = 'libreoffice'; document.getElementById('converter-progress').classList.add('hidden'); updateConverterFormats('libreoffice'); } function updateConverterFormats(tool) { const fromSelect = document.getElementById('converter-from'); const toSelect = document.getElementById('converter-to'); const helpText = document.getElementById('converter-tool-help'); // Clear existing options fromSelect.innerHTML = ''; toSelect.innerHTML = ''; // Get formats for selected tool const formats = converterFormats[tool]; if (formats) { // Populate input formats formats.input.forEach((format) => { const option = document.createElement('option'); option.value = format.value; option.textContent = format.label; fromSelect.appendChild(option); }); // Populate output formats formats.output.forEach((format) => { const option = document.createElement('option'); option.value = format.value; option.textContent = format.label; toSelect.appendChild(option); }); // Update help text if (tool === 'libreoffice') { helpText.textContent = 'Documents, Spreadsheets, Presentations - Office file conversions'; } else if (tool === 'imagemagick') { helpText.textContent = 'Image format conversions - JPG, PNG, GIF, TIFF, WebP, SVG, and more'; } else if (tool === 'ffmpeg') { helpText.textContent = 'Video and audio conversions - MP4, AVI, MOV, MP3, WAV, and more'; } else if (tool === 'pandoc') { helpText.textContent = 'Document markup conversions - Markdown, HTML, LaTeX, EPUB, and more'; } } } function updateConverterAdvancedOptions(tool) { // Hide all tool-specific options const allOptions = document.querySelectorAll('.converter-options'); allOptions.forEach((opt) => opt.classList.add('hidden')); // Show options for selected tool const toolOptions = document.querySelector(`.${tool}-options`); if (toolOptions) { toolOptions.classList.remove('hidden'); } } function collectConverterAdvancedOptions(tool) { const options = {}; const advancedMode = document.getElementById('converter-advanced-toggle').checked; if (!advancedMode) { return options; } // Tool-specific options if (tool === 'imagemagick') { options.quality = document.getElementById('imagemagick-quality').value; options.dpi = document.getElementById('imagemagick-dpi').value || null; options.resize = document.getElementById('imagemagick-resize').value || null; options.compression = document.getElementById('imagemagick-compression').value || null; } else if (tool === 'ffmpeg') { options.videoCodec = document.getElementById('ffmpeg-video-codec').value || null; options.audioCodec = document.getElementById('ffmpeg-audio-codec').value || null; options.bitrate = document.getElementById('ffmpeg-bitrate').value || null; options.preset = document.getElementById('ffmpeg-preset').value || null; options.framerate = document.getElementById('ffmpeg-framerate').value || null; } else if (tool === 'libreoffice') { options.quality = document.getElementById('libreoffice-quality').value || null; options.pageRange = document.getElementById('libreoffice-page-range').value || null; options.exportBookmarks = document.getElementById('libreoffice-export-bookmarks').checked; } return options; } document.addEventListener('DOMContentLoaded', () => { // Universal Converter tool change const converterTool = document.getElementById('converter-tool'); if (converterTool) { converterTool.addEventListener('change', (e) => { updateConverterFormats(e.target.value); updateConverterAdvancedOptions(e.target.value); }); } // Batch mode toggle const converterBatchMode = document.getElementById('converter-batch-mode'); if (converterBatchMode) { converterBatchMode.addEventListener('change', (e) => { const batchOptions = document.getElementById('converter-batch-options'); const singleFileSection = document .getElementById('converter-file-path') .closest('.export-section'); if (e.target.checked) { batchOptions.classList.remove('hidden'); singleFileSection.style.display = 'none'; } else { batchOptions.classList.add('hidden'); singleFileSection.style.display = 'block'; } }); } // Advanced options toggle const converterAdvancedToggle = document.getElementById('converter-advanced-toggle'); if (converterAdvancedToggle) { converterAdvancedToggle.addEventListener('change', (e) => { const advancedOptions = document.getElementById('converter-advanced-options'); if (e.target.checked) { advancedOptions.classList.remove('hidden'); // Update which tool-specific options to show updateConverterAdvancedOptions(document.getElementById('converter-tool').value); } else { advancedOptions.classList.add('hidden'); } }); } // ImageMagick quality slider const imagemagickQuality = document.getElementById('imagemagick-quality'); if (imagemagickQuality) { imagemagickQuality.addEventListener('input', (e) => { document.getElementById('imagemagick-quality-value').textContent = e.target.value; }); } // Browse batch input folder const browseBatchInput = document.getElementById('browse-converter-batch-input'); if (browseBatchInput) { browseBatchInput.addEventListener('click', () => { ipcRenderer.send('select-folder', 'converter-batch-input'); }); } // Browse batch output folder const browseBatchOutput = document.getElementById('browse-converter-batch-output'); if (browseBatchOutput) { browseBatchOutput.addEventListener('click', () => { ipcRenderer.send('select-folder', 'converter-batch-output'); }); } // Browse for file to convert const browseConverterFile = document.getElementById('browse-converter-file'); if (browseConverterFile) { browseConverterFile.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '*'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { converterFilePath = file.path; document.getElementById('converter-file-path').value = file.path; } }; input.click(); }); } // Universal Converter dialog close const converterDialogClose = document.getElementById('converter-dialog-close'); if (converterDialogClose) { converterDialogClose.addEventListener('click', () => { window.modals.converterModal.close(); }); } // Universal Converter cancel const converterCancel = document.getElementById('converter-cancel'); if (converterCancel) { converterCancel.addEventListener('click', () => { window.modals.converterModal.close(); }); } // Universal Converter convert const converterConvert = document.getElementById('converter-convert'); if (converterConvert) { converterConvert.addEventListener('click', () => { const tool = document.getElementById('converter-tool').value; const fromFormat = document.getElementById('converter-from').value; const toFormat = document.getElementById('converter-to').value; const batchMode = document.getElementById('converter-batch-mode').checked; const advancedOptions = collectConverterAdvancedOptions(tool); if (batchMode) { // Batch conversion const inputFolder = document.getElementById('converter-batch-input-folder').value.trim(); const outputFolder = document.getElementById('converter-batch-output-folder').value.trim(); const includeSubfolders = document.getElementById('converter-batch-subfolders').checked; if (!inputFolder || !outputFolder) { document.getElementById('converter-progress').classList.remove('hidden'); document.getElementById('converter-status').textContent = 'Select both input and output folders for batch conversion.'; return; } // Show progress document.getElementById('converter-progress').classList.remove('hidden'); // Send batch conversion request ipcRenderer.send('universal-convert-batch', { tool, fromFormat, toFormat, inputFolder, outputFolder, includeSubfolders, advancedOptions, }); } else { // Single file conversion const filePath = converterFilePath; if (!filePath) { document.getElementById('converter-progress').classList.remove('hidden'); document.getElementById('converter-status').textContent = 'Select a file to convert.'; return; } // Show progress document.getElementById('converter-progress').classList.remove('hidden'); // Send single file conversion request ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath, advancedOptions, }); } }); } }); // IPC event listeners for recent files functionality ipcRenderer.on('recent-files-cleared', () => { if (tabManager) { tabManager.recentFiles = []; localStorage.setItem('recentFiles', JSON.stringify([])); } }); // ======================================== // PDF Editor Dialog Functionality // ======================================== let currentPDFOperation = null; let mergeFilePaths = []; // Show Table Generator Dialog ipcRenderer.on('show-table-generator', () => { showTableGenerator(); }); // Show PDF Editor Dialog ipcRenderer.on('show-pdf-editor-dialog', (event, operation, openedFilePath) => { currentPDFOperation = operation; showPDFEditorDialog(operation, openedFilePath); }); // Show Media Operations Dialogs (Tools > Image/Audio/Video Tools...) ipcRenderer.on('show-image-converter', () => { showMediaOperationsDialog('image'); }); ipcRenderer.on('show-audio-converter', () => { showMediaOperationsDialog('audio'); }); ipcRenderer.on('show-video-converter', () => { showMediaOperationsDialog('video'); }); // Show Document Compare Dialog (Tools > Document Compare) ipcRenderer.on('show-document-compare', () => { const tab = tabManager?.tabs.get(tabManager.activeTabId); showDocumentCompareDialog({ filePath: tab?.filePath || null }); }); function showPDFEditorDialog(operation, openedFilePath = null) { const title = document.getElementById('pdf-editor-title'); // Hide all operation sections document.querySelectorAll('.pdf-operation-section').forEach((section) => { section.classList.add('hidden'); }); // Hide visual thumbnail sidebar by default const sidebar = document.getElementById('pdf-thumbnail-sidebar'); if (sidebar) { sidebar.classList.add('hidden'); } const body = document.querySelector('.pdf-editor-body'); if (body) { body.classList.remove('side-by-side'); } pdfDocInstance = null; pdfPreviewState = { pages: [], filePath: '', }; // Show the appropriate section and set title let sectionId, titleText; switch (operation) { case 'merge': sectionId = 'pdf-merge-section'; titleText = 'Merge PDFs'; mergeFilePaths = []; // If we have an opened file, add it as the first file to merge if (openedFilePath) { mergeFilePaths.push(openedFilePath); } updateMergeFilesList(); break; case 'split': sectionId = 'pdf-split-section'; titleText = 'Split PDF'; // Pre-fill input path if we have an opened file if (openedFilePath) { document.getElementById('split-input-path').value = openedFilePath; } break; case 'compress': sectionId = 'pdf-compress-section'; titleText = 'Compress PDF'; if (openedFilePath) { document.getElementById('compress-input-path').value = openedFilePath; } break; case 'rotate': sectionId = 'pdf-rotate-section'; titleText = 'Rotate Pages'; if (openedFilePath) { const rotateInput = document.getElementById('rotate-input-path'); if (rotateInput) { rotateInput.value = openedFilePath; // Trigger thumbnail generation setTimeout(() => onPDFFileSelected('rotate-input-path', openedFilePath), 50); } } break; case 'delete': sectionId = 'pdf-delete-section'; titleText = 'Delete Pages'; if (openedFilePath) { const deleteInput = document.getElementById('delete-input-path'); if (deleteInput) { deleteInput.value = openedFilePath; // Trigger thumbnail generation setTimeout(() => onPDFFileSelected('delete-input-path', openedFilePath), 50); } } break; case 'reorder': sectionId = 'pdf-reorder-section'; titleText = 'Reorder Pages'; if (openedFilePath) { const reorderInput = document.getElementById('reorder-input-path'); if (reorderInput) { reorderInput.value = openedFilePath; // Trigger thumbnail generation setTimeout(() => onPDFFileSelected('reorder-input-path', openedFilePath), 50); } } break; case 'watermark': sectionId = 'pdf-watermark-section'; titleText = 'Add Watermark'; if (openedFilePath) { const watermarkInput = document.getElementById('watermark-input-path'); if (watermarkInput) watermarkInput.value = openedFilePath; } break; case 'encrypt': sectionId = 'pdf-encrypt-section'; titleText = 'Password Protection'; if (openedFilePath) { const encryptInput = document.getElementById('encrypt-input-path'); if (encryptInput) encryptInput.value = openedFilePath; } break; case 'decrypt': sectionId = 'pdf-decrypt-section'; titleText = 'Remove Password'; if (openedFilePath) { const decryptInput = document.getElementById('decrypt-input-path'); if (decryptInput) decryptInput.value = openedFilePath; } break; case 'permissions': sectionId = 'pdf-permissions-section'; titleText = 'Set Permissions'; if (openedFilePath) { const permInput = document.getElementById('permissions-input-path'); if (permInput) permInput.value = openedFilePath; } break; case 'extractText': sectionId = 'pdf-extract-text-section'; titleText = 'Extract Text'; if (openedFilePath) { const extractTextInput = document.getElementById('extract-text-input-path'); if (extractTextInput) extractTextInput.value = openedFilePath; } break; case 'pageNumbers': sectionId = 'pdf-page-numbers-section'; titleText = 'Add Page Numbers'; if (openedFilePath) { const pageNumbersInput = document.getElementById('page-numbers-input-path'); if (pageNumbersInput) pageNumbersInput.value = openedFilePath; } break; case 'crop': sectionId = 'pdf-crop-section'; titleText = 'Crop Pages'; if (openedFilePath) { const cropInput = document.getElementById('crop-input-path'); if (cropInput) cropInput.value = openedFilePath; } break; case 'extractImages': sectionId = 'pdf-extract-images-section'; titleText = 'Extract Images'; if (openedFilePath) { const extractImagesInput = document.getElementById('extract-images-input-path'); if (extractImagesInput) extractImagesInput.value = openedFilePath; } break; case 'fillForm': { sectionId = 'pdf-fill-form-section'; titleText = 'Fill Form'; const fieldsList = document.getElementById('fill-form-fields-list'); if (fieldsList) { fieldsList.innerHTML = 'Select a PDF with fillable fields to list them here.'; } if (openedFilePath) { const fillFormInput = document.getElementById('fill-form-input-path'); if (fillFormInput) { fillFormInput.value = openedFilePath; setTimeout(() => loadFillFormFields(openedFilePath), 50); } } break; } } title.textContent = titleText; document.getElementById(sectionId).classList.remove('hidden'); window.modals.pdfEditorModal.open(); } function hidePDFEditorDialog() { window.modals.pdfEditorModal.close(); clearPDFStatus(); document.getElementById('pdf-progress').classList.add('hidden'); document.getElementById('pdf-progress-text').textContent = 'Processing...'; const progressFill = document.getElementById('pdf-progress-fill'); if (progressFill) { progressFill.style.width = '0%'; } currentPDFOperation = null; } function updateMergeFilesList() { const listContainer = document.getElementById('merge-files-list'); listContainer.innerHTML = ''; mergeFilePaths.forEach((filePath, index) => { const fileEntry = document.createElement('div'); fileEntry.className = 'file-entry'; fileEntry.innerHTML = ` ${filePath.split(/[\\/]/).pop()} `; listContainer.appendChild(fileEntry); }); } // PDF Editor Event Listeners document.addEventListener('DOMContentLoaded', () => { // Close PDF Editor Dialog const pdfEditorClose = document.getElementById('pdf-editor-dialog-close'); if (pdfEditorClose) { pdfEditorClose.addEventListener('click', hidePDFEditorDialog); } const pdfEditorCancel = document.getElementById('pdf-editor-cancel'); if (pdfEditorCancel) { pdfEditorCancel.addEventListener('click', hidePDFEditorDialog); } // Process button const pdfEditorProcess = document.getElementById('pdf-editor-process'); if (pdfEditorProcess) { pdfEditorProcess.addEventListener('click', processPDFOperation); } // Merge PDFs - Add file button const addMergeFile = document.getElementById('add-merge-file'); if (addMergeFile) { addMergeFile.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.pdf'; input.multiple = true; input.onchange = (e) => { const files = Array.from(e.target.files); files.forEach((file) => { if (!mergeFilePaths.includes(file.path)) { mergeFilePaths.push(file.path); } }); updateMergeFilesList(); }; input.click(); }); } // Remove file from merge list (using event delegation) const mergeFilesList = document.getElementById('merge-files-list'); if (mergeFilesList) { mergeFilesList.addEventListener('click', (e) => { if (e.target.classList.contains('remove-file')) { const index = parseInt(e.target.dataset.index); mergeFilePaths.splice(index, 1); updateMergeFilesList(); } }); } // Browse buttons for all operations const browseButtons = [ { id: 'browse-merge-output', inputId: 'merge-output-path', saveDialog: true, }, { id: 'browse-split-input', inputId: 'split-input-path', saveDialog: false, }, { id: 'browse-split-output', inputId: 'split-output-folder', folder: true, }, { id: 'browse-compress-input', inputId: 'compress-input-path', saveDialog: false, }, { id: 'browse-compress-output', inputId: 'compress-output-path', saveDialog: true, }, { id: 'browse-rotate-input', inputId: 'rotate-input-path', saveDialog: false, }, { id: 'browse-rotate-output', inputId: 'rotate-output-path', saveDialog: true, }, { id: 'browse-delete-input', inputId: 'delete-input-path', saveDialog: false, }, { id: 'browse-delete-output', inputId: 'delete-output-path', saveDialog: true, }, { id: 'browse-reorder-input', inputId: 'reorder-input-path', saveDialog: false, }, { id: 'browse-reorder-output', inputId: 'reorder-output-path', saveDialog: true, }, { id: 'browse-watermark-input', inputId: 'watermark-input-path', saveDialog: false, }, { id: 'browse-watermark-output', inputId: 'watermark-output-path', saveDialog: true, }, { id: 'browse-encrypt-input', inputId: 'encrypt-input-path', saveDialog: false, }, { id: 'browse-encrypt-output', inputId: 'encrypt-output-path', saveDialog: true, }, { id: 'browse-decrypt-input', inputId: 'decrypt-input-path', saveDialog: false, }, { id: 'browse-decrypt-output', inputId: 'decrypt-output-path', saveDialog: true, }, { id: 'browse-permissions-input', inputId: 'permissions-input-path', saveDialog: false, }, { id: 'browse-permissions-output', inputId: 'permissions-output-path', saveDialog: true, }, { id: 'browse-extract-text-input', inputId: 'extract-text-input-path', saveDialog: false, }, { id: 'browse-extract-text-output', inputId: 'extract-text-output-path', saveDialog: true, }, { id: 'browse-page-numbers-input', inputId: 'page-numbers-input-path', saveDialog: false, }, { id: 'browse-page-numbers-output', inputId: 'page-numbers-output-path', saveDialog: true, }, { id: 'browse-crop-input', inputId: 'crop-input-path', saveDialog: false, }, { id: 'browse-crop-output', inputId: 'crop-output-path', saveDialog: true, }, { id: 'browse-extract-images-input', inputId: 'extract-images-input-path', saveDialog: false, }, { id: 'browse-extract-images-output', inputId: 'extract-images-output-folder', folder: true, }, { id: 'browse-fill-form-input', inputId: 'fill-form-input-path', saveDialog: false, }, { id: 'browse-fill-form-output', inputId: 'fill-form-output-path', saveDialog: true, }, ]; browseButtons.forEach((button) => { const btn = document.getElementById(button.id); if (btn) { btn.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; if (button.folder) { // Request folder selection via IPC ipcRenderer.send('select-pdf-folder', button.inputId); } else if (button.saveDialog) { input.nwsaveas = true; input.accept = '.pdf'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { document.getElementById(button.inputId).value = file.path; onPDFFileSelected(button.inputId, file.path); } }; input.click(); } else { input.accept = '.pdf'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { document.getElementById(button.inputId).value = file.path; onPDFFileSelected(button.inputId, file.path); if (button.inputId === 'fill-form-input-path') { loadFillFormFields(file.path); } } }; input.click(); } }); } }); // Split mode change handler const splitMode = document.getElementById('split-mode'); if (splitMode) { splitMode.addEventListener('change', (e) => { // Hide all split options document.getElementById('split-pages-options').classList.add('hidden'); document.getElementById('split-interval-options').classList.add('hidden'); document.getElementById('split-size-options').classList.add('hidden'); // Show selected split option if (e.target.value === 'pages') { document.getElementById('split-pages-options').classList.remove('hidden'); } else if (e.target.value === 'interval') { document.getElementById('split-interval-options').classList.remove('hidden'); } else if (e.target.value === 'size') { document.getElementById('split-size-options').classList.remove('hidden'); } }); } // Watermark opacity slider const watermarkOpacity = document.getElementById('watermark-opacity'); if (watermarkOpacity) { watermarkOpacity.addEventListener('input', (e) => { document.getElementById('watermark-opacity-value').textContent = e.target.value; }); } // Watermark pages selection const watermarkPages = document.getElementById('watermark-pages'); if (watermarkPages) { watermarkPages.addEventListener('change', (e) => { const customPages = document.getElementById('watermark-custom-pages'); if (e.target.value === 'custom') { customPages.classList.remove('hidden'); } else { customPages.classList.add('hidden'); } }); } // Overwrite checkbox handlers - toggle Save As section visibility const overwriteCheckboxes = [ { checkbox: 'compress-overwrite', section: 'compress-saveas-section', }, { checkbox: 'rotate-overwrite', section: 'rotate-saveas-section', }, { checkbox: 'delete-overwrite', section: 'delete-saveas-section', }, { checkbox: 'reorder-overwrite', section: 'reorder-saveas-section', }, { checkbox: 'watermark-overwrite', section: 'watermark-saveas-section', }, { checkbox: 'encrypt-overwrite', section: 'encrypt-saveas-section', }, { checkbox: 'decrypt-overwrite', section: 'decrypt-saveas-section', }, { checkbox: 'permissions-overwrite', section: 'permissions-saveas-section', }, { checkbox: 'page-numbers-overwrite', section: 'page-numbers-saveas-section', }, { checkbox: 'crop-overwrite', section: 'crop-saveas-section', }, { checkbox: 'fill-form-overwrite', section: 'fill-form-saveas-section', }, ]; overwriteCheckboxes.forEach((item) => { const checkbox = document.getElementById(item.checkbox); const section = document.getElementById(item.section); if (checkbox && section) { checkbox.addEventListener('change', (e) => { if (e.target.checked) { section.classList.add('hidden'); } else { section.classList.remove('hidden'); } }); } }); // Load current page order button const loadCurrentOrder = document.getElementById('load-current-order'); if (loadCurrentOrder) { loadCurrentOrder.addEventListener('click', () => { const inputPath = document.getElementById('reorder-input-path').value; if (!inputPath) { showPDFValidationMessage( 'Select a PDF file before loading the current page order.', '#reorder-input-path' ); return; } // Request page count from main process ipcRenderer.send('get-pdf-page-count', inputPath); }); } }); // Handle folder selection response ipcRenderer.on('pdf-folder-selected', (event, { inputId, path }) => { document.getElementById(inputId).value = path; }); // Handle PDF page count response ipcRenderer.on('pdf-page-count', (event, { count, error }) => { if (error) { showPDFStatus(`Error reading PDF: ${error}`, 'warning'); return; } const currentOrder = Array.from( { length: count, }, (_, i) => i + 1 ).join(', '); document.getElementById('current-order-display').textContent = currentOrder; document.getElementById('current-page-order').classList.remove('hidden'); document.getElementById('reorder-pages').value = currentOrder; }); // Fill Form: request the AcroForm text fields for a selected PDF, then render // one text input per field so the user can supply values before submitting. function loadFillFormFields(filePath) { const container = document.getElementById('fill-form-fields-list'); if (!container) return; container.innerHTML = ''; const loading = document.createElement('small'); loading.textContent = 'Loading form fields...'; container.appendChild(loading); ipcRenderer.send('get-pdf-form-fields', filePath); } ipcRenderer.on('pdf-form-fields', (event, result) => { const container = document.getElementById('fill-form-fields-list'); if (!container) return; container.innerHTML = ''; if (!result.success) { const msg = document.createElement('small'); msg.textContent = `Error reading form fields: ${result.error || 'unknown error'}`; container.appendChild(msg); return; } const textFields = (result.fields || []).filter((field) => field.type === 'PDFTextField'); if (textFields.length === 0) { const msg = document.createElement('small'); msg.textContent = 'No fillable text fields were found in this PDF.'; container.appendChild(msg); return; } textFields.forEach((field) => { const row = document.createElement('div'); row.className = 'fill-form-field-row'; const label = document.createElement('label'); label.textContent = field.name; const input = document.createElement('input'); input.type = 'text'; input.className = 'fill-form-field-input'; input.dataset.fieldName = field.name; if (field.value) { input.value = field.value; } label.appendChild(input); row.appendChild(label); container.appendChild(row); }); }); function getPDFStatusElement() { return document.getElementById('pdf-status-message'); } function showPDFStatus(message, type = 'info') { const status = getPDFStatusElement(); if (!status) return; status.textContent = message; status.classList.remove('hidden', 'info-message', 'warning-message', 'success-message'); status.classList.add(`${type}-message`); } function clearPDFStatus() { const status = getPDFStatusElement(); if (!status) return; status.textContent = ''; status.classList.remove('info-message', 'warning-message', 'success-message'); status.classList.add('hidden'); } function showPDFValidationMessage(message, focusSelector = null) { showPDFStatus(message, 'warning'); if (focusSelector) { const field = document.querySelector(focusSelector); if (field && typeof field.focus === 'function') { field.focus(); } } } // Process PDF Operation function processPDFOperation() { const operation = currentPDFOperation; const operationData = { operation, }; try { switch (operation) { case 'merge': if (mergeFilePaths.length < 2) { showPDFValidationMessage('Add at least 2 PDF files to merge first.', '#add-merge-file'); return; } operationData.inputFiles = mergeFilePaths; operationData.outputPath = document.getElementById('merge-output-path').value.trim(); if (!operationData.outputPath) { showPDFValidationMessage('Select an output file path.', '#merge-output-path'); return; } break; case 'split': operationData.inputPath = document.getElementById('split-input-path').value.trim(); operationData.outputFolder = document.getElementById('split-output-folder').value.trim(); operationData.splitMode = document.getElementById('split-mode').value; if (!operationData.inputPath || !operationData.outputFolder) { showPDFValidationMessage( 'Select both an input PDF and an output folder.', '#split-input-path' ); return; } if (operationData.splitMode === 'pages') { operationData.pageRanges = document.getElementById('split-page-ranges').value.trim(); } else if (operationData.splitMode === 'interval') { operationData.interval = parseInt(document.getElementById('split-interval').value); } else if (operationData.splitMode === 'size') { operationData.maxSize = parseInt(document.getElementById('split-size').value); } break; case 'compress': operationData.inputPath = document.getElementById('compress-input-path').value.trim(); operationData.overwrite = document.getElementById('compress-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('compress-output-path').value.trim(); operationData.compressionLevel = document.getElementById('compress-level').value; operationData.compressImages = document.getElementById('compress-images').checked; operationData.removeDuplicates = document.getElementById( 'compress-remove-duplicates' ).checked; operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked; if (!operationData.inputPath || !operationData.outputPath) { showPDFValidationMessage( 'Select an input PDF' + (operationData.overwrite ? '.' : ' and an output file path.'), '#compress-input-path' ); return; } break; case 'rotate': operationData.inputPath = document.getElementById('rotate-input-path').value.trim(); operationData.overwrite = document.getElementById('rotate-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('rotate-output-path').value.trim(); operationData.pages = document.getElementById('rotate-pages').value.trim(); operationData.angle = parseInt(document.getElementById('rotate-angle').value); if (!operationData.inputPath || !operationData.outputPath) { showPDFValidationMessage( 'Select an input PDF' + (operationData.overwrite ? '.' : ' and an output file path.'), '#rotate-input-path' ); return; } break; case 'delete': operationData.inputPath = document.getElementById('delete-input-path').value.trim(); operationData.overwrite = document.getElementById('delete-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('delete-output-path').value.trim(); operationData.pages = document.getElementById('delete-pages').value.trim(); if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) { showPDFValidationMessage( 'Fill in the input file, output path, and pages to delete.', '#delete-input-path' ); return; } break; case 'reorder': operationData.inputPath = document.getElementById('reorder-input-path').value.trim(); operationData.overwrite = document.getElementById('reorder-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('reorder-output-path').value.trim(); operationData.newOrder = document.getElementById('reorder-pages').value.trim(); if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) { showPDFValidationMessage( 'Fill in the input file, output path, and new page order.', '#reorder-input-path' ); return; } break; case 'watermark': operationData.inputPath = document.getElementById('watermark-input-path').value.trim(); operationData.overwrite = document.getElementById('watermark-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('watermark-output-path').value.trim(); operationData.text = document.getElementById('watermark-text').value.trim(); operationData.fontSize = parseInt(document.getElementById('watermark-font-size').value); operationData.opacity = parseInt(document.getElementById('watermark-opacity').value) / 100; operationData.position = document.getElementById('watermark-position').value; operationData.color = document.getElementById('watermark-color').value; operationData.pages = document.getElementById('watermark-pages').value; if (operationData.pages === 'custom') { operationData.customPages = document .getElementById('watermark-custom-pages') .value.trim(); } if (!operationData.inputPath || !operationData.outputPath || !operationData.text) { showPDFValidationMessage( 'Fill in the input file, output path, and watermark text.', '#watermark-input-path' ); return; } break; case 'encrypt': operationData.inputPath = document.getElementById('encrypt-input-path').value.trim(); operationData.overwrite = document.getElementById('encrypt-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('encrypt-output-path').value.trim(); operationData.userPassword = document.getElementById('encrypt-user-password').value; operationData.ownerPassword = document.getElementById('encrypt-owner-password').value; operationData.encryptionLevel = parseInt(document.getElementById('encrypt-level').value); operationData.permissions = { printing: document.getElementById('encrypt-allow-printing').checked, modifying: document.getElementById('encrypt-allow-modify').checked, copying: document.getElementById('encrypt-allow-copy').checked, annotating: document.getElementById('encrypt-allow-annotate').checked, fillingForms: document.getElementById('encrypt-allow-forms').checked, contentAccessibility: document.getElementById('encrypt-allow-extract').checked, documentAssembly: document.getElementById('encrypt-allow-assemble').checked, printingQuality: document.getElementById('encrypt-allow-print-high').checked, }; if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) { showPDFValidationMessage( 'Select a file, output path, and user password.', '#encrypt-input-path' ); return; } break; case 'decrypt': operationData.inputPath = document.getElementById('decrypt-input-path').value.trim(); operationData.overwrite = document.getElementById('decrypt-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('decrypt-output-path').value.trim(); operationData.password = document.getElementById('decrypt-password').value; if (!operationData.inputPath || !operationData.outputPath || !operationData.password) { showPDFValidationMessage( 'Fill in the input file, output path, and password.', '#decrypt-input-path' ); return; } break; case 'permissions': operationData.inputPath = document.getElementById('permissions-input-path').value.trim(); operationData.overwrite = document.getElementById('permissions-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('permissions-output-path').value.trim(); operationData.currentPassword = document.getElementById( 'permissions-current-password' ).value; operationData.ownerPassword = document.getElementById('permissions-owner-password').value; operationData.permissions = { printing: document.getElementById('permissions-allow-printing').checked, modifying: document.getElementById('permissions-allow-modify').checked, copying: document.getElementById('permissions-allow-copy').checked, annotating: document.getElementById('permissions-allow-annotate').checked, fillingForms: document.getElementById('permissions-allow-forms').checked, contentAccessibility: document.getElementById('permissions-allow-extract').checked, documentAssembly: document.getElementById('permissions-allow-assemble').checked, printingQuality: document.getElementById('permissions-allow-print-high').checked, }; if (!operationData.inputPath || !operationData.outputPath || !operationData.ownerPassword) { showPDFValidationMessage( 'Fill in the input file, output path, and owner password.', '#permissions-input-path' ); return; } break; case 'extractText': operationData.inputPath = document.getElementById('extract-text-input-path').value.trim(); operationData.outputPath = document.getElementById('extract-text-output-path').value.trim(); if (!operationData.inputPath || !operationData.outputPath) { showPDFValidationMessage( 'Select an input PDF and where to save the extracted text.', '#extract-text-input-path' ); return; } break; case 'pageNumbers': operationData.inputPath = document.getElementById('page-numbers-input-path').value.trim(); operationData.overwrite = document.getElementById('page-numbers-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('page-numbers-output-path').value.trim(); operationData.position = document.getElementById('page-numbers-position').value; operationData.startNumber = parseInt(document.getElementById('page-numbers-start').value) || 1; if (!operationData.inputPath || !operationData.outputPath) { showPDFValidationMessage( 'Select an input PDF' + (operationData.overwrite ? '.' : ' and an output file path.'), '#page-numbers-input-path' ); return; } break; case 'crop': operationData.inputPath = document.getElementById('crop-input-path').value.trim(); operationData.overwrite = document.getElementById('crop-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('crop-output-path').value.trim(); operationData.margins = { top: parseFloat(document.getElementById('crop-margin-top').value) || 0, bottom: parseFloat(document.getElementById('crop-margin-bottom').value) || 0, left: parseFloat(document.getElementById('crop-margin-left').value) || 0, right: parseFloat(document.getElementById('crop-margin-right').value) || 0, }; if (!operationData.inputPath || !operationData.outputPath) { showPDFValidationMessage( 'Select an input PDF' + (operationData.overwrite ? '.' : ' and an output file path.'), '#crop-input-path' ); return; } break; case 'extractImages': operationData.inputPath = document.getElementById('extract-images-input-path').value.trim(); operationData.outputDir = document .getElementById('extract-images-output-folder') .value.trim(); if (!operationData.inputPath || !operationData.outputDir) { showPDFValidationMessage( 'Select an input PDF and an output folder.', '#extract-images-input-path' ); return; } break; case 'fillForm': operationData.inputPath = document.getElementById('fill-form-input-path').value.trim(); operationData.overwrite = document.getElementById('fill-form-overwrite').checked; operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('fill-form-output-path').value.trim(); operationData.flatten = document.getElementById('fill-form-flatten').checked; operationData.values = {}; document .querySelectorAll('#fill-form-fields-list .fill-form-field-input') .forEach((input) => { operationData.values[input.dataset.fieldName] = input.value; }); if (!operationData.inputPath || !operationData.outputPath) { showPDFValidationMessage( 'Select an input PDF' + (operationData.overwrite ? '.' : ' and an output file path.'), '#fill-form-input-path' ); return; } break; } clearPDFStatus(); // Show progress document.getElementById('pdf-progress').classList.remove('hidden'); document.getElementById('pdf-progress-text').textContent = 'Processing PDF...'; const progressFill = document.getElementById('pdf-progress-fill'); if (progressFill) { progressFill.style.width = '10%'; } // Send to main process ipcRenderer.send('process-pdf-operation', operationData); } catch (error) { showPDFStatus(`Error: ${error.message}`, 'warning'); console.error('PDF operation error:', error); } } // Handle PDF operation completion ipcRenderer.on('pdf-operation-complete', (event, { success, error, message }) => { document.getElementById('pdf-progress').classList.add('hidden'); if (success) { showPDFStatus(message || 'PDF operation completed successfully.', 'success'); setTimeout(() => { if (window.modals?.pdfEditorModal?.isOpen()) { hidePDFEditorDialog(); } }, 800); } else { showPDFStatus(`Error: ${error || 'PDF operation failed'}`, 'warning'); } }); // Handle PDF operation progress ipcRenderer.on('pdf-operation-progress', (event, { message, progress }) => { document.getElementById('pdf-progress-text').textContent = message; if (progress !== undefined) { const progressFill = document.getElementById('pdf-progress-fill'); if (progressFill) { progressFill.style.width = `${progress}%`; } } }); // Add math rendering support using KaTeX for enhanced preview function initMathSupport() { // Add KaTeX CSS const katexCSS = document.createElement('link'); katexCSS.rel = 'stylesheet'; katexCSS.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css'; katexCSS.crossOrigin = 'anonymous'; document.head.appendChild(katexCSS); // Add KaTeX JS const katexJS = document.createElement('script'); katexJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js'; katexJS.crossOrigin = 'anonymous'; katexJS.onload = () => { // Add auto-render extension const autoRenderJS = document.createElement('script'); autoRenderJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js'; autoRenderJS.crossOrigin = 'anonymous'; autoRenderJS.onload = () => { // Re-render current preview to include math if (tabManager) { tabManager.updatePreview(); } }; document.head.appendChild(autoRenderJS); }; document.head.appendChild(katexJS); } // Initialize math support on load initMathSupport(); // ================================ // Header & Footer Dialog Management // ================================ let currentFieldTarget = null; // Track which input field is being edited // Open header/footer settings dialog function openHeaderFooterDialog() { window.modals.headerFooterModal.open(); // Request current settings from main process ipcRenderer.send('get-header-footer-settings'); } // Close header/footer settings dialog function closeHeaderFooterDialog() { window.modals.headerFooterModal.close(); } // Open field picker dialog function openFieldPickerDialog(targetInputId) { currentFieldTarget = targetInputId; window.modals.fieldPickerModal.open(); } // Close field picker dialog function closeFieldPickerDialog() { window.modals.fieldPickerModal.close(); currentFieldTarget = null; } // Load settings into dialog ipcRenderer.on('header-footer-settings-data', (event, settings) => { // Enable/disable checkbox document.getElementById('hf-enabled').checked = settings.enabled; // Header fields document.getElementById('header-left').value = settings.header.left || ''; document.getElementById('header-center').value = settings.header.center || ''; document.getElementById('header-right').value = settings.header.right || ''; // Footer fields document.getElementById('footer-left').value = settings.footer.left || ''; document.getElementById('footer-center').value = settings.footer.center || ''; document.getElementById('footer-right').value = settings.footer.right || ''; // Logo previews if (settings.header.logo) { document.getElementById('header-logo-preview').textContent = settings.header.logo .split(/[\\/]/) .pop(); } else { document.getElementById('header-logo-preview').textContent = ''; } if (settings.footer.logo) { document.getElementById('footer-logo-preview').textContent = settings.footer.logo .split(/[\\/]/) .pop(); } else { document.getElementById('footer-logo-preview').textContent = ''; } // Update config content visibility toggleConfigContent(); }); // Toggle config content based on enabled checkbox function toggleConfigContent() { const enabled = document.getElementById('hf-enabled').checked; const configContent = document.getElementById('hf-config-content'); if (enabled) { configContent.classList.remove('disabled'); } else { configContent.classList.add('disabled'); } } // Save header/footer settings function saveHeaderFooterSettings() { const settings = { enabled: document.getElementById('hf-enabled').checked, header: { left: document.getElementById('header-left').value || '', center: document.getElementById('header-center').value || '', right: document.getElementById('header-right').value || '', logo: null, // Logo paths are managed separately }, footer: { left: document.getElementById('footer-left').value || '', center: document.getElementById('footer-center').value || '', right: document.getElementById('footer-right').value || '', logo: null, }, }; ipcRenderer.send('save-header-footer-settings', settings); closeHeaderFooterDialog(); } // Handle logo browsing - ask main process to show open dialog function browseForLogo(position) { ipcRenderer.send('browse-header-footer-logo', position); } // Handle logo saved confirmation ipcRenderer.on('header-footer-logo-saved', (event, { position, path }) => { document.getElementById(`${position}-logo-preview`).textContent = path.split(/[\\/]/).pop(); }); // Clear logo function clearLogo(position) { ipcRenderer.send('clear-header-footer-logo', position); document.getElementById(`${position}-logo-preview`).textContent = ''; } // Handle logo cleared confirmation ipcRenderer.on('header-footer-logo-cleared', (_event, _position) => {}); // Insert dynamic field into input function insertDynamicField(field) { if (currentFieldTarget) { const input = document.getElementById(currentFieldTarget); const cursorPos = input.selectionStart; const textBefore = input.value.substring(0, cursorPos); const textAfter = input.value.substring(cursorPos); input.value = textBefore + field + textAfter; input.focus(); input.setSelectionRange(cursorPos + field.length, cursorPos + field.length); } closeFieldPickerDialog(); } // Event Listeners for Header/Footer Dialog // Close buttons document.getElementById('header-footer-close').addEventListener('click', closeHeaderFooterDialog); document.getElementById('header-footer-cancel').addEventListener('click', closeHeaderFooterDialog); document.getElementById('header-footer-save').addEventListener('click', saveHeaderFooterSettings); // Enable/disable checkbox document.getElementById('hf-enabled').addEventListener('change', toggleConfigContent); // Field insert buttons document.querySelectorAll('.field-insert-btn').forEach((btn) => { btn.addEventListener('click', () => { const target = btn.getAttribute('data-target'); openFieldPickerDialog(target); }); }); // Logo browse buttons document .getElementById('header-logo-browse') .addEventListener('click', () => browseForLogo('header')); document .getElementById('footer-logo-browse') .addEventListener('click', () => browseForLogo('footer')); // Logo clear buttons document.getElementById('header-logo-clear').addEventListener('click', () => clearLogo('header')); document.getElementById('footer-logo-clear').addEventListener('click', () => clearLogo('footer')); // Field picker dialog document.getElementById('field-picker-close').addEventListener('click', closeFieldPickerDialog); document.querySelectorAll('.field-option').forEach((btn) => { btn.addEventListener('click', () => { const field = btn.getAttribute('data-field'); insertDynamicField(field); }); }); // Export function to make openHeaderFooterDialog accessible globally window.openHeaderFooterDialog = openHeaderFooterDialog; // Listen for menu command to open dialog ipcRenderer.on('open-header-footer-dialog', () => { openHeaderFooterDialog(); }); // ================================ // Word Template Settings Dialog // ================================ // // Replaces the two former native OS dialogs ("Select Word Template...", // "Template Settings...") with a single in-app modal that shows the // currently active template state, instead of that state being invisible // until a user thought to reopen a menu item. "Browse..." still triggers // a native file picker in the main process — there is no bundled folder // of templates to enumerate, so a gallery of multiple templates would // require inventing content that doesn't exist; see Task 18 report. // Tracks the template path chosen via Browse in this dialog session, // before it is persisted by Save. let pendingWordTemplatePath = null; let wordTemplateDefaultAvailable = false; function openWordTemplateDialog() { window.modals.wordTemplateModal.open(); ipcRenderer.send('get-word-template-settings'); } function closeWordTemplateDialog() { window.modals.wordTemplateModal.close(); } function renderWordTemplateStatus(templateFileName) { const statusEl = document.getElementById('word-template-status'); const titleEl = document.getElementById('word-template-status-title'); const detailEl = document.getElementById('word-template-status-detail'); statusEl.classList.remove('wt-status-none', 'wt-status-selected', 'wt-status-missing'); if (templateFileName) { statusEl.classList.add('wt-status-selected'); titleEl.textContent = templateFileName; detailEl.textContent = 'Custom template selected'; } else if (wordTemplateDefaultAvailable) { statusEl.classList.add('wt-status-none'); titleEl.textContent = 'No template selected'; detailEl.textContent = 'Using the bundled default template'; } else { statusEl.classList.add('wt-status-missing'); titleEl.textContent = 'No template selected'; detailEl.textContent = 'Using default formatting (no default template is bundled)'; } } // Populate dialog with current settings from main process ipcRenderer.on('word-template-settings-data', (event, data) => { pendingWordTemplatePath = data.templatePath || null; wordTemplateDefaultAvailable = !!data.defaultTemplateAvailable; renderWordTemplateStatus(data.templateFileName); document.getElementById('word-template-start-page').value = data.startPage || 3; }); // Result of a Browse... click (native picker) or Clear ipcRenderer.on('word-template-browsed', (event, data) => { pendingWordTemplatePath = data.templatePath || null; renderWordTemplateStatus(data.templateFileName); }); function saveWordTemplateSettings() { const startPageInput = document.getElementById('word-template-start-page'); let startPage = parseInt(startPageInput.value, 10); if (!Number.isFinite(startPage) || startPage < 1) startPage = 1; if (startPage > 100) startPage = 100; ipcRenderer.send('save-word-template-settings', { templatePath: pendingWordTemplatePath, startPage, }); closeWordTemplateDialog(); } document.getElementById('word-template-close').addEventListener('click', closeWordTemplateDialog); document.getElementById('word-template-cancel').addEventListener('click', closeWordTemplateDialog); document.getElementById('word-template-save').addEventListener('click', saveWordTemplateSettings); document.getElementById('word-template-browse').addEventListener('click', () => { ipcRenderer.send('browse-word-template'); }); document.getElementById('word-template-clear').addEventListener('click', () => { ipcRenderer.send('clear-word-template'); }); window.openWordTemplateDialog = openWordTemplateDialog; ipcRenderer.on('open-word-template-dialog', () => { openWordTemplateDialog(); }); // Command Palette - initialized via CommandPalette class // (see DOMContentLoaded handler for registration of commands) // ============================================================================ // TABLE GENERATOR // ============================================================================ function showTableGenerator() { window.modals.tableModal.open(); // Generate initial preview generateTablePreview(); // Focus on rows input setTimeout(() => { document.getElementById('table-rows').focus(); }, 100); } function hideTableGenerator() { window.modals.tableModal.close(); } function generateTablePreview() { const rows = parseInt(document.getElementById('table-rows').value) || 3; const cols = parseInt(document.getElementById('table-cols').value) || 3; const hasHeader = document.getElementById('table-has-header').checked; const alignment = document.getElementById('table-alignment').value; const table = generateMarkdownTable(rows, cols, hasHeader, alignment); document.getElementById('table-preview').textContent = table; } function generateMarkdownTable(rows, cols, hasHeader, alignment) { let table = ''; // Generate alignment string let alignChar = '-'; if (alignment === 'center') { alignChar = ':' + '-'.repeat(Math.max(8, 10)) + ':'; } else if (alignment === 'right') { alignChar = '-'.repeat(Math.max(8, 10)) + ':'; } else { alignChar = '-'.repeat(Math.max(8, 10)); } // Generate header row if (hasHeader) { table += '| '; for (let c = 1; c <= cols; c++) { table += `Header ${c}`; if (c < cols) table += ' | '; } table += ' |\n'; // Separator row table += '| '; for (let c = 1; c <= cols; c++) { table += alignChar; if (c < cols) table += ' | '; } table += ' |\n'; // Data rows (excluding header) for (let r = 1; r < rows; r++) { table += '| '; for (let c = 1; c <= cols; c++) { table += `Cell ${r},${c}`; if (c < cols) table += ' | '; } table += ' |\n'; } } else { // No header - all rows are data rows // First row (acts as separator position) table += '| '; for (let c = 1; c <= cols; c++) { table += `Cell 1,${c}`; if (c < cols) table += ' | '; } table += ' |\n'; // Separator row table += '| '; for (let c = 1; c <= cols; c++) { table += alignChar; if (c < cols) table += ' | '; } table += ' |\n'; // Remaining data rows for (let r = 2; r <= rows; r++) { table += '| '; for (let c = 1; c <= cols; c++) { table += `Cell ${r},${c}`; if (c < cols) table += ' | '; } table += ' |\n'; } } return table; } function insertGeneratedTable() { const table = document.getElementById('table-preview').textContent; if (!table) { notifyUser('Generate a table preview first.', 'warning'); return; } // Insert table using CodeMirror if (!tabManager) { notifyUser('No active editor found.', 'warning'); return; } // Add newlines for spacing const tableWithSpacing = '\n' + table + '\n'; tabManager.insertAtCursor(tableWithSpacing); // Trigger update if (tabManager) { tabManager.handleEditorInput(tabManager.activeTabId); } // Close dialog hideTableGenerator(); } // Table Generator Event Listeners document.getElementById('table-dialog-close').addEventListener('click', hideTableGenerator); document.getElementById('table-cancel').addEventListener('click', hideTableGenerator); document.getElementById('table-generate-preview').addEventListener('click', generateTablePreview); document.getElementById('table-insert').addEventListener('click', insertGeneratedTable); // Auto-update preview when inputs change document.getElementById('table-rows').addEventListener('input', generateTablePreview); document.getElementById('table-cols').addEventListener('input', generateTablePreview); document.getElementById('table-has-header').addEventListener('change', generateTablePreview); document.getElementById('table-alignment').addEventListener('change', generateTablePreview); // Handle Enter key in inputs document.getElementById('table-rows').addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); insertGeneratedTable(); } }); document.getElementById('table-cols').addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); insertGeneratedTable(); } }); // ============================================================================ // ASCII ART GENERATOR // ============================================================================ let currentASCIIMode = 'text'; function showASCIIGenerator() { window.modals.asciiModal.open(); // Initialize with text mode switchASCIIMode('text'); // Generate initial preview setTimeout(() => { generateASCIIPreview(); }, 100); } function hideASCIIGenerator() { window.modals.asciiModal.close(); } function switchASCIIMode(mode) { currentASCIIMode = mode; // Update button states document.querySelectorAll('.ascii-mode-btn').forEach((btn) => { btn.classList.remove('active'); }); // Hide all mode sections document.querySelectorAll('.ascii-mode-section').forEach((section) => { section.classList.add('hidden'); }); // Show selected mode if (mode === 'text') { document.getElementById('ascii-mode-text').classList.add('active'); document.getElementById('ascii-text-mode').classList.remove('hidden'); } else if (mode === 'box') { document.getElementById('ascii-mode-box').classList.add('active'); document.getElementById('ascii-box-mode').classList.remove('hidden'); } else if (mode === 'templates') { document.getElementById('ascii-mode-templates').classList.add('active'); document.getElementById('ascii-templates-mode').classList.remove('hidden'); } // Generate preview generateASCIIPreview(); } function generateASCIIPreview() { let result = ''; if (currentASCIIMode === 'text') { const text = document.getElementById('ascii-text-input').value || 'Sample'; const style = document.getElementById('ascii-font-style').value; result = textToASCII(text, style); } else if (currentASCIIMode === 'box') { const text = document.getElementById('ascii-box-text').value || 'Sample Text'; const style = document.getElementById('ascii-box-style').value; const padding = parseInt(document.getElementById('ascii-box-padding').value) || 2; result = createASCIIBox(text, style, padding); } else if (currentASCIIMode === 'templates') { result = 'Select a template from the buttons above'; } document.getElementById('ascii-preview').textContent = result; } // Simple ASCII art text generator function textToASCII(text, style) { const fonts = { standard: { height: 5, chars: { A: [' _ ', ' / \\ ', '/___\\', '| |', '| |'], B: ['____ ', '| \\', '|___/', '| \\', '|___/'], C: [' ___ ', '/ _/', '| \\', '| ', '\\___/'], D: ['____ ', '| \\', '| |', '| |', '|___/'], E: ['_____', '| ', '|___ ', '| ', '|____'], F: ['_____', '| ', '|___ ', '| ', '| '], G: [' ___ ', '/ _', '| ||', '| |', '\\___/'], H: ['| |', '| |', '|___|', '| |', '| |'], I: ['_____', ' | ', ' | ', ' | ', '_____'], J: ['_____', ' | ', ' | ', '| | ', '\\__/ '], K: ['| |', '| / ', '|_/ ', '| \\ ', '| \\ '], L: ['| ', '| ', '| ', '| ', '|____'], M: ['| |', '|\\ /|', '| V |', '| |', '| |'], N: ['| |', '|\\ |', '| \\ |', '| \\|', '| |'], O: [' ___ ', '/ \\', '| |', '| |', '\\___/'], P: ['____ ', '| \\', '|___/', '| ', '| '], Q: [' ___ ', '/ \\', '| |', '| |\\ ', '\\__\\|'], R: ['____ ', '| \\', '|___/', '| \\ ', '| \\ '], S: [' ___ ', '/ _/', '\\_ \\', ' \\ ', '\\___/'], T: ['_____', ' | ', ' | ', ' | ', ' | '], U: ['| |', '| |', '| |', '| |', '\\___/'], V: ['| |', '| |', '| |', ' \\ / ', ' V '], W: ['| |', '| |', '| W |', '|/ \\|', '| |'], X: ['| |', ' \\ / ', ' X ', ' / \\ ', '| |'], Y: ['| |', ' \\ / ', ' Y ', ' | ', ' | '], Z: ['_____', ' / ', ' / ', ' / ', '/___ '], ' ': [' ', ' ', ' ', ' ', ' '], 0: [' ___ ', '/ \\', '| | |', '| |', '\\___/'], 1: [' | ', ' || ', ' | ', ' | ', ' _|_ '], 2: [' ___ ', '\\ /', ' __/ ', '/ ', '\\____'], 3: [' ___ ', '\\ /', ' __/ ', ' / ', '\\___/'], 4: ['| |', '| |', '|___|', ' |', ' |'], 5: [' ___ ', '| ', '|___ ', ' / ', '\\___/'], 6: [' ___ ', '/ ', '|___ ', '| |', '\\___/'], 7: ['_____', ' / ', ' / ', ' / ', '/ '], 8: [' ___ ', '| |', ' ___ ', '| |', '\\___/'], 9: [' ___ ', '| |', '\\___|', ' |', '\\___/'], }, }, banner: { height: 7, chars: { A: [ ' ### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ## ', ' ## ## ', ], B: [ ' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ], C: [ ' ##### ', ' ## ## ', ' ## ', ' ## ', ' ## ', ' ## ## ', ' ##### ', ], D: [ ' ###### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ###### ', ], E: [ ' ####### ', ' ## ', ' ## ', ' ##### ', ' ## ', ' ## ', ' ####### ', ], F: [ ' ####### ', ' ## ', ' ## ', ' ##### ', ' ## ', ' ## ', ' ## ', ], G: [ ' ##### ', ' ## ## ', ' ## ', ' ## ### ', ' ## ## ', ' ## ## ', ' ##### ', ], H: [ ' ## ## ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ## ', ' ## ## ', ' ## ## ', ], I: [ ' ####### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ####### ', ], J: [ ' ####### ', ' ## ', ' ## ', ' ## ', ' ## ## ', ' ## ## ', ' #### ', ], K: [ ' ## ## ', ' ## ## ', ' ## ## ', ' #### ', ' ## ## ', ' ## ## ', ' ## ## ', ], L: [ ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### ', ], M: [ ' ## ## ', ' ### ### ', ' ####### ', ' ## # ## ', ' ## ## ', ' ## ## ', ' ## ## ', ], N: [ ' ## ## ', ' ### ## ', ' #### ## ', ' ## #### ', ' ## ### ', ' ## ## ', ' ## ## ', ], O: [ ' ##### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ##### ', ], P: [ ' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ', ' ## ', ' ## ', ], Q: [ ' ##### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## # ## ', ' ## ## ', ' ### ## ', ], R: [ ' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ## ## ', ], S: [ ' ##### ', ' ## ## ', ' ## ', ' ##### ', ' ## ', ' ## ## ', ' ##### ', ], T: [ ' ####### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### ', ], U: [ ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ##### ', ], V: [ ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ### ', ], W: [ ' ## ## ', ' ## ## ', ' ## ## ', ' ## # ## ', ' ####### ', ' ### ### ', ' ## ## ', ], X: [ ' ## ## ', ' ## ## ', ' ### ', ' # ', ' ### ', ' ## ## ', ' ## ## ', ], Y: [ ' ## ## ', ' ## ## ', ' ### ', ' # ', ' # ', ' # ', ' # ', ], Z: [ ' ####### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### ', ], ' ': [ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ], 0: [ ' ##### ', ' ## ## ', ' ## ### ', ' ## #### ', ' ### ## ', ' ## ## ', ' ##### ', ], 1: [ ' ## ', ' ### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### ', ], 2: [ ' ##### ', ' ## ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### ', ], 3: [ ' ##### ', ' ## ## ', ' ## ', ' ### ', ' ## ', ' ## ## ', ' ##### ', ], 4: [ ' ### ', ' #### ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ', ' ## ', ], 5: [ ' ####### ', ' ## ', ' ###### ', ' ## ', ' ## ', ' ## ## ', ' ##### ', ], 6: [ ' ##### ', ' ## ## ', ' ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ##### ', ], 7: [ ' ####### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ], 8: [ ' ##### ', ' ## ## ', ' ## ## ', ' ##### ', ' ## ## ', ' ## ## ', ' ##### ', ], 9: [ ' ##### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ', ' ## ## ', ' ##### ', ], }, }, block: { height: 6, chars: { A: ['█████╗ ', '██╔══██╗', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'], B: ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██████╔╝', '╚═════╝ '], C: ['██████╗', '██╔════╝', '██║ ', '██║ ', '╚██████╗', ' ╚═════╝'], D: ['██████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '██████╔╝', '╚═════╝ '], E: ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'], F: ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '██║ ', '╚═╝ '], G: ['██████╗ ', '██╔════╝', '██║ ███╗', '██║ ██║', '╚██████╔╝', ' ╚═════╝ '], H: ['██╗ ██╗', '██║ ██║', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'], I: ['██╗', '██║', '██║', '██║', '██║', '╚═╝'], ' ': [' ', ' ', ' ', ' ', ' ', ' '], }, }, bubble: { height: 5, chars: { A: [' Ⓐ ', ' ⒜ ⒜ ', '⒜⒜⒜⒜⒜', '⒜ ⒜', '⒜ ⒜'], B: ['ⒷⒷⒷⒷ ', 'Ⓑ Ⓑ', 'ⒷⒷⒷⒷ ', 'Ⓑ Ⓑ', 'ⒷⒷⒷⒷ '], C: [' ⒸⒸⒸ ', 'Ⓒ ', 'Ⓒ ', 'Ⓒ ', ' ⒸⒸⒸ '], ' ': [' ', ' ', ' ', ' ', ' '], }, }, digital: { height: 7, chars: { A: [' ▄▀▀▀▄ ', '▐ ▌', '▐▄▄▄▄▌', '▐ ▌', '▐ ▌', '▐ ▌', ' '], B: ['▐▀▀▀▄ ', '▐▄▄▄▀ ', '▐▄▄▄▄ ', '▐ ▌', '▐▄▄▄▀ ', ' ', ' '], C: [' ▄▀▀▀▄', '▐ ', '▐ ', '▐ ', ' ▀▄▄▄▀', ' ', ' '], ' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '], }, }, }; // Use selected font or fallback to standard const font = fonts[style] || fonts.standard; const lines = []; const upperText = text.toUpperCase(); for (let i = 0; i < font.height; i++) { lines[i] = ''; } for (const char of upperText) { const charLines = font.chars[char] || font.chars[' ']; if (charLines) { for (let i = 0; i < font.height; i++) { lines[i] += charLines[i] || ' '.repeat(font.chars[' '][i].length); } } } return lines.join('\n'); } // Create ASCII box/frame function createASCIIBox(text, style, padding) { const styles = { single: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│', }, double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║', }, rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│', }, bold: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃', }, ascii: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|', }, }; const chars = styles[style] || styles.single; const lines = text.split('\n'); const maxLength = Math.max(...lines.map((l) => l.length)); const width = maxLength + padding * 2; let result = ''; // Top border result += chars.tl + chars.h.repeat(width) + chars.tr + '\n'; // Padding rows above for (let i = 0; i < padding - 1; i++) { result += chars.v + ' '.repeat(width) + chars.v + '\n'; } // Content lines for (const line of lines) { const paddedLine = line.padEnd(maxLength, ' '); result += chars.v + ' '.repeat(padding) + paddedLine + ' '.repeat(padding) + chars.v + '\n'; } // Padding rows below for (let i = 0; i < padding - 1; i++) { result += chars.v + ' '.repeat(width) + chars.v + '\n'; } // Bottom border result += chars.bl + chars.h.repeat(width) + chars.br; return result; } // ASCII Art Templates function getASCIITemplate(templateName) { const templates = { 'arrow-right': ` ┌────────┐ │ │ ────► NEXT │ │ │ └────────┘`, 'arrow-down': ` │ │ ▼ ┌──────┐ │ NEXT │ └──────┘`, check: ` ✓ Task completed ✓ All tests passed ✓ Ready to deploy ✓ Documentation updated`, divider: ` ═══════════════════════════════════════════════════════════ `, header: ` ╔═══════════════════════════════════════════════════╗ ║ SECTION HEADER ║ ╚═══════════════════════════════════════════════════╝`, flowchart: ` ┌─────────────┐ │ START │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ PROCESS │ └──────┬──────┘ │ ┌───┴───┐ │ Check │ └───┬───┘ │ ┌───▼──┬──────┐ │ Yes │ No │ ▼ ▼ │ ┌────┐ ┌────┐ │ │ A │ │ B │ │ └─┬──┘ └─┬──┘ │ │ │ │ └──┬───┘ │ │◄──────────┘ ▼ ┌─────────────┐ │ END │ └─────────────┘`, decision: ` ┌─────────┐ │ Check? │ └────┬────┘ │ ┌─────┴─────┐ │ │ Yes│ │No ▼ ▼ ┌────────┐ ┌────────┐ │ True │ │ False │ └────────┘ └────────┘`, sequence: ` User System Database │ │ │ │ Request │ │ ├──────────►│ │ │ │ Query │ │ ├──────────►│ │ │ │ │ │ Result │ │ │◄──────────┤ │ Response │ │ │◄──────────┤ │ │ │ │`, 'table-simple': ` ┌──────────┬──────────┬──────────┐ │ Header 1 │ Header 2 │ Header 3 │ ├──────────┼──────────┼──────────┤ │ Data 1 │ Data 2 │ Data 3 │ ├──────────┼──────────┼──────────┤ │ Data 4 │ Data 5 │ Data 6 │ └──────────┴──────────┴──────────┘`, timeline: ` 2020 2021 2022 2023 │ │ │ │ ●───────────●───────────●───────────● │ │ │ │ Start Milestone Release Current`, network: ` ┌──────────┐ │ Server │ └────┬─────┘ │ ┌─────────┼─────────┐ │ │ │ ┌───▼───┐ ┌──▼────┐ ┌──▼────┐ │Client1│ │Client2│ │Client3│ └───────┘ └───────┘ └───────┘`, hierarchy: ` ┌─────────┐ │ Root │ └────┬────┘ │ ┌────────┴────────┐ │ │ ┌───▼───┐ ┌───▼───┐ │Child 1│ │Child 2│ └───┬───┘ └───┬───┘ │ │ ┌───▼───┐ ┌───▼───┐ │Grand 1│ │Grand 2│ └───────┘ └───────┘`, 'process-flow': ` ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Input │────►│Process 1│────►│ Output │ └─────────┘ └────┬────┘ └─────────┘ │ ┌────▼────┐ │Process 2│ └─────────┘`, 'note-box': ` ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ NOTE: ┃ ┃ This is an important note ┃ ┃ that requires attention! ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛`, 'warning-box': ` ╔════════════════════════════════════╗ ║ ⚠ WARNING ║ ║ Proceed with caution! ║ ╚════════════════════════════════════╝`, 'info-box': ` ╭────────────────────────────────────╮ │ ℹ Information │ │ Additional context here │ ╰────────────────────────────────────╯`, 'separator-fancy': ` ╭──────────────────────────────────────────────────────╮ │ │ ╰──────────────────────────────────────────────────────╯`, brackets: ` 【 Important Text 】`, 'banner-stars': ` ************************************************************************ * IMPORTANT BANNER * ************************************************************************`, 'progress-bar': ` Progress: [████████████░░░░░░░░] 60% 0% 100%`, }; return templates[templateName] || ''; } function loadASCIITemplate(templateName) { const template = getASCIITemplate(templateName); document.getElementById('ascii-preview').textContent = template; } function insertASCIIArt() { const asciiArt = document.getElementById('ascii-preview').textContent; if (!asciiArt || asciiArt === 'Select a template from the buttons above') { notifyUser('Generate ASCII art first.', 'warning'); return; } // Insert using CodeMirror if (!tabManager) { notifyUser('No active editor found.', 'warning'); return; } // Insert in code block for proper rendering const codeBlock = '\n```\n' + asciiArt + '\n```\n'; tabManager.insertAtCursor(codeBlock); // Trigger update if (tabManager) { tabManager.handleEditorInput(tabManager.activeTabId); } // Close dialog hideASCIIGenerator(); } // ASCII Art Event Listeners document.getElementById('ascii-dialog-close').addEventListener('click', hideASCIIGenerator); document.getElementById('ascii-cancel').addEventListener('click', hideASCIIGenerator); document.getElementById('ascii-generate').addEventListener('click', generateASCIIPreview); document.getElementById('ascii-insert').addEventListener('click', insertASCIIArt); // Mode switching document.getElementById('ascii-mode-text').addEventListener('click', () => switchASCIIMode('text')); document.getElementById('ascii-mode-box').addEventListener('click', () => switchASCIIMode('box')); document .getElementById('ascii-mode-templates') .addEventListener('click', () => switchASCIIMode('templates')); // Auto-update preview for text mode document.getElementById('ascii-text-input').addEventListener('input', generateASCIIPreview); document.getElementById('ascii-font-style').addEventListener('change', generateASCIIPreview); // Auto-update preview for box mode document.getElementById('ascii-box-text').addEventListener('input', generateASCIIPreview); document.getElementById('ascii-box-style').addEventListener('change', generateASCIIPreview); document.getElementById('ascii-box-padding').addEventListener('input', generateASCIIPreview); // Template buttons document.querySelectorAll('.ascii-template-btn').forEach((btn) => { btn.addEventListener('click', function () { const template = this.getAttribute('data-template'); loadASCIITemplate(template); }); }); // IPC listener for menu ipcRenderer.on('show-ascii-generator', () => { showASCIIGenerator(); }); // ============================================================================ // PANE RESIZING // ============================================================================ let isResizing = false; let currentResizer = null; document.addEventListener('mousedown', (e) => { if (e.target.classList.contains('pane-resizer')) { isResizing = true; currentResizer = e.target; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; } }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const container = currentResizer.parentElement; const editorPane = currentResizer.previousElementSibling; const previewPane = currentResizer.nextElementSibling; const containerRect = container.getBoundingClientRect(); const containerWidth = containerRect.width; const mouseX = e.clientX - containerRect.left; // Calculate percentages (minimum 20%, maximum 80% for each pane) let editorPercentage = (mouseX / containerWidth) * 100; editorPercentage = Math.max(20, Math.min(80, editorPercentage)); const previewPercentage = 100 - editorPercentage; editorPane.style.flex = `0 0 ${editorPercentage}%`; previewPane.style.flex = `0 0 ${previewPercentage}%`; }); document.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; currentResizer = null; document.body.style.cursor = ''; document.body.style.userSelect = ''; } }); // ============================================ // Insert Content from Generator Windows // ============================================ ipcRenderer.on('insert-content', (event, content) => { if (tabManager) { tabManager.insertAtCursor(content); const activeTabId = tabManager.activeTabId; const tab = tabManager.tabs.get(activeTabId); if (tab) { tab.isDirty = true; tabManager.updatePreview(activeTabId); tabManager.updateTabBar(); } } }); // ============================================ // PDF VIEWER FUNCTIONALITY // ============================================ // Legacy PDF viewer globals - kept for PDF editor dialogs that still use them let pdfDoc = null; let pdfCurrentPage = 1; let pdfZoomLevel = 1.0; let pdfRotation = 0; let pdfFilePath = null; // Initialize PDF.js // 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 - now creates a tab instead of replacing the entire view async function openPdfFile(filePath) { // Check if this PDF is already open in a tab for (const [tabId, tab] of tabManager.tabs) { if (tab.type === 'pdf' && tab.filePath === filePath) { // Just switch to the existing tab tabManager.switchToTab(tabId); return; } } // Create a new PDF tab tabManager.createPdfTab(filePath); } // Render PDF page async function renderPdfPage(pageNum) { if (!pdfDoc) return; try { const page = await pdfDoc.getPage(pageNum); const canvas = document.getElementById('pdf-canvas'); const ctx = canvas.getContext('2d'); // Calculate viewport with zoom and rotation const viewport = page.getViewport({ scale: pdfZoomLevel, rotation: pdfRotation, }); canvas.width = viewport.width; canvas.height = viewport.height; const renderContext = { canvasContext: ctx, viewport: viewport, }; await page.render(renderContext).promise; // Update page input document.getElementById('pdf-page-input').value = pageNum; } catch (error) { console.error('Error rendering PDF page:', error); } } // PDF navigation handlers document.getElementById('pdf-prev-page')?.addEventListener('click', async () => { if (pdfCurrentPage > 1) { pdfCurrentPage--; await renderPdfPage(pdfCurrentPage); } }); document.getElementById('pdf-next-page')?.addEventListener('click', async () => { if (pdfDoc && pdfCurrentPage < pdfDoc.numPages) { pdfCurrentPage++; await renderPdfPage(pdfCurrentPage); } }); document.getElementById('pdf-page-input')?.addEventListener('change', async (e) => { const pageNum = parseInt(e.target.value); if (pdfDoc && pageNum >= 1 && pageNum <= pdfDoc.numPages) { pdfCurrentPage = pageNum; await renderPdfPage(pdfCurrentPage); } }); // PDF zoom handlers document.getElementById('pdf-zoom-out')?.addEventListener('click', async () => { if (pdfZoomLevel > 0.25) { pdfZoomLevel -= 0.25; document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; await renderPdfPage(pdfCurrentPage); } }); document.getElementById('pdf-zoom-in')?.addEventListener('click', async () => { if (pdfZoomLevel < 4.0) { pdfZoomLevel += 0.25; document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; await renderPdfPage(pdfCurrentPage); } }); document.getElementById('pdf-fit-width')?.addEventListener('click', async () => { if (!pdfDoc) return; const page = await pdfDoc.getPage(pdfCurrentPage); const viewport = page.getViewport({ scale: 1, rotation: pdfRotation, }); const containerWidth = document.getElementById('pdf-viewer').clientWidth - 40; pdfZoomLevel = containerWidth / viewport.width; document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; await renderPdfPage(pdfCurrentPage); }); document.getElementById('pdf-fit-page')?.addEventListener('click', async () => { if (!pdfDoc) return; const page = await pdfDoc.getPage(pdfCurrentPage); const viewport = page.getViewport({ scale: 1, rotation: pdfRotation, }); const container = document.getElementById('pdf-viewer'); const containerWidth = container.clientWidth - 40; const containerHeight = container.clientHeight - 40; const scaleX = containerWidth / viewport.width; const scaleY = containerHeight / viewport.height; pdfZoomLevel = Math.min(scaleX, scaleY); document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; await renderPdfPage(pdfCurrentPage); }); // PDF rotation handlers document.getElementById('pdf-rotate-left')?.addEventListener('click', async () => { pdfRotation = (pdfRotation - 90 + 360) % 360; await renderPdfPage(pdfCurrentPage); }); document.getElementById('pdf-rotate-right')?.addEventListener('click', async () => { pdfRotation = (pdfRotation + 90) % 360; await renderPdfPage(pdfCurrentPage); }); // Close PDF viewer document.getElementById('pdf-close')?.addEventListener('click', () => { closePdfViewer(); }); async function closePdfViewer() { // Legacy PDF viewer close - for backward compatibility with PDF editor dialogs if (pdfDoc) { try { await pdfDoc.destroy(); } catch (e) { console.warn('Error destroying PDF:', e); } } pdfDoc = null; pdfFilePath = null; // If we're using the new tab-based system, close the current PDF tab if (tabManager) { const activeTab = tabManager.tabs.get(tabManager.activeTabId); if (activeTab && activeTab.type === 'pdf') { tabManager.closeTab(tabManager.activeTabId); return; } } // Legacy: Hide PDF viewer container document.getElementById('pdf-viewer-container')?.classList.add('hidden'); // Show markdown tabs, tab bar, and toolbar document.getElementById('tab-bar')?.classList.remove('hidden'); document.querySelectorAll('.tab-content').forEach((tc) => tc.classList.remove('hidden')); document.querySelector('.toolbar')?.classList.remove('hidden'); // Activate the correct markdown tab if (tabManager) { const activeTab = document.querySelector( `.tab-content[data-tab-id="${tabManager.activeTabId}"]` ); if (activeTab) activeTab.classList.add('active'); tabManager.updatePreview(tabManager.activeTabId); } document.getElementById('status-text').textContent = 'Ready'; } // Handle PDF file open from main process ipcRenderer.on('open-pdf-file', (event, filePath) => { openPdfFile(filePath); }); // ============================================ // PDF EDITOR TOOLBAR BUTTONS // ============================================ // Helper function to trigger PDF editor dialog function openPdfEditorDialog(operation) { // Use the opened PDF file if available, otherwise prompt if (pdfFilePath) { ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: pdfFilePath, }); } else { ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: null, }); } } // PDF Editor toolbar button handlers document.getElementById('pdf-tb-merge')?.addEventListener('click', () => { openPdfEditorDialog('merge'); }); document.getElementById('pdf-tb-split')?.addEventListener('click', () => { openPdfEditorDialog('split'); }); document.getElementById('pdf-tb-compress')?.addEventListener('click', () => { openPdfEditorDialog('compress'); }); document.getElementById('pdf-tb-rotate')?.addEventListener('click', () => { openPdfEditorDialog('rotate'); }); document.getElementById('pdf-tb-delete')?.addEventListener('click', () => { openPdfEditorDialog('delete'); }); document.getElementById('pdf-tb-reorder')?.addEventListener('click', () => { openPdfEditorDialog('reorder'); }); document.getElementById('pdf-tb-watermark')?.addEventListener('click', () => { openPdfEditorDialog('watermark'); }); document.getElementById('pdf-tb-encrypt')?.addEventListener('click', () => { openPdfEditorDialog('encrypt'); }); document.getElementById('pdf-tb-decrypt')?.addEventListener('click', () => { openPdfEditorDialog('decrypt'); }); document.getElementById('pdf-tb-extract-text')?.addEventListener('click', () => { openPdfEditorDialog('extractText'); }); document.getElementById('pdf-tb-page-numbers')?.addEventListener('click', () => { openPdfEditorDialog('pageNumbers'); }); document.getElementById('pdf-tb-crop')?.addEventListener('click', () => { openPdfEditorDialog('crop'); }); document.getElementById('pdf-tb-extract-images')?.addEventListener('click', () => { openPdfEditorDialog('extractImages'); }); // ============================================ // DYNAMIC PANE RESIZER // ============================================ function initPaneResizer(tabId) { const resizer = document.getElementById(`pane-resizer-${tabId}`); const editorPane = document.getElementById(`editor-pane-${tabId}`); const previewPane = document.getElementById(`preview-pane-${tabId}`); const tabContent = document.getElementById(`tab-content-${tabId}`); if (!resizer || !editorPane || !previewPane) return; let isResizing = false; let startX = 0; let startEditorWidth = 0; resizer.addEventListener('mousedown', (e) => { isResizing = true; startX = e.clientX; startEditorWidth = editorPane.offsetWidth; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const containerWidth = tabContent.offsetWidth; const deltaX = e.clientX - startX; let newEditorWidth = startEditorWidth + deltaX; // Minimum widths const minWidth = 150; const maxWidth = containerWidth - minWidth - 6; // 6px for resizer newEditorWidth = Math.max(minWidth, Math.min(maxWidth, newEditorWidth)); const editorPercent = (newEditorWidth / containerWidth) * 100; const previewPercent = 100 - editorPercent - 1; // 1% for resizer editorPane.style.flex = `0 0 ${editorPercent}%`; previewPane.style.flex = `0 0 ${previewPercent}%`; }); document.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; } }); } // Initialize resizer for first tab document.addEventListener('DOMContentLoaded', () => { initPaneResizer(1); }); // ============================================ // IMAGE/AUDIO/VIDEO CONVERTER HANDLERS // These require external tools (ImageMagick, FFmpeg) // ============================================ ipcRenderer.on('show-image-tool', (event, tool) => { notifyUser( `Image ${tool} requires ImageMagick. Install it from imagemagick.org to enable this tool.`, 'info', { duration: 5000, } ); }); ipcRenderer.on('show-audio-tool', (event, tool) => { notifyUser( `Audio ${tool} requires FFmpeg. Install it from ffmpeg.org to enable this tool.`, 'info', { duration: 5000, } ); }); ipcRenderer.on('show-video-tool', (event, tool) => { notifyUser( `Video ${tool} requires FFmpeg. Install it from ffmpeg.org to enable this tool.`, 'info', { duration: 5000, } ); }); // ============================================ // PDF EDITOR VISUAL PAGE SIDEBAR HANDLERS // ============================================ let pdfDocInstance = null; let pdfPreviewState = { pages: [], filePath: '', }; function onPDFFileSelected(inputId, filePath) { const eligibleInputs = ['rotate-input-path', 'delete-input-path', 'reorder-input-path']; if (!eligibleInputs.includes(inputId)) { return; } const sidebar = document.getElementById('pdf-thumbnail-sidebar'); if (sidebar) { sidebar.classList.remove('hidden'); } const body = document.querySelector('.pdf-editor-body'); if (body) { body.classList.add('side-by-side'); } loadPDFThumbnails(filePath); } async function loadPDFThumbnails(filePath) { const grid = document.getElementById('pdf-thumbnail-grid'); if (!grid) return; // Reset previous inputs const rotateInput = document.getElementById('rotate-pages'); if (rotateInput) rotateInput.value = ''; const deleteInput = document.getElementById('delete-pages'); if (deleteInput) deleteInput.value = ''; const reorderInput = document.getElementById('reorder-pages'); if (reorderInput) reorderInput.value = ''; grid.innerHTML = '
Loading page thumbnails...
'; try { const pdfjs = getPdfjsLib(); const loadingTask = pdfjs.getDocument(filePath); pdfDocInstance = await loadingTask.promise; pdfPreviewState.filePath = filePath; pdfPreviewState.pages = []; for (let i = 1; i <= pdfDocInstance.numPages; i++) { pdfPreviewState.pages.push({ originalPageNum: i, rotation: 0, isDeleted: false, }); } renderThumbnailGrid(); } catch (err) { console.error('Error loading PDF thumbnails:', err); grid.innerHTML = '
Failed to load PDF thumbnails.
'; } } function renderThumbnailGrid() { const grid = document.getElementById('pdf-thumbnail-grid'); if (!grid || !pdfDocInstance) return; grid.innerHTML = ''; pdfPreviewState.pages.forEach((pageInfo, index) => { const card = document.createElement('div'); card.className = 'pdf-thumbnail-card'; if (pageInfo.isDeleted) { card.classList.add('marked-delete'); } const heading = document.createElement('div'); heading.className = 'pdf-thumbnail-header'; heading.innerHTML = `Page ${pageInfo.originalPageNum}`; const canvasContainer = document.createElement('div'); canvasContainer.className = 'canvas-container'; const canvas = document.createElement('canvas'); canvas.className = 'pdf-thumbnail-canvas'; if (pageInfo.rotation > 0) { canvas.classList.add('rot' + pageInfo.rotation); } canvasContainer.appendChild(canvas); // Render thumbnail canvas renderThumbnail(pdfDocInstance, pageInfo.originalPageNum, canvas, pageInfo.rotation); const controlsRow = document.createElement('div'); controlsRow.className = 'pdf-thumbnail-controls'; // Rotate Button const rotateBtn = document.createElement('button'); rotateBtn.type = 'button'; rotateBtn.className = 'btn-rotate-thumbnail'; rotateBtn.innerHTML = '↻ Rotate 90°'; rotateBtn.addEventListener('click', () => { pageInfo.rotation = (pageInfo.rotation + 90) % 360; canvas.className = 'pdf-thumbnail-canvas'; if (pageInfo.rotation > 0) { canvas.classList.add('rot' + pageInfo.rotation); } renderThumbnail(pdfDocInstance, pageInfo.originalPageNum, canvas, pageInfo.rotation); syncRotateInput(); }); // Delete Checkbox const deleteLabel = document.createElement('label'); deleteLabel.className = 'delete-thumbnail-checkbox'; const deleteChk = document.createElement('input'); deleteChk.type = 'checkbox'; deleteChk.checked = pageInfo.isDeleted; deleteChk.addEventListener('change', (e) => { pageInfo.isDeleted = e.target.checked; card.classList.toggle('marked-delete', pageInfo.isDeleted); syncDeleteInput(); }); deleteLabel.appendChild(deleteChk); deleteLabel.appendChild(document.createTextNode(' Delete')); // Reorder buttons const reorderDiv = document.createElement('div'); reorderDiv.className = 'pdf-thumbnail-reorder'; const leftBtn = document.createElement('button'); leftBtn.type = 'button'; leftBtn.className = 'btn-reorder-left'; leftBtn.innerHTML = '◀'; leftBtn.disabled = index === 0; leftBtn.addEventListener('click', () => { const temp = pdfPreviewState.pages[index]; pdfPreviewState.pages[index] = pdfPreviewState.pages[index - 1]; pdfPreviewState.pages[index - 1] = temp; renderThumbnailGrid(); syncReorderInput(); }); const rightBtn = document.createElement('button'); rightBtn.type = 'button'; rightBtn.className = 'btn-reorder-right'; rightBtn.innerHTML = '▶'; rightBtn.disabled = index === pdfPreviewState.pages.length - 1; rightBtn.addEventListener('click', () => { const temp = pdfPreviewState.pages[index]; pdfPreviewState.pages[index] = pdfPreviewState.pages[index + 1]; pdfPreviewState.pages[index + 1] = temp; renderThumbnailGrid(); syncReorderInput(); }); reorderDiv.appendChild(leftBtn); reorderDiv.appendChild(rightBtn); controlsRow.appendChild(rotateBtn); controlsRow.appendChild(deleteLabel); controlsRow.appendChild(reorderDiv); card.appendChild(heading); card.appendChild(canvasContainer); card.appendChild(controlsRow); grid.appendChild(card); }); } async function renderThumbnail(pdfDoc, originalPageNum, canvasElement, _rotation) { try { const page = await pdfDoc.getPage(originalPageNum); const ctx = canvasElement.getContext('2d'); // Render at a fixed width of 100px const unscaledViewport = page.getViewport({ scale: 1, rotation: 0, }); const scale = 100 / unscaledViewport.width; const viewport = page.getViewport({ scale: scale, rotation: 0, }); canvasElement.width = viewport.width; canvasElement.height = viewport.height; await page.render({ canvasContext: ctx, viewport: viewport, }).promise; } catch (err) { console.error('Error rendering page canvas:', err); } } function syncRotateInput() { const rotatedPages = pdfPreviewState.pages .filter((p) => p.rotation > 0) .map((p) => p.originalPageNum); const rotateInput = document.getElementById('rotate-pages'); if (rotateInput) { rotateInput.value = rotatedPages.join(', '); } } function syncDeleteInput() { const deletedPages = pdfPreviewState.pages .filter((p) => p.isDeleted) .map((p) => p.originalPageNum); const deleteInput = document.getElementById('delete-pages'); if (deleteInput) { deleteInput.value = deletedPages.join(', '); } } function syncReorderInput() { const order = pdfPreviewState.pages.map((p) => p.originalPageNum); const reorderInput = document.getElementById('reorder-pages'); if (reorderInput) { reorderInput.value = order.join(', '); } }