全面优化:暗色主题、文件监听、未保存提醒、滚动同步、分屏记忆、文件大小显示、删除死代码
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
const { app, BrowserWindow, dialog, ipcMain, Menu } = require('electron');
|
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
let currentFilePath = null;
|
let currentFilePath = null;
|
||||||
let pendingFilePath = null; // For file association race condition
|
let pendingFilePath = null;
|
||||||
|
let fileWatcher = null;
|
||||||
|
let isModifiedExternally = false;
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
// Create the browser window
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
@@ -23,7 +24,6 @@ function createWindow() {
|
|||||||
show: false
|
show: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register did-finish-load BEFORE loadFile to avoid race condition
|
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
if (pendingFilePath) {
|
if (pendingFilePath) {
|
||||||
openFile(pendingFilePath);
|
openFile(pendingFilePath);
|
||||||
@@ -31,102 +31,48 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load the index.html
|
|
||||||
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||||
|
|
||||||
// Show window when ready
|
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-show', () => {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove native menu bar
|
|
||||||
mainWindow.setMenu(null);
|
mainWindow.setMenu(null);
|
||||||
|
|
||||||
|
// Unsaved changes warning on close
|
||||||
|
mainWindow.on('close', (e) => {
|
||||||
|
mainWindow.webContents.send('window:closing');
|
||||||
|
e.preventDefault();
|
||||||
|
mainWindow.webContents.send('window:confirmClose');
|
||||||
|
});
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
|
stopWatching();
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMenu() {
|
// File watcher - detect external modifications
|
||||||
const template = [
|
function startWatching(filePath) {
|
||||||
{
|
stopWatching();
|
||||||
label: '文件',
|
try {
|
||||||
submenu: [
|
isModifiedExternally = false;
|
||||||
{
|
fileWatcher = fs.watch(filePath, (eventType) => {
|
||||||
label: '打开文件',
|
if (eventType === 'change' && mainWindow && !mainWindow.isDestroyed()) {
|
||||||
accelerator: 'CmdOrCtrl+O',
|
isModifiedExternally = true;
|
||||||
click: () => handleOpenFile()
|
mainWindow.webContents.send('file:externallyModified', filePath);
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '保存',
|
|
||||||
accelerator: 'CmdOrCtrl+S',
|
|
||||||
click: () => mainWindow.webContents.send('menu:save')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '另存为',
|
|
||||||
accelerator: 'CmdOrCtrl+Shift+S',
|
|
||||||
click: () => mainWindow.webContents.send('menu:saveAs')
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: '退出',
|
|
||||||
accelerator: 'CmdOrCtrl+Q',
|
|
||||||
click: () => app.quit()
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '视图',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: '编辑 + 预览',
|
|
||||||
accelerator: 'CmdOrCtrl+1',
|
|
||||||
click: () => mainWindow.webContents.send('menu:viewMode', 'split')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '纯编辑',
|
|
||||||
accelerator: 'CmdOrCtrl+2',
|
|
||||||
click: () => mainWindow.webContents.send('menu:viewMode', 'editor')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '纯预览',
|
|
||||||
accelerator: 'CmdOrCtrl+3',
|
|
||||||
click: () => mainWindow.webContents.send('menu:viewMode', 'preview')
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: '开发者工具',
|
|
||||||
accelerator: 'F12',
|
|
||||||
click: () => mainWindow.webContents.toggleDevTools()
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: '重新加载',
|
|
||||||
accelerator: 'CmdOrCtrl+R',
|
|
||||||
click: () => mainWindow.webContents.reload()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '帮助',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: '关于 MarkLite',
|
|
||||||
click: () => {
|
|
||||||
dialog.showMessageBox(mainWindow, {
|
|
||||||
type: 'info',
|
|
||||||
title: '关于 MarkLite',
|
|
||||||
message: 'MarkLite v1.0.0',
|
|
||||||
detail: '一款轻量级的 Markdown 阅读器。\n\n技术栈:Electron + marked.js + highlight.js'
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Silently ignore watch errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(template);
|
function stopWatching() {
|
||||||
Menu.setApplicationMenu(menu);
|
if (fileWatcher) {
|
||||||
|
fileWatcher.close();
|
||||||
|
fileWatcher = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showOpenDialog() {
|
async function showOpenDialog() {
|
||||||
@@ -143,17 +89,11 @@ async function showOpenDialog() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOpenFile() {
|
|
||||||
const filePath = await showOpenDialog();
|
|
||||||
if (filePath) {
|
|
||||||
openFile(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFile(filePath) {
|
function openFile(filePath) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
currentFilePath = filePath;
|
currentFilePath = filePath;
|
||||||
|
startWatching(filePath);
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
||||||
mainWindow.webContents.send('file:opened', { filePath, content });
|
mainWindow.webContents.send('file:opened', { filePath, content });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -163,14 +103,19 @@ function openFile(filePath) {
|
|||||||
|
|
||||||
// IPC Handlers
|
// IPC Handlers
|
||||||
ipcMain.handle('dialog:openFile', async () => {
|
ipcMain.handle('dialog:openFile', async () => {
|
||||||
|
try {
|
||||||
const filePath = await showOpenDialog();
|
const filePath = await showOpenDialog();
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
currentFilePath = filePath;
|
currentFilePath = filePath;
|
||||||
|
startWatching(filePath);
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
||||||
return { filePath, content };
|
return { filePath, content };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('file:read', async (event, filePath) => {
|
ipcMain.handle('file:read', async (event, filePath) => {
|
||||||
@@ -185,12 +130,13 @@ ipcMain.handle('file:read', async (event, filePath) => {
|
|||||||
ipcMain.handle('file:save', async (event, { filePath, content }) => {
|
ipcMain.handle('file:save', async (event, { filePath, content }) => {
|
||||||
try {
|
try {
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
|
stopWatching();
|
||||||
fs.writeFileSync(filePath, content, 'utf-8');
|
fs.writeFileSync(filePath, content, 'utf-8');
|
||||||
currentFilePath = filePath;
|
currentFilePath = filePath;
|
||||||
|
startWatching(filePath);
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
||||||
return { success: true, filePath };
|
return { success: true, filePath };
|
||||||
} else {
|
} else {
|
||||||
// No file path, do save as
|
|
||||||
const result = await dialog.showSaveDialog(mainWindow, {
|
const result = await dialog.showSaveDialog(mainWindow, {
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'Markdown 文件', extensions: ['md'] },
|
{ name: 'Markdown 文件', extensions: ['md'] },
|
||||||
@@ -198,8 +144,10 @@ ipcMain.handle('file:save', async (event, { filePath, content }) => {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
|
stopWatching();
|
||||||
fs.writeFileSync(result.filePath, content, 'utf-8');
|
fs.writeFileSync(result.filePath, content, 'utf-8');
|
||||||
currentFilePath = result.filePath;
|
currentFilePath = result.filePath;
|
||||||
|
startWatching(result.filePath);
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
|
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
|
||||||
return { success: true, filePath: result.filePath };
|
return { success: true, filePath: result.filePath };
|
||||||
}
|
}
|
||||||
@@ -219,8 +167,10 @@ ipcMain.handle('file:saveAs', async (event, { content }) => {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
|
stopWatching();
|
||||||
fs.writeFileSync(result.filePath, content, 'utf-8');
|
fs.writeFileSync(result.filePath, content, 'utf-8');
|
||||||
currentFilePath = result.filePath;
|
currentFilePath = result.filePath;
|
||||||
|
startWatching(result.filePath);
|
||||||
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
|
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
|
||||||
return { success: true, filePath: result.filePath };
|
return { success: true, filePath: result.filePath };
|
||||||
}
|
}
|
||||||
@@ -234,6 +184,36 @@ ipcMain.handle('file:getCurrentPath', () => {
|
|||||||
return currentFilePath;
|
return currentFilePath;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('file:stats', async (event, filePath) => {
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
size: stats.size,
|
||||||
|
mtime: stats.mtime.toISOString()
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('file:reload', async () => {
|
||||||
|
if (!currentFilePath) return { success: false, error: '没有打开的文件' };
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(currentFilePath, 'utf-8');
|
||||||
|
isModifiedExternally = false;
|
||||||
|
return { success: true, content, filePath: currentFilePath };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window:forceClose', () => {
|
||||||
|
stopWatching();
|
||||||
|
mainWindow.removeAllListeners('close');
|
||||||
|
mainWindow.close();
|
||||||
|
});
|
||||||
|
|
||||||
// Handle file open from command line or file association
|
// Handle file open from command line or file association
|
||||||
function getCommandLineFile() {
|
function getCommandLineFile() {
|
||||||
const args = process.argv.slice(1);
|
const args = process.argv.slice(1);
|
||||||
@@ -248,28 +228,23 @@ function getCommandLineFile() {
|
|||||||
|
|
||||||
// App lifecycle
|
// App lifecycle
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Register open-file handler BEFORE creating window (fixes race condition)
|
|
||||||
app.on('open-file', (event, filePath) => {
|
app.on('open-file', (event, filePath) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (mainWindow && mainWindow.webContents.isLoading()) {
|
if (mainWindow && mainWindow.webContents.isLoading()) {
|
||||||
// Window is still loading, queue the file
|
|
||||||
pendingFilePath = filePath;
|
pendingFilePath = filePath;
|
||||||
} else if (mainWindow) {
|
} else if (mainWindow) {
|
||||||
openFile(filePath);
|
openFile(filePath);
|
||||||
} else {
|
} else {
|
||||||
// Window not created yet, store for later
|
|
||||||
pendingFilePath = filePath;
|
pendingFilePath = filePath;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
// Check for file passed via command line
|
|
||||||
const fileToOpen = getCommandLineFile();
|
const fileToOpen = getCommandLineFile();
|
||||||
if (fileToOpen) {
|
if (fileToOpen) {
|
||||||
pendingFilePath = fileToOpen;
|
pendingFilePath = fileToOpen;
|
||||||
}
|
}
|
||||||
// pendingFilePath will be opened by the did-finish-load handler in createWindow()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
|||||||
+12
@@ -7,6 +7,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
saveFile: (data) => ipcRenderer.invoke('file:save', data),
|
saveFile: (data) => ipcRenderer.invoke('file:save', data),
|
||||||
saveFileAs: (data) => ipcRenderer.invoke('file:saveAs', data),
|
saveFileAs: (data) => ipcRenderer.invoke('file:saveAs', data),
|
||||||
getCurrentPath: () => ipcRenderer.invoke('file:getCurrentPath'),
|
getCurrentPath: () => ipcRenderer.invoke('file:getCurrentPath'),
|
||||||
|
getFileStats: (filePath) => ipcRenderer.invoke('file:stats', filePath),
|
||||||
|
reloadFile: () => ipcRenderer.invoke('file:reload'),
|
||||||
|
|
||||||
|
// Window control
|
||||||
|
forceClose: () => ipcRenderer.invoke('window:forceClose'),
|
||||||
|
|
||||||
// Menu events
|
// Menu events
|
||||||
onFileOpened: (callback) => ipcRenderer.on('file:opened', (event, data) => callback(data)),
|
onFileOpened: (callback) => ipcRenderer.on('file:opened', (event, data) => callback(data)),
|
||||||
@@ -14,6 +19,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
onMenuSaveAs: (callback) => ipcRenderer.on('menu:saveAs', () => callback()),
|
onMenuSaveAs: (callback) => ipcRenderer.on('menu:saveAs', () => callback()),
|
||||||
onViewModeChange: (callback) => ipcRenderer.on('menu:viewMode', (event, mode) => callback(mode)),
|
onViewModeChange: (callback) => ipcRenderer.on('menu:viewMode', (event, mode) => callback(mode)),
|
||||||
|
|
||||||
|
// File modification detection
|
||||||
|
onExternalModification: (callback) => ipcRenderer.on('file:externallyModified', (event, filePath) => callback(filePath)),
|
||||||
|
|
||||||
|
// Window close confirmation
|
||||||
|
onConfirmClose: (callback) => ipcRenderer.on('window:confirmClose', () => callback()),
|
||||||
|
onClosing: (callback) => ipcRenderer.on('window:closing', () => callback()),
|
||||||
|
|
||||||
// Remove listeners
|
// Remove listeners
|
||||||
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
|
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
|
||||||
});
|
});
|
||||||
|
|||||||
+28
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: https:;">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:;">
|
||||||
<title>MarkLite</title>
|
<title>MarkLite</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="stylesheet" href="../lib/highlight-github.css">
|
<link rel="stylesheet" href="../lib/highlight-github.css">
|
||||||
@@ -50,6 +50,31 @@
|
|||||||
<span>预览</span>
|
<span>预览</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<!-- Main content area -->
|
<!-- Main content area -->
|
||||||
@@ -81,6 +106,8 @@
|
|||||||
<span class="status-divider">|</span>
|
<span class="status-divider">|</span>
|
||||||
<span id="status-lang">Markdown</span>
|
<span id="status-lang">Markdown</span>
|
||||||
<span class="status-divider">|</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>
|
<span id="status-cursor">行 1, 列 1</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+200
-49
@@ -8,8 +8,11 @@
|
|||||||
const lineNumbers = document.getElementById('line-numbers');
|
const lineNumbers = document.getElementById('line-numbers');
|
||||||
const welcomeScreen = document.getElementById('welcome-screen');
|
const welcomeScreen = document.getElementById('welcome-screen');
|
||||||
const dropOverlay = document.getElementById('drop-overlay');
|
const dropOverlay = document.getElementById('drop-overlay');
|
||||||
|
const modifiedBanner = document.getElementById('modified-banner');
|
||||||
const statusText = document.getElementById('status-text');
|
const statusText = document.getElementById('status-text');
|
||||||
const statusCursor = document.getElementById('status-cursor');
|
const statusCursor = document.getElementById('status-cursor');
|
||||||
|
const statusSize = document.getElementById('status-size');
|
||||||
|
const statusSizeDivider = document.querySelector('.status-size-divider');
|
||||||
const resizer = document.getElementById('resizer');
|
const resizer = document.getElementById('resizer');
|
||||||
const editorPanel = document.getElementById('editor-panel');
|
const editorPanel = document.getElementById('editor-panel');
|
||||||
const previewPanel = document.getElementById('preview-panel');
|
const previewPanel = document.getElementById('preview-panel');
|
||||||
@@ -23,18 +26,58 @@
|
|||||||
const btnPreview = document.getElementById('btn-preview');
|
const btnPreview = document.getElementById('btn-preview');
|
||||||
const btnWelcomeOpen = document.getElementById('btn-welcome-open');
|
const btnWelcomeOpen = document.getElementById('btn-welcome-open');
|
||||||
const btnWelcomeNew = document.getElementById('btn-welcome-new');
|
const btnWelcomeNew = document.getElementById('btn-welcome-new');
|
||||||
|
const btnDark = document.getElementById('btn-dark');
|
||||||
|
const btnReload = document.getElementById('btn-reload');
|
||||||
|
const btnDismiss = document.getElementById('btn-dismiss');
|
||||||
|
|
||||||
// ===== State =====
|
// ===== State =====
|
||||||
let currentFilePath = null;
|
let currentFilePath = null;
|
||||||
let currentContent = '';
|
let currentContent = '';
|
||||||
let viewMode = 'split'; // split, editor, preview
|
let viewMode = 'split';
|
||||||
let isModified = false;
|
let isModified = false;
|
||||||
let updateTimer = null;
|
let updateTimer = null;
|
||||||
|
let lineCountCache = 0;
|
||||||
|
|
||||||
|
// ===== localStorage Helpers =====
|
||||||
|
function loadSettings() {
|
||||||
|
try {
|
||||||
|
const s = JSON.parse(localStorage.getItem('marklite-settings') || '{}');
|
||||||
|
return s;
|
||||||
|
} catch { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSetting(key, value) {
|
||||||
|
try {
|
||||||
|
const s = loadSettings();
|
||||||
|
s[key] = value;
|
||||||
|
localStorage.setItem('marklite-settings', JSON.stringify(s));
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Dark Mode =====
|
||||||
|
function initDarkMode() {
|
||||||
|
const settings = loadSettings();
|
||||||
|
const iconDark = document.getElementById('icon-dark');
|
||||||
|
const iconLight = document.getElementById('icon-light');
|
||||||
|
|
||||||
|
function apply(isDark) {
|
||||||
|
document.documentElement.classList.toggle('dark', isDark);
|
||||||
|
iconDark.style.display = isDark ? 'none' : '';
|
||||||
|
iconLight.style.display = isDark ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(!!settings.darkMode);
|
||||||
|
|
||||||
|
btnDark.addEventListener('click', () => {
|
||||||
|
const isDark = !document.documentElement.classList.contains('dark');
|
||||||
|
apply(isDark);
|
||||||
|
saveSetting('darkMode', isDark);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Initialize marked.js =====
|
// ===== Initialize marked.js =====
|
||||||
function initMarked() {
|
function initMarked() {
|
||||||
if (typeof marked !== 'undefined') {
|
if (typeof marked !== 'undefined') {
|
||||||
// Custom renderer for code block highlighting (compatible with marked v12+)
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
renderer.code = function ({ text, lang }) {
|
renderer.code = function ({ text, lang }) {
|
||||||
let highlighted = text;
|
let highlighted = text;
|
||||||
@@ -72,15 +115,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Line Numbers =====
|
// ===== Line Numbers (optimized: batch innerHTML) =====
|
||||||
function updateLineNumbers() {
|
function updateLineNumbers() {
|
||||||
const lines = editor.value.split('\n');
|
const count = editor.value.split('\n').length;
|
||||||
const count = lines.length;
|
if (count === lineCountCache) return;
|
||||||
let html = '';
|
lineCountCache = count;
|
||||||
|
|
||||||
|
const fragment = [];
|
||||||
for (let i = 1; i <= count; i++) {
|
for (let i = 1; i <= count; i++) {
|
||||||
html += '<div class="line-num">' + i + '</div>';
|
fragment.push('<div class="line-num">', i, '</div>');
|
||||||
}
|
}
|
||||||
lineNumbers.innerHTML = html;
|
lineNumbers.innerHTML = fragment.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Cursor Position =====
|
// ===== Cursor Position =====
|
||||||
@@ -94,25 +139,55 @@
|
|||||||
statusCursor.textContent = `行 ${line}, 列 ${col}`;
|
statusCursor.textContent = `行 ${line}, 列 ${col}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Sync Scroll (line-based for more accurate sync) =====
|
// ===== File Size =====
|
||||||
|
function updateFileSize() {
|
||||||
|
const bytes = new TextEncoder().encode(editor.value).length;
|
||||||
|
let display;
|
||||||
|
if (bytes < 1024) display = bytes + ' B';
|
||||||
|
else if (bytes < 1024 * 1024) display = (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
else display = (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||||
|
statusSize.textContent = display;
|
||||||
|
statusSizeDivider.classList.add('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Improved Scroll Sync (DOM position mapping) =====
|
||||||
|
let previewElements = null;
|
||||||
|
let previewPositions = null;
|
||||||
|
|
||||||
|
function cachePreviewPositions() {
|
||||||
|
previewElements = preview.children;
|
||||||
|
previewPositions = [];
|
||||||
|
for (let i = 0; i < previewElements.length; i++) {
|
||||||
|
previewPositions.push(previewElements[i].offsetTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function syncScroll() {
|
function syncScroll() {
|
||||||
if (viewMode !== 'split') return;
|
if (viewMode !== 'split') return;
|
||||||
|
|
||||||
// Calculate which line is at the top of the editor viewport
|
// Map editor scroll to preview using line-to-DOM correlation
|
||||||
const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8;
|
const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8;
|
||||||
const scrollTop = editor.scrollTop;
|
const scrollTop = editor.scrollTop;
|
||||||
const topLine = Math.floor(scrollTop / lineHeight);
|
const topLine = Math.floor(scrollTop / lineHeight);
|
||||||
|
|
||||||
// Calculate corresponding scroll position in preview
|
|
||||||
const previewChildren = preview.children;
|
|
||||||
if (!previewChildren.length) return;
|
|
||||||
|
|
||||||
// Find the element that corresponds to the current top line
|
|
||||||
// Approximate: map editor line ratio to preview element positions
|
|
||||||
const totalLines = editor.value.split('\n').length;
|
const totalLines = editor.value.split('\n').length;
|
||||||
const ratio = totalLines > 0 ? topLine / totalLines : 0;
|
|
||||||
const targetScrollTop = ratio * (previewPanel.scrollHeight - previewPanel.clientHeight);
|
if (!previewPositions || previewPositions.length === 0) {
|
||||||
previewPanel.scrollTop = targetScrollTop;
|
cachePreviewPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previewPositions || previewPositions.length === 0) return;
|
||||||
|
|
||||||
|
// Find which preview element corresponds to the current editor line
|
||||||
|
// Use a rough heuristic: map line ratio to element ratio
|
||||||
|
const ratio = totalLines > 1 ? topLine / (totalLines - 1) : 0;
|
||||||
|
const targetIndex = Math.min(
|
||||||
|
Math.floor(ratio * previewPositions.length),
|
||||||
|
previewPositions.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) {
|
||||||
|
previewPanel.scrollTop = previewPositions[targetIndex];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Update Preview =====
|
// ===== Update Preview =====
|
||||||
@@ -122,6 +197,8 @@
|
|||||||
const text = editor.value;
|
const text = editor.value;
|
||||||
renderMarkdown(text);
|
renderMarkdown(text);
|
||||||
updateLineNumbers();
|
updateLineNumbers();
|
||||||
|
updateFileSize();
|
||||||
|
cachePreviewPositions();
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,35 +215,55 @@
|
|||||||
return filePath.split(/[/\\]/).pop();
|
return filePath.split(/[/\\]/).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes < 1024) return bytes + ' B';
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateFileStats(filePath) {
|
||||||
|
if (typeof window.electronAPI !== 'undefined' && filePath) {
|
||||||
|
const stats = await window.electronAPI.getFileStats(filePath);
|
||||||
|
if (stats.success) {
|
||||||
|
statusSize.textContent = formatBytes(stats.size);
|
||||||
|
statusSizeDivider.classList.add('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setEditorContent(text, filePath) {
|
function setEditorContent(text, filePath) {
|
||||||
editor.value = text;
|
editor.value = text;
|
||||||
currentContent = text;
|
currentContent = text;
|
||||||
currentFilePath = filePath || null;
|
currentFilePath = filePath || null;
|
||||||
isModified = false;
|
isModified = false;
|
||||||
|
lineCountCache = 0;
|
||||||
updateLineNumbers();
|
updateLineNumbers();
|
||||||
renderMarkdown(text);
|
renderMarkdown(text);
|
||||||
updateCursorPosition();
|
updateCursorPosition();
|
||||||
|
updateFileSize();
|
||||||
|
cachePreviewPositions();
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
document.title = `MarkLite - ${getFileName(filePath)}`;
|
document.title = `MarkLite - ${getFileName(filePath)}`;
|
||||||
statusText.textContent = getFileName(filePath);
|
statusText.textContent = getFileName(filePath);
|
||||||
|
updateFileStats(filePath);
|
||||||
} else {
|
} else {
|
||||||
document.title = 'MarkLite - 未命名';
|
document.title = 'MarkLite - 未命名';
|
||||||
statusText.textContent = '未命名';
|
statusText.textContent = '未命名';
|
||||||
|
statusSize.textContent = '';
|
||||||
|
statusSizeDivider.classList.remove('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide welcome screen
|
|
||||||
welcomeScreen.classList.add('hidden');
|
welcomeScreen.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOpenFile() {
|
async function handleOpenFile() {
|
||||||
if (typeof window.electronAPI !== 'undefined') {
|
if (typeof window.electronAPI !== 'undefined') {
|
||||||
const result = await window.electronAPI.openFile();
|
const result = await window.electronAPI.openFile();
|
||||||
if (result) {
|
if (result && !result.error) {
|
||||||
setEditorContent(result.content, result.filePath);
|
setEditorContent(result.content, result.filePath);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: use file input
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.accept = '.md,.markdown,.txt';
|
input.accept = '.md,.markdown,.txt';
|
||||||
@@ -200,7 +297,6 @@
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: download file
|
|
||||||
const blob = new Blob([editor.value], { type: 'text/markdown' });
|
const blob = new Blob([editor.value], { type: 'text/markdown' });
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
@@ -230,15 +326,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== View Mode =====
|
// ===== View Mode (with localStorage) =====
|
||||||
function setViewMode(mode) {
|
function setViewMode(mode) {
|
||||||
viewMode = mode;
|
viewMode = mode;
|
||||||
const app = document.getElementById('app');
|
const app = document.getElementById('app');
|
||||||
|
|
||||||
// Remove all mode classes
|
|
||||||
app.classList.remove('mode-editor', 'mode-preview');
|
app.classList.remove('mode-editor', 'mode-preview');
|
||||||
|
|
||||||
// Update button states
|
|
||||||
btnSplit.classList.remove('active');
|
btnSplit.classList.remove('active');
|
||||||
btnEditor.classList.remove('active');
|
btnEditor.classList.remove('active');
|
||||||
btnPreview.classList.remove('active');
|
btnPreview.classList.remove('active');
|
||||||
@@ -254,16 +348,25 @@
|
|||||||
case 'preview':
|
case 'preview':
|
||||||
btnPreview.classList.add('active');
|
btnPreview.classList.add('active');
|
||||||
app.classList.add('mode-preview');
|
app.classList.add('mode-preview');
|
||||||
// Update preview when switching to preview mode
|
|
||||||
renderMarkdown(editor.value);
|
renderMarkdown(editor.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSetting('viewMode', mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Resizer =====
|
// ===== Resizer (with localStorage) =====
|
||||||
let isResizing = false;
|
let isResizing = false;
|
||||||
|
|
||||||
function initResizer() {
|
function initResizer() {
|
||||||
|
// Restore saved ratio
|
||||||
|
const settings = loadSettings();
|
||||||
|
if (settings.splitRatio) {
|
||||||
|
const ratio = settings.splitRatio;
|
||||||
|
editorPanel.style.flex = `0 0 ${ratio}%`;
|
||||||
|
previewPanel.style.flex = `0 0 ${100 - ratio}%`;
|
||||||
|
}
|
||||||
|
|
||||||
resizer.addEventListener('mousedown', (e) => {
|
resizer.addEventListener('mousedown', (e) => {
|
||||||
isResizing = true;
|
isResizing = true;
|
||||||
resizer.classList.add('active');
|
resizer.classList.add('active');
|
||||||
@@ -279,6 +382,7 @@
|
|||||||
const clamped = Math.max(20, Math.min(80, percentage));
|
const clamped = Math.max(20, Math.min(80, percentage));
|
||||||
editorPanel.style.flex = `0 0 ${clamped}%`;
|
editorPanel.style.flex = `0 0 ${clamped}%`;
|
||||||
previewPanel.style.flex = `0 0 ${100 - clamped}%`;
|
previewPanel.style.flex = `0 0 ${100 - clamped}%`;
|
||||||
|
saveSetting('splitRatio', clamped);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
@@ -325,10 +429,9 @@
|
|||||||
const files = e.dataTransfer.files;
|
const files = e.dataTransfer.files;
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
const filePath = file.path; // Electron provides the real file path
|
const filePath = file.path;
|
||||||
|
|
||||||
if (filePath && typeof window.electronAPI !== 'undefined') {
|
if (filePath && typeof window.electronAPI !== 'undefined') {
|
||||||
// Use IPC to read file through main process (tracks currentFilePath)
|
|
||||||
const result = await window.electronAPI.readFile(filePath);
|
const result = await window.electronAPI.readFile(filePath);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditorContent(result.content, filePath);
|
setEditorContent(result.content, filePath);
|
||||||
@@ -336,7 +439,6 @@
|
|||||||
statusText.textContent = '打开失败: ' + result.error;
|
statusText.textContent = '打开失败: ' + result.error;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: use FileReader (browser mode)
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (ev) => {
|
reader.onload = (ev) => {
|
||||||
setEditorContent(ev.target.result, file.name);
|
setEditorContent(ev.target.result, file.name);
|
||||||
@@ -350,32 +452,26 @@
|
|||||||
// ===== Keyboard Shortcuts =====
|
// ===== Keyboard Shortcuts =====
|
||||||
function initKeyboard() {
|
function initKeyboard() {
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
// Ctrl+O: Open
|
|
||||||
if (e.ctrlKey && e.key === 'o') {
|
if (e.ctrlKey && e.key === 'o') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleOpenFile();
|
handleOpenFile();
|
||||||
}
|
}
|
||||||
// Ctrl+S: Save
|
|
||||||
if (e.ctrlKey && e.key === 's' && !e.shiftKey) {
|
if (e.ctrlKey && e.key === 's' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSave();
|
handleSave();
|
||||||
}
|
}
|
||||||
// Ctrl+Shift+S: Save As
|
|
||||||
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
|
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSaveAs();
|
handleSaveAs();
|
||||||
}
|
}
|
||||||
// Ctrl+1: Split view
|
|
||||||
if (e.ctrlKey && e.key === '1') {
|
if (e.ctrlKey && e.key === '1') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setViewMode('split');
|
setViewMode('split');
|
||||||
}
|
}
|
||||||
// Ctrl+2: Editor only
|
|
||||||
if (e.ctrlKey && e.key === '2') {
|
if (e.ctrlKey && e.key === '2') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setViewMode('editor');
|
setViewMode('editor');
|
||||||
}
|
}
|
||||||
// Ctrl+3: Preview only
|
|
||||||
if (e.ctrlKey && e.key === '3') {
|
if (e.ctrlKey && e.key === '3') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setViewMode('preview');
|
setViewMode('preview');
|
||||||
@@ -383,14 +479,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Tab key support in editor (preserves undo history) =====
|
// ===== Tab key support =====
|
||||||
function initEditorTab() {
|
function initEditorTab() {
|
||||||
editor.addEventListener('keydown', (e) => {
|
editor.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Use insertText command to preserve undo/redo history
|
|
||||||
if (!document.execCommand('insertText', false, ' ')) {
|
if (!document.execCommand('insertText', false, ' ')) {
|
||||||
// Fallback for environments where execCommand is unsupported
|
|
||||||
const start = editor.selectionStart;
|
const start = editor.selectionStart;
|
||||||
const end = editor.selectionEnd;
|
const end = editor.selectionEnd;
|
||||||
editor.setRangeText(' ', start, end, 'end');
|
editor.setRangeText(' ', start, end, 'end');
|
||||||
@@ -400,36 +494,88 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== External File Modification =====
|
||||||
|
function initFileWatch() {
|
||||||
|
if (typeof window.electronAPI === 'undefined') return;
|
||||||
|
|
||||||
|
window.electronAPI.onExternalModification((filePath) => {
|
||||||
|
modifiedBanner.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
btnReload.addEventListener('click', async () => {
|
||||||
|
const result = await window.electronAPI.reloadFile();
|
||||||
|
if (result.success) {
|
||||||
|
editor.value = result.content;
|
||||||
|
currentContent = result.content;
|
||||||
|
currentFilePath = result.filePath;
|
||||||
|
isModified = false;
|
||||||
|
lineCountCache = 0;
|
||||||
|
updateLineNumbers();
|
||||||
|
renderMarkdown(result.content);
|
||||||
|
updateCursorPosition();
|
||||||
|
updateFileSize();
|
||||||
|
cachePreviewPositions();
|
||||||
|
}
|
||||||
|
modifiedBanner.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDismiss.addEventListener('click', () => {
|
||||||
|
modifiedBanner.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Unsaved Changes Warning =====
|
||||||
|
function initUnsavedWarning() {
|
||||||
|
if (typeof window.electronAPI === 'undefined') {
|
||||||
|
// Browser fallback
|
||||||
|
window.addEventListener('beforeunload', (e) => {
|
||||||
|
if (isModified) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Electron: handle close confirmation from main process
|
||||||
|
window.electronAPI.onConfirmClose(() => {
|
||||||
|
if (!isModified) {
|
||||||
|
window.electronAPI.forceClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a simple confirm dialog
|
||||||
|
const shouldClose = confirm('文件尚未保存,确定要关闭吗?');
|
||||||
|
if (shouldClose) {
|
||||||
|
window.electronAPI.forceClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Event Listeners =====
|
// ===== Event Listeners =====
|
||||||
function initEventListeners() {
|
function initEventListeners() {
|
||||||
// Editor input
|
|
||||||
editor.addEventListener('input', () => {
|
editor.addEventListener('input', () => {
|
||||||
setModified(true);
|
setModified(true);
|
||||||
scheduleUpdate();
|
scheduleUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Editor scroll sync
|
|
||||||
editor.addEventListener('scroll', syncScroll);
|
editor.addEventListener('scroll', syncScroll);
|
||||||
|
|
||||||
// Editor cursor position
|
|
||||||
editor.addEventListener('click', updateCursorPosition);
|
editor.addEventListener('click', updateCursorPosition);
|
||||||
editor.addEventListener('keyup', updateCursorPosition);
|
editor.addEventListener('keyup', updateCursorPosition);
|
||||||
editor.addEventListener('select', updateCursorPosition);
|
editor.addEventListener('select', updateCursorPosition);
|
||||||
|
|
||||||
// Toolbar buttons
|
|
||||||
btnOpen.addEventListener('click', handleOpenFile);
|
btnOpen.addEventListener('click', handleOpenFile);
|
||||||
btnSave.addEventListener('click', handleSave);
|
btnSave.addEventListener('click', handleSave);
|
||||||
btnSplit.addEventListener('click', () => setViewMode('split'));
|
btnSplit.addEventListener('click', () => setViewMode('split'));
|
||||||
btnEditor.addEventListener('click', () => setViewMode('editor'));
|
btnEditor.addEventListener('click', () => setViewMode('editor'));
|
||||||
btnPreview.addEventListener('click', () => setViewMode('preview'));
|
btnPreview.addEventListener('click', () => setViewMode('preview'));
|
||||||
|
|
||||||
// Welcome buttons
|
|
||||||
btnWelcomeOpen.addEventListener('click', handleOpenFile);
|
btnWelcomeOpen.addEventListener('click', handleOpenFile);
|
||||||
btnWelcomeNew.addEventListener('click', () => {
|
btnWelcomeNew.addEventListener('click', () => {
|
||||||
setEditorContent('', null);
|
setEditorContent('', null);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Electron IPC events
|
|
||||||
if (typeof window.electronAPI !== 'undefined') {
|
if (typeof window.electronAPI !== 'undefined') {
|
||||||
window.electronAPI.onFileOpened((data) => {
|
window.electronAPI.onFileOpened((data) => {
|
||||||
setEditorContent(data.content, data.filePath);
|
setEditorContent(data.content, data.filePath);
|
||||||
@@ -452,16 +598,21 @@
|
|||||||
// ===== Initialize =====
|
// ===== Initialize =====
|
||||||
function init() {
|
function init() {
|
||||||
initMarked();
|
initMarked();
|
||||||
|
initDarkMode();
|
||||||
initResizer();
|
initResizer();
|
||||||
initDragDrop();
|
initDragDrop();
|
||||||
initKeyboard();
|
initKeyboard();
|
||||||
initEditorTab();
|
initEditorTab();
|
||||||
initEventListeners();
|
initEventListeners();
|
||||||
|
initFileWatch();
|
||||||
|
initUnsavedWarning();
|
||||||
updateLineNumbers();
|
updateLineNumbers();
|
||||||
setViewMode('split');
|
|
||||||
|
// Restore saved view mode
|
||||||
|
const settings = loadSettings();
|
||||||
|
setViewMode(settings.viewMode || 'split');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run when DOM is ready
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+82
-1
@@ -27,6 +27,24 @@
|
|||||||
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace;
|
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Dark Theme ===== */
|
||||||
|
:root.dark {
|
||||||
|
--primary: #8ab4f8;
|
||||||
|
--primary-light: #1a3a5c;
|
||||||
|
--primary-dark: #aecbfa;
|
||||||
|
--bg: #1e1e1e;
|
||||||
|
--bg-secondary: #252526;
|
||||||
|
--bg-tertiary: #2d2d2d;
|
||||||
|
--text: #d4d4d4;
|
||||||
|
--text-secondary: #9e9e9e;
|
||||||
|
--text-tertiary: #6e6e6e;
|
||||||
|
--border: #3e3e3e;
|
||||||
|
--border-light: #333333;
|
||||||
|
--code-bg: #2d2d2d;
|
||||||
|
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
@@ -50,12 +68,14 @@ html, body {
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-left {
|
.toolbar-left,
|
||||||
|
.toolbar-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@@ -98,6 +118,55 @@ html, body {
|
|||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Modified Banner ===== */
|
||||||
|
#modified-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border-bottom: 1px solid #ffc107;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #856404;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark #modified-banner {
|
||||||
|
background: #3a3000;
|
||||||
|
border-bottom-color: #665500;
|
||||||
|
color: #ffd54f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modified-banner.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-btn {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: #856404;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .banner-btn {
|
||||||
|
border-color: #665500;
|
||||||
|
color: #ffd54f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-btn:hover {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .banner-btn:hover {
|
||||||
|
background: #665500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Main Content ===== */
|
/* ===== Main Content ===== */
|
||||||
#main-content {
|
#main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -298,6 +367,10 @@ html, body {
|
|||||||
color: #e83e8c;
|
color: #e83e8c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root.dark .markdown-body code {
|
||||||
|
color: #f48fb1;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown-body pre {
|
.markdown-body pre {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -370,6 +443,14 @@ html, body {
|
|||||||
color: var(--border);
|
color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-size-divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-size-divider.visible {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Drop Overlay ===== */
|
/* ===== Drop Overlay ===== */
|
||||||
#drop-overlay {
|
#drop-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user