chore: 删除旧版 v1 代码 (main.js/preload.js/renderer/lib/)
重构完成后清理遗留的旧版 JavaScript 代码: - main.js (447行) — 旧版主进程 - preload.js (40行) — 旧版预加载脚本 - renderer/ — 旧版 HTML/JS/CSS - lib/ — 旧版第三方库 (marked.min.js, highlight.min.js) 新版代码统一在 src/ 目录下,技术栈:Electron 28 + React 18 + TypeScript 5.6 + Zustand 5 + CodeMirror 6
This commit is contained in:
@@ -1,10 +0,0 @@
|
|||||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
|
||||||
Theme: GitHub
|
|
||||||
Description: Light theme as seen on github.com
|
|
||||||
Author: github.com
|
|
||||||
Maintainer: @Hirse
|
|
||||||
Updated: 2021-05-15
|
|
||||||
|
|
||||||
Outdated base version: https://github.com/primer/github-syntax-light
|
|
||||||
Current colors taken from GitHub's CSS
|
|
||||||
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
|
||||||
Vendored
-1213
File diff suppressed because one or more lines are too long
Vendored
-6
File diff suppressed because one or more lines are too long
@@ -1,447 +0,0 @@
|
|||||||
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const { readFile, stat } = require('fs/promises');
|
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB
|
|
||||||
|
|
||||||
let mainWindow = null;
|
|
||||||
let activeFilePath = null; // Currently active tab's file path
|
|
||||||
let pendingFilePath = null;
|
|
||||||
let fileWatcher = null;
|
|
||||||
let closeTimeout = null;
|
|
||||||
let isClosing = false; // Prevent re-entrant close
|
|
||||||
let isSelfWriting = false; // Skip fs.watch events triggered by our own writes
|
|
||||||
|
|
||||||
// ===== Single Instance Lock =====
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
|
||||||
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.quit();
|
|
||||||
} else {
|
|
||||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
|
||||||
// A second instance was launched (e.g. double-clicking a .md file)
|
|
||||||
// Focus the existing window and open the file in it
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
||||||
mainWindow.focus();
|
|
||||||
|
|
||||||
// Extract file path from the second instance's command line args
|
|
||||||
const filePath = getFilePathFromArgs(commandLine);
|
|
||||||
if (filePath) {
|
|
||||||
openFileInTab(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract a valid file path from command line arguments (skip Electron flags)
|
|
||||||
function getFilePathFromArgs(args) {
|
|
||||||
for (let i = 1; i < args.length; i++) {
|
|
||||||
const arg = args[i];
|
|
||||||
if (!arg.startsWith('--') && !arg.startsWith('-')) {
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(arg)) return arg;
|
|
||||||
} catch (e) { /* ignore */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWindow() {
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
minWidth: 800,
|
|
||||||
minHeight: 600,
|
|
||||||
icon: path.join(__dirname, 'assets', 'icon.ico'),
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
|
||||||
contextIsolation: true,
|
|
||||||
nodeIntegration: false
|
|
||||||
},
|
|
||||||
titleBarStyle: 'default',
|
|
||||||
show: false
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
|
||||||
if (pendingFilePath) {
|
|
||||||
openFileInTab(pendingFilePath);
|
|
||||||
pendingFilePath = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
|
||||||
|
|
||||||
mainWindow.once('ready-to-show', () => {
|
|
||||||
mainWindow.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.setMenu(null);
|
|
||||||
|
|
||||||
// Unsaved changes warning on close
|
|
||||||
mainWindow.on('close', (e) => {
|
|
||||||
if (isClosing) return; // Prevent re-entrant close
|
|
||||||
isClosing = true;
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
// 检查 webContents 是否仍可用
|
|
||||||
if (!mainWindow.webContents.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('window:confirmClose');
|
|
||||||
} else {
|
|
||||||
// 渲染进程已销毁,直接关闭
|
|
||||||
mainWindow.removeAllListeners('close');
|
|
||||||
mainWindow.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
mainWindow.removeAllListeners('close');
|
|
||||||
mainWindow.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 5s safety timeout in case renderer is unresponsive
|
|
||||||
closeTimeout = setTimeout(() => {
|
|
||||||
closeTimeout = null;
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.removeAllListeners('close');
|
|
||||||
mainWindow.close();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
|
||||||
stopWatching();
|
|
||||||
stopSidebarWatching();
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// File watcher
|
|
||||||
function startWatching(filePath) {
|
|
||||||
stopWatching();
|
|
||||||
if (!filePath) return;
|
|
||||||
try {
|
|
||||||
fileWatcher = fs.watch(filePath, (eventType) => {
|
|
||||||
if (eventType === 'change' && mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
// 跳过自身写入触发的 change 事件
|
|
||||||
if (isSelfWriting) return;
|
|
||||||
mainWindow.webContents.send('file:externallyModified', filePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
// Silently ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopWatching() {
|
|
||||||
if (fileWatcher) {
|
|
||||||
fileWatcher.close();
|
|
||||||
fileWatcher = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showOpenDialog() {
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
|
||||||
properties: ['openFile'],
|
|
||||||
filters: [
|
|
||||||
{ name: 'Markdown 文件', extensions: ['md', 'markdown', 'txt'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
|
||||||
return result.filePaths;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readFileContent(filePath) {
|
|
||||||
// 文件大小检查
|
|
||||||
const stats = await stat(filePath);
|
|
||||||
if (stats.size > MAX_FILE_SIZE) {
|
|
||||||
throw new Error(`文件过大(${(stats.size / 1024 / 1024).toFixed(1)} MB),暂不支持超过 20MB 的文件`);
|
|
||||||
}
|
|
||||||
let content = await readFile(filePath, 'utf-8');
|
|
||||||
// 剥离 UTF-8 BOM(Windows 某些编辑器会添加)
|
|
||||||
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open file in a new tab (renderer manages tabs)
|
|
||||||
async function openFileInTab(filePath) {
|
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
||||||
try {
|
|
||||||
const content = await readFileContent(filePath);
|
|
||||||
activeFilePath = filePath;
|
|
||||||
startWatching(filePath);
|
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
|
||||||
mainWindow.webContents.send('file:openInTab', { filePath, content });
|
|
||||||
} catch (err) {
|
|
||||||
dialog.showErrorBox('错误', `无法读取文件: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch active file (for tab switching)
|
|
||||||
function switchActiveFile(filePath) {
|
|
||||||
activeFilePath = filePath || null;
|
|
||||||
startWatching(filePath);
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
if (filePath) {
|
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
|
||||||
} else {
|
|
||||||
mainWindow.setTitle('MarkLite');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC Handlers
|
|
||||||
|
|
||||||
// Save file to a specific path with watcher management
|
|
||||||
async function saveToPath(filePath, content) {
|
|
||||||
stopWatching();
|
|
||||||
try {
|
|
||||||
isSelfWriting = true;
|
|
||||||
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
||||||
isSelfWriting = false;
|
|
||||||
} catch (err) {
|
|
||||||
isSelfWriting = false;
|
|
||||||
startWatching(activeFilePath);
|
|
||||||
return { success: false, error: err.message };
|
|
||||||
}
|
|
||||||
activeFilePath = filePath;
|
|
||||||
// 延迟 300ms 重新监听,确保文件系统的 change 事件已过期
|
|
||||||
setTimeout(() => {
|
|
||||||
if (activeFilePath === filePath) {
|
|
||||||
startWatching(filePath);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
|
||||||
}
|
|
||||||
return { success: true, filePath };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open single file via dialog
|
|
||||||
ipcMain.handle('dialog:openFile', async () => {
|
|
||||||
try {
|
|
||||||
const files = await showOpenDialog();
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const content = await readFileContent(files[0]);
|
|
||||||
activeFilePath = files[0];
|
|
||||||
startWatching(files[0]);
|
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(files[0])}`);
|
|
||||||
return { filePath: files[0], content };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (err) {
|
|
||||||
return { error: err.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('file:read', async (event, filePath) => {
|
|
||||||
try {
|
|
||||||
const content = await readFileContent(filePath);
|
|
||||||
return { success: true, content };
|
|
||||||
} catch (err) {
|
|
||||||
return { success: false, error: err.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('file:save', async (event, { filePath, content }) => {
|
|
||||||
try {
|
|
||||||
if (filePath) {
|
|
||||||
return await saveToPath(filePath, content);
|
|
||||||
} else {
|
|
||||||
const result = await dialog.showSaveDialog(mainWindow, {
|
|
||||||
filters: [
|
|
||||||
{ name: 'Markdown 文件', extensions: ['md'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
if (!result.canceled) {
|
|
||||||
return await saveToPath(result.filePath, content);
|
|
||||||
}
|
|
||||||
return { success: false, canceled: true };
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return { success: false, error: err.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('file:saveAs', async (event, { content }) => {
|
|
||||||
try {
|
|
||||||
const result = await dialog.showSaveDialog(mainWindow, {
|
|
||||||
filters: [
|
|
||||||
{ name: 'Markdown 文件', extensions: ['md'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
if (!result.canceled) {
|
|
||||||
return await saveToPath(result.filePath, content);
|
|
||||||
}
|
|
||||||
return { success: false, canceled: true };
|
|
||||||
} catch (err) {
|
|
||||||
return { success: false, error: err.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('file:getCurrentPath', () => {
|
|
||||||
return activeFilePath;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('file:stats', async (event, filePath) => {
|
|
||||||
try {
|
|
||||||
const fileStat = await stat(filePath);
|
|
||||||
return { success: true, size: fileStat.size, mtime: fileStat.mtime.toISOString() };
|
|
||||||
} catch (err) {
|
|
||||||
return { success: false, error: err.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('file:reload', async () => {
|
|
||||||
if (!activeFilePath) return { success: false, error: '没有打开的文件' };
|
|
||||||
try {
|
|
||||||
const content = await readFileContent(activeFilePath);
|
|
||||||
return { success: true, content, filePath: activeFilePath };
|
|
||||||
} catch (err) {
|
|
||||||
return { success: false, error: err.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== File Tree (Sidebar) =====
|
|
||||||
const SKIP_DIRS = new Set(['node_modules', '.git', '.svn', '.hg', 'dist', 'out', '.next', '.nuxt', '__pycache__', '.DS_Store']);
|
|
||||||
const MARKDOWN_EXTS = new Set(['.md', '.markdown', '.txt']);
|
|
||||||
|
|
||||||
async function buildDirTree(dirPath) {
|
|
||||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
||||||
// Sort: directories first, then files, alphabetically
|
|
||||||
entries.sort((a, b) => {
|
|
||||||
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
||||||
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const children = [];
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (SKIP_DIRS.has(entry.name)) continue;
|
|
||||||
if (entry.name.startsWith('.')) continue;
|
|
||||||
|
|
||||||
const childPath = path.join(dirPath, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const subChildren = await buildDirTree(childPath);
|
|
||||||
if (subChildren.length > 0) {
|
|
||||||
children.push({ name: entry.name, path: childPath, type: 'dir', children: subChildren });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const ext = path.extname(entry.name).toLowerCase();
|
|
||||||
if (MARKDOWN_EXTS.has(ext)) {
|
|
||||||
children.push({ name: entry.name, path: childPath, type: 'file' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('dir:readTree', async (event, dirPath) => {
|
|
||||||
try {
|
|
||||||
const tree = await buildDirTree(dirPath);
|
|
||||||
return { success: true, tree, rootPath: dirPath };
|
|
||||||
} catch (err) {
|
|
||||||
return { success: false, error: err.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let sidebarWatcher = null;
|
|
||||||
let sidebarWatchPath = null;
|
|
||||||
|
|
||||||
function startSidebarWatching(dirPath) {
|
|
||||||
stopSidebarWatching();
|
|
||||||
if (!dirPath) return;
|
|
||||||
try {
|
|
||||||
sidebarWatchPath = dirPath;
|
|
||||||
sidebarWatcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('sidebar:dirChanged');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopSidebarWatching() {
|
|
||||||
if (sidebarWatcher) { sidebarWatcher.close(); sidebarWatcher = null; }
|
|
||||||
sidebarWatchPath = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('dir:watch', (event, dirPath) => {
|
|
||||||
startSidebarWatching(dirPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('dir:unwatch', () => {
|
|
||||||
stopSidebarWatching();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('dir:openDialog', async () => {
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
|
||||||
properties: ['openDirectory']
|
|
||||||
});
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
|
||||||
return result.filePaths[0];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tab switched - update active file tracking
|
|
||||||
ipcMain.handle('tab:switched', (event, filePath) => {
|
|
||||||
switchActiveFile(filePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('window:forceClose', () => {
|
|
||||||
if (closeTimeout) {
|
|
||||||
clearTimeout(closeTimeout);
|
|
||||||
closeTimeout = null;
|
|
||||||
}
|
|
||||||
// 窗口已销毁则无需操作(超时兜底可能已经关闭了窗口)
|
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
||||||
stopWatching();
|
|
||||||
mainWindow.removeAllListeners('close');
|
|
||||||
mainWindow.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('window:cancelClose', () => {
|
|
||||||
if (closeTimeout) {
|
|
||||||
clearTimeout(closeTimeout);
|
|
||||||
closeTimeout = null;
|
|
||||||
}
|
|
||||||
isClosing = false; // Reset so close can be triggered again
|
|
||||||
});
|
|
||||||
|
|
||||||
// Command line file (first instance)
|
|
||||||
function getCommandLineFile() {
|
|
||||||
return getFilePathFromArgs(process.argv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// App lifecycle
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
app.on('open-file', (event, filePath) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents.isLoading()) {
|
|
||||||
pendingFilePath = filePath;
|
|
||||||
} else if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
openFileInTab(filePath);
|
|
||||||
} else {
|
|
||||||
pendingFilePath = filePath;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
createWindow();
|
|
||||||
|
|
||||||
const fileToOpen = getCommandLineFile();
|
|
||||||
if (fileToOpen) {
|
|
||||||
pendingFilePath = fileToOpen;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') app.quit();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
||||||
});
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
const { contextBridge, ipcRenderer, shell } = require('electron');
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
|
||||||
// File operations
|
|
||||||
openFile: () => ipcRenderer.invoke('dialog:openFile'),
|
|
||||||
readFile: (filePath) => ipcRenderer.invoke('file:read', filePath),
|
|
||||||
saveFile: (data) => ipcRenderer.invoke('file:save', data),
|
|
||||||
saveFileAs: (data) => ipcRenderer.invoke('file:saveAs', data),
|
|
||||||
getCurrentPath: () => ipcRenderer.invoke('file:getCurrentPath'),
|
|
||||||
getFileStats: (filePath) => ipcRenderer.invoke('file:stats', filePath),
|
|
||||||
reloadFile: () => ipcRenderer.invoke('file:reload'),
|
|
||||||
|
|
||||||
// Tab management
|
|
||||||
tabSwitched: (filePath) => ipcRenderer.invoke('tab:switched', filePath),
|
|
||||||
|
|
||||||
// Window control
|
|
||||||
forceClose: () => ipcRenderer.invoke('window:forceClose'),
|
|
||||||
cancelClose: () => ipcRenderer.invoke('window:cancelClose'),
|
|
||||||
|
|
||||||
// Shell
|
|
||||||
openExternal: (url) => shell.openExternal(url),
|
|
||||||
|
|
||||||
// File Tree (Sidebar)
|
|
||||||
readDirTree: (dirPath) => ipcRenderer.invoke('dir:readTree', dirPath),
|
|
||||||
openFolderDialog: () => ipcRenderer.invoke('dir:openDialog'),
|
|
||||||
watchDir: (dirPath) => ipcRenderer.invoke('dir:watch', dirPath),
|
|
||||||
unwatchDir: () => ipcRenderer.invoke('dir:unwatch'),
|
|
||||||
|
|
||||||
// Events from main process
|
|
||||||
onFileOpenInTab: (callback) => ipcRenderer.on('file:openInTab', (event, data) => callback(data)),
|
|
||||||
onMenuSave: (callback) => ipcRenderer.on('menu:save', () => callback()),
|
|
||||||
onMenuSaveAs: (callback) => ipcRenderer.on('menu:saveAs', () => callback()),
|
|
||||||
onViewModeChange: (callback) => ipcRenderer.on('menu:viewMode', (event, mode) => callback(mode)),
|
|
||||||
onExternalModification: (callback) => ipcRenderer.on('file:externallyModified', (event, filePath) => callback(filePath)),
|
|
||||||
onDirChanged: (callback) => ipcRenderer.on('sidebar:dirChanged', () => callback()),
|
|
||||||
onConfirmClose: (callback) => ipcRenderer.on('window:confirmClose', () => callback()),
|
|
||||||
|
|
||||||
// Remove listeners
|
|
||||||
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
|
|
||||||
});
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<!-- CSP: script-src 只允许 self,阻断内联脚本执行 -->
|
|
||||||
<!-- style-src 保留 unsafe-inline:highlight.js 代码高亮依赖 CSS class,
|
|
||||||
但已通过 sanitizeHTML 移除恶意 style 属性和 expression() -->
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self';">
|
|
||||||
<title>MarkLite</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<link rel="stylesheet" href="../lib/highlight-github.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<!-- Toolbar -->
|
|
||||||
<div id="toolbar">
|
|
||||||
<div class="toolbar-left">
|
|
||||||
<button id="btn-open" class="toolbar-btn" title="打开文件 (Ctrl+O)">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>打开</span>
|
|
||||||
</button>
|
|
||||||
<button id="btn-save" class="toolbar-btn" title="保存文件 (Ctrl+S)">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
|
||||||
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
|
||||||
<polyline points="7 3 7 8 15 8"></polyline>
|
|
||||||
</svg>
|
|
||||||
<span>保存</span>
|
|
||||||
</button>
|
|
||||||
<div class="toolbar-divider"></div>
|
|
||||||
<button id="btn-split" class="toolbar-btn active" title="编辑+预览 (Ctrl+1)">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
||||||
<line x1="12" y1="3" x2="12" y2="21"></line>
|
|
||||||
</svg>
|
|
||||||
<span>分屏</span>
|
|
||||||
</button>
|
|
||||||
<button id="btn-editor" class="toolbar-btn" title="纯编辑 (Ctrl+2)">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>编辑</span>
|
|
||||||
</button>
|
|
||||||
<button id="btn-preview" class="toolbar-btn" title="纯预览 (Ctrl+3)">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
|
||||||
</svg>
|
|
||||||
<span>预览</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="toolbar-right">
|
|
||||||
<button id="btn-dark" class="toolbar-btn" title="切换暗色主题">
|
|
||||||
<svg id="icon-dark" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
||||||
</svg>
|
|
||||||
<svg id="icon-light" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">
|
|
||||||
<circle cx="12" cy="12" r="5"></circle>
|
|
||||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
||||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
||||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
||||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
||||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
||||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Workspace: Sidebar + Main Content -->
|
|
||||||
<div id="workspace">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div id="sidebar">
|
|
||||||
<div id="sidebar-header">
|
|
||||||
<span id="sidebar-title">资源管理器</span>
|
|
||||||
<button id="btn-open-folder" class="sidebar-header-btn" title="打开文件夹">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="sidebar-tree"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content area -->
|
|
||||||
<div id="main-content">
|
|
||||||
<!-- Tab bar -->
|
|
||||||
<div id="tab-bar">
|
|
||||||
<div id="tab-list"></div>
|
|
||||||
<button id="btn-new-tab" class="tab-add-btn" title="新建标签页 (Ctrl+T)">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="content-wrapper">
|
|
||||||
<!-- External modification banner -->
|
|
||||||
<div id="modified-banner" class="hidden">
|
|
||||||
<span>文件已被外部程序修改</span>
|
|
||||||
<button id="btn-reload" class="banner-btn">重新加载</button>
|
|
||||||
<button id="btn-dismiss" class="banner-btn">忽略</button>
|
|
||||||
</div>
|
|
||||||
<!-- Editor panel -->
|
|
||||||
<div id="editor-panel">
|
|
||||||
<!-- Search & Replace Bar -->
|
|
||||||
<div id="search-bar" class="hidden">
|
|
||||||
<div class="search-row">
|
|
||||||
<button id="btn-toggle-replace" class="search-opt-btn" title="展开替换行">▾</button>
|
|
||||||
<input type="text" id="search-input" placeholder="查找..." />
|
|
||||||
<span id="search-count"></span>
|
|
||||||
<button id="btn-case" class="search-opt-btn" title="区分大小写 (Alt+C)">Aa</button>
|
|
||||||
<button id="btn-regex" class="search-opt-btn" title="正则表达式 (Alt+R)">.*</button>
|
|
||||||
<button id="btn-prev" class="search-nav-btn" title="上一个 (Shift+Enter)">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="18 15 12 9 6 15"></polyline></svg>
|
|
||||||
</button>
|
|
||||||
<button id="btn-next" class="search-nav-btn" title="下一个 (Enter)">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
|
||||||
</button>
|
|
||||||
<button id="btn-search-close" class="search-nav-btn" title="关闭 (Escape)">✕</button>
|
|
||||||
</div>
|
|
||||||
<div id="replace-row" class="hidden">
|
|
||||||
<input type="text" id="replace-input" placeholder="替换..." />
|
|
||||||
<button id="btn-replace" class="replace-btn" title="替换 (Ctrl+Shift+G)">替换</button>
|
|
||||||
<button id="btn-replace-all" class="replace-btn" title="全部替换 (Ctrl+Shift+H)">全部</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="editor-wrapper">
|
|
||||||
<div id="line-numbers"></div>
|
|
||||||
<textarea id="editor" spellcheck="false" placeholder="在此输入 Markdown 内容,或拖拽 .md 文件到窗口打开..."></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resizer -->
|
|
||||||
<div id="resizer"></div>
|
|
||||||
|
|
||||||
<!-- Preview panel -->
|
|
||||||
<div id="preview-panel">
|
|
||||||
<div id="preview" class="markdown-body"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Welcome screen -->
|
|
||||||
<div id="welcome-screen">
|
|
||||||
<div class="welcome-content">
|
|
||||||
<div class="welcome-icon">
|
|
||||||
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="10" y="5" width="80" height="90" rx="8" fill="#f0f6ff" stroke="#1a73e8" stroke-width="2"/>
|
|
||||||
<text x="50" y="45" text-anchor="middle" fill="#1a73e8" font-family="system-ui" font-weight="bold" font-size="32">M↓</text>
|
|
||||||
<text x="50" y="70" text-anchor="middle" fill="#5f6368" font-family="system-ui" font-size="12">MarkLite</text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1>欢迎使用 MarkLite</h1>
|
|
||||||
<p>一款轻量级的 Markdown 阅读器</p>
|
|
||||||
<div class="welcome-actions">
|
|
||||||
<button id="btn-welcome-open" class="welcome-btn primary">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
||||||
</svg>
|
|
||||||
打开文件
|
|
||||||
</button>
|
|
||||||
<button id="btn-welcome-new" class="welcome-btn secondary">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
新建文件
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="welcome-tips">
|
|
||||||
<p>💡 提示:可以直接拖拽 .md 文件到窗口打开</p>
|
|
||||||
<p>⌨️ 快捷键:Ctrl+O 打开 | Ctrl+S 保存 | Ctrl+F 搜索 | Ctrl+H 替换 | Ctrl+1/2/3 视图 | Ctrl+T 标签</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status bar -->
|
|
||||||
<div id="statusbar">
|
|
||||||
<div class="status-left">
|
|
||||||
<span id="status-text">就绪</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-right">
|
|
||||||
<span id="status-encoding">UTF-8</span>
|
|
||||||
<span class="status-divider">|</span>
|
|
||||||
<span id="status-lang">Markdown</span>
|
|
||||||
<span class="status-divider">|</span>
|
|
||||||
<span id="status-size"></span>
|
|
||||||
<span class="status-divider status-size-divider">|</span>
|
|
||||||
<span id="status-cursor">行 1, 列 1</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Drop overlay -->
|
|
||||||
<div id="drop-overlay" class="hidden">
|
|
||||||
<div class="drop-content">
|
|
||||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
||||||
</svg>
|
|
||||||
<p>释放文件以打开</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../lib/marked.min.js"></script>
|
|
||||||
<script src="../lib/highlight.min.js"></script>
|
|
||||||
<script src="renderer.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because it is too large
Load Diff
-1119
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user