commit 5fafcd28e35ac3d67cf6ca50465e0a0efa10516e Author: Odboy Date: Sat Jun 13 21:08:00 2026 +0800 feat: 初始化 kenaito-notebook 桌面记事本项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f24c7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/bin +node_modules +frontend/dist +server/config.properties + +# 编译产物 +kenaito-notebook diff --git a/README.md b/README.md new file mode 100644 index 0000000..76f85d7 --- /dev/null +++ b/README.md @@ -0,0 +1,450 @@ +# Kenaito Notebook + +一个基于 Go + Wails + Vue 2 + Gin Server 的多层级嵌套 Markdown 笔记应用。 + +## 📖 项目简介 + +Kenaito Notebook 是一款轻量级、高性能的 Markdown 笔记应用,支持无限层级嵌套的文件夹结构,帮助你高效组织和管理笔记内容。采用前后端分离架构,Wails 桌面客户端通过 REST API 与独立的 Gin 服务端通信。支持用户注册登录,笔记数据按用户隔离。 + +## 🏗️ 架构概览 + +``` +┌─────────────────────────────────┐ +│ Wails 桌面客户端 │ +│ app.go (HTTP 客户端 + JWT) │ +│ + Wails 原生功能 (文件对话框等) │ +│ Vue 2 + Element UI + mavon │ +└──────────┬──────────────────────┘ + │ HTTP REST API (Bearer Token) + ▼ +┌─────────────────────────────────┐ +│ Server (Gin) :8080 │ +│ /api/auth/* (公开) │ +│ /api/notes/* (需登录) │ +│ /api/files/* (需登录) │ +│ /api/export/* (需登录) │ +├─────────────────────────────────┤ +│ AuthService (bcrypt + JWT) │ +│ Database (SQLite) │ +│ FileService │ +│ ExportService │ +└─────────────────────────────────┘ +``` + +## 🛠️ 技术栈 + +| 层级 | 技术 | 版本 | +|--------------|-----------------------------|---------| +| 桌面框架 | Wails | v2.12.0 | +| 客户端后端 | Go | 1.25+ | +| 服务端框架 | Gin | v1.10.0 | +| 数据库 | SQLite (modernc.org/sqlite) | v1.53.0 | +| 密码哈希 | bcrypt (golang.org/x/crypto)| v0.x | +| 前端 | Vue | 2.7.x | +| UI 框架 | Element UI | 2.15.x | +| Markdown 编辑 | mavon-editor | 2.10.x | +| Markdown 解析 | goldmark | v1.8.x | + +## ✨ 功能特性 + +### 用户系统 + +- 🔐 **用户注册/登录** - 用户名 + 密码 + 邮箱注册,JWT Token 认证 +- 🔑 **安全存储** - 密码 bcrypt 加密,Token 7 天有效期 +- 🚪 **登录/注销** - 优雅的登录界面,一键注销 + +### 核心功能 + +- 📁 **多层级嵌套** - 支持无限层级的文件夹和笔记组织 +- ✏️ **Markdown 编辑** - 实时预览、语法高亮、工具栏快捷操作 +- 🔍 **全文搜索** - 支持标题和内容的模糊搜索 +- 🏷️ **标签管理** - 为笔记添加自定义标签分类 +- 💾 **自动保存** - 编辑内容 500ms 防抖自动保存 +- 🗑️ **回收站** - 软删除 + 恢复 + 永久删除 + +### 导出功能 + +- 📤 **HTML 导出** - 生成格式完整的 HTML 页面 +- 📝 **Markdown 导出** - 导出原始 Markdown 文件 +- 📦 **ZIP 导出** - 文件夹/全部笔记打包导出 + +### Markdown 增强 + +- 🎨 **代码高亮** - 支持多种编程语言的语法高亮 +- 📊 **表格支持** - Markdown 表格渲染 +- 🖼️ **图片上传** - 支持粘贴和上传图片,本地文件服务存储 +- 🔗 **链接处理** - 自动识别和处理链接 +- 📐 **数学公式** - KaTeX 数学公式支持 + +## 📦 安装与使用 + +### 环境要求 + +- **Go** >= 1.25 +- **Node.js** >= 18 +- **Wails CLI** v2.x + +### 安装 Wails CLI + +```bash +go install github.com/wailsapp/wails/v2/cmd/wails@latest +``` + +### 克隆项目 + +```bash +git clone +cd kenaito-notebook +``` + +### 安装依赖 + +```bash +# 安装客户端前端依赖 +cd frontend && npm install && cd .. + +# 安装 Server 依赖(会自动下载) +cd server && go mod tidy && cd .. +``` + +### 启动 + +> ⚠️ 必须先启动 Server,再启动 Wails 客户端。 + +```bash +# 1. 启动 Server(默认监听 :8080) +cd server +go run . + +# 2. 新开终端,启动 Wails 客户端 +cd kenaito-notebook +wails dev +``` + +### 构建生产版本 + +```bash +# 构建 Server +cd server && go build -o kenaito-notebook-server . && cd .. + +# 构建 Wails 客户端 +wails build +``` + +构建产物: + +``` +server/kenaito-notebook-server # Server 可执行文件 +build/bin/kenaito-notebook.app # macOS 客户端 +build/bin/kenaito-notebook.exe # Windows 客户端 +build/bin/kenaito-notebook # Linux 客户端 +``` + +## 📁 项目结构 + +``` +kenaito-notebook/ +├── app.go # Wails 客户端 (HTTP 客户端 + 原生功能) +├── main.go # Wails 入口 +├── go.mod # 客户端模块 (仅依赖 wails/v2) +├── wails.json # Wails 项目配置 +│ +├── models/ # 共享数据模型 +│ └── note.go # Note / NoteTree 定义 +│ +├── server/ # 独立服务端 (Gin) +│ ├── main.go # Gin 启动入口 (路由注册 + 认证中间件) +│ ├── config.go # 配置管理 +│ ├── go.mod # 服务端模块 +│ ├── models/ # 数据模型 +│ │ ├── note.go # Note / NoteTree +│ │ └── user.go # User 用户模型 +│ ├── services/ # 业务服务层 +│ │ ├── database.go # SQLite 数据库服务 (笔记 + 用户表) +│ │ ├── fileservice.go # 文件/图片上传服务 +│ │ ├── export.go # 导出服务 (HTML/Markdown/ZIP) +│ │ └── auth.go # 认证服务 (bcrypt + JWT) +│ └── handlers/ # REST API 处理器 +│ ├── note.go # 笔记 CRUD API +│ ├── file.go # 文件上传 API +│ ├── export.go # 导出 API +│ └── auth.go # 认证 API (注册/登录/中间件) +│ +├── frontend/ # 前端项目 +│ ├── index.html +│ ├── package.json +│ ├── vite.config.js +│ └── src/ +│ ├── main.js +│ ├── App.vue # 主组件 (登录态判断 + 布局) +│ ├── style.css +│ └── components/ +│ ├── LoginView.vue # 登录/注册页面 +│ ├── NoteTree.vue # 树形导航组件 +│ └── TrashView.vue # 回收站视图 +│ +├── build/ # 构建输出 +│ └── bin/ +│ +└── README.md +``` + +## 🔧 数据存储 + +### 数据库位置 + +SQLite 数据库和资源文件存储在用户主目录下: + +``` +~/.kenaito-notebook/ +├── notes.db # SQLite 数据库 +└── assets/ # 图片资源 + └── 2026/06/ # 按日期分组 + ├── a1b2c3d4.png + └── e5f6g7h8.jpg +``` + +### 数据库表结构 + +```sql +-- 用户表 +CREATE TABLE users +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL, + hashed_password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX idx_users_username ON users (username); + +-- 笔记表 +CREATE TABLE notes +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parent_id INTEGER DEFAULT 0, -- 父级 ID,0 为根目录 + title TEXT NOT NULL DEFAULT '', + content TEXT DEFAULT '', + sort_order INTEGER DEFAULT 0, + is_folder BOOLEAN DEFAULT FALSE, + tag TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + deleted_at DATETIME DEFAULT NULL -- 软删除标记 +); + +CREATE INDEX idx_notes_parent_id ON notes (parent_id); +CREATE INDEX idx_notes_sort_order ON notes (sort_order); +CREATE INDEX idx_notes_deleted_at ON notes (deleted_at); +``` + +## 📡 API 文档 + +### Server REST API + +Server 默认运行在 `http://localhost:8080`,所有端点前缀为 `/api`。 + +#### 认证 + +| 方法 | 路径 | 请求体 | 返回 | 说明 | +|------|---------------------|------------------------------------------|----------------|----------| +| POST | `/api/auth/register`| `{username, password, email}` | `{token}` | 用户注册 | +| POST | `/api/auth/login` | `{username, password}` | `{token}` | 用户登录 | +| GET | `/api/auth/me` | - (需 Bearer Token) | `{id, username, email, created_at}` | 获取当前用户 | + +> 🔒 除 `/api/auth/register` 和 `/api/auth/login` 外,所有 API 需在请求头中携带 `Authorization: Bearer `。 + +#### 笔记管理 + +| 方法 | 路径 | 请求体 | 返回 | 说明 | +|--------|--------------------------|-----------------------------------------------|---------------|----------| +| GET | `/api/notes/tree` | - | `[]NoteTree` | 获取笔记树 | +| GET | `/api/notes/:id` | - | `Note` | 获取单个笔记 | +| POST | `/api/notes` | `{parentId, title, isFolder}` | `Note` | 创建笔记/文件夹 | +| PUT | `/api/notes/:id` | `{title, content, tag}` | `{message}` | 更新笔记 | +| DELETE | `/api/notes/:id` | - | `{message}` | 软删除(移入回收站)| +| GET | `/api/notes/search?q=xxx`| - | `[]Note` | 搜索笔记 | + +#### 回收站 + +| 方法 | 路径 | 返回 | 说明 | +|--------|-----------------------|-------------|----------| +| GET | `/api/trash` | `[]Note` | 获取回收站 | +| POST | `/api/trash/:id/restore`| `{message}` | 恢复笔记 | +| DELETE | `/api/trash/:id` | `{message}` | 永久删除 | +| DELETE | `/api/trash` | `{message}` | 清空回收站 | + +#### 文件管理 + +| 方法 | 路径 | 请求体 | 返回 | 说明 | +|------|----------------------|--------------------------|-----------|---------| +| POST | `/api/files/image` | `{data: base64}` | `{url}` | 上传图片 | +| POST | `/api/files/upload` | `{data: base64, filename}`| `{url}` | 上传文件 | +| GET | `/api/files/server-url`| - | `{url}` | 文件服务器地址 | +| GET | `/api/files/assets-path`| - | `{path}` | 资源目录路径 | + +#### 导出 + +| 方法 | 路径 | 请求体 | 返回 | 说明 | +|------|---------------------------------|--------------------------|-------------|-----------| +| POST | `/api/export/html` | `{title, content}` | `{filePath}`| 导出 HTML | +| POST | `/api/export/markdown` | `{title, content}` | `{filePath}`| 导出 Markdown| +| POST | `/api/export/folder-zip/:folderId`| - | `{filePath}`| 文件夹导出 ZIP | +| POST | `/api/export/all-zip` | - | `{filePath}`| 全部导出 ZIP | + +### Wails 客户端 API + +Wails 客户端通过 `window.go.main.App.*` 调用,方法名与 Server API 对应: + +```javascript +// 用户认证 +const token = await window.go.main.App.Register(username, password, email) +const token = await window.go.main.App.Login(username, password) +window.go.main.App.Logout() +const user = await window.go.main.App.GetUser() +const loggedIn = await window.go.main.App.IsLoggedIn() + +// 获取笔记树 +const tree = await window.go.main.App.GetNoteTree() + +// 获取单个笔记 +const note = await window.go.main.App.GetNote(noteId) + +// 创建笔记 +await window.go.main.App.CreateNote(parentId, title, isFolder) + +// 更新笔记 +await window.go.main.App.UpdateNote(id, title, content, tag) + +// 删除笔记 +await window.go.main.App.DeleteNote(id) + +// 搜索笔记 +const results = await window.go.main.App.SearchNotes(keyword) + +// 保存图片 +const url = await window.go.main.App.SaveImage(base64Data) + +// 导出 +await window.go.main.App.ExportHTML(title, content) +await window.go.main.App.ExportMarkdown(title, content) +``` + +## 🎨 界面说明 + +### 登录/注册页面 + +首次打开应用会显示登录/注册页面,支持: +- 用户名 + 密码登录 +- 用户名 + 密码 + 邮箱注册 +- 登录/注册模式一键切换 +- 表单验证(用户名长度、密码长度、邮箱格式、确认密码) + +### 主界面布局 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 📝 Kenaito Notebook 👤 username [退出] │ +├─────────────┬───────────────────────────────────────────────┤ +│ 🔍 搜索... │ 笔记标题 │ +├─────────────┤ [导出 ▼] │ +│ [新建笔记] │ [标签] │ +│ [新建文件夹] │ ┌─────────────────────────────────────────────┐ +├─────────────┤ │ │ +│ 📁 文件夹 │ │ Markdown 编辑器 │ +│ 📄 笔记1 │ │ (实时预览) │ +│ 📁 子文件夹│ │ │ +│ 📄 笔记2│ │ │ +│ 📄 笔记3 │ │ │ +│ │ └─────────────────────────────────────────────┘ +└─────────────┴───────────────────────────────────────────────┘ +``` + +### 操作说明 + +| 操作 | 方式 | +|----------|--------------| +| 登录/注册 | 首页自动显示 | +| 选择笔记 | 单击笔记项 | +| 展开/收起文件夹 | 单击文件夹左侧箭头 | +| 新建笔记 | 点击「新建笔记」按钮 | +| 新建文件夹 | 点击「新建文件夹」按钮 | +| 在文件夹内新建 | 右键文件夹 → 选择操作 | +| 删除笔记 | 右键笔记 → 删除 | +| 搜索 | 在搜索框输入关键词 | +| 导出 | 点击右上角「导出」按钮 | +| 退出登录 | 点击右上角「退出」按钮 | + +## 🚀 开发指南 + +### 开发环境配置 + +1. **安装 Go**: https://golang.org/dl/ +2. **安装 Node.js**: https://nodejs.org/ +3. **安装 Wails**: + ```bash + go install github.com/wailsapp/wails/v2/cmd/wails@latest + ``` + +### 添加新功能 + +#### 添加 Server API + +1. 在 `server/handlers/` 中添加处理器 +2. 在 `server/main.go` 的路由组中注册路由(公开接口放 `auth` 组,需登录接口放 `protected` 组) +3. 在 `app.go` 中添加对应的 HTTP 调用方法 +4. 在 `frontend/wailsjs/go/main/App.js` 和 `App.d.ts` 中添加绑定声明 +5. 前端通过 `window.go.main.App.MethodName()` 调用 + +#### 认证机制 + +- 注册:POST `/api/auth/register`,返回 JWT Token,自动保存到客户端 +- 登录:POST `/api/auth/login`,返回 JWT Token,自动保存到客户端 +- 客户端 `doRequest()` 自动在请求头中添加 `Authorization: Bearer ` +- Server 中间件校验 Token 有效性,无效/过期返回 401 +- Token 使用 HMAC-SHA256 签名,有效期 7 天 + +### 代码规范 + +- Go 代码遵循 `gofmt` 标准 +- Vue 代码使用 ESLint 格式化 +- 提交信息使用中文描述 + +## 🐛 常见问题 + +### Q: 数据库文件在哪里? + +A: 数据库存储在 `~/.kenaito-notebook/notes.db` + +### Q: 如何备份笔记? + +A: 备份 `~/.kenaito-notebook/` 目录即可 + +### Q: 图片上传后存储在哪里? + +A: 图片存储在 `~/.kenaito-notebook/assets/` 目录,按日期分组 + +### Q: Server 启动失败怎么办? + +A: 确认 8080 端口未被占用。可通过环境变量 `SERVER_PORT=9090` 修改端口。 + +### Q: Wails 客户端连接不上 Server? + +A: 确保 Server 已启动。默认连接 `http://localhost:8080`。 + +### Q: 忘记密码了怎么办? + +A: 目前暂不支持密码重置功能,请重新注册账号。 + +## 📄 许可证 + +MIT License + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 📮 联系方式 + +如有问题或建议,请提交 Issue。 diff --git a/app.go b/app.go new file mode 100644 index 0000000..26c1605 --- /dev/null +++ b/app.go @@ -0,0 +1,530 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + stdruntime "runtime" + "time" + + "github.com/wailsapp/wails/v2/pkg/runtime" + + "kenaito-notebook/models" +) + +// 配置后端地址 +const defaultServerURL = "http://localhost:8080" + +type App struct { + ctx context.Context + serverURL string + http *http.Client + token string +} + +func NewApp() *App { + return &App{ + serverURL: defaultServerURL, + http: &http.Client{Timeout: 30 * time.Second}, + } +} + +func (a *App) startup(ctx context.Context) { + a.ctx = ctx + log.Println("[App] Starting up, server:", a.serverURL) +} + +func (a *App) shutdown(ctx context.Context) { + log.Println("[App] Shutting down") +} + +// doRequest 发起 HTTP 请求的通用方法 +func (a *App) doRequest(method, path string, body interface{}) ([]byte, error) { + var bodyReader io.Reader + if body != nil { + data, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(data) + } + + req, err := http.NewRequest(method, a.serverURL+path, bodyReader) + if err != nil { + return nil, err + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if a.token != "" { + req.Header.Set("Authorization", "Bearer "+a.token) + } + + resp, err := a.http.Do(req) + if err != nil { + return nil, fmt.Errorf("server connection failed: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode >= 400 { + var errResp struct { + Error string `json:"error"` + } + if json.Unmarshal(data, &errResp) == nil && errResp.Error != "" { + return nil, fmt.Errorf(errResp.Error) + } + return nil, fmt.Errorf("server returned %d", resp.StatusCode) + } + + return data, nil +} + +// Register 用户注册 +func (a *App) Register(email, password, code string) (string, error) { + body := map[string]string{ + "email": email, + "password": password, + "code": code, + } + data, err := a.doRequest("POST", "/api/auth/register", body) + if err != nil { + return "", err + } + var resp struct { + Token string `json:"token"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", err + } + a.token = resp.Token + return resp.Token, nil +} + +// SendVerifyCode 发送验证码 +func (a *App) SendVerifyCode(email, purpose string) error { + body := map[string]string{ + "email": email, + "purpose": purpose, + } + _, err := a.doRequest("POST", "/api/auth/send-code", body) + return err +} + +// VerifyCode 验证验证码 +func (a *App) VerifyCode(email, code, purpose string) error { + body := map[string]string{ + "email": email, + "code": code, + "purpose": purpose, + } + _, err := a.doRequest("POST", "/api/auth/verify-code", body) + return err +} + +// ResetPassword 重置密码 +func (a *App) ResetPassword(email, code, newPassword string) error { + body := map[string]string{ + "email": email, + "code": code, + "newPassword": newPassword, + } + _, err := a.doRequest("POST", "/api/auth/reset-password", body) + return err +} + +// Login 用户登录 +func (a *App) Login(email, password string) (string, error) { + body := map[string]string{ + "email": email, + "password": password, + } + data, err := a.doRequest("POST", "/api/auth/login", body) + if err != nil { + return "", err + } + var resp struct { + Token string `json:"token"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", err + } + a.token = resp.Token + return resp.Token, nil +} + +// Logout 退出登录 +func (a *App) Logout() { + a.token = "" +} + +// GetUser 获取当前用户信息 +func (a *App) GetUser() (map[string]interface{}, error) { + data, err := a.doRequest("GET", "/api/auth/me", nil) + if err != nil { + return nil, err + } + var user map[string]interface{} + if err := json.Unmarshal(data, &user); err != nil { + return nil, err + } + return user, nil +} + +// IsLoggedIn 是否已登录 +func (a *App) IsLoggedIn() bool { + return a.token != "" +} + +// GetNoteTree 获取笔记树 +func (a *App) GetNoteTree() ([]*models.NoteTree, error) { + data, err := a.doRequest("GET", "/api/notes/tree", nil) + if err != nil { + return nil, err + } + var result []*models.NoteTree + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return result, nil +} + +// GetNote 获取单个笔记 +func (a *App) GetNote(id int64) (*models.Note, error) { + data, err := a.doRequest("GET", fmt.Sprintf("/api/notes/%d", id), nil) + if err != nil { + return nil, err + } + var note models.Note + if err := json.Unmarshal(data, ¬e); err != nil { + return nil, err + } + return ¬e, nil +} + +// CreateNote 创建笔记或文件夹 +func (a *App) CreateNote(parentID int64, title string, isFolder bool) (*models.Note, error) { + body := map[string]interface{}{ + "parentId": parentID, + "title": title, + "isFolder": isFolder, + } + data, err := a.doRequest("POST", "/api/notes", body) + if err != nil { + return nil, err + } + var note models.Note + if err := json.Unmarshal(data, ¬e); err != nil { + return nil, err + } + return ¬e, nil +} + +// UpdateNote 更新笔记 +func (a *App) UpdateNote(id int64, title, content, tag string) error { + body := map[string]string{ + "title": title, + "content": content, + "tag": tag, + } + _, err := a.doRequest("PUT", fmt.Sprintf("/api/notes/%d", id), body) + return err +} + +// DeleteNote 删除笔记 +func (a *App) DeleteNote(id int64) error { + _, err := a.doRequest("DELETE", fmt.Sprintf("/api/notes/%d", id), nil) + return err +} + +// SearchNotes 搜索笔记 +func (a *App) SearchNotes(keyword string) ([]*models.Note, error) { + data, err := a.doRequest("GET", fmt.Sprintf("/api/notes/search?q=%s", keyword), nil) + if err != nil { + return nil, err + } + var notes []*models.Note + if err := json.Unmarshal(data, ¬es); err != nil { + return nil, err + } + return notes, nil +} + +// GetTrashNotes 获取回收站中的笔记 +func (a *App) GetTrashNotes() ([]*models.Note, error) { + data, err := a.doRequest("GET", "/api/trash", nil) + if err != nil { + return nil, err + } + var notes []*models.Note + if err := json.Unmarshal(data, ¬es); err != nil { + return nil, err + } + return notes, nil +} + +// RestoreNote 恢复笔记 +func (a *App) RestoreNote(id int64) error { + _, err := a.doRequest("POST", fmt.Sprintf("/api/trash/%d/restore", id), nil) + return err +} + +// PermanentDeleteNote 永久删除笔记 +func (a *App) PermanentDeleteNote(id int64) error { + _, err := a.doRequest("DELETE", fmt.Sprintf("/api/trash/%d", id), nil) + return err +} + +// EmptyTrash 清空回收站 +func (a *App) EmptyTrash() error { + _, err := a.doRequest("DELETE", "/api/trash", nil) + return err +} + +// SaveImage 保存图片 +func (a *App) SaveImage(base64Data string) (string, error) { + body := map[string]string{"data": base64Data} + data, err := a.doRequest("POST", "/api/files/image", body) + if err != nil { + return "", err + } + var resp struct { + URL string `json:"url"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", err + } + return resp.URL, nil +} + +// SaveFile 保存任意文件 +func (a *App) SaveFile(base64Data, filename string) (string, error) { + body := map[string]string{ + "data": base64Data, + "filename": filename, + } + data, err := a.doRequest("POST", "/api/files/upload", body) + if err != nil { + return "", err + } + var resp struct { + URL string `json:"url"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", err + } + return resp.URL, nil +} + +// GetFileServerURL 获取文件服务器地址 +func (a *App) GetFileServerURL() string { + data, err := a.doRequest("GET", "/api/files/server-url", nil) + if err != nil { + return "" + } + var resp struct { + URL string `json:"url"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "" + } + return resp.URL +} + +// GetAssetsPath 获取资源路径 +func (a *App) GetAssetsPath() string { + data, err := a.doRequest("GET", "/api/files/assets-path", nil) + if err != nil { + return "" + } + var resp struct { + Path string `json:"path"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "" + } + return resp.Path +} + +// SelectDirectory 弹出目录选择对话框 +func (a *App) SelectDirectory(title string) (string, error) { + result, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{ + Title: title, + }) + if err != nil { + return "", err + } + if result == "" { + return "", fmt.Errorf("用户取消选择") + } + return result, nil +} + +// SelectSaveFile 弹出文件保存对话框 +func (a *App) SelectSaveFile(title, defaultFilename string) (string, error) { + result, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: title, + DefaultFilename: defaultFilename, + }) + if err != nil { + return "", err + } + if result == "" { + return "", fmt.Errorf("用户取消保存") + } + return result, nil +} + +// RevealInFinder 在 Finder 中显示文件 +func (a *App) RevealInFinder(path string) { + var cmd *exec.Cmd + switch stdruntime.GOOS { + case "darwin": + cmd = exec.Command("open", "-R", path) + case "windows": + cmd = exec.Command("explorer", "/select,", path) + default: + cmd = exec.Command("xdg-open", filepath.Dir(path)) + } + cmd.Start() +} + +// ExportHTML 导出 HTML +func (a *App) ExportHTML(title, content string) (string, error) { + body := map[string]string{"title": title, "content": content} + data, err := a.doRequest("POST", "/api/export/html", body) + if err != nil { + return "", err + } + var resp struct { + FilePath string `json:"filePath"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", err + } + + defaultFilename := title + ".html" + savePath, err := a.SelectSaveFile("导出 HTML 文件", defaultFilename) + if err != nil { + return "", err + } + if filepath.Ext(savePath) != ".html" { + savePath += ".html" + } + + if err := copyFile(resp.FilePath, savePath); err != nil { + return "", err + } + return savePath, nil +} + +// ExportMarkdown 导出 Markdown +func (a *App) ExportMarkdown(title, content string) (string, error) { + body := map[string]string{"title": title, "content": content} + data, err := a.doRequest("POST", "/api/export/markdown", body) + if err != nil { + return "", err + } + var resp struct { + FilePath string `json:"filePath"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", err + } + + defaultFilename := title + ".md" + savePath, err := a.SelectSaveFile("导出 Markdown 文件", defaultFilename) + if err != nil { + return "", err + } + if filepath.Ext(savePath) != ".md" { + savePath += ".md" + } + + if err := copyFile(resp.FilePath, savePath); err != nil { + return "", err + } + return savePath, nil +} + +// ExportFolderMarkdown 导出整个文件夹为 ZIP +func (a *App) ExportFolderMarkdown(folderID int64) (string, error) { + data, err := a.doRequest("POST", fmt.Sprintf("/api/export/folder-zip/%d", folderID), nil) + if err != nil { + return "", err + } + var resp struct { + FilePath string `json:"filePath"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", err + } + + savePath, err := a.SelectSaveFile("导出文件夹为 ZIP", "export.zip") + if err != nil { + return "", err + } + if filepath.Ext(savePath) != ".zip" { + savePath += ".zip" + } + + if err := copyFile(resp.FilePath, savePath); err != nil { + return "", err + } + return savePath, nil +} + +// ExportAllMarkdown 导出所有笔记为 ZIP +func (a *App) ExportAllMarkdown() (string, error) { + data, err := a.doRequest("POST", "/api/export/all-zip", nil) + if err != nil { + return "", err + } + var resp struct { + FilePath string `json:"filePath"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", err + } + + savePath, err := a.SelectSaveFile("导出所有笔记为 ZIP", "所有笔记.zip") + if err != nil { + return "", err + } + if filepath.Ext(savePath) != ".zip" { + savePath += ".zip" + } + + if err := copyFile(resp.FilePath, savePath); err != nil { + return "", err + } + return savePath, nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..1ae2f67 --- /dev/null +++ b/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/build/appicon.png differ diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist new file mode 100644 index 0000000..14121ef --- /dev/null +++ b/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist new file mode 100644 index 0000000..d17a747 --- /dev/null +++ b/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000..f334798 Binary files /dev/null and b/build/windows/icon.ico differ diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/build/windows/installer/project.nsi b/build/windows/installer/project.nsi new file mode 100644 index 0000000..654ae2e --- /dev/null +++ b/build/windows/installer/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/installer/wails_tools.nsh new file mode 100644 index 0000000..2f6d321 --- /dev/null +++ b/build/windows/installer/wails_tools.nsh @@ -0,0 +1,249 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest new file mode 100644 index 0000000..17e1a23 --- /dev/null +++ b/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/data/notes.db b/data/notes.db new file mode 100644 index 0000000..e69de29 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..b4719be --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,8 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..0dc68e4 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2399 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "element-ui": "^2.15.14", + "highlight.js": "^11.9.0", + "marked": "^9.1.6", + "mavon-editor": "^2.10.4", + "vue": "^2.7.14" + }, + "devDependencies": { + "@vitejs/plugin-vue2": "^2.3.1", + "vite": "^3.0.7", + "vite-plugin-vue2": "^2.0.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@vitejs/plugin-vue2": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue2/-/plugin-vue2-2.3.4.tgz", + "integrity": "sha512-LgqtRRedJb1KdmgcllwGX0gtlPvOvtR6pITXmqxGwQhBZaAysg0Hd7wvj3sjCsj4+PENWsqS7O+ceYSOgJ+H9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >= 16.0.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^2.7.0-0" + } + }, + "node_modules/@vue/babel-helper-vue-jsx-merge-props": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", + "integrity": "sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-transform-vue-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.4.0.tgz", + "integrity": "sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "html-tags": "^2.0.0", + "lodash.kebabcase": "^4.1.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-preset-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.4.0.tgz", + "integrity": "sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "@vue/babel-sugar-composition-api-inject-h": "^1.4.0", + "@vue/babel-sugar-composition-api-render-instance": "^1.4.0", + "@vue/babel-sugar-functional-vue": "^1.4.0", + "@vue/babel-sugar-inject-h": "^1.4.0", + "@vue/babel-sugar-v-model": "^1.4.0", + "@vue/babel-sugar-v-on": "^1.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "vue": "*" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/babel-sugar-composition-api-inject-h": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.4.0.tgz", + "integrity": "sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-composition-api-render-instance": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.4.0.tgz", + "integrity": "sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-functional-vue": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.4.0.tgz", + "integrity": "sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-inject-h": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.4.0.tgz", + "integrity": "sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-v-model": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.4.0.tgz", + "integrity": "sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "camelcase": "^5.0.0", + "html-tags": "^2.0.0", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-v-on": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.4.0.tgz", + "integrity": "sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "camelcase": "^5.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "dependencies": { + "@babel/parser": "^7.23.5", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/component-compiler-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", + "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consolidate": "^0.15.1", + "hash-sum": "^1.0.2", + "lru-cache": "^4.1.2", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.36", + "postcss-selector-parser": "^6.0.2", + "source-map": "~0.6.1", + "vue-template-es2015-compiler": "^1.9.0" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vue/component-compiler-utils/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/async-validator": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-1.8.5.tgz", + "integrity": "sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA==", + "dependencies": { + "babel-runtime": "6.x" + } + }, + "node_modules/babel-helper-vue-jsx-merge-props": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", + "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==", + "license": "MIT" + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "license": "MIT", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/consolidate": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz", + "integrity": "sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==", + "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/element-ui": { + "version": "2.15.14", + "resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.15.14.tgz", + "integrity": "sha512-2v9fHL0ZGINotOlRIAJD5YuVB8V7WKxrE9Qy7dXhRipa035+kF7WuU/z+tEmLVPBcJ0zt8mOu1DKpWcVzBK8IA==", + "license": "MIT", + "dependencies": { + "async-validator": "~1.8.1", + "babel-helper-vue-jsx-merge-props": "^2.0.0", + "deepmerge": "^1.2.0", + "normalize-wheel": "^1.0.1", + "resize-observer-polyfill": "^1.5.0", + "throttle-debounce": "^1.0.1" + }, + "peerDependencies": { + "vue": "^2.5.17" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", + "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/mavon-editor": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/mavon-editor/-/mavon-editor-2.10.4.tgz", + "integrity": "sha512-CFsBLkgt/KZBDg+SJYe2fyYv4zClY149PiwpH0rDAiiP4ae1XNs0GC8nBsoTeipsHcebDLN1QMkt3bUsnMDjQw==", + "license": "MIT", + "dependencies": { + "xss": "^1.0.6" + } + }, + "node_modules/merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/merge-source-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "devOptional": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/throttle-debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-1.1.0.tgz", + "integrity": "sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/vite-plugin-vue2/-/vite-plugin-vue2-2.0.3.tgz", + "integrity": "sha512-t3Tu93GWsMHbpeIv66MTO5e/rRAo8/+/eWoUtFYuAdKDMyEnn1dqsrXh+CfG+SJAlxJvcTP8U0eXkzhLjKNyMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.17.9", + "@babel/parser": "^7.17.9", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-decorators": "^7.17.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-optional-chaining": "^7.16.7", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-block-scoping": "^7.16.7", + "@babel/plugin-transform-computed-properties": "^7.16.7", + "@babel/plugin-transform-destructuring": "^7.17.7", + "@babel/plugin-transform-parameters": "^7.16.7", + "@babel/plugin-transform-spread": "^7.16.7", + "@babel/plugin-transform-typescript": "^7.16.8", + "@rollup/pluginutils": "^4.2.1", + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "@vue/babel-preset-jsx": "^1.2.4", + "@vue/component-compiler-utils": "^3.3.0", + "consolidate": "^0.16.0", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "hash-sum": "^2.0.0", + "magic-string": "^0.26.1", + "prettier": "^2.6.2", + "querystring": "^0.2.1", + "rollup": "^2.70.2", + "slash": "^3.0.0", + "source-map": "^0.7.3", + "vue-template-babel-compiler": "^1.2.0" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0", + "vue-template-compiler": "^2.2.0" + } + }, + "node_modules/vue": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "2.7.16", + "csstype": "^3.1.0" + } + }, + "node_modules/vue-template-babel-compiler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vue-template-babel-compiler/-/vue-template-babel-compiler-1.2.0.tgz", + "integrity": "sha512-CScBSX1/wCdmmZ/Lvj/63p2CCVTS0FMj0F69VRBo73CuJrjvPAPGmeNJ7D/cwt/VS2PduowRWbO8N4Zh4Z3b0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.14.3", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.15.6", + "@babel/plugin-proposal-optional-chaining": "^7.14.2", + "@babel/plugin-transform-arrow-functions": "^7.14.5", + "@babel/plugin-transform-block-scoping": "^7.14.5", + "@babel/plugin-transform-computed-properties": "^7.14.5", + "@babel/plugin-transform-destructuring": "^7.14.5", + "@babel/plugin-transform-parameters": "^7.14.5", + "@babel/plugin-transform-spread": "^7.14.5", + "@babel/types": "^7.14.5", + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "vue-template-compiler": "^2.6.0" + } + }, + "node_modules/vue-template-babel-compiler/node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-template-es2015-compiler": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", + "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..521996e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^2.7.14", + "element-ui": "^2.15.14", + "mavon-editor": "^2.10.4", + "highlight.js": "^11.9.0", + "marked": "^9.1.6" + }, + "devDependencies": { + "@vitejs/plugin-vue2": "^2.3.1", + "vite": "^3.0.7", + "vite-plugin-vue2": "^2.0.3" + } +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100755 index 0000000..653ac7a --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +2bd2793b3ea7c36dd202ef834837c1bb \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..7ed1ae4 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,938 @@ + + + + + diff --git a/frontend/src/assets/fonts/OFL.txt b/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 0000000..9cac04c --- /dev/null +++ b/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 0000000..2f9cc59 Binary files /dev/null and b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/frontend/src/assets/images/logo-universal.png b/frontend/src/assets/images/logo-universal.png new file mode 100644 index 0000000..e9913c1 Binary files /dev/null and b/frontend/src/assets/images/logo-universal.png differ diff --git a/frontend/src/components/LoginView.vue b/frontend/src/components/LoginView.vue new file mode 100644 index 0000000..080fa2d --- /dev/null +++ b/frontend/src/components/LoginView.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/frontend/src/components/NoteTree.vue b/frontend/src/components/NoteTree.vue new file mode 100644 index 0000000..e76cbc4 --- /dev/null +++ b/frontend/src/components/NoteTree.vue @@ -0,0 +1,385 @@ + + + + + diff --git a/frontend/src/components/TrashView.vue b/frontend/src/components/TrashView.vue new file mode 100644 index 0000000..964dfee --- /dev/null +++ b/frontend/src/components/TrashView.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..fecff88 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,14 @@ +import Vue from 'vue' +import ElementUI from 'element-ui' +import 'element-ui/lib/theme-chalk/index.css' +import mavonEditor from 'mavon-editor' +import 'mavon-editor/dist/css/index.css' +import App from './App.vue' +import './style.css' + +Vue.use(ElementUI, { size: 'small' }) +Vue.use(mavonEditor) + +new Vue({ + render: h => h(App) +}).$mount('#app') diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..3a7e976 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,160 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: #ffffff; + color: #303133; +} + +#app { + height: 100vh; +} + +/* 全局滚动条样式 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-thumb { + background-color: #c0c4cc; + border-radius: 3px; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +/* Element UI 样式覆盖 */ +.el-menu { + border-right: none !important; +} + +.el-tree { + background-color: transparent !important; +} + +/* Markdown 预览样式 */ +.markdown-body { + font-size: 16px; + line-height: 1.6; + 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; +} + +.markdown-body h1 { + padding-bottom: 0.3em; + font-size: 2em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h2 { + padding-bottom: 0.3em; + font-size: 1.5em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body code { + background-color: rgba(27, 31, 35, 0.05); + border-radius: 3px; + font-size: 85%; + margin: 0; + padding: 0.2em 0.4em; +} + +.markdown-body pre { + background-color: #f6f8fa; + border-radius: 3px; + font-size: 85%; + line-height: 1.45; + overflow: auto; + padding: 16px; +} + +.markdown-body pre code { + background-color: transparent; + border: 0; + display: inline; + line-height: inherit; + margin: 0; + max-width: auto; + overflow: visible; + padding: 0; + white-space: pre; + word-break: normal; +} + +.markdown-body blockquote { + border-left: 0.25em solid #dfe2e5; + color: #6a737d; + margin: 0; + padding: 0 1em; +} + +.markdown-body table { + border-collapse: collapse; + border-spacing: 0; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; +} + +.markdown-body table th, +.markdown-body table td { + border: 1px solid #dfe2e5; + padding: 6px 13px; +} + +.markdown-body table th { + font-weight: 600; + background-color: #f6f8fa; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: border-box; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..0419843 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,21 @@ +import {defineConfig} from 'vite' +import { createVuePlugin } from 'vite-plugin-vue2' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [createVuePlugin()], + server: { + // Wails dev 模式下的配置 + host: '0.0.0.0', + port: 5173, + strictPort: true, + // 不启用 HMR 的 WebSocket 代理,让 Wails 处理 + }, + // 确保资源路径正确 + base: './', + build: { + // 确保输出目录正确 + outDir: 'dist', + assetsDir: 'assets' + } +}) diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100755 index 0000000..f047653 --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,61 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {models} from '../models'; + +export function CreateNote(arg1:number,arg2:string,arg3:boolean):Promise; + +export function DeleteNote(arg1:number):Promise; + +export function EmptyTrash():Promise; + +export function ExportAllMarkdown():Promise; + +export function ExportFolderMarkdown(arg1:number):Promise; + +export function ExportHTML(arg1:string,arg2:string):Promise; + +export function ExportMarkdown(arg1:string,arg2:string):Promise; + +export function GetAssetsPath():Promise; + +export function GetFileServerURL():Promise; + +export function GetNote(arg1:number):Promise; + +export function GetNoteTree():Promise>; + +export function GetTrashNotes():Promise>; + +export function GetUser():Promise>; + +export function IsLoggedIn():Promise; + +export function Login(arg1:string,arg2:string):Promise; + +export function Logout():Promise; + +export function PermanentDeleteNote(arg1:number):Promise; + +export function Register(arg1:string,arg2:string,arg3:string):Promise; + +export function ResetPassword(arg1:string,arg2:string,arg3:string):Promise; + +export function RestoreNote(arg1:number):Promise; + +export function RevealInFinder(arg1:string):Promise; + +export function SaveFile(arg1:string,arg2:string):Promise; + +export function SaveImage(arg1:string):Promise; + +export function SearchNotes(arg1:string):Promise>; + +export function SelectDirectory(arg1:string):Promise; + +export function SelectSaveFile(arg1:string,arg2:string):Promise; + +export function SendVerifyCode(arg1:string,arg2:string):Promise; + +export function UpdateNote(arg1:number,arg2:string,arg3:string,arg4:string):Promise; + +export function VerifyCode(arg1:string,arg2:string,arg3:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100755 index 0000000..733cb36 --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -0,0 +1,119 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function CreateNote(arg1, arg2, arg3) { + return window['go']['main']['App']['CreateNote'](arg1, arg2, arg3); +} + +export function DeleteNote(arg1) { + return window['go']['main']['App']['DeleteNote'](arg1); +} + +export function EmptyTrash() { + return window['go']['main']['App']['EmptyTrash'](); +} + +export function ExportAllMarkdown() { + return window['go']['main']['App']['ExportAllMarkdown'](); +} + +export function ExportFolderMarkdown(arg1) { + return window['go']['main']['App']['ExportFolderMarkdown'](arg1); +} + +export function ExportHTML(arg1, arg2) { + return window['go']['main']['App']['ExportHTML'](arg1, arg2); +} + +export function ExportMarkdown(arg1, arg2) { + return window['go']['main']['App']['ExportMarkdown'](arg1, arg2); +} + +export function GetAssetsPath() { + return window['go']['main']['App']['GetAssetsPath'](); +} + +export function GetFileServerURL() { + return window['go']['main']['App']['GetFileServerURL'](); +} + +export function GetNote(arg1) { + return window['go']['main']['App']['GetNote'](arg1); +} + +export function GetNoteTree() { + return window['go']['main']['App']['GetNoteTree'](); +} + +export function GetTrashNotes() { + return window['go']['main']['App']['GetTrashNotes'](); +} + +export function GetUser() { + return window['go']['main']['App']['GetUser'](); +} + +export function IsLoggedIn() { + return window['go']['main']['App']['IsLoggedIn'](); +} + +export function Login(arg1, arg2) { + return window['go']['main']['App']['Login'](arg1, arg2); +} + +export function Logout() { + return window['go']['main']['App']['Logout'](); +} + +export function PermanentDeleteNote(arg1) { + return window['go']['main']['App']['PermanentDeleteNote'](arg1); +} + +export function Register(arg1, arg2, arg3) { + return window['go']['main']['App']['Register'](arg1, arg2, arg3); +} + +export function ResetPassword(arg1, arg2, arg3) { + return window['go']['main']['App']['ResetPassword'](arg1, arg2, arg3); +} + +export function RestoreNote(arg1) { + return window['go']['main']['App']['RestoreNote'](arg1); +} + +export function RevealInFinder(arg1) { + return window['go']['main']['App']['RevealInFinder'](arg1); +} + +export function SaveFile(arg1, arg2) { + return window['go']['main']['App']['SaveFile'](arg1, arg2); +} + +export function SaveImage(arg1) { + return window['go']['main']['App']['SaveImage'](arg1); +} + +export function SearchNotes(arg1) { + return window['go']['main']['App']['SearchNotes'](arg1); +} + +export function SelectDirectory(arg1) { + return window['go']['main']['App']['SelectDirectory'](arg1); +} + +export function SelectSaveFile(arg1, arg2) { + return window['go']['main']['App']['SelectSaveFile'](arg1, arg2); +} + +export function SendVerifyCode(arg1, arg2) { + return window['go']['main']['App']['SendVerifyCode'](arg1, arg2); +} + +export function UpdateNote(arg1, arg2, arg3, arg4) { + return window['go']['main']['App']['UpdateNote'](arg1, arg2, arg3, arg4); +} + +export function VerifyCode(arg1, arg2, arg3) { + return window['go']['main']['App']['VerifyCode'](arg1, arg2, arg3); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts new file mode 100755 index 0000000..5cae125 --- /dev/null +++ b/frontend/wailsjs/go/models.ts @@ -0,0 +1,113 @@ +export namespace models { + + export class Note { + id: number; + parentId: number; + title: string; + content: string; + sortOrder: number; + isFolder: boolean; + tag: string; + // Go type: time + createdAt: any; + // Go type: time + updatedAt: any; + // Go type: time + deletedAt?: any; + children?: Note[]; + + static createFrom(source: any = {}) { + return new Note(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.parentId = source["parentId"]; + this.title = source["title"]; + this.content = source["content"]; + this.sortOrder = source["sortOrder"]; + this.isFolder = source["isFolder"]; + this.tag = source["tag"]; + this.createdAt = this.convertValues(source["createdAt"], null); + this.updatedAt = this.convertValues(source["updatedAt"], null); + this.deletedAt = this.convertValues(source["deletedAt"], null); + this.children = this.convertValues(source["children"], Note); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class NoteTree { + id: number; + parentId: number; + title: string; + content: string; + sortOrder: number; + isFolder: boolean; + tag: string; + // Go type: time + createdAt: any; + // Go type: time + updatedAt: any; + // Go type: time + deletedAt?: any; + children?: Note[]; + children: NoteTree[]; + + static createFrom(source: any = {}) { + return new NoteTree(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.parentId = source["parentId"]; + this.title = source["title"]; + this.content = source["content"]; + this.sortOrder = source["sortOrder"]; + this.isFolder = source["isFolder"]; + this.tag = source["tag"]; + this.createdAt = this.convertValues(source["createdAt"], null); + this.updatedAt = this.convertValues(source["updatedAt"], null); + this.deletedAt = this.convertValues(source["deletedAt"], null); + this.children = this.convertValues(source["children"], Note); + this.children = this.convertValues(source["children"], NoteTree); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + +} + diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..3bbea84 --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,330 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void + +// Notification types +export interface NotificationOptions { + id: string; + title: string; + subtitle?: string; // macOS and Linux only + body?: string; + categoryId?: string; + data?: { [key: string]: any }; +} + +export interface NotificationAction { + id?: string; + title?: string; + destructive?: boolean; // macOS-specific +} + +export interface NotificationCategory { + id?: string; + actions?: NotificationAction[]; + hasReplyField?: boolean; + replyPlaceholder?: string; + replyButtonTitle?: string; +} + +// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) +// Initializes the notification service for the application. +// This must be called before sending any notifications. +export function InitializeNotifications(): Promise; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) +// Sends a notification with action buttons. Requires a registered category. +export function SendNotificationWithActions(options: NotificationOptions): Promise; + +// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) +// Registers a notification category that can be used with SendNotificationWithActions. +export function RegisterNotificationCategory(category: NotificationCategory): Promise; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) +// Removes a notification by its identifier (cross-platform convenience function). +export function RemoveNotification(identifier: string): Promise; \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..556621e --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,298 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} + +export function InitializeNotifications() { + return window.runtime.InitializeNotifications(); +} + +export function CleanupNotifications() { + return window.runtime.CleanupNotifications(); +} + +export function IsNotificationAvailable() { + return window.runtime.IsNotificationAvailable(); +} + +export function RequestNotificationAuthorization() { + return window.runtime.RequestNotificationAuthorization(); +} + +export function CheckNotificationAuthorization() { + return window.runtime.CheckNotificationAuthorization(); +} + +export function SendNotification(options) { + return window.runtime.SendNotification(options); +} + +export function SendNotificationWithActions(options) { + return window.runtime.SendNotificationWithActions(options); +} + +export function RegisterNotificationCategory(category) { + return window.runtime.RegisterNotificationCategory(category); +} + +export function RemoveNotificationCategory(categoryId) { + return window.runtime.RemoveNotificationCategory(categoryId); +} + +export function RemoveAllPendingNotifications() { + return window.runtime.RemoveAllPendingNotifications(); +} + +export function RemovePendingNotification(identifier) { + return window.runtime.RemovePendingNotification(identifier); +} + +export function RemoveAllDeliveredNotifications() { + return window.runtime.RemoveAllDeliveredNotifications(); +} + +export function RemoveDeliveredNotification(identifier) { + return window.runtime.RemoveDeliveredNotification(identifier); +} + +export function RemoveNotification(identifier) { + return window.runtime.RemoveNotification(identifier); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..12ab25f --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module kenaito-notebook + +go 1.25.0 + +require github.com/wailsapp/wails/v2 v2.12.0 + +require ( + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.22.0 // indirect +) + +// replace github.com/wailsapp/wails/v2 v2.12.0 => /Users/mac/go/pkg/mod diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1bdaa84 --- /dev/null +++ b/go.sum @@ -0,0 +1,82 @@ +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c= +github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..185761a --- /dev/null +++ b/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/options/mac" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "kenaito-notebook", + Width: 1024, + Height: 768, + MinWidth: 800, + MinHeight: 600, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + Mac: &mac.Options{ + DisableZoom: false, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/models/note.go b/models/note.go new file mode 100644 index 0000000..f7ef058 --- /dev/null +++ b/models/note.go @@ -0,0 +1,22 @@ +package models + +import "time" + +type Note struct { + ID int64 `json:"id"` + ParentID int64 `json:"parentId"` // 0 = root level + Title string `json:"title"` + Content string `json:"content"` + SortOrder int `json:"sortOrder"` + IsFolder bool `json:"isFolder"` + Tag string `json:"tag"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt"` // nil = not deleted + Children []*Note `json:"children,omitempty"` // 子项(用于树形结构) +} + +type NoteTree struct { + Note + Children []*NoteTree `json:"children"` +} diff --git a/server/config.go b/server/config.go new file mode 100644 index 0000000..87b8346 --- /dev/null +++ b/server/config.go @@ -0,0 +1,89 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "kenaito-notebook-server/services" +) + +type Config struct { + Port int + BaseURL string + SMTP services.EmailConfig +} + +func loadProperties(filename string) map[string]string { + props := make(map[string]string) + + // Try executable directory first + exe, err := os.Executable() + if err == nil { + exeDir := filepath.Dir(exe) + props = loadFromFile(filepath.Join(exeDir, filename)) + if len(props) > 0 { + return props + } + } + + // Try working directory + props = loadFromFile(filename) + return props +} + +func loadFromFile(path string) map[string]string { + props := make(map[string]string) + + f, err := os.Open(path) + if err != nil { + return props + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if idx := strings.Index(line, "="); idx > 0 { + key := strings.TrimSpace(line[:idx]) + value := strings.TrimSpace(line[idx+1:]) + props[key] = value + } + } + + return props +} + +func DefaultConfig() *Config { + props := loadProperties("config.properties") + + port := 8080 + if p := props["server.port"]; p != "" { + if v, err := strconv.Atoi(p); err == nil { + port = v + } + } + + baseURL := props["server.base_url"] + if baseURL == "" { + baseURL = fmt.Sprintf("http://localhost:%d", port) + } + + return &Config{ + Port: port, + BaseURL: baseURL, + SMTP: services.EmailConfig{ + Host: props["smtp.host"], + Port: props["smtp.port"], + Username: props["smtp.username"], + Password: props["smtp.password"], + From: props["smtp.from"], + }, + } +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..5a2fd76 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,48 @@ +module kenaito-notebook-server + +go 1.25.0 + +require ( + github.com/gin-gonic/gin v1.12.0 + github.com/google/uuid v1.6.0 + github.com/yuin/goldmark v1.8.2 + golang.org/x/crypto v0.48.0 + modernc.org/sqlite v1.53.0 +) + +require ( + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + modernc.org/libc v1.73.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..f01a89a --- /dev/null +++ b/server/go.sum @@ -0,0 +1,137 @@ +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= +modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= +modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/server/handlers/auth.go b/server/handlers/auth.go new file mode 100644 index 0000000..ad72d3a --- /dev/null +++ b/server/handlers/auth.go @@ -0,0 +1,338 @@ +package handlers + +import ( + "log" + "net/http" + "net/mail" + "strings" + + "github.com/gin-gonic/gin" + + "kenaito-notebook-server/services" +) + +func isValidEmail(email string) bool { + _, err := mail.ParseAddress(email) + return err == nil +} + +type AuthHandler struct { + db *services.Database + auth *services.AuthService + email *services.EmailService + verify *services.VerificationService +} + +func NewAuthHandler(db *services.Database, auth *services.AuthService, email *services.EmailService, verify *services.VerificationService) *AuthHandler { + return &AuthHandler{db: db, auth: auth, email: email, verify: verify} +} + +type RegisterRequest struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` + Code string `json:"code" binding:"required"` +} + +type LoginRequest struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type SendCodeRequest struct { + Email string `json:"email" binding:"required"` + Purpose string `json:"purpose" binding:"required"` +} + +type VerifyCodeRequest struct { + Email string `json:"email" binding:"required"` + Code string `json:"code" binding:"required"` + Purpose string `json:"purpose" binding:"required"` +} + +type ResetPasswordRequest struct { + Email string `json:"email" binding:"required"` + Code string `json:"code" binding:"required"` + NewPassword string `json:"newPassword" binding:"required"` +} + +// SendCode 发送验证码 +func (h *AuthHandler) SendCode(c *gin.Context) { + var req SendCodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("[Auth] SendCode bind error: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"}) + return + } + + if !isValidEmail(req.Email) { + log.Printf("[Auth] SendCode invalid email: %s", req.Email) + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"}) + return + } + + if req.Purpose != "register" && req.Purpose != "reset" { + log.Printf("[Auth] SendCode invalid purpose: %s", req.Purpose) + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求类型"}) + return + } + + // 注册时检查邮箱是否已注册 + if req.Purpose == "register" { + existing, _ := h.db.GetUserByEmail(req.Email) + if existing != nil { + log.Printf("[Auth] SendCode email already registered: %s", req.Email) + c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"}) + return + } + } + + // 重置密码时检查邮箱是否存在 + if req.Purpose == "reset" { + existing, _ := h.db.GetUserByEmail(req.Email) + if existing == nil { + log.Printf("[Auth] SendCode email not found for reset: %s", req.Email) + c.JSON(http.StatusNotFound, gin.H{"error": "邮箱未注册"}) + return + } + } + + // 限频检查 + if !h.verify.CheckRateLimit(req.Email, req.Purpose) { + log.Printf("[Auth] SendCode rate limit exceeded: %s, purpose: %s", req.Email, req.Purpose) + c.JSON(http.StatusTooManyRequests, gin.H{"error": "发送过于频繁,请 60 秒后重试"}) + return + } + + code := h.verify.GenerateCode(req.Email, req.Purpose) + if err := h.email.SendVerificationCode(req.Email, code, req.Purpose); err != nil { + log.Printf("[Auth] SendCode send email failed: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "发送验证码失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "验证码已发送"}) +} + +// VerifyCode 验证验证码 +func (h *AuthHandler) VerifyCode(c *gin.Context) { + var req VerifyCodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("[Auth] VerifyCode bind error: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"}) + return + } + + if err := h.verify.VerifyCode(req.Email, req.Code, req.Purpose); err != nil { + log.Printf("[Auth] VerifyCode failed: email=%s, purpose=%s, err=%v", req.Email, req.Purpose, err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "验证码正确"}) +} + +// Register 用户注册 +func (h *AuthHandler) Register(c *gin.Context) { + var req RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("[Auth] Register bind error: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"}) + return + } + + if !isValidEmail(req.Email) { + log.Printf("[Auth] Register invalid email: %s", req.Email) + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"}) + return + } + + // 检查邮箱是否已注册 + existing, _ := h.db.GetUserByEmail(req.Email) + if existing != nil { + log.Printf("[Auth] Register email already registered: %s", req.Email) + c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"}) + return + } + + // 验证并消耗验证码 + if err := h.verify.UseCode(req.Email, req.Code, "register"); err != nil { + log.Printf("[Auth] Register use code failed: email=%s, err=%v", req.Email, err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 哈希密码 + hashedPassword, err := h.auth.HashPassword(req.Password) + if err != nil { + log.Printf("[Auth] Register hash password failed: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"}) + return + } + + // 创建用户 + user, err := h.db.CreateUser(req.Email, hashedPassword) + if err != nil { + log.Printf("[Auth] Register create user failed: email=%s, err=%v", req.Email, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"}) + return + } + + // 生成 token + token, err := h.auth.GenerateToken(user.ID, user.Email) + if err != nil { + log.Printf("[Auth] Register generate token failed: userID=%d, err=%v", user.ID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "token": token, + "user": user, + }) +} + +// Login 用户登录 +func (h *AuthHandler) Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("[Auth] Login bind error: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "请填写邮箱和密码"}) + return + } + + if !isValidEmail(req.Email) { + log.Printf("[Auth] Login invalid email: %s", req.Email) + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"}) + return + } + + // 查找用户 + user, err := h.db.GetUserByEmail(req.Email) + if err != nil { + log.Printf("[Auth] Login user not found: %s, err=%v", req.Email, err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) + return + } + + // 验证密码 + if !h.auth.CheckPassword(req.Password, user.HashedPassword) { + log.Printf("[Auth] Login password mismatch: email=%s", req.Email) + c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) + return + } + + // 生成 token + token, err := h.auth.GenerateToken(user.ID, user.Email) + if err != nil { + log.Printf("[Auth] Login generate token failed: userID=%d, err=%v", user.ID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "登录失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "token": token, + "user": user, + }) +} + +// ResetPassword 重置密码 +func (h *AuthHandler) ResetPassword(c *gin.Context) { + var req ResetPasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("[Auth] ResetPassword bind error: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"}) + return + } + + if !isValidEmail(req.Email) { + log.Printf("[Auth] ResetPassword invalid email: %s", req.Email) + c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"}) + return + } + + // 验证并消耗验证码 + if err := h.verify.UseCode(req.Email, req.Code, "reset"); err != nil { + log.Printf("[Auth] ResetPassword use code failed: email=%s, err=%v", req.Email, err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 查找用户 + user, err := h.db.GetUserByEmail(req.Email) + if err != nil { + log.Printf("[Auth] ResetPassword user not found: %s, err=%v", req.Email, err) + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + return + } + + // 哈希新密码 + hashedPassword, err := h.auth.HashPassword(req.NewPassword) + if err != nil { + log.Printf("[Auth] ResetPassword hash password failed: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"}) + return + } + + // 更新密码 + if err := h.db.UpdatePassword(user.ID, hashedPassword); err != nil { + log.Printf("[Auth] ResetPassword update password failed: userID=%d, err=%v", user.ID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "密码重置成功"}) +} + +// GetMe 获取当前用户信息 +func (h *AuthHandler) GetMe(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + log.Printf("[Auth] GetMe userID not found in context") + c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) + return + } + + user, err := h.db.GetUserByID(userID.(int64)) + if err != nil { + log.Printf("[Auth] GetMe user not found: userID=%v, err=%v", userID, err) + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + return + } + + c.JSON(http.StatusOK, user) +} + +// AuthMiddleware JWT 认证中间件 +func AuthMiddleware(auth *services.AuthService) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + log.Printf("[Auth] Middleware missing Authorization header: ip=%s", c.ClientIP()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) + c.Abort() + return + } + + // 提取 Bearer token + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + log.Printf("[Auth] Middleware invalid auth format: ip=%s", c.ClientIP()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证格式"}) + c.Abort() + return + } + + payload, err := auth.ValidateToken(parts[1]) + if err != nil { + log.Printf("[Auth] Middleware token validation failed: ip=%s, err=%v", c.ClientIP(), err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"}) + c.Abort() + return + } + + // 将用户信息存入上下文 + c.Set("userID", payload.UserID) + c.Set("email", payload.Email) + c.Next() + } +} diff --git a/server/handlers/export.go b/server/handlers/export.go new file mode 100644 index 0000000..b082e4d --- /dev/null +++ b/server/handlers/export.go @@ -0,0 +1,182 @@ +package handlers + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strconv" + + "github.com/gin-gonic/gin" + + "kenaito-notebook-server/models" + "kenaito-notebook-server/services" +) + +type ExportHandler struct { + db *services.Database + exportSvc *services.ExportService +} + +func NewExportHandler(db *services.Database, exportSvc *services.ExportService) *ExportHandler { + return &ExportHandler{db: db, exportSvc: exportSvc} +} + +func (h *ExportHandler) ExportHTML(c *gin.Context) { + var req struct { + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + outputPath, err := h.getTempExportPath(req.Title, ".html") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if err := h.exportSvc.ExportHTML(req.Title, req.Content, outputPath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"filePath": outputPath}) +} + +func (h *ExportHandler) ExportMarkdown(c *gin.Context) { + var req struct { + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + outputPath, err := h.getTempExportPath(req.Title, ".md") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if err := h.exportSvc.ExportMarkdown(req.Title, req.Content, outputPath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"filePath": outputPath}) +} + +func (h *ExportHandler) ExportFolderZip(c *gin.Context) { + userID := c.GetInt64("userID") + + folderID, err := strconv.ParseInt(c.Param("folderId"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder id"}) + return + } + + folderTree, err := h.db.GetFolderTree(userID, folderID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + noteInfoTree := convertNoteToNoteInfo(folderTree) + + outputPath, err := h.getTempExportPath(folderTree.Title, ".zip") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if err := h.exportSvc.ExportFolderZip(folderTree.Title, noteInfoTree.Children, outputPath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"filePath": outputPath}) +} + +func (h *ExportHandler) ExportAllZip(c *gin.Context) { + userID := c.GetInt64("userID") + + allNotesTree, err := h.db.GetAllNotesTree(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + noteInfos := make([]*services.NoteInfo, len(allNotesTree)) + for i, noteTree := range allNotesTree { + noteInfos[i] = convertNoteTreeToNoteInfo(noteTree) + } + + outputPath, err := h.getTempExportPath("所有笔记", ".zip") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if err := h.exportSvc.ExportAllZip(noteInfos, outputPath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"filePath": outputPath}) +} + +func (h *ExportHandler) getTempExportPath(title, ext string) (string, error) { + tmpDir := filepath.Join(os.TempDir(), "kenaito-notebook-export") + if err := os.MkdirAll(tmpDir, 0755); err != nil { + return "", fmt.Errorf("failed to create temp export dir: %w", err) + } + + safeName := h.exportSvc.GetSafeFilename(title) + return filepath.Join(tmpDir, fmt.Sprintf("%s%s", safeName, ext)), nil +} + +func convertNoteToNoteInfo(note *models.Note) *services.NoteInfo { + if note == nil { + return nil + } + + info := &services.NoteInfo{ + Title: note.Title, + Content: note.Content, + IsFolder: note.IsFolder, + } + + if note.Children != nil && len(note.Children) > 0 { + info.Children = make([]*services.NoteInfo, len(note.Children)) + for i, child := range note.Children { + info.Children[i] = convertNoteToNoteInfo(child) + } + } + + return info +} + +func convertNoteTreeToNoteInfo(noteTree *models.NoteTree) *services.NoteInfo { + if noteTree == nil { + return nil + } + + info := &services.NoteInfo{ + Title: noteTree.Title, + Content: noteTree.Content, + IsFolder: noteTree.IsFolder, + } + + if noteTree.Children != nil && len(noteTree.Children) > 0 { + info.Children = make([]*services.NoteInfo, len(noteTree.Children)) + for i, child := range noteTree.Children { + info.Children[i] = convertNoteTreeToNoteInfo(child) + } + } + + return info +} diff --git a/server/handlers/file.go b/server/handlers/file.go new file mode 100644 index 0000000..0d0fcdc --- /dev/null +++ b/server/handlers/file.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "kenaito-notebook-server/services" +) + +type FileHandler struct { + fileSvc *services.FileService +} + +func NewFileHandler(fileSvc *services.FileService) *FileHandler { + return &FileHandler{fileSvc: fileSvc} +} + +func (h *FileHandler) SaveImage(c *gin.Context) { + var req struct { + Data string `json:"data" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + url, err := h.fileSvc.SaveImage(req.Data) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"url": url}) +} + +func (h *FileHandler) SaveFile(c *gin.Context) { + var req struct { + Data string `json:"data" binding:"required"` + Filename string `json:"filename" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + url, err := h.fileSvc.SaveFile(req.Data, req.Filename) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"url": url}) +} + +func (h *FileHandler) GetFileServerURL(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"url": h.fileSvc.GetBaseURL()}) +} + +func (h *FileHandler) GetAssetsPath(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"path": h.fileSvc.GetAssetsPath()}) +} diff --git a/server/handlers/note.go b/server/handlers/note.go new file mode 100644 index 0000000..9c7d1ce --- /dev/null +++ b/server/handlers/note.go @@ -0,0 +1,170 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "kenaito-notebook-server/services" +) + +type NoteHandler struct { + db *services.Database +} + +func NewNoteHandler(db *services.Database) *NoteHandler { + return &NoteHandler{db: db} +} + +func (h *NoteHandler) GetNoteTree(c *gin.Context) { + userID := c.GetInt64("userID") + result, err := h.db.GetNoteTree(userID) + if err != nil { + log.Printf("[API] GetNoteTree error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, result) +} + +func (h *NoteHandler) GetNote(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + + note, err := h.db.GetNote(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, note) +} + +func (h *NoteHandler) CreateNote(c *gin.Context) { + userID := c.GetInt64("userID") + + var req struct { + ParentID int64 `json:"parentId"` + Title string `json:"title"` + IsFolder bool `json:"isFolder"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + note, err := h.db.CreateNote(userID, req.ParentID, req.Title, req.IsFolder) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, note) +} + +func (h *NoteHandler) UpdateNote(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + + var req struct { + Title string `json:"title"` + Content string `json:"content"` + Tag string `json:"tag"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.db.UpdateNote(id, req.Title, req.Content, req.Tag); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "updated"}) +} + +func (h *NoteHandler) DeleteNote(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + + if err := h.db.DeleteNote(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "deleted"}) +} + +func (h *NoteHandler) SearchNotes(c *gin.Context) { + userID := c.GetInt64("userID") + + keyword := c.Query("q") + if keyword == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "keyword is required"}) + return + } + + notes, err := h.db.SearchNotes(userID, keyword) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, notes) +} + +func (h *NoteHandler) GetTrashNotes(c *gin.Context) { + userID := c.GetInt64("userID") + + notes, err := h.db.GetTrashNotes(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, notes) +} + +func (h *NoteHandler) RestoreNote(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + + if err := h.db.RestoreNote(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "restored"}) +} + +func (h *NoteHandler) PermanentDeleteNote(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + + if err := h.db.PermanentDeleteNote(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "permanently deleted"}) +} + +func (h *NoteHandler) EmptyTrash(c *gin.Context) { + userID := c.GetInt64("userID") + + if err := h.db.EmptyTrash(userID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "trash emptied"}) +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..5e728fe --- /dev/null +++ b/server/main.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "log" + + "github.com/gin-gonic/gin" + + "kenaito-notebook-server/handlers" + "kenaito-notebook-server/services" +) + +func main() { + cfg := DefaultConfig() + + db, err := services.NewDatabase() + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + defer db.Close() + log.Println("[Server] Database initialized") + + fileSvc, err := services.NewFileService(cfg.BaseURL) + if err != nil { + log.Fatalf("Failed to initialize file service: %v", err) + } + log.Println("[Server] File service initialized") + + exportSvc := services.NewExportService() + authSvc := services.NewAuthService() + emailSvc := services.NewEmailService(cfg.SMTP) + verifySvc := services.NewVerificationService() + log.Println("[Server] Services initialized") + + gin.SetMode(gin.ReleaseMode) + r := gin.Default() + + // CORS + r.Use(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(200) + return + } + c.Next() + }) + + noteHandler := handlers.NewNoteHandler(db) + fileHandler := handlers.NewFileHandler(fileSvc) + exportHandler := handlers.NewExportHandler(db, exportSvc) + authHandler := handlers.NewAuthHandler(db, authSvc, emailSvc, verifySvc) + + // 静态文件服务(图片、附件等) + r.Static("/api/files/assets", fileSvc.GetAssetsPath()) + + api := r.Group("/api") + { + // 认证路由(无需登录) + auth := api.Group("/auth") + { + auth.POST("/register", authHandler.Register) + auth.POST("/login", authHandler.Login) + auth.POST("/send-code", authHandler.SendCode) + auth.POST("/verify-code", authHandler.VerifyCode) + auth.POST("/reset-password", authHandler.ResetPassword) + } + + // 需要登录的路由 + protected := api.Group("") + protected.Use(handlers.AuthMiddleware(authSvc)) + { + // 用户信息 + protected.GET("/auth/me", authHandler.GetMe) + + // 笔记管理 + notes := protected.Group("/notes") + { + notes.GET("/tree", noteHandler.GetNoteTree) + notes.GET("/search", noteHandler.SearchNotes) + notes.GET("/:id", noteHandler.GetNote) + notes.POST("", noteHandler.CreateNote) + notes.PUT("/:id", noteHandler.UpdateNote) + notes.DELETE("/:id", noteHandler.DeleteNote) + } + + // 回收站 + trash := protected.Group("/trash") + { + trash.GET("", noteHandler.GetTrashNotes) + trash.POST("/:id/restore", noteHandler.RestoreNote) + trash.DELETE("/:id", noteHandler.PermanentDeleteNote) + trash.DELETE("", noteHandler.EmptyTrash) + } + + // 文件管理 + files := protected.Group("/files") + { + files.POST("/image", fileHandler.SaveImage) + files.POST("/upload", fileHandler.SaveFile) + files.GET("/server-url", fileHandler.GetFileServerURL) + files.GET("/assets-path", fileHandler.GetAssetsPath) + } + + // 导出 + export := protected.Group("/export") + { + export.POST("/html", exportHandler.ExportHTML) + export.POST("/markdown", exportHandler.ExportMarkdown) + export.POST("/folder-zip/:folderId", exportHandler.ExportFolderZip) + export.POST("/all-zip", exportHandler.ExportAllZip) + } + } + } + + addr := fmt.Sprintf(":%d", cfg.Port) + log.Printf("[Server] Starting on %s", addr) + if err := r.Run(addr); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} diff --git a/server/models/note.go b/server/models/note.go new file mode 100644 index 0000000..6e2b2ea --- /dev/null +++ b/server/models/note.go @@ -0,0 +1,22 @@ +package models + +import "time" + +type Note struct { + ID int64 `json:"id"` + ParentID int64 `json:"parentId"` + Title string `json:"title"` + Content string `json:"content"` + SortOrder int `json:"sortOrder"` + IsFolder bool `json:"isFolder"` + Tag string `json:"tag"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt"` + Children []*Note `json:"children,omitempty"` +} + +type NoteTree struct { + Note + Children []*NoteTree `json:"children"` +} diff --git a/server/models/user.go b/server/models/user.go new file mode 100644 index 0000000..b987f19 --- /dev/null +++ b/server/models/user.go @@ -0,0 +1,9 @@ +package models + +import "time" + +type User struct { + ID int64 `json:"id"` + Email string `json:"email"` + CreatedAt time.Time `json:"createdAt"` +} diff --git a/server/services/auth.go b/server/services/auth.go new file mode 100644 index 0000000..63f73d3 --- /dev/null +++ b/server/services/auth.go @@ -0,0 +1,142 @@ +package services + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrInvalidCredentials = errors.New("邮箱或密码错误") + ErrUserExists = errors.New("邮箱已被注册") + ErrInvalidToken = errors.New("无效的 token") + ErrTokenExpired = errors.New("token 已过期") +) + +// JWT 密钥,生产环境应从环境变量或配置文件读取 +var jwtSecret = []byte("kenaito-notebook-jwt-secret-2026") + +type jwtHeader struct { + Alg string `json:"alg"` + Typ string `json:"typ"` +} + +type jwtPayload struct { + UserID int64 `json:"userId"` + Email string `json:"email"` + Exp int64 `json:"exp"` + Iat int64 `json:"iat"` +} + +type AuthService struct{} + +func NewAuthService() *AuthService { + return &AuthService{} +} + +// HashPassword 使用 bcrypt 哈希密码 +func (s *AuthService) HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPassword 验证密码 +func (s *AuthService) CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// GenerateToken 生成 JWT token +func (s *AuthService) GenerateToken(userID int64, email string) (string, error) { + now := time.Now() + payload := jwtPayload{ + UserID: userID, + Email: email, + Exp: now.Add(7 * 24 * time.Hour).Unix(), // 7 天过期 + Iat: now.Unix(), + } + + // Header + header := jwtHeader{Alg: "HS256", Typ: "JWT"} + headerJSON, err := json.Marshal(header) + if err != nil { + return "", err + } + headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON) + + // Payload + payloadJSON, err := json.Marshal(payload) + if err != nil { + return "", err + } + payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON) + + // 签名 + signingInput := headerB64 + "." + payloadB64 + mac := hmac.New(sha256.New, jwtSecret) + mac.Write([]byte(signingInput)) + signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) + + return signingInput + "." + signature, nil +} + +// ValidateToken 验证 JWT token,返回 payload +func (s *AuthService) ValidateToken(tokenStr string) (*jwtPayload, error) { + parts := strings.Split(tokenStr, ".") + if len(parts) != 3 { + return nil, ErrInvalidToken + } + + signingInput := parts[0] + "." + parts[1] + + // 验证签名 + mac := hmac.New(sha256.New, jwtSecret) + mac.Write([]byte(signingInput)) + expectedSig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) + + if !hmac.Equal([]byte(parts[2]), []byte(expectedSig)) { + return nil, ErrInvalidToken + } + + // 解析 payload + payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, ErrInvalidToken + } + + var payload jwtPayload + if err := json.Unmarshal(payloadJSON, &payload); err != nil { + return nil, ErrInvalidToken + } + + // 检查过期 + if time.Now().Unix() > payload.Exp { + return nil, ErrTokenExpired + } + + return &payload, nil +} + +// ExtractTokenID 从 token 字符串中提取用户 ID(不验证签名,仅用于快速检查) +func ExtractTokenID(token string) (int64, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return 0, fmt.Errorf("invalid token format") + } + payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return 0, err + } + var payload jwtPayload + if err := json.Unmarshal(payloadJSON, &payload); err != nil { + return 0, err + } + return payload.UserID, nil +} diff --git a/server/services/database.go b/server/services/database.go new file mode 100644 index 0000000..1498bb8 --- /dev/null +++ b/server/services/database.go @@ -0,0 +1,581 @@ +package services + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "time" + + _ "modernc.org/sqlite" + + "kenaito-notebook-server/models" +) + +type Database struct { + db *sql.DB +} + +func NewDatabase() (*Database, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %w", err) + } + + dbDir := filepath.Join(homeDir, ".kenaito-notebook") + if err := os.MkdirAll(dbDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create data directory: %w", err) + } + + dbPath := filepath.Join(dbDir, "notes.db") + db, err := sql.Open("sqlite", dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { + db.Close() + return nil, fmt.Errorf("failed to enable WAL mode: %w", err) + } + + if err := createTables(db); err != nil { + db.Close() + return nil, err + } + + return &Database{db: db}, nil +} + +func createTables(db *sql.DB) error { + query := ` + CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL DEFAULT 0, + parent_id INTEGER DEFAULT 0, + title TEXT NOT NULL DEFAULT '', + content TEXT DEFAULT '', + sort_order INTEGER DEFAULT 0, + is_folder BOOLEAN DEFAULT FALSE, + tag TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + deleted_at DATETIME DEFAULT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_notes_parent_id ON notes(parent_id); + CREATE INDEX IF NOT EXISTS idx_notes_sort_order ON notes(sort_order); + CREATE INDEX IF NOT EXISTS idx_notes_deleted_at ON notes(deleted_at); + CREATE INDEX IF NOT EXISTS idx_notes_user_id ON notes(user_id); + + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + hashed_password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + ` + + // 迁移:给已有表加 deleted_at 列 + var count int + err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name='deleted_at'").Scan(&count) + if err == nil && count == 0 { + _, err = db.Exec("ALTER TABLE notes ADD COLUMN deleted_at DATETIME DEFAULT NULL") + if err != nil { + return err + } + } + + // 迁移:给已有表加 user_id 列 + err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name='user_id'").Scan(&count) + if err == nil && count == 0 { + _, err = db.Exec("ALTER TABLE notes ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0") + if err != nil { + return err + } + } + + _, err = db.Exec(query) + return err +} + +func (d *Database) Close() error { + return d.db.Close() +} + +// --- 笔记相关方法 --- + +func (d *Database) CreateNote(userID int64, parentID int64, title string, isFolder bool) (*models.Note, error) { + now := time.Now() + result, err := d.db.Exec(` + INSERT INTO notes (user_id, parent_id, title, is_folder, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + `, userID, parentID, title, isFolder, now, now) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + return &models.Note{ + ID: id, + ParentID: parentID, + Title: title, + IsFolder: isFolder, + CreatedAt: now, + UpdatedAt: now, + }, nil +} + +func (d *Database) GetNote(id int64) (*models.Note, error) { + note := &models.Note{} + err := d.db.QueryRow(` + SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at + FROM notes WHERE id = ? + `, id).Scan( + ¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content, + ¬e.SortOrder, ¬e.IsFolder, ¬e.Tag, + ¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt, + ) + if err != nil { + return nil, err + } + return note, nil +} + +func (d *Database) GetNotesByParentID(userID, parentID int64) ([]*models.Note, error) { + rows, err := d.db.Query(` + SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at + FROM notes WHERE user_id = ? AND parent_id = ? AND deleted_at IS NULL + ORDER BY sort_order, created_at + `, userID, parentID) + if err != nil { + return nil, err + } + defer rows.Close() + + var notes []*models.Note + for rows.Next() { + note := &models.Note{} + err := rows.Scan( + ¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content, + ¬e.SortOrder, ¬e.IsFolder, ¬e.Tag, + ¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt, + ) + if err != nil { + return nil, err + } + notes = append(notes, note) + } + return notes, nil +} + +func (d *Database) GetAllNotes(userID int64) ([]*models.Note, error) { + rows, err := d.db.Query(` + SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at + FROM notes WHERE user_id = ? AND deleted_at IS NULL AND is_folder = FALSE + ORDER BY sort_order, created_at + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var notes []*models.Note + for rows.Next() { + note := &models.Note{} + err := rows.Scan( + ¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content, + ¬e.SortOrder, ¬e.IsFolder, ¬e.Tag, + ¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt, + ) + if err != nil { + return nil, err + } + notes = append(notes, note) + } + return notes, nil +} + +func (d *Database) GetAllNotesWithFolders(userID int64) ([]*models.Note, error) { + rows, err := d.db.Query(` + SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at + FROM notes WHERE user_id = ? AND deleted_at IS NULL + ORDER BY sort_order, created_at + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var notes []*models.Note + for rows.Next() { + note := &models.Note{} + err := rows.Scan( + ¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content, + ¬e.SortOrder, ¬e.IsFolder, ¬e.Tag, + ¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt, + ) + if err != nil { + return nil, err + } + notes = append(notes, note) + } + return notes, nil +} + +func (d *Database) GetFolderTree(userID, folderID int64) (*models.Note, error) { + folder, err := d.GetNote(folderID) + if err != nil { + return nil, err + } + + children, err := d.GetNotesByParentID(userID, folderID) + if err != nil { + return nil, err + } + + folder.Children = make([]*models.Note, len(children)) + for i, child := range children { + if child.IsFolder { + childFolder, err := d.GetFolderTree(userID, child.ID) + if err == nil { + folder.Children[i] = childFolder + } else { + folder.Children[i] = child + } + } else { + folder.Children[i] = child + } + } + + return folder, nil +} + +func (d *Database) GetAllNotesTree(userID int64) ([]*models.NoteTree, error) { + notes, err := d.GetAllNotesWithFolders(userID) + if err != nil { + return nil, err + } + + return buildTree(notes, 0), nil +} + +func (d *Database) UpdateNote(id int64, title, content, tag string) error { + now := time.Now() + _, err := d.db.Exec(` + UPDATE notes SET title = ?, content = ?, tag = ?, updated_at = ? + WHERE id = ? + `, title, content, tag, now, id) + return err +} + +func (d *Database) DeleteNote(id int64) error { + now := time.Now() + + err := d.softDeleteChildren(id, now) + if err != nil { + return err + } + + _, err = d.db.Exec("UPDATE notes SET deleted_at = ? WHERE id = ?", now, id) + return err +} + +func (d *Database) softDeleteChildren(parentID int64, deletedAt time.Time) error { + rows, err := d.db.Query("SELECT id FROM notes WHERE parent_id = ? AND deleted_at IS NULL", parentID) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return err + } + if err := d.softDeleteChildren(id, deletedAt); err != nil { + return err + } + } + + _, err = d.db.Exec("UPDATE notes SET deleted_at = ? WHERE parent_id = ? AND deleted_at IS NULL", deletedAt, parentID) + return err +} + +func (d *Database) GetNoteTree(userID int64) ([]*models.NoteTree, error) { + notes, err := d.getAllNotes(userID) + if err != nil { + return nil, err + } + + return buildTree(notes, 0), nil +} + +func (d *Database) getAllNotes(userID int64) ([]*models.Note, error) { + rows, err := d.db.Query(` + SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at + FROM notes + WHERE user_id = ? AND deleted_at IS NULL + ORDER BY sort_order, created_at + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var notes []*models.Note + for rows.Next() { + note := &models.Note{} + err := rows.Scan( + ¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content, + ¬e.SortOrder, ¬e.IsFolder, ¬e.Tag, + ¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt, + ) + if err != nil { + return nil, err + } + notes = append(notes, note) + } + return notes, nil +} + +func buildTree(notes []*models.Note, parentID int64) []*models.NoteTree { + var result []*models.NoteTree + for _, note := range notes { + if note.ParentID == parentID { + node := &models.NoteTree{ + Note: *note, + } + node.Children = buildTree(notes, note.ID) + result = append(result, node) + } + } + return result +} + +func (d *Database) SearchNotes(userID int64, keyword string) ([]*models.Note, error) { + rows, err := d.db.Query(` + SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at + FROM notes + WHERE user_id = ? AND (title LIKE ? OR content LIKE ?) AND deleted_at IS NULL + ORDER BY updated_at DESC + `, userID, "%"+keyword+"%", "%"+keyword+"%") + if err != nil { + return nil, err + } + defer rows.Close() + + var notes []*models.Note + for rows.Next() { + note := &models.Note{} + err := rows.Scan( + ¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content, + ¬e.SortOrder, ¬e.IsFolder, ¬e.Tag, + ¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt, + ) + if err != nil { + return nil, err + } + notes = append(notes, note) + } + return notes, nil +} + +func (d *Database) UpdateSortOrder(id int64, sortOrder int) error { + _, err := d.db.Exec("UPDATE notes SET sort_order = ? WHERE id = ?", sortOrder, id) + return err +} + +func (d *Database) GetTrashNotes(userID int64) ([]*models.Note, error) { + rows, err := d.db.Query(` + SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at + FROM notes + WHERE user_id = ? AND deleted_at IS NOT NULL + ORDER BY deleted_at DESC + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var notes []*models.Note + for rows.Next() { + note := &models.Note{} + err := rows.Scan( + ¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content, + ¬e.SortOrder, ¬e.IsFolder, ¬e.Tag, + ¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt, + ) + if err != nil { + return nil, err + } + notes = append(notes, note) + } + return notes, nil +} + +func (d *Database) RestoreNote(id int64) error { + note, err := d.GetNote(id) + if err != nil { + return err + } + + if note.IsFolder { + err = d.restoreChildrenRecursive(id) + if err != nil { + return err + } + _, err = d.db.Exec("UPDATE notes SET deleted_at = NULL WHERE id = ?", id) + } else { + _, err = d.db.Exec("UPDATE notes SET deleted_at = NULL, parent_id = 0 WHERE id = ?", id) + } + return err +} + +func (d *Database) restoreChildrenRecursive(parentID int64) error { + var parentDeletedAt *time.Time + err := d.db.QueryRow("SELECT deleted_at FROM notes WHERE id = ?", parentID).Scan(&parentDeletedAt) + if err != nil || parentDeletedAt == nil { + return nil + } + + rows, err := d.db.Query( + "SELECT id, is_folder FROM notes WHERE parent_id = ? AND deleted_at = ?", + parentID, *parentDeletedAt, + ) + if err != nil { + return err + } + defer rows.Close() + + type childInfo struct { + id int64 + isFolder bool + } + var children []childInfo + + for rows.Next() { + var child childInfo + if err := rows.Scan(&child.id, &child.isFolder); err != nil { + return err + } + children = append(children, child) + } + + for _, child := range children { + if child.isFolder { + if err := d.restoreChildrenRecursive(child.id); err != nil { + return err + } + } + } + + if len(children) > 0 { + _, err = d.db.Exec("UPDATE notes SET deleted_at = NULL WHERE parent_id = ? AND deleted_at = ?", parentID, *parentDeletedAt) + if err != nil { + return err + } + } + + return nil +} + +func (d *Database) PermanentDeleteNote(id int64) error { + err := d.permanentDeleteChildren(id) + if err != nil { + return err + } + + _, err = d.db.Exec("DELETE FROM notes WHERE id = ?", id) + return err +} + +func (d *Database) permanentDeleteChildren(parentID int64) error { + rows, err := d.db.Query("SELECT id FROM notes WHERE parent_id = ?", parentID) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return err + } + if err := d.permanentDeleteChildren(id); err != nil { + return err + } + } + + _, err = d.db.Exec("DELETE FROM notes WHERE parent_id = ?", parentID) + return err +} + +func (d *Database) EmptyTrash(userID int64) error { + _, err := d.db.Exec("DELETE FROM notes WHERE user_id = ? AND deleted_at IS NOT NULL", userID) + return err +} + +// --- 用户相关方法 --- + +type UserRecord struct { + ID int64 `json:"id"` + HashedPassword string `json:"-"` + Email string `json:"email"` + CreatedAt time.Time `json:"createdAt"` +} + +func (d *Database) CreateUser(email, hashedPassword string) (*UserRecord, error) { + now := time.Now() + result, err := d.db.Exec(` + INSERT INTO users (email, hashed_password, created_at) + VALUES (?, ?, ?) + `, email, hashedPassword, now) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + return &UserRecord{ + ID: id, + HashedPassword: hashedPassword, + Email: email, + CreatedAt: now, + }, nil +} + +func (d *Database) GetUserByEmail(email string) (*UserRecord, error) { + user := &UserRecord{} + err := d.db.QueryRow(` + SELECT id, hashed_password, email, created_at + FROM users WHERE email = ? + `, email).Scan(&user.ID, &user.HashedPassword, &user.Email, &user.CreatedAt) + if err != nil { + return nil, err + } + return user, nil +} + +func (d *Database) GetUserByID(id int64) (*UserRecord, error) { + user := &UserRecord{} + err := d.db.QueryRow(` + SELECT id, hashed_password, email, created_at + FROM users WHERE id = ? + `, id).Scan(&user.ID, &user.HashedPassword, &user.Email, &user.CreatedAt) + if err != nil { + return nil, err + } + return user, nil +} + +func (d *Database) UpdatePassword(id int64, hashedPassword string) error { + _, err := d.db.Exec("UPDATE users SET hashed_password = ? WHERE id = ?", hashedPassword, id) + return err +} diff --git a/server/services/email.go b/server/services/email.go new file mode 100644 index 0000000..387b16a --- /dev/null +++ b/server/services/email.go @@ -0,0 +1,136 @@ +package services + +import ( + "crypto/tls" + "fmt" + "log" + "net" + "net/smtp" + "strings" +) + +type EmailConfig struct { + Host string + Port string + Username string + Password string + From string +} + +type EmailService struct { + cfg EmailConfig + configured bool +} + +func NewEmailService(cfg EmailConfig) *EmailService { + configured := cfg.Host != "" && cfg.Username != "" && cfg.Password != "" + if !configured { + log.Println("[Email] SMTP not configured, will print codes to console") + } + return &EmailService{cfg: cfg, configured: configured} +} + +// SendVerificationCode 发送验证码邮件 +func (s *EmailService) SendVerificationCode(to, code, purpose string) error { + var subject, body string + switch purpose { + case "register": + subject = "Kenaito Notebook - 注册验证码" + body = fmt.Sprintf(` +
+

Kenaito Notebook

+

你正在注册账号,验证码如下:

+
+ %s +
+

验证码 5 分钟内有效,请勿泄露给他人。

+
`, code) + case "reset": + subject = "Kenaito Notebook - 重置密码验证码" + body = fmt.Sprintf(` +
+

Kenaito Notebook

+

你正在重置密码,验证码如下:

+
+ %s +
+

验证码 5 分钟内有效,请勿泄露给他人。

+
`, code) + default: + return fmt.Errorf("unknown purpose: %s", purpose) + } + + if !s.configured { + log.Printf("[Email] SMTP not configured. Verification code for %s (%s): %s", to, purpose, code) + return nil + } + + return s.send(to, subject, body) +} + +func (s *EmailService) send(to, subject, htmlBody string) error { + addr := net.JoinHostPort(s.cfg.Host, s.cfg.Port) + + headers := make(map[string]string) + headers["From"] = s.cfg.From + headers["To"] = to + headers["Subject"] = subject + headers["MIME-Version"] = "1.0" + headers["Content-Type"] = "text/html; charset=UTF-8" + + var msg strings.Builder + for k, v := range headers { + msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v)) + } + msg.WriteString("\r\n") + msg.WriteString(htmlBody) + + auth := smtp.PlainAuth("", s.cfg.Username, s.cfg.Password, s.cfg.Host) + + // SSL connection (port 465) + if s.cfg.Port == "465" { + return s.sendTLS(addr, auth, to, msg.String()) + } + + // STARTTLS connection (port 587) + return smtp.SendMail(addr, auth, s.cfg.From, []string{to}, []byte(msg.String())) +} + +func (s *EmailService) sendTLS(addr string, auth smtp.Auth, to, msg string) error { + conn, err := tls.Dial("tcp", addr, &tls.Config{ + ServerName: s.cfg.Host, + }) + if err != nil { + return fmt.Errorf("failed to connect to SMTP server: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, s.cfg.Host) + if err != nil { + return fmt.Errorf("failed to create SMTP client: %w", err) + } + defer client.Close() + + if err = client.Auth(auth); err != nil { + return fmt.Errorf("SMTP auth failed: %w", err) + } + if err = client.Mail(s.cfg.From); err != nil { + return fmt.Errorf("SMTP MAIL FROM failed: %w", err) + } + if err = client.Rcpt(to); err != nil { + return fmt.Errorf("SMTP RCPT TO failed: %w", err) + } + + w, err := client.Data() + if err != nil { + return fmt.Errorf("SMTP DATA failed: %w", err) + } + if _, err = w.Write([]byte(msg)); err != nil { + return fmt.Errorf("SMTP write failed: %w", err) + } + if err = w.Close(); err != nil { + return fmt.Errorf("SMTP close data failed: %w", err) + } + + return client.Quit() +} diff --git a/server/services/export.go b/server/services/export.go new file mode 100644 index 0000000..d0b3f50 --- /dev/null +++ b/server/services/export.go @@ -0,0 +1,247 @@ +package services + +import ( + "archive/zip" + "fmt" + "html/template" + "os" + "path/filepath" + "strings" + "time" + + "github.com/yuin/goldmark" +) + +type ExportService struct{} + +func NewExportService() *ExportService { + return &ExportService{} +} + +func (es *ExportService) ExportHTML(title, content, outputPath string) error { + var htmlContent strings.Builder + if err := goldmark.Convert([]byte(content), &htmlContent); err != nil { + return err + } + + htmlTemplate := ` + + + + + {{.Title}} + + + +

{{.Title}}

+ {{.Content}} +
+

+ 导出于 {{.ExportTime}} | Kenaito Notebook +

+ +` + + tmpl, err := template.New("html").Parse(htmlTemplate) + if err != nil { + return err + } + + file, err := os.Create(outputPath) + if err != nil { + return err + } + defer file.Close() + + return tmpl.Execute(file, map[string]string{ + "Title": title, + "Content": htmlContent.String(), + "ExportTime": fmt.Sprintf("%s", time.Now().Format("2006-01-02 15:04:05")), + }) +} + +func (es *ExportService) ExportMarkdown(title, content, outputPath string) error { + fullContent := fmt.Sprintf("# %s\n\n%s", title, content) + return os.WriteFile(outputPath, []byte(fullContent), 0644) +} + +func (es *ExportService) ExportFolderMarkdown(folderTitle string, notes []NoteInfo, outputPath string) error { + var content strings.Builder + content.WriteString(fmt.Sprintf("# %s\n\n", folderTitle)) + + for _, note := range notes { + content.WriteString(fmt.Sprintf("## %s\n\n", note.Title)) + content.WriteString(note.Content) + content.WriteString("\n\n---\n\n") + } + + return os.WriteFile(outputPath, []byte(content.String()), 0644) +} + +func (es *ExportService) ExportAllMarkdown(notes []NoteInfo, outputPath string) error { + var content strings.Builder + content.WriteString("# 所有笔记\n\n") + + for _, note := range notes { + content.WriteString(fmt.Sprintf("## %s\n\n", note.Title)) + content.WriteString(note.Content) + content.WriteString("\n\n---\n\n") + } + + return os.WriteFile(outputPath, []byte(content.String()), 0644) +} + +func (es *ExportService) ExportFolderZip(folderTitle string, notes []*NoteInfo, outputPath string) error { + file, err := os.Create(outputPath) + if err != nil { + return err + } + defer file.Close() + + w := zip.NewWriter(file) + defer w.Close() + + rootFolder := es.GetSafeFilename(folderTitle) + "/" + _, err = w.Create(rootFolder) + if err != nil { + return err + } + + es.addNotesToZip(w, notes, rootFolder) + + return nil +} + +func (es *ExportService) ExportAllZip(notes []*NoteInfo, outputPath string) error { + file, err := os.Create(outputPath) + if err != nil { + return err + } + defer file.Close() + + w := zip.NewWriter(file) + defer w.Close() + + rootFolder := "所有笔记/" + _, err = w.Create(rootFolder) + if err != nil { + return err + } + + es.addNotesToZip(w, notes, rootFolder) + + return nil +} + +func (es *ExportService) addNotesToZip(w *zip.Writer, notes []*NoteInfo, parentPath string) { + for _, note := range notes { + safeName := es.GetSafeFilename(note.Title) + + if note.IsFolder { + folderPath := parentPath + safeName + "/" + _, _ = w.Create(folderPath) + + if note.Children != nil { + es.addNotesToZip(w, note.Children, folderPath) + } + } else { + filePath := parentPath + safeName + ".md" + fullContent := fmt.Sprintf("# %s\n\n%s", note.Title, note.Content) + + f, err := w.Create(filePath) + if err != nil { + continue + } + _, _ = f.Write([]byte(fullContent)) + } + } +} + +type NoteInfo struct { + Title string + Content string + IsFolder bool + Children []*NoteInfo +} + +func (es *ExportService) GetDefaultExportPath(title, ext string) string { + homeDir, _ := os.UserHomeDir() + exportDir := filepath.Join(homeDir, "Desktop") + safeName := es.GetSafeFilename(title) + return filepath.Join(exportDir, fmt.Sprintf("%s%s", safeName, ext)) +} + +func (es *ExportService) GetSafeFilename(title string) string { + safeName := strings.ReplaceAll(title, "/", "-") + safeName = strings.ReplaceAll(safeName, "\\", "-") + safeName = strings.ReplaceAll(safeName, ":", "-") + safeName = strings.ReplaceAll(safeName, "*", "") + safeName = strings.ReplaceAll(safeName, "?", "") + safeName = strings.ReplaceAll(safeName, "\"", "") + safeName = strings.ReplaceAll(safeName, "<", "") + safeName = strings.ReplaceAll(safeName, ">", "") + safeName = strings.ReplaceAll(safeName, "|", "") + + if strings.TrimSpace(safeName) == "" { + safeName = "未命名笔记" + } + + return safeName +} diff --git a/server/services/fileservice.go b/server/services/fileservice.go new file mode 100644 index 0000000..0aa39f8 --- /dev/null +++ b/server/services/fileservice.go @@ -0,0 +1,122 @@ +package services + +import ( + "encoding/base64" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" +) + +const MaxFileSize = 6 * 1024 * 1024 // 6MB + +type FileService struct { + dataDir string + baseURL string +} + +func NewFileService(baseURL string) (*FileService, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + dataDir := filepath.Join(homeDir, ".kenaito-notebook", "assets") + if err := os.MkdirAll(dataDir, 0755); err != nil { + return nil, err + } + + log.Printf("[FileService] Assets directory: %s", dataDir) + log.Printf("[FileService] Base URL: %s", baseURL) + + return &FileService{ + dataDir: dataDir, + baseURL: strings.TrimRight(baseURL, "/"), + }, nil +} + +func (fs *FileService) GetBaseURL() string { + return fs.baseURL +} + +func (fs *FileService) SaveImage(base64Data string) (string, error) { + parts := strings.Split(base64Data, ",") + if len(parts) != 2 { + return "", fmt.Errorf("invalid base64 format") + } + + header := parts[0] + ext := ".png" + if strings.Contains(header, "jpeg") || strings.Contains(header, "jpg") { + ext = ".jpg" + } else if strings.Contains(header, "gif") { + ext = ".gif" + } else if strings.Contains(header, "webp") { + ext = ".webp" + } + + data, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return "", err + } + + if int64(len(data)) > MaxFileSize { + return "", fmt.Errorf("文件大小超过 6MB 限制") + } + + filename := fmt.Sprintf("%s%s", uuid.New().String()[:8], ext) + + dateDir := time.Now().Format("2006/01") + dirPath := filepath.Join(fs.dataDir, dateDir) + if err := os.MkdirAll(dirPath, 0755); err != nil { + return "", err + } + + filePath := filepath.Join(dirPath, filename) + if err := os.WriteFile(filePath, data, 0644); err != nil { + return "", err + } + + return fmt.Sprintf("%s/api/files/assets/%s/%s", fs.baseURL, dateDir, filename), nil +} + +func (fs *FileService) SaveFile(base64Data, filename string) (string, error) { + parts := strings.Split(base64Data, ",") + if len(parts) != 2 { + return "", fmt.Errorf("invalid base64 format") + } + + data, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return "", err + } + + if int64(len(data)) > MaxFileSize { + return "", fmt.Errorf("文件大小超过 6MB 限制") + } + + ext := filepath.Ext(filename) + nameWithoutExt := strings.TrimSuffix(filename, ext) + uniqueFilename := fmt.Sprintf("%s_%s%s", nameWithoutExt, uuid.New().String()[:8], ext) + + dateDir := time.Now().Format("2006/01") + dirPath := filepath.Join(fs.dataDir, dateDir) + if err := os.MkdirAll(dirPath, 0755); err != nil { + return "", err + } + + filePath := filepath.Join(dirPath, uniqueFilename) + if err := os.WriteFile(filePath, data, 0644); err != nil { + return "", err + } + + return fmt.Sprintf("%s/api/files/assets/%s/%s", fs.baseURL, dateDir, uniqueFilename), nil +} + +func (fs *FileService) GetAssetsPath() string { + return fs.dataDir +} diff --git a/server/services/verification.go b/server/services/verification.go new file mode 100644 index 0000000..788224d --- /dev/null +++ b/server/services/verification.go @@ -0,0 +1,116 @@ +package services + +import ( + "fmt" + "math/rand" + "sync" + "time" +) + +type verificationEntry struct { + code string + expiresAt time.Time +} + +type VerificationService struct { + mu sync.RWMutex + codes map[string]*verificationEntry // key: "email:purpose" + times map[string]time.Time // key: "email:purpose" — 上次发送时间 +} + +func NewVerificationService() *VerificationService { + v := &VerificationService{ + codes: make(map[string]*verificationEntry), + times: make(map[string]time.Time), + } + // 定期清理过期验证码 + go v.cleanup() + return v +} + +// GenerateCode 生成 6 位随机验证码 +func (v *VerificationService) GenerateCode(email, purpose string) string { + code := fmt.Sprintf("%06d", rand.Intn(1000000)) + key := email + ":" + purpose + + v.mu.Lock() + v.codes[key] = &verificationEntry{ + code: code, + expiresAt: time.Now().Add(5 * time.Minute), + } + v.times[key] = time.Now() + v.mu.Unlock() + + return code +} + +// CheckRateLimit 检查是否在 60 秒限频内 +func (v *VerificationService) CheckRateLimit(email, purpose string) bool { + key := email + ":" + purpose + v.mu.RLock() + last, exists := v.times[key] + v.mu.RUnlock() + + if exists && time.Since(last) < 60*time.Second { + return false // 被限频 + } + return true +} + +// VerifyCode 验证验证码(不消耗,仅校验) +func (v *VerificationService) VerifyCode(email, code, purpose string) error { + key := email + ":" + purpose + v.mu.RLock() + entry, exists := v.codes[key] + v.mu.RUnlock() + + if !exists { + return fmt.Errorf("验证码不存在或已过期") + } + if time.Now().After(entry.expiresAt) { + return fmt.Errorf("验证码已过期") + } + if entry.code != code { + return fmt.Errorf("验证码错误") + } + return nil +} + +// UseCode 验证并消耗验证码(验证通过后删除) +func (v *VerificationService) UseCode(email, code, purpose string) error { + key := email + ":" + purpose + v.mu.Lock() + defer v.mu.Unlock() + + entry, exists := v.codes[key] + if !exists { + return fmt.Errorf("验证码不存在或已过期") + } + if time.Now().After(entry.expiresAt) { + delete(v.codes, key) + return fmt.Errorf("验证码已过期") + } + if entry.code != code { + return fmt.Errorf("验证码错误") + } + + delete(v.codes, key) + return nil +} + +// cleanup 定期清理过期验证码 +func (v *VerificationService) cleanup() { + ticker := time.NewTicker(10 * time.Minute) + defer ticker.Stop() + + for range ticker.C { + v.mu.Lock() + now := time.Now() + for key, entry := range v.codes { + if now.After(entry.expiresAt) { + delete(v.codes, key) + } + } + v.mu.Unlock() + } +} diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..b2857bd --- /dev/null +++ b/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "kenaito-notebook", + "outputfilename": "kenaito-notebook", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "http://localhost:5173", + "author": { + "name": "Odboy", + "email": "tianjun@odboy.cn" + } +}