mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
fix: improve tab safety and app feedback
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
Core application code lives in `src/`. Use `src/main.js` for the Electron main process, `src/preload.js` for the preload bridge, and `src/renderer.js` plus `src/editor/`, `src/sidebar/`, `src/repl/`, and `src/utils/` for renderer-side features. Electron adapter code is in `src/adapters/electron/`. Reusable markdown/document templates live in `src/templates/`. Static assets and icons are in `assets/`. Tests are in `tests/`, and build output goes to `dist/`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `npm start`: launch the Electron app locally.
|
||||
- `npm test`: run the Jest suite once.
|
||||
- `npm run test:watch`: rerun tests during local development.
|
||||
- `npm run test:coverage`: generate coverage output.
|
||||
- `npm run lint` / `npm run lint:fix`: check or fix ESLint issues in `src` and `tests`.
|
||||
- `npm run format` / `npm run format:check`: apply or verify Prettier formatting.
|
||||
- `npm run build:linux`, `npm run build:win`, `npm run build:mac`: create platform packages with `electron-builder`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
This repo uses Prettier and ESLint. Follow `.prettierrc`: 2-space indentation, single quotes, semicolons, trailing commas where valid in ES5, and a 100-character line width. Prefer `camelCase` for variables/functions, `PascalCase` for classes, and kebab-case for file names only when already established. Keep module boundaries clear: UI logic in renderer modules, OS/file-system work behind Electron IPC and adapters.
|
||||
|
||||
## Testing Guidelines
|
||||
Tests use Jest with `jest-environment-jsdom`. Add new tests under `tests/` with `*.test.js` names, mirroring the feature area when possible, for example `tests/sidebar.test.js` or `tests/print-preview.test.js`. Update or add regression tests for renderer behavior, preload APIs, and utility helpers when fixing bugs. Run `npm test` before opening a PR; use `npm run test:coverage` for larger refactors.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
Recent history follows Conventional Commit prefixes such as `feat:`, `fix:`, and `refactor:`. Keep subjects short and imperative, for example `fix: guard modal cleanup on close`. PRs should describe the user-visible change, note test coverage, link any related issue, and include screenshots or GIFs for UI changes.
|
||||
|
||||
## Security & Configuration Tips
|
||||
Do not bypass preload boundaries or introduce direct `eval`/dynamic code paths; ESLint already treats these as errors. Export and conversion features depend on external tools such as Pandoc, FFmpeg, ImageMagick, and LibreOffice, so document any new runtime dependency in `README.md` and packaging config.
|
||||
@@ -162,4 +162,4 @@ Amit Haridas (amit.wh@gmail.com)
|
||||
|
||||
## Version
|
||||
|
||||
v3.0.0
|
||||
v4.1.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MarkdownConverter - STRIDE Threat Model Analysis
|
||||
|
||||
**Version:** 4.0.0
|
||||
**Version:** 4.1.0
|
||||
**Date:** 2026-03-15
|
||||
**Methodology:** STRIDE + MITRE ATT&CK Mapping
|
||||
**Analyst:** Security Assessment Team
|
||||
|
||||
+20
-16
@@ -18,7 +18,7 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<string>} File content
|
||||
*/
|
||||
async readFile(path) {
|
||||
return await window.electronAPI.readFile(path);
|
||||
return await window.electronAPI.file.read(path);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async writeFile(path, content) {
|
||||
return await window.electronAPI.writeFile(path, content);
|
||||
return await window.electronAPI.file.write(path, content);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -37,8 +37,7 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteFile(path) {
|
||||
// TODO: Add IPC channel for delete
|
||||
throw new Error('deleteFile not implemented');
|
||||
return await window.electronAPI.file.delete(path);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -47,8 +46,7 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async ensureDir(path) {
|
||||
// TODO: Add IPC channel for ensureDir
|
||||
throw new Error('ensureDir not implemented');
|
||||
return await window.electronAPI.file.ensureDir(path);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -57,8 +55,18 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<Array<import('../types').FileInfo>>}
|
||||
*/
|
||||
async listDirectory(path) {
|
||||
// TODO: Add IPC channel for listDirectory
|
||||
throw new Error('listDirectory not implemented');
|
||||
const result = await window.electronAPI.invoke('list-directory', path);
|
||||
if (!result?.entries) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
isDir: entry.isDirectory,
|
||||
size: entry.size ?? 0,
|
||||
modified: entry.modified ?? 0,
|
||||
path: entry.path
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -67,8 +75,7 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async exists(path) {
|
||||
// TODO: Add IPC channel for exists
|
||||
throw new Error('exists not implemented');
|
||||
return await window.electronAPI.file.exists(path);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -77,8 +84,7 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isDirectory(path) {
|
||||
// TODO: Add IPC channel for isDirectory
|
||||
throw new Error('isDirectory not implemented');
|
||||
return await window.electronAPI.file.isDirectory(path);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -88,8 +94,7 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async copy(source, dest) {
|
||||
// TODO: Add IPC channel for copy
|
||||
throw new Error('copy not implemented');
|
||||
return await window.electronAPI.file.copy(source, dest);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -99,8 +104,7 @@ const electronFsAdapter = {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async move(source, dest) {
|
||||
// TODO: Add IPC channel for move
|
||||
throw new Error('move not implemented');
|
||||
return await window.electronAPI.file.move(source, dest);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1275,6 +1275,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pdf-status-message" class="info-message hidden" aria-live="polite"></div>
|
||||
|
||||
<!-- Progress indicator -->
|
||||
<div id="pdf-progress" class="batch-progress hidden">
|
||||
<div class="progress-bar">
|
||||
|
||||
+136
-4
@@ -124,6 +124,35 @@ function validatePath(filePath) {
|
||||
return { valid: true, resolved };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a path for operations where the target may not exist yet.
|
||||
* Validates string shape and blocks obviously sensitive locations.
|
||||
* @param {string} filePath
|
||||
* @returns {{ valid: boolean, resolved: string, error?: string }}
|
||||
*/
|
||||
function resolveWritablePath(filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return { valid: false, resolved: '', error: 'Invalid path' };
|
||||
}
|
||||
|
||||
let resolved;
|
||||
try {
|
||||
resolved = path.normalize(path.resolve(filePath));
|
||||
} catch (err) {
|
||||
return { valid: false, resolved: '', error: 'Invalid path format' };
|
||||
}
|
||||
|
||||
if (resolved.includes('\0')) {
|
||||
return { valid: false, resolved: '', error: 'Null byte in path' };
|
||||
}
|
||||
|
||||
if (!isPathAccessible(resolved)) {
|
||||
return { valid: false, resolved, error: 'Path is not accessible' };
|
||||
}
|
||||
|
||||
return { valid: true, resolved };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a resolved path is within allowed directories
|
||||
* For an editor app, we allow access to all user-accessible paths
|
||||
@@ -1000,7 +1029,7 @@ function showAboutDialog() {
|
||||
<body>
|
||||
<img src="${iconBase64}" class="logo" alt="MarkdownConverter">
|
||||
<h1>MarkdownConverter</h1>
|
||||
<div class="version">Version 4.0.0</div>
|
||||
<div class="version">Version 4.1.0</div>
|
||||
|
||||
<div class="company">
|
||||
<span>by</span>
|
||||
@@ -2793,9 +2822,13 @@ ipcMain.on('save-file', (event, { path, content }) => {
|
||||
currentFile = path;
|
||||
});
|
||||
|
||||
ipcMain.on('save-current-file', (event, content) => {
|
||||
if (currentFile) {
|
||||
fs.writeFileSync(currentFile, content, 'utf-8');
|
||||
ipcMain.on('save-current-file', (event, payload) => {
|
||||
const content = typeof payload === 'string' ? payload : payload?.content;
|
||||
const targetFile = typeof payload === 'string' ? currentFile : payload?.filePath || currentFile;
|
||||
|
||||
if (targetFile) {
|
||||
fs.writeFileSync(targetFile, content, 'utf-8');
|
||||
currentFile = targetFile;
|
||||
} else {
|
||||
saveAsFile();
|
||||
}
|
||||
@@ -4344,6 +4377,8 @@ ipcMain.handle('list-directory', async (event, dirPath) => {
|
||||
.map(e => ({
|
||||
name: e.name,
|
||||
isDirectory: e.isDirectory(),
|
||||
size: e.isDirectory() ? 0 : fs.statSync(path.join(validation.resolved, e.name)).size,
|
||||
modified: fs.statSync(path.join(validation.resolved, e.name)).mtimeMs,
|
||||
path: path.join(validation.resolved, e.name)
|
||||
}));
|
||||
return { path: validation.resolved, entries };
|
||||
@@ -4353,6 +4388,103 @@ ipcMain.handle('list-directory', async (event, dirPath) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('read-file', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
|
||||
return fs.readFileSync(validation.resolved, 'utf-8');
|
||||
});
|
||||
|
||||
ipcMain.handle('write-file', async (event, payload) => {
|
||||
const validation = resolveWritablePath(payload?.path);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(validation.resolved), { recursive: true });
|
||||
fs.writeFileSync(validation.resolved, payload?.content ?? '', 'utf-8');
|
||||
return { path: validation.resolved };
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-file', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
|
||||
fs.rmSync(validation.resolved, { recursive: true, force: false });
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('ensure-directory', async (event, dirPath) => {
|
||||
const validation = resolveWritablePath(dirPath);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error || 'Invalid directory path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(validation.resolved, { recursive: true });
|
||||
return validation.resolved;
|
||||
});
|
||||
|
||||
ipcMain.handle('path-exists', async (event, filePath) => {
|
||||
const validation = resolveWritablePath(filePath);
|
||||
return validation.valid ? fs.existsSync(validation.resolved) : false;
|
||||
});
|
||||
|
||||
ipcMain.handle('is-directory', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fs.statSync(validation.resolved).isDirectory();
|
||||
});
|
||||
|
||||
ipcMain.handle('copy-path', async (event, payload) => {
|
||||
const sourceValidation = validatePath(payload?.source);
|
||||
const destinationValidation = resolveWritablePath(payload?.destination);
|
||||
|
||||
if (!sourceValidation.valid || !isPathAccessible(sourceValidation.resolved)) {
|
||||
throw new Error(sourceValidation.error || 'Invalid source path');
|
||||
}
|
||||
if (!destinationValidation.valid) {
|
||||
throw new Error(destinationValidation.error || 'Invalid destination path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(destinationValidation.resolved), { recursive: true });
|
||||
fs.cpSync(sourceValidation.resolved, destinationValidation.resolved, { recursive: true });
|
||||
return { source: sourceValidation.resolved, destination: destinationValidation.resolved };
|
||||
});
|
||||
|
||||
ipcMain.handle('move-path', async (event, payload) => {
|
||||
const sourceValidation = validatePath(payload?.source);
|
||||
const destinationValidation = resolveWritablePath(payload?.destination);
|
||||
|
||||
if (!sourceValidation.valid || !isPathAccessible(sourceValidation.resolved)) {
|
||||
throw new Error(sourceValidation.error || 'Invalid source path');
|
||||
}
|
||||
if (!destinationValidation.valid) {
|
||||
throw new Error(destinationValidation.error || 'Invalid destination path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(destinationValidation.resolved), { recursive: true });
|
||||
|
||||
try {
|
||||
fs.renameSync(sourceValidation.resolved, destinationValidation.resolved);
|
||||
} catch (error) {
|
||||
if (error.code !== 'EXDEV') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
fs.cpSync(sourceValidation.resolved, destinationValidation.resolved, { recursive: true });
|
||||
fs.rmSync(sourceValidation.resolved, { recursive: true, force: false });
|
||||
}
|
||||
|
||||
return { source: sourceValidation.resolved, destination: destinationValidation.resolved };
|
||||
});
|
||||
|
||||
// Open a file by path (from explorer panel)
|
||||
ipcMain.on('open-file-path', (event, filePath) => {
|
||||
try {
|
||||
|
||||
+18
-2
@@ -9,7 +9,7 @@
|
||||
* - All IPC channels are explicitly whitelisted
|
||||
* - Prevents XSS from escalating to full system access
|
||||
*
|
||||
* @version 4.0.0
|
||||
* @version 4.1.0
|
||||
*/
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
@@ -101,6 +101,14 @@ const ALLOWED_SEND_CHANNELS = [
|
||||
|
||||
// File Explorer
|
||||
'list-directory',
|
||||
'read-file',
|
||||
'write-file',
|
||||
'delete-file',
|
||||
'ensure-directory',
|
||||
'path-exists',
|
||||
'is-directory',
|
||||
'copy-path',
|
||||
'move-path',
|
||||
|
||||
// Git
|
||||
'git-status',
|
||||
@@ -321,7 +329,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
setCurrent: (filePath) => ipcRenderer.send('set-current-file', filePath),
|
||||
saveRecent: (recentFiles) => ipcRenderer.send('save-recent-files', recentFiles),
|
||||
clearRecent: () => ipcRenderer.send('clear-recent-files'),
|
||||
rendererReady: () => ipcRenderer.send('renderer-ready')
|
||||
rendererReady: () => ipcRenderer.send('renderer-ready'),
|
||||
read: (filePath) => ipcRenderer.invoke('read-file', filePath),
|
||||
write: (filePath, content) => ipcRenderer.invoke('write-file', { path: filePath, content }),
|
||||
delete: (filePath) => ipcRenderer.invoke('delete-file', filePath),
|
||||
ensureDir: (dirPath) => ipcRenderer.invoke('ensure-directory', dirPath),
|
||||
exists: (filePath) => ipcRenderer.invoke('path-exists', filePath),
|
||||
isDirectory: (filePath) => ipcRenderer.invoke('is-directory', filePath),
|
||||
copy: (source, destination) => ipcRenderer.invoke('copy-path', { source, destination }),
|
||||
move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination })
|
||||
},
|
||||
|
||||
// Theme Operations
|
||||
|
||||
+142
-38
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* MarkdownConverter Renderer Process
|
||||
* @version 3.0.0
|
||||
* @version 4.1.0
|
||||
*/
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
@@ -31,6 +31,45 @@ function getZenMode() { if (!_ZenMode) _ZenMode = require('./zen-mode').ZenMode;
|
||||
let _showAnalyticsModal;
|
||||
function getShowAnalyticsModal() { if (!_showAnalyticsModal) _showAnalyticsModal = require('./analytics/analytics-panel').showAnalyticsModal; return _showAnalyticsModal; }
|
||||
|
||||
function ensureToastContainer() {
|
||||
let container = document.getElementById('app-toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'app-toast-container';
|
||||
container.className = 'app-toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
function notifyUser(message, type = 'info', options = {}) {
|
||||
if (!message) return;
|
||||
|
||||
const { duration = 3500 } = options;
|
||||
const container = ensureToastContainer();
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `app-toast app-toast-${type}`;
|
||||
toast.setAttribute('role', type === 'warning' ? 'alert' : 'status');
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('visible');
|
||||
});
|
||||
|
||||
const dismiss = () => {
|
||||
toast.classList.remove('visible');
|
||||
setTimeout(() => toast.remove(), 180);
|
||||
};
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(dismiss, duration);
|
||||
}
|
||||
|
||||
toast.addEventListener('click', dismiss);
|
||||
return toast;
|
||||
}
|
||||
|
||||
// Configure marked with highlight extension
|
||||
marked.use(markedHighlight({
|
||||
highlight: function(code, lang) {
|
||||
@@ -357,7 +396,7 @@ class TabManager {
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
document.getElementById('status-text').textContent = 'Error loading PDF';
|
||||
alert('Error loading PDF: ' + error.message);
|
||||
notifyUser(`Error loading PDF: ${error.message}`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,9 +496,7 @@ class TabManager {
|
||||
|
||||
// Notify main process about current file for exports
|
||||
const tab = this.tabs.get(tabId);
|
||||
if (tab?.filePath) {
|
||||
ipcRenderer.send('set-current-file', tab.filePath);
|
||||
}
|
||||
ipcRenderer.send('set-current-file', tab?.filePath || null);
|
||||
|
||||
// Refresh outline panel for new tab content
|
||||
if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline();
|
||||
@@ -1729,7 +1766,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Auto-save logic for all tabs
|
||||
tabManager.tabs.forEach(tab => {
|
||||
if (tab.isDirty && tab.filePath) {
|
||||
ipcRenderer.send('save-current-file', tab.content);
|
||||
ipcRenderer.send('save-current-file', { filePath: tab.filePath, content: tab.content });
|
||||
}
|
||||
});
|
||||
}, 30000);
|
||||
@@ -1753,7 +1790,7 @@ ipcRenderer.on('file-save', () => {
|
||||
const currentContent = tabManager.getCurrentContent();
|
||||
const currentFilePath = tabManager.getCurrentFilePath();
|
||||
// send to main process which will save or trigger save-as dialog
|
||||
ipcRenderer.send('save-current-file', currentContent);
|
||||
ipcRenderer.send('save-current-file', { filePath: currentFilePath, content: currentContent });
|
||||
});
|
||||
|
||||
ipcRenderer.on('get-content-for-save', (event, filePath) => {
|
||||
@@ -1870,7 +1907,7 @@ function openPrintPreviewDialog() {
|
||||
const previewContent = document.getElementById(`preview-${activeTabId}`);
|
||||
|
||||
if (!previewContent || !previewContent.innerHTML.trim()) {
|
||||
alert('Nothing to print. Please create or open a document and ensure the preview is visible.');
|
||||
notifyUser('Nothing to print. Create or open a document and keep the preview visible.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2112,7 +2149,7 @@ function saveCurrentProfile() {
|
||||
// Select the newly created profile
|
||||
document.getElementById('export-profile-select').value = profileName;
|
||||
|
||||
alert(`Profile "${profileName}" saved successfully!`);
|
||||
notifyUser(`Profile "${profileName}" saved successfully.`, 'success');
|
||||
}
|
||||
|
||||
function loadProfile(profileName) {
|
||||
@@ -2152,7 +2189,7 @@ function deleteSelectedProfile() {
|
||||
const profileName = select.value;
|
||||
|
||||
if (!profileName) {
|
||||
alert('Please select a profile to delete.');
|
||||
notifyUser('Select a profile to delete.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2161,7 +2198,7 @@ function deleteSelectedProfile() {
|
||||
saveExportProfiles();
|
||||
populateProfileDropdown();
|
||||
select.value = '';
|
||||
alert(`Profile "${profileName}" deleted successfully!`);
|
||||
notifyUser(`Profile "${profileName}" deleted successfully.`, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2855,7 +2892,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const includeSubfolders = document.getElementById('converter-batch-subfolders').checked;
|
||||
|
||||
if (!inputFolder || !outputFolder) {
|
||||
alert('Please select both input and output folders for batch conversion');
|
||||
document.getElementById('converter-progress').classList.remove('hidden');
|
||||
document.getElementById('converter-status').textContent = 'Select both input and output folders for batch conversion.';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2877,7 +2915,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const filePath = converterFilePath;
|
||||
|
||||
if (!filePath) {
|
||||
alert('Please select a file to convert');
|
||||
document.getElementById('converter-progress').classList.remove('hidden');
|
||||
document.getElementById('converter-status').textContent = 'Select a file to convert.';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3025,7 +3064,13 @@ function showPDFEditorDialog(operation, openedFilePath = null) {
|
||||
|
||||
function hidePDFEditorDialog() {
|
||||
window.modals.pdfEditorModal.close();
|
||||
clearPDFStatus();
|
||||
document.getElementById('pdf-progress').classList.add('hidden');
|
||||
document.getElementById('pdf-progress-text').textContent = 'Processing...';
|
||||
const progressFill = document.getElementById('pdf-progress-fill');
|
||||
if (progressFill) {
|
||||
progressFill.style.width = '0%';
|
||||
}
|
||||
currentPDFOperation = null;
|
||||
}
|
||||
|
||||
@@ -3226,7 +3271,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadCurrentOrder.addEventListener('click', () => {
|
||||
const inputPath = document.getElementById('reorder-input-path').value;
|
||||
if (!inputPath) {
|
||||
alert('Please select a PDF file first');
|
||||
showPDFValidationMessage('Select a PDF file before loading the current page order.', '#reorder-input-path');
|
||||
return;
|
||||
}
|
||||
// Request page count from main process
|
||||
@@ -3243,7 +3288,7 @@ ipcRenderer.on('pdf-folder-selected', (event, { inputId, path }) => {
|
||||
// Handle PDF page count response
|
||||
ipcRenderer.on('pdf-page-count', (event, { count, error }) => {
|
||||
if (error) {
|
||||
alert('Error reading PDF: ' + error);
|
||||
showPDFStatus(`Error reading PDF: ${error}`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3253,6 +3298,39 @@ ipcRenderer.on('pdf-page-count', (event, { count, error }) => {
|
||||
document.getElementById('reorder-pages').value = currentOrder;
|
||||
});
|
||||
|
||||
function getPDFStatusElement() {
|
||||
return document.getElementById('pdf-status-message');
|
||||
}
|
||||
|
||||
function showPDFStatus(message, type = 'info') {
|
||||
const status = getPDFStatusElement();
|
||||
if (!status) return;
|
||||
|
||||
status.textContent = message;
|
||||
status.classList.remove('hidden', 'info-message', 'warning-message', 'success-message');
|
||||
status.classList.add(`${type}-message`);
|
||||
}
|
||||
|
||||
function clearPDFStatus() {
|
||||
const status = getPDFStatusElement();
|
||||
if (!status) return;
|
||||
|
||||
status.textContent = '';
|
||||
status.classList.remove('info-message', 'warning-message', 'success-message');
|
||||
status.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showPDFValidationMessage(message, focusSelector = null) {
|
||||
showPDFStatus(message, 'warning');
|
||||
|
||||
if (focusSelector) {
|
||||
const field = document.querySelector(focusSelector);
|
||||
if (field && typeof field.focus === 'function') {
|
||||
field.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process PDF Operation
|
||||
function processPDFOperation() {
|
||||
const operation = currentPDFOperation;
|
||||
@@ -3262,13 +3340,13 @@ function processPDFOperation() {
|
||||
switch (operation) {
|
||||
case 'merge':
|
||||
if (mergeFilePaths.length < 2) {
|
||||
alert('Please add at least 2 PDF files to merge');
|
||||
showPDFValidationMessage('Add at least 2 PDF files to merge first.', '#add-merge-file');
|
||||
return;
|
||||
}
|
||||
operationData.inputFiles = mergeFilePaths;
|
||||
operationData.outputPath = document.getElementById('merge-output-path').value.trim();
|
||||
if (!operationData.outputPath) {
|
||||
alert('Please select an output file path');
|
||||
showPDFValidationMessage('Select an output file path.', '#merge-output-path');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -3279,7 +3357,7 @@ function processPDFOperation() {
|
||||
operationData.splitMode = document.getElementById('split-mode').value;
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputFolder) {
|
||||
alert('Please select input file and output folder');
|
||||
showPDFValidationMessage('Select both an input PDF and an output folder.', '#split-input-path');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3302,7 +3380,10 @@ function processPDFOperation() {
|
||||
operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked;
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath) {
|
||||
alert('Please select input file' + (operationData.overwrite ? '' : ' and output file paths'));
|
||||
showPDFValidationMessage(
|
||||
'Select an input PDF' + (operationData.overwrite ? '.' : ' and an output file path.'),
|
||||
'#compress-input-path'
|
||||
);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -3315,7 +3396,10 @@ function processPDFOperation() {
|
||||
operationData.angle = parseInt(document.getElementById('rotate-angle').value);
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath) {
|
||||
alert('Please select input file' + (operationData.overwrite ? '' : ' and output file'));
|
||||
showPDFValidationMessage(
|
||||
'Select an input PDF' + (operationData.overwrite ? '.' : ' and an output file path.'),
|
||||
'#rotate-input-path'
|
||||
);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -3327,7 +3411,7 @@ function processPDFOperation() {
|
||||
operationData.pages = document.getElementById('delete-pages').value.trim();
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) {
|
||||
alert('Please fill in all required fields');
|
||||
showPDFValidationMessage('Fill in the input file, output path, and pages to delete.', '#delete-input-path');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -3339,7 +3423,7 @@ function processPDFOperation() {
|
||||
operationData.newOrder = document.getElementById('reorder-pages').value.trim();
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) {
|
||||
alert('Please fill in all required fields');
|
||||
showPDFValidationMessage('Fill in the input file, output path, and new page order.', '#reorder-input-path');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -3360,7 +3444,7 @@ function processPDFOperation() {
|
||||
}
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.text) {
|
||||
alert('Please fill in all required fields');
|
||||
showPDFValidationMessage('Fill in the input file, output path, and watermark text.', '#watermark-input-path');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -3385,7 +3469,7 @@ function processPDFOperation() {
|
||||
};
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) {
|
||||
alert('Please select file and enter a user password');
|
||||
showPDFValidationMessage('Select a file, output path, and user password.', '#encrypt-input-path');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -3397,7 +3481,7 @@ function processPDFOperation() {
|
||||
operationData.password = document.getElementById('decrypt-password').value;
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.password) {
|
||||
alert('Please fill in all required fields');
|
||||
showPDFValidationMessage('Fill in the input file, output path, and password.', '#decrypt-input-path');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -3421,21 +3505,26 @@ function processPDFOperation() {
|
||||
};
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.ownerPassword) {
|
||||
alert('Please fill in all required fields');
|
||||
showPDFValidationMessage('Fill in the input file, output path, and owner password.', '#permissions-input-path');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
clearPDFStatus();
|
||||
// Show progress
|
||||
document.getElementById('pdf-progress').classList.remove('hidden');
|
||||
document.getElementById('pdf-progress-text').textContent = 'Processing PDF...';
|
||||
const progressFill = document.getElementById('pdf-progress-fill');
|
||||
if (progressFill) {
|
||||
progressFill.style.width = '10%';
|
||||
}
|
||||
|
||||
// Send to main process
|
||||
ipcRenderer.send('process-pdf-operation', operationData);
|
||||
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
showPDFStatus(`Error: ${error.message}`, 'warning');
|
||||
console.error('PDF operation error:', error);
|
||||
}
|
||||
}
|
||||
@@ -3445,10 +3534,14 @@ ipcRenderer.on('pdf-operation-complete', (event, { success, error, message }) =>
|
||||
document.getElementById('pdf-progress').classList.add('hidden');
|
||||
|
||||
if (success) {
|
||||
alert(message || 'PDF operation completed successfully!');
|
||||
hidePDFEditorDialog();
|
||||
showPDFStatus(message || 'PDF operation completed successfully.', 'success');
|
||||
setTimeout(() => {
|
||||
if (window.modals?.pdfEditorModal?.isOpen()) {
|
||||
hidePDFEditorDialog();
|
||||
}
|
||||
}, 800);
|
||||
} else {
|
||||
alert('Error: ' + (error || 'PDF operation failed'));
|
||||
showPDFStatus(`Error: ${error || 'PDF operation failed'}`, 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3780,13 +3873,13 @@ function insertGeneratedTable() {
|
||||
const table = document.getElementById('table-preview').textContent;
|
||||
|
||||
if (!table) {
|
||||
alert('Please generate a table preview first');
|
||||
notifyUser('Generate a table preview first.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert table using CodeMirror
|
||||
if (!tabManager) {
|
||||
alert('No active editor found');
|
||||
notifyUser('No active editor found.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4254,13 +4347,13 @@ function insertASCIIArt() {
|
||||
const asciiArt = document.getElementById('ascii-preview').textContent;
|
||||
|
||||
if (!asciiArt || asciiArt === 'Select a template from the buttons above') {
|
||||
alert('Please generate ASCII art first');
|
||||
notifyUser('Generate ASCII art first.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert using CodeMirror
|
||||
if (!tabManager) {
|
||||
alert('No active editor found');
|
||||
notifyUser('No active editor found.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4683,14 +4776,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// ============================================
|
||||
|
||||
ipcRenderer.on('show-image-tool', (event, tool) => {
|
||||
alert(`Image ${tool} tool requires ImageMagick to be installed.\n\nPlease install ImageMagick from: https://imagemagick.org/\n\nThis feature will be available in a future update with built-in support.`);
|
||||
notifyUser(
|
||||
`Image ${tool} requires ImageMagick. Install it from imagemagick.org to enable this tool.`,
|
||||
'info',
|
||||
{ duration: 5000 }
|
||||
);
|
||||
});
|
||||
|
||||
ipcRenderer.on('show-audio-tool', (event, tool) => {
|
||||
alert(`Audio ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`);
|
||||
notifyUser(
|
||||
`Audio ${tool} requires FFmpeg. Install it from ffmpeg.org to enable this tool.`,
|
||||
'info',
|
||||
{ duration: 5000 }
|
||||
);
|
||||
});
|
||||
|
||||
ipcRenderer.on('show-video-tool', (event, tool) => {
|
||||
alert(`Video ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`);
|
||||
notifyUser(
|
||||
`Video ${tool} requires FFmpeg. Install it from ffmpeg.org to enable this tool.`,
|
||||
'info',
|
||||
{ duration: 5000 }
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* ConcreteInfo Theme for MarkdownConverter
|
||||
* Based on logo palette: #464646, #9a9696, #e5461f, #e3e3e3, #0d0b09
|
||||
* Version: 3.0.0
|
||||
* Version: 4.1.0
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
|
||||
@@ -522,6 +522,54 @@ body.dark .pdf-tab-container .pdf-controls {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-toast-container {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
z-index: var(--z-toast, 500);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-toast {
|
||||
min-width: 260px;
|
||||
max-width: 420px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
|
||||
color: #111827;
|
||||
background: #ffffff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.app-toast.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.app-toast-info {
|
||||
border-left-color: #2563eb;
|
||||
}
|
||||
|
||||
.app-toast-success {
|
||||
border-left-color: #059669;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.app-toast-warning {
|
||||
border-left-color: #d97706;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
/* Dark theme adjustments for states */
|
||||
body[class*="dark"] .preview-error-message {
|
||||
color: #9ca3af;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Modal System Styles
|
||||
* Unified modal components with glassmorphism backdrop
|
||||
* @version 4.0.0
|
||||
* @version 4.1.0
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* ModalManager - Unified modal system with accessibility support
|
||||
* @version 4.0.0
|
||||
* @version 4.1.0
|
||||
*/
|
||||
class ModalManager {
|
||||
#modal;
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ function createWelcomeContent(recentFiles = []) {
|
||||
<div class="welcome-container">
|
||||
<div class="welcome-hero">
|
||||
<h1 class="welcome-title">MarkdownConverter</h1>
|
||||
<p class="welcome-version">Version 4.0.0</p>
|
||||
<p class="welcome-version">Version 4.1.0</p>
|
||||
<p class="welcome-subtitle">Professional Markdown Editor & Universal Document Converter</p>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ function createWelcomeContent(recentFiles = []) {
|
||||
</div>
|
||||
|
||||
<div class="welcome-section">
|
||||
<h2>What's New in v4.0.0</h2>
|
||||
<h2>What's New in v4.x</h2>
|
||||
<ul class="welcome-features">
|
||||
<li><strong>CodeMirror Editor</strong> — Syntax highlighting, code folding, multiple cursors</li>
|
||||
<li><strong>Sidebar Panels</strong> — File Explorer, Git, Snippets, Templates</li>
|
||||
|
||||
@@ -54,6 +54,14 @@ describe('Preload Security', () => {
|
||||
'save-pasted-image',
|
||||
'load-template',
|
||||
'list-directory',
|
||||
'read-file',
|
||||
'write-file',
|
||||
'delete-file',
|
||||
'ensure-directory',
|
||||
'path-exists',
|
||||
'is-directory',
|
||||
'copy-path',
|
||||
'move-path',
|
||||
'open-file-path',
|
||||
'git-status',
|
||||
'git-stage',
|
||||
|
||||
+9
-1
@@ -17,7 +17,15 @@ global.window.electronAPI = {
|
||||
setCurrent: jest.fn(),
|
||||
saveRecent: jest.fn(),
|
||||
clearRecent: jest.fn(),
|
||||
rendererReady: jest.fn()
|
||||
rendererReady: jest.fn(),
|
||||
read: jest.fn(() => Promise.resolve('')),
|
||||
write: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
ensureDir: jest.fn(() => Promise.resolve()),
|
||||
exists: jest.fn(() => Promise.resolve(false)),
|
||||
isDirectory: jest.fn(() => Promise.resolve(false)),
|
||||
copy: jest.fn(() => Promise.resolve()),
|
||||
move: jest.fn(() => Promise.resolve())
|
||||
},
|
||||
theme: {
|
||||
get: jest.fn()
|
||||
|
||||
Reference in New Issue
Block a user