全面优化:暗色主题、文件监听、未保存提醒、滚动同步、分屏记忆、文件大小显示、删除死代码

This commit is contained in:
thzxx
2026-05-18 13:09:31 +08:00
parent dbbf1efc78
commit cb29c67f94
5 changed files with 403 additions and 157 deletions
+200 -49
View File
@@ -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 += '<div class="line-num">' + i + '</div>';
fragment.push('<div class="line-num">', i, '</div>');
}
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 {