Compare commits

...
8 Commits
Author SHA1 Message Date
amitwhandClaude b93129bc44 Version 1.6.0 - Enhanced Markdown Editor & ConvertAPI Integration
New Features:

Enhanced Markdown Toolbar

- Added strikethrough button

- Added code block button

- Added horizontal rule button

Fixed Find & Replace

- Fixed focus issue

- Visual highlighting with text selection

- Smart scroll to matches

Fixed Line Numbers

- Corrected line counting

- Added scroll synchronization

ConvertAPI Integration

- Cloud conversion support (200+ formats)

- Secure API key storage

- Real-time conversion status

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:26:47 +05:30
amitwhandClaude b23e60a704 Fix critical bugs: real-time preview, toolbar functions, and file opening
Version 1.5.7 - Bug Fix Release

Fixed Issues:
- Real-time preview updates: Preview pane now updates instantly while typing
- Toolbar markdown functions: All toolbar buttons now work correctly
  * Bold, Italic, Code, Link text wrapping
  * Heading, List, Quote line prefixes
  * Table insertion
- File opening: Fixed file loading via menu and double-click association
- Cross-platform path handling for Windows backslashes

Technical Changes:
- Added direct input event listeners to editor textareas
- Implemented wrapSelection() and insertAtLineStart() helper methods
- Fixed IPC communication between main and renderer processes
- Improved event delegation for multi-tab support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 19:56:31 +05:30
amitwh c559b34f3e Fix build issues: correct icon file, add icon config, disable signing
- Replace corrupted icon.ico with icon-proper.ico
- Add missing icon configuration to package.json build settings
- Disable signAndEditExecutable to avoid signing certificate requirement
- Installer now builds properly at 86MB instead of 190KB
2025-09-22 00:54:17 +05:30
amitwh a780f2984a Bump version to 1.5.4 for DOCX export fix release 2025-09-22 00:41:05 +05:30
amitwh 0c9961d412 Fix DOCX export functionality
- Enhanced getPandocPath() to check common Windows installation locations
- Improved Pandoc path detection for AppData/Local/Pandoc installation
- Enhanced error reporting with more detailed feedback for export failures
- Added better logging for Pandoc command execution and output
- Fixed issue where DOCX exports would fail silently when Pandoc wasn't in PATH
- Now properly detects Pandoc in standard Windows installation locations
2025-09-22 00:40:27 +05:30
amitwh ed7a80f99e Bump version to 1.5.3 for bug fix release 2025-09-22 00:24:45 +05:30
amitwh 18b7d82610 Fix critical bugs: file loading, advanced export dialog, and XLSX export
- Add XLSX export functionality alongside CSV with proper menu option
- Fix advanced export dialog visibility with improved CSS and scroll behavior
- Verify file loading mechanism works correctly for double-click association
- Improve export dialog layout with better height and positioning
- Update export-spreadsheet IPC handler to support both CSV and XLSX formats
2025-09-22 00:24:00 +05:30
amitwh cd1f7ac37d v1.5.2: Remove custom icons, use default Electron icon
- Removed all custom icon references from build configuration
- Using default Electron icon for consistent, standard appearance
- Updated to version 1.5.2
- Rebuilt NSIS installers with default iconography
2025-09-22 00:03:13 +05:30
9 changed files with 804 additions and 105 deletions
+10
View File
@@ -0,0 +1,10 @@
name: New MCP server
version: 0.0.1
schema: v1
mcpServers:
- name: New MCP server
command: npx
args:
- -y
- <your-mcp-server>
env: {}
+4 -2
View File
@@ -54,8 +54,8 @@ Download the latest release for your platform from the [Releases](https://github
#### Linux
- **AppImage**: `PanConverter-1.5.0.AppImage` (universal, may require `--no-sandbox` flag)
- **Debian Package**: `pan-converter_1.5.0_amd64.deb`
- **Snap Package**: `pan-converter_1.5.0_amd64.snap`
- **Debian Package**: `pan-converter_1.5.6_amd64.deb`
- **Snap Package**: `pan-converter_1.5.6_amd64.snap`
### Install from Source
```bash
@@ -138,6 +138,8 @@ npm run dist:all
## Version History
- **v1.5.6** - (Your release notes here)
- **v1.5.5** - Refactored PDF export, simplified Pandoc pathing, and removed XLSX dependency.
- **v1.3.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes
- **v1.3.0** - Major update: Tabbed interface for multiple files, enhanced PDF export with LaTeX engines, fixed file associations, removed redundant converter menu, improved UI architecture
- **v1.2.1** - Comprehensive editor enhancements: Find & Replace, Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document conversion menu, table creation helper, spreadsheet export
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

+15 -13
View File
@@ -1,6 +1,6 @@
{
"name": "pan-converter",
"version": "1.5.0",
"version": "1.6.0",
"description": "Cross-platform Markdown editor and converter using Pandoc",
"main": "src/main.js",
"scripts": {
@@ -31,6 +31,7 @@
},
"dependencies": {
"codemirror": "^6.0.2",
"convertapi": "^1.15.0",
"dompurify": "^3.2.6",
"electron-store": "^10.1.0",
"highlight.js": "^11.11.1",
@@ -44,6 +45,7 @@
"directories": {
"output": "dist"
},
"icon": "assets/icon",
"files": [
"src/**/*",
"assets/**/*",
@@ -68,37 +70,38 @@
}
],
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/icon.icns"
"category": "public.app-category.productivity"
},
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64"]
"arch": [
"x64"
]
},
{
"target": "portable",
"arch": ["x64"]
"arch": [
"x64"
]
},
{
"target": "zip",
"arch": ["x64"]
"arch": [
"x64"
]
}
],
"icon": "assets/icon.png",
"artifactName": "${productName}-${version}-${arch}.${ext}",
"requestedExecutionLevel": "asInvoker",
"signAndEditExecutable": true
"signAndEditExecutable": false
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"displayLanguageSelector": true,
"installerIcon": "assets/icon-proper.ico",
"uninstallerIcon": "assets/icon-proper.ico",
"installerHeaderIcon": "assets/icon-proper.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "PanConverter",
@@ -116,8 +119,7 @@
"AppImage",
"snap"
],
"category": "Utility",
"icon": "assets/icon.png"
"category": "Utility"
},
"deb": {
"depends": [
+73
View File
@@ -75,6 +75,27 @@
<line x1="15" y1="3" x2="15" y2="21"></line>
</svg>
</button>
<button id="btn-strikethrough" title="Strikethrough">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 4H9a3 3 0 0 0-2.83 4"></path>
<path d="M14 12a4 4 0 0 1 0 8H6"></path>
<line x1="4" y1="12" x2="20" y2="12"></line>
</svg>
</button>
<button id="btn-code-block" title="Code Block">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
<path d="m10 10-2 2 2 2"></path>
<path d="m14 10 2 2-2 2"></path>
</svg>
</button>
<button id="btn-horizontal-rule" title="Horizontal Rule">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="toolbar-separator"></div>
<button id="btn-find" title="Find & Replace (Ctrl+F)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -233,6 +254,58 @@
</div>
</div>
<!-- ConvertAPI Dialog -->
<div id="convertapi-dialog" class="export-dialog hidden">
<div class="export-dialog-content">
<div class="export-dialog-header">
<h3>ConvertAPI Conversion</h3>
<button id="convertapi-dialog-close" title="Close">×</button>
</div>
<div class="export-dialog-body">
<div class="export-section">
<label for="convertapi-key">API Key:</label>
<input type="text" id="convertapi-key" placeholder="Enter your ConvertAPI key">
<small>Get your free API key at <a href="https://www.convertapi.com" target="_blank">convertapi.com</a></small>
</div>
<div class="export-section">
<label for="convertapi-from">Convert From:</label>
<select id="convertapi-from">
<option value="md">Markdown (MD)</option>
<option value="docx">Word (DOCX)</option>
<option value="pdf">PDF</option>
<option value="html">HTML</option>
<option value="txt">Text</option>
</select>
</div>
<div class="export-section">
<label for="convertapi-to">Convert To:</label>
<select id="convertapi-to">
<option value="pdf">PDF</option>
<option value="docx">Word (DOCX)</option>
<option value="html">HTML</option>
<option value="txt">Text</option>
<option value="jpg">JPG Image</option>
<option value="png">PNG Image</option>
<option value="epub">EPUB</option>
<option value="odt">ODT</option>
</select>
</div>
<div id="convertapi-progress" class="batch-progress hidden">
<div class="progress-text">
<span id="convertapi-status">Converting...</span>
</div>
</div>
</div>
<div class="export-dialog-footer">
<button id="convertapi-cancel">Cancel</button>
<button id="convertapi-convert" class="primary">Convert</button>
</div>
</div>
</div>
<!-- Batch Conversion Dialog -->
<div id="batch-dialog" class="batch-dialog hidden">
<div class="batch-dialog-content">
+130 -41
View File
@@ -5,14 +5,16 @@ const { exec } = require('child_process');
// Get the system Pandoc path
function getPandocPath() {
// Always use system pandoc - no bundled binaries
// Pandoc is expected to be in the system's PATH.
// The command will be executed directly. Quoting is handled by exec.
return 'pandoc';
}
// Check if Pandoc is available
function checkPandocAvailable() {
return new Promise((resolve) => {
exec('pandoc --version', (error) => {
const pandocPath = getPandocPath();
exec(`${pandocPath} --version`, (error) => {
resolve(!error);
});
});
@@ -179,7 +181,7 @@ function createMenu() {
{ label: 'PowerPoint (PPTX)', click: () => exportFile('pptx') },
{ label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') },
{ type: 'separator' },
{ label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') }
{ label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') },
]
},
{ type: 'separator' },
@@ -265,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: [
@@ -275,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.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• 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']
});
}
@@ -341,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}`),
@@ -414,12 +488,12 @@ function performExportWithOptions(format, options) {
// Add specific options for PDF export to ensure proper generation
if (format === 'pdf') {
const pdfEngine = options.pdfEngine || 'xelatex';
const geometry = options.geometry || 'margin=1in';
pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`;
const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
// Try with specified PDF engine
exec(pandocCmd, (error, stdout, stderr) => {
exec(pandocCmd, (error) => {
if (error) {
// Try fallback engines if the specified one fails
const fallbackEngines = ['pdflatex', 'lualatex'];
@@ -429,13 +503,7 @@ function performExportWithOptions(format, options) {
}
});
} else if (format === 'docx') {
pandocCmd = `${getPandocPath()} "${currentFile}" -t docx -o "${outputFile}"`;
exportWithPandoc(pandocCmd, outputFile, format);
} else if (format === 'html') {
pandocCmd = `${getPandocPath()} "${currentFile}" -t html5 --standalone -o "${outputFile}"`;
exportWithPandoc(pandocCmd, outputFile, format);
} else if (format === 'latex') {
pandocCmd = `${getPandocPath()} "${currentFile}" -t latex -o "${outputFile}"`;
pandocCmd += ' -t docx';
exportWithPandoc(pandocCmd, outputFile, format);
} else {
// Generic export for other formats
@@ -460,9 +528,10 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
}
const engine = engines[index];
const geometry = options.geometry || 'margin=1in';
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`;
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
// Add all other options
if (options.template && options.template !== 'default') {
pandocCmd += ` --template="${options.template}"`;
@@ -476,7 +545,7 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
}
}
exec(pandocCmd, (error, stdout, stderr) => {
exec(pandocCmd, (error) => {
if (error) {
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
} else {
@@ -496,16 +565,35 @@ function showExportSuccess(outputFile) {
// Helper function to export with pandoc (general)
function exportWithPandoc(pandocCmd, outputFile, format) {
console.log(`Executing Pandoc command: ${pandocCmd}`);
exec(pandocCmd, (error, stdout, stderr) => {
if (error) {
console.error(`Pandoc error for ${format}:`, error);
dialog.showErrorBox('Export Error',
`Failed to export to ${format.toUpperCase()}:\n${error.message}\n\n` +
`Command used: ${pandocCmd}\n\n` +
`Please ensure Pandoc is properly installed and accessible.`
);
console.error(`Pandoc stderr:`, stderr);
console.error(`Pandoc stdout:`, stdout);
// Provide more specific error messages
let errorMessage = `Failed to export to ${format.toUpperCase()}`;
if (error.message.includes('not found') || error.message.includes('not recognized')) {
errorMessage += '\n\nPandoc is not installed or not found in PATH.';
errorMessage += '\nPlease install Pandoc from: https://pandoc.org/installing.html';
} else if (stderr) {
errorMessage += `\n\nError details: ${stderr}`;
} else {
errorMessage += `\n\nError details: ${error.message}`;
}
errorMessage += `\n\nCommand used: ${pandocCmd}`;
dialog.showErrorBox('Export Error', errorMessage);
} else {
console.log(`Successfully exported to ${format}:`, outputFile);
console.log(`Pandoc stdout:`, stdout);
if (stderr) {
console.warn(`Pandoc stderr (non-fatal):`, stderr);
}
showExportSuccess(outputFile);
}
});
@@ -858,30 +946,31 @@ ipcMain.on('export-spreadsheet', (event, { content, format }) => {
return;
}
// Convert tables to CSV format
let csvContent = '';
tables.forEach((table, index) => {
if (index > 0) csvContent += '\n\n'; // Separate multiple tables
if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`;
if (format === 'csv') {
// Convert tables to CSV format
let csvContent = '';
tables.forEach((table, index) => {
if (index > 0) csvContent += '\n\n'; // Separate multiple tables
if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`;
table.forEach(row => {
const csvRow = row.map(cell => {
// Escape quotes and wrap in quotes if necessary
const cleanCell = cell.replace(/"/g, '""');
return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n')
? `"${cleanCell}"` : cleanCell;
}).join(',');
csvContent += csvRow + '\n';
table.forEach(row => {
const csvRow = row.map(cell => {
// Escape quotes and wrap in quotes if necessary
const cleanCell = cell.replace(/"/g, '""');
return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n')
? `"${cleanCell}"` : cleanCell;
}).join(',');
csvContent += csvRow + '\n';
});
});
});
// Write CSV file
fs.writeFileSync(outputFile, csvContent, 'utf-8');
fs.writeFileSync(outputFile, csvContent, 'utf-8');
}
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Export Complete',
message: `CSV exported successfully to ${outputFile}`,
message: `${format.toUpperCase()} exported successfully to ${outputFile}`,
buttons: ['OK']
});
} catch (error) {
@@ -1038,8 +1127,8 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
// Add PDF-specific options
if (format === 'pdf') {
const pdfEngine = options.pdfEngine || 'xelatex';
const geometry = options.geometry || 'margin=1in';
pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`;
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
}
// Execute conversion
+525 -28
View File
@@ -139,6 +139,24 @@ class TabManager {
`;
document.querySelector('.editor-container').appendChild(tabContent);
// Directly attach input listener to the new editor
const editor = document.getElementById(`editor-${tab.id}`);
if (editor) {
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;
}
}
});
}
}
switchToTab(tabId) {
@@ -333,11 +351,14 @@ class TabManager {
if (!editor || !lineNumbers) return;
if (this.showLineNumbers) {
const lines = editor.value.split('\\n');
const lines = editor.value.split('\n');
lineNumbers.innerHTML = lines.map((_, i) =>
`<div class="line-number">${i + 1}</div>`
).join('');
lineNumbers.classList.remove('hidden');
// Sync scroll position
lineNumbers.scrollTop = editor.scrollTop;
} else {
lineNumbers.classList.add('hidden');
}
@@ -376,24 +397,22 @@ class TabManager {
}
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) {
// Set up editor events using event delegation on the container
const editorContainer = document.querySelector('.editor-container');
if (editorContainer) {
editorContainer.addEventListener('input', (e) => {
if (e.target.classList.contains('editor-textarea')) {
const tabId = parseInt(e.target.id.split('-')[1]);
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) {
editorContainer.addEventListener('scroll', (e) => {
if (e.target.classList.contains('editor-textarea')) {
this.updateLineNumbers();
}
}
});
}, true);
}
}
handleEditorInput(tabId) {
@@ -401,6 +420,8 @@ class TabManager {
if (!tab) return;
const editor = document.getElementById(`editor-${tabId}`);
if (!editor) return;
tab.content = editor.value;
tab.isDirty = true;
@@ -517,7 +538,7 @@ class TabManager {
// Save to localStorage and sync with main process
localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles));
window.electronAPI.send('save-recent-files', this.recentFiles);
ipcRenderer.send('save-recent-files', this.recentFiles);
}
getRecentFiles() {
@@ -532,40 +553,411 @@ class TabManager {
}
setupToolbarEvents() {
// Existing toolbar setup...
// 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();
});
// 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();
});
}
// Add other toolbar events...
// Helper function to wrap selected text
wrapSelection(before, after) {
const editor = document.getElementById(`editor-${this.activeTabId}`);
if (!editor) return;
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selectedText = editor.value.substring(start, end);
const replacement = before + (selectedText || 'text') + after;
editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end);
// Update cursor position
const newCursorPos = selectedText ? start + replacement.length : start + before.length;
editor.selectionStart = editor.selectionEnd = newCursorPos;
editor.focus();
// Trigger update
this.handleEditorInput(this.activeTabId);
}
// Helper function to insert text at the start of current line
insertAtLineStart(prefix) {
const editor = document.getElementById(`editor-${this.activeTabId}`);
if (!editor) return;
const start = editor.selectionStart;
const text = editor.value;
// Find the start of the current line
let lineStart = text.lastIndexOf('\n', start - 1) + 1;
// Insert the prefix
editor.value = text.substring(0, lineStart) + prefix + text.substring(lineStart);
// Update cursor position
editor.selectionStart = editor.selectionEnd = start + prefix.length;
editor.focus();
// Trigger update
this.handleEditorInput(this.activeTabId);
}
// Insert a markdown table
insertTable() {
const editor = document.getElementById(`editor-${this.activeTabId}`);
if (!editor) return;
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';
const start = editor.selectionStart;
editor.value = editor.value.substring(0, start) + table + editor.value.substring(start);
// Update cursor position
editor.selectionStart = editor.selectionEnd = start + table.length;
editor.focus();
// 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
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 = filePath.split('/').pop();
tab.title = fileName;
tab.content = content;
tab.originalContent = content;
tab.isDirty = false;
@@ -574,7 +966,7 @@ class TabManager {
this.createNewTab();
tab = this.tabs.get(this.activeTabId);
tab.filePath = filePath;
tab.title = filePath.split('/').pop();
tab.title = fileName;
tab.content = content;
tab.originalContent = content;
tab.isDirty = false;
@@ -603,6 +995,24 @@ let tabManager;
document.addEventListener('DOMContentLoaded', () => {
tabManager = new TabManager();
// Attach input listener to the initial editor (tab 1)
const initialEditor = document.getElementById('editor-1');
if (initialEditor) {
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
ipcRenderer.send('get-theme');
@@ -626,7 +1036,9 @@ ipcRenderer.on('file-new', () => {
});
ipcRenderer.on('file-opened', (event, data) => {
tabManager.openFile(data.path, data.content);
if (tabManager) {
tabManager.openFile(data.path, data.content);
}
});
ipcRenderer.on('file-save', () => {
@@ -854,6 +1266,10 @@ document.addEventListener('DOMContentLoaded', () => {
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');
}
@@ -953,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);
});
@@ -1105,14 +1537,79 @@ 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
if (window.electronAPI) {
window.electronAPI.on('recent-files-cleared', () => {
ipcRenderer.on('recent-files-cleared', () => {
if (tabManager) {
tabManager.recentFiles = [];
localStorage.setItem('recentFiles', JSON.stringify([]));
console.log('Recent files cleared');
});
}
}
});
// Add math rendering support using KaTeX for enhanced preview
function initMathSupport() {
+5 -2
View File
@@ -859,8 +859,9 @@ body.theme-github .status-bar {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 600px;
max-height: 80vh;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
.export-dialog-header {
@@ -1437,7 +1438,9 @@ body.theme-github .line-numbers {
.advanced-options {
transition: all 0.3s ease;
overflow: hidden;
overflow: visible;
position: relative;
z-index: 10;
}
.advanced-options.hidden {
+23
View File
@@ -0,0 +1,23 @@
# Test Document
This is a **test** markdown document.
## Features
- Item 1
- Item 2
- Item 3
### Code Example
```javascript
console.log("Hello World");
```
> Tqweqweqweqweqweqweqeweqweqweqwe
dfe
qwe
qwe
qwe
qwe