mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-23 23:10:17 +05:30
6525 lines
213 KiB
JavaScript
6525 lines
213 KiB
JavaScript
/**
|
||
* 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 `<div class="admonition admonition-${token.admonitionType}">
|
||
<div class="admonition-title">${icon} ${token.admonitionType.charAt(0).toUpperCase() + token.admonitionType.slice(1)}</div>
|
||
<div class="admonition-content">${inner}</div>
|
||
</div>`;
|
||
},
|
||
},
|
||
],
|
||
});
|
||
|
||
// 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 = `
|
||
<div class="pdf-tab-container" id="pdf-container-${tab.id}">
|
||
<div class="pdf-controls">
|
||
<button class="pdf-nav-btn" data-action="prev" title="Previous Page">◀</button>
|
||
<input type="number" class="pdf-page-input" id="pdf-page-${tab.id}" value="1" min="1">
|
||
<span class="pdf-page-info">/ <span id="pdf-total-${tab.id}">1</span></span>
|
||
<button class="pdf-nav-btn" data-action="next" title="Next Page">▶</button>
|
||
<button class="pdf-zoom-btn" data-action="zoom-out" title="Zoom Out">−</button>
|
||
<span class="pdf-zoom-level" id="pdf-zoom-${tab.id}">100%</span>
|
||
<button class="pdf-zoom-btn" data-action="zoom-in" title="Zoom In">+</button>
|
||
<button class="pdf-zoom-btn" data-action="fit-width" title="Fit Width">↔</button>
|
||
<button class="pdf-rotate-btn" data-action="rotate-left" title="Rotate Left">↺</button>
|
||
<button class="pdf-rotate-btn" data-action="rotate-right" title="Rotate Right">↻</button>
|
||
</div>
|
||
<div class="pdf-canvas-container">
|
||
<canvas id="pdf-canvas-${tab.id}"></canvas>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div id="editor-pane-${tab.id}" class="pane">
|
||
<div class="editor-wrapper">
|
||
<div id="editor-cm-${tab.id}" class="codemirror-container"></div>
|
||
</div>
|
||
</div>
|
||
<div class="pane-resizer" id="pane-resizer-${tab.id}" title="Drag to resize"></div>
|
||
<div id="preview-pane-${tab.id}" class="pane">
|
||
<div id="preview-${tab.id}" class="preview-content"></div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<span class="tab-title">${typeIndicator}${title}${dirtyIndicator}</span>
|
||
<button class="tab-close" title="Close tab">×</button>
|
||
`;
|
||
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 =
|
||
'<div class="preview-error"><div class="preview-error-icon">⚠️</div><div class="preview-error-title">Libraries Not Loaded</div><div class="preview-error-message">Required libraries (marked/DOMPurify) could not be loaded. Please check your installation.</div></div>';
|
||
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])[^>]*>(.*?)<\/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 =
|
||
'<nav class="toc"><h2>Table of Contents</h2><ul>' +
|
||
toc
|
||
.map((h) => `<li class="toc-h${h.level}"><a href="#${h.id}">${h.text}</a></li>`)
|
||
.join('') +
|
||
'</ul></nav>';
|
||
sanitizedHtml = sanitizedHtml.replace(/\[\[toc\]\]|\[TOC\]/gi, tocHtml);
|
||
|
||
// Add IDs to headings for anchor links
|
||
toc.forEach((h) => {
|
||
sanitizedHtml = sanitizedHtml.replace(
|
||
new RegExp(`(<h${h.level}[^>]*)>`, '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 =
|
||
'<p class="error">Error rendering preview. Please check your markdown syntax.</p>';
|
||
}
|
||
}
|
||
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(`\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(`\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(''));
|
||
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 = `
|
||
<input type="text" placeholder="key" class="metadata-key">
|
||
<input type="text" placeholder="value" class="metadata-value">
|
||
`;
|
||
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 =
|
||
'<small>Select a PDF with fillable fields to list them here.</small>';
|
||
}
|
||
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 = `
|
||
<span class="file-name">${filePath.split(/[\\/]/).pop()}</span>
|
||
<button class="remove-file" data-index="${index}">Remove</button>
|
||
`;
|
||
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 = '<div class="loading-thumbnails">Loading page thumbnails...</div>';
|
||
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 = '<div class="thumbnail-error">Failed to load PDF thumbnails.</div>';
|
||
}
|
||
}
|
||
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 = `<span class="badge">Page ${pageInfo.originalPageNum}</span>`;
|
||
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(', ');
|
||
}
|
||
}
|