Files
markdown-converter/src/renderer.js
T
amitwh 328451f88d Major v1.3.0 update: Fix PDF export, file associations, remove converter menu, and implement tabbed interface
- Enhanced PDF export with multiple LaTeX engine fallbacks
- Fixed file association and direct file opening from OS
- Removed redundant converter menu, moved import to File menu
- Implemented comprehensive tabbed interface for multiple files
- Added tab management with keyboard shortcuts (Ctrl+N, Ctrl+W, Ctrl+Tab)
- Enhanced UI with tab bar and improved navigation
- Updated to version 1.3.0 with new features
- Improved main process and renderer architecture for multi-file support
2025-09-01 20:53:17 +05:30

540 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { ipcRenderer } = require('electron');
const marked = require('marked');
const DOMPurify = require('dompurify');
const hljs = require('highlight.js');
// Configure marked
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {}
}
return hljs.highlightAuto(code).value;
},
breaks: true,
gfm: true
});
// Tab Management
class TabManager {
constructor() {
this.tabs = new Map();
this.activeTabId = 1;
this.nextTabId = 2;
this.isPreviewVisible = true;
this.showLineNumbers = false;
// Initialize first tab
this.tabs.set(1, {
id: 1,
title: 'Untitled',
content: '',
filePath: null,
isDirty: false,
undoStack: [],
redoStack: [],
findMatches: [],
currentMatchIndex: -1
});
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':
e.preventDefault();
this.createNewTab();
break;
case 'w':
if (this.tabs.size > 1) {
e.preventDefault();
this.closeTab(this.activeTabId);
}
break;
case 't':
e.preventDefault();
this.createNewTab();
break;
case 'Tab':
if (this.tabs.size > 1) {
e.preventDefault();
this.switchToNextTab();
}
break;
}
}
});
}
createNewTab() {
const newTabId = this.nextTabId++;
const tab = {
id: newTabId,
title: 'Untitled',
content: '',
filePath: null,
isDirty: false,
undoStack: [],
redoStack: [],
findMatches: [],
currentMatchIndex: -1
};
this.tabs.set(newTabId, tab);
this.createTabElements(tab);
this.switchToTab(newTabId);
this.updateTabBar();
}
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="line-numbers-${tab.id}" class="line-numbers hidden"></div>
<textarea id="editor-${tab.id}" class="editor-textarea"></textarea>
</div>
</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);
}
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();
// Notify main process about current file for exports
const tab = this.tabs.get(tabId);
if (tab && tab.filePath) {
ipcRenderer.send('set-current-file', tab.filePath);
}
}
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.isDirty) {
// TODO: Show confirmation dialog
}
// Remove tab elements
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
const tabContent = document.getElementById(`tab-content-${tabId}`);
if (tabElement && 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');
tabElement.className = `tab ${tab.id === this.activeTabId ? 'active' : ''}`;
tabElement.dataset.tabId = tab.id;
const title = tab.filePath ?
tab.filePath.split('/').pop() :
tab.title;
const dirtyIndicator = tab.isDirty ? ' •' : '';
tabElement.innerHTML = `
<span class="tab-title">${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');
}
// Update preview visibility
this.updatePreviewVisibility();
this.updateLineNumbers();
this.updateTabBar();
}
saveCurrentTabState() {
const tab = this.tabs.get(this.activeTabId);
if (!tab) return;
const editor = document.getElementById(`editor-${this.activeTabId}`);
if (editor) {
tab.content = editor.value;
tab.isDirty = tab.content !== (tab.originalContent || '');
}
}
restoreTabState(tabId) {
const tab = this.tabs.get(tabId);
if (!tab) return;
const editor = document.getElementById(`editor-${tabId}`);
const preview = document.getElementById(`preview-${tabId}`);
if (editor) {
editor.value = tab.content;
this.updatePreview(tabId);
this.updateWordCount();
}
}
focusActiveEditor() {
const editor = document.getElementById(`editor-${this.activeTabId}`);
if (editor) {
editor.focus();
}
}
updatePreview(tabId = this.activeTabId) {
const tab = this.tabs.get(tabId);
const preview = document.getElementById(`preview-${tabId}`);
if (!tab || !preview) return;
try {
const html = marked.parse(tab.content);
const sanitizedHtml = DOMPurify.sanitize(html);
preview.innerHTML = sanitizedHtml;
} catch (error) {
preview.innerHTML = '<p>Error rendering preview</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() {
const editor = document.getElementById(`editor-${this.activeTabId}`);
const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`);
if (!editor || !lineNumbers) return;
if (this.showLineNumbers) {
const lines = editor.value.split('\\n');
lineNumbers.innerHTML = lines.map((_, i) =>
`<div class="line-number">${i + 1}</div>`
).join('');
lineNumbers.classList.remove('hidden');
} else {
lineNumbers.classList.add('hidden');
}
}
updateWordCount() {
const tab = this.tabs.get(this.activeTabId);
if (!tab) return;
const words = tab.content.trim().split(/\\s+/).filter(word => word.length > 0).length;
const chars = tab.content.length;
document.getElementById('word-count').textContent = `Words: ${words} | Characters: ${chars}`;
}
setupEditorEvents() {
// Set up editor events using event delegation
document.addEventListener('input', (e) => {
if (e.target.classList.contains('editor-textarea')) {
const tabId = parseInt(e.target.id.split('-')[1]);
if (tabId === this.activeTabId) {
this.handleEditorInput(tabId);
}
}
});
document.addEventListener('scroll', (e) => {
if (e.target.classList.contains('editor-textarea')) {
const tabId = parseInt(e.target.id.split('-')[1]);
if (tabId === this.activeTabId) {
this.updateLineNumbers();
}
}
});
}
handleEditorInput(tabId) {
const tab = this.tabs.get(tabId);
if (!tab) return;
const editor = document.getElementById(`editor-${tabId}`);
tab.content = editor.value;
tab.isDirty = true;
this.updatePreview(tabId);
this.updateWordCount();
this.updateLineNumbers();
this.updateTabBar();
// Add to undo stack
this.pushUndoState(tabId);
}
pushUndoState(tabId) {
const tab = this.tabs.get(tabId);
if (!tab) return;
tab.undoStack.push(tab.content);
if (tab.undoStack.length > 50) {
tab.undoStack.shift();
}
tab.redoStack = [];
}
undo() {
const tab = this.tabs.get(this.activeTabId);
if (!tab || tab.undoStack.length === 0) return;
tab.redoStack.push(tab.content);
tab.content = tab.undoStack.pop();
const editor = document.getElementById(`editor-${this.activeTabId}`);
if (editor) {
editor.value = tab.content;
this.updatePreview();
this.updateWordCount();
}
}
redo() {
const tab = this.tabs.get(this.activeTabId);
if (!tab || tab.redoStack.length === 0) return;
tab.undoStack.push(tab.content);
tab.content = tab.redoStack.pop();
const editor = document.getElementById(`editor-${this.activeTabId}`);
if (editor) {
editor.value = tab.content;
this.updatePreview();
this.updateWordCount();
}
}
setupToolbarEvents() {
// Existing toolbar setup...
document.getElementById('btn-preview-toggle').addEventListener('click', () => {
this.isPreviewVisible = !this.isPreviewVisible;
this.updatePreviewVisibility();
});
document.getElementById('btn-line-numbers').addEventListener('click', () => {
this.showLineNumbers = !this.showLineNumbers;
this.updateLineNumbers();
});
// Add other toolbar events...
}
setupFindEvents() {
// Find dialog implementation...
document.getElementById('btn-find').addEventListener('click', () => {
document.getElementById('find-dialog').classList.remove('hidden');
document.getElementById('find-input').focus();
});
document.getElementById('btn-find-close').addEventListener('click', () => {
document.getElementById('find-dialog').classList.add('hidden');
});
}
// File operations
openFile(filePath, content) {
let tab = this.tabs.get(this.activeTabId);
// If current tab is empty and untitled, reuse it
if (!tab.filePath && !tab.isDirty && tab.content === '') {
tab.filePath = filePath;
tab.title = filePath.split('/').pop();
tab.content = content;
tab.originalContent = content;
tab.isDirty = false;
} else {
// Create new tab for the file
this.createNewTab();
tab = this.tabs.get(this.activeTabId);
tab.filePath = filePath;
tab.title = filePath.split('/').pop();
tab.content = content;
tab.originalContent = content;
tab.isDirty = false;
}
this.restoreTabState(this.activeTabId);
this.updateTabBar();
}
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;
document.addEventListener('DOMContentLoaded', () => {
tabManager = new TabManager();
// Request current theme
ipcRenderer.send('get-theme');
// 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', tab.content);
}
});
}, 30000);
});
// IPC event listeners
ipcRenderer.on('file-new', () => {
tabManager.createNewTab();
});
ipcRenderer.on('file-opened', (event, data) => {
tabManager.openFile(data.path, data.content);
});
ipcRenderer.on('file-save', () => {
const currentContent = tabManager.getCurrentContent();
const currentFilePath = tabManager.getCurrentFilePath();
if (currentFilePath) {
ipcRenderer.send('save-current-file', currentContent);
}
});
ipcRenderer.on('get-content-for-save', (event, filePath) => {
const currentContent = tabManager.getCurrentContent();
ipcRenderer.send('save-file', { path: filePath, content: currentContent });
});
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', () => {
const findDialog = document.getElementById('find-dialog');
if (findDialog.classList.contains('hidden')) {
findDialog.classList.remove('hidden');
document.getElementById('find-input').focus();
} else {
findDialog.classList.add('hidden');
}
});
ipcRenderer.on('theme-changed', (event, theme) => {
document.body.className = `theme-${theme}`;
});