mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17c32f0b7d | ||
|
|
b93129bc44 | ||
|
|
b23e60a704 | ||
|
|
c559b34f3e | ||
|
|
a780f2984a | ||
|
|
0c9961d412 |
@@ -54,8 +54,8 @@ Download the latest release for your platform from the [Releases](https://github
|
|||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
- **AppImage**: `PanConverter-1.5.0.AppImage` (universal, may require `--no-sandbox` flag)
|
- **AppImage**: `PanConverter-1.5.0.AppImage` (universal, may require `--no-sandbox` flag)
|
||||||
- **Debian Package**: `pan-converter_1.5.0_amd64.deb`
|
- **Debian Package**: `pan-converter_1.5.6_amd64.deb`
|
||||||
- **Snap Package**: `pan-converter_1.5.0_amd64.snap`
|
- **Snap Package**: `pan-converter_1.5.6_amd64.snap`
|
||||||
|
|
||||||
### Install from Source
|
### Install from Source
|
||||||
```bash
|
```bash
|
||||||
@@ -138,6 +138,8 @@ npm run dist:all
|
|||||||
|
|
||||||
## Version History
|
## 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.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.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
|
- **v1.2.1** - Comprehensive editor enhancements: Find & Replace, Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document conversion menu, table creation helper, spreadsheet export
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.5 KiB |
+13
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pan-converter",
|
"name": "pan-converter",
|
||||||
"version": "1.5.3",
|
"version": "1.6.1",
|
||||||
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
"convertapi": "^1.15.0",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"electron-store": "^10.1.0",
|
"electron-store": "^10.1.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist"
|
"output": "dist"
|
||||||
},
|
},
|
||||||
|
"icon": "assets/icon",
|
||||||
"files": [
|
"files": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
@@ -74,20 +76,26 @@
|
|||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"target": "portable",
|
"target": "portable",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"target": "zip",
|
"target": "zip",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||||
"requestedExecutionLevel": "asInvoker",
|
"requestedExecutionLevel": "asInvoker",
|
||||||
"signAndEditExecutable": true
|
"signAndEditExecutable": false
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
|
|||||||
@@ -75,6 +75,27 @@
|
|||||||
<line x1="15" y1="3" x2="15" y2="21"></line>
|
<line x1="15" y1="3" x2="15" y2="21"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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>
|
<div class="toolbar-separator"></div>
|
||||||
<button id="btn-find" title="Find & Replace (Ctrl+F)">
|
<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">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
@@ -233,6 +254,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Batch Conversion Dialog -->
|
||||||
<div id="batch-dialog" class="batch-dialog hidden">
|
<div id="batch-dialog" class="batch-dialog hidden">
|
||||||
<div class="batch-dialog-content">
|
<div class="batch-dialog-content">
|
||||||
|
|||||||
+118
-38
@@ -2,18 +2,19 @@ const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron')
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
const XLSX = require('xlsx');
|
|
||||||
|
|
||||||
// Get the system Pandoc path
|
// Get the system Pandoc path
|
||||||
function getPandocPath() {
|
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';
|
return 'pandoc';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Pandoc is available
|
// Check if Pandoc is available
|
||||||
function checkPandocAvailable() {
|
function checkPandocAvailable() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
exec('pandoc --version', (error) => {
|
const pandocPath = getPandocPath();
|
||||||
|
exec(`${pandocPath} --version`, (error) => {
|
||||||
resolve(!error);
|
resolve(!error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -181,7 +182,6 @@ function createMenu() {
|
|||||||
{ label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') },
|
{ label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') },
|
{ label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') },
|
||||||
{ label: 'XLSX (Tables)', click: () => exportSpreadsheet('xlsx') }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@@ -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',
|
label: 'Help',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -277,7 +301,7 @@ function createMenu() {
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'About PanConverter',
|
title: 'About PanConverter',
|
||||||
message: '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.1\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']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -343,6 +367,54 @@ function showBatchConversionDialog() {
|
|||||||
mainWindow.webContents.send('show-batch-dialog');
|
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) {
|
function performExportWithOptions(format, options) {
|
||||||
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
||||||
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
||||||
@@ -416,12 +488,12 @@ function performExportWithOptions(format, options) {
|
|||||||
|
|
||||||
// Add specific options for PDF export to ensure proper generation
|
// Add specific options for PDF export to ensure proper generation
|
||||||
if (format === 'pdf') {
|
if (format === 'pdf') {
|
||||||
const pdfEngine = options.pdfEngine || 'xelatex';
|
const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex
|
||||||
const geometry = options.geometry || 'margin=1in';
|
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
|
||||||
pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`;
|
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
||||||
|
|
||||||
// Try with specified PDF engine
|
// Try with specified PDF engine
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
// Try fallback engines if the specified one fails
|
// Try fallback engines if the specified one fails
|
||||||
const fallbackEngines = ['pdflatex', 'lualatex'];
|
const fallbackEngines = ['pdflatex', 'lualatex'];
|
||||||
@@ -431,13 +503,7 @@ function performExportWithOptions(format, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (format === 'docx') {
|
} else if (format === 'docx') {
|
||||||
pandocCmd = `${getPandocPath()} "${currentFile}" -t docx -o "${outputFile}"`;
|
pandocCmd += ' -t docx';
|
||||||
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}"`;
|
|
||||||
exportWithPandoc(pandocCmd, outputFile, format);
|
exportWithPandoc(pandocCmd, outputFile, format);
|
||||||
} else {
|
} else {
|
||||||
// Generic export for other formats
|
// Generic export for other formats
|
||||||
@@ -462,9 +528,10 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const engine = engines[index];
|
const engine = engines[index];
|
||||||
const geometry = options.geometry || 'margin=1in';
|
|
||||||
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`;
|
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`;
|
||||||
|
|
||||||
|
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
||||||
|
|
||||||
// Add all other options
|
// Add all other options
|
||||||
if (options.template && options.template !== 'default') {
|
if (options.template && options.template !== 'default') {
|
||||||
pandocCmd += ` --template="${options.template}"`;
|
pandocCmd += ` --template="${options.template}"`;
|
||||||
@@ -478,7 +545,7 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
|
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
|
||||||
} else {
|
} else {
|
||||||
@@ -498,16 +565,35 @@ function showExportSuccess(outputFile) {
|
|||||||
|
|
||||||
// Helper function to export with pandoc (general)
|
// Helper function to export with pandoc (general)
|
||||||
function exportWithPandoc(pandocCmd, outputFile, format) {
|
function exportWithPandoc(pandocCmd, outputFile, format) {
|
||||||
|
console.log(`Executing Pandoc command: ${pandocCmd}`);
|
||||||
|
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`Pandoc error for ${format}:`, error);
|
console.error(`Pandoc error for ${format}:`, error);
|
||||||
dialog.showErrorBox('Export Error',
|
console.error(`Pandoc stderr:`, stderr);
|
||||||
`Failed to export to ${format.toUpperCase()}:\n${error.message}\n\n` +
|
console.error(`Pandoc stdout:`, stdout);
|
||||||
`Command used: ${pandocCmd}\n\n` +
|
|
||||||
`Please ensure Pandoc is properly installed and accessible.`
|
// 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 {
|
} else {
|
||||||
console.log(`Successfully exported to ${format}:`, outputFile);
|
console.log(`Successfully exported to ${format}:`, outputFile);
|
||||||
|
console.log(`Pandoc stdout:`, stdout);
|
||||||
|
if (stderr) {
|
||||||
|
console.warn(`Pandoc stderr (non-fatal):`, stderr);
|
||||||
|
}
|
||||||
showExportSuccess(outputFile);
|
showExportSuccess(outputFile);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -878,20 +964,7 @@ ipcMain.on('export-spreadsheet', (event, { content, format }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write CSV file
|
|
||||||
fs.writeFileSync(outputFile, csvContent, 'utf-8');
|
fs.writeFileSync(outputFile, csvContent, 'utf-8');
|
||||||
} else if (format === 'xlsx') {
|
|
||||||
// Convert tables to XLSX format
|
|
||||||
const workbook = XLSX.utils.book_new();
|
|
||||||
|
|
||||||
tables.forEach((table, index) => {
|
|
||||||
const sheetName = tables.length > 1 ? `Table${index + 1}` : 'Table';
|
|
||||||
const worksheet = XLSX.utils.aoa_to_sheet(table);
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Write XLSX file
|
|
||||||
XLSX.writeFile(workbook, outputFile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(mainWindow, {
|
||||||
@@ -1054,8 +1127,8 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
// Add PDF-specific options
|
// Add PDF-specific options
|
||||||
if (format === 'pdf') {
|
if (format === 'pdf') {
|
||||||
const pdfEngine = options.pdfEngine || 'xelatex';
|
const pdfEngine = options.pdfEngine || 'xelatex';
|
||||||
const geometry = options.geometry || 'margin=1in';
|
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
|
||||||
pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`;
|
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute conversion
|
// Execute conversion
|
||||||
@@ -1293,7 +1366,14 @@ function openFileFromPath(filePath) {
|
|||||||
currentFile = filePath;
|
currentFile = filePath;
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
if (mainWindow && mainWindow.webContents) {
|
if (mainWindow && mainWindow.webContents) {
|
||||||
|
// Wait for the renderer to be fully loaded before sending file data
|
||||||
|
if (mainWindow.webContents.isLoading()) {
|
||||||
|
mainWindow.webContents.once('did-finish-load', () => {
|
||||||
|
mainWindow.webContents.send('file-opened', { path: filePath, content });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
mainWindow.webContents.send('file-opened', { path: filePath, content });
|
mainWindow.webContents.send('file-opened', { path: filePath, content });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+515
-22
@@ -139,6 +139,24 @@ class TabManager {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
document.querySelector('.editor-container').appendChild(tabContent);
|
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) {
|
switchToTab(tabId) {
|
||||||
@@ -333,11 +351,14 @@ class TabManager {
|
|||||||
if (!editor || !lineNumbers) return;
|
if (!editor || !lineNumbers) return;
|
||||||
|
|
||||||
if (this.showLineNumbers) {
|
if (this.showLineNumbers) {
|
||||||
const lines = editor.value.split('\\n');
|
const lines = editor.value.split('\n');
|
||||||
lineNumbers.innerHTML = lines.map((_, i) =>
|
lineNumbers.innerHTML = lines.map((_, i) =>
|
||||||
`<div class="line-number">${i + 1}</div>`
|
`<div class="line-number">${i + 1}</div>`
|
||||||
).join('');
|
).join('');
|
||||||
lineNumbers.classList.remove('hidden');
|
lineNumbers.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Sync scroll position
|
||||||
|
lineNumbers.scrollTop = editor.scrollTop;
|
||||||
} else {
|
} else {
|
||||||
lineNumbers.classList.add('hidden');
|
lineNumbers.classList.add('hidden');
|
||||||
}
|
}
|
||||||
@@ -376,24 +397,22 @@ class TabManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupEditorEvents() {
|
setupEditorEvents() {
|
||||||
// Set up editor events using event delegation
|
// Set up editor events using event delegation on the container
|
||||||
document.addEventListener('input', (e) => {
|
const editorContainer = document.querySelector('.editor-container');
|
||||||
|
if (editorContainer) {
|
||||||
|
editorContainer.addEventListener('input', (e) => {
|
||||||
if (e.target.classList.contains('editor-textarea')) {
|
if (e.target.classList.contains('editor-textarea')) {
|
||||||
const tabId = parseInt(e.target.id.split('-')[1]);
|
const tabId = parseInt(e.target.id.split('-')[1]);
|
||||||
if (tabId === this.activeTabId) {
|
|
||||||
this.handleEditorInput(tabId);
|
this.handleEditorInput(tabId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('scroll', (e) => {
|
editorContainer.addEventListener('scroll', (e) => {
|
||||||
if (e.target.classList.contains('editor-textarea')) {
|
if (e.target.classList.contains('editor-textarea')) {
|
||||||
const tabId = parseInt(e.target.id.split('-')[1]);
|
|
||||||
if (tabId === this.activeTabId) {
|
|
||||||
this.updateLineNumbers();
|
this.updateLineNumbers();
|
||||||
}
|
}
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditorInput(tabId) {
|
handleEditorInput(tabId) {
|
||||||
@@ -401,6 +420,8 @@ class TabManager {
|
|||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
const editor = document.getElementById(`editor-${tabId}`);
|
const editor = document.getElementById(`editor-${tabId}`);
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
tab.content = editor.value;
|
tab.content = editor.value;
|
||||||
tab.isDirty = true;
|
tab.isDirty = true;
|
||||||
|
|
||||||
@@ -517,7 +538,7 @@ class TabManager {
|
|||||||
|
|
||||||
// Save to localStorage and sync with main process
|
// Save to localStorage and sync with main process
|
||||||
localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles));
|
localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles));
|
||||||
window.electronAPI.send('save-recent-files', this.recentFiles);
|
ipcRenderer.send('save-recent-files', this.recentFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecentFiles() {
|
getRecentFiles() {
|
||||||
@@ -532,40 +553,411 @@ class TabManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupToolbarEvents() {
|
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', () => {
|
document.getElementById('btn-preview-toggle').addEventListener('click', () => {
|
||||||
this.isPreviewVisible = !this.isPreviewVisible;
|
this.isPreviewVisible = !this.isPreviewVisible;
|
||||||
this.updatePreviewVisibility();
|
this.updatePreviewVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Line numbers
|
||||||
document.getElementById('btn-line-numbers').addEventListener('click', () => {
|
document.getElementById('btn-line-numbers').addEventListener('click', () => {
|
||||||
this.showLineNumbers = !this.showLineNumbers;
|
this.showLineNumbers = !this.showLineNumbers;
|
||||||
this.updateLineNumbers();
|
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() {
|
setupFindEvents() {
|
||||||
// Find dialog implementation...
|
const btnFind = document.getElementById('btn-find');
|
||||||
document.getElementById('btn-find').addEventListener('click', () => {
|
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-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');
|
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
|
// File operations
|
||||||
openFile(filePath, content) {
|
openFile(filePath, content) {
|
||||||
let tab = this.tabs.get(this.activeTabId);
|
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 current tab is empty and untitled, reuse it
|
||||||
if (!tab.filePath && !tab.isDirty && tab.content === '') {
|
if (!tab.filePath && !tab.isDirty && tab.content === '') {
|
||||||
tab.filePath = filePath;
|
tab.filePath = filePath;
|
||||||
tab.title = filePath.split('/').pop();
|
tab.title = fileName;
|
||||||
tab.content = content;
|
tab.content = content;
|
||||||
tab.originalContent = content;
|
tab.originalContent = content;
|
||||||
tab.isDirty = false;
|
tab.isDirty = false;
|
||||||
@@ -574,7 +966,7 @@ class TabManager {
|
|||||||
this.createNewTab();
|
this.createNewTab();
|
||||||
tab = this.tabs.get(this.activeTabId);
|
tab = this.tabs.get(this.activeTabId);
|
||||||
tab.filePath = filePath;
|
tab.filePath = filePath;
|
||||||
tab.title = filePath.split('/').pop();
|
tab.title = fileName;
|
||||||
tab.content = content;
|
tab.content = content;
|
||||||
tab.originalContent = content;
|
tab.originalContent = content;
|
||||||
tab.isDirty = false;
|
tab.isDirty = false;
|
||||||
@@ -603,6 +995,24 @@ let tabManager;
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
tabManager = new TabManager();
|
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
|
// Request current theme
|
||||||
ipcRenderer.send('get-theme');
|
ipcRenderer.send('get-theme');
|
||||||
|
|
||||||
@@ -626,7 +1036,9 @@ ipcRenderer.on('file-new', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('file-opened', (event, data) => {
|
ipcRenderer.on('file-opened', (event, data) => {
|
||||||
|
if (tabManager) {
|
||||||
tabManager.openFile(data.path, data.content);
|
tabManager.openFile(data.path, data.content);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('file-save', () => {
|
ipcRenderer.on('file-save', () => {
|
||||||
@@ -957,6 +1369,22 @@ ipcRenderer.on('show-batch-dialog', () => {
|
|||||||
showBatchDialog();
|
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) => {
|
ipcRenderer.on('batch-progress', (event, progress) => {
|
||||||
updateBatchProgress(progress);
|
updateBatchProgress(progress);
|
||||||
});
|
});
|
||||||
@@ -1109,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
|
// IPC event listeners for recent files functionality
|
||||||
if (window.electronAPI) {
|
ipcRenderer.on('recent-files-cleared', () => {
|
||||||
window.electronAPI.on('recent-files-cleared', () => {
|
if (tabManager) {
|
||||||
tabManager.recentFiles = [];
|
tabManager.recentFiles = [];
|
||||||
localStorage.setItem('recentFiles', JSON.stringify([]));
|
localStorage.setItem('recentFiles', JSON.stringify([]));
|
||||||
console.log('Recent files cleared');
|
console.log('Recent files cleared');
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Add math rendering support using KaTeX for enhanced preview
|
// Add math rendering support using KaTeX for enhanced preview
|
||||||
function initMathSupport() {
|
function initMathSupport() {
|
||||||
|
|||||||
Reference in New Issue
Block a user