Initial commit: MarkLite Markdown Reader
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
*.exe
|
||||||
|
*.msi
|
||||||
|
*.dmg
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# MarkLite - 设计文档
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
MarkLite 是一款轻量级的 Windows 本地 Markdown 阅读器桌面应用程序。基于 Electron 框架构建,提供简洁现代的用户界面,支持 Markdown 文件的打开、编辑和实时预览。
|
||||||
|
|
||||||
|
## 2. 技术架构
|
||||||
|
|
||||||
|
### 2.1 技术栈
|
||||||
|
|
||||||
|
| 组件 | 技术选型 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 桌面框架 | Electron | 跨平台桌面应用框架 |
|
||||||
|
| 前端 | HTML + CSS + JavaScript | 原生前端技术,无框架依赖 |
|
||||||
|
| Markdown解析 | marked.js v12.0.2 | 高性能 Markdown 解析库 |
|
||||||
|
| 代码高亮 | highlight.js v11.9.0 | 代码块语法高亮 |
|
||||||
|
| 打包 | electron-builder | 生成 Windows exe 安装包 |
|
||||||
|
|
||||||
|
### 2.2 进程架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Main Process (main.js) │
|
||||||
|
│ - 窗口管理 │
|
||||||
|
│ - 文件系统操作 (fs) │
|
||||||
|
│ - 原生菜单 │
|
||||||
|
│ - IPC 通信主端 │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│ IPC (contextBridge)
|
||||||
|
┌──────────────▼──────────────────────────┐
|
||||||
|
│ Preload Script (preload.js) │
|
||||||
|
│ - 安全的 API 桥接 │
|
||||||
|
│ - exposeInMainWorld │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────▼──────────────────────────┐
|
||||||
|
│ Renderer Process (renderer/) │
|
||||||
|
│ - UI 渲染 │
|
||||||
|
│ - Markdown 编辑与预览 │
|
||||||
|
│ - 拖拽文件处理 │
|
||||||
|
│ - 快捷键处理 │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 功能设计
|
||||||
|
|
||||||
|
### 3.1 核心功能
|
||||||
|
|
||||||
|
1. **文件打开**
|
||||||
|
- 菜单栏 → 文件 → 打开(支持 .md / .txt / .markdown)
|
||||||
|
- 拖拽文件到窗口打开
|
||||||
|
- 支持 Windows 文件关联(双击 .md 文件打开)
|
||||||
|
|
||||||
|
2. **文件保存**
|
||||||
|
- Ctrl+S 快捷键保存
|
||||||
|
- 菜单栏 → 文件 → 保存
|
||||||
|
- 另存为功能
|
||||||
|
|
||||||
|
3. **实时预览**
|
||||||
|
- 左侧编辑器 + 右侧预览(默认模式)
|
||||||
|
- 纯预览模式(隐藏编辑器)
|
||||||
|
- 编辑时实时更新预览
|
||||||
|
|
||||||
|
4. **视图模式**
|
||||||
|
- 编辑+预览(Split View)
|
||||||
|
- 纯编辑模式
|
||||||
|
- 纯预览模式
|
||||||
|
|
||||||
|
### 3.2 UI 设计
|
||||||
|
|
||||||
|
#### 色彩方案
|
||||||
|
- **主色调**: #1a73e8(蓝色)
|
||||||
|
- **背景色**: #ffffff(白色)
|
||||||
|
- **侧边栏**: #f8f9fa(浅灰)
|
||||||
|
- **文字色**: #333333(深灰)
|
||||||
|
- **代码块背景**: #f6f8fa
|
||||||
|
- **边框色**: #e1e4e8
|
||||||
|
|
||||||
|
#### 字体
|
||||||
|
- **UI字体**: system-ui, -apple-system, "Segoe UI", sans-serif
|
||||||
|
- **编辑器字体**: "Cascadia Code", "Fira Code", "Consolas", monospace
|
||||||
|
- **预览字体**: system-ui, -apple-system, "Segoe UI", sans-serif
|
||||||
|
|
||||||
|
#### 布局
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ 📄 MarkLite - filename.md ─ □ ✕ │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ 📁 打开 │ 💾 保存 │ 📝 编辑 │ 👁 预览 │ 分屏 │
|
||||||
|
├─────────────────┬────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ Editor Area │ Preview Area │
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
├─────────────────┴────────────────────────────┤
|
||||||
|
│ 就绪 │ UTF-8 │ Markdown │ 行: 1, 列: 1 │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
MarkLite/
|
||||||
|
├── package.json # 项目配置与依赖
|
||||||
|
├── main.js # Electron 主进程
|
||||||
|
├── preload.js # 预加载脚本(IPC 桥接)
|
||||||
|
├── renderer/
|
||||||
|
│ ├── index.html # 主页面
|
||||||
|
│ ├── style.css # 样式表
|
||||||
|
│ └── renderer.js # 渲染进程逻辑
|
||||||
|
├── lib/
|
||||||
|
│ ├── marked.min.js # Markdown 解析库
|
||||||
|
│ ├── highlight.min.js # 代码高亮库
|
||||||
|
│ └── highlight-github.css # 代码高亮主题
|
||||||
|
├── assets/
|
||||||
|
│ └── icon.ico # 应用图标
|
||||||
|
├── DESIGN.md # 设计文档(本文件)
|
||||||
|
├── README.md # 项目说明
|
||||||
|
└── .gitignore # Git 忽略文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. IPC 通信设计
|
||||||
|
|
||||||
|
| 通道 | 方向 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `dialog:openFile` | Renderer → Main | 打开文件对话框 |
|
||||||
|
| `file:read` | Renderer → Main | 读取文件内容 |
|
||||||
|
| `file:save` | Renderer → Main | 保存文件内容 |
|
||||||
|
| `file:saveAs` | Renderer → Main | 另存为 |
|
||||||
|
| `menu:action` | Main → Renderer | 菜单操作通知 |
|
||||||
|
| `window:setTitle` | Renderer → Main | 设置窗口标题 |
|
||||||
|
|
||||||
|
## 6. Markdown 渲染支持
|
||||||
|
|
||||||
|
支持标准 Markdown 和 GFM(GitHub Flavored Markdown):
|
||||||
|
- 标题(h1-h6)
|
||||||
|
- 段落、换行
|
||||||
|
- **粗体**、*斜体*、~~删除线~~
|
||||||
|
- 有序/无序列表
|
||||||
|
- 任务列表(- [x])
|
||||||
|
- 代码块(围栏式 + 缩进式)
|
||||||
|
- 行内代码
|
||||||
|
- 链接、图片
|
||||||
|
- 表格
|
||||||
|
- 引用块
|
||||||
|
- 水平线
|
||||||
|
- HTML 内联
|
||||||
|
|
||||||
|
## 7. 构建与发布
|
||||||
|
|
||||||
|
使用 `electron-builder` 打包:
|
||||||
|
- 输出格式:NSIS 安装包(.exe)
|
||||||
|
- 目标平台:Windows x64
|
||||||
|
- 应用图标:assets/icon.ico
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# MarkLite
|
||||||
|
|
||||||
|
一款轻量级的 Windows 本地 Markdown 阅读器桌面应用程序。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 📝 Markdown 文件打开与编辑
|
||||||
|
- 👁 实时预览(编辑+预览 / 纯预览模式)
|
||||||
|
- 🎨 现代化简洁 UI 设计
|
||||||
|
- 📂 支持拖拽打开文件
|
||||||
|
- 💾 文件保存功能
|
||||||
|
- 🔤 代码语法高亮
|
||||||
|
- ⌨️ 快捷键支持
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **Electron** - 桌面应用框架
|
||||||
|
- **marked.js** - Markdown 解析
|
||||||
|
- **highlight.js** - 代码高亮
|
||||||
|
- **HTML + CSS + JavaScript** - 前端技术
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发模式
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 打包为 Windows exe 安装包
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快捷键
|
||||||
|
|
||||||
|
| 快捷键 | 功能 |
|
||||||
|
|--------|------|
|
||||||
|
| Ctrl+O | 打开文件 |
|
||||||
|
| Ctrl+S | 保存文件 |
|
||||||
|
| Ctrl+Shift+S | 另存为 |
|
||||||
|
| Ctrl+1 | 编辑+预览模式 |
|
||||||
|
| Ctrl+2 | 纯编辑模式 |
|
||||||
|
| Ctrl+3 | 纯预览模式 |
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
MarkLite/
|
||||||
|
├── package.json # 项目配置
|
||||||
|
├── main.js # Electron 主进程
|
||||||
|
├── preload.js # 预加载脚本
|
||||||
|
├── renderer/ # 渲染进程
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── style.css
|
||||||
|
│ └── renderer.js
|
||||||
|
├── lib/ # 第三方库
|
||||||
|
├── assets/ # 资源文件
|
||||||
|
├── DESIGN.md # 设计文档
|
||||||
|
└── README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||||
|
Theme: GitHub
|
||||||
|
Description: Light theme as seen on github.com
|
||||||
|
Author: github.com
|
||||||
|
Maintainer: @Hirse
|
||||||
|
Updated: 2021-05-15
|
||||||
|
|
||||||
|
Outdated base version: https://github.com/primer/github-syntax-light
|
||||||
|
Current colors taken from GitHub's CSS
|
||||||
|
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
||||||
Vendored
+1213
File diff suppressed because one or more lines are too long
Vendored
+6
File diff suppressed because one or more lines are too long
@@ -0,0 +1,273 @@
|
|||||||
|
const { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let mainWindow = null;
|
||||||
|
let currentFilePath = null;
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// Create the browser window
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 800,
|
||||||
|
minHeight: 600,
|
||||||
|
icon: path.join(__dirname, 'assets', 'icon.ico'),
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false
|
||||||
|
},
|
||||||
|
titleBarStyle: 'default',
|
||||||
|
show: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the index.html
|
||||||
|
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||||
|
|
||||||
|
// Show window when ready
|
||||||
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build menu
|
||||||
|
buildMenu();
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOpenFile() {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [
|
||||||
|
{ name: 'Markdown 文件', extensions: ['md', 'markdown', 'txt'] },
|
||||||
|
{ name: '所有文件', extensions: ['*'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
openFile(result.filePaths[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFile(filePath) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
currentFilePath = filePath;
|
||||||
|
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
||||||
|
mainWindow.webContents.send('file:opened', { filePath, content });
|
||||||
|
} catch (err) {
|
||||||
|
dialog.showErrorBox('错误', `无法读取文件: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC Handlers
|
||||||
|
ipcMain.handle('dialog:openFile', async () => {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [
|
||||||
|
{ name: 'Markdown 文件', extensions: ['md', 'markdown', 'txt'] },
|
||||||
|
{ name: '所有文件', extensions: ['*'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
const filePath = result.filePaths[0];
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
currentFilePath = filePath;
|
||||||
|
mainWindow.setTitle(`MarkLite - ${path.basename(filePath)}`);
|
||||||
|
return { filePath, content };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('file:read', async (event, filePath) => {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
return { success: true, content };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('file:save', async (event, { filePath, content }) => {
|
||||||
|
try {
|
||||||
|
if (filePath) {
|
||||||
|
fs.writeFileSync(filePath, content, 'utf-8');
|
||||||
|
currentFilePath = 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'] },
|
||||||
|
{ name: '文本文件', extensions: ['txt'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if (!result.canceled) {
|
||||||
|
fs.writeFileSync(result.filePath, content, 'utf-8');
|
||||||
|
currentFilePath = result.filePath;
|
||||||
|
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
|
||||||
|
return { success: true, filePath: result.filePath };
|
||||||
|
}
|
||||||
|
return { success: false, canceled: true };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('file:saveAs', async (event, { content }) => {
|
||||||
|
try {
|
||||||
|
const result = await dialog.showSaveDialog(mainWindow, {
|
||||||
|
filters: [
|
||||||
|
{ name: 'Markdown 文件', extensions: ['md'] },
|
||||||
|
{ name: '文本文件', extensions: ['txt'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if (!result.canceled) {
|
||||||
|
fs.writeFileSync(result.filePath, content, 'utf-8');
|
||||||
|
currentFilePath = result.filePath;
|
||||||
|
mainWindow.setTitle(`MarkLite - ${path.basename(result.filePath)}`);
|
||||||
|
return { success: true, filePath: result.filePath };
|
||||||
|
}
|
||||||
|
return { success: false, canceled: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('file:getCurrentPath', () => {
|
||||||
|
return currentFilePath;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle file open from command line or file association
|
||||||
|
function handleFileOpen() {
|
||||||
|
const args = process.argv.slice(1);
|
||||||
|
if (args.length > 0 && !args[0].startsWith('--')) {
|
||||||
|
const filePath = args[0];
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// App lifecycle
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
// Check for file passed via command line
|
||||||
|
const fileToOpen = handleFileOpen();
|
||||||
|
if (fileToOpen) {
|
||||||
|
// Wait for window to be ready
|
||||||
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
|
openFile(fileToOpen);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle macOS open-file event
|
||||||
|
app.on('open-file', (event, filePath) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (mainWindow) {
|
||||||
|
openFile(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"name": "marklite",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Lightweight Markdown Reader for Windows",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron .",
|
||||||
|
"build": "electron-builder --win",
|
||||||
|
"build:portable": "electron-builder --win portable"
|
||||||
|
},
|
||||||
|
"author": "MarkLite",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^28.0.0",
|
||||||
|
"electron-builder": "^24.9.1"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.marklite.app",
|
||||||
|
"productName": "MarkLite",
|
||||||
|
"directories": {
|
||||||
|
"output": "dist"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"main.js",
|
||||||
|
"preload.js",
|
||||||
|
"renderer/**/*",
|
||||||
|
"lib/**/*",
|
||||||
|
"assets/**/*"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "nsis",
|
||||||
|
"arch": ["x64"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": "assets/icon.ico"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "MarkLite"
|
||||||
|
},
|
||||||
|
"fileAssociations": [
|
||||||
|
{
|
||||||
|
"ext": "md",
|
||||||
|
"name": "Markdown",
|
||||||
|
"description": "Markdown File",
|
||||||
|
"icon": "assets/icon.ico"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ext": "markdown",
|
||||||
|
"name": "Markdown",
|
||||||
|
"description": "Markdown File",
|
||||||
|
"icon": "assets/icon.ico"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ext": "txt",
|
||||||
|
"name": "Text",
|
||||||
|
"description": "Text File",
|
||||||
|
"icon": "assets/icon.ico"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// File operations
|
||||||
|
openFile: () => ipcRenderer.invoke('dialog:openFile'),
|
||||||
|
readFile: (filePath) => ipcRenderer.invoke('file:read', filePath),
|
||||||
|
saveFile: (data) => ipcRenderer.invoke('file:save', data),
|
||||||
|
saveFileAs: (data) => ipcRenderer.invoke('file:saveAs', data),
|
||||||
|
getCurrentPath: () => ipcRenderer.invoke('file:getCurrentPath'),
|
||||||
|
|
||||||
|
// Menu events
|
||||||
|
onFileOpened: (callback) => ipcRenderer.on('file:opened', (event, data) => callback(data)),
|
||||||
|
onMenuSave: (callback) => ipcRenderer.on('menu:save', () => callback()),
|
||||||
|
onMenuSaveAs: (callback) => ipcRenderer.on('menu:saveAs', () => callback()),
|
||||||
|
onViewModeChange: (callback) => ipcRenderer.on('menu:viewMode', (event, mode) => callback(mode)),
|
||||||
|
|
||||||
|
// Remove listeners
|
||||||
|
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
|
||||||
|
});
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<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' 'unsafe-inline'; img-src 'self' data: https:;">
|
||||||
|
<title>MarkLite</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="../lib/highlight-github.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div id="toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<button id="btn-open" class="toolbar-btn" title="打开文件 (Ctrl+O)">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>打开</span>
|
||||||
|
</button>
|
||||||
|
<button id="btn-save" class="toolbar-btn" title="保存文件 (Ctrl+S)">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
||||||
|
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
||||||
|
<polyline points="7 3 7 8 15 8"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span>保存</span>
|
||||||
|
</button>
|
||||||
|
<div class="toolbar-divider"></div>
|
||||||
|
<button id="btn-split" class="toolbar-btn active" title="编辑+预览 (Ctrl+1)">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="21"></line>
|
||||||
|
</svg>
|
||||||
|
<span>分屏</span>
|
||||||
|
</button>
|
||||||
|
<button id="btn-editor" class="toolbar-btn" title="纯编辑 (Ctrl+2)">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>编辑</span>
|
||||||
|
</button>
|
||||||
|
<button id="btn-preview" class="toolbar-btn" title="纯预览 (Ctrl+3)">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
<span>预览</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content area -->
|
||||||
|
<div id="main-content">
|
||||||
|
<!-- Editor panel -->
|
||||||
|
<div id="editor-panel">
|
||||||
|
<div id="editor-wrapper">
|
||||||
|
<div id="line-numbers"></div>
|
||||||
|
<textarea id="editor" spellcheck="false" placeholder="在此输入 Markdown 内容,或拖拽 .md 文件到窗口打开..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resizer -->
|
||||||
|
<div id="resizer"></div>
|
||||||
|
|
||||||
|
<!-- Preview panel -->
|
||||||
|
<div id="preview-panel">
|
||||||
|
<div id="preview" class="markdown-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<div id="statusbar">
|
||||||
|
<div class="status-left">
|
||||||
|
<span id="status-text">就绪</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-right">
|
||||||
|
<span id="status-encoding">UTF-8</span>
|
||||||
|
<span class="status-divider">|</span>
|
||||||
|
<span id="status-lang">Markdown</span>
|
||||||
|
<span class="status-divider">|</span>
|
||||||
|
<span id="status-cursor">行 1, 列 1</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drop overlay -->
|
||||||
|
<div id="drop-overlay" class="hidden">
|
||||||
|
<div class="drop-content">
|
||||||
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<p>释放文件以打开</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Welcome screen -->
|
||||||
|
<div id="welcome-screen">
|
||||||
|
<div class="welcome-content">
|
||||||
|
<div class="welcome-icon">
|
||||||
|
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="10" y="5" width="80" height="90" rx="8" fill="#f0f6ff" stroke="#1a73e8" stroke-width="2"/>
|
||||||
|
<text x="50" y="45" text-anchor="middle" fill="#1a73e8" font-family="system-ui" font-weight="bold" font-size="32">M↓</text>
|
||||||
|
<text x="50" y="70" text-anchor="middle" fill="#5f6368" font-family="system-ui" font-size="12">MarkLite</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1>欢迎使用 MarkLite</h1>
|
||||||
|
<p>一款轻量级的 Markdown 阅读器</p>
|
||||||
|
<div class="welcome-actions">
|
||||||
|
<button id="btn-welcome-open" class="welcome-btn primary">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
打开文件
|
||||||
|
</button>
|
||||||
|
<button id="btn-welcome-new" class="welcome-btn secondary">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
新建文件
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="welcome-tips">
|
||||||
|
<p>💡 提示:可以直接拖拽 .md 文件到窗口打开</p>
|
||||||
|
<p>⌨️ 快捷键:Ctrl+O 打开 | Ctrl+S 保存 | Ctrl+1/2/3 切换视图</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../lib/marked.min.js"></script>
|
||||||
|
<script src="../lib/highlight.min.js"></script>
|
||||||
|
<script src="renderer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
// 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 statusText = document.getElementById('status-text');
|
||||||
|
const statusCursor = document.getElementById('status-cursor');
|
||||||
|
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');
|
||||||
|
|
||||||
|
// ===== State =====
|
||||||
|
let currentFilePath = null;
|
||||||
|
let currentContent = '';
|
||||||
|
let viewMode = 'split'; // split, editor, preview
|
||||||
|
let isModified = false;
|
||||||
|
let updateTimer = null;
|
||||||
|
|
||||||
|
// ===== Initialize marked.js =====
|
||||||
|
function initMarked() {
|
||||||
|
if (typeof marked !== 'undefined') {
|
||||||
|
marked.setOptions({
|
||||||
|
gfm: true,
|
||||||
|
breaks: true,
|
||||||
|
pedantic: false,
|
||||||
|
highlight: function (code, lang) {
|
||||||
|
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return hljs.highlight(code, { language: lang }).value;
|
||||||
|
} catch (e) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Auto detect if no language specified
|
||||||
|
if (typeof hljs !== 'undefined') {
|
||||||
|
try {
|
||||||
|
return hljs.highlightAuto(code).value;
|
||||||
|
} catch (e) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Markdown Rendering =====
|
||||||
|
function renderMarkdown(text) {
|
||||||
|
if (typeof marked === 'undefined') {
|
||||||
|
preview.innerHTML = '<p style="color: red;">错误: Markdown 解析库未加载</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const html = marked.parse(text);
|
||||||
|
preview.innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
preview.innerHTML = '<p style="color: red;">渲染错误: ' + e.message + '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Line Numbers =====
|
||||||
|
function updateLineNumbers() {
|
||||||
|
const lines = editor.value.split('\n');
|
||||||
|
const count = lines.length;
|
||||||
|
let html = '';
|
||||||
|
for (let i = 1; i <= count; i++) {
|
||||||
|
html += '<div class="line-num">' + i + '</div>';
|
||||||
|
}
|
||||||
|
lineNumbers.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Sync Scroll =====
|
||||||
|
function syncScroll() {
|
||||||
|
if (viewMode !== 'split') return;
|
||||||
|
const editorEl = editor;
|
||||||
|
const previewEl = previewPanel;
|
||||||
|
const scrollPercent = editorEl.scrollTop / (editorEl.scrollHeight - editorEl.clientHeight);
|
||||||
|
previewEl.scrollTop = scrollPercent * (previewEl.scrollHeight - previewEl.clientHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Update Preview =====
|
||||||
|
function scheduleUpdate() {
|
||||||
|
if (updateTimer) clearTimeout(updateTimer);
|
||||||
|
updateTimer = setTimeout(() => {
|
||||||
|
const text = editor.value;
|
||||||
|
renderMarkdown(text);
|
||||||
|
updateLineNumbers();
|
||||||
|
}, 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorContent(text, filePath) {
|
||||||
|
editor.value = text;
|
||||||
|
currentContent = text;
|
||||||
|
currentFilePath = filePath || null;
|
||||||
|
isModified = false;
|
||||||
|
updateLineNumbers();
|
||||||
|
renderMarkdown(text);
|
||||||
|
updateCursorPosition();
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
document.title = `MarkLite - ${getFileName(filePath)}`;
|
||||||
|
statusText.textContent = getFileName(filePath);
|
||||||
|
} else {
|
||||||
|
document.title = 'MarkLite - 未命名';
|
||||||
|
statusText.textContent = '未命名';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide welcome screen
|
||||||
|
welcomeScreen.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOpenFile() {
|
||||||
|
if (typeof window.electronAPI !== 'undefined') {
|
||||||
|
const result = await window.electronAPI.openFile();
|
||||||
|
if (result) {
|
||||||
|
setEditorContent(result.content, result.filePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: use file input
|
||||||
|
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 {
|
||||||
|
// Fallback: download file
|
||||||
|
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 =====
|
||||||
|
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');
|
||||||
|
|
||||||
|
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');
|
||||||
|
// Update preview when switching to preview mode
|
||||||
|
renderMarkdown(editor.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Resizer =====
|
||||||
|
let isResizing = false;
|
||||||
|
|
||||||
|
function initResizer() {
|
||||||
|
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}%`;
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
dragCounter++;
|
||||||
|
dropOverlay.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter--;
|
||||||
|
if (dragCounter === 0) {
|
||||||
|
dropOverlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter = 0;
|
||||||
|
dropOverlay.classList.add('hidden');
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
setEditorContent(ev.target.result, file.name);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Tab key support in editor =====
|
||||||
|
function initEditorTab() {
|
||||||
|
editor.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
const start = editor.selectionStart;
|
||||||
|
const end = editor.selectionEnd;
|
||||||
|
const value = editor.value;
|
||||||
|
editor.value = value.substring(0, start) + ' ' + value.substring(end);
|
||||||
|
editor.selectionStart = editor.selectionEnd = start + 4;
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onMenuSave(() => {
|
||||||
|
handleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onMenuSaveAs(() => {
|
||||||
|
handleSaveAs();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onViewModeChange((mode) => {
|
||||||
|
setViewMode(mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Initialize =====
|
||||||
|
function init() {
|
||||||
|
initMarked();
|
||||||
|
initResizer();
|
||||||
|
initDragDrop();
|
||||||
|
initKeyboard();
|
||||||
|
initEditorTab();
|
||||||
|
initEventListeners();
|
||||||
|
updateLineNumbers();
|
||||||
|
setViewMode('split');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,548 @@
|
|||||||
|
/* ===== Reset & Base ===== */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #1a73e8;
|
||||||
|
--primary-light: #e8f0fe;
|
||||||
|
--primary-dark: #1557b0;
|
||||||
|
--bg: #ffffff;
|
||||||
|
--bg-secondary: #f8f9fa;
|
||||||
|
--bg-tertiary: #f1f3f4;
|
||||||
|
--text: #333333;
|
||||||
|
--text-secondary: #5f6368;
|
||||||
|
--text-tertiary: #9aa0a6;
|
||||||
|
--border: #e1e4e8;
|
||||||
|
--border-light: #f0f0f0;
|
||||||
|
--code-bg: #f6f8fa;
|
||||||
|
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
--radius: 6px;
|
||||||
|
--toolbar-height: 44px;
|
||||||
|
--statusbar-height: 28px;
|
||||||
|
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Toolbar ===== */
|
||||||
|
#toolbar {
|
||||||
|
height: var(--toolbar-height);
|
||||||
|
background: var(--bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.active {
|
||||||
|
background: var(--primary-light);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--border);
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Main Content ===== */
|
||||||
|
#main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Editor Panel ===== */
|
||||||
|
#editor-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#line-numbers {
|
||||||
|
width: 50px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-right: 1px solid var(--border-light);
|
||||||
|
padding: 12px 0;
|
||||||
|
text-align: right;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#line-numbers .line-num {
|
||||||
|
padding-right: 12px;
|
||||||
|
height: 20.8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
tab-size: 4;
|
||||||
|
overflow-y: auto;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor::placeholder {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Resizer ===== */
|
||||||
|
#resizer {
|
||||||
|
width: 4px;
|
||||||
|
background: var(--border);
|
||||||
|
cursor: col-resize;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resizer:hover,
|
||||||
|
#resizer.active {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Preview Panel ===== */
|
||||||
|
#preview-panel {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview {
|
||||||
|
padding: 24px 32px;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Markdown Body Styles ===== */
|
||||||
|
.markdown-body {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h3 { font-size: 1.25em; }
|
||||||
|
.markdown-body h4 { font-size: 1em; }
|
||||||
|
.markdown-body h5 { font-size: 0.875em; }
|
||||||
|
.markdown-body h6 { font-size: 0.85em; color: var(--text-secondary); }
|
||||||
|
|
||||||
|
.markdown-body p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body strong { font-weight: 600; }
|
||||||
|
|
||||||
|
.markdown-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
height: 2px;
|
||||||
|
background: var(--border);
|
||||||
|
border: none;
|
||||||
|
margin: 24px 0;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 4px 16px;
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 0 var(--radius) var(--radius) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ul,
|
||||||
|
.markdown-body ol {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li + li {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.9em;
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e83e8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre code {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th,
|
||||||
|
.markdown-body table td {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th {
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table tr:nth-child(even) {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body input[type="checkbox"] {
|
||||||
|
margin-right: 6px;
|
||||||
|
accent-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Status Bar ===== */
|
||||||
|
#statusbar {
|
||||||
|
height: var(--statusbar-height);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-left,
|
||||||
|
.status-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-divider {
|
||||||
|
color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Drop Overlay ===== */
|
||||||
|
#drop-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(26, 115, 232, 0.1);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
border: 3px dashed var(--primary);
|
||||||
|
margin: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#drop-overlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-content {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-content svg {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-content p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Welcome Screen ===== */
|
||||||
|
#welcome-screen {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#welcome-screen.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 480px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-icon {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content > p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn.primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn.primary:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn.secondary {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-btn.secondary:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tips {
|
||||||
|
text-align: left;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px 20px;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tips p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tips p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== View Modes ===== */
|
||||||
|
#app.mode-editor #preview-panel,
|
||||||
|
#app.mode-editor #resizer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app.mode-preview #editor-panel,
|
||||||
|
#app.mode-preview #resizer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Scrollbar ===== */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Animations ===== */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user