Release v1.8.3: Major UI and Export Enhancements

This release includes significant improvements to PDF Editor UI, enhanced PDF/Word
export functionality, print fixes, and configurable template settings.
🎯 v1.8.1 - Streamlined PDF Editor UI:
- Clean, focused interface showing only the selected PDF operation
- Smooth fade-in animations when switching between functions
- Optimized dialog sizing (max-width 600px, max-height 85vh)
- Enhanced file list styling with better contrast
- Complete dark theme support for all PDF editor components
- Better scrolling behavior for long forms
📄 v1.8.2 - Enhanced PDF Export & Print Fix:
- New "PDF (Enhanced)" export option using Word template + LibreOffice conversion
- Added to both single-file export and batch converter
- Keyboard shortcut: Ctrl+Shift+P for enhanced PDF export
- Fixed print preview showing blank output issue
- Simplified print handler using CSS @media print rules
- Better print styling with proper element hiding/showing
⚙️ v1.8.3 - Configurable Template Settings:
- Added "Template Settings..." menu option
- Configurable start page for Word template content insertion
- Choose from Pages 1-5 or custom page number (1-100)
- Settings persisted across sessions
- Updated WordTemplateExporter to use configurable start page
- Works with both DOCX (Enhanced) and PDF (Enhanced) export
- Applied to single-file and batch conversion workflows
🔧 Technical Improvements:
- Enhanced WordTemplateExporter constructor to accept startPage parameter
- Updated insertContentAfterPage method for flexible page insertion
- Added Template Settings dialog with quick-select buttons
- IPC communication for custom page number input
- Settings stored in userData/settings.json
- Improved CSS specificity with PDF editor-specific classes
- Fixed @media print rules to ensure preview content is visible
🎨 UI/UX Enhancements:
- PDF Editor sections now animate smoothly when switching
- File lists for merge operations styled with proper contrast
- Remove buttons styled with danger color (#dc3545)
- Template settings dialog shows current template and start page
- All enhancements support all 22 existing themes
The application now provides professional-grade PDF editing, enhanced export
capabilities with full control over template-based generation, and a streamlined
user interface that makes complex operations simple and intuitive.
🤖 Generated with Claude Code
This commit is contained in:
2025-10-28 16:59:15 +05:30
parent f6116d7008
commit c5d310b465
6 changed files with 284 additions and 101 deletions
+1
View File
@@ -515,6 +515,7 @@
<select id="batch-format">
<option value="html">HTML</option>
<option value="pdf">PDF</option>
<option value="pdf-enhanced">PDF (Enhanced)</option>
<option value="docx">DOCX</option>
<option value="docx-enhanced">DOCX (Enhanced)</option>
<option value="latex">LaTeX</option>
+217 -6
View File
@@ -49,6 +49,7 @@ let mainWindow;
let currentFile = null; // This will now represent the active tab's file
let pandocAvailable = null; // Cache pandoc availability check
let wordTemplatePath = null; // Path to selected Word template
let templateStartPage = 3; // Which page to start inserting content (default: page 3)
let rendererReady = false; // Track if renderer is ready to receive file data
// Handle single instance lock for Windows file association
@@ -238,6 +239,7 @@ function createMenu() {
submenu: [
{ label: 'HTML', click: () => exportFile('html') },
{ label: 'PDF', click: () => exportFile('pdf') },
{ label: 'PDF (Enhanced)', click: () => exportPDFViaWordTemplate(), accelerator: 'Ctrl+Shift+P' },
{ label: 'DOCX', click: () => exportFile('docx') },
{ label: 'DOCX (Enhanced)', click: () => exportWordWithTemplate(), accelerator: 'Ctrl+Shift+W' },
{ label: 'LaTeX', click: () => exportFile('latex') },
@@ -256,6 +258,10 @@ function createMenu() {
label: 'Select Word Template...',
click: selectWordTemplate
},
{
label: 'Template Settings...',
click: showTemplateSettings
},
{ type: 'separator' },
{
label: 'Quit',
@@ -475,7 +481,7 @@ function createMenu() {
type: 'info',
title: 'About PanConverter',
message: 'PanConverter',
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.8.1\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Modern glassmorphism UI with gradient backgrounds\n• Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\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• Enhanced Word export with template support (single file & batch)\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• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Undo/redo functionality\n• Live word count and statistics',
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.8.3\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Modern glassmorphism UI with gradient backgrounds\n• Enhanced PDF export via Word template with configurable start page\n• Configurable template settings (start page selection)\n• Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\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• Enhanced Word export with template support (single file & batch)\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• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Undo/redo functionality\n• Live word count and statistics',
buttons: ['OK']
});
}
@@ -562,6 +568,56 @@ async function selectWordTemplate() {
}
}
// Template Settings Dialog
async function showTemplateSettings() {
const result = await dialog.showMessageBox(mainWindow, {
type: 'question',
title: 'Template Settings',
message: 'Configure Word Template Export',
detail: `Current template: ${wordTemplatePath ? path.basename(wordTemplatePath) : 'Default template'}\nContent starts from page: ${templateStartPage}\n\nWhich page should content start from?\n(Templates usually have cover pages, TOC, etc.)`,
buttons: ['Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5', 'Custom...', 'Cancel'],
defaultId: templateStartPage - 1,
cancelId: 6
});
if (result.response === 6) return; // Cancel
let newStartPage;
if (result.response === 5) { // Custom
// Show input dialog for custom page number
mainWindow.webContents.send('show-custom-start-page-dialog', templateStartPage);
} else {
newStartPage = result.response + 1; // Convert button index to page number
templateStartPage = newStartPage;
store.set('templateStartPage', templateStartPage);
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Settings Updated',
message: 'Template settings have been updated',
detail: `Content will now start from page ${templateStartPage}`
});
}
}
// Handle custom start page input from renderer
ipcMain.on('set-custom-start-page', (event, pageNumber) => {
const page = parseInt(pageNumber);
if (page >= 1 && page <= 100) {
templateStartPage = page;
store.set('templateStartPage', templateStartPage);
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Settings Updated',
message: 'Template settings have been updated',
detail: `Content will now start from page ${templateStartPage}`
});
} else {
dialog.showErrorBox('Invalid Page Number', 'Please enter a page number between 1 and 100');
}
});
// Enhanced Word Export with Template Support
async function exportWordWithTemplate() {
if (!currentFile) {
@@ -582,8 +638,8 @@ async function exportWordWithTemplate() {
if (result.canceled) return;
// Create exporter instance with selected template (if any)
const exporter = new WordTemplateExporter(wordTemplatePath);
// Create exporter instance with selected template and start page
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
// Convert markdown to DOCX
await exporter.convert(content, result.filePath);
@@ -600,6 +656,79 @@ async function exportWordWithTemplate() {
}
}
// Enhanced PDF Export via Word Template
async function exportPDFViaWordTemplate() {
if (!currentFile) {
dialog.showErrorBox('Error', 'Please save the file first');
return;
}
try {
// Get markdown content
const content = fs.readFileSync(currentFile, 'utf-8');
// Show dialog for output file
const result = await dialog.showSaveDialog(mainWindow, {
title: 'Export to PDF (Enhanced)',
defaultPath: currentFile.replace(/\.md$/, '.pdf'),
filters: [{ name: 'PDF Document', extensions: ['pdf'] }]
});
if (result.canceled) return;
// Step 1: Create temporary DOCX file using Word template
const tempDocxPath = result.filePath.replace(/\.pdf$/, '_temp.docx');
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
await exporter.convert(content, tempDocxPath);
// Step 2: Convert DOCX to PDF using LibreOffice
const soffice = process.platform === 'win32'
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
: 'soffice';
const outputDir = path.dirname(result.filePath);
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
exec(convertCmd, (error, stdout, stderr) => {
// Clean up temporary DOCX file
try {
fs.unlinkSync(tempDocxPath);
} catch (e) {
console.error('Failed to delete temp file:', e);
}
if (error) {
dialog.showErrorBox('PDF Conversion Error',
`Failed to convert to PDF. Please ensure LibreOffice is installed.\n\nError: ${error.message}`);
return;
}
// LibreOffice creates file with same base name as input
const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf');
// Rename if needed
if (generatedPdfPath !== result.filePath) {
try {
fs.renameSync(generatedPdfPath, result.filePath);
} catch (e) {
console.error('Failed to rename PDF:', e);
}
}
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Export Successful',
message: 'PDF exported successfully using Word template!',
detail: `Saved to: ${result.filePath}`
});
});
} catch (error) {
dialog.showErrorBox('Export Error', `Failed to export PDF: ${error.message}`);
}
}
// Universal File Converter integration
function showUniversalConverterDialog() {
mainWindow.webContents.send('show-universal-converter-dialog');
@@ -1447,7 +1576,9 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
const inputFile = markdownFiles[index];
const relativePath = path.relative(inputFolder, inputFile);
const baseName = path.basename(relativePath, path.extname(relativePath));
const outputExtension = format === 'docx-enhanced' ? 'docx' : format;
let outputExtension = format;
if (format === 'docx-enhanced') outputExtension = 'docx';
if (format === 'pdf-enhanced') outputExtension = 'pdf';
const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${outputExtension}`));
// Create subdirectories in output folder if needed
@@ -1460,7 +1591,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
if (format === 'docx-enhanced') {
try {
const content = fs.readFileSync(inputFile, 'utf-8');
const exporter = new WordTemplateExporter(wordTemplatePath);
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
await exporter.convert(content, outputFile);
completedCount++;
@@ -1490,6 +1621,85 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
return;
}
// Handle PDF Enhanced format with Word Template → PDF conversion
if (format === 'pdf-enhanced') {
try {
const content = fs.readFileSync(inputFile, 'utf-8');
// Step 1: Create temporary DOCX file using Word template
const tempDocxPath = outputFile.replace(/\.pdf$/, '_temp.docx');
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
await exporter.convert(content, tempDocxPath);
// Step 2: Convert DOCX to PDF using LibreOffice
const soffice = process.platform === 'win32'
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
: 'soffice';
const outputDir = path.dirname(outputFile);
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
exec(convertCmd, (error, stdout, stderr) => {
// Clean up temporary DOCX file
try {
fs.unlinkSync(tempDocxPath);
} catch (e) {
console.error('Failed to delete temp file:', e);
}
if (error) {
// Update progress with error
mainWindow.webContents.send('batch-progress', {
completed: index + 1,
total: totalCount,
currentFile: path.basename(inputFile),
success: false
});
processNextFile(index + 1);
return;
}
// LibreOffice creates file with same base name as input
const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf');
// Rename if needed
if (generatedPdfPath !== outputFile) {
try {
fs.renameSync(generatedPdfPath, outputFile);
} catch (e) {
console.error('Failed to rename PDF:', e);
}
}
completedCount++;
// Update progress
mainWindow.webContents.send('batch-progress', {
completed: index + 1,
total: totalCount,
currentFile: path.basename(inputFile),
success: true
});
// Process next file
processNextFile(index + 1);
});
} catch (error) {
// Update progress with error
mainWindow.webContents.send('batch-progress', {
completed: index + 1,
total: totalCount,
currentFile: path.basename(inputFile),
success: false
});
// Process next file even if this one failed
processNextFile(index + 1);
}
return;
}
// Build pandoc command for other formats
let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`;
@@ -1692,8 +1902,9 @@ function buildPandocCommand(content, format, outputPath) {
}
app.whenReady().then(() => {
// Load saved Word template path
// Load saved Word template path and settings
wordTemplatePath = store.get('wordTemplatePath', null);
templateStartPage = store.get('templateStartPage', 3);
// Check for command line conversion requests
const args = process.argv.slice(2);
+13 -82
View File
@@ -1184,95 +1184,26 @@ function handlePrintPreview(withStyles) {
return;
}
// Store original display values for restoration
const elementsToHide = [];
console.log('[RENDERER] Starting print with withStyles:', withStyles);
console.log('[RENDERER] Preview content length:', previewContent.innerHTML.length);
// Hide toolbar
const toolbar = document.querySelector('.toolbar');
if (toolbar) {
elementsToHide.push({ elem: toolbar, display: toolbar.style.display });
toolbar.style.display = 'none';
// Add body classes for print mode - let CSS handle everything
document.body.classList.add('printing');
if (!withStyles) {
document.body.classList.add('printing-no-styles');
}
// Hide tab bar
const tabBar = document.querySelector('.tab-bar');
if (tabBar) {
elementsToHide.push({ elem: tabBar, display: tabBar.style.display });
tabBar.style.display = 'none';
}
// Hide status bar
const statusBar = document.querySelector('.status-bar');
if (statusBar) {
elementsToHide.push({ elem: statusBar, display: statusBar.style.display });
statusBar.style.display = 'none';
}
// Hide editor pane
const editorPane = document.getElementById(`editor-pane-${activeTabId}`);
if (editorPane) {
elementsToHide.push({ elem: editorPane, display: editorPane.style.display });
editorPane.style.display = 'none';
}
// Hide all dialogs
const dialogs = document.querySelectorAll('.dialog, .modal-overlay');
dialogs.forEach(dialog => {
elementsToHide.push({ elem: dialog, display: dialog.style.display });
dialog.style.display = 'none';
});
// Make preview pane full width
const previewPane = document.getElementById(`preview-pane-${activeTabId}`);
let originalPreviewStyles = {};
if (previewPane) {
originalPreviewStyles = {
position: previewPane.style.position,
top: previewPane.style.top,
left: previewPane.style.left,
width: previewPane.style.width,
height: previewPane.style.height,
margin: previewPane.style.margin,
padding: previewPane.style.padding
};
previewPane.style.position = 'absolute';
previewPane.style.top = '0';
previewPane.style.left = '0';
previewPane.style.width = '100%';
previewPane.style.height = 'auto';
previewPane.style.margin = '0';
previewPane.style.padding = '20px';
}
// Apply no-styles if needed
if (!withStyles && previewContent) {
previewContent.style.color = '#000';
previewContent.style.background = '#fff';
}
// Wait for DOM updates then print
// Wait for CSS to apply then print
setTimeout(() => {
console.log('[RENDERER] Sending do-print to main process');
ipcRenderer.send('do-print', { withStyles });
// Restore everything after print dialog opens
// Restore classes after print dialog opens
setTimeout(() => {
// Restore hidden elements
elementsToHide.forEach(({ elem, display }) => {
elem.style.display = display;
});
// Restore preview pane styles
if (previewPane) {
Object.assign(previewPane.style, originalPreviewStyles);
}
// Restore preview content styles
if (!withStyles && previewContent) {
previewContent.style.color = '';
previewContent.style.background = '';
}
}, 500);
}, 200);
document.body.classList.remove('printing', 'printing-no-styles');
console.log('[RENDERER] Print classes removed');
}, 1000);
}, 100);
}
// Export Dialog functionality
+28 -2
View File
@@ -2945,18 +2945,33 @@ body.theme-concrete-warm .status-bar {
.editor-container,
#status-bar,
.status-bar,
.toolbar,
.tab-bar,
.editor-pane,
[id^="editor-pane-"] {
[id^="editor-pane-"],
.dialog,
.modal-overlay,
.export-dialog {
display: none !important;
}
/* Ensure body and html take full width */
html, body {
width: 100% !important;
height: auto !important;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
}
/* Show preview in full width */
#preview,
.preview,
.preview-content,
[id^="preview-"],
[id^="preview-pane-"],
.tab-content {
.tab-content,
.pane {
display: block !important;
width: 100% !important;
max-width: 100% !important;
@@ -2965,6 +2980,10 @@ body.theme-concrete-warm .status-bar {
padding: 20px !important;
overflow: visible !important;
position: static !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
}
/* Optimize preview content for printing */
@@ -2976,6 +2995,13 @@ body.theme-concrete-warm .status-bar {
background: white !important;
}
/* No-styles mode for printing without theme colors */
body.printing-no-styles .preview-content,
body.printing-no-styles [id^="preview-"] {
color: #000 !important;
background: #fff !important;
}
/* Text formatting */
.preview-content h1,
.preview-content h2,
+24 -10
View File
@@ -10,8 +10,9 @@ const PizZip = require('pizzip');
const Docx = require('docx4js').default;
class WordTemplateExporter {
constructor(templatePath) {
constructor(templatePath, startPage = 3) {
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
this.startPage = startPage; // Which page to start inserting content
}
/**
@@ -29,8 +30,8 @@ class WordTemplateExporter {
// Parse markdown and generate Word XML
const newContentXml = this.markdownToWordXml(markdownContent);
// Insert new content after page 2 (after the section break)
const modifiedXml = this.insertContentAfterPage2(documentXml, newContentXml);
// Insert new content after the specified start page
const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage);
// Update the zip with modified XML
zip.file('word/document.xml', modifiedXml);
@@ -47,20 +48,33 @@ class WordTemplateExporter {
}
/**
* Insert markdown content after page 2 in the document
* Insert markdown content after the specified page in the document
* @param {string} documentXml - The document XML
* @param {string} newContentXml - The new content to insert
* @param {number} afterPage - Insert content after this page number (1-based)
*/
insertContentAfterPage2(documentXml, newContentXml) {
// Find the last section break (after page 2)
insertContentAfterPage(documentXml, newContentXml, afterPage) {
// Find section breaks that mark page boundaries
// Look for the section properties tag that marks page breaks
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
const matches = [...documentXml.matchAll(sectionBreakRegex)];
if (matches.length >= 2) {
// Insert after the 2nd section break
const insertPoint = matches[1].index + matches[1][0].length;
// Calculate which section break to insert after
// Page 1 = before 1st section break
// Page 2 = after 1st section break
// Page 3 = after 2nd section break, etc.
const sectionIndex = afterPage - 1;
if (matches.length >= sectionIndex && sectionIndex > 0) {
// Insert after the specified section break
const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length;
return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint);
} else if (afterPage === 1 || matches.length === 0) {
// Insert at the beginning (after <w:body>) or if no section breaks found
const bodyStart = documentXml.indexOf('<w:body>') + 8;
return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart);
} else {
// If no section breaks found, insert before closing body tag
// If not enough section breaks, insert before closing body tag
return documentXml.replace('</w:body>', newContentXml + '</w:body>');
}
}