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
|
## Version
|
||||||
|
|
||||||
v3.0.0
|
v4.1.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MarkdownConverter - STRIDE Threat Model Analysis
|
# MarkdownConverter - STRIDE Threat Model Analysis
|
||||||
|
|
||||||
**Version:** 4.0.0
|
**Version:** 4.1.0
|
||||||
**Date:** 2026-03-15
|
**Date:** 2026-03-15
|
||||||
**Methodology:** STRIDE + MITRE ATT&CK Mapping
|
**Methodology:** STRIDE + MITRE ATT&CK Mapping
|
||||||
**Analyst:** Security Assessment Team
|
**Analyst:** Security Assessment Team
|
||||||
|
|||||||
+20
-16
@@ -18,7 +18,7 @@ const electronFsAdapter = {
|
|||||||
* @returns {Promise<string>} File content
|
* @returns {Promise<string>} File content
|
||||||
*/
|
*/
|
||||||
async readFile(path) {
|
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>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async writeFile(path, content) {
|
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>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async deleteFile(path) {
|
async deleteFile(path) {
|
||||||
// TODO: Add IPC channel for delete
|
return await window.electronAPI.file.delete(path);
|
||||||
throw new Error('deleteFile not implemented');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,8 +46,7 @@ const electronFsAdapter = {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async ensureDir(path) {
|
async ensureDir(path) {
|
||||||
// TODO: Add IPC channel for ensureDir
|
return await window.electronAPI.file.ensureDir(path);
|
||||||
throw new Error('ensureDir not implemented');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,8 +55,18 @@ const electronFsAdapter = {
|
|||||||
* @returns {Promise<Array<import('../types').FileInfo>>}
|
* @returns {Promise<Array<import('../types').FileInfo>>}
|
||||||
*/
|
*/
|
||||||
async listDirectory(path) {
|
async listDirectory(path) {
|
||||||
// TODO: Add IPC channel for listDirectory
|
const result = await window.electronAPI.invoke('list-directory', path);
|
||||||
throw new Error('listDirectory not implemented');
|
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>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async exists(path) {
|
async exists(path) {
|
||||||
// TODO: Add IPC channel for exists
|
return await window.electronAPI.file.exists(path);
|
||||||
throw new Error('exists not implemented');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,8 +84,7 @@ const electronFsAdapter = {
|
|||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async isDirectory(path) {
|
async isDirectory(path) {
|
||||||
// TODO: Add IPC channel for isDirectory
|
return await window.electronAPI.file.isDirectory(path);
|
||||||
throw new Error('isDirectory not implemented');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,8 +94,7 @@ const electronFsAdapter = {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async copy(source, dest) {
|
async copy(source, dest) {
|
||||||
// TODO: Add IPC channel for copy
|
return await window.electronAPI.file.copy(source, dest);
|
||||||
throw new Error('copy not implemented');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,8 +104,7 @@ const electronFsAdapter = {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async move(source, dest) {
|
async move(source, dest) {
|
||||||
// TODO: Add IPC channel for move
|
return await window.electronAPI.file.move(source, dest);
|
||||||
throw new Error('move not implemented');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1275,6 +1275,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="pdf-status-message" class="info-message hidden" aria-live="polite"></div>
|
||||||
|
|
||||||
<!-- Progress indicator -->
|
<!-- Progress indicator -->
|
||||||
<div id="pdf-progress" class="batch-progress hidden">
|
<div id="pdf-progress" class="batch-progress hidden">
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
|
|||||||
+136
-4
@@ -124,6 +124,35 @@ function validatePath(filePath) {
|
|||||||
return { valid: true, resolved };
|
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
|
* Checks if a resolved path is within allowed directories
|
||||||
* For an editor app, we allow access to all user-accessible paths
|
* For an editor app, we allow access to all user-accessible paths
|
||||||
@@ -1000,7 +1029,7 @@ function showAboutDialog() {
|
|||||||
<body>
|
<body>
|
||||||
<img src="${iconBase64}" class="logo" alt="MarkdownConverter">
|
<img src="${iconBase64}" class="logo" alt="MarkdownConverter">
|
||||||
<h1>MarkdownConverter</h1>
|
<h1>MarkdownConverter</h1>
|
||||||
<div class="version">Version 4.0.0</div>
|
<div class="version">Version 4.1.0</div>
|
||||||
|
|
||||||
<div class="company">
|
<div class="company">
|
||||||
<span>by</span>
|
<span>by</span>
|
||||||
@@ -2793,9 +2822,13 @@ ipcMain.on('save-file', (event, { path, content }) => {
|
|||||||
currentFile = path;
|
currentFile = path;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('save-current-file', (event, content) => {
|
ipcMain.on('save-current-file', (event, payload) => {
|
||||||
if (currentFile) {
|
const content = typeof payload === 'string' ? payload : payload?.content;
|
||||||
fs.writeFileSync(currentFile, content, 'utf-8');
|
const targetFile = typeof payload === 'string' ? currentFile : payload?.filePath || currentFile;
|
||||||
|
|
||||||
|
if (targetFile) {
|
||||||
|
fs.writeFileSync(targetFile, content, 'utf-8');
|
||||||
|
currentFile = targetFile;
|
||||||
} else {
|
} else {
|
||||||
saveAsFile();
|
saveAsFile();
|
||||||
}
|
}
|
||||||
@@ -4344,6 +4377,8 @@ ipcMain.handle('list-directory', async (event, dirPath) => {
|
|||||||
.map(e => ({
|
.map(e => ({
|
||||||
name: e.name,
|
name: e.name,
|
||||||
isDirectory: e.isDirectory(),
|
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)
|
path: path.join(validation.resolved, e.name)
|
||||||
}));
|
}));
|
||||||
return { path: validation.resolved, entries };
|
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)
|
// Open a file by path (from explorer panel)
|
||||||
ipcMain.on('open-file-path', (event, filePath) => {
|
ipcMain.on('open-file-path', (event, filePath) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+18
-2
@@ -9,7 +9,7 @@
|
|||||||
* - All IPC channels are explicitly whitelisted
|
* - All IPC channels are explicitly whitelisted
|
||||||
* - Prevents XSS from escalating to full system access
|
* - Prevents XSS from escalating to full system access
|
||||||
*
|
*
|
||||||
* @version 4.0.0
|
* @version 4.1.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
@@ -101,6 +101,14 @@ const ALLOWED_SEND_CHANNELS = [
|
|||||||
|
|
||||||
// File Explorer
|
// File Explorer
|
||||||
'list-directory',
|
'list-directory',
|
||||||
|
'read-file',
|
||||||
|
'write-file',
|
||||||
|
'delete-file',
|
||||||
|
'ensure-directory',
|
||||||
|
'path-exists',
|
||||||
|
'is-directory',
|
||||||
|
'copy-path',
|
||||||
|
'move-path',
|
||||||
|
|
||||||
// Git
|
// Git
|
||||||
'git-status',
|
'git-status',
|
||||||
@@ -321,7 +329,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
setCurrent: (filePath) => ipcRenderer.send('set-current-file', filePath),
|
setCurrent: (filePath) => ipcRenderer.send('set-current-file', filePath),
|
||||||
saveRecent: (recentFiles) => ipcRenderer.send('save-recent-files', recentFiles),
|
saveRecent: (recentFiles) => ipcRenderer.send('save-recent-files', recentFiles),
|
||||||
clearRecent: () => ipcRenderer.send('clear-recent-files'),
|
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
|
// Theme Operations
|
||||||
|
|||||||
+141
-37
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* MarkdownConverter Renderer Process
|
* MarkdownConverter Renderer Process
|
||||||
* @version 3.0.0
|
* @version 4.1.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { ipcRenderer } = require('electron');
|
const { ipcRenderer } = require('electron');
|
||||||
@@ -31,6 +31,45 @@ function getZenMode() { if (!_ZenMode) _ZenMode = require('./zen-mode').ZenMode;
|
|||||||
let _showAnalyticsModal;
|
let _showAnalyticsModal;
|
||||||
function getShowAnalyticsModal() { if (!_showAnalyticsModal) _showAnalyticsModal = require('./analytics/analytics-panel').showAnalyticsModal; return _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
|
// Configure marked with highlight extension
|
||||||
marked.use(markedHighlight({
|
marked.use(markedHighlight({
|
||||||
highlight: function(code, lang) {
|
highlight: function(code, lang) {
|
||||||
@@ -357,7 +396,7 @@ class TabManager {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF:', error);
|
||||||
document.getElementById('status-text').textContent = 'Error loading PDF';
|
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
|
// Notify main process about current file for exports
|
||||||
const tab = this.tabs.get(tabId);
|
const tab = this.tabs.get(tabId);
|
||||||
if (tab?.filePath) {
|
ipcRenderer.send('set-current-file', tab?.filePath || null);
|
||||||
ipcRenderer.send('set-current-file', tab.filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh outline panel for new tab content
|
// Refresh outline panel for new tab content
|
||||||
if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline();
|
if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline();
|
||||||
@@ -1729,7 +1766,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Auto-save logic for all tabs
|
// Auto-save logic for all tabs
|
||||||
tabManager.tabs.forEach(tab => {
|
tabManager.tabs.forEach(tab => {
|
||||||
if (tab.isDirty && tab.filePath) {
|
if (tab.isDirty && tab.filePath) {
|
||||||
ipcRenderer.send('save-current-file', tab.content);
|
ipcRenderer.send('save-current-file', { filePath: tab.filePath, content: tab.content });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 30000);
|
}, 30000);
|
||||||
@@ -1753,7 +1790,7 @@ ipcRenderer.on('file-save', () => {
|
|||||||
const currentContent = tabManager.getCurrentContent();
|
const currentContent = tabManager.getCurrentContent();
|
||||||
const currentFilePath = tabManager.getCurrentFilePath();
|
const currentFilePath = tabManager.getCurrentFilePath();
|
||||||
// send to main process which will save or trigger save-as dialog
|
// 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) => {
|
ipcRenderer.on('get-content-for-save', (event, filePath) => {
|
||||||
@@ -1870,7 +1907,7 @@ function openPrintPreviewDialog() {
|
|||||||
const previewContent = document.getElementById(`preview-${activeTabId}`);
|
const previewContent = document.getElementById(`preview-${activeTabId}`);
|
||||||
|
|
||||||
if (!previewContent || !previewContent.innerHTML.trim()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2112,7 +2149,7 @@ function saveCurrentProfile() {
|
|||||||
// Select the newly created profile
|
// Select the newly created profile
|
||||||
document.getElementById('export-profile-select').value = profileName;
|
document.getElementById('export-profile-select').value = profileName;
|
||||||
|
|
||||||
alert(`Profile "${profileName}" saved successfully!`);
|
notifyUser(`Profile "${profileName}" saved successfully.`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadProfile(profileName) {
|
function loadProfile(profileName) {
|
||||||
@@ -2152,7 +2189,7 @@ function deleteSelectedProfile() {
|
|||||||
const profileName = select.value;
|
const profileName = select.value;
|
||||||
|
|
||||||
if (!profileName) {
|
if (!profileName) {
|
||||||
alert('Please select a profile to delete.');
|
notifyUser('Select a profile to delete.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2161,7 +2198,7 @@ function deleteSelectedProfile() {
|
|||||||
saveExportProfiles();
|
saveExportProfiles();
|
||||||
populateProfileDropdown();
|
populateProfileDropdown();
|
||||||
select.value = '';
|
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;
|
const includeSubfolders = document.getElementById('converter-batch-subfolders').checked;
|
||||||
|
|
||||||
if (!inputFolder || !outputFolder) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2877,7 +2915,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const filePath = converterFilePath;
|
const filePath = converterFilePath;
|
||||||
|
|
||||||
if (!filePath) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3025,7 +3064,13 @@ function showPDFEditorDialog(operation, openedFilePath = null) {
|
|||||||
|
|
||||||
function hidePDFEditorDialog() {
|
function hidePDFEditorDialog() {
|
||||||
window.modals.pdfEditorModal.close();
|
window.modals.pdfEditorModal.close();
|
||||||
|
clearPDFStatus();
|
||||||
document.getElementById('pdf-progress').classList.add('hidden');
|
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;
|
currentPDFOperation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3226,7 +3271,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
loadCurrentOrder.addEventListener('click', () => {
|
loadCurrentOrder.addEventListener('click', () => {
|
||||||
const inputPath = document.getElementById('reorder-input-path').value;
|
const inputPath = document.getElementById('reorder-input-path').value;
|
||||||
if (!inputPath) {
|
if (!inputPath) {
|
||||||
alert('Please select a PDF file first');
|
showPDFValidationMessage('Select a PDF file before loading the current page order.', '#reorder-input-path');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Request page count from main process
|
// Request page count from main process
|
||||||
@@ -3243,7 +3288,7 @@ ipcRenderer.on('pdf-folder-selected', (event, { inputId, path }) => {
|
|||||||
// Handle PDF page count response
|
// Handle PDF page count response
|
||||||
ipcRenderer.on('pdf-page-count', (event, { count, error }) => {
|
ipcRenderer.on('pdf-page-count', (event, { count, error }) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
alert('Error reading PDF: ' + error);
|
showPDFStatus(`Error reading PDF: ${error}`, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3253,6 +3298,39 @@ ipcRenderer.on('pdf-page-count', (event, { count, error }) => {
|
|||||||
document.getElementById('reorder-pages').value = currentOrder;
|
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
|
// Process PDF Operation
|
||||||
function processPDFOperation() {
|
function processPDFOperation() {
|
||||||
const operation = currentPDFOperation;
|
const operation = currentPDFOperation;
|
||||||
@@ -3262,13 +3340,13 @@ function processPDFOperation() {
|
|||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'merge':
|
case 'merge':
|
||||||
if (mergeFilePaths.length < 2) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
operationData.inputFiles = mergeFilePaths;
|
operationData.inputFiles = mergeFilePaths;
|
||||||
operationData.outputPath = document.getElementById('merge-output-path').value.trim();
|
operationData.outputPath = document.getElementById('merge-output-path').value.trim();
|
||||||
if (!operationData.outputPath) {
|
if (!operationData.outputPath) {
|
||||||
alert('Please select an output file path');
|
showPDFValidationMessage('Select an output file path.', '#merge-output-path');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3279,7 +3357,7 @@ function processPDFOperation() {
|
|||||||
operationData.splitMode = document.getElementById('split-mode').value;
|
operationData.splitMode = document.getElementById('split-mode').value;
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputFolder) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3302,7 +3380,10 @@ function processPDFOperation() {
|
|||||||
operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked;
|
operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked;
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputPath) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3315,7 +3396,10 @@ function processPDFOperation() {
|
|||||||
operationData.angle = parseInt(document.getElementById('rotate-angle').value);
|
operationData.angle = parseInt(document.getElementById('rotate-angle').value);
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputPath) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3327,7 +3411,7 @@ function processPDFOperation() {
|
|||||||
operationData.pages = document.getElementById('delete-pages').value.trim();
|
operationData.pages = document.getElementById('delete-pages').value.trim();
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3339,7 +3423,7 @@ function processPDFOperation() {
|
|||||||
operationData.newOrder = document.getElementById('reorder-pages').value.trim();
|
operationData.newOrder = document.getElementById('reorder-pages').value.trim();
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3360,7 +3444,7 @@ function processPDFOperation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.text) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3385,7 +3469,7 @@ function processPDFOperation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3397,7 +3481,7 @@ function processPDFOperation() {
|
|||||||
operationData.password = document.getElementById('decrypt-password').value;
|
operationData.password = document.getElementById('decrypt-password').value;
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.password) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3421,21 +3505,26 @@ function processPDFOperation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.ownerPassword) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearPDFStatus();
|
||||||
// Show progress
|
// Show progress
|
||||||
document.getElementById('pdf-progress').classList.remove('hidden');
|
document.getElementById('pdf-progress').classList.remove('hidden');
|
||||||
document.getElementById('pdf-progress-text').textContent = 'Processing PDF...';
|
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
|
// Send to main process
|
||||||
ipcRenderer.send('process-pdf-operation', operationData);
|
ipcRenderer.send('process-pdf-operation', operationData);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error: ' + error.message);
|
showPDFStatus(`Error: ${error.message}`, 'warning');
|
||||||
console.error('PDF operation error:', error);
|
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');
|
document.getElementById('pdf-progress').classList.add('hidden');
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
alert(message || 'PDF operation completed successfully!');
|
showPDFStatus(message || 'PDF operation completed successfully.', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.modals?.pdfEditorModal?.isOpen()) {
|
||||||
hidePDFEditorDialog();
|
hidePDFEditorDialog();
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
} else {
|
} 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;
|
const table = document.getElementById('table-preview').textContent;
|
||||||
|
|
||||||
if (!table) {
|
if (!table) {
|
||||||
alert('Please generate a table preview first');
|
notifyUser('Generate a table preview first.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert table using CodeMirror
|
// Insert table using CodeMirror
|
||||||
if (!tabManager) {
|
if (!tabManager) {
|
||||||
alert('No active editor found');
|
notifyUser('No active editor found.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4254,13 +4347,13 @@ function insertASCIIArt() {
|
|||||||
const asciiArt = document.getElementById('ascii-preview').textContent;
|
const asciiArt = document.getElementById('ascii-preview').textContent;
|
||||||
|
|
||||||
if (!asciiArt || asciiArt === 'Select a template from the buttons above') {
|
if (!asciiArt || asciiArt === 'Select a template from the buttons above') {
|
||||||
alert('Please generate ASCII art first');
|
notifyUser('Generate ASCII art first.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert using CodeMirror
|
// Insert using CodeMirror
|
||||||
if (!tabManager) {
|
if (!tabManager) {
|
||||||
alert('No active editor found');
|
notifyUser('No active editor found.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4683,14 +4776,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
ipcRenderer.on('show-image-tool', (event, tool) => {
|
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) => {
|
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) => {
|
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
|
* ConcreteInfo Theme for MarkdownConverter
|
||||||
* Based on logo palette: #464646, #9a9696, #e5461f, #e3e3e3, #0d0b09
|
* 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;
|
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 */
|
/* Dark theme adjustments for states */
|
||||||
body[class*="dark"] .preview-error-message {
|
body[class*="dark"] .preview-error-message {
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Modal System Styles
|
* Modal System Styles
|
||||||
* Unified modal components with glassmorphism backdrop
|
* 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
|
* ModalManager - Unified modal system with accessibility support
|
||||||
* @version 4.0.0
|
* @version 4.1.0
|
||||||
*/
|
*/
|
||||||
class ModalManager {
|
class ModalManager {
|
||||||
#modal;
|
#modal;
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ function createWelcomeContent(recentFiles = []) {
|
|||||||
<div class="welcome-container">
|
<div class="welcome-container">
|
||||||
<div class="welcome-hero">
|
<div class="welcome-hero">
|
||||||
<h1 class="welcome-title">MarkdownConverter</h1>
|
<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>
|
<p class="welcome-subtitle">Professional Markdown Editor & Universal Document Converter</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ function createWelcomeContent(recentFiles = []) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="welcome-section">
|
<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">
|
<ul class="welcome-features">
|
||||||
<li><strong>CodeMirror Editor</strong> — Syntax highlighting, code folding, multiple cursors</li>
|
<li><strong>CodeMirror Editor</strong> — Syntax highlighting, code folding, multiple cursors</li>
|
||||||
<li><strong>Sidebar Panels</strong> — File Explorer, Git, Snippets, Templates</li>
|
<li><strong>Sidebar Panels</strong> — File Explorer, Git, Snippets, Templates</li>
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ describe('Preload Security', () => {
|
|||||||
'save-pasted-image',
|
'save-pasted-image',
|
||||||
'load-template',
|
'load-template',
|
||||||
'list-directory',
|
'list-directory',
|
||||||
|
'read-file',
|
||||||
|
'write-file',
|
||||||
|
'delete-file',
|
||||||
|
'ensure-directory',
|
||||||
|
'path-exists',
|
||||||
|
'is-directory',
|
||||||
|
'copy-path',
|
||||||
|
'move-path',
|
||||||
'open-file-path',
|
'open-file-path',
|
||||||
'git-status',
|
'git-status',
|
||||||
'git-stage',
|
'git-stage',
|
||||||
|
|||||||
+9
-1
@@ -17,7 +17,15 @@ global.window.electronAPI = {
|
|||||||
setCurrent: jest.fn(),
|
setCurrent: jest.fn(),
|
||||||
saveRecent: jest.fn(),
|
saveRecent: jest.fn(),
|
||||||
clearRecent: 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: {
|
theme: {
|
||||||
get: jest.fn()
|
get: jest.fn()
|
||||||
|
|||||||
Reference in New Issue
Block a user