// MarkLite Renderer Process (function () { 'use strict'; // ===== DOM Elements ===== const editor = document.getElementById('editor'); const preview = document.getElementById('preview'); 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'); const mainContent = document.getElementById('main-content'); // Buttons const btnOpen = document.getElementById('btn-open'); const btnSave = document.getElementById('btn-save'); const btnSplit = document.getElementById('btn-split'); const btnEditor = document.getElementById('btn-editor'); 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'; 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') { const renderer = new marked.Renderer(); renderer.code = function ({ text, lang }) { let highlighted = text; if (typeof hljs !== 'undefined') { if (lang && hljs.getLanguage(lang)) { try { highlighted = hljs.highlight(text, { language: lang }).value; } catch (e) { /* fallback */ } } else { try { highlighted = hljs.highlightAuto(text).value; } catch (e) { /* fallback */ } } } const langClass = lang ? ` language-${lang}` : ''; return `
${highlighted}
`; }; marked.setOptions({ gfm: true, breaks: true, pedantic: false }); marked.use({ renderer }); } } // ===== Markdown Rendering ===== function renderMarkdown(text) { if (typeof marked === 'undefined') { preview.innerHTML = '

错误: Markdown 解析库未加载

'; return; } try { const html = marked.parse(text); preview.innerHTML = html; } catch (e) { preview.innerHTML = '

渲染错误: ' + e.message + '

'; } } // ===== Line Numbers (optimized: batch innerHTML) ===== function updateLineNumbers() { const count = editor.value.split('\n').length; if (count === lineCountCache) return; lineCountCache = count; const fragment = []; for (let i = 1; i <= count; i++) { fragment.push('
', i, '
'); } lineNumbers.innerHTML = fragment.join(''); } // ===== Cursor Position ===== function updateCursorPosition() { const text = editor.value; const pos = editor.selectionStart; const textBeforeCursor = text.substring(0, pos); const lines = textBeforeCursor.split('\n'); const line = lines.length; const col = lines[lines.length - 1].length + 1; statusCursor.textContent = `行 ${line}, 列 ${col}`; } // ===== File Size ===== 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'; } function updateFileSize() { const bytes = new Blob([editor.value]).size; statusSize.textContent = formatBytes(bytes); 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; // 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); const totalLines = editor.value.split('\n').length; 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 ===== function scheduleUpdate() { if (updateTimer) clearTimeout(updateTimer); updateTimer = setTimeout(() => { const text = editor.value; renderMarkdown(text); updateLineNumbers(); updateFileSize(); cachePreviewPositions(); }, 150); } // ===== File Operations ===== function setModified(modified) { isModified = modified; const title = currentFilePath ? `MarkLite - ${getFileName(currentFilePath)}${modified ? ' *' : ''}` : `MarkLite - 未命名${modified ? ' *' : ''}`; document.title = title; } function getFileName(filePath) { return filePath.split(/[/\\]/).pop(); } 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'); } welcomeScreen.classList.add('hidden'); } async function handleOpenFile() { if (typeof window.electronAPI !== 'undefined') { const result = await window.electronAPI.openFile(); if (result && !result.error) { setEditorContent(result.content, result.filePath); } } else { const input = document.createElement('input'); input.type = 'file'; input.accept = '.md,.markdown,.txt'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (ev) => { setEditorContent(ev.target.result, file.name); }; reader.readAsText(file); } }; input.click(); } } async function handleSave() { if (typeof window.electronAPI !== 'undefined') { const result = await window.electronAPI.saveFile({ filePath: currentFilePath, content: editor.value }); if (result.success) { currentFilePath = result.filePath; currentContent = editor.value; setModified(false); statusText.textContent = '已保存'; setTimeout(() => { statusText.textContent = getFileName(currentFilePath) || '就绪'; }, 2000); } } else { const blob = new Blob([editor.value], { type: 'text/markdown' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = (currentFilePath || 'untitled.md'); a.click(); URL.revokeObjectURL(a.href); setModified(false); } } async function handleSaveAs() { if (typeof window.electronAPI !== 'undefined') { const result = await window.electronAPI.saveFileAs({ content: editor.value }); if (result.success) { currentFilePath = result.filePath; currentContent = editor.value; setModified(false); statusText.textContent = '已保存'; setTimeout(() => { statusText.textContent = getFileName(currentFilePath) || '就绪'; }, 2000); } } else { handleSave(); } } // ===== View Mode (with localStorage) ===== function setViewMode(mode) { viewMode = mode; const app = document.getElementById('app'); app.classList.remove('mode-editor', 'mode-preview'); btnSplit.classList.remove('active'); btnEditor.classList.remove('active'); btnPreview.classList.remove('active'); switch (mode) { case 'split': btnSplit.classList.add('active'); break; case 'editor': btnEditor.classList.add('active'); app.classList.add('mode-editor'); break; case 'preview': btnPreview.classList.add('active'); app.classList.add('mode-preview'); renderMarkdown(editor.value); break; } saveSetting('viewMode', mode); } // ===== 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'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const containerRect = mainContent.getBoundingClientRect(); const percentage = ((e.clientX - containerRect.left) / containerRect.width) * 100; 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', () => { if (isResizing) { isResizing = false; resizer.classList.remove('active'); document.body.style.cursor = ''; document.body.style.userSelect = ''; } }); } // ===== Drag & Drop ===== function initDragDrop() { let dragCounter = 0; document.addEventListener('dragenter', (e) => { e.preventDefault(); e.stopPropagation(); dragCounter++; dropOverlay.classList.remove('hidden'); }); document.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); dragCounter--; if (dragCounter === 0) { dropOverlay.classList.add('hidden'); } }); document.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); }); document.addEventListener('drop', async (e) => { e.preventDefault(); e.stopPropagation(); dragCounter = 0; dropOverlay.classList.add('hidden'); const files = e.dataTransfer.files; if (files.length > 0) { const file = files[0]; const filePath = file.path; if (filePath && typeof window.electronAPI !== 'undefined') { const result = await window.electronAPI.readFile(filePath); if (result.success) { setEditorContent(result.content, filePath); } else { statusText.textContent = '打开失败: ' + result.error; } } else { const reader = new FileReader(); reader.onload = (ev) => { setEditorContent(ev.target.result, file.name); }; reader.readAsText(file); } } }); } // ===== Keyboard Shortcuts ===== function initKeyboard() { document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'o') { e.preventDefault(); handleOpenFile(); } if (e.ctrlKey && e.key === 's' && !e.shiftKey) { e.preventDefault(); handleSave(); } if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); handleSaveAs(); } if (e.ctrlKey && e.key === '1') { e.preventDefault(); setViewMode('split'); } if (e.ctrlKey && e.key === '2') { e.preventDefault(); setViewMode('editor'); } if (e.ctrlKey && e.key === '3') { e.preventDefault(); setViewMode('preview'); } }); } // ===== Tab key support ===== function initEditorTab() { editor.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); if (!document.execCommand('insertText', false, ' ')) { const start = editor.selectionStart; const end = editor.selectionEnd; editor.setRangeText(' ', start, end, 'end'); } scheduleUpdate(); } }); } // ===== 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.addEventListener('input', () => { setModified(true); scheduleUpdate(); }); editor.addEventListener('scroll', syncScroll); editor.addEventListener('click', updateCursorPosition); editor.addEventListener('keyup', updateCursorPosition); editor.addEventListener('select', updateCursorPosition); btnOpen.addEventListener('click', handleOpenFile); btnSave.addEventListener('click', handleSave); btnSplit.addEventListener('click', () => setViewMode('split')); btnEditor.addEventListener('click', () => setViewMode('editor')); btnPreview.addEventListener('click', () => setViewMode('preview')); btnWelcomeOpen.addEventListener('click', handleOpenFile); btnWelcomeNew.addEventListener('click', () => { setEditorContent('', null); }); if (typeof window.electronAPI !== 'undefined') { window.electronAPI.onFileOpened((data) => { setEditorContent(data.content, data.filePath); }); window.electronAPI.onMenuSave(() => { handleSave(); }); window.electronAPI.onMenuSaveAs(() => { handleSaveAs(); }); window.electronAPI.onViewModeChange((mode) => { setViewMode(mode); }); } } // ===== Initialize ===== function init() { initMarked(); initDarkMode(); initResizer(); initDragDrop(); initKeyboard(); initEditorTab(); initEventListeners(); initFileWatch(); initUnsavedWarning(); updateLineNumbers(); // Restore saved view mode const settings = loadSettings(); setViewMode(settings.viewMode || 'split'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();