diff --git a/main.js b/main.js
index 2ffb10b..4fbab82 100644
--- a/main.js
+++ b/main.js
@@ -1,13 +1,14 @@
-const { app, BrowserWindow, dialog, ipcMain, Menu } = require('electron');
+const { app, BrowserWindow, dialog, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
let mainWindow = null;
let currentFilePath = null;
-let pendingFilePath = null; // For file association race condition
+let pendingFilePath = null;
+let fileWatcher = null;
+let isModifiedExternally = false;
function createWindow() {
- // Create the browser window
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
@@ -23,7 +24,6 @@ function createWindow() {
show: false
});
- // Register did-finish-load BEFORE loadFile to avoid race condition
mainWindow.webContents.on('did-finish-load', () => {
if (pendingFilePath) {
openFile(pendingFilePath);
@@ -31,102 +31,48 @@ function createWindow() {
}
});
- // Load the index.html
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
- // Show window when ready
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
- // Remove native menu bar
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', () => {
+ stopWatching();
mainWindow = null;
});
}
-function buildMenu() {
- const template = [
- {
- label: '文件',
- submenu: [
- {
- label: '打开文件',
- accelerator: 'CmdOrCtrl+O',
- click: () => handleOpenFile()
- },
- {
- 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'
- });
- }
- }
- ]
- }
- ];
+// File watcher - detect external modifications
+function startWatching(filePath) {
+ stopWatching();
+ try {
+ isModifiedExternally = false;
+ fileWatcher = fs.watch(filePath, (eventType) => {
+ if (eventType === 'change' && mainWindow && !mainWindow.isDestroyed()) {
+ isModifiedExternally = true;
+ mainWindow.webContents.send('file:externallyModified', filePath);
+ }
+ });
+ } catch (err) {
+ // Silently ignore watch errors
+ }
+}
- const menu = Menu.buildFromTemplate(template);
- Menu.setApplicationMenu(menu);
+function stopWatching() {
+ if (fileWatcher) {
+ fileWatcher.close();
+ fileWatcher = null;
+ }
}
async function showOpenDialog() {
@@ -143,17 +89,11 @@ async function showOpenDialog() {
return null;
}
-async function handleOpenFile() {
- const filePath = await showOpenDialog();
- if (filePath) {
- openFile(filePath);
- }
-}
-
function openFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
currentFilePath = filePath;
+ startWatching(filePath);
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
mainWindow.webContents.send('file:opened', { filePath, content });
} catch (err) {
@@ -163,14 +103,19 @@ function openFile(filePath) {
// IPC Handlers
ipcMain.handle('dialog:openFile', async () => {
- const filePath = await showOpenDialog();
- if (filePath) {
- const content = fs.readFileSync(filePath, 'utf-8');
- currentFilePath = filePath;
- mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
- return { filePath, content };
+ try {
+ const filePath = await showOpenDialog();
+ if (filePath) {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ currentFilePath = filePath;
+ startWatching(filePath);
+ mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
+ return { filePath, content };
+ }
+ return null;
+ } catch (err) {
+ return { error: err.message };
}
- return null;
});
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 }) => {
try {
if (filePath) {
+ stopWatching();
fs.writeFileSync(filePath, content, 'utf-8');
currentFilePath = filePath;
+ startWatching(filePath);
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
return { success: true, filePath };
} else {
- // No file path, do save as
const result = await dialog.showSaveDialog(mainWindow, {
filters: [
{ name: 'Markdown 文件', extensions: ['md'] },
@@ -198,8 +144,10 @@ ipcMain.handle('file:save', async (event, { filePath, content }) => {
]
});
if (!result.canceled) {
+ stopWatching();
fs.writeFileSync(result.filePath, content, 'utf-8');
currentFilePath = result.filePath;
+ startWatching(result.filePath);
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
return { success: true, filePath: result.filePath };
}
@@ -219,8 +167,10 @@ ipcMain.handle('file:saveAs', async (event, { content }) => {
]
});
if (!result.canceled) {
+ stopWatching();
fs.writeFileSync(result.filePath, content, 'utf-8');
currentFilePath = result.filePath;
+ startWatching(result.filePath);
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
return { success: true, filePath: result.filePath };
}
@@ -234,6 +184,36 @@ ipcMain.handle('file:getCurrentPath', () => {
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
function getCommandLineFile() {
const args = process.argv.slice(1);
@@ -248,28 +228,23 @@ function getCommandLineFile() {
// App lifecycle
app.whenReady().then(() => {
- // Register open-file handler BEFORE creating window (fixes race condition)
app.on('open-file', (event, filePath) => {
event.preventDefault();
if (mainWindow && mainWindow.webContents.isLoading()) {
- // Window is still loading, queue the file
pendingFilePath = filePath;
} else if (mainWindow) {
openFile(filePath);
} else {
- // Window not created yet, store for later
pendingFilePath = filePath;
}
});
createWindow();
- // Check for file passed via command line
const fileToOpen = getCommandLineFile();
if (fileToOpen) {
pendingFilePath = fileToOpen;
}
- // pendingFilePath will be opened by the did-finish-load handler in createWindow()
});
app.on('window-all-closed', () => {
diff --git a/preload.js b/preload.js
index 29faeea..f043511 100644
--- a/preload.js
+++ b/preload.js
@@ -7,6 +7,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
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'),
+
+ // Window control
+ forceClose: () => ipcRenderer.invoke('window:forceClose'),
// Menu events
onFileOpened: (callback) => ipcRenderer.on('file:opened', (event, data) => callback(data)),
@@ -14,6 +19,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
onMenuSaveAs: (callback) => ipcRenderer.on('menu:saveAs', () => callback()),
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
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
});
diff --git a/renderer/index.html b/renderer/index.html
index c65a81c..41cd540 100644
--- a/renderer/index.html
+++ b/renderer/index.html
@@ -3,7 +3,7 @@
-
+
MarkLite
@@ -50,6 +50,31 @@
预览
+
+
+
+
+
+ 文件已被外部程序修改
+
+
@@ -81,6 +106,8 @@
|
Markdown
|
+
+ |
行 1, 列 1
diff --git a/renderer/renderer.js b/renderer/renderer.js
index 4d06a23..d0ebfb7 100644
--- a/renderer/renderer.js
+++ b/renderer/renderer.js
@@ -8,8 +8,11 @@
const lineNumbers = document.getElementById('line-numbers');
const welcomeScreen = document.getElementById('welcome-screen');
const dropOverlay = document.getElementById('drop-overlay');
+ const modifiedBanner = document.getElementById('modified-banner');
const statusText = document.getElementById('status-text');
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 editorPanel = document.getElementById('editor-panel');
const previewPanel = document.getElementById('preview-panel');
@@ -23,18 +26,58 @@
const btnPreview = document.getElementById('btn-preview');
const btnWelcomeOpen = document.getElementById('btn-welcome-open');
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 =====
let currentFilePath = null;
let currentContent = '';
- let viewMode = 'split'; // split, editor, preview
+ let viewMode = 'split';
let isModified = false;
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 =====
function initMarked() {
if (typeof marked !== 'undefined') {
- // Custom renderer for code block highlighting (compatible with marked v12+)
const renderer = new marked.Renderer();
renderer.code = function ({ text, lang }) {
let highlighted = text;
@@ -72,15 +115,17 @@
}
}
- // ===== Line Numbers =====
+ // ===== Line Numbers (optimized: batch innerHTML) =====
function updateLineNumbers() {
- const lines = editor.value.split('\n');
- const count = lines.length;
- let html = '';
+ const count = editor.value.split('\n').length;
+ if (count === lineCountCache) return;
+ lineCountCache = count;
+
+ const fragment = [];
for (let i = 1; i <= count; i++) {
- html += '' + i + '
';
+ fragment.push('', i, '
');
}
- lineNumbers.innerHTML = html;
+ lineNumbers.innerHTML = fragment.join('');
}
// ===== Cursor Position =====
@@ -94,25 +139,55 @@
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() {
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 scrollTop = editor.scrollTop;
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 ratio = totalLines > 0 ? topLine / totalLines : 0;
- const targetScrollTop = ratio * (previewPanel.scrollHeight - previewPanel.clientHeight);
- previewPanel.scrollTop = targetScrollTop;
+
+ if (!previewPositions || previewPositions.length === 0) {
+ 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 =====
@@ -122,6 +197,8 @@
const text = editor.value;
renderMarkdown(text);
updateLineNumbers();
+ updateFileSize();
+ cachePreviewPositions();
}, 150);
}
@@ -138,35 +215,55 @@
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) {
editor.value = text;
currentContent = text;
currentFilePath = filePath || null;
isModified = false;
+ lineCountCache = 0;
updateLineNumbers();
renderMarkdown(text);
updateCursorPosition();
+ updateFileSize();
+ cachePreviewPositions();
if (filePath) {
document.title = `MarkLite - ${getFileName(filePath)}`;
statusText.textContent = getFileName(filePath);
+ updateFileStats(filePath);
} else {
document.title = 'MarkLite - 未命名';
statusText.textContent = '未命名';
+ statusSize.textContent = '';
+ statusSizeDivider.classList.remove('visible');
}
- // Hide welcome screen
welcomeScreen.classList.add('hidden');
}
async function handleOpenFile() {
if (typeof window.electronAPI !== 'undefined') {
const result = await window.electronAPI.openFile();
- if (result) {
+ if (result && !result.error) {
setEditorContent(result.content, result.filePath);
}
} else {
- // Fallback: use file input
const input = document.createElement('input');
input.type = 'file';
input.accept = '.md,.markdown,.txt';
@@ -200,7 +297,6 @@
}, 2000);
}
} else {
- // Fallback: download file
const blob = new Blob([editor.value], { type: 'text/markdown' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
@@ -230,15 +326,13 @@
}
}
- // ===== View Mode =====
+ // ===== View Mode (with localStorage) =====
function setViewMode(mode) {
viewMode = mode;
const app = document.getElementById('app');
- // Remove all mode classes
app.classList.remove('mode-editor', 'mode-preview');
- // Update button states
btnSplit.classList.remove('active');
btnEditor.classList.remove('active');
btnPreview.classList.remove('active');
@@ -254,16 +348,25 @@
case 'preview':
btnPreview.classList.add('active');
app.classList.add('mode-preview');
- // Update preview when switching to preview mode
renderMarkdown(editor.value);
break;
}
+
+ saveSetting('viewMode', mode);
}
- // ===== Resizer =====
+ // ===== Resizer (with localStorage) =====
let isResizing = false;
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) => {
isResizing = true;
resizer.classList.add('active');
@@ -279,6 +382,7 @@
const clamped = Math.max(20, Math.min(80, percentage));
editorPanel.style.flex = `0 0 ${clamped}%`;
previewPanel.style.flex = `0 0 ${100 - clamped}%`;
+ saveSetting('splitRatio', clamped);
});
document.addEventListener('mouseup', () => {
@@ -325,10 +429,9 @@
const files = e.dataTransfer.files;
if (files.length > 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') {
- // Use IPC to read file through main process (tracks currentFilePath)
const result = await window.electronAPI.readFile(filePath);
if (result.success) {
setEditorContent(result.content, filePath);
@@ -336,7 +439,6 @@
statusText.textContent = '打开失败: ' + result.error;
}
} else {
- // Fallback: use FileReader (browser mode)
const reader = new FileReader();
reader.onload = (ev) => {
setEditorContent(ev.target.result, file.name);
@@ -350,32 +452,26 @@
// ===== Keyboard Shortcuts =====
function initKeyboard() {
document.addEventListener('keydown', (e) => {
- // Ctrl+O: Open
if (e.ctrlKey && e.key === 'o') {
e.preventDefault();
handleOpenFile();
}
- // Ctrl+S: Save
if (e.ctrlKey && e.key === 's' && !e.shiftKey) {
e.preventDefault();
handleSave();
}
- // Ctrl+Shift+S: Save As
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
handleSaveAs();
}
- // Ctrl+1: Split view
if (e.ctrlKey && e.key === '1') {
e.preventDefault();
setViewMode('split');
}
- // Ctrl+2: Editor only
if (e.ctrlKey && e.key === '2') {
e.preventDefault();
setViewMode('editor');
}
- // Ctrl+3: Preview only
if (e.ctrlKey && e.key === '3') {
e.preventDefault();
setViewMode('preview');
@@ -383,14 +479,12 @@
});
}
- // ===== Tab key support in editor (preserves undo history) =====
+ // ===== Tab key support =====
function initEditorTab() {
editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
- // Use insertText command to preserve undo/redo history
if (!document.execCommand('insertText', false, ' ')) {
- // Fallback for environments where execCommand is unsupported
const start = editor.selectionStart;
const end = editor.selectionEnd;
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 =====
function initEventListeners() {
- // Editor input
editor.addEventListener('input', () => {
setModified(true);
scheduleUpdate();
});
- // Editor scroll sync
editor.addEventListener('scroll', syncScroll);
- // Editor cursor position
editor.addEventListener('click', updateCursorPosition);
editor.addEventListener('keyup', updateCursorPosition);
editor.addEventListener('select', updateCursorPosition);
- // Toolbar buttons
btnOpen.addEventListener('click', handleOpenFile);
btnSave.addEventListener('click', handleSave);
btnSplit.addEventListener('click', () => setViewMode('split'));
btnEditor.addEventListener('click', () => setViewMode('editor'));
btnPreview.addEventListener('click', () => setViewMode('preview'));
- // Welcome buttons
btnWelcomeOpen.addEventListener('click', handleOpenFile);
btnWelcomeNew.addEventListener('click', () => {
setEditorContent('', null);
});
- // Electron IPC events
if (typeof window.electronAPI !== 'undefined') {
window.electronAPI.onFileOpened((data) => {
setEditorContent(data.content, data.filePath);
@@ -452,16 +598,21 @@
// ===== Initialize =====
function init() {
initMarked();
+ initDarkMode();
initResizer();
initDragDrop();
initKeyboard();
initEditorTab();
initEventListeners();
+ initFileWatch();
+ initUnsavedWarning();
updateLineNumbers();
- setViewMode('split');
+
+ // Restore saved view mode
+ const settings = loadSettings();
+ setViewMode(settings.viewMode || 'split');
}
- // Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
diff --git a/renderer/style.css b/renderer/style.css
index cc0b971..b96adac 100644
--- a/renderer/style.css
+++ b/renderer/style.css
@@ -27,6 +27,24 @@
--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 {
height: 100%;
font-family: var(--font-ui);
@@ -50,12 +68,14 @@ html, body {
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
+ justify-content: space-between;
padding: 0 8px;
flex-shrink: 0;
z-index: 10;
}
-.toolbar-left {
+.toolbar-left,
+.toolbar-right {
display: flex;
align-items: center;
gap: 2px;
@@ -98,6 +118,55 @@ html, body {
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 {
flex: 1;
@@ -298,6 +367,10 @@ html, body {
color: #e83e8c;
}
+:root.dark .markdown-body code {
+ color: #f48fb1;
+}
+
.markdown-body pre {
margin-top: 0;
margin-bottom: 16px;
@@ -370,6 +443,14 @@ html, body {
color: var(--border);
}
+.status-size-divider {
+ display: none;
+}
+
+.status-size-divider.visible {
+ display: inline;
+}
+
/* ===== Drop Overlay ===== */
#drop-overlay {
position: fixed;