diff --git a/src/main.js b/src/main.js
index ded42fc..9d30e47 100644
--- a/src/main.js
+++ b/src/main.js
@@ -267,6 +267,30 @@ function createMenu() {
}
]
},
+ {
+ label: 'ConvertAPI',
+ submenu: [
+ {
+ label: 'Cloud Conversion...',
+ click: () => showConvertAPIDialog()
+ },
+ {
+ type: 'separator'
+ },
+ {
+ label: 'About ConvertAPI',
+ click: () => {
+ dialog.showMessageBox(mainWindow, {
+ type: 'info',
+ title: 'About ConvertAPI',
+ message: 'ConvertAPI Integration',
+ detail: 'ConvertAPI provides cloud-based file conversion with support for 200+ formats.\n\nFeatures:\n• Convert between 200+ file formats\n• High-quality conversions\n• Fast processing\n• Secure cloud storage\n\nGet your free API key at:\nhttps://www.convertapi.com\n\nFree tier includes 250 conversions/month.',
+ buttons: ['OK']
+ });
+ }
+ }
+ ]
+ },
{
label: 'Help',
submenu: [
@@ -277,7 +301,7 @@ function createMenu() {
type: 'info',
title: 'About PanConverter',
message: 'PanConverter',
- detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.5.7\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality\n• Live word count and statistics',
+ detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.6.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality\n• Live word count and statistics',
buttons: ['OK']
});
}
@@ -343,6 +367,54 @@ function showBatchConversionDialog() {
mainWindow.webContents.send('show-batch-dialog');
}
+// ConvertAPI integration
+function showConvertAPIDialog() {
+ mainWindow.webContents.send('show-convertapi-dialog');
+}
+
+// Handle ConvertAPI conversion
+ipcMain.on('convertapi-convert', async (event, { apiKey, fromFormat, toFormat, filePath }) => {
+ try {
+ const ConvertAPI = require('convertapi')(apiKey);
+
+ mainWindow.webContents.send('convertapi-status', 'Converting file...');
+
+ const result = await ConvertAPI.convert(toFormat, {
+ File: filePath
+ }, fromFormat);
+
+ // Save the converted file
+ const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`);
+ await result.saveFiles(path.dirname(outputPath));
+
+ mainWindow.webContents.send('convertapi-complete', {
+ success: true,
+ outputPath: result.files[0].path
+ });
+
+ dialog.showMessageBox(mainWindow, {
+ type: 'info',
+ title: 'Conversion Complete',
+ message: 'File converted successfully!',
+ detail: `Saved to: ${result.files[0].path}`,
+ buttons: ['OK']
+ });
+ } catch (error) {
+ mainWindow.webContents.send('convertapi-complete', {
+ success: false,
+ error: error.message
+ });
+
+ dialog.showMessageBox(mainWindow, {
+ type: 'error',
+ title: 'Conversion Failed',
+ message: 'ConvertAPI conversion failed',
+ detail: error.message,
+ buttons: ['OK']
+ });
+ }
+});
+
function performExportWithOptions(format, options) {
const outputFile = dialog.showSaveDialogSync(mainWindow, {
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
diff --git a/src/renderer.js b/src/renderer.js
index 14f358f..9fe16fd 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -146,6 +146,16 @@ class TabManager {
editor.addEventListener('input', () => {
this.handleEditorInput(tab.id);
});
+
+ // Add scroll listener for line number sync
+ editor.addEventListener('scroll', () => {
+ if (this.showLineNumbers && this.activeTabId === tab.id) {
+ const lineNumbers = document.getElementById(`line-numbers-${tab.id}`);
+ if (lineNumbers) {
+ lineNumbers.scrollTop = editor.scrollTop;
+ }
+ }
+ });
}
}
@@ -337,15 +347,18 @@ class TabManager {
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) =>
+ const lines = editor.value.split('\n');
+ lineNumbers.innerHTML = lines.map((_, i) =>
`
${i + 1}
`
).join('');
lineNumbers.classList.remove('hidden');
+
+ // Sync scroll position
+ lineNumbers.scrollTop = editor.scrollTop;
} else {
lineNumbers.classList.add('hidden');
}
@@ -580,6 +593,21 @@ class TabManager {
this.insertTable();
});
+ // 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;
@@ -656,17 +684,267 @@ class TabManager {
// Trigger update
this.handleEditorInput(this.activeTabId);
}
+
+ // Insert a code block
+ insertCodeBlock() {
+ const editor = document.getElementById(`editor-${this.activeTabId}`);
+ if (!editor) return;
+
+ const selectedText = editor.value.substring(editor.selectionStart, editor.selectionEnd);
+ const codeBlock = '\n```\n' + (selectedText || 'code here') + '\n```\n';
+
+ const start = editor.selectionStart;
+ editor.value = editor.value.substring(0, start) + codeBlock + editor.value.substring(editor.selectionEnd);
+
+ // Update cursor position
+ editor.selectionStart = editor.selectionEnd = start + codeBlock.length;
+ editor.focus();
+
+ // Trigger update
+ this.handleEditorInput(this.activeTabId);
+ }
+
+ // Insert a horizontal rule
+ insertHorizontalRule() {
+ const editor = document.getElementById(`editor-${this.activeTabId}`);
+ if (!editor) return;
+
+ const hr = '\n\n---\n\n';
+ const start = editor.selectionStart;
+
+ editor.value = editor.value.substring(0, start) + hr + editor.value.substring(start);
+
+ // Update cursor position
+ editor.selectionStart = editor.selectionEnd = start + hr.length;
+ editor.focus();
+
+ // Trigger update
+ this.handleEditorInput(this.activeTabId);
+ }
setupFindEvents() {
- // Find dialog implementation...
- document.getElementById('btn-find').addEventListener('click', () => {
+ 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', () => {
document.getElementById('find-dialog').classList.remove('hidden');
- document.getElementById('find-input').focus();
+ findInput.focus();
});
-
- document.getElementById('btn-find-close').addEventListener('click', () => {
+
+ // Close find dialog
+ btnFindClose.addEventListener('click', () => {
document.getElementById('find-dialog').classList.add('hidden');
+ 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);
+ const editor = document.getElementById(`editor-${this.activeTabId}`);
+
+ if (!findText || !tab || !editor) {
+ this.clearFindHighlights();
+ const findCount = document.getElementById('find-count');
+ if (findCount) {
+ findCount.textContent = '0 matches';
+ }
+ return;
+ }
+
+ const content = editor.value;
+ 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 editor = document.getElementById(`editor-${this.activeTabId}`);
+ const findText = document.getElementById('find-input').value;
+
+ if (!tab || !editor || matchIndex < 0 || matchIndex >= tab.findMatches.length) return;
+
+ const position = tab.findMatches[matchIndex];
+
+ // Select the match WITHOUT focusing (to keep focus on find input)
+ editor.setSelectionRange(position, position + findText.length);
+
+ // Make the selection visible by briefly focusing and then restoring focus
+ const findInput = document.getElementById('find-input');
+ const hadFocus = document.activeElement === findInput;
+
+ // Temporarily focus editor to make selection visible
+ editor.focus();
+
+ // Restore focus to find input if it had focus
+ if (hadFocus) {
+ setTimeout(() => {
+ findInput.focus();
+ // Restore cursor position in find input
+ findInput.setSelectionRange(findInput.value.length, findInput.value.length);
+ }, 10);
+ }
+
+ // Scroll into view
+ const lineHeight = 20; // Approximate line height
+ const charPosition = position;
+ const numLines = editor.value.substring(0, charPosition).split('\n').length;
+ const scrollPosition = (numLines - 5) * lineHeight; // Show match 5 lines from top
+
+ editor.scrollTop = Math.max(0, scrollPosition);
+
+ // 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 editor = document.getElementById(`editor-${this.activeTabId}`);
+ const findText = document.getElementById('find-input').value;
+ const replaceText = document.getElementById('replace-input').value;
+
+ if (!tab || !editor || tab.findMatches.length === 0 || tab.currentMatchIndex < 0) return;
+
+ const position = tab.findMatches[tab.currentMatchIndex];
+ const before = editor.value.substring(0, position);
+ const after = editor.value.substring(position + findText.length);
+
+ editor.value = before + replaceText + after;
+ tab.content = editor.value;
+ 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 editor = document.getElementById(`editor-${this.activeTabId}`);
+ const findText = document.getElementById('find-input').value;
+ const replaceText = document.getElementById('replace-input').value;
+
+ if (!tab || !editor || !findText) return;
+
+ // Simple replace all
+ const newContent = editor.value.split(findText).join(replaceText);
+ const replacedCount = tab.findMatches.length;
+
+ editor.value = newContent;
+ tab.content = newContent;
+ 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;
+ }
}
// File operations
@@ -723,6 +1001,16 @@ document.addEventListener('DOMContentLoaded', () => {
initialEditor.addEventListener('input', () => {
tabManager.handleEditorInput(1);
});
+
+ // Add scroll listener for line number sync
+ initialEditor.addEventListener('scroll', () => {
+ if (tabManager.showLineNumbers && tabManager.activeTabId === 1) {
+ const lineNumbers = document.getElementById('line-numbers-1');
+ if (lineNumbers) {
+ lineNumbers.scrollTop = initialEditor.scrollTop;
+ }
+ }
+ });
}
// Request current theme
@@ -1081,6 +1369,22 @@ ipcRenderer.on('show-batch-dialog', () => {
showBatchDialog();
});
+// ConvertAPI dialog handlers
+ipcRenderer.on('show-convertapi-dialog', () => {
+ showConvertAPIDialog();
+});
+
+ipcRenderer.on('convertapi-status', (event, status) => {
+ document.getElementById('convertapi-status').textContent = status;
+});
+
+ipcRenderer.on('convertapi-complete', (event, result) => {
+ document.getElementById('convertapi-progress').classList.add('hidden');
+ if (result.success) {
+ document.getElementById('convertapi-dialog').classList.add('hidden');
+ }
+});
+
ipcRenderer.on('batch-progress', (event, progress) => {
updateBatchProgress(progress);
});
@@ -1233,6 +1537,71 @@ if (originalExportConfirm) {
});
}
+// ConvertAPI Dialog Functions
+function showConvertAPIDialog() {
+ const dialog = document.getElementById('convertapi-dialog');
+ dialog.classList.remove('hidden');
+
+ // Load saved API key if exists
+ const savedKey = localStorage.getItem('convertapi-key');
+ if (savedKey) {
+ document.getElementById('convertapi-key').value = savedKey;
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ // ConvertAPI dialog close
+ const convertAPIDialogClose = document.getElementById('convertapi-dialog-close');
+ if (convertAPIDialogClose) {
+ convertAPIDialogClose.addEventListener('click', () => {
+ document.getElementById('convertapi-dialog').classList.add('hidden');
+ });
+ }
+
+ // ConvertAPI cancel
+ const convertAPICancel = document.getElementById('convertapi-cancel');
+ if (convertAPICancel) {
+ convertAPICancel.addEventListener('click', () => {
+ document.getElementById('convertapi-dialog').classList.add('hidden');
+ });
+ }
+
+ // ConvertAPI convert
+ const convertAPIConvert = document.getElementById('convertapi-convert');
+ if (convertAPIConvert) {
+ convertAPIConvert.addEventListener('click', () => {
+ const apiKey = document.getElementById('convertapi-key').value;
+ const fromFormat = document.getElementById('convertapi-from').value;
+ const toFormat = document.getElementById('convertapi-to').value;
+ const filePath = tabManager.getCurrentFilePath();
+
+ if (!apiKey) {
+ alert('Please enter your ConvertAPI key');
+ return;
+ }
+
+ if (!filePath) {
+ alert('Please save the file first');
+ return;
+ }
+
+ // Save API key for future use
+ localStorage.setItem('convertapi-key', apiKey);
+
+ // Show progress
+ document.getElementById('convertapi-progress').classList.remove('hidden');
+
+ // Send conversion request
+ ipcRenderer.send('convertapi-convert', {
+ apiKey,
+ fromFormat,
+ toFormat,
+ filePath
+ });
+ });
+ }
+});
+
// IPC event listeners for recent files functionality
ipcRenderer.on('recent-files-cleared', () => {
if (tabManager) {