9 Commits

Author SHA1 Message Date
4b0f39d747 Merge branch 'master' into dev 2025-11-21 23:37:36 +08:00
096cc1da94 🎨 Optimize hyperlink extension 2025-11-21 23:35:42 +08:00
2d3200ad97 ♻️ Refactor context menu 2025-11-21 22:30:47 +08:00
4e82e2f6f7 ♻️ Refactor the Markdown preview theme application logic 2025-11-21 20:20:06 +08:00
339ed53c2e ♻️ Refactor theme module 2025-11-21 00:03:03 +08:00
fc7c162e2f ♻️ Refactor theme module 2025-11-20 23:07:12 +08:00
dependabot[bot]
24f1549730 ⬆️ Bump golang.org/x/crypto from 0.44.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 03:04:41 +00:00
5584a46ca2 ♻️ Refactor theme module 2025-11-20 00:39:00 +08:00
4471441d6f ♻️ Refactor some code 2025-11-19 20:54:58 +08:00
61 changed files with 3314 additions and 4468 deletions

View File

@@ -1170,7 +1170,7 @@ export class Theme {
this["type"] = ("" as ThemeType); this["type"] = ("" as ThemeType);
} }
if (!("colors" in $$source)) { if (!("colors" in $$source)) {
this["colors"] = (new ThemeColorConfig()); this["colors"] = ({} as ThemeColorConfig);
} }
if (!("isDefault" in $$source)) { if (!("isDefault" in $$source)) {
this["isDefault"] = false; this["isDefault"] = false;
@@ -1199,303 +1199,9 @@ export class Theme {
} }
/** /**
* ThemeColorConfig 主题颜色配置(与前端 ThemeColors 接口保持一致) * ThemeColorConfig 使用与前端 ThemeColors 相同的结构,存储任意主题键值
*/ */
export class ThemeColorConfig { export type ThemeColorConfig = { [_: string]: any };
/**
* 主题基本信息
* 主题名称
*/
"name": string;
/**
* 是否为深色主题
*/
"dark": boolean;
/**
* 基础色调
* 主背景色
*/
"background": string;
/**
* 次要背景色(用于代码块交替背景)
*/
"backgroundSecondary": string;
/**
* 面板背景
*/
"surface": string;
/**
* 下拉菜单背景
*/
"dropdownBackground": string;
/**
* 下拉菜单边框
*/
"dropdownBorder": string;
/**
* 文本颜色
* 主文本色
*/
"foreground": string;
/**
* 次要文本色
*/
"foregroundSecondary": string;
/**
* 注释色
*/
"comment": string;
/**
* 语法高亮色 - 核心
* 关键字
*/
"keyword": string;
/**
* 字符串
*/
"string": string;
/**
* 函数名
*/
"function": string;
/**
* 数字
*/
"number": string;
/**
* 操作符
*/
"operator": string;
/**
* 变量
*/
"variable": string;
/**
* 类型
*/
"type": string;
/**
* 语法高亮色 - 扩展
* 常量
*/
"constant": string;
/**
* 存储类型(如 static, const
*/
"storage": string;
/**
* 参数
*/
"parameter": string;
/**
* 类名
*/
"class": string;
/**
* 标题Markdown等
*/
"heading": string;
/**
* 无效内容/错误
*/
"invalid": string;
/**
* 正则表达式
*/
"regexp": string;
/**
* 界面元素
* 光标
*/
"cursor": string;
/**
* 选中背景
*/
"selection": string;
/**
* 失焦选中背景
*/
"selectionBlur": string;
/**
* 当前行高亮
*/
"activeLine": string;
/**
* 行号
*/
"lineNumber": string;
/**
* 活动行号颜色
*/
"activeLineNumber": string;
/**
* 边框和分割线
* 边框色
*/
"borderColor": string;
/**
* 浅色边框
*/
"borderLight": string;
/**
* 搜索和匹配
* 搜索匹配
*/
"searchMatch": string;
/**
* 匹配括号
*/
"matchingBracket": string;
/** Creates a new ThemeColorConfig instance. */
constructor($$source: Partial<ThemeColorConfig> = {}) {
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("dark" in $$source)) {
this["dark"] = false;
}
if (!("background" in $$source)) {
this["background"] = "";
}
if (!("backgroundSecondary" in $$source)) {
this["backgroundSecondary"] = "";
}
if (!("surface" in $$source)) {
this["surface"] = "";
}
if (!("dropdownBackground" in $$source)) {
this["dropdownBackground"] = "";
}
if (!("dropdownBorder" in $$source)) {
this["dropdownBorder"] = "";
}
if (!("foreground" in $$source)) {
this["foreground"] = "";
}
if (!("foregroundSecondary" in $$source)) {
this["foregroundSecondary"] = "";
}
if (!("comment" in $$source)) {
this["comment"] = "";
}
if (!("keyword" in $$source)) {
this["keyword"] = "";
}
if (!("string" in $$source)) {
this["string"] = "";
}
if (!("function" in $$source)) {
this["function"] = "";
}
if (!("number" in $$source)) {
this["number"] = "";
}
if (!("operator" in $$source)) {
this["operator"] = "";
}
if (!("variable" in $$source)) {
this["variable"] = "";
}
if (!("type" in $$source)) {
this["type"] = "";
}
if (!("constant" in $$source)) {
this["constant"] = "";
}
if (!("storage" in $$source)) {
this["storage"] = "";
}
if (!("parameter" in $$source)) {
this["parameter"] = "";
}
if (!("class" in $$source)) {
this["class"] = "";
}
if (!("heading" in $$source)) {
this["heading"] = "";
}
if (!("invalid" in $$source)) {
this["invalid"] = "";
}
if (!("regexp" in $$source)) {
this["regexp"] = "";
}
if (!("cursor" in $$source)) {
this["cursor"] = "";
}
if (!("selection" in $$source)) {
this["selection"] = "";
}
if (!("selectionBlur" in $$source)) {
this["selectionBlur"] = "";
}
if (!("activeLine" in $$source)) {
this["activeLine"] = "";
}
if (!("lineNumber" in $$source)) {
this["lineNumber"] = "";
}
if (!("activeLineNumber" in $$source)) {
this["activeLineNumber"] = "";
}
if (!("borderColor" in $$source)) {
this["borderColor"] = "";
}
if (!("borderLight" in $$source)) {
this["borderLight"] = "";
}
if (!("searchMatch" in $$source)) {
this["searchMatch"] = "";
}
if (!("matchingBracket" in $$source)) {
this["matchingBracket"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new ThemeColorConfig instance from a string or object.
*/
static createFrom($$source: any = {}): ThemeColorConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ThemeColorConfig($$parsedSource as Partial<ThemeColorConfig>);
}
}
/** /**
* ThemeType 主题类型枚举 * ThemeType 主题类型枚举
@@ -1636,6 +1342,11 @@ var $$createType6 = (function $$initCreateType6(...args): any {
}); });
const $$createType7 = $Create.Map($Create.Any, $Create.Any); const $$createType7 = $Create.Map($Create.Any, $Create.Any);
const $$createType8 = HotkeyCombo.createFrom; const $$createType8 = HotkeyCombo.createFrom;
const $$createType9 = ThemeColorConfig.createFrom; var $$createType9 = (function $$initCreateType9(...args): any {
if ($$createType9 === $$initCreateType9) {
$$createType9 = $$createType7;
}
return $$createType9(...args);
});
const $$createType10 = GithubConfig.createFrom; const $$createType10 = GithubConfig.createFrom;
const $$createType11 = GiteaConfig.createFrom; const $$createType11 = GiteaConfig.createFrom;

View File

@@ -18,23 +18,10 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
import * as models$0 from "../models/models.js"; import * as models$0 from "../models/models.js";
/** /**
* GetAllThemes 获取所有主题 * GetThemeByName 通过名称获取主题覆盖,若不存在则返回 nil
*/ */
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } { export function GetThemeByName(name: string): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2425053076) as any; let $resultPromise = $Call.ByID(1938954770, name) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetThemeByID 根据ID或名称获取主题
* 如果 id > 0按ID查询如果 id = 0按名称查询
*/
export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(127385338, id, name) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType1($result);
}) as any; }) as any;
@@ -43,10 +30,10 @@ export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<model
} }
/** /**
* ResetTheme 重置主题为预设配置 * ResetTheme 删除指定主题的覆盖配置
*/ */
export function ResetTheme(id: number, ...name: string[]): Promise<void> & { cancel(): void } { export function ResetTheme(name: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1806334457, id, name) as any; let $resultPromise = $Call.ByID(1806334457, name) as any;
return $resultPromise; return $resultPromise;
} }
@@ -59,7 +46,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
} }
/** /**
* ServiceStartup 服务启动时初始化 * ServiceStartup 服务启动
*/ */
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2915959937, options) as any; let $resultPromise = $Call.ByID(2915959937, options) as any;
@@ -67,14 +54,13 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
} }
/** /**
* UpdateTheme 更新主题 * UpdateTheme 保存或更新主题覆盖
*/ */
export function UpdateTheme(id: number, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } { export function UpdateTheme(name: string, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(70189749, id, colors) as any; let $resultPromise = $Call.ByID(70189749, name, colors) as any;
return $resultPromise; return $resultPromise;
} }
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.Theme.createFrom; const $$createType0 = models$0.Theme.createFrom;
const $$createType1 = $Create.Nullable($$createType0); const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);

View File

@@ -1,8 +1,9 @@
/* 导入所有CSS文件 */ /* 导入所有CSS文件 */
@import 'normalize.css'; @import 'normalize.css';
@import 'variables.css';
@import 'scrollbar.css';
@import "harmony_fonts.css"; @import "harmony_fonts.css";
@import 'hack_fonts.css'; @import 'hack_fonts.css';
@import 'opensans_fonts.css'; @import 'opensans_fonts.css';
@import "monocraft_fonts.css"; @import "monocraft_fonts.css";
@import 'variables.css';
@import 'scrollbar.css';
@import 'styles.css';

View File

@@ -0,0 +1,3 @@
body {
background-color: var(--bg-primary);
}

View File

@@ -1,255 +1,148 @@
:root { :root {
/* 编辑器区域 */ --voidraft-font-mono: "HarmonyOS", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
--text-primary: #9BB586; /* 内容区域字体颜色 */
/* 深色主题颜色变量 */
--dark-toolbar-bg: #2d2d2d;
--dark-toolbar-border: #404040;
--dark-toolbar-text: #ffffff;
--dark-toolbar-text-secondary: #cccccc;
--dark-toolbar-button-hover: #404040;
--dark-tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
--dark-bg-secondary: #0E1217;
--dark-text-secondary: #a0aec0;
--dark-text-muted: #666;
--dark-border-color: #2d3748;
--dark-settings-bg: #2a2a2a;
--dark-settings-card-bg: #333333;
--dark-settings-text: #ffffff;
--dark-settings-text-secondary: #cccccc;
--dark-settings-border: #444444;
--dark-settings-input-bg: #3a3a3a;
--dark-settings-input-border: #555555;
--dark-settings-hover: #404040;
--dark-scrollbar-track: #2a2a2a;
--dark-scrollbar-thumb: #555555;
--dark-scrollbar-thumb-hover: #666666;
--dark-selection-bg: rgba(181, 206, 168, 0.1);
--dark-selection-text: #b5cea8;
--dark-danger-color: #ff6b6b;
--dark-bg-primary: #1a1a1a;
--dark-bg-hover: #2a2a2a;
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
--dark-loading-color: #fff;
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--dark-loading-done-color: #6f6;
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
/* 浅色主题颜色变量 */
--light-toolbar-bg: #f8f9fa;
--light-toolbar-border: #e9ecef;
--light-toolbar-text: #212529;
--light-toolbar-text-secondary: #495057;
--light-toolbar-button-hover: #e9ecef;
--light-tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--light-bg-secondary: #f7fef7;
--light-text-secondary: #374151;
--light-text-muted: #6b7280;
--light-border-color: #e5e7eb;
--light-settings-bg: #ffffff;
--light-settings-card-bg: #f8f9fa;
--light-settings-text: #212529;
--light-settings-text-secondary: #6c757d;
--light-settings-border: #dee2e6;
--light-settings-input-bg: #ffffff;
--light-settings-input-border: #ced4da;
--light-settings-hover: #e9ecef;
--light-scrollbar-track: #f1f3f4;
--light-scrollbar-thumb: #c1c1c1;
--light-scrollbar-thumb-hover: #a8a8a8;
--light-selection-bg: rgba(59, 130, 246, 0.15);
--light-selection-text: #2563eb;
--light-danger-color: #dc3545;
--light-bg-primary: #ffffff;
--light-bg-hover: #f1f3f4;
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--light-loading-color: #1a3c1a;
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--light-loading-done-color: #008800;
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* 默认使用深色主题 */
--toolbar-bg: var(--dark-toolbar-bg);
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover);
--tab-active-line: var(--dark-tab-active-line);
--bg-secondary: var(--dark-bg-secondary);
--text-secondary: var(--dark-text-secondary);
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg);
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary);
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border);
--settings-hover: var(--dark-settings-hover);
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
color-scheme: light dark;
} }
/* 监听系统深色主题 */ /* 默认/暗色主题 */
@media (prefers-color-scheme: dark) { :root,
:root[data-theme="auto"] { :root[data-theme="dark"],
--toolbar-bg: var(--dark-toolbar-bg); :root[data-theme="auto"] {
--toolbar-border: var(--dark-toolbar-border); color-scheme: dark;
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary); --text-primary: #ffffff;
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover); --toolbar-bg: #2d2d2d;
--tab-active-line: var(--dark-tab-active-line); --toolbar-border: #404040;
--bg-secondary: var(--dark-bg-secondary); --toolbar-text: #ffffff;
--text-secondary: var(--dark-text-secondary); --toolbar-text-secondary: #cccccc;
--text-muted: var(--dark-text-muted); --toolbar-button-hover: #404040;
--border-color: var(--dark-border-color); --toolbar-separator: #404040;
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg); --tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
--settings-text: var(--dark-settings-text); --bg-secondary: #0e1217;
--settings-text-secondary: var(--dark-settings-text-secondary); --bg-primary: #1a1a1a;
--settings-border: var(--dark-settings-border); --bg-hover: #2a2a2a;
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border); --text-secondary: #a0aec0;
--settings-hover: var(--dark-settings-hover); --text-muted: #666666;
--scrollbar-track: var(--dark-scrollbar-track); --text-danger: #ff6b6b;
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover); --border-color: #2d3748;
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text); --settings-bg: #2a2a2a;
--text-danger: var(--dark-danger-color); --settings-card-bg: #333333;
--bg-primary: var(--dark-bg-primary); --settings-text: #ffffff;
--bg-hover: var(--dark-bg-hover); --settings-text-secondary: #cccccc;
--voidraft-bg-gradient: var(--dark-loading-bg-gradient); --settings-border: #444444;
--voidraft-loading-color: var(--dark-loading-color); --settings-input-bg: #3a3a3a;
--voidraft-loading-glow: var(--dark-loading-glow); --settings-input-border: #555555;
--voidraft-loading-done-color: var(--dark-loading-done-color); --settings-hover: #404040;
--voidraft-loading-overlay: var(--dark-loading-overlay);
} --scrollbar-track: #2a2a2a;
--scrollbar-thumb: #555555;
--scrollbar-thumb-hover: #666666;
--selection-bg: rgba(181, 206, 168, 0.1);
--selection-text: #b5cea8;
--voidraft-bg-gradient: radial-gradient(#222922, #000500);
--voidraft-loading-color: #ffffff;
--voidraft-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--voidraft-loading-done-color: #66ff66;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
} }
/* 监听系统浅色主题 */ /* 色主题 */
:root[data-theme="light"] {
color-scheme: light;
--text-primary: #000000;
--toolbar-bg: #f8f9fa;
--toolbar-border: #e9ecef;
--toolbar-text: #212529;
--toolbar-text-secondary: #495057;
--toolbar-button-hover: #e9ecef;
--toolbar-separator: #e9ecef;
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--bg-secondary: #f7fef7;
--bg-primary: #ffffff;
--bg-hover: #f1f3f4;
--text-secondary: #374151;
--text-muted: #6b7280;
--text-danger: #dc3545;
--border-color: #e5e7eb;
--settings-bg: #ffffff;
--settings-card-bg: #f8f9fa;
--settings-text: #212529;
--settings-text-secondary: #6c757d;
--settings-border: #dee2e6;
--settings-input-bg: #ffffff;
--settings-input-border: #ced4da;
--settings-hover: #e9ecef;
--scrollbar-track: #f1f3f4;
--scrollbar-thumb: #c1c1c1;
--scrollbar-thumb-hover: #a8a8a8;
--selection-bg: rgba(59, 130, 246, 0.15);
--selection-text: #2563eb;
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--voidraft-loading-color: #1a3c1a;
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--voidraft-loading-done-color: #008800;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
}
/* 跟随系统的浅色偏好 */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root[data-theme="auto"] { :root[data-theme="auto"] {
--toolbar-bg: var(--light-toolbar-bg); color-scheme: light;
--toolbar-border: var(--light-toolbar-border);
--toolbar-text: var(--light-toolbar-text); --text-primary: #000000;
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
--toolbar-button-hover: var(--light-toolbar-button-hover); --toolbar-bg: #f8f9fa;
--toolbar-separator: var(--light-toolbar-button-hover); --toolbar-border: #e9ecef;
--tab-active-line: var(--light-tab-active-line); --toolbar-text: #212529;
--bg-secondary: var(--light-bg-secondary); --toolbar-text-secondary: #495057;
--text-secondary: var(--light-text-secondary); --toolbar-button-hover: #e9ecef;
--text-muted: var(--light-text-muted); --toolbar-separator: #e9ecef;
--border-color: var(--light-border-color);
--settings-bg: var(--light-settings-bg); --tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--settings-card-bg: var(--light-settings-card-bg); --bg-secondary: #f7fef7;
--settings-text: var(--light-settings-text); --bg-primary: #ffffff;
--settings-text-secondary: var(--light-settings-text-secondary); --bg-hover: #f1f3f4;
--settings-border: var(--light-settings-border);
--settings-input-bg: var(--light-settings-input-bg); --text-secondary: #374151;
--settings-input-border: var(--light-settings-input-border); --text-muted: #6b7280;
--settings-hover: var(--light-settings-hover); --text-danger: #dc3545;
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb); --border-color: #e5e7eb;
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg); --settings-bg: #ffffff;
--selection-text: var(--light-selection-text); --settings-card-bg: #f8f9fa;
--text-danger: var(--light-danger-color); --settings-text: #212529;
--bg-primary: var(--light-bg-primary); --settings-text-secondary: #6c757d;
--bg-hover: var(--light-bg-hover); --settings-border: #dee2e6;
--voidraft-bg-gradient: var(--light-loading-bg-gradient); --settings-input-bg: #ffffff;
--voidraft-loading-color: var(--light-loading-color); --settings-input-border: #ced4da;
--voidraft-loading-glow: var(--light-loading-glow); --settings-hover: #e9ecef;
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay); --scrollbar-track: #f1f3f4;
--scrollbar-thumb: #c1c1c1;
--scrollbar-thumb-hover: #a8a8a8;
--selection-bg: rgba(59, 130, 246, 0.15);
--selection-text: #2563eb;
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--voidraft-loading-color: #1a3c1a;
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--voidraft-loading-done-color: #008800;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
} }
} }
/* 手动选择浅色主题 */
:root[data-theme="light"] {
--toolbar-bg: var(--light-toolbar-bg);
--toolbar-border: var(--light-toolbar-border);
--toolbar-text: var(--light-toolbar-text);
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
--toolbar-button-hover: var(--light-toolbar-button-hover);
--toolbar-separator: var(--light-toolbar-button-hover);
--tab-active-line: var(--light-tab-active-line);
--bg-secondary: var(--light-bg-secondary);
--text-secondary: var(--light-text-secondary);
--text-muted: var(--light-text-muted);
--border-color: var(--light-border-color);
--settings-bg: var(--light-settings-bg);
--settings-card-bg: var(--light-settings-card-bg);
--settings-text: var(--light-settings-text);
--settings-text-secondary: var(--light-settings-text-secondary);
--settings-border: var(--light-settings-border);
--settings-input-bg: var(--light-settings-input-bg);
--settings-input-border: var(--light-settings-input-border);
--settings-hover: var(--light-settings-hover);
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb);
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg);
--selection-text: var(--light-selection-text);
--text-danger: var(--light-danger-color);
--bg-primary: var(--light-bg-primary);
--bg-hover: var(--light-bg-hover);
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
--voidraft-loading-color: var(--light-loading-color);
--voidraft-loading-glow: var(--light-loading-glow);
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay);
}
/* 手动选择深色主题 */
:root[data-theme="dark"] {
--toolbar-bg: var(--dark-toolbar-bg);
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover);
--tab-active-line: var(--dark-tab-active-line);
--bg-secondary: var(--dark-bg-secondary);
--text-secondary: var(--dark-text-secondary);
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg);
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary);
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border);
--settings-hover: var(--dark-settings-hover);
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
}

View File

@@ -142,7 +142,7 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-family: var(--voidraft-mono-font, monospace),serif; font-family: var(--voidraft-font-mono),serif;
} }
.loading-word { .loading-word {

View File

@@ -147,7 +147,7 @@ onUnmounted(() => {
padding: 8px 12px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
color: var(--text-muted); color: var(--text-primary);
transition: all 0.15s ease; transition: all 0.15s ease;
gap: 8px; gap: 8px;
@@ -165,7 +165,7 @@ onUnmounted(() => {
flex-shrink: 0; flex-shrink: 0;
width: 12px; width: 12px;
height: 12px; height: 12px;
color: var(--text-muted); color: var(--text-primary);
transition: color 0.15s ease; transition: color 0.15s ease;
.menu-item:hover & { .menu-item:hover & {

View File

@@ -161,53 +161,6 @@ export default {
customThemeColors: 'Custom Theme Colors', customThemeColors: 'Custom Theme Colors',
resetToDefault: 'Reset to Default', resetToDefault: 'Reset to Default',
colorValue: 'Color Value', colorValue: 'Color Value',
themeColors: {
basic: 'Basic Colors',
text: 'Text Colors',
syntax: 'Syntax Highlighting',
interface: 'Interface Elements',
border: 'Borders & Dividers',
search: 'Search & Matching',
// Base Colors
background: 'Main Background',
backgroundSecondary: 'Secondary Background',
surface: 'Panel Background',
dropdownBackground: 'Dropdown Background',
dropdownBorder: 'Dropdown Border',
// Text Colors
foreground: 'Primary Text',
foregroundSecondary: 'Secondary Text',
comment: 'Comments',
// Syntax Highlighting - Core
keyword: 'Keywords',
string: 'Strings',
function: 'Functions',
number: 'Numbers',
operator: 'Operators',
variable: 'Variables',
type: 'Types',
// Syntax Highlighting - Extended
constant: 'Constants',
storage: 'Storage Type',
parameter: 'Parameters',
class: 'Class Names',
heading: 'Headings',
invalid: 'Invalid/Error',
regexp: 'Regular Expressions',
// Interface Elements
cursor: 'Cursor',
selection: 'Selection Background',
selectionBlur: 'Unfocused Selection',
activeLine: 'Active Line Highlight',
lineNumber: 'Line Numbers',
activeLineNumber: 'Active Line Number',
// Borders & Dividers
borderColor: 'Border Color',
borderLight: 'Light Border',
// Search & Matching
searchMatch: 'Search Match',
matchingBracket: 'Matching Bracket'
},
lineHeight: 'Line Height', lineHeight: 'Line Height',
tabSettings: 'Tab Settings', tabSettings: 'Tab Settings',
tabSize: 'Tab Size', tabSize: 'Tab Size',

View File

@@ -202,54 +202,6 @@ export default {
customThemeColors: '自定义主题颜色', customThemeColors: '自定义主题颜色',
resetToDefault: '重置为默认', resetToDefault: '重置为默认',
colorValue: '颜色值', colorValue: '颜色值',
themeColors: {
basic: '基础色调',
text: '文本颜色',
syntax: '语法高亮',
interface: '界面元素',
border: '边框分割线',
search: '搜索匹配',
// 基础色调
background: '主背景色',
backgroundSecondary: '次要背景色',
surface: '面板背景',
dropdownBackground: '下拉菜单背景',
dropdownBorder: '下拉菜单边框',
// 文本颜色
foreground: '主文本色',
foregroundSecondary: '次要文本色',
comment: '注释色',
// 语法高亮 - 核心
keyword: '关键字',
string: '字符串',
function: '函数名',
number: '数字',
operator: '操作符',
variable: '变量',
type: '类型',
// 语法高亮 - 扩展
constant: '常量',
storage: '存储类型',
parameter: '参数',
class: '类名',
heading: '标题',
invalid: '无效内容',
regexp: '正则表达式',
// 界面元素
cursor: '光标',
selection: '选中背景',
selectionBlur: '失焦选中背景',
activeLine: '当前行高亮',
lineNumber: '行号',
activeLineNumber: '活动行号',
// 边框和分割线
borderColor: '边框色',
borderLight: '浅色边框',
// 搜索和匹配
searchMatch: '搜索匹配',
matchingBracket: '匹配括号'
},
hotkeyPreview: '预览:', hotkeyPreview: '预览:',
none: '无', none: '无',
backup: { backup: {

View File

@@ -14,6 +14,7 @@ import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtensi
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension'; import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension'; import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
import { import {
createDynamicExtensions, createDynamicExtensions,
@@ -29,7 +30,7 @@ import {generateContentHash} from "@/common/utils/hashUtils";
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils'; import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
import {EDITOR_CONFIG} from '@/common/constant/editor'; import {EDITOR_CONFIG} from '@/common/constant/editor';
import {createHttpClientExtension} from "@/views/editor/extensions/httpclient"; import {createHttpClientExtension} from "@/views/editor/extensions/httpclient";
import {markdownPreviewExtension} from "@/views/editor/extensions/markdownPreview"; import {markdownPreviewExtension, updateMarkdownPreviewTheme} from "@/views/editor/extensions/markdownPreview";
import {createDebounce} from '@/common/utils/debounce'; import {createDebounce} from '@/common/utils/debounce';
export interface DocumentStats { export interface DocumentStats {
@@ -242,6 +243,11 @@ export const useEditorStore = defineStore('editor', () => {
fontWeight: configStore.config.editing.fontWeight fontWeight: configStore.config.editing.fontWeight
}); });
const wheelZoomExtension = createWheelZoomExtension(
() => configStore.increaseFontSize(),
() => configStore.decreaseFontSize()
);
// 统计扩展 // 统计扩展
const statsExtension = createStatsUpdateExtension(updateDocumentStats); const statsExtension = createStatsUpdateExtension(updateDocumentStats);
@@ -287,6 +293,7 @@ export const useEditorStore = defineStore('editor', () => {
themeExtension, themeExtension,
...tabExtensions, ...tabExtensions,
fontExtension, fontExtension,
wheelZoomExtension,
statsExtension, statsExtension,
contentChangeExtension, contentChangeExtension,
codeBlockExtension, codeBlockExtension,
@@ -635,6 +642,13 @@ export const useEditorStore = defineStore('editor', () => {
}); });
}; };
// 应用 Markdown 预览主题
const applyPreviewThemeSettings = () => {
editorCache.values().forEach(instance => {
updateMarkdownPreviewTheme(instance.view);
});
};
// 应用Tab设置 // 应用Tab设置
const applyTabSettings = () => { const applyTabSettings = () => {
editorCache.values().forEach(instance => { editorCache.values().forEach(instance => {
@@ -707,12 +721,15 @@ export const useEditorStore = defineStore('editor', () => {
// 更新前端编辑器扩展 - 应用于所有实例 // 更新前端编辑器扩展 - 应用于所有实例
const manager = getExtensionManager(); const manager = getExtensionManager();
if (manager) { if (manager) {
// 使用立即更新模式,跳过防抖 // 直接更新前端扩展至所有视图
manager.updateExtensionImmediate(id, enabled, config || {}); manager.updateExtension(id, enabled, config);
} }
// 重新加载扩展配置 // 重新加载扩展配置
await extensionStore.loadExtensions(); await extensionStore.loadExtensions();
if (manager) {
manager.initExtensions(extensionStore.extensions);
}
await applyKeymapSettings(); await applyKeymapSettings();
}; };
@@ -773,6 +790,7 @@ export const useEditorStore = defineStore('editor', () => {
// 配置更新方法 // 配置更新方法
applyFontSettings, applyFontSettings,
applyThemeSettings, applyThemeSettings,
applyPreviewThemeSettings,
applyTabSettings, applyTabSettings,
applyKeymapSettings, applyKeymapSettings,

View File

@@ -1,191 +1,157 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import {SystemThemeType, ThemeType, Theme, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models'; import { SystemThemeType, ThemeType, ThemeColorConfig } from '@/../bindings/voidraft/internal/models/models';
import { ThemeService } from '@/../bindings/voidraft/internal/services'; import { ThemeService } from '@/../bindings/voidraft/internal/services';
import { useConfigStore } from './configStore'; import { useConfigStore } from './configStore';
import { useEditorStore } from './editorStore'; import { useEditorStore } from './editorStore';
import type { ThemeColors } from '@/views/editor/theme/types'; import type { ThemeColors } from '@/views/editor/theme/types';
import { cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap } from '@/views/editor/theme/presets';
type ThemeOption = { name: string; type: ThemeType };
const resolveThemeName = (name?: string) =>
name && themePresetMap[name] ? name : FALLBACK_THEME_NAME;
const createThemeOptions = (type: ThemeType): ThemeOption[] =>
themePresetList
.filter(preset => preset.type === type)
.map(preset => ({ name: preset.name, type: preset.type }));
const darkThemeOptions = createThemeOptions(ThemeType.ThemeTypeDark);
const lightThemeOptions = createThemeOptions(ThemeType.ThemeTypeLight);
const cloneColors = (colors: ThemeColorConfig): ThemeColors =>
JSON.parse(JSON.stringify(colors)) as ThemeColors;
const getPresetColors = (name: string): ThemeColors => {
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
const colors = cloneThemeColors(preset.colors);
colors.themeName = name;
return colors;
};
const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => {
const safeName = resolveThemeName(themeName);
try {
const theme = await ThemeService.GetThemeByName(safeName);
if (theme?.colors) {
const colors = cloneColors(theme.colors);
colors.themeName = safeName;
return colors;
}
} catch (error) {
console.error('Failed to load theme override:', error);
}
return getPresetColors(safeName);
};
/**
* 主题管理 Store
* 职责:管理主题状态、颜色配置和预设主题列表
*/
export const useThemeStore = defineStore('theme', () => { export const useThemeStore = defineStore('theme', () => {
const configStore = useConfigStore(); const configStore = useConfigStore();
// 所有主题列表
const allThemes = ref<Theme[]>([]);
// 当前主题的颜色配置
const currentColors = ref<ThemeColors | null>(null); const currentColors = ref<ThemeColors | null>(null);
// 计算属性:当前系统主题模式 const currentTheme = computed(
const currentTheme = computed(() => () => configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
); );
// 计算属性:当前是否为深色模式 const isDarkMode = computed(
const isDarkMode = computed(() => () =>
currentTheme.value === SystemThemeType.SystemThemeDark || currentTheme.value === SystemThemeType.SystemThemeDark ||
(currentTheme.value === SystemThemeType.SystemThemeAuto && (currentTheme.value === SystemThemeType.SystemThemeAuto &&
window.matchMedia('(prefers-color-scheme: dark)').matches) window.matchMedia('(prefers-color-scheme: dark)').matches)
); );
// 计算属性:根据类型获取主题列表 const availableThemes = computed<ThemeOption[]>(() =>
const darkThemes = computed(() => isDarkMode.value ? darkThemeOptions : lightThemeOptions
allThemes.value.filter(t => t.type === ThemeType.ThemeTypeDark)
); );
const lightThemes = computed(() =>
allThemes.value.filter(t => t.type === ThemeType.ThemeTypeLight)
);
// 计算属性:当前可用的主题列表
const availableThemes = computed(() =>
isDarkMode.value ? darkThemes.value : lightThemes.value
);
// 应用主题到 DOM
const applyThemeToDOM = (theme: SystemThemeType) => { const applyThemeToDOM = (theme: SystemThemeType) => {
const themeMap = { const themeMap = {
[SystemThemeType.SystemThemeAuto]: 'auto', [SystemThemeType.SystemThemeAuto]: 'auto',
[SystemThemeType.SystemThemeDark]: 'dark', [SystemThemeType.SystemThemeDark]: 'dark',
[SystemThemeType.SystemThemeLight]: 'light' [SystemThemeType.SystemThemeLight]: 'light',
}; };
document.documentElement.setAttribute('data-theme', themeMap[theme]); document.documentElement.setAttribute('data-theme', themeMap[theme]);
}; };
// 从数据库加载所有主题 const loadThemeColors = async (themeName?: string) => {
const loadAllThemes = async () => { const targetName = resolveThemeName(
try { themeName || configStore.config?.appearance?.currentTheme
const themes = await ThemeService.GetAllThemes(); );
allThemes.value = (themes || []).filter((t): t is Theme => t !== null); currentColors.value = await fetchThemeColors(targetName);
return allThemes.value;
} catch (error) {
console.error('Failed to load themes from database:', error);
allThemes.value = [];
return [];
}
}; };
// 初始化主题颜色
const initializeThemeColors = async () => {
// 加载所有主题
await loadAllThemes();
// 从配置获取当前主题名称并加载
const currentThemeName = configStore.config?.appearance?.currentTheme || 'default-dark';
const theme = allThemes.value.find(t => t.name === currentThemeName);
if (!theme) {
console.error(`Theme not found: ${currentThemeName}`);
return;
}
// 直接设置当前主题颜色
currentColors.value = theme.colors as ThemeColors;
};
// 初始化主题
const initializeTheme = async () => { const initializeTheme = async () => {
const theme = currentTheme.value; applyThemeToDOM(currentTheme.value);
applyThemeToDOM(theme); await loadThemeColors();
await initializeThemeColors();
}; };
// 设置系统主题模式(深色/浅色/自动)
const setTheme = async (theme: SystemThemeType) => { const setTheme = async (theme: SystemThemeType) => {
await configStore.setSystemTheme(theme); await configStore.setSystemTheme(theme);
applyThemeToDOM(theme); applyThemeToDOM(theme);
refreshEditorTheme(); refreshEditorTheme();
}; };
// 切换到指定的预设主题
const switchToTheme = async (themeName: string) => { const switchToTheme = async (themeName: string) => {
const theme = allThemes.value.find(t => t.name === themeName); if (!themePresetMap[themeName]) {
if (!theme) {
console.error('Theme not found:', themeName); console.error('Theme not found:', themeName);
return false; return false;
} }
// 直接设置当前主题颜色 await loadThemeColors(themeName);
currentColors.value = theme.colors as ThemeColors;
// 持久化到配置
await configStore.setCurrentTheme(themeName); await configStore.setCurrentTheme(themeName);
// 刷新编辑器
refreshEditorTheme(); refreshEditorTheme();
return true; return true;
}; };
// 更新当前主题的颜色配置
const updateCurrentColors = (colors: Partial<ThemeColors>) => { const updateCurrentColors = (colors: Partial<ThemeColors>) => {
if (!currentColors.value) return; if (!currentColors.value) return;
Object.assign(currentColors.value, colors); Object.assign(currentColors.value, colors);
}; };
// 保存当前主题颜色到数据库
const saveCurrentTheme = async () => { const saveCurrentTheme = async () => {
if (!currentColors.value) { if (!currentColors.value) {
throw new Error('No theme selected'); throw new Error('No theme selected');
} }
const theme = allThemes.value.find(t => t.name === currentColors.value!.name); const themeName = resolveThemeName(currentColors.value.themeName);
if (!theme) { currentColors.value.themeName = themeName;
throw new Error('Theme not found');
}
await ThemeService.UpdateTheme(theme.id, currentColors.value as ThemeColorConfig); await ThemeService.UpdateTheme(themeName, currentColors.value as unknown as ThemeColorConfig);
await loadThemeColors(themeName);
refreshEditorTheme();
return true; return true;
}; };
// 重置当前主题为预设配置
const resetCurrentTheme = async () => { const resetCurrentTheme = async () => {
if (!currentColors.value) { if (!currentColors.value) {
throw new Error('No theme selected'); throw new Error('No theme selected');
} }
// 调用后端重置 const themeName = resolveThemeName(currentColors.value.themeName);
await ThemeService.ResetTheme(0, currentColors.value.name); await ThemeService.ResetTheme(themeName);
// 重新加载所有主题
await loadAllThemes();
const updatedTheme = allThemes.value.find(t => t.name === currentColors.value!.name);
if (updatedTheme) {
currentColors.value = updatedTheme.colors as ThemeColors;
}
await loadThemeColors(themeName);
refreshEditorTheme(); refreshEditorTheme();
return true; return true;
}; };
// 刷新编辑器主题
const refreshEditorTheme = () => { const refreshEditorTheme = () => {
applyThemeToDOM(currentTheme.value); applyThemeToDOM(currentTheme.value);
const editorStore = useEditorStore(); const editorStore = useEditorStore();
editorStore?.applyThemeSettings(); editorStore?.applyThemeSettings();
editorStore?.applyPreviewThemeSettings();
}; };
return { return {
// 状态
allThemes,
darkThemes,
lightThemes,
availableThemes, availableThemes,
currentTheme, currentTheme,
currentColors, currentColors,
isDarkMode, isDarkMode,
// 方法
setTheme, setTheme,
switchToTheme, switchToTheme,
initializeTheme, initializeTheme,
loadAllThemes,
updateCurrentColors, updateCurrentColors,
saveCurrentTheme, saveCurrentTheme,
resetCurrentTheme, resetCurrentTheme,

View File

@@ -1,13 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref} from 'vue'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import {useEditorStore} from '@/stores/editorStore'; import { useEditorStore } from '@/stores/editorStore';
import {useDocumentStore} from '@/stores/documentStore'; import { useDocumentStore } from '@/stores/documentStore';
import {useConfigStore} from '@/stores/configStore'; import { useConfigStore } from '@/stores/configStore';
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
import Toolbar from '@/components/toolbar/Toolbar.vue'; import Toolbar from '@/components/toolbar/Toolbar.vue';
import {useWindowStore} from "@/stores/windowStore"; import { useWindowStore } from '@/stores/windowStore';
import LoadingScreen from '@/components/loading/LoadingScreen.vue'; import LoadingScreen from '@/components/loading/LoadingScreen.vue';
import {useTabStore} from "@/stores/tabStore"; import { useTabStore } from '@/stores/tabStore';
import ContextMenu from './contextMenu/ContextMenu.vue';
import { contextMenuManager } from './contextMenu/manager';
const editorStore = useEditorStore(); const editorStore = useEditorStore();
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
@@ -19,47 +20,31 @@ const editorElement = ref<HTMLElement | null>(null);
const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation); const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation);
// 创建滚轮缩放处理器
const wheelHandler = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
);
onMounted(async () => { onMounted(async () => {
if (!editorElement.value) return; if (!editorElement.value) return;
// 从URL查询参数中获取documentId
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined; const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
// 初始化文档存储优先使用URL参数中的文档ID
await documentStore.initialize(urlDocumentId); await documentStore.initialize(urlDocumentId);
// 设置编辑器容器
editorStore.setEditorContainer(editorElement.value); editorStore.setEditorContainer(editorElement.value);
await tabStore.initializeTab(); await tabStore.initializeTab();
// 添加滚轮事件监听
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 移除滚轮事件监听 contextMenuManager.destroy();
if (editorElement.value) {
editorElement.value.removeEventListener('wheel', wheelHandler);
}
editorStore.clearAllEditors();
}); });
</script> </script>
<template> <template>
<div class="editor-container"> <div class="editor-container">
<div ref="editorElement" class="editor"></div>
<Toolbar/>
<transition name="loading-fade"> <transition name="loading-fade">
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/> <LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT" />
</transition> </transition>
<div ref="editorElement" class="editor"></div>
<Toolbar />
<ContextMenu :portal-target="editorElement" />
</div> </div>
</template> </template>
@@ -76,6 +61,7 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
position: relative;
} }
} }
@@ -88,7 +74,6 @@ onBeforeUnmount(() => {
overflow: auto; overflow: auto;
} }
// 加载动画过渡效果
.loading-fade-enter-active, .loading-fade-enter-active,
.loading-fade-leave-active { .loading-fade-leave-active {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;

View File

@@ -1,33 +1,47 @@
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'; import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
import { useEditorStore } from '@/stores/editorStore'; import type {Text} from '@codemirror/state';
import {useEditorStore} from '@/stores/editorStore';
/** /**
* 内容变化监听插件 - 集成文档和编辑器管理
*/ */
export function createContentChangePlugin() { export function createContentChangePlugin() {
return ViewPlugin.fromClass( return ViewPlugin.fromClass(
class ContentChangePlugin { class ContentChangePlugin {
private editorStore = useEditorStore(); private readonly editorStore = useEditorStore();
private lastContent = ''; private lastDoc: Text;
private rafId: number | null = null;
private pendingNotification = false;
constructor(private view: EditorView) { constructor(private view: EditorView) {
this.lastContent = view.state.doc.toString(); this.lastDoc = view.state.doc;
} }
update(update: ViewUpdate) { update(update: ViewUpdate) {
if (!update.docChanged) return; if (!update.docChanged || update.state.doc === this.lastDoc) {
return;
const newContent = this.view.state.doc.toString(); }
if (newContent === this.lastContent) return;
this.lastContent = newContent;
this.editorStore.onContentChange();
this.lastDoc = update.state.doc;
this.scheduleNotification();
} }
destroy() { destroy() {
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.pendingNotification = false;
}
private scheduleNotification() {
if (this.pendingNotification) return;
this.pendingNotification = true;
this.rafId = requestAnimationFrame(() => {
this.pendingNotification = false;
this.rafId = null;
this.editorStore.onContentChange();
});
} }
} }
); );

View File

@@ -1,22 +1,40 @@
// 处理滚轮缩放字体的事件处理函数 import {EditorView} from '@codemirror/view';
export const createWheelZoomHandler = ( import type {Extension} from '@codemirror/state';
increaseFontSize: () => void,
decreaseFontSize: () => void type FontAdjuster = () => Promise<void> | void;
) => {
return (event: WheelEvent) => { const runAdjuster = (adjuster: FontAdjuster) => {
// 检查是否按住了Ctrl键 try {
if (event.ctrlKey) { const result = adjuster();
// 阻止默认行为(防止页面缩放) if (result && typeof (result as Promise<void>).then === 'function') {
(result as Promise<void>).catch((error) => {
console.error('Failed to adjust font size:', error);
});
}
} catch (error) {
console.error('Failed to adjust font size:', error);
}
};
export const createWheelZoomExtension = (
increaseFontSize: FontAdjuster,
decreaseFontSize: FontAdjuster
): Extension => {
return EditorView.domEventHandlers({
wheel(event) {
if (!event.ctrlKey) {
return false;
}
event.preventDefault(); event.preventDefault();
// 根据滚轮方向增大或减小字体
if (event.deltaY < 0) { if (event.deltaY < 0) {
// 向上滚动,增大字体 runAdjuster(increaseFontSize);
increaseFontSize(); } else if (event.deltaY > 0) {
} else { runAdjuster(decreaseFontSize);
// 向下滚动,减小字体
decreaseFontSize();
} }
return true;
} }
}; });
}; };

View File

@@ -0,0 +1,180 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { contextMenuManager } from './manager';
import type { RenderMenuItem } from './menuSchema';
const props = defineProps<{
portalTarget?: HTMLElement | null;
}>();
const menuState = contextMenuManager.useState();
const menuRef = ref<HTMLDivElement | null>(null);
const adjustedPosition = ref({ x: 0, y: 0 });
const isVisible = computed(() => menuState.value.visible);
const items = computed(() => menuState.value.items);
const position = computed(() => menuState.value.position);
const teleportTarget = computed<HTMLElement | string>(() => props.portalTarget ?? 'body');
watch(
position,
(newPosition) => {
adjustedPosition.value = { ...newPosition };
if (isVisible.value) {
nextTick(adjustMenuWithinViewport);
}
},
{ deep: true }
);
watch(isVisible, (visible) => {
if (visible) {
nextTick(adjustMenuWithinViewport);
}
});
const menuStyle = computed(() => ({
left: `${adjustedPosition.value.x}px`,
top: `${adjustedPosition.value.y}px`
}));
async function adjustMenuWithinViewport() {
await nextTick();
const menuEl = menuRef.value;
if (!menuEl) return;
const rect = menuEl.getBoundingClientRect();
let nextX = adjustedPosition.value.x;
let nextY = adjustedPosition.value.y;
if (rect.right > window.innerWidth) {
nextX = Math.max(0, window.innerWidth - rect.width - 8);
}
if (rect.bottom > window.innerHeight) {
nextY = Math.max(0, window.innerHeight - rect.height - 8);
}
adjustedPosition.value = { x: nextX, y: nextY };
}
function handleItemClick(item: RenderMenuItem) {
if (item.type !== "action" || item.disabled) {
return;
}
contextMenuManager.runCommand(item);
}
function handleOverlayMouseDown() {
contextMenuManager.hide();
}
function stopPropagation(event: MouseEvent) {
event.stopPropagation();
}
</script>
<template>
<Teleport :to="teleportTarget">
<template v-if="isVisible">
<div class="cm-context-overlay" @mousedown="handleOverlayMouseDown" />
<div
ref="menuRef"
class="cm-context-menu show"
:style="menuStyle"
role="menu"
@contextmenu.prevent
@mousedown="stopPropagation"
>
<template v-for="item in items" :key="item.id">
<div v-if="item.type === 'separator'" class="cm-context-menu-divider" />
<div
v-else
class="cm-context-menu-item"
:class="{ 'is-disabled': item.disabled }"
role="menuitem"
:aria-disabled="item.disabled ? 'true' : 'false'"
@click="handleItemClick(item)"
>
<div class="cm-context-menu-item-label">
<span>{{ item.label }}</span>
</div>
<span v-if="item.shortcut" class="cm-context-menu-item-shortcut">
{{ item.shortcut }}
</span>
</div>
</template>
</div>
</template>
</Teleport>
</template>
<style scoped lang="scss">
.cm-context-overlay {
position: absolute;
inset: 0;
z-index: 9000;
background: transparent;
}
.cm-context-menu {
position: fixed;
min-width: 180px;
max-width: 320px;
padding: 4px 0;
border-radius: 3px;
background-color: var(--settings-card-bg, #1c1c1e);
color: var(--settings-text, #f6f6f6);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
z-index: 10000;
opacity: 0;
transform: scale(0.96);
transform-origin: top left;
transition: opacity 0.12s ease, transform 0.12s ease;
}
.cm-context-menu.show {
opacity: 1;
transform: scale(1);
}
.cm-context-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 14px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.12s ease, color 0.12s ease;
white-space: nowrap;
}
.cm-context-menu-item:hover {
background-color: var(--toolbar-button-hover);
color: var(--toolbar-text, #ffffff);
}
.cm-context-menu-item.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.cm-context-menu-item-label {
display: flex;
align-items: center;
gap: 8px;
}
.cm-context-menu-item-shortcut {
font-size: 12px;
opacity: 0.65;
}
.cm-context-menu-divider {
height: 1px;
margin: 4px 0;
border: none;
background-color: rgba(255, 255, 255, 0.08);
}
</style>

View File

@@ -1,156 +0,0 @@
/**
* 编辑器上下文菜单样式
* 支持系统主题自动适配
*/
.cm-context-menu {
position: fixed;
background-color: var(--settings-card-bg);
color: var(--settings-text);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 4px 0;
/* 优化阴影效果,只在右下角显示自然的阴影 */
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12);
min-width: 200px;
max-width: 320px;
z-index: 9999;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
opacity: 0;
transform: scale(0.95);
transition: opacity 0.15s ease-out, transform 0.15s ease-out;
overflow: visible; /* 确保子菜单可以显示在外部 */
}
.cm-context-menu-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
transition: all 0.1s ease;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cm-context-menu-item:hover {
background-color: var(--toolbar-button-hover);
color: var(--toolbar-text);
}
.cm-context-menu-item-label {
display: flex;
align-items: center;
gap: 8px;
}
.cm-context-menu-item-shortcut {
opacity: 0.7;
font-size: 12px;
padding: 2px 4px;
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text-secondary);
margin-left: 16px;
}
.cm-context-menu-item-ripple {
position: absolute;
border-radius: 50%;
background-color: var(--selection-bg);
width: 100px;
height: 100px;
opacity: 0.5;
transform: scale(0);
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}
/* 菜单分组标题样式 */
.cm-context-menu-group-title {
padding: 6px 12px;
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
user-select: none;
}
/* 菜单分隔线样式 */
.cm-context-menu-divider {
height: 1px;
background-color: var(--border-color);
margin: 4px 0;
}
/* 子菜单样式 */
.cm-context-submenu-container {
position: relative;
}
.cm-context-menu-item-with-submenu {
position: relative;
}
.cm-context-menu-item-with-submenu::after {
content: "";
position: absolute;
right: 12px;
font-size: 16px;
opacity: 0.7;
}
.cm-context-submenu {
position: fixed; /* 改为fixed定位避免受父元素影响 */
min-width: 180px;
opacity: 0;
pointer-events: none;
transform: translateX(10px);
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 10000;
border-radius: 6px;
background-color: var(--settings-card-bg);
color: var(--settings-text);
border: 1px solid var(--border-color);
padding: 4px 0;
/* 子菜单也使用相同的阴影效果 */
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.cm-context-menu-item-with-submenu:hover .cm-context-submenu {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
/* 深色主题下的特殊样式 */
:root[data-theme="dark"] .cm-context-menu {
/* 深色主题下阴影更深,但仍然只在右下角 */
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
}
:root[data-theme="dark"] .cm-context-submenu {
/* 深色主题下子菜单阴影 */
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
}
:root[data-theme="dark"] .cm-context-menu-divider {
background-color: var(--dark-border-color);
opacity: 0.6;
}
/* 动画相关类 */
.cm-context-menu.show {
opacity: 1;
transform: scale(1);
}
.cm-context-menu.hide {
opacity: 0;
}

View File

@@ -1,585 +0,0 @@
/**
* 上下文菜单视图实现
*/
import { EditorView } from "@codemirror/view";
import { MenuItem } from "../contextMenu";
import "./contextMenu.css";
// 为Window对象添加cmSubmenus属性
declare global {
interface Window {
cmSubmenus?: Map<string, HTMLElement>;
}
}
/**
* 菜单项元素池用于复用DOM元素
*/
class MenuItemPool {
private pool: HTMLElement[] = [];
private maxPoolSize = 50; // 最大池大小
/**
* 获取或创建菜单项元素
*/
get(): HTMLElement {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
const menuItem = document.createElement("div");
menuItem.className = "cm-context-menu-item";
return menuItem;
}
/**
* 回收菜单项元素
*/
release(element: HTMLElement): void {
if (this.pool.length < this.maxPoolSize) {
// 清理元素状态
element.className = "cm-context-menu-item";
element.innerHTML = "";
element.style.cssText = "";
// 移除所有事件监听器(通过克隆节点)
const cleanElement = element.cloneNode(false) as HTMLElement;
this.pool.push(cleanElement);
}
}
/**
* 清空池
*/
clear(): void {
this.pool.length = 0;
}
}
/**
* 上下文菜单管理器
*/
class ContextMenuManager {
private static instance: ContextMenuManager;
private menuElement: HTMLElement | null = null;
private submenuPool: Map<string, HTMLElement> = new Map();
private menuItemPool = new MenuItemPool();
private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;
private keyDownHandler: ((e: KeyboardEvent) => void) | null = null;
private currentView: EditorView | null = null;
private activeSubmenus: Set<HTMLElement> = new Set();
private ripplePool: HTMLElement[] = [];
// 事件委托处理器
private menuClickHandler: ((e: MouseEvent) => void) | null = null;
private menuMouseHandler: ((e: MouseEvent) => void) | null = null;
private constructor() {
this.initializeEventHandlers();
}
/**
* 获取单例实例
*/
static getInstance(): ContextMenuManager {
if (!ContextMenuManager.instance) {
ContextMenuManager.instance = new ContextMenuManager();
}
return ContextMenuManager.instance;
}
/**
* 初始化事件处理器
*/
private initializeEventHandlers(): void {
// 点击事件委托
this.menuClickHandler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const menuItem = target.closest('.cm-context-menu-item') as HTMLElement;
if (menuItem && menuItem.dataset.command) {
e.preventDefault();
e.stopPropagation();
// 添加点击动画
this.addRippleEffect(menuItem, e);
// 执行命令
const commandName = menuItem.dataset.command;
const command = this.getCommandByName(commandName);
if (command && this.currentView) {
command(this.currentView);
}
// 隐藏菜单
this.hide();
}
};
// 鼠标事件委托
this.menuMouseHandler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const menuItem = target.closest('.cm-context-menu-item') as HTMLElement;
if (!menuItem) return;
if (e.type === 'mouseenter') {
this.handleMenuItemMouseEnter(menuItem);
} else if (e.type === 'mouseleave') {
this.handleMenuItemMouseLeave(menuItem, e);
}
};
// 键盘事件处理器
this.keyDownHandler = (e: KeyboardEvent) => {
if (e.key === "Escape") {
this.hide();
}
};
// 点击外部关闭处理器
this.clickOutsideHandler = (e: MouseEvent) => {
if (this.menuElement && !this.isClickInsideMenu(e.target as Node)) {
this.hide();
}
};
}
/**
* 获取或创建主菜单元素
*/
private getOrCreateMenuElement(): HTMLElement {
if (!this.menuElement) {
this.menuElement = document.createElement("div");
this.menuElement.className = "cm-context-menu";
this.menuElement.style.display = "none";
document.body.appendChild(this.menuElement);
// 阻止菜单内右键点击冒泡
this.menuElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
// 添加事件委托
this.menuElement.addEventListener('click', this.menuClickHandler!);
this.menuElement.addEventListener('mouseenter', this.menuMouseHandler!, true);
this.menuElement.addEventListener('mouseleave', this.menuMouseHandler!, true);
}
return this.menuElement;
}
/**
* 创建或获取子菜单元素
*/
private getOrCreateSubmenu(id: string): HTMLElement {
if (!this.submenuPool.has(id)) {
const submenu = document.createElement("div");
submenu.className = "cm-context-menu cm-context-submenu";
submenu.style.display = "none";
document.body.appendChild(submenu);
this.submenuPool.set(id, submenu);
// 阻止子菜单点击事件冒泡
submenu.addEventListener('click', (e) => {
e.stopPropagation();
});
// 添加事件委托
submenu.addEventListener('click', this.menuClickHandler!);
submenu.addEventListener('mouseenter', this.menuMouseHandler!, true);
submenu.addEventListener('mouseleave', this.menuMouseHandler!, true);
}
return this.submenuPool.get(id)!;
}
/**
* 创建菜单项DOM元素
*/
private createMenuItemElement(item: MenuItem): HTMLElement {
const menuItem = this.menuItemPool.get();
// 如果有子菜单,添加相应类
if (item.submenu && item.submenu.length > 0) {
menuItem.classList.add("cm-context-menu-item-with-submenu");
}
// 创建内容容器
const contentContainer = document.createElement("div");
contentContainer.className = "cm-context-menu-item-label";
// 标签文本
const label = document.createElement("span");
label.textContent = item.label;
contentContainer.appendChild(label);
menuItem.appendChild(contentContainer);
// 快捷键提示(如果有)
if (item.shortcut) {
const shortcut = document.createElement("span");
shortcut.className = "cm-context-menu-item-shortcut";
shortcut.textContent = item.shortcut;
menuItem.appendChild(shortcut);
}
// 存储命令信息用于事件委托
if (item.command) {
menuItem.dataset.command = this.registerCommand(item.command);
}
// 处理子菜单
if (item.submenu && item.submenu.length > 0) {
const submenuId = `submenu-${item.label.replace(/\s+/g, '-').toLowerCase()}`;
menuItem.dataset.submenuId = submenuId;
const submenu = this.getOrCreateSubmenu(submenuId);
this.populateSubmenu(submenu, item.submenu);
// 记录子菜单
if (!window.cmSubmenus) {
window.cmSubmenus = new Map();
}
window.cmSubmenus.set(submenuId, submenu);
}
return menuItem;
}
/**
* 填充子菜单内容
*/
private populateSubmenu(submenu: HTMLElement, items: MenuItem[]): void {
// 清空现有内容
while (submenu.firstChild) {
submenu.removeChild(submenu.firstChild);
}
// 添加子菜单项
items.forEach(item => {
const subMenuItemElement = this.createMenuItemElement(item);
submenu.appendChild(subMenuItemElement);
});
// 初始状态设置为隐藏
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden';
submenu.style.display = 'block';
}
/**
* 命令注册和管理
*/
private commands: Map<string, (view: EditorView) => void> = new Map();
private commandCounter = 0;
private registerCommand(command: (view: EditorView) => void): string {
const commandId = `cmd_${this.commandCounter++}`;
this.commands.set(commandId, command);
return commandId;
}
private getCommandByName(commandId: string): ((view: EditorView) => void) | undefined {
return this.commands.get(commandId);
}
/**
* 处理菜单项鼠标进入事件
*/
private handleMenuItemMouseEnter(menuItem: HTMLElement): void {
const submenuId = menuItem.dataset.submenuId;
if (!submenuId) return;
const submenu = this.submenuPool.get(submenuId);
if (!submenu) return;
const rect = menuItem.getBoundingClientRect();
// 计算子菜单位置
submenu.style.left = `${rect.right}px`;
submenu.style.top = `${rect.top}px`;
// 检查子菜单是否会超出屏幕
requestAnimationFrame(() => {
const submenuRect = submenu.getBoundingClientRect();
if (submenuRect.right > window.innerWidth) {
submenu.style.left = `${rect.left - submenuRect.width}px`;
}
if (submenuRect.bottom > window.innerHeight) {
const newTop = rect.top - (submenuRect.bottom - window.innerHeight);
submenu.style.top = `${Math.max(0, newTop)}px`;
}
});
// 显示子菜单
submenu.style.opacity = '1';
submenu.style.pointerEvents = 'auto';
submenu.style.visibility = 'visible';
submenu.style.transform = 'translateX(0)';
this.activeSubmenus.add(submenu);
}
/**
* 处理菜单项鼠标离开事件
*/
private handleMenuItemMouseLeave(menuItem: HTMLElement, e: MouseEvent): void {
const submenuId = menuItem.dataset.submenuId;
if (!submenuId) return;
const submenu = this.submenuPool.get(submenuId);
if (!submenu) return;
// 检查是否移动到子菜单上
const toElement = e.relatedTarget as HTMLElement;
if (submenu.contains(toElement)) {
return;
}
this.hideSubmenu(submenu);
}
/**
* 隐藏子菜单
*/
private hideSubmenu(submenu: HTMLElement): void {
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.transform = 'translateX(10px)';
setTimeout(() => {
if (submenu.style.opacity === '0') {
submenu.style.visibility = 'hidden';
}
}, 200);
this.activeSubmenus.delete(submenu);
}
/**
* 添加点击波纹效果
*/
private addRippleEffect(menuItem: HTMLElement, e: MouseEvent): void {
let ripple: HTMLElement;
if (this.ripplePool.length > 0) {
ripple = this.ripplePool.pop()!;
} else {
ripple = document.createElement("div");
ripple.className = "cm-context-menu-item-ripple";
}
// 计算相对位置
const rect = menuItem.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
ripple.style.left = (x - 50) + "px";
ripple.style.top = (y - 50) + "px";
ripple.style.transform = "scale(0)";
ripple.style.opacity = "1";
menuItem.appendChild(ripple);
// 执行动画
requestAnimationFrame(() => {
ripple.style.transform = "scale(1)";
ripple.style.opacity = "0";
setTimeout(() => {
if (ripple.parentNode === menuItem) {
menuItem.removeChild(ripple);
this.ripplePool.push(ripple);
}
}, 300);
});
}
/**
* 检查点击是否在菜单内
*/
private isClickInsideMenu(target: Node): boolean {
if (this.menuElement && this.menuElement.contains(target)) {
return true;
}
// 检查是否在子菜单内
for (const submenu of this.activeSubmenus) {
if (submenu.contains(target)) {
return true;
}
}
return false;
}
/**
* 定位菜单元素
*/
private positionMenu(menu: HTMLElement, clientX: number, clientY: number): void {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
let left = clientX;
let top = clientY;
requestAnimationFrame(() => {
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
if (left + menuWidth > windowWidth) {
left = windowWidth - menuWidth - 5;
}
if (top + menuHeight > windowHeight) {
top = windowHeight - menuHeight - 5;
}
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
});
}
/**
* 显示上下文菜单
*/
show(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void {
this.currentView = view;
// 获取或创建菜单元素
const menu = this.getOrCreateMenuElement();
// 隐藏所有子菜单
this.hideAllSubmenus();
// 清空现有菜单项并回收到池中
while (menu.firstChild) {
const child = menu.firstChild as HTMLElement;
if (child.classList.contains('cm-context-menu-item')) {
this.menuItemPool.release(child);
}
menu.removeChild(child);
}
// 清空命令注册
this.commands.clear();
this.commandCounter = 0;
// 添加主菜单项
items.forEach(item => {
const menuItemElement = this.createMenuItemElement(item);
menu.appendChild(menuItemElement);
});
// 显示菜单
menu.style.display = "block";
// 定位菜单
this.positionMenu(menu, clientX, clientY);
// 添加全局事件监听器
document.addEventListener("click", this.clickOutsideHandler!, true);
document.addEventListener("keydown", this.keyDownHandler!);
// 触发显示动画
requestAnimationFrame(() => {
if (menu) {
menu.classList.add("show");
}
});
}
/**
* 隐藏所有子菜单
*/
private hideAllSubmenus(): void {
this.activeSubmenus.forEach(submenu => {
this.hideSubmenu(submenu);
});
this.activeSubmenus.clear();
if (window.cmSubmenus) {
window.cmSubmenus.forEach((submenu) => {
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden';
submenu.style.transform = 'translateX(10px)';
});
}
}
/**
* 隐藏上下文菜单
*/
hide(): void {
// 隐藏所有子菜单
this.hideAllSubmenus();
if (this.menuElement) {
// 添加淡出动画
this.menuElement.classList.remove("show");
this.menuElement.classList.add("hide");
// 等待动画完成后隐藏
setTimeout(() => {
if (this.menuElement) {
this.menuElement.style.display = "none";
this.menuElement.classList.remove("hide");
}
}, 150);
}
// 移除全局事件监听器
if (this.clickOutsideHandler) {
document.removeEventListener("click", this.clickOutsideHandler, true);
}
if (this.keyDownHandler) {
document.removeEventListener("keydown", this.keyDownHandler);
}
this.currentView = null;
}
/**
* 销毁管理器
*/
destroy(): void {
this.hide();
if (this.menuElement) {
document.body.removeChild(this.menuElement);
this.menuElement = null;
}
this.submenuPool.forEach(submenu => {
if (submenu.parentNode) {
document.body.removeChild(submenu);
}
});
this.submenuPool.clear();
this.menuItemPool.clear();
this.commands.clear();
this.activeSubmenus.clear();
this.ripplePool.length = 0;
if (window.cmSubmenus) {
window.cmSubmenus.clear();
}
}
}
// 获取单例实例
const contextMenuManager = ContextMenuManager.getInstance();
/**
* 显示上下文菜单
*/
export function showContextMenu(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void {
contextMenuManager.show(view, clientX, clientY, items);
}

View File

@@ -1,174 +1,141 @@
/** import { EditorView } from '@codemirror/view';
* 编辑器上下文菜单实现 import { Extension } from '@codemirror/state';
* 提供基本的复制、剪切、粘贴等操作,支持动态快捷键显示 import { copyCommand, cutCommand, pasteCommand } from '../extensions/codeblock/copyPaste';
*/ import { KeyBindingCommand } from '@/../bindings/voidraft/internal/models/models';
import { useKeybindingStore } from '@/stores/keybindingStore';
import { EditorView } from "@codemirror/view"; import { undo, redo } from '@codemirror/commands';
import { Extension } from "@codemirror/state"; import i18n from '@/i18n';
import { copyCommand, cutCommand, pasteCommand } from "../extensions/codeblock/copyPaste"; import { useSystemStore } from '@/stores/systemStore';
import { KeyBindingCommand } from "@/../bindings/voidraft/internal/models/models"; import { showContextMenu } from './manager';
import { useKeybindingStore } from "@/stores/keybindingStore";
import { import {
undo, redo buildRegisteredMenu,
} from "@codemirror/commands"; createMenuContext,
import i18n from "@/i18n"; registerMenuNodes
import {useSystemStore} from "@/stores/systemStore"; } from './menuSchema';
import type { MenuSchemaNode } from './menuSchema';
/**
* 菜单项类型定义
*/
export interface MenuItem {
/** 菜单项显示文本 */
label: string;
/** 点击时执行的命令 (如果有子菜单可以为null) */
command?: (view: EditorView) => boolean;
/** 快捷键提示文本 (可选) */
shortcut?: string;
/** 子菜单项 (可选) */
submenu?: MenuItem[];
}
// 导入相关功能
import { showContextMenu } from "./contextMenuView";
/**
* 获取翻译文本
* @param key 翻译键
* @returns 翻译后的文本
*/
function t(key: string): string { function t(key: string): string {
return i18n.global.t(key); return i18n.global.t(key);
} }
/**
* 获取快捷键显示文本 function formatKeyBinding(keyBinding: string): string {
* @param command 命令ID const systemStore = useSystemStore();
* @returns 快捷键显示文本 const isMac = systemStore.isMacOS;
*/
function getShortcutText(command: KeyBindingCommand): string { return keyBinding
.replace("Mod", isMac ? "Cmd" : "Ctrl")
.replace("Shift", "Shift")
.replace("Alt", isMac ? "Option" : "Alt")
.replace("Ctrl", isMac ? "Ctrl" : "Ctrl")
.replace(/-/g, " + ");
}
const shortcutCache = new Map<KeyBindingCommand, string>();
function getShortcutText(command?: KeyBindingCommand): string {
if (command === undefined) {
return "";
}
const cached = shortcutCache.get(command);
if (cached !== undefined) {
return cached;
}
try { try {
const keybindingStore = useKeybindingStore(); const keybindingStore = useKeybindingStore();
const binding = keybindingStore.keyBindings.find(
// 如果找到该命令的快捷键配置 (kb) => kb.command === command && kb.enabled
const binding = keybindingStore.keyBindings.find(kb =>
kb.command === command && kb.enabled
); );
if (binding && binding.key) { if (binding?.key) {
// 格式化快捷键显示 const formatted = formatKeyBinding(binding.key);
return formatKeyBinding(binding.key); shortcutCache.set(command, formatted);
return formatted;
} }
} catch (error) { } catch (error) {
console.warn("An error occurred while getting the shortcut:", error); console.warn("An error occurred while getting the shortcut:", error);
} }
shortcutCache.set(command, "");
return ""; return "";
} }
/**
* 格式化快捷键显示
* @param keyBinding 快捷键字符串
* @returns 格式化后的显示文本
*/
function formatKeyBinding(keyBinding: string): string {
// 获取系统信息
const systemStore = useSystemStore();
const isMac = systemStore.isMacOS;
// 替换修饰键名称为更友好的显示 function getBuiltinMenuNodes(): MenuSchemaNode[] {
return keyBinding
.replace("Mod", isMac ? "⌘" : "Ctrl")
.replace("Shift", isMac ? "⇧" : "Shift")
.replace("Alt", isMac ? "⌥" : "Alt")
.replace("Ctrl", isMac ? "⌃" : "Ctrl")
.replace(/-/g, " + ");
}
/**
* 创建编辑菜单项
*/
function createEditItems(): MenuItem[] {
return [ return [
{ {
label: t("keybindings.commands.blockCopy"), id: "copy",
labelKey: "keybindings.commands.blockCopy",
command: copyCommand, command: copyCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockCopyCommand) shortcutCommand: KeyBindingCommand.BlockCopyCommand,
enabled: (context) => context.hasSelection
}, },
{ {
label: t("keybindings.commands.blockCut"), id: "cut",
labelKey: "keybindings.commands.blockCut",
command: cutCommand, command: cutCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockCutCommand) shortcutCommand: KeyBindingCommand.BlockCutCommand,
visible: (context) => context.isEditable,
enabled: (context) => context.hasSelection && context.isEditable
}, },
{ {
label: t("keybindings.commands.blockPaste"), id: "paste",
labelKey: "keybindings.commands.blockPaste",
command: pasteCommand, command: pasteCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockPasteCommand) shortcutCommand: KeyBindingCommand.BlockPasteCommand,
} visible: (context) => context.isEditable
];
}
/**
* 创建历史操作菜单项
*/
function createHistoryItems(): MenuItem[] {
return [
{
label: t("keybindings.commands.historyUndo"),
command: undo,
shortcut: getShortcutText(KeyBindingCommand.HistoryUndoCommand)
}, },
{ {
label: t("keybindings.commands.historyRedo"), id: "undo",
labelKey: "keybindings.commands.historyUndo",
command: undo,
shortcutCommand: KeyBindingCommand.HistoryUndoCommand,
visible: (context) => context.isEditable
},
{
id: "redo",
labelKey: "keybindings.commands.historyRedo",
command: redo, command: redo,
shortcut: getShortcutText(KeyBindingCommand.HistoryRedoCommand) shortcutCommand: KeyBindingCommand.HistoryRedoCommand,
visible: (context) => context.isEditable
} }
]; ];
} }
let builtinMenuRegistered = false;
/** function ensureBuiltinMenuRegistered(): void {
* 创建主菜单项 if (builtinMenuRegistered) return;
*/ registerMenuNodes(getBuiltinMenuNodes());
function createMainMenuItems(): MenuItem[] { builtinMenuRegistered = true;
// 基本编辑操作放在主菜单
const basicItems = createEditItems();
// 历史操作放在主菜单
const historyItems = createHistoryItems();
// 构建主菜单
return [
...basicItems,
...historyItems
];
} }
/**
* 创建编辑器上下文菜单
*/
export function createEditorContextMenu(): Extension { export function createEditorContextMenu(): Extension {
// 为编辑器添加右键事件处理 ensureBuiltinMenuRegistered();
return EditorView.domEventHandlers({ return EditorView.domEventHandlers({
contextmenu: (event, view) => { contextmenu: (event, view) => {
// 阻止默认右键菜单
event.preventDefault(); event.preventDefault();
// 获取菜单项 const context = createMenuContext(view, event as MouseEvent);
const menuItems = createMainMenuItems(); const menuItems = buildRegisteredMenu(context, {
translate: t,
formatShortcut: getShortcutText
});
if (menuItems.length === 0) {
return false;
}
// 显示上下文菜单
showContextMenu(view, event.clientX, event.clientY, menuItems); showContextMenu(view, event.clientX, event.clientY, menuItems);
return true; return true;
} }
}); });
} }
/**
* 默认导出
*/
export default createEditorContextMenu; export default createEditorContextMenu;

View File

@@ -0,0 +1,88 @@
import type { EditorView } from '@codemirror/view';
import { readonly, shallowRef, type ShallowRef } from 'vue';
import type { RenderMenuItem } from './menuSchema';
interface MenuPosition {
x: number;
y: number;
}
interface ContextMenuState {
visible: boolean;
position: MenuPosition;
items: RenderMenuItem[];
view: EditorView | null;
}
class ContextMenuManager {
private state: ShallowRef<ContextMenuState> = shallowRef({
visible: false,
position: { x: 0, y: 0 },
items: [] as RenderMenuItem[],
view: null as EditorView | null
});
useState() {
return readonly(this.state);
}
show(view: EditorView, clientX: number, clientY: number, items: RenderMenuItem[]): void {
this.state.value = {
visible: true,
position: { x: clientX, y: clientY },
items,
view
};
}
hide(): void {
if (!this.state.value.visible) {
return;
}
const previousPosition = this.state.value.position;
const view = this.state.value.view;
this.state.value = {
visible: false,
position: previousPosition,
items: [],
view: null
};
if (view) {
view.focus();
}
}
runCommand(item: RenderMenuItem): void {
if (item.type !== "action" || item.disabled) {
return;
}
const { view } = this.state.value;
if (item.command && view) {
item.command(view);
}
this.hide();
}
destroy(): void {
this.state.value = {
visible: false,
position: { x: 0, y: 0 },
items: [],
view: null
};
}
}
export const contextMenuManager = new ContextMenuManager();
export function showContextMenu(
view: EditorView,
clientX: number,
clientY: number,
items: RenderMenuItem[]
): void {
contextMenuManager.show(view, clientX, clientY, items);
}

View File

@@ -0,0 +1,102 @@
import type { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import type { KeyBindingCommand } from '@/../bindings/voidraft/internal/models/models';
export interface MenuContext {
view: EditorView;
event: MouseEvent;
hasSelection: boolean;
selectionText: string;
isEditable: boolean;
}
export type MenuSchemaNode =
| {
id: string;
type?: "action";
labelKey: string;
command?: (view: EditorView) => boolean;
shortcutCommand?: KeyBindingCommand;
visible?: (context: MenuContext) => boolean;
enabled?: (context: MenuContext) => boolean;
}
| {
id: string;
type: "separator";
visible?: (context: MenuContext) => boolean;
};
export interface RenderMenuItem {
id: string;
type: "action" | "separator";
label?: string;
shortcut?: string;
disabled?: boolean;
command?: (view: EditorView) => boolean;
}
interface MenuBuildOptions {
translate: (key: string) => string;
formatShortcut: (command?: KeyBindingCommand) => string;
}
const menuRegistry: MenuSchemaNode[] = [];
export function createMenuContext(view: EditorView, event: MouseEvent): MenuContext {
const { state } = view;
const hasSelection = state.selection.ranges.some((range) => !range.empty);
const selectionText = hasSelection
? state.sliceDoc(state.selection.main.from, state.selection.main.to)
: "";
const isEditable = !state.facet(EditorState.readOnly);
return {
view,
event,
hasSelection,
selectionText,
isEditable
};
}
export function registerMenuNodes(nodes: MenuSchemaNode[]): void {
menuRegistry.push(...nodes);
}
export function buildRegisteredMenu(
context: MenuContext,
options: MenuBuildOptions
): RenderMenuItem[] {
return menuRegistry
.map((node) => convertNode(node, context, options))
.filter((item): item is RenderMenuItem => Boolean(item));
}
function convertNode(
node: MenuSchemaNode,
context: MenuContext,
options: MenuBuildOptions
): RenderMenuItem | null {
if (node.visible && !node.visible(context)) {
return null;
}
if (node.type === "separator") {
return {
id: node.id,
type: "separator"
};
}
const disabled = node.enabled ? !node.enabled(context) : false;
const shortcut = options.formatShortcut(node.shortcutCommand);
return {
id: node.id,
type: "action",
label: options.translate(node.labelKey),
shortcut: shortcut || undefined,
disabled,
command: node.command
};
}

View File

@@ -9,9 +9,8 @@ import {
} from '@codemirror/view'; } from '@codemirror/view';
import { Extension, Range } from '@codemirror/state'; import { Extension, Range } from '@codemirror/state';
import * as runtime from "@wailsio/runtime"; import * as runtime from "@wailsio/runtime";
import { getNoteBlockFromPos } from '../codeblock/state';
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`; const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
const defaultRegexp = /\b((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)\b/gi; const defaultRegexp = /\b(([a-zA-Z][\w+\-.]*):\/\/[^\s/$.?#].[^\s]*)\b/gi;
export interface HyperLinkState { export interface HyperLinkState {
at: number; at: number;
@@ -54,18 +53,8 @@ function hyperLinkDecorations(view: EditorView, anchor?: HyperLinkExtensionOptio
const from = match.index; const from = match.index;
const to = from + match[0].length; const to = from + match[0].length;
// 检查当前位置是否在 HTTP 代码块中
const block = getNoteBlockFromPos(view.state, from);
if (block && block.language.name === 'http') {
// 如果在 HTTP 代码块中,跳过超链接装饰
continue;
}
const linkMark = Decoration.mark({ const linkMark = Decoration.mark({
class: 'cm-hyper-link-text', class: 'cm-hyper-link-text'
attributes: {
'data-url': match[0]
}
}); });
widgets.push(linkMark.range(from, to)); widgets.push(linkMark.range(from, to));
@@ -91,14 +80,7 @@ const linkDecorator = (
) => ) =>
new MatchDecorator({ new MatchDecorator({
regexp: regexp || defaultRegexp, regexp: regexp || defaultRegexp,
decorate: (add, from, to, match, view) => { decorate: (add, from, to, match, _view) => {
// 检查当前位置是否在 HTTP 代码块中
const block = getNoteBlockFromPos(view.state, from);
if (block && block.language.name === 'http') {
// 如果在 HTTP 代码块中,跳过超链接装饰
return;
}
const url = match[0]; const url = match[0];
let urlStr = matchFn && typeof matchFn === 'function' ? matchFn(url, match.input, from, to) : url; let urlStr = matchFn && typeof matchFn === 'function' ? matchFn(url, match.input, from, to) : url;
if (matchData && matchData[url]) { if (matchData && matchData[url]) {
@@ -109,10 +91,7 @@ const linkDecorator = (
const linkIcon = new HyperLinkIcon({ at: start, url: urlStr, anchor }); const linkIcon = new HyperLinkIcon({ at: start, url: urlStr, anchor });
add(from, to, Decoration.mark({ add(from, to, Decoration.mark({
class: 'cm-hyper-link-text cm-hyper-link-underline', class: 'cm-hyper-link-text cm-hyper-link-underline'
attributes: {
'data-url': urlStr
}
})); }));
add(start, end, Decoration.widget({ widget: linkIcon, side: 1 })); add(start, end, Decoration.widget({ widget: linkIcon, side: 1 }));
}, },
@@ -158,7 +137,7 @@ export function hyperLinkExtension({ regexp, match, handle, anchor, showIcon = t
export const hyperLinkStyle = EditorView.baseTheme({ export const hyperLinkStyle = EditorView.baseTheme({
'.cm-hyper-link-text': { '.cm-hyper-link-text': {
color: '#0969da', color: '#0969da',
cursor: 'pointer', cursor: 'text',
transition: 'color 0.2s ease', transition: 'color 0.2s ease',
textDecoration: 'underline', textDecoration: 'underline',
textDecorationColor: '#0969da', textDecorationColor: '#0969da',
@@ -216,17 +195,12 @@ export const hyperLinkStyle = EditorView.baseTheme({
}); });
export const hyperLinkClickHandler = EditorView.domEventHandlers({ export const hyperLinkClickHandler = EditorView.domEventHandlers({
click: (event, view) => { click: (event) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement | null;
let urlElement = target; const iconElement = target?.closest?.('.cm-hyper-link-icon') as (HTMLElement | null);
while (urlElement && !urlElement.hasAttribute('data-url')) { if (iconElement && iconElement.hasAttribute('data-url')) {
urlElement = urlElement.parentElement as HTMLElement; const url = iconElement.getAttribute('data-url');
if (!urlElement || urlElement === document.body) break;
}
if (urlElement && urlElement.hasAttribute('data-url')) {
const url = urlElement.getAttribute('data-url');
if (url) { if (url) {
runtime.Browser.OpenURL(url); runtime.Browser.OpenURL(url);
event.preventDefault(); event.preventDefault();

View File

@@ -2,6 +2,7 @@
* Markdown 预览扩展主入口 * Markdown 预览扩展主入口
*/ */
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
import { Compartment } from "@codemirror/state";
import { useThemeStore } from "@/stores/themeStore"; import { useThemeStore } from "@/stores/themeStore";
import { usePanelStore } from "@/stores/panelStore"; import { usePanelStore } from "@/stores/panelStore";
import { useDocumentStore } from "@/stores/documentStore"; import { useDocumentStore } from "@/stores/documentStore";
@@ -52,11 +53,30 @@ export function toggleMarkdownPreview(view: EditorView): boolean {
/** /**
* 导出 Markdown 预览扩展 * 导出 Markdown 预览扩展
*/ */
export function markdownPreviewExtension() { const previewThemeCompartment = new Compartment();
const buildPreviewTheme = () => {
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const colors = themeStore.currentColors; const colors = themeStore.currentColors;
return colors ? createMarkdownPreviewTheme(colors) : EditorView.baseTheme({});
};
const theme = colors ? createMarkdownPreviewTheme(colors) : EditorView.baseTheme({}); export function markdownPreviewExtension() {
return [
return [previewPanelState, previewPanelPlugin, theme]; previewPanelState,
previewPanelPlugin,
previewThemeCompartment.of(buildPreviewTheme())
];
}
export function updateMarkdownPreviewTheme(view: EditorView): void {
if (!view?.dispatch) return;
try {
view.dispatch({
effects: previewThemeCompartment.reconfigure(buildPreviewTheme())
});
} catch (error) {
console.error("Failed to update markdown preview theme", error);
}
} }

View File

@@ -22,7 +22,7 @@ export class MarkdownPreviewPanel {
private readonly resizeHandle: HTMLDivElement; private readonly resizeHandle: HTMLDivElement;
private readonly content: HTMLDivElement; private readonly content: HTMLDivElement;
private view: EditorView; private view: EditorView;
private themeUnwatch?: () => void; private themeUnwatchers: Array<() => void> = [];
private lastRenderedContent: string = ""; private lastRenderedContent: string = "";
private readonly debouncedUpdate: ReturnType<typeof createDebounce>; private readonly debouncedUpdate: ReturnType<typeof createDebounce>;
private isDestroyed: boolean = false; // 标记面板是否已销毁 private isDestroyed: boolean = false; // 标记面板是否已销毁
@@ -38,11 +38,22 @@ export class MarkdownPreviewPanel {
// 监听主题变化 // 监听主题变化
const themeStore = useThemeStore(); const themeStore = useThemeStore();
this.themeUnwatch = watch(() => themeStore.isDarkMode, (isDark) => { this.themeUnwatchers.push(
watch(() => themeStore.isDarkMode, (isDark) => {
const newTheme = isDark ? "dark" : "default"; const newTheme = isDark ? "dark" : "default";
updateMermaidTheme(newTheme); updateMermaidTheme(newTheme);
this.lastRenderedContent = ""; // 清空缓存,强制重新渲染 this.resetPreviewContent();
}); })
);
this.themeUnwatchers.push(
watch(
() => themeStore.currentColors,
() => {
this.resetPreviewContent();
},
{ deep: true }
)
);
// 创建 DOM 结构 // 创建 DOM 结构
this.dom = document.createElement("div"); this.dom = document.createElement("div");
@@ -315,6 +326,16 @@ export class MarkdownPreviewPanel {
}); });
} }
private resetPreviewContent(): void {
if (this.isDestroyed) {
return;
}
this.md = createMarkdownRenderer();
this.lastRenderedContent = "";
this.updateContentInternal();
}
/** /**
* 响应编辑器更新 * 响应编辑器更新
*/ */
@@ -342,6 +363,11 @@ export class MarkdownPreviewPanel {
// 清空缓存 // 清空缓存
this.lastRenderedContent = ""; this.lastRenderedContent = "";
if (this.themeUnwatchers.length) {
this.themeUnwatchers.forEach(unwatch => unwatch());
this.themeUnwatchers = [];
}
} }
/** /**

View File

@@ -78,6 +78,34 @@ export function createMarkdownPreviewTheme(colors: ThemeColors) {
} }
}, },
// 面板动画效果
'.cm-panels.cm-panels-top': {
borderBottom: '2px solid black'
},
'.cm-panels.cm-panels-bottom': {
animation: 'panelSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
},
'@keyframes panelSlideUp': {
from: {
transform: 'translateY(100%)',
opacity: '0'
},
to: {
transform: 'translateY(0)',
opacity: '1'
}
},
'@keyframes panelSlideDown': {
from: {
transform: 'translateY(0)',
opacity: '1'
},
to: {
transform: 'translateY(100%)',
opacity: '0'
}
},
// 内容区域 // 内容区域
".cm-preview-content": { ".cm-preview-content": {
flex: 1, flex: 1,

View File

@@ -1,7 +1,7 @@
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { useKeybindingStore } from '@/stores/keybindingStore'; import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore'; import { useExtensionStore } from '@/stores/extensionStore';
import { KeymapManager } from './keymapManager'; import { Manager } from './manager';
/** /**
* 异步创建快捷键扩展 * 异步创建快捷键扩展
@@ -23,7 +23,7 @@ export const createDynamicKeymapExtension = async (): Promise<Extension> => {
// 获取启用的扩展ID列表 // 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id); const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds); return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
}; };
/** /**
@@ -37,10 +37,10 @@ export const updateKeymapExtension = (view: any): void => {
// 获取启用的扩展ID列表 // 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id); const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds); Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
}; };
// 导出相关模块 // 导出相关模块
export { KeymapManager } from './keymapManager'; export { Manager } from './manager';
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands'; export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'; export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';

View File

@@ -8,7 +8,7 @@ import {getCommandHandler, isCommandRegistered} from './commands';
* *
* CodeMirror快捷键扩展 * CodeMirror快捷键扩展
*/ */
export class KeymapManager { export class Manager {
private static compartment = new Compartment(); private static compartment = new Compartment();
/** /**

View File

@@ -1,299 +0,0 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {ExtensionState, EditorViewInfo, ExtensionFactory} from './types'
import {createDebounce} from '@/common/utils/debounce';
/**
* 扩展管理器
* 负责管理所有动态扩展的注册、启用、禁用和配置更新
* 采用统一配置,多视图同步的设计模式
*/
export class ExtensionManager {
// 统一的扩展状态存储
private extensionStates = new Map<ExtensionID, ExtensionState>();
// 编辑器视图管理
private viewsMap = new Map<number, EditorViewInfo>();
private activeViewId: number | null = null;
// 注册的扩展工厂
private extensionFactories = new Map<ExtensionID, ExtensionFactory>();
// 防抖处理
private debouncedUpdateFunctions = new Map<ExtensionID, {
debouncedFn: (enabled: boolean, config: any) => void;
cancel: () => void;
flush: () => void;
}>();
/**
* 注册扩展工厂
* @param id 扩展ID
* @param factory 扩展工厂
*/
registerExtension(id: ExtensionID, factory: ExtensionFactory): void {
this.extensionFactories.set(id, factory);
// 创建初始状态
if (!this.extensionStates.has(id)) {
const compartment = new Compartment();
const defaultConfig = factory.getDefaultConfig();
this.extensionStates.set(id, {
id,
factory,
config: defaultConfig,
enabled: false,
compartment,
extension: [] // 默认为空扩展(禁用状态)
});
}
// 为每个扩展创建防抖函数
if (!this.debouncedUpdateFunctions.has(id)) {
const { debouncedFn, cancel, flush } = createDebounce(
(enabled: boolean, config: any) => {
this.updateExtensionImmediate(id, enabled, config);
},
{ delay: 300 }
);
this.debouncedUpdateFunctions.set(id, {
debouncedFn,
cancel,
flush
});
}
}
/**
* 获取所有注册的扩展ID列表
*/
getRegisteredExtensions(): ExtensionID[] {
return Array.from(this.extensionFactories.keys());
}
/**
* 检查扩展是否已注册
* @param id 扩展ID
*/
isExtensionRegistered(id: ExtensionID): boolean {
return this.extensionFactories.has(id);
}
/**
* 从后端配置初始化扩展状态
* @param extensionConfigs 后端扩展配置列表
*/
initializeExtensionsFromConfig(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const factory = this.extensionFactories.get(config.id);
if (!factory) continue;
// 验证配置
if (factory.validateConfig && !factory.validateConfig(config.config)) {
continue;
}
try {
// 创建扩展实例
const extension = config.enabled ? factory.create(config.config) : [];
// 如果状态已存在则更新,否则创建新状态
if (this.extensionStates.has(config.id)) {
const state = this.extensionStates.get(config.id)!;
state.config = config.config;
state.enabled = config.enabled;
state.extension = extension;
} else {
const compartment = new Compartment();
this.extensionStates.set(config.id, {
id: config.id,
factory,
config: config.config,
enabled: config.enabled,
compartment,
extension
});
}
} catch (error) {
console.error(`Failed to initialize extension ${config.id}:`, error);
}
}
}
/**
* 获取初始扩展配置数组(用于创建编辑器)
* @returns CodeMirror扩展数组
*/
getInitialExtensions(): Extension[] {
const extensions: Extension[] = [];
// 为每个注册的扩展添加compartment
for (const state of this.extensionStates.values()) {
extensions.push(state.compartment.of(state.extension));
}
return extensions;
}
/**
* 设置编辑器视图
* @param view 编辑器视图实例
* @param documentId 文档ID
*/
setView(view: EditorView, documentId: number): void {
// 保存视图信息
this.viewsMap.set(documentId, {
view,
documentId,
registered: true
});
// 设置当前活动视图
this.activeViewId = documentId;
}
/**
* 获取当前活动视图
*/
private getActiveView(): EditorView | null {
if (this.activeViewId === null) return null;
const viewInfo = this.viewsMap.get(this.activeViewId);
return viewInfo ? viewInfo.view : null;
}
/**
* 更新单个扩展配置并应用到所有视图(带防抖功能)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
const debouncedUpdate = this.debouncedUpdateFunctions.get(id);
if (debouncedUpdate) {
debouncedUpdate.debouncedFn(enabled, config);
} else {
// 如果没有防抖函数,直接执行
this.updateExtensionImmediate(id, enabled, config);
}
}
/**
* 立即更新扩展(无防抖)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void {
// 获取扩展状态
const state = this.extensionStates.get(id);
if (!state) return;
// 获取工厂
const factory = state.factory;
// 验证配置
if (factory.validateConfig && !factory.validateConfig(config)) {
return;
}
try {
// 创建新的扩展实例
const extension = enabled ? factory.create(config) : [];
// 更新内部状态
state.config = config;
state.enabled = enabled;
state.extension = extension;
// 应用到所有视图
this.applyExtensionToAllViews(id);
} catch (error) {
console.error(`Failed to update extension ${id}:`, error);
}
}
/**
* 将指定扩展的当前状态应用到所有视图
* @param id 扩展ID
*/
private applyExtensionToAllViews(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
// 遍历所有视图并应用更改
for (const viewInfo of this.viewsMap.values()) {
try {
if (!viewInfo.registered) continue;
viewInfo.view.dispatch({
effects: state.compartment.reconfigure(state.extension)
});
} catch (error) {
console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error);
}
}
}
/**
* 获取扩展当前状态
* @param id 扩展ID
*/
getExtensionState(id: ExtensionID): {
enabled: boolean
config: any
} | null {
const state = this.extensionStates.get(id);
if (!state) return null;
return {
enabled: state.enabled,
config: state.config
};
}
/**
* 重置扩展到默认配置
* @param id 扩展ID
*/
resetExtensionToDefault(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
const defaultConfig = state.factory.getDefaultConfig();
this.updateExtension(id, true, defaultConfig);
}
/**
* 从管理器中移除视图
* @param documentId 文档ID
*/
removeView(documentId: number): void {
if (this.activeViewId === documentId) {
this.activeViewId = null;
}
this.viewsMap.delete(documentId);
}
/**
* 销毁管理器
*/
destroy(): void {
// 清除所有防抖函数
for (const { cancel } of this.debouncedUpdateFunctions.values()) {
cancel();
}
this.debouncedUpdateFunctions.clear();
this.viewsMap.clear();
this.activeViewId = null;
this.extensionFactories.clear();
this.extensionStates.clear();
}
}

View File

@@ -1,301 +1,152 @@
import {ExtensionManager} from './extensionManager'; import {Manager} from './manager';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import i18n from '@/i18n'; import i18n from '@/i18n';
import {ExtensionFactory} from './types' import {ExtensionDefinition} from './types';
// 导入现有扩展的创建函数
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'; import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'; import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension';
import {color} from '../extensions/colorSelector'; import {color} from '../extensions/colorSelector';
import {hyperLink} from '../extensions/hyperlink'; import {hyperLink} from '../extensions/hyperlink';
import {minimap} from '../extensions/minimap'; import {minimap} from '../extensions/minimap';
import {vscodeSearch} from '../extensions/vscodeSearch'; import {vscodeSearch} from '../extensions/vscodeSearch';
import {createCheckboxExtension} from '../extensions/checkbox'; import {createCheckboxExtension} from '../extensions/checkbox';
import {createTranslatorExtension} from '../extensions/translator'; import {createTranslatorExtension} from '../extensions/translator';
import {foldingOnIndent} from '../extensions/fold/foldExtension'; import {foldingOnIndent} from '../extensions/fold/foldExtension';
/** type ExtensionEntry = {
* 彩虹括号扩展工厂 definition: ExtensionDefinition
*/ displayNameKey: string
export const rainbowBracketsFactory: ExtensionFactory = { descriptionKey: string
create(_config: any) {
return rainbowBracketsExtension();
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
}; };
/** type RegisteredExtensionID = Exclude<ExtensionID, ExtensionID.$zero | ExtensionID.ExtensionEditor>;
* 文本高亮扩展工厂
*/
export const textHighlightFactory: ExtensionFactory = {
create(config: any) {
return createTextHighlighter({
backgroundColor: config.backgroundColor || '#FFD700',
opacity: config.opacity || 0.3
});
},
getDefaultConfig() {
return {
backgroundColor: '#FFD700', // 金黄色
opacity: 0.3 // 透明度
};
},
validateConfig(config: any) {
return typeof config === 'object' &&
(!config.backgroundColor || typeof config.backgroundColor === 'string') &&
(!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1));
}
};
/** const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
* 小地图扩展工厂 create,
*/ defaultConfig
export const minimapFactory: ExtensionFactory = { });
create(config: any) {
const options = {
displayText: config.displayText || 'characters',
showOverlay: config.showOverlay || 'always',
autohide: config.autohide || false
};
return minimap(options);
},
getDefaultConfig() {
return {
displayText: 'characters',
showOverlay: 'always',
autohide: false
};
},
validateConfig(config: any) {
return typeof config === 'object' &&
(!config.displayText || typeof config.displayText === 'string') &&
(!config.showOverlay || typeof config.showOverlay === 'string') &&
(!config.autohide || typeof config.autohide === 'boolean');
}
};
/** const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
* 超链接扩展工厂
*/
export const hyperlinkFactory: ExtensionFactory = {
create(_config: any) {
return hyperLink;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 颜色选择器扩展工厂
*/
export const colorSelectorFactory: ExtensionFactory = {
create(_config: any) {
return color;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 搜索扩展工厂
*/
export const searchFactory: ExtensionFactory = {
create(_config: any) {
return vscodeSearch;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
export const foldFactory: ExtensionFactory = {
create(_config: any) {
return foldingOnIndent;
},
getDefaultConfig(): any {
return {};
},
validateConfig(config: any): boolean {
return typeof config === 'object';
}
};
/**
* 选择框扩展工厂
*/
export const checkboxFactory: ExtensionFactory = {
create(_config: any) {
return createCheckboxExtension();
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 翻译扩展工厂
*/
export const translatorFactory: ExtensionFactory = {
create(config: any) {
return createTranslatorExtension({
minSelectionLength: config.minSelectionLength || 2,
maxTranslationLength: config.maxTranslationLength || 5000,
});
},
getDefaultConfig() {
return {
minSelectionLength: 2,
maxTranslationLength: 5000,
};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 所有扩展的统一配置
* 排除$zero值以避免TypeScript类型错误
*/
const EXTENSION_CONFIGS = {
// 编辑增强扩展
[ExtensionID.ExtensionRainbowBrackets]: { [ExtensionID.ExtensionRainbowBrackets]: {
factory: rainbowBracketsFactory, definition: defineExtension(() => rainbowBracketsExtension()),
displayNameKey: 'extensions.rainbowBrackets.name', displayNameKey: 'extensions.rainbowBrackets.name',
descriptionKey: 'extensions.rainbowBrackets.description' descriptionKey: 'extensions.rainbowBrackets.description'
}, },
[ExtensionID.ExtensionHyperlink]: { [ExtensionID.ExtensionHyperlink]: {
factory: hyperlinkFactory, definition: defineExtension(() => hyperLink),
displayNameKey: 'extensions.hyperlink.name', displayNameKey: 'extensions.hyperlink.name',
descriptionKey: 'extensions.hyperlink.description' descriptionKey: 'extensions.hyperlink.description'
}, },
[ExtensionID.ExtensionColorSelector]: { [ExtensionID.ExtensionColorSelector]: {
factory: colorSelectorFactory, definition: defineExtension(() => color),
displayNameKey: 'extensions.colorSelector.name', displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description' descriptionKey: 'extensions.colorSelector.description'
}, },
[ExtensionID.ExtensionTranslator]: { [ExtensionID.ExtensionTranslator]: {
factory: translatorFactory, definition: defineExtension((config: any) => createTranslatorExtension({
minSelectionLength: config?.minSelectionLength ?? 2,
maxTranslationLength: config?.maxTranslationLength ?? 5000
}), {
minSelectionLength: 2,
maxTranslationLength: 5000
}),
displayNameKey: 'extensions.translator.name', displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description' descriptionKey: 'extensions.translator.description'
}, },
// UI增强扩展
[ExtensionID.ExtensionMinimap]: { [ExtensionID.ExtensionMinimap]: {
factory: minimapFactory, definition: defineExtension((config: any) => minimap({
displayText: config?.displayText ?? 'characters',
showOverlay: config?.showOverlay ?? 'always',
autohide: config?.autohide ?? false
}), {
displayText: 'characters',
showOverlay: 'always',
autohide: false
}),
displayNameKey: 'extensions.minimap.name', displayNameKey: 'extensions.minimap.name',
descriptionKey: 'extensions.minimap.description' descriptionKey: 'extensions.minimap.description'
}, },
// 工具扩展
[ExtensionID.ExtensionSearch]: { [ExtensionID.ExtensionSearch]: {
factory: searchFactory, definition: defineExtension(() => vscodeSearch),
displayNameKey: 'extensions.search.name', displayNameKey: 'extensions.search.name',
descriptionKey: 'extensions.search.description' descriptionKey: 'extensions.search.description'
}, },
[ExtensionID.ExtensionFold]: { [ExtensionID.ExtensionFold]: {
factory: foldFactory, definition: defineExtension(() => foldingOnIndent),
displayNameKey: 'extensions.fold.name', displayNameKey: 'extensions.fold.name',
descriptionKey: 'extensions.fold.description' descriptionKey: 'extensions.fold.description'
}, },
[ExtensionID.ExtensionTextHighlight]: { [ExtensionID.ExtensionTextHighlight]: {
factory: textHighlightFactory, definition: defineExtension((config: any) => createTextHighlighter({
backgroundColor: config?.backgroundColor ?? '#FFD700',
opacity: config?.opacity ?? 0.3
}), {
backgroundColor: '#FFD700',
opacity: 0.3
}),
displayNameKey: 'extensions.textHighlight.name', displayNameKey: 'extensions.textHighlight.name',
descriptionKey: 'extensions.textHighlight.description' descriptionKey: 'extensions.textHighlight.description'
}, },
[ExtensionID.ExtensionCheckbox]: { [ExtensionID.ExtensionCheckbox]: {
factory: checkboxFactory, definition: defineExtension(() => createCheckboxExtension()),
displayNameKey: 'extensions.checkbox.name', displayNameKey: 'extensions.checkbox.name',
descriptionKey: 'extensions.checkbox.description' descriptionKey: 'extensions.checkbox.description'
} }
} as const;
const isRegisteredExtension = (id: ExtensionID): id is RegisteredExtensionID =>
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, id);
const getRegistryEntry = (id: ExtensionID): ExtensionEntry | undefined => {
if (!isRegisteredExtension(id)) {
return undefined;
}
return EXTENSION_REGISTRY[id];
}; };
/** export function registerAllExtensions(manager: Manager): void {
* 注册所有扩展工厂到管理器 (Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => {
* @param manager 扩展管理器实例 manager.registerExtension(id, entry.definition);
*/
export function registerAllExtensions(manager: ExtensionManager): void {
Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => {
manager.registerExtension(id as ExtensionID, config.factory);
}); });
} }
/**
* 获取扩展工厂的显示名称
* @param id 扩展ID
* @returns 显示名称
*/
export function getExtensionDisplayName(id: ExtensionID): string { export function getExtensionDisplayName(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]; const entry = getRegistryEntry(id);
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id; return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id;
} }
/**
* 获取扩展工厂的描述
* @param id 扩展ID
* @returns 描述
*/
export function getExtensionDescription(id: ExtensionID): string { export function getExtensionDescription(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]; const entry = getRegistryEntry(id);
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''; return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : '';
} }
/** function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined {
* 获取扩展工厂实例 return getRegistryEntry(id)?.definition;
* @param id 扩展ID
* @returns 扩展工厂实例
*/
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
return EXTENSION_CONFIGS[id as ExtensionID]?.factory;
} }
/**
* 获取扩展的默认配置
* @param id 扩展ID
* @returns 默认配置对象
*/
export function getExtensionDefaultConfig(id: ExtensionID): any { export function getExtensionDefaultConfig(id: ExtensionID): any {
const factory = getExtensionFactory(id); const definition = getExtensionDefinition(id);
return factory?.getDefaultConfig() || {}; if (!definition) return {};
return cloneConfig(definition.defaultConfig);
} }
/**
* 检查扩展是否有配置项
* @param id 扩展ID
* @returns 是否有配置项
*/
export function hasExtensionConfig(id: ExtensionID): boolean { export function hasExtensionConfig(id: ExtensionID): boolean {
const defaultConfig = getExtensionDefaultConfig(id); return Object.keys(getExtensionDefaultConfig(id)).length > 0;
return Object.keys(defaultConfig).length > 0;
} }
/**
* 获取所有可用扩展的ID列表
* @returns 扩展ID数组
*/
export function getAllExtensionIds(): ExtensionID[] { export function getAllExtensionIds(): ExtensionID[] {
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[]; return Object.keys(EXTENSION_REGISTRY) as RegisteredExtensionID[];
} }
const cloneConfig = (config: any) => {
if (Array.isArray(config)) {
return config.map(cloneConfig);
}
if (config && typeof config === 'object') {
return Object.keys(config).reduce((acc, key) => {
acc[key] = cloneConfig(config[key]);
return acc;
}, {} as Record<string, any>);
}
return config;
};

View File

@@ -1,13 +1,13 @@
import {Extension} from '@codemirror/state'; import {Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {useExtensionStore} from '@/stores/extensionStore'; import {useExtensionStore} from '@/stores/extensionStore';
import {ExtensionManager} from './extensionManager'; import {Manager} from './manager';
import {registerAllExtensions} from './extensions'; import {registerAllExtensions} from './extensions';
/** /**
* 全局扩展管理器实例 * 全局扩展管理器实例
*/ */
const extensionManager = new ExtensionManager(); const extensionManager = new Manager();
/** /**
* 异步创建动态扩展 * 异步创建动态扩展
@@ -26,7 +26,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
} }
// 初始化扩展管理器配置 // 初始化扩展管理器配置
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions); extensionManager.initExtensions(extensionStore.extensions);
// 获取初始扩展配置 // 获取初始扩展配置
return extensionManager.getInitialExtensions(); return extensionManager.getInitialExtensions();
@@ -36,7 +36,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
* 获取扩展管理器实例 * 获取扩展管理器实例
* @returns 扩展管理器 * @returns 扩展管理器
*/ */
export const getExtensionManager = (): ExtensionManager => { export const getExtensionManager = (): Manager => {
return extensionManager; return extensionManager;
}; };
@@ -58,5 +58,5 @@ export const removeExtensionManagerView = (documentId: number): void => {
}; };
// 导出相关模块 // 导出相关模块
export {ExtensionManager} from './extensionManager'; export {Manager} from './manager';
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions'; export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions';

View File

@@ -0,0 +1,135 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {ExtensionDefinition, ExtensionState} from './types';
/**
* 扩展管理器
* 负责注册、初始化与同步所有动态扩展
*/
export class Manager {
private extensionStates = new Map<ExtensionID, ExtensionState>();
private views = new Map<number, EditorView>();
registerExtension(id: ExtensionID, definition: ExtensionDefinition): void {
const existingState = this.extensionStates.get(id);
if (existingState) {
existingState.definition = definition;
if (existingState.config === undefined) {
existingState.config = this.cloneConfig(definition.defaultConfig ?? {});
}
} else {
const compartment = new Compartment();
const defaultConfig = this.cloneConfig(definition.defaultConfig ?? {});
this.extensionStates.set(id, {
id,
definition,
config: defaultConfig,
enabled: false,
compartment,
extension: []
});
}
}
initExtensions(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const state = this.extensionStates.get(config.id);
if (!state) continue;
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
this.commitExtensionState(state, config.enabled, resolvedConfig);
}
}
getInitialExtensions(): Extension[] {
const extensions: Extension[] = [];
for (const state of this.extensionStates.values()) {
extensions.push(state.compartment.of(state.extension));
}
return extensions;
}
setView(view: EditorView, documentId: number): void {
this.views.set(documentId, view);
this.applyAllExtensionsToView(view);
}
updateExtension(id: ExtensionID, enabled: boolean, config?: any): void {
const state = this.extensionStates.get(id);
if (!state) return;
const resolvedConfig = this.resolveConfig(state, config);
this.commitExtensionState(state, enabled, resolvedConfig);
}
removeView(documentId: number): void {
this.views.delete(documentId);
}
destroy(): void {
this.views.clear();
this.extensionStates.clear();
}
private resolveConfig(state: ExtensionState, config?: any): any {
if (config !== undefined) {
return this.cloneConfig(config);
}
if (state.config !== undefined) {
return this.cloneConfig(state.config);
}
return this.cloneConfig(state.definition.defaultConfig ?? {});
}
private commitExtensionState(state: ExtensionState, enabled: boolean, config: any): void {
try {
const runtimeExtension = enabled ? state.definition.create(config) : [];
state.enabled = enabled;
state.config = config;
state.extension = runtimeExtension;
this.applyExtensionToAllViews(state.id);
} catch (error) {
console.error(`Failed to update extension ${state.id}:`, error);
}
}
private applyExtensionToAllViews(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
for (const [documentId, view] of this.views.entries()) {
try {
view.dispatch({effects: state.compartment.reconfigure(state.extension)});
} catch (error) {
console.error(`Failed to apply extension ${id} to document ${documentId}:`, error);
}
}
}
private applyAllExtensionsToView(view: EditorView): void {
const effects: any[] = [];
for (const state of this.extensionStates.values()) {
effects.push(state.compartment.reconfigure(state.extension));
}
if (effects.length === 0) return;
try {
view.dispatch({effects});
} catch (error) {
console.error('Failed to register extensions on view:', error);
}
}
private cloneConfig<T>(config: T): T {
if (Array.isArray(config)) {
return config.map(item => this.cloneConfig(item)) as unknown as T;
}
if (config && typeof config === 'object') {
return Object.keys(config as Record<string, any>).reduce((acc, key) => {
(acc as any)[key] = this.cloneConfig((config as Record<string, any>)[key]);
return acc;
}, {} as Record<string, any>) as T;
}
return config;
}
}

View File

@@ -1,49 +1 @@
import {Compartment, Extension} from '@codemirror/state'; import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
/**
* 扩展工厂接口
* 每个扩展需要实现此接口来创建和配置扩展
*/
export interface ExtensionFactory {
/**
* 创建扩展实例
* @param config 扩展配置
* @returns CodeMirror扩展
*/
create(config: any): Extension
/**
* 获取默认配置
* @returns 默认配置对象
*/
getDefaultConfig(): any
/**
* 验证配置
* @param config 配置对象
* @returns 是否有效
*/
validateConfig?(config: any): boolean
}
/**
* 扩展状态
*/
export interface ExtensionState {
id: ExtensionID
factory: ExtensionFactory
config: any
enabled: boolean
compartment: Compartment
extension: Extension
}
/**
* 视图信息
*/
export interface EditorViewInfo {
view: EditorView
documentId: number
registered: boolean
}

View File

@@ -4,6 +4,8 @@ import {tags} from '@lezer/highlight';
import {Extension} from '@codemirror/state'; import {Extension} from '@codemirror/state';
import type {ThemeColors} from './types'; import type {ThemeColors} from './types';
const MONO_FONT_FALLBACK = 'var(--voidraft-font-mono, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace)';
/** /**
* 创建通用主题 * 创建通用主题
* @param colors 主题颜色配置 * @param colors 主题颜色配置
@@ -12,28 +14,15 @@ import type {ThemeColors} from './types';
export function createBaseTheme(colors: ThemeColors): Extension { export function createBaseTheme(colors: ThemeColors): Extension {
// 编辑器主题样式 // 编辑器主题样式
const theme = EditorView.theme({ const theme = EditorView.theme({
'&': { '&': {
color: colors.foreground,
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
// 确保编辑器容器背景一致
'.cm-editor': { '.cm-editor': {
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
// 确保滚动区域背景一致
'.cm-scroller': {
backgroundColor: colors.background,
transition: 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
},
// 编辑器内容
'.cm-content': {
caretColor: colors.cursor,
paddingTop: '4px',
},
// 光标 // 光标
'.cm-cursor, .cm-dropCursor': { '.cm-cursor, .cm-dropCursor': {
borderLeftColor: colors.cursor, borderLeftColor: colors.cursor,
@@ -42,19 +31,11 @@ export function createBaseTheme(colors: ThemeColors): Extension {
marginTop: '-2px', marginTop: '-2px',
}, },
// 选择 // 选择背景
'.cm-selectionBackground': {
backgroundColor: colors.selectionBlur,
},
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': { '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
backgroundColor: colors.selection, backgroundColor: colors.selection,
}, },
'.cm-content ::selection': {
backgroundColor: colors.selection,
},
'.cm-activeLine.code-empty-block-selected': {
backgroundColor: colors.selection,
},
// 当前行高亮 // 当前行高亮
'.cm-activeLine': { '.cm-activeLine': {
@@ -66,7 +47,6 @@ export function createBaseTheme(colors: ThemeColors): Extension {
backgroundColor: colors.dark ? 'rgba(0,0,0, 0.1)' : 'rgba(0,0,0, 0.04)', backgroundColor: colors.dark ? 'rgba(0,0,0, 0.1)' : 'rgba(0,0,0, 0.04)',
color: colors.lineNumber, color: colors.lineNumber,
border: 'none', border: 'none',
borderRight: colors.dark ? 'none' : `1px solid ${colors.borderLight}`,
padding: '0 2px 0 4px', padding: '0 2px 0 4px',
userSelect: 'none', userSelect: 'none',
}, },
@@ -75,105 +55,11 @@ export function createBaseTheme(colors: ThemeColors): Extension {
color: colors.activeLineNumber, color: colors.activeLineNumber,
}, },
// 折叠功能
'.cm-foldGutter': {
marginLeft: '0px',
},
'.cm-foldGutter .cm-gutterElement': {
opacity: 0,
transition: 'opacity 400ms',
},
'.cm-gutters:hover .cm-gutterElement': {
opacity: 1,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: colors.comment,
},
// 面板
'.cm-panels': {
// backgroundColor: colors.dropdownBackground,
// color: colors.foreground
},
'.cm-panels.cm-panels-top': {
borderBottom: '2px solid black'
},
'.cm-panels.cm-panels-bottom': {
animation: 'panelSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
},
'@keyframes panelSlideUp': {
from: {
transform: 'translateY(100%)',
opacity: '0'
},
to: {
transform: 'translateY(0)',
opacity: '1'
}
},
'@keyframes panelSlideDown': {
from: {
transform: 'translateY(0)',
opacity: '1'
},
to: {
transform: 'translateY(100%)',
opacity: '0'
}
},
// 搜索匹配
'.cm-searchMatch': {
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: colors.searchMatch,
color: colors.background,
},
'.cm-selectionMatch': {
backgroundColor: colors.dark ? '#50606D' : '#e6f3ff',
},
// 括号匹配 // 括号匹配
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
outline: `0.5px solid ${colors.searchMatch}`, outline: `0.5px solid ${colors.matchingBracket}`,
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: colors.matchingBracket,
color: 'inherit',
},
'&.cm-focused .cm-nonmatchingBracket': {
outline: colors.dark ? '0.5px solid #bc8f8f' : '0.5px solid #d73a49',
},
// 编辑器焦点
'&.cm-editor.cm-focused': {
outline: 'none',
},
// 工具提示
'.cm-tooltip': {
border: colors.dark ? 'none' : `1px solid ${colors.dropdownBorder}`,
backgroundColor: colors.surface,
color: colors.foreground,
boxShadow: colors.dark ? 'none' : '0 2px 8px rgba(0,0,0,0.1)',
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
color: colors.foreground,
},
}, },
// 代码块层(自定义) // 代码块层(自定义)
@@ -195,8 +81,19 @@ export function createBaseTheme(colors: ThemeColors): Extension {
background: colors.backgroundSecondary, background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.borderColor}`, borderTop: `1px solid ${colors.borderColor}`,
}, },
'.code-block-empty-selected': {
backgroundColor: colors.selection,
},
// 代码块开始标记
'.code-block-start': {
height: '12px',
position: 'relative',
},
'.code-block-start.first': {
height: '0px',
},
// 数学计算结果(自定义) // 数学计算结果
'.code-blocks-math-result': { '.code-blocks-math-result': {
paddingLeft: "12px", paddingLeft: "12px",
position: "relative", position: "relative",
@@ -223,91 +120,116 @@ export function createBaseTheme(colors: ThemeColors): Extension {
'.code-blocks-math-result-copied.fade-out': { '.code-blocks-math-result-copied.fade-out': {
opacity: 0, opacity: 0,
}, },
// 代码块开始标记(自定义)
'.code-block-start': {
height: '12px',
position: 'relative',
},
'.code-block-start.first': {
height: '0px',
},
}, {dark: colors.dark}); }, {dark: colors.dark});
// 语法高亮样式
const highlightStyle = HighlightStyle.define([ const highlightStyle = HighlightStyle.define([
// 关键字 {tag: tags.comment, color: colors.comment, fontStyle: 'italic'},
{tag: tags.lineComment, color: colors.lineComment, fontStyle: 'italic'},
{tag: tags.blockComment, color: colors.blockComment, fontStyle: 'italic'},
{tag: tags.docComment, color: colors.docComment, fontStyle: 'italic'},
{tag: tags.name, color: colors.name},
{tag: tags.variableName, color: colors.variableName},
{tag: tags.typeName, color: colors.typeName},
{tag: tags.tagName, color: colors.tagName},
{tag: tags.propertyName, color: colors.propertyName},
{tag: tags.attributeName, color: colors.attributeName},
{tag: tags.className, color: colors.className},
{tag: tags.labelName, color: colors.labelName},
{tag: tags.namespace, color: colors.namespace},
{tag: tags.macroName, color: colors.macroName},
{tag: tags.literal, color: colors.literal},
{tag: tags.string, color: colors.string},
{tag: tags.docString, color: colors.docString},
{tag: tags.character, color: colors.character},
{tag: tags.attributeValue, color: colors.attributeValue},
{tag: tags.number, color: colors.number},
{tag: tags.integer, color: colors.integer},
{tag: tags.float, color: colors.float},
{tag: tags.bool, color: colors.bool},
{tag: tags.regexp, color: colors.regexp},
{tag: tags.escape, color: colors.escape},
{tag: tags.color, color: colors.color},
{tag: tags.url, color: colors.url},
{tag: tags.keyword, color: colors.keyword}, {tag: tags.keyword, color: colors.keyword},
{tag: tags.self, color: colors.self},
{tag: tags.null, color: colors.null},
{tag: tags.atom, color: colors.atom},
{tag: tags.unit, color: colors.unit},
{tag: tags.modifier, color: colors.modifier},
{tag: tags.operatorKeyword, color: colors.operatorKeyword},
{tag: tags.controlKeyword, color: colors.controlKeyword},
{tag: tags.definitionKeyword, color: colors.definitionKeyword},
{tag: tags.moduleKeyword, color: colors.moduleKeyword},
// 操作符 {tag: tags.operator, color: colors.operator},
{tag: [tags.operator, tags.operatorKeyword], color: colors.operator}, {tag: tags.derefOperator, color: colors.derefOperator},
{tag: tags.arithmeticOperator, color: colors.arithmeticOperator},
{tag: tags.logicOperator, color: colors.logicOperator},
{tag: tags.bitwiseOperator, color: colors.bitwiseOperator},
{tag: tags.compareOperator, color: colors.compareOperator},
{tag: tags.updateOperator, color: colors.updateOperator},
{tag: tags.definitionOperator, color: colors.definitionOperator},
{tag: tags.typeOperator, color: colors.typeOperator},
{tag: tags.controlOperator, color: colors.controlOperator},
// 名称、变量 {tag: tags.punctuation, color: colors.punctuation},
{tag: [tags.name, tags.deleted, tags.character, tags.macroName], color: colors.variable}, {tag: tags.separator, color: colors.separator},
{tag: [tags.variableName], color: colors.variable}, {tag: tags.bracket, color: colors.bracket},
{tag: [tags.labelName], color: colors.operator}, {tag: tags.angleBracket, color: colors.angleBracket},
{tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: colors.variable}, {tag: tags.squareBracket, color: colors.squareBracket},
{tag: tags.paren, color: colors.paren},
{tag: tags.brace, color: colors.brace},
// 函数 {tag: tags.content, color: colors.content},
{tag: [tags.function(tags.variableName)], color: colors.function}, {tag: tags.heading, color: colors.heading, fontWeight: 'bold'},
{tag: [tags.propertyName], color: colors.function}, {tag: tags.heading1, color: colors.heading1, fontWeight: 'bold', fontSize: '1.4em'},
{tag: tags.heading2, color: colors.heading2, fontWeight: 'bold', fontSize: '1.3em'},
{tag: tags.heading3, color: colors.heading3, fontWeight: 'bold', fontSize: '1.2em'},
{tag: tags.heading4, color: colors.heading4, fontWeight: 'bold', fontSize: '1.1em'},
{tag: tags.heading5, color: colors.heading5, fontWeight: 'bold'},
{tag: tags.heading6, color: colors.heading6, fontWeight: 'bold'},
{tag: tags.contentSeparator, color: colors.contentSeparator},
{tag: tags.list, color: colors.list},
{tag: tags.quote, color: colors.quote, fontStyle: 'italic'},
{tag: tags.emphasis, color: colors.emphasis, fontStyle: 'italic'},
{tag: tags.strong, color: colors.strong, fontWeight: 'bold'},
{tag: tags.link, color: colors.link, textDecoration: 'underline'},
{tag: tags.monospace, color: colors.monospace, fontFamily: MONO_FONT_FALLBACK},
{tag: tags.strikethrough, color: colors.strikethrough, textDecoration: 'line-through'},
// 类型、类 {tag: tags.inserted, color: colors.inserted},
{tag: [tags.typeName], color: colors.type}, {tag: tags.deleted, color: colors.deleted},
{tag: [tags.className], color: colors.class}, {tag: tags.changed, color: colors.changed},
// 常量 {tag: tags.meta, color: colors.meta, fontStyle: 'italic'},
{tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.constant}, {tag: tags.documentMeta, color: colors.documentMeta},
{tag: tags.annotation, color: colors.annotation},
{tag: tags.processingInstruction, color: colors.processingInstruction},
// 字符串 {tag: tags.definition(tags.variableName), color: colors.definition},
{tag: [tags.processingInstruction, tags.string, tags.inserted], color: colors.string}, {tag: tags.definition(tags.propertyName), color: colors.definition},
{tag: [tags.special(tags.string)], color: colors.string}, {tag: tags.definition(tags.name), color: colors.definition},
{tag: [tags.quote], color: colors.comment}, {tag: tags.constant(tags.variableName), color: colors.constant},
{tag: tags.constant(tags.propertyName), color: colors.constant},
{tag: tags.constant(tags.name), color: colors.constant},
{tag: tags.function(tags.variableName), color: colors.function},
{tag: tags.function(tags.propertyName), color: colors.function},
{tag: tags.function(tags.name), color: colors.function},
{tag: tags.standard(tags.variableName), color: colors.standard},
{tag: tags.standard(tags.name), color: colors.standard},
{tag: tags.local(tags.variableName), color: colors.local},
{tag: tags.local(tags.name), color: colors.local},
{tag: tags.special(tags.variableName), color: colors.special},
{tag: tags.special(tags.name), color: colors.special},
{tag: tags.special(tags.string), color: colors.special},
// 数字
{
tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace],
color: colors.number
},
// 正则表达式
{tag: [tags.url, tags.escape, tags.regexp, tags.link], color: colors.regexp},
// 注释
{tag: [tags.meta, tags.comment], color: colors.comment, fontStyle: 'italic'},
// 分隔符、括号
{tag: [tags.definition(tags.name), tags.separator], color: colors.variable},
{tag: [tags.brace], color: colors.variable},
{tag: [tags.squareBracket], color: colors.dark ? '#bf616a' : colors.keyword},
{tag: [tags.angleBracket], color: colors.dark ? '#d08770' : colors.operator},
{tag: [tags.attributeName], color: colors.variable},
// 标签
{tag: [tags.tagName], color: colors.number},
// 注解
{tag: [tags.annotation], color: colors.invalid},
// 特殊样式
{tag: tags.strong, fontWeight: 'bold'},
{tag: tags.emphasis, fontStyle: 'italic'},
{tag: tags.strikethrough, textDecoration: 'line-through'},
{tag: tags.link, color: colors.variable, textDecoration: 'underline'},
// 标题
{tag: tags.heading, fontWeight: 'bold', color: colors.heading},
{tag: [tags.heading1, tags.heading2], fontSize: '1.4em'},
{tag: [tags.heading3, tags.heading4], fontSize: '1.2em'},
{tag: [tags.heading5, tags.heading6], fontSize: '1.1em'},
// 无效内容
{tag: tags.invalid, color: colors.invalid}, {tag: tags.invalid, color: colors.invalid},
]); ]);
return [ return [
theme, theme,
syntaxHighlighting(highlightStyle), syntaxHighlighting(highlightStyle),
]; ];
} }

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'aura', themeName: 'aura',
dark: true, dark: true,
// 基础色调
background: '#21202e', background: '#21202e',
backgroundSecondary: '#2B2A3BFF', backgroundSecondary: '#2b2a3b',
surface: '#21202e',
dropdownBackground: '#21202e',
dropdownBorder: '#3b334b',
// 文本颜色
foreground: '#edecee', foreground: '#edecee',
foregroundSecondary: '#edecee',
comment: '#6d6d6d',
// 语法高亮色 - 核心
keyword: '#a277ff',
string: '#61ffca',
function: '#ffca85',
number: '#61ffca',
operator: '#a277ff',
variable: '#edecee',
type: '#82e2ff',
// 语法高亮色 - 扩展
constant: '#61ffca',
storage: '#a277ff',
parameter: '#edecee',
class: '#82e2ff',
heading: '#a277ff',
invalid: '#ff6767',
regexp: '#61ffca',
// 界面元素
cursor: '#a277ff', cursor: '#a277ff',
selection: '#3d375e7f', selection: '#3d375e7f',
selectionBlur: '#3d375e7f',
activeLine: '#4d4b6622', activeLine: '#4d4b6622',
lineNumber: '#a394f033', lineNumber: '#a394f033',
activeLineNumber: '#cdccce', activeLineNumber: '#cdccce',
diffInserted: '#61ffca',
// 边框和分割线 diffDeleted: '#ff6767',
diffChanged: '#ffca85',
borderColor: '#3b334b', borderColor: '#3b334b',
borderLight: '#edecee19',
// 搜索和匹配
searchMatch: '#61ffca',
matchingBracket: '#a394f033', matchingBracket: '#a394f033',
}
// 使用通用主题工厂函数创建 Aura 主题 comment: '#6d6d6d',
export const aura: Extension = createBaseTheme(config) lineComment: '#5c5c5c',
blockComment: '#5a5a5a',
docComment: '#747474',
name: '#edecee',
variableName: '#edecee',
typeName: '#82e2ff',
tagName: '#7cd4ff',
propertyName: '#d2d1f9',
attributeName: '#f6d1ff',
className: '#95dbff',
labelName: '#ffc285',
namespace: '#6fd0ff',
macroName: '#ffca85',
literal: '#82e2ff',
string: '#61ffca',
docString: '#61ffca',
character: '#73ffd7',
attributeValue: '#ffe3c4',
number: '#82e2ff',
integer: '#82e2ff',
float: '#82e2ff',
bool: '#ffd18b',
regexp: '#61ffca',
escape: '#4ff7c6',
color: '#ffc57c',
url: '#7cd4ff',
keyword: '#a277ff',
self: '#c89eff',
null: '#f69aff',
atom: '#61ffca',
unit: '#61ffca',
modifier: '#c094ff',
operatorKeyword: '#b98dff',
controlKeyword: '#c17aff',
definitionKeyword: '#bd8eff',
moduleKeyword: '#cfa2ff',
operator: '#a277ff',
derefOperator: '#c59bff',
arithmeticOperator: '#c78df5',
logicOperator: '#c088ff',
bitwiseOperator: '#ce8cff',
compareOperator: '#c786ff',
updateOperator: '#bb7cff',
definitionOperator: '#b070ff',
typeOperator: '#b98aff',
controlOperator: '#a867ff',
punctuation: '#d1a6ff',
separator: '#ceb1ff',
bracket: '#adabff',
angleBracket: '#ffc3ff',
squareBracket: '#ff9ddd',
paren: '#f39ddf',
brace: '#f589d6',
content: '#edecee',
heading: '#a277ff',
heading1: '#caa0ff',
heading2: '#c192ff',
heading3: '#b684ff',
heading4: '#aa76ff',
heading5: '#9f68ff',
heading6: '#955aff',
contentSeparator: '#a277ff',
list: '#c0c0c0',
quote: '#9280a3',
emphasis: '#edecee',
strong: '#f4f3f5',
link: '#79d3ff',
monospace: '#d5d0d8',
strikethrough: '#b9b3c0',
inserted: '#61ffca',
deleted: '#ff6767',
changed: '#ffca85',
invalid: '#ff6767',
meta: '#807d8c',
documentMeta: '#7b7886',
annotation: '#7df5d9',
processingInstruction: '#7b7490',
definition: '#d0cfe4',
constant: '#61ffca',
function: '#ffca85',
standard: '#c1c0cf',
local: '#c9c8d7',
special: '#ffd9a8',
};
export const aura: Extension = createBaseTheme(config);

View File

@@ -1,63 +1,114 @@
import {createBaseTheme} from '../base'; import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types'; import type {ThemeColors} from '../types';
// 默认深色主题颜色
export const defaultDarkColors: ThemeColors = { export const defaultDarkColors: ThemeColors = {
// 主题信息 themeName: 'default-dark',
name: 'default-dark',
dark: true, dark: true,
// 基础色调 // 基础色调
background: '#252B37', // 主背景色 background: '#252B37',
backgroundSecondary: '#213644', // 次要背景色 backgroundSecondary: '#213644',
surface: '#474747', // 面板背景
dropdownBackground: '#252B37', // 下拉菜单背景
dropdownBorder: '#ffffff19', // 下拉菜单边框
// 文本 // 文本与界面
foreground: '#9BB586', // 主文本色 foreground: '#ffffff',
foregroundSecondary: '#9c9c9c', // 次要文本色 cursor: '#ffffff',
comment: '#6272a4', // 注释色 selection: '#0865a9',
activeLine: '#ffffff0a',
lineNumber: '#ffffff26',
activeLineNumber: '#ffffff99',
diffInserted: '#64d189',
diffDeleted: '#ff6b6b',
diffChanged: '#ffb86c',
borderColor: '#1e222a',
matchingBracket: '#ffffff19',
// 语法高亮色 - 核心 // 语法标签色值
keyword: '#ff79c6', // 关键字 comment: '#6272a4',
string: '#f1fa8c', // 字符串 lineComment: '#5c6b99',
function: '#50fa7b', // 函数名 blockComment: '#596492',
number: '#bd93f9', // 数字 docComment: '#6e7bb5',
operator: '#ff79c6', // 操作符 name: '#dfe8ce',
variable: '#8fbcbb', // 变量 variableName: '#8fbcbb',
type: '#8be9fd', // 类型 typeName: '#8be9fd',
tagName: '#77d7f4',
// 语法高亮色 - 扩展 propertyName: '#c9e3b0',
constant: '#bd93f9', // 常量 attributeName: '#e1c8ff',
storage: '#ff79c6', // 存储类型 className: '#a5e0ff',
parameter: '#8fbcbb', // 参数 labelName: '#f7b267',
class: '#8be9fd', // 类名 namespace: '#5cd1ff',
heading: '#ff79c6', // 标题 macroName: '#ffcf8b',
invalid: '#d30102', // 无效内容 literal: '#c3b5ff',
regexp: '#f1fa8c', // 正则表达式 string: '#f1fa8c',
docString: '#e9f28a',
// 界面元素 character: '#ffd684',
cursor: '#ffffff', // 光标 attributeValue: '#ffe099',
selection: '#0865a9', // 选中背景 number: '#bd93f9',
selectionBlur: '#225377', // 失焦选中背景 integer: '#c6a5ff',
activeLine: '#ffffff0a', // 当前行高亮 float: '#b68afd',
lineNumber: '#ffffff26', // 行号 bool: '#7dd4cc',
activeLineNumber: '#ffffff99', // 活动行号 regexp: '#9cf0f1',
escape: '#85dedd',
// 边框和分割线 color: '#ffd38d',
borderColor: '#1e222a', // 边框色 url: '#8de0ff',
borderLight: '#ffffff19', // 浅色边框 keyword: '#ff79c6',
self: '#ff94d6',
// 搜索和匹配 null: '#ff9fe2',
searchMatch: '#8fbcbb', // 搜索匹配 atom: '#cba6ff',
matchingBracket: '#ffffff19', // 匹配括号 unit: '#a8dbd2',
modifier: '#f78cc8',
operatorKeyword: '#ff84cf',
controlKeyword: '#ff6fb6',
definitionKeyword: '#ff92d6',
moduleKeyword: '#ff8aca',
operator: '#ff79c6',
derefOperator: '#ff9bd6',
arithmeticOperator: '#ff7fc4',
logicOperator: '#ff9fcf',
bitwiseOperator: '#ff6fb8',
compareOperator: '#ff85c7',
updateOperator: '#ff76bd',
definitionOperator: '#ff6db7',
typeOperator: '#ff9bdd',
controlOperator: '#ff69ad',
punctuation: '#f5a6d9',
separator: '#f0a3d7',
bracket: '#cda0ff',
angleBracket: '#ffc0f1',
squareBracket: '#ff8db5',
paren: '#ff9ec8',
brace: '#fe7ab1',
content: '#dfeed0',
heading: '#ff9b6b',
heading1: '#ffb75f',
heading2: '#ffad57',
heading3: '#ffa14e',
heading4: '#ff9447',
heading5: '#ff8842',
heading6: '#ff7b3c',
contentSeparator: '#ff79c6',
list: '#acd1a2',
quote: '#7c8fb5',
emphasis: '#d9f7c1',
strong: '#fdf1c1',
link: '#6ac8ff',
monospace: '#d1dbc0',
strikethrough: '#b7c3a5',
inserted: '#64d189',
deleted: '#ff6b6b',
changed: '#ffb86c',
invalid: '#d30102',
meta: '#7285bb',
documentMeta: '#6a7caa',
annotation: '#9bf0ff',
processingInstruction: '#7685bd',
definition: '#9ec9c3',
constant: '#bd93f9',
function: '#50fa7b',
standard: '#8ab0a8',
local: '#92c7bb',
special: '#f4d67a',
}; };
// 创建深色主题
export function createDarkTheme(colors: ThemeColors = defaultDarkColors) { export function createDarkTheme(colors: ThemeColors = defaultDarkColors) {
return createBaseTheme({...colors, dark: true}); return createBaseTheme({...colors, dark: true});
} }
// 默认深色主题
export const defaultDark = createDarkTheme(defaultDarkColors);

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'dracula', themeName: 'dracula',
dark: true, dark: true,
// 基础色调 background: '#282a36',
background: '#282A36', backgroundSecondary: '#323543',
backgroundSecondary: '#323543FF',
surface: '#282A36',
dropdownBackground: '#282A36',
dropdownBorder: '#191A21',
// 文本颜色 foreground: '#f8f8f2',
foreground: '#F8F8F2', cursor: '#f8f8f2',
foregroundSecondary: '#F8F8F2', selection: '#44475a',
comment: '#6272A4',
// 语法高亮色 - 核心
keyword: '#FF79C6',
string: '#F1FA8C',
function: '#50FA7B',
number: '#BD93F9',
operator: '#FF79C6',
variable: '#F8F8F2',
type: '#8BE9FD',
// 语法高亮色 - 扩展
constant: '#BD93F9',
storage: '#FF79C6',
parameter: '#F8F8F2',
class: '#8BE9FD',
heading: '#BD93F9',
invalid: '#FF5555',
regexp: '#F1FA8C',
// 界面元素
cursor: '#F8F8F2',
selection: '#44475A',
selectionBlur: '#44475A',
activeLine: '#53576c22', activeLine: '#53576c22',
lineNumber: '#6272A4', lineNumber: '#6272a4',
activeLineNumber: '#F8F8F2', activeLineNumber: '#f8f8f2',
diffInserted: '#50fa7b',
diffDeleted: '#ff5555',
diffChanged: '#f1fa8c',
borderColor: '#191a21',
matchingBracket: '#44475a',
// 边框和分割线 comment: '#6272a4',
borderColor: '#191A21', lineComment: '#55608c',
borderLight: '#F8F8F219', blockComment: '#4f597f',
docComment: '#7c89bd',
name: '#f8f8f2',
variableName: '#f8f8f2',
typeName: '#8be9fd',
tagName: '#7de5ff',
propertyName: '#dcdce5',
attributeName: '#fcb5ff',
className: '#9cecff',
labelName: '#ffb86c',
namespace: '#6deeff',
macroName: '#50fa7b',
literal: '#bd93f9',
string: '#f1fa8c',
docString: '#f5ffa9',
character: '#ffec99',
attributeValue: '#ffcf99',
number: '#bd93f9',
integer: '#cfa6ff',
float: '#b48cff',
bool: '#ffb38b',
regexp: '#f1fa8c',
escape: '#f7ffae',
color: '#ffcf99',
url: '#8ae8ff',
keyword: '#ff79c6',
self: '#ff9dd7',
null: '#ff8fb0',
atom: '#bd93f9',
unit: '#bd93f9',
modifier: '#ff90d4',
operatorKeyword: '#ff8bd2',
controlKeyword: '#ff7dc1',
definitionKeyword: '#ff91d1',
moduleKeyword: '#ffacd9',
operator: '#ff79c6',
derefOperator: '#ff91d1',
arithmeticOperator: '#ff88c5',
logicOperator: '#ff8bcf',
bitwiseOperator: '#ff74ba',
compareOperator: '#ff86c6',
updateOperator: '#ff7cbf',
definitionOperator: '#ff6aae',
typeOperator: '#ff98d9',
controlOperator: '#ff6aa6',
punctuation: '#f4ade4',
separator: '#f3a6dc',
bracket: '#cfaefc',
angleBracket: '#ffcff1',
squareBracket: '#ff9fcc',
paren: '#ffb1d8',
brace: '#ff90c1',
content: '#f8f8f2',
heading: '#bd93f9',
heading1: '#d2b3ff',
heading2: '#c7a8ff',
heading3: '#bb9dff',
heading4: '#af92ff',
heading5: '#a387ff',
heading6: '#977cff',
contentSeparator: '#ff79c6',
list: '#c8cbd1',
quote: '#7b86a7',
emphasis: '#f8f8f2',
strong: '#ffffff',
link: '#8be9fd',
monospace: '#dadfde',
strikethrough: '#c2c8d1',
inserted: '#50fa7b',
deleted: '#ff5555',
changed: '#f1fa8c',
invalid: '#ff5555',
meta: '#8791bb',
documentMeta: '#7b84aa',
annotation: '#a7f7d4',
processingInstruction: '#6c7699',
definition: '#d6d9f2',
constant: '#bd93f9',
function: '#50fa7b',
standard: '#bac4d8',
local: '#c3c8da',
special: '#ffd6a5',
};
// 搜索和匹配 export const dracula: Extension = createBaseTheme(config);
searchMatch: '#50FA7B',
matchingBracket: '#44475A',
}
// 使用通用主题工厂函数创建 Dracula 主题
export const dracula: Extension = createBaseTheme(config)

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'github-dark', themeName: 'github-dark',
dark: true, dark: true,
// 基础色调
background: '#24292e', background: '#24292e',
backgroundSecondary: '#2E343BFF', backgroundSecondary: '#2e343b',
surface: '#24292e',
dropdownBackground: '#24292e',
dropdownBorder: '#1b1f23',
// 文本颜色
foreground: '#d1d5da', foreground: '#d1d5da',
foregroundSecondary: '#d1d5da',
comment: '#6a737d',
// 语法高亮色 - 核心
keyword: '#f97583',
string: '#9ecbff',
function: '#79b8ff',
number: '#79b8ff',
operator: '#f97583',
variable: '#ffab70',
type: '#79b8ff',
// 语法高亮色 - 扩展
constant: '#79b8ff',
storage: '#f97583',
parameter: '#e1e4e8',
class: '#b392f0',
heading: '#79b8ff',
invalid: '#f97583',
regexp: '#9ecbff',
// 界面元素
cursor: '#c8e1ff', cursor: '#c8e1ff',
selection: '#3392FF44', selection: '#3392ff44',
selectionBlur: '#3392FF44',
activeLine: '#4d566022', activeLine: '#4d566022',
lineNumber: '#444d56', lineNumber: '#444d56',
activeLineNumber: '#e1e4e8', activeLineNumber: '#e1e4e8',
diffInserted: '#2ea043',
// 边框和分割线 diffDeleted: '#d73a49',
diffChanged: '#c69026',
borderColor: '#1b1f23', borderColor: '#1b1f23',
borderLight: '#d1d5da19', matchingBracket: '#17e5e650',
// 搜索和匹配 comment: '#6a737d',
searchMatch: '#79b8ff', lineComment: '#596068',
matchingBracket: '#17E5E650', blockComment: '#4f555c',
} docComment: '#7c858f',
name: '#d1d5da',
variableName: '#ffab70',
typeName: '#79b8ff',
tagName: '#8dd1ff',
propertyName: '#d9dee5',
attributeName: '#c0a7ff',
className: '#b392f0',
labelName: '#ffab70',
namespace: '#84c5ff',
macroName: '#79b8ff',
literal: '#79b8ff',
string: '#9ecbff',
docString: '#aed3ff',
character: '#ffe4b2',
attributeValue: '#ffcf9a',
number: '#79b8ff',
integer: '#6fb1ff',
float: '#62a7ff',
bool: '#ffa657',
regexp: '#9ecbff',
escape: '#8bc2ff',
color: '#ffc27c',
url: '#68b7ff',
keyword: '#f97583',
self: '#ffa5b1',
null: '#ff8b76',
atom: '#79b8ff',
unit: '#79b8ff',
modifier: '#ff9a8c',
operatorKeyword: '#ff8c80',
controlKeyword: '#ff7f73',
definitionKeyword: '#ff9aa1',
moduleKeyword: '#ffb1ae',
operator: '#f97583',
derefOperator: '#ff8a7d',
arithmeticOperator: '#ff7c6a',
logicOperator: '#ff8172',
bitwiseOperator: '#ff6958',
compareOperator: '#ff7c6c',
updateOperator: '#ff6d5e',
definitionOperator: '#ff5d54',
typeOperator: '#ff8ca5',
controlOperator: '#ff5b4f',
punctuation: '#d6a3c5',
separator: '#d2a9c9',
bracket: '#98a6c8',
angleBracket: '#c3d5ff',
squareBracket: '#b6c4e4',
paren: '#b0bace',
brace: '#a1aabf',
content: '#d1d5da',
heading: '#79b8ff',
heading1: '#9ac7ff',
heading2: '#8fbfff',
heading3: '#85b7ff',
heading4: '#7bafff',
heading5: '#70a7ff',
heading6: '#669eff',
contentSeparator: '#f97583',
list: '#b8bfc7',
quote: '#7d848c',
emphasis: '#d1d5da',
strong: '#f5f7f9',
link: '#79b8ff',
monospace: '#cfd6df',
strikethrough: '#acb4bd',
inserted: '#2ea043',
deleted: '#d73a49',
changed: '#c69026',
invalid: '#f97583',
meta: '#8591a1',
documentMeta: '#7b8593',
annotation: '#90d6ff',
processingInstruction: '#6a7380',
definition: '#cdd4de',
constant: '#79b8ff',
function: '#79b8ff',
standard: '#bac4d1',
local: '#c5ccd7',
special: '#ffd9a6',
};
// 使用通用主题工厂函数创建 GitHub Dark 主题 export const githubDark: Extension = createBaseTheme(config);
export const githubDark: Extension = createBaseTheme(config)

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'material-dark', themeName: 'material-dark',
dark: true, dark: true,
// 基础色调
background: '#263238', background: '#263238',
backgroundSecondary: '#2D3E46FF', backgroundSecondary: '#2d3e46',
surface: '#263238',
dropdownBackground: '#263238',
dropdownBorder: '#FFFFFF10',
// 文本颜色 foreground: '#eeffff',
foreground: '#EEFFFF', cursor: '#ffcc00',
foregroundSecondary: '#EEFFFF', selection: '#80cbc420',
comment: '#546E7A',
// 语法高亮色 - 核心
keyword: '#C792EA',
string: '#C3E88D',
function: '#82AAFF',
number: '#F78C6C',
operator: '#C792EA',
variable: '#EEFFFF',
type: '#B2CCD6',
// 语法高亮色 - 扩展
constant: '#F78C6C',
storage: '#C792EA',
parameter: '#EEFFFF',
class: '#FFCB6B',
heading: '#C3E88D',
invalid: '#FF5370',
regexp: '#89DDFF',
// 界面元素
cursor: '#FFCC00',
selection: '#80CBC420',
selectionBlur: '#80CBC420',
activeLine: '#4c616c22', activeLine: '#4c616c22',
lineNumber: '#37474F', lineNumber: '#37474f',
activeLineNumber: '#607a86', activeLineNumber: '#607a86',
diffInserted: '#c3e88d',
// 边框和分割线 diffDeleted: '#ff5370',
borderColor: '#FFFFFF10', diffChanged: '#ffcb6b',
borderLight: '#EEFFFF19', borderColor: '#ffffff10',
// 搜索和匹配
searchMatch: '#82AAFF',
matchingBracket: '#263238', matchingBracket: '#263238',
}
// 使用通用主题工厂函数创建 Material Dark 主题 comment: '#546e7a',
export const materialDark: Extension = createBaseTheme(config) lineComment: '#4b606a',
blockComment: '#455962',
docComment: '#6c8795',
name: '#eeffff',
variableName: '#eeffff',
typeName: '#b2ccd6',
tagName: '#9ad4f5',
propertyName: '#e0f2ff',
attributeName: '#ffdcdc',
className: '#ffcb6b',
labelName: '#ffd17a',
namespace: '#8ad2e7',
macroName: '#82aaff',
literal: '#f78c6c',
string: '#c3e88d',
docString: '#d3f8a8',
character: '#ffe8c0',
attributeValue: '#ffd99f',
number: '#f78c6c',
integer: '#ff996e',
float: '#ffad80',
bool: '#ffd37d',
regexp: '#89ddff',
escape: '#66d9ff',
color: '#ffd492',
url: '#72d1ff',
keyword: '#c792ea',
self: '#d29ef2',
null: '#ff8aad',
atom: '#f78c6c',
unit: '#f78c6c',
modifier: '#dca8f0',
operatorKeyword: '#ca8de3',
controlKeyword: '#c280e1',
definitionKeyword: '#ce95ea',
moduleKeyword: '#d8a8f0',
operator: '#c792ea',
derefOperator: '#d79ef4',
arithmeticOperator: '#d28aec',
logicOperator: '#cd84e3',
bitwiseOperator: '#c77cdf',
compareOperator: '#cc8fe5',
updateOperator: '#c47ad9',
definitionOperator: '#bb6fd0',
typeOperator: '#cfa2ed',
controlOperator: '#b767cf',
punctuation: '#d9b4ff',
separator: '#d5aef6',
bracket: '#9fb6c5',
angleBracket: '#c4ddff',
squareBracket: '#a7c5dd',
paren: '#adc3d4',
brace: '#92aabd',
content: '#eeffff',
heading: '#c3e88d',
heading1: '#aeea9c',
heading2: '#a0dd92',
heading3: '#92d087',
heading4: '#85c37d',
heading5: '#78b673',
heading6: '#6aa969',
contentSeparator: '#c792ea',
list: '#b7cad4',
quote: '#758892',
emphasis: '#eeffff',
strong: '#f8ffff',
link: '#89ddff',
monospace: '#d7e4ec',
strikethrough: '#b4c4cc',
inserted: '#c3e88d',
deleted: '#ff5370',
changed: '#ffcb6b',
invalid: '#ff5370',
meta: '#6d8795',
documentMeta: '#648292',
annotation: '#73e0ff',
processingInstruction: '#617480',
definition: '#d0dae4',
constant: '#f78c6c',
function: '#82aaff',
standard: '#bacdd8',
local: '#c3d3dc',
special: '#ffd8a6',
};
export const materialDark: Extension = createBaseTheme(config);

View File

@@ -1,76 +1,123 @@
import {Extension} from "@codemirror/state" import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors const chalky = '#e5c07b';
const coral = '#e06c75';
const chalky = "#e5c07b", const cyan = '#56b6c2';
coral = "#e06c75", const ivory = '#abb2bf';
cyan = "#56b6c2", const stone = '#7d8799';
invalid = "#ffffff", const malibu = '#61afef';
ivory = "#abb2bf", const sage = '#98c379';
stone = "#7d8799", // Brightened compared to original to increase contrast const whiskey = '#d19a66';
malibu = "#61afef", const violet = '#c678dd';
sage = "#98c379", const darkBackground = '#21252b';
whiskey = "#d19a66", const highlightBackground = '#313949ff';
violet = "#c678dd", const background = '#282c34';
darkBackground = "#21252b", const selection = '#3e4451';
highlightBackground = "#313949FF",
background = "#282c34",
tooltipBackground = "#353a42",
selection = "#3E4451",
cursor = "#528bff"
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'one-dark', themeName: 'one-dark',
dark: true, dark: true,
// 基础色调 background,
background: background,
backgroundSecondary: highlightBackground, backgroundSecondary: highlightBackground,
surface: tooltipBackground,
dropdownBackground: darkBackground,
dropdownBorder: stone,
// 文本颜色
foreground: ivory, foreground: ivory,
foregroundSecondary: stone, cursor: '#528bff',
comment: stone, selection,
// 语法高亮色 - 核心
keyword: violet,
string: sage,
function: malibu,
number: chalky,
operator: cyan,
variable: coral,
type: chalky,
// 语法高亮色 - 扩展
constant: whiskey,
storage: violet,
parameter: coral,
class: chalky,
heading: coral,
invalid: invalid,
regexp: cyan,
// 界面元素
cursor: cursor,
selection: selection,
selectionBlur: selection,
activeLine: '#6699ff0b', activeLine: '#6699ff0b',
lineNumber: stone, lineNumber: stone,
activeLineNumber: ivory, activeLineNumber: ivory,
diffInserted: sage,
// 边框和分割线 diffDeleted: coral,
diffChanged: whiskey,
borderColor: darkBackground, borderColor: darkBackground,
borderLight: ivory + '19',
// 搜索和匹配
searchMatch: malibu,
matchingBracket: '#bad0f847', matchingBracket: '#bad0f847',
}
// 使用通用主题工厂函数创建 One Dark 主题 comment: stone,
export const oneDark: Extension = createBaseTheme(config) lineComment: '#6c7484',
blockComment: '#606775',
docComment: '#8b92a0',
name: ivory,
variableName: coral,
typeName: chalky,
tagName: '#e4c78f',
propertyName: '#d7dee8',
attributeName: '#efb8c2',
className: chalky,
labelName: '#f7b267',
namespace: '#88c0ff',
macroName: malibu,
literal: chalky,
string: sage,
docString: '#b3d899',
character: '#d9f59c',
attributeValue: '#f0c390',
number: chalky,
integer: '#f2c78d',
float: '#f1ba6a',
bool: '#f28f6a',
regexp: cyan,
escape: '#7fd5e9',
color: whiskey,
url: '#7dc7ff',
keyword: violet,
self: '#d98ae8',
null: '#ef8fa8',
atom: whiskey,
unit: '#fbd38a',
modifier: '#d391f2',
operatorKeyword: '#78c3d6',
controlKeyword: '#bf6edb',
definitionKeyword: '#d383e6',
moduleKeyword: '#a6c1ff',
operator: cyan,
derefOperator: '#72c1d3',
arithmeticOperator: '#6ab4ce',
logicOperator: '#6ccad7',
bitwiseOperator: '#4fa8c2',
compareOperator: '#64b9cc',
updateOperator: '#4299b8',
definitionOperator: '#398daf',
typeOperator: '#3fc4e2',
controlOperator: '#3f96b0',
punctuation: '#8eaac2',
separator: '#7a96b1',
bracket: '#b3bcc7',
angleBracket: '#cfd5dd',
squareBracket: '#96a2ae',
paren: '#7f8c97',
brace: '#9aa5af',
content: ivory,
heading: coral,
heading1: '#ffb19d',
heading2: '#ffa188',
heading3: '#ff9173',
heading4: '#ff825e',
heading5: '#ff7249',
heading6: '#ff6234',
contentSeparator: cyan,
list: '#9da7b4',
quote: '#8b94a4',
emphasis: ivory,
strong: '#f4f6f8',
link: malibu,
monospace: '#c2cad1',
strikethrough: '#9ea5b1',
inserted: sage,
deleted: coral,
changed: whiskey,
invalid: '#ffffff',
meta: '#96a1b4',
documentMeta: '#8a95a6',
annotation: '#84d0ff',
processingInstruction: '#7c889c',
definition: '#c9cfd8',
constant: whiskey,
function: malibu,
standard: '#aeb7c5',
local: '#b9c2ce',
special: '#f4d67a',
};
export const oneDark: Extension = createBaseTheme(config);

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'solarized-dark', themeName: 'solarized-dark',
dark: true, dark: true,
// 基础色调 background: '#002b36',
background: '#002B36', backgroundSecondary: '#003643',
backgroundSecondary: '#003643FF',
surface: '#002B36',
dropdownBackground: '#002B36',
dropdownBorder: '#2AA19899',
// 文本颜色 foreground: '#fdf6e3',
foreground: '#93A1A1', cursor: '#d30102',
foregroundSecondary: '#93A1A1',
comment: '#586E75',
// 语法高亮色 - 核心
keyword: '#859900',
string: '#2AA198',
function: '#268BD2',
number: '#D33682',
operator: '#859900',
variable: '#268BD2',
type: '#CB4B16',
// 语法高亮色 - 扩展
constant: '#CB4B16',
storage: '#93A1A1',
parameter: '#268BD2',
class: '#CB4B16',
heading: '#268BD2',
invalid: '#DC322F',
regexp: '#DC322F',
// 界面元素
cursor: '#D30102',
selection: '#274642', selection: '#274642',
selectionBlur: '#274642',
activeLine: '#005b7022', activeLine: '#005b7022',
lineNumber: '#93A1A1', lineNumber: '#93a1a1',
activeLineNumber: '#949494', activeLineNumber: '#949494',
diffInserted: '#859900',
// 边框和分割线 diffDeleted: '#dc322f',
diffChanged: '#b58900',
borderColor: '#073642', borderColor: '#073642',
borderLight: '#93A1A119',
// 搜索和匹配
searchMatch: '#2AA198',
matchingBracket: '#073642', matchingBracket: '#073642',
}
// 使用通用主题工厂函数创建 Solarized Dark 主题 comment: '#586e75',
export const solarizedDark: Extension = createBaseTheme(config) lineComment: '#4f646a',
blockComment: '#46595e',
docComment: '#7c8f94',
name: '#fdf6e3',
variableName: '#b58900',
typeName: '#2aa198',
tagName: '#2ab7a5',
propertyName: '#d7c8a1',
attributeName: '#f1c795',
className: '#b58900',
labelName: '#d7991f',
namespace: '#3ca8a0',
macroName: '#268bd2',
literal: '#d33682',
string: '#859900',
docString: '#9cc200',
character: '#b3dd00',
attributeValue: '#e1c272',
number: '#d33682',
integer: '#c0478a',
float: '#b03a79',
bool: '#ffcc4d',
regexp: '#2aa198',
escape: '#35bcb1',
color: '#d19100',
url: '#268bd2',
keyword: '#cb4b16',
self: '#e2572f',
null: '#ff6845',
atom: '#d33682',
unit: '#ad8100',
modifier: '#d96d22',
operatorKeyword: '#bc5822',
controlKeyword: '#c14a17',
definitionKeyword: '#de5c29',
moduleKeyword: '#d4975b',
operator: '#6c71c4',
derefOperator: '#8a78d8',
arithmeticOperator: '#7d6fd0',
logicOperator: '#8376d5',
bitwiseOperator: '#6a5dc3',
compareOperator: '#8171cd',
updateOperator: '#5d54b4',
definitionOperator: '#5f56b8',
typeOperator: '#379e9d',
controlOperator: '#5950a7',
punctuation: '#b1a6d2',
separator: '#a090c1',
bracket: '#cac5dc',
angleBracket: '#e4e1ee',
squareBracket: '#bdb6cf',
paren: '#aba5c0',
brace: '#c2bcd5',
content: '#fdf6e3',
heading: '#cb4b16',
heading1: '#e06c2c',
heading2: '#d95d1b',
heading3: '#c1500f',
heading4: '#b3450a',
heading5: '#a33805',
heading6: '#932c00',
contentSeparator: '#6c71c4',
list: '#c3b79f',
quote: '#8b968f',
emphasis: '#fdf6e3',
strong: '#fefaf0',
link: '#268bd2',
monospace: '#d6cfbd',
strikethrough: '#c4bba5',
inserted: '#859900',
deleted: '#dc322f',
changed: '#b58900',
invalid: '#dc322f',
meta: '#687b84',
documentMeta: '#5f7179',
annotation: '#2bb0cf',
processingInstruction: '#5a6b71',
definition: '#dacfb9',
constant: '#d33682',
function: '#268bd2',
standard: '#9bb1b0',
local: '#b4c3bb',
special: '#b58900',
};
export const solarizedDark: Extension = createBaseTheme(config);

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'tokyo-night-storm', themeName: 'tokyo-night-storm',
dark: true, dark: true,
// 基础色调
background: '#24283b', background: '#24283b',
backgroundSecondary: '#2B3151FF', backgroundSecondary: '#2b3151',
surface: '#24283b',
dropdownBackground: '#24283b',
dropdownBorder: '#7982a9',
// 文本颜色 foreground: '#c0caf5',
foreground: '#7982a9',
foregroundSecondary: '#7982a9',
comment: '#565f89',
// 语法高亮色 - 核心
keyword: '#bb9af7',
string: '#9ece6a',
function: '#7aa2f7',
number: '#ff9e64',
operator: '#bb9af7',
variable: '#c0caf5',
type: '#2ac3de',
// 语法高亮色 - 扩展
constant: '#bb9af7',
storage: '#bb9af7',
parameter: '#c0caf5',
class: '#c0caf5',
heading: '#89ddff',
invalid: '#ff5370',
regexp: '#b4f9f8',
// 界面元素
cursor: '#c0caf5', cursor: '#c0caf5',
selection: '#6f7bb630', selection: '#6f7bb630',
selectionBlur: '#6f7bb630',
activeLine: '#4d547722', activeLine: '#4d547722',
lineNumber: '#3b4261', lineNumber: '#3b4261',
activeLineNumber: '#737aa2', activeLineNumber: '#737aa2',
diffInserted: '#9ece6a',
// 边框和分割线 diffDeleted: '#f7768e',
diffChanged: '#ff9e64',
borderColor: '#1f2335', borderColor: '#1f2335',
borderLight: '#7982a919',
// 搜索和匹配
searchMatch: '#7aa2f7',
matchingBracket: '#1f2335', matchingBracket: '#1f2335',
}
// 使用通用主题工厂函数创建 Tokyo Night Storm 主题 comment: '#565f89',
export const tokyoNightStorm: Extension = createBaseTheme(config) lineComment: '#4d567b',
blockComment: '#454e6f',
docComment: '#6f789b',
name: '#c0caf5',
variableName: '#c0caf5',
typeName: '#2ac3de',
tagName: '#5ad4e9',
propertyName: '#cfd4ed',
attributeName: '#f2bde1',
className: '#8fb7ff',
labelName: '#ffc28b',
namespace: '#3cd4e9',
macroName: '#7aa2f7',
literal: '#ff9e64',
string: '#9ece6a',
docString: '#abd88a',
character: '#cff7a8',
attributeValue: '#f8cda5',
number: '#ff9e64',
integer: '#ffae7d',
float: '#ffa467',
bool: '#ffbf75',
regexp: '#b4f9f8',
escape: '#91f1ff',
color: '#ffb782',
url: '#7aa2f7',
keyword: '#bb9af7',
self: '#d8a2ff',
null: '#ff96be',
atom: '#ff9e64',
unit: '#f7d38a',
modifier: '#dab5ff',
operatorKeyword: '#d0b2ff',
controlKeyword: '#ce9cff',
definitionKeyword: '#dcbaff',
moduleKeyword: '#aab8ff',
operator: '#bb9af7',
derefOperator: '#d2a7ff',
arithmeticOperator: '#cda0ff',
logicOperator: '#b78fff',
bitwiseOperator: '#a179f0',
compareOperator: '#b984ff',
updateOperator: '#a071f1',
definitionOperator: '#9366e3',
typeOperator: '#3ed2f2',
controlOperator: '#8a61f6',
punctuation: '#9da7cd',
separator: '#8f9abc',
bracket: '#afb6cb',
angleBracket: '#cad1e3',
squareBracket: '#959bb3',
paren: '#828aa4',
brace: '#96a0b8',
content: '#c0caf5',
heading: '#89ddff',
heading1: '#9fe2ff',
heading2: '#8bd9ff',
heading3: '#77d0ff',
heading4: '#63c7ff',
heading5: '#4fbeff',
heading6: '#3bb5ff',
contentSeparator: '#bb9af7',
list: '#858caa',
quote: '#5f6583',
emphasis: '#c0caf5',
strong: '#e7ebff',
link: '#7aa2f7',
monospace: '#a8aac1',
strikethrough: '#7f85a5',
inserted: '#9ece6a',
deleted: '#f7768e',
changed: '#ff9e64',
invalid: '#ff5370',
meta: '#656c90',
documentMeta: '#5c6381',
annotation: '#8df1ff',
processingInstruction: '#545975',
definition: '#bcc3df',
constant: '#ff9e64',
function: '#7aa2f7',
standard: '#8e9ac0',
local: '#98a2c8',
special: '#ffc68a',
};
export const tokyoNightStorm: Extension = createBaseTheme(config);

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'tokyo-night', themeName: 'tokyo-night',
dark: true, dark: true,
// 基础色调
background: '#1a1b26', background: '#1a1b26',
backgroundSecondary: '#272839FF', backgroundSecondary: '#272839',
surface: '#1a1b26',
dropdownBackground: '#1a1b26',
dropdownBorder: '#787c99',
// 文本颜色 foreground: '#c0caf5',
foreground: '#787c99',
foregroundSecondary: '#787c99',
comment: '#444b6a',
// 语法高亮色 - 核心
keyword: '#bb9af7',
string: '#9ece6a',
function: '#7aa2f7',
number: '#ff9e64',
operator: '#bb9af7',
variable: '#c0caf5',
type: '#0db9d7',
// 语法高亮色 - 扩展
constant: '#bb9af7',
storage: '#bb9af7',
parameter: '#c0caf5',
class: '#c0caf5',
heading: '#89ddff',
invalid: '#ff5370',
regexp: '#b4f9f8',
// 界面元素
cursor: '#c0caf5', cursor: '#c0caf5',
selection: '#515c7e40', selection: '#515c7e40',
selectionBlur: '#515c7e40',
activeLine: '#43455c22', activeLine: '#43455c22',
lineNumber: '#363b54', lineNumber: '#363b54',
activeLineNumber: '#737aa2', activeLineNumber: '#737aa2',
diffInserted: '#9ece6a',
// 边框和分割线 diffDeleted: '#f7768e',
diffChanged: '#ff9e64',
borderColor: '#16161e', borderColor: '#16161e',
borderLight: '#787c9919',
// 搜索和匹配
searchMatch: '#7aa2f7',
matchingBracket: '#16161e', matchingBracket: '#16161e',
}
// 使用通用主题工厂函数创建 Tokyo Night 主题 comment: '#444b6a',
export const tokyoNight: Extension = createBaseTheme(config) lineComment: '#3d4360',
blockComment: '#373d55',
docComment: '#5a6084',
name: '#c0caf5',
variableName: '#c0caf5',
typeName: '#0db9d7',
tagName: '#68dce9',
propertyName: '#d0d5f0',
attributeName: '#f0b7d7',
className: '#7aa2f7',
labelName: '#ffbd82',
namespace: '#38c5d9',
macroName: '#7aa2f7',
literal: '#ff9e64',
string: '#9ece6a',
docString: '#adda7d',
character: '#d7f5a2',
attributeValue: '#f6c299',
number: '#ff9e64',
integer: '#ffae7d',
float: '#ffa467',
bool: '#ffb86c',
regexp: '#b4f9f8',
escape: '#8df0f0',
color: '#ff9e64',
url: '#7aa2f7',
keyword: '#bb9af7',
self: '#d0a0ff',
null: '#ff90b2',
atom: '#ff9e64',
unit: '#fbd38a',
modifier: '#d6b4ff',
operatorKeyword: '#ccabff',
controlKeyword: '#c89cff',
definitionKeyword: '#d6baff',
moduleKeyword: '#9fb3ff',
operator: '#bb9af7',
derefOperator: '#c7a3ff',
arithmeticOperator: '#c39aff',
logicOperator: '#b08eff',
bitwiseOperator: '#9b78f0',
compareOperator: '#ae9bf5',
updateOperator: '#9a82e7',
definitionOperator: '#8b75d7',
typeOperator: '#3fc4e2',
controlOperator: '#9266f2',
punctuation: '#98a1c3',
separator: '#8b93b4',
bracket: '#a4acc6',
angleBracket: '#c1c8df',
squareBracket: '#8e96b5',
paren: '#7a83a3',
brace: '#8f98b5',
content: '#c0caf5',
heading: '#89ddff',
heading1: '#9ce9ff',
heading2: '#88dfff',
heading3: '#74d5ff',
heading4: '#60cbff',
heading5: '#4cc1ff',
heading6: '#38b7ff',
contentSeparator: '#bb9af7',
list: '#7d849f',
quote: '#5d6382',
emphasis: '#c0caf5',
strong: '#e6e9ff',
link: '#7aa2f7',
monospace: '#a9aac0',
strikethrough: '#7f8399',
inserted: '#9ece6a',
deleted: '#f7768e',
changed: '#ff9e64',
invalid: '#ff5370',
meta: '#61678b',
documentMeta: '#585d7c',
annotation: '#8cf5ff',
processingInstruction: '#525670',
definition: '#bfc6e4',
constant: '#ff9e64',
function: '#7aa2f7',
standard: '#8490b3',
local: '#909abf',
special: '#ffb347',
};
export const tokyoNight: Extension = createBaseTheme(config);

View File

@@ -1,63 +1,111 @@
import {createBaseTheme} from '../base'; import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types'; import type {ThemeColors} from '../types';
// 默认浅色主题颜色
export const defaultLightColors: ThemeColors = { export const defaultLightColors: ThemeColors = {
// 主题信息 themeName: 'default-light',
name: 'default-light',
dark: false, dark: false,
// 基础色调 background: '#ffffff',
background: '#ffffff', // 主背景色 backgroundSecondary: '#f4f7fb',
backgroundSecondary: '#f1faf1', // 次要背景色
surface: '#f5f5f5', // 面板背景
dropdownBackground: '#ffffff', // 下拉菜单背景
dropdownBorder: '#e1e4e8', // 下拉菜单边框
// 文本颜色 foreground: '#24292e',
foreground: '#444d56', // 主文本色 cursor: '#000000',
foregroundSecondary: '#6a737d', // 次要文本色 selection: '#77baff',
comment: '#6a737d', // 注释色 activeLine: '#0000000a',
lineNumber: '#00000040',
activeLineNumber: '#000000aa',
diffInserted: '#2da44e',
diffDeleted: '#d73a49',
diffChanged: '#c69026',
borderColor: '#d8dee4',
matchingBracket: '#00000019',
// 语法高亮色 - 核心 comment: '#6a737d',
keyword: '#d73a49', // 关键字 lineComment: '#808a95',
string: '#032f62', // 字符串 blockComment: '#5c646f',
function: '#005cc5', // 函数名 docComment: '#909ba8',
number: '#005cc5', // 数字 name: '#1f2329',
operator: '#d73a49', // 操作符 variableName: '#27313c',
variable: '#24292e', // 变量 typeName: '#5b32c2',
type: '#6f42c1', // 类型 tagName: '#3d60c9',
propertyName: '#384252',
// 语法高亮色 - 扩展 attributeName: '#7c3aed',
constant: '#005cc5', // 常量 className: '#4c3ad7',
storage: '#d73a49', // 存储类型 labelName: '#975400',
parameter: '#24292e', // 参数 namespace: '#2f6f9f',
class: '#6f42c1', // 类名 macroName: '#ae5f00',
heading: '#d73a49', // 标题 literal: '#0b5cc5',
invalid: '#cb2431', // 无效内容 string: '#032f62',
regexp: '#032f62', // 正则表达式 docString: '#0a477f',
character: '#174f92',
// 界面元素 attributeValue: '#8c4f00',
cursor: '#000000', // 光标 number: '#0f65c9',
selection: '#77baff', // 选中背景 integer: '#0075d6',
selectionBlur: '#b2c2ca', // 失焦选中背景 float: '#0086e6',
activeLine: '#0000000a', // 当前行高亮 bool: '#b7410e',
lineNumber: '#00000040', // 行号 regexp: '#2362a1',
activeLineNumber: '#000000aa', // 活动行号 escape: '#3383c5',
color: '#db6e00',
// 边框和分割线 url: '#005cc5',
borderColor: '#dfdfdf', // 边框色 keyword: '#d73a49',
borderLight: '#0000000c', // 浅色边框 self: '#b92548',
null: '#be1347',
// 搜索和匹配 atom: '#8241c1',
searchMatch: '#005cc5', // 搜索匹配 unit: '#a75500',
matchingBracket: '#00000019', // 匹配括号 modifier: '#c9245d',
operatorKeyword: '#c23143',
controlKeyword: '#bf213a',
definitionKeyword: '#d45563',
moduleKeyword: '#c2476a',
operator: '#c93a56',
derefOperator: '#cf4c67',
arithmeticOperator: '#c82a57',
logicOperator: '#c23a5e',
bitwiseOperator: '#c23f4d',
compareOperator: '#c9455b',
updateOperator: '#c2304d',
definitionOperator: '#ca3c6b',
typeOperator: '#a642d9',
controlOperator: '#bd3552',
punctuation: '#a37a00',
separator: '#a87700',
bracket: '#7c8ba1',
angleBracket: '#7a9fbd',
squareBracket: '#99a7c3',
paren: '#5c6e90',
brace: '#8d96a8',
content: '#1f2329',
heading: '#b35900',
heading1: '#b04a00',
heading2: '#b55e00',
heading3: '#b96f00',
heading4: '#bb7c00',
heading5: '#be8900',
heading6: '#c29500',
contentSeparator: '#c4b200',
list: '#566266',
quote: '#7c858f',
emphasis: '#1f2329',
strong: '#111217',
link: '#0a58ca',
monospace: '#3a434f',
strikethrough: '#5d6469',
inserted: '#2da44e',
deleted: '#d73a49',
changed: '#c69026',
invalid: '#cb2431',
meta: '#4c5a6b',
documentMeta: '#5e6977',
annotation: '#7f4cd6',
processingInstruction: '#4f5b63',
definition: '#30404d',
constant: '#0b5cc5',
function: '#005cc5',
standard: '#40566b',
local: '#2f3944',
special: '#a44500',
}; };
// 创建浅色主题
export function createLightTheme(colors: ThemeColors = defaultLightColors) { export function createLightTheme(colors: ThemeColors = defaultLightColors) {
return createBaseTheme({...colors, dark: false}); return createBaseTheme({...colors, dark: false});
} }
// 默认浅色主题
export const defaultLight = createLightTheme(defaultLightColors);

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'github-light', themeName: 'github-light',
dark: false, dark: false,
// 基础色调 background: '#ffffff',
background: '#fff',
backgroundSecondary: '#f1faf1', backgroundSecondary: '#f1faf1',
surface: '#fff',
dropdownBackground: '#fff',
dropdownBorder: '#e1e4e8',
// 文本颜色
foreground: '#444d56', foreground: '#444d56',
foregroundSecondary: '#444d56',
comment: '#6a737d',
// 语法高亮色 - 核心
keyword: '#d73a49',
string: '#032f62',
function: '#005cc5',
number: '#005cc5',
operator: '#d73a49',
variable: '#e36209',
type: '#005cc5',
// 语法高亮色 - 扩展
constant: '#005cc5',
storage: '#d73a49',
parameter: '#24292e',
class: '#6f42c1',
heading: '#005cc5',
invalid: '#cb2431',
regexp: '#032f62',
// 界面元素
cursor: '#044289', cursor: '#044289',
selection: '#0366d625', selection: '#0366d625',
selectionBlur: '#0366d625',
activeLine: '#c6c6c622', activeLine: '#c6c6c622',
lineNumber: '#1b1f234d', lineNumber: '#1b1f234d',
activeLineNumber: '#24292e', activeLineNumber: '#24292e',
diffInserted: '#2ea043',
// 边框和分割线 diffDeleted: '#cb2431',
diffChanged: '#f2cc60',
borderColor: '#e1e4e8', borderColor: '#e1e4e8',
borderLight: '#444d5619',
// 搜索和匹配
searchMatch: '#005cc5',
matchingBracket: '#34d05840', matchingBracket: '#34d05840',
}
// 使用通用主题工厂函数创建 GitHub Light 主题 comment: '#6a737d',
export const githubLight: Extension = createBaseTheme(config) lineComment: '#5e6873',
blockComment: '#4f5864',
docComment: '#7a828c',
name: '#444d56',
variableName: '#e36209',
typeName: '#005cc5',
tagName: '#0b5cbf',
propertyName: '#5c6670',
attributeName: '#a34f8b',
className: '#6f42c1',
labelName: '#e36209',
namespace: '#1667c1',
macroName: '#005cc5',
literal: '#005cc5',
string: '#032f62',
docString: '#1a4a7d',
character: '#205893',
attributeValue: '#ad6f2c',
number: '#005cc5',
integer: '#0a4cb0',
float: '#0c58c6',
bool: '#b08800',
regexp: '#032f62',
escape: '#0a7dd6',
color: '#bf7a0f',
url: '#005cc5',
keyword: '#d73a49',
self: '#f14e6f',
null: '#d73a49',
atom: '#005cc5',
unit: '#a07400',
modifier: '#e36209',
operatorKeyword: '#cc3745',
controlKeyword: '#c2303d',
definitionKeyword: '#de4f5d',
moduleKeyword: '#9c4fd8',
operator: '#d73a49',
derefOperator: '#c02c3a',
arithmeticOperator: '#b02834',
logicOperator: '#b83341',
bitwiseOperator: '#9a2334',
compareOperator: '#c1343f',
updateOperator: '#a8282f',
definitionOperator: '#99212c',
typeOperator: '#1c8198',
controlOperator: '#882033',
punctuation: '#c3a5d5',
separator: '#bf9fcf',
bracket: '#97a3bf',
angleBracket: '#bcc7dd',
squareBracket: '#9aa6bf',
paren: '#7d889f',
brace: '#a8b2c6',
content: '#444d56',
heading: '#005cc5',
heading1: '#2a72c7',
heading2: '#2665b5',
heading3: '#2158a3',
heading4: '#1c4b91',
heading5: '#173e7f',
heading6: '#12316d',
contentSeparator: '#d73a49',
list: '#9aa2ad',
quote: '#7a828c',
emphasis: '#444d56',
strong: '#1b1f23',
link: '#005cc5',
monospace: '#6a737d',
strikethrough: '#737b84',
inserted: '#2ea043',
deleted: '#cb2431',
changed: '#f2cc60',
invalid: '#cb2431',
meta: '#5f6a74',
documentMeta: '#57606a',
annotation: '#0b7bbd',
processingInstruction: '#4d5860',
definition: '#7a848f',
constant: '#005cc5',
function: '#005cc5',
standard: '#5a646d',
local: '#6c757f',
special: '#b08800',
};
export const githubLight: Extension = createBaseTheme(config);

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'material-light', themeName: 'material-light',
dark: false, dark: false,
// 基础色调 background: '#fafafa',
background: '#FAFAFA',
backgroundSecondary: '#f1faf1', backgroundSecondary: '#f1faf1',
surface: '#FAFAFA',
dropdownBackground: '#FAFAFA',
dropdownBorder: '#00000010',
// 文本颜色 foreground: '#90a4ae',
foreground: '#90A4AE',
foregroundSecondary: '#90A4AE',
comment: '#90A4AE',
// 语法高亮色 - 核心
keyword: '#7C4DFF',
string: '#91B859',
function: '#6182B8',
number: '#F76D47',
operator: '#7C4DFF',
variable: '#90A4AE',
type: '#8796B0',
// 语法高亮色 - 扩展
constant: '#F76D47',
storage: '#7C4DFF',
parameter: '#90A4AE',
class: '#FFB62C',
heading: '#91B859',
invalid: '#E53935',
regexp: '#39ADB5',
// 界面元素
cursor: '#272727', cursor: '#272727',
selection: '#80CBC440', selection: '#80cbc440',
selectionBlur: '#80CBC440',
activeLine: '#c2c2c222', activeLine: '#c2c2c222',
lineNumber: '#CFD8DC', lineNumber: '#cfd8dc',
activeLineNumber: '#7E939E', activeLineNumber: '#7e939e',
diffInserted: '#91b859',
// 边框和分割线 diffDeleted: '#e53935',
diffChanged: '#ffcb6b',
borderColor: '#00000010', borderColor: '#00000010',
borderLight: '#90A4AE19', matchingBracket: '#fafafa',
// 搜索和匹配 comment: '#90a4ae',
searchMatch: '#6182B8', lineComment: '#8598a3',
matchingBracket: '#FAFAFA', blockComment: '#788b97',
} docComment: '#a3b6c1',
name: '#90a4ae',
variableName: '#90a4ae',
typeName: '#8796b0',
tagName: '#8ab0c7',
propertyName: '#bcccd5',
attributeName: '#ffb7c5',
className: '#ffb62c',
labelName: '#f78c6c',
namespace: '#61bcd2',
macroName: '#6182b8',
literal: '#f76d47',
string: '#91b859',
docString: '#a2cf6e',
character: '#cbe58f',
attributeValue: '#f7c493',
number: '#f76d47',
integer: '#f88760',
float: '#ff9a73',
bool: '#caa840',
regexp: '#39adb5',
escape: '#5ed1ce',
color: '#f4a35d',
url: '#4c91d6',
keyword: '#7c4dff',
self: '#9f77ff',
null: '#c9495f',
atom: '#f76d47',
unit: '#cf7a44',
modifier: '#a07ffe',
operatorKeyword: '#9c7bff',
controlKeyword: '#8a66ff',
definitionKeyword: '#a782ff',
moduleKeyword: '#6c89ff',
operator: '#7c4dff',
derefOperator: '#a07cfe',
arithmeticOperator: '#916dff',
logicOperator: '#9c74ff',
bitwiseOperator: '#7a5bdd',
compareOperator: '#8b64ef',
updateOperator: '#6f4ecf',
definitionOperator: '#6a4bc7',
typeOperator: '#5bbbd2',
controlOperator: '#6244b5',
punctuation: '#a2b3c0',
separator: '#94a3b0',
bracket: '#c2cad0',
angleBracket: '#dee5eb',
squareBracket: '#b8c1c7',
paren: '#a5aebb',
brace: '#bcc4ce',
content: '#90a4ae',
heading: '#91b859',
heading1: '#a5cf6c',
heading2: '#9bc261',
heading3: '#92b656',
heading4: '#89a94c',
heading5: '#809d41',
heading6: '#779136',
contentSeparator: '#7c4dff',
list: '#a6b4bb',
quote: '#7c8a91',
emphasis: '#90a4ae',
strong: '#3e4a52',
link: '#6182b8',
monospace: '#b7c4cc',
strikethrough: '#98a5ad',
inserted: '#91b859',
deleted: '#e53935',
changed: '#ffcb6b',
invalid: '#e53935',
meta: '#8ca0a9',
documentMeta: '#7e9099',
annotation: '#58d3e6',
processingInstruction: '#6d7e87',
definition: '#c7d2d6',
constant: '#f76d47',
function: '#6182b8',
standard: '#a5b2b8',
local: '#b3c0c7',
special: '#ffb62c',
};
// 使用通用主题工厂函数创建 Material Light 主题 export const materialLight: Extension = createBaseTheme(config);
export const materialLight: Extension = createBaseTheme(config)

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'solarized-light', themeName: 'solarized-light',
dark: false, dark: false,
// 基础色调 background: '#fdf6e3',
background: '#FDF6E3', backgroundSecondary: '#ffeeccd4',
backgroundSecondary: '#FFEEBCD4',
surface: '#FDF6E3',
dropdownBackground: '#FDF6E3',
dropdownBorder: '#D3AF86',
// 文本颜色 foreground: '#586e75',
foreground: '#586E75', cursor: '#657b83',
foregroundSecondary: '#586E75', selection: '#eee8d5',
comment: '#93A1A1',
// 语法高亮色 - 核心
keyword: '#859900',
string: '#2AA198',
function: '#268BD2',
number: '#D33682',
operator: '#859900',
variable: '#268BD2',
type: '#CB4B16',
// 语法高亮色 - 扩展
constant: '#CB4B16',
storage: '#586E75',
parameter: '#268BD2',
class: '#CB4B16',
heading: '#268BD2',
invalid: '#DC322F',
regexp: '#DC322F',
// 界面元素
cursor: '#657B83',
selection: '#EEE8D5',
selectionBlur: '#EEE8D5',
activeLine: '#d5bd5c22', activeLine: '#d5bd5c22',
lineNumber: '#586E75', lineNumber: '#586e75',
activeLineNumber: '#567983', activeLineNumber: '#567983',
diffInserted: '#2aa198',
diffDeleted: '#dc322f',
diffChanged: '#b58900',
borderColor: '#eee8d5',
matchingBracket: '#eee8d5',
// 边框和分割线 comment: '#93a1a1',
borderColor: '#EEE8D5', lineComment: '#82939d',
borderLight: '#586E7519', blockComment: '#7a8b95',
docComment: '#a5b6be',
name: '#586e75',
variableName: '#268bd2',
typeName: '#cb4b16',
tagName: '#2cbeb1',
propertyName: '#b3baba',
attributeName: '#d4835a',
className: '#cb4b16',
labelName: '#c98c0d',
namespace: '#3bb3ae',
macroName: '#268bd2',
literal: '#d33682',
string: '#2aa198',
docString: '#23b1a2',
character: '#4bd2c7',
attributeValue: '#c09a53',
number: '#d33682',
integer: '#c74a78',
float: '#b93d6b',
bool: '#b58900',
regexp: '#2aa198',
escape: '#3ad1c5',
color: '#cb4b16',
url: '#268bd2',
keyword: '#859900',
self: '#97aa06',
null: '#bf5f00',
atom: '#cb4b16',
unit: '#a57300',
modifier: '#a1871e',
operatorKeyword: '#76860a',
controlKeyword: '#7d9509',
definitionKeyword: '#7ba600',
moduleKeyword: '#5e9d76',
operator: '#859900',
derefOperator: '#9daa22',
arithmeticOperator: '#8c9b19',
logicOperator: '#85a612',
bitwiseOperator: '#6a7e0a',
compareOperator: '#7da811',
updateOperator: '#63740a',
definitionOperator: '#5f6c08',
typeOperator: '#2aa198',
controlOperator: '#586e0b',
punctuation: '#9db3ae',
separator: '#8c9f96',
bracket: '#cad2c9',
angleBracket: '#e3e8e0',
squareBracket: '#b7bfb7',
paren: '#939c93',
brace: '#c2c9c1',
content: '#586e75',
heading: '#268bd2',
heading1: '#3fb7d4',
heading2: '#36abc8',
heading3: '#2d9fbc',
heading4: '#2493b0',
heading5: '#1b87a4',
heading6: '#127b98',
contentSeparator: '#859900',
list: '#a7a591',
quote: '#8f9b8e',
emphasis: '#586e75',
strong: '#657b83',
link: '#268bd2',
monospace: '#c8c2b4',
strikethrough: '#bab29d',
inserted: '#2aa198',
deleted: '#dc322f',
changed: '#b58900',
invalid: '#dc322f',
meta: '#7d8b8f',
documentMeta: '#758288',
annotation: '#3ab4c3',
processingInstruction: '#6a7377',
definition: '#bcc7c0',
constant: '#cb4b16',
function: '#268bd2',
standard: '#8da1a0',
local: '#9eb1ac',
special: '#b58900',
};
// 搜索和匹配 export const solarizedLight: Extension = createBaseTheme(config);
searchMatch: '#268BD2',
matchingBracket: '#EEE8D5',
}
// 使用通用主题工厂函数创建 Solarized Light 主题
export const solarizedLight: Extension = createBaseTheme(config)

View File

@@ -1,57 +1,110 @@
import {Extension} from '@codemirror/state' import {Extension} from '@codemirror/state';
import {createBaseTheme} from '../base' import {createBaseTheme} from '../base';
import type {ThemeColors} from '../types' import type {ThemeColors} from '../types';
export const config: ThemeColors = { export const config: ThemeColors = {
name: 'tokyo-night-day', themeName: 'tokyo-night-day',
dark: false, dark: false,
// 基础色调
background: '#e1e2e7', background: '#e1e2e7',
backgroundSecondary: '#D2D8EFFF', backgroundSecondary: '#d2d8ef',
surface: '#e1e2e7',
dropdownBackground: '#e1e2e7',
dropdownBorder: '#6a6f8e',
// 文本颜色
foreground: '#6a6f8e', foreground: '#6a6f8e',
foregroundSecondary: '#6a6f8e',
comment: '#9da3c2',
// 语法高亮色 - 核心
keyword: '#9854f1',
string: '#587539',
function: '#2e7de9',
number: '#b15c00',
operator: '#9854f1',
variable: '#3760bf',
type: '#07879d',
// 语法高亮色 - 扩展
constant: '#9854f1',
storage: '#9854f1',
parameter: '#3760bf',
class: '#3760bf',
heading: '#006a83',
invalid: '#ff3e64',
regexp: '#2e5857',
// 界面元素
cursor: '#3760bf', cursor: '#3760bf',
selection: '#8591b840', selection: '#8591b840',
selectionBlur: '#8591b840',
activeLine: '#a7aaba22', activeLine: '#a7aaba22',
lineNumber: '#b3b6cd', lineNumber: '#b3b6cd',
activeLineNumber: '#68709a', activeLineNumber: '#68709a',
diffInserted: '#587539',
// 边框和分割线 diffDeleted: '#ff3e64',
diffChanged: '#b15c00',
borderColor: '#e9e9ec', borderColor: '#e9e9ec',
borderLight: '#6a6f8e19',
// 搜索和匹配
searchMatch: '#2e7de9',
matchingBracket: '#e9e9ec', matchingBracket: '#e9e9ec',
}
// 使用通用主题工厂函数创建 Tokyo Night Day 主题 comment: '#9da3c2',
export const tokyoNightDay: Extension = createBaseTheme(config) lineComment: '#8b90a8',
blockComment: '#7e849d',
docComment: '#aeb3cb',
name: '#6a6f8e',
variableName: '#3760bf',
typeName: '#07879d',
tagName: '#4d8cff',
propertyName: '#8fa3d2',
attributeName: '#df8fb0',
className: '#4a71d6',
labelName: '#c37300',
namespace: '#3c99c0',
macroName: '#2e7de9',
literal: '#b15c00',
string: '#587539',
docString: '#4f8646',
character: '#79a058',
attributeValue: '#d28e43',
number: '#b15c00',
integer: '#d77500',
float: '#c36800',
bool: '#c79200',
regexp: '#2e5857',
escape: '#2c6f68',
color: '#c06f0f',
url: '#2e7de9',
keyword: '#9854f1',
self: '#c277ff',
null: '#ff5c7f',
atom: '#9854f1',
unit: '#ba6a00',
modifier: '#b16fff',
operatorKeyword: '#b67bff',
controlKeyword: '#ad68ff',
definitionKeyword: '#be84ff',
moduleKeyword: '#8f7dff',
operator: '#9854f1',
derefOperator: '#bb7fff',
arithmeticOperator: '#b173ff',
logicOperator: '#a369ff',
bitwiseOperator: '#8d59f0',
compareOperator: '#a673ff',
updateOperator: '#8c57dd',
definitionOperator: '#8150d3',
typeOperator: '#0aa5b5',
controlOperator: '#7741ca',
punctuation: '#9aa3c9',
separator: '#8f98be',
bracket: '#b5bdd6',
angleBracket: '#d8def0',
squareBracket: '#adb5cb',
paren: '#939ab7',
brace: '#b1b7cb',
content: '#6a6f8e',
heading: '#006a83',
heading1: '#0083a3',
heading2: '#007796',
heading3: '#006a89',
heading4: '#005e7c',
heading5: '#00516f',
heading6: '#004562',
contentSeparator: '#9854f1',
list: '#9ca1b8',
quote: '#8087a4',
emphasis: '#6a6f8e',
strong: '#404868',
link: '#2e7de9',
monospace: '#9ca0be',
strikethrough: '#7d819b',
inserted: '#587539',
deleted: '#ff3e64',
changed: '#b15c00',
invalid: '#ff3e64',
meta: '#8189a3',
documentMeta: '#737a92',
annotation: '#4ab2c9',
processingInstruction: '#6d7389',
definition: '#bdc2de',
constant: '#9854f1',
function: '#2e7de9',
standard: '#7a83a4',
local: '#8d95b3',
special: '#c17800',
};
export const tokyoNightDay: Extension = createBaseTheme(config);

View File

@@ -0,0 +1,52 @@
import type {ThemeColors} from './types';
import {ThemeType} from '@/../bindings/voidraft/internal/models/models';
import {defaultDarkColors} from './dark/default-dark';
import {defaultLightColors} from './light/default-light';
import {config as draculaColors} from './dark/dracula';
import {config as auraColors} from './dark/aura';
import {config as githubDarkColors} from './dark/github-dark';
import {config as materialDarkColors} from './dark/material-dark';
import {config as oneDarkColors} from './dark/one-dark';
import {config as solarizedDarkColors} from './dark/solarized-dark';
import {config as tokyoNightColors} from './dark/tokyo-night';
import {config as tokyoNightStormColors} from './dark/tokyo-night-storm';
import {config as githubLightColors} from './light/github-light';
import {config as materialLightColors} from './light/material-light';
import {config as solarizedLightColors} from './light/solarized-light';
import {config as tokyoNightDayColors} from './light/tokyo-night-day';
export interface ThemePreset {
name: string;
type: ThemeType;
colors: ThemeColors;
}
export const FALLBACK_THEME_NAME = defaultDarkColors.themeName;
export const themePresetList: ThemePreset[] = [
{name: defaultDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: defaultDarkColors},
{name: draculaColors.themeName, type: ThemeType.ThemeTypeDark, colors: draculaColors},
{name: auraColors.themeName, type: ThemeType.ThemeTypeDark, colors: auraColors},
{name: githubDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: githubDarkColors},
{name: materialDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: materialDarkColors},
{name: oneDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: oneDarkColors},
{name: solarizedDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: solarizedDarkColors},
{name: tokyoNightColors.themeName, type: ThemeType.ThemeTypeDark, colors: tokyoNightColors},
{name: tokyoNightStormColors.themeName, type: ThemeType.ThemeTypeDark, colors: tokyoNightStormColors},
{name: defaultLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: defaultLightColors},
{name: githubLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: githubLightColors},
{name: materialLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: materialLightColors},
{name: solarizedLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: solarizedLightColors},
{name: tokyoNightDayColors.themeName, type: ThemeType.ThemeTypeLight, colors: tokyoNightDayColors},
];
export const themePresetMap: Record<string, ThemePreset> = themePresetList.reduce(
(map, preset) => {
map[preset.name] = preset;
return map;
},
{} as Record<string, ThemePreset>
);
export const cloneThemeColors = (colors: ThemeColors): ThemeColors =>
JSON.parse(JSON.stringify(colors)) as ThemeColors;

View File

@@ -1,52 +1,196 @@
export interface ThemeColors { export type SyntaxTag =
// 主题基本信息 | 'comment'
name: string; // 主题名称 | 'lineComment'
dark: boolean; // 是否为深色主题标识 | 'blockComment'
| 'docComment'
| 'name'
| 'variableName'
| 'typeName'
| 'tagName'
| 'propertyName'
| 'attributeName'
| 'className'
| 'labelName'
| 'namespace'
| 'macroName'
| 'literal'
| 'string'
| 'docString'
| 'character'
| 'attributeValue'
| 'number'
| 'integer'
| 'float'
| 'bool'
| 'regexp'
| 'escape'
| 'color'
| 'url'
| 'keyword'
| 'self'
| 'null'
| 'atom'
| 'unit'
| 'modifier'
| 'operatorKeyword'
| 'controlKeyword'
| 'definitionKeyword'
| 'moduleKeyword'
| 'operator'
| 'derefOperator'
| 'arithmeticOperator'
| 'logicOperator'
| 'bitwiseOperator'
| 'compareOperator'
| 'updateOperator'
| 'definitionOperator'
| 'typeOperator'
| 'controlOperator'
| 'punctuation'
| 'separator'
| 'bracket'
| 'angleBracket'
| 'squareBracket'
| 'paren'
| 'brace'
| 'content'
| 'heading'
| 'heading1'
| 'heading2'
| 'heading3'
| 'heading4'
| 'heading5'
| 'heading6'
| 'contentSeparator'
| 'list'
| 'quote'
| 'emphasis'
| 'strong'
| 'link'
| 'monospace'
| 'strikethrough'
| 'inserted'
| 'deleted'
| 'changed'
| 'invalid'
| 'meta'
| 'documentMeta'
| 'annotation'
| 'processingInstruction'
| 'definition'
| 'constant'
| 'function'
| 'standard'
| 'local'
| 'special';
// 基础色调 export interface ThemeTagColors {
background: string; // 主背景色 comment: string;
backgroundSecondary: string; // 次要背景色(用于代码块交替背景) lineComment: string;
surface: string; // 面板背景 blockComment: string;
dropdownBackground: string; // 下拉菜单背景 docComment: string;
dropdownBorder: string; // 下拉菜单边框 name: string;
variableName: string;
// 文本颜色 typeName: string;
foreground: string; // 主文本色 tagName: string;
foregroundSecondary: string; // 次要文本色 propertyName: string;
comment: string; // 注释色 attributeName: string;
className: string;
// 语法高亮色 - 核心 labelName: string;
keyword: string; // 关键字 namespace: string;
string: string; // 字符串 macroName: string;
function: string; // 函数名 literal: string;
number: string; // 数字 string: string;
operator: string; // 操作符 docString: string;
variable: string; // 变量 character: string;
type: string; // 类型 attributeValue: string;
number: string;
// 语法高亮色 - 扩展 integer: string;
constant: string; // 常量 float: string;
storage: string; // 存储类型(如 static, const bool: string;
parameter: string; // 参数 regexp: string;
class: string; // 类名 escape: string;
heading: string; // 标题Markdown等 color: string;
invalid: string; // 无效内容/错误 url: string;
regexp: string; // 正则表达式 keyword: string;
self: string;
// 界面元素 null: string;
cursor: string; // 光标 atom: string;
selection: string; // 选中背景 unit: string;
selectionBlur: string; // 失焦选中背景 modifier: string;
activeLine: string; // 当前行高亮 operatorKeyword: string;
lineNumber: string; // 行号 controlKeyword: string;
activeLineNumber: string; // 活动行号颜色 definitionKeyword: string;
moduleKeyword: string;
// 边框和分割线 operator: string;
borderColor: string; // 边框色 derefOperator: string;
borderLight: string; // 浅色边框 arithmeticOperator: string;
logicOperator: string;
// 搜索和匹配 bitwiseOperator: string;
searchMatch: string; // 搜索匹配 compareOperator: string;
matchingBracket: string; // 匹配括号 updateOperator: string;
definitionOperator: string;
typeOperator: string;
controlOperator: string;
punctuation: string;
separator: string;
bracket: string;
angleBracket: string;
squareBracket: string;
paren: string;
brace: string;
content: string;
heading: string;
heading1: string;
heading2: string;
heading3: string;
heading4: string;
heading5: string;
heading6: string;
contentSeparator: string;
list: string;
quote: string;
emphasis: string;
strong: string;
link: string;
monospace: string;
strikethrough: string;
inserted: string;
deleted: string;
changed: string;
invalid: string;
meta: string;
documentMeta: string;
annotation: string;
processingInstruction: string;
definition: string;
constant: string;
function: string;
standard: string;
local: string;
special: string;
}
export interface ThemeColors extends ThemeTagColors {
themeName: string;
dark: boolean;
background: string; // 背景
backgroundSecondary: string; // 第二背景块
foreground: string; // 背景文字颜色
cursor: string; // 光标颜色
selection: string; // 选中文字颜色
activeLine: string; // 当前行颜色
lineNumber: string; // 行号颜色
activeLineNumber: string; // 当前行号颜色
diffInserted?: string; // 插入颜色
diffDeleted?: string; // 删除颜色
diffChanged?: string; // 变更颜色
borderColor: string; // 边框颜色
matchingBracket: string; // 匹配括号颜色
} }

View File

@@ -2,7 +2,7 @@
import { useConfigStore } from '@/stores/configStore'; import { useConfigStore } from '@/stores/configStore';
import { useThemeStore } from '@/stores/themeStore'; import { useThemeStore } from '@/stores/themeStore';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, watch, onMounted, ref } from 'vue'; import { computed, watch, onMounted, ref, nextTick } from 'vue';
import SettingSection from '../components/SettingSection.vue'; import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue'; import SettingItem from '../components/SettingItem.vue';
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models'; import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
@@ -50,7 +50,10 @@ const resetButtonState = ref({
// 当前选中的主题名称 // 当前选中的主题名称
const currentThemeName = computed({ const currentThemeName = computed({
get: () => themeStore.currentColors?.name || '', get: () =>
themeStore.currentColors?.themeName ||
configStore.config.appearance.currentTheme ||
'',
set: async (themeName: string) => { set: async (themeName: string) => {
await themeStore.switchToTheme(themeName); await themeStore.switchToTheme(themeName);
syncTempColors(); syncTempColors();
@@ -90,20 +93,38 @@ onMounted(() => {
const colorKeys = computed(() => { const colorKeys = computed(() => {
if (!tempColors.value) return []; if (!tempColors.value) return [];
// 获取所有字段,排除 name 和 dark这两个是元数据 return Object.keys(tempColors.value)
return Object.keys(tempColors.value).filter(key => .filter(key => key !== 'themeName' && key !== 'dark')
key !== 'name' && key !== 'dark' .sort((a, b) => a.localeCompare(b));
);
}); });
// 颜色配置列表
const colorList = computed(() => const colorList = computed(() =>
colorKeys.value.map(colorKey => ({ colorKeys.value.map(colorKey => ({
key: colorKey, key: colorKey,
label: t(`settings.themeColors.${colorKey}`) label: colorKey
})) }))
); );
const colorSearch = ref('');
const showSearch = ref(false);
const searchInputRef = ref<HTMLInputElement | null>(null);
const filteredColorList = computed(() => {
const keyword = colorSearch.value.trim().toLowerCase();
if (!keyword) return colorList.value;
return colorList.value.filter(color => color.key.toLowerCase().includes(keyword));
});
const toggleSearch = async () => {
showSearch.value = !showSearch.value;
if (showSearch.value) {
await nextTick();
searchInputRef.value?.focus();
} else {
colorSearch.value = '';
}
};
// 处理重置按钮点击 // 处理重置按钮点击
const handleResetClick = () => { const handleResetClick = () => {
if (resetButtonState.value.confirming) { if (resetButtonState.value.confirming) {
@@ -193,7 +214,7 @@ const updateSystemTheme = async (event: Event) => {
await themeStore.setTheme(selectedSystemTheme); await themeStore.setTheme(selectedSystemTheme);
const availableThemes = themeStore.availableThemes; const availableThemes = themeStore.availableThemes;
const currentThemeName = currentColors.value?.name; const currentThemeName = currentColors.value?.themeName;
const isCurrentThemeAvailable = availableThemes.some(t => t.name === currentThemeName); const isCurrentThemeAvailable = availableThemes.some(t => t.name === currentThemeName);
if (!isCurrentThemeAvailable && availableThemes.length > 0) { if (!isCurrentThemeAvailable && availableThemes.length > 0) {
@@ -242,7 +263,7 @@ const handlePickerClose = () => {
<!-- 预设主题选择 --> <!-- 预设主题选择 -->
<SettingItem :title="t('settings.presetTheme')"> <SettingItem :title="t('settings.presetTheme')">
<select class="select-input" v-model="currentThemeName" :disabled="hasUnsavedChanges"> <select class="select-input" v-model="currentThemeName" :disabled="hasUnsavedChanges">
<option v-for="theme in themeStore.availableThemes" :key="theme.id" :value="theme.name"> <option v-for="theme in themeStore.availableThemes" :key="theme.name" :value="theme.name">
{{ theme.name }} {{ theme.name }}
</option> </option>
</select> </select>
@@ -251,14 +272,27 @@ const handlePickerClose = () => {
<!-- 自定义主题颜色配置 --> <!-- 自定义主题颜色配置 -->
<SettingSection :title="t('settings.customThemeColors')"> <SettingSection :title="t('settings.customThemeColors')">
<template #title>
<div class="theme-section-title">
<span class="section-title-text">{{ t('settings.customThemeColors') }}</span>
<span v-if="currentColors.name" class="current-theme-name">{{ currentColors.name }}</span>
</div>
</template>
<template #title-right> <template #title-right>
<div class="theme-controls"> <div class="theme-controls">
<div :class="['theme-search-wrapper', showSearch ? 'active' : '']">
<input
ref="searchInputRef"
class="theme-search-input"
type="text"
v-model="colorSearch"
placeholder="Search..."
/>
</div>
<button class="search-toggle-button" @click="toggleSearch" :title="showSearch ? '关闭搜索' : '搜索颜色'">
<svg v-if="!showSearch" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<button <button
v-if="!hasUnsavedChanges" v-if="!hasUnsavedChanges"
:class="['reset-button', resetButtonState.confirming ? 'reset-button-confirming' : '']" :class="['reset-button', resetButtonState.confirming ? 'reset-button-confirming' : '']"
@@ -280,7 +314,7 @@ const handlePickerClose = () => {
<!-- 颜色列表 --> <!-- 颜色列表 -->
<div class="color-list"> <div class="color-list">
<SettingItem <SettingItem
v-for="color in colorList" v-for="color in filteredColorList"
:key="color.key" :key="color.key"
:title="color.label" :title="color.label"
class="color-setting-item" class="color-setting-item"
@@ -318,10 +352,6 @@ const handlePickerClose = () => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.settings-page {
//max-width: 1000px;
}
.select-input { .select-input {
min-width: 140px; min-width: 140px;
padding: 6px 10px; padding: 6px 10px;
@@ -349,27 +379,6 @@ const handlePickerClose = () => {
} }
} }
// 主题部分标题
.theme-section-title {
display: flex;
align-items: center;
gap: 12px;
}
.section-title-text {
font-weight: 500;
}
.current-theme-name {
font-size: 13px;
color: var(--settings-text-secondary);
font-weight: normal;
padding: 2px 8px;
background-color: var(--settings-input-bg);
border: 1px solid var(--settings-input-border);
border-radius: 4px;
}
// 主题控制区域 // 主题控制区域
.theme-controls { .theme-controls {
display: flex; display: flex;
@@ -441,6 +450,78 @@ const handlePickerClose = () => {
margin-top: 12px; margin-top: 12px;
} }
.theme-search-wrapper {
width: 0;
overflow: hidden;
transition: width 0.3s ease;
opacity: 0;
}
.theme-search-wrapper.active {
width: 150px;
margin-right: 8px;
opacity: 1;
}
.theme-search-input {
width: 100%;
padding: 6px 12px;
border: 1px solid var(--settings-input-border);
border-radius: 20px;
background-color: var(--settings-input-bg);
color: var(--settings-text);
font-size: 12px;
transition: all 0.2s ease;
height: 25px;
box-sizing: border-box;
&:focus {
outline: none;
border-color: #4a9eff;
background-color: var(--settings-card-bg);
box-shadow: 0 0 0 3px rgba(74, 158, 255, 0.1);
}
&::placeholder {
color: var(--settings-text-secondary);
opacity: 0.6;
}
}
.search-toggle-button {
display: flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
margin-right: 8px;
padding: 0;
border: 1px solid var(--settings-input-border);
border-radius: 50%;
background-color: var(--settings-button-bg);
color: var(--settings-button-text);
cursor: pointer;
transition: all 0.2s ease;
svg {
transition: transform 0.2s ease;
}
&:hover {
border-color: #4a9eff;
background-color: var(--settings-button-hover-bg);
transform: scale(1.05);
svg {
transform: scale(1.1);
}
}
&:active {
transform: scale(0.95);
}
}
.color-setting-item { .color-setting-item {
:deep(.setting-item-content) { :deep(.setting-item-content) {
align-items: center; align-items: center;

View File

@@ -19,16 +19,17 @@ onMounted(async () => {
// 字体选择选项 // 字体选择选项
const fontFamilyOptions = computed(() => configStore.fontOptions); const fontFamilyOptions = computed(() => configStore.fontOptions);
const currentFontFamily = computed(() => configStore.config.editing.fontFamily); const fontFamilyModel = computed({
get: () =>
// 字体选择 configStore.config.editing.fontFamily ||
const handleFontFamilyChange = async (event: Event) => { fontFamilyOptions.value[0]?.value ||
const target = event.target as HTMLSelectElement; '',
const fontFamily = target.value; set: async (fontFamily: string) => {
if (fontFamily) { if (fontFamily) {
await configStore.setFontFamily(fontFamily); await configStore.setFontFamily(fontFamily);
} }
}; }
});
// 字体粗细选项 // 字体粗细选项
const fontWeightOptions = [ const fontWeightOptions = [
@@ -44,10 +45,14 @@ const fontWeightOptions = [
]; ];
// 字体粗细选择 // 字体粗细选择
const handleFontWeightChange = async (event: Event) => { const fontWeightModel = computed({
const target = event.target as HTMLSelectElement; get: () => configStore.config.editing.fontWeight || fontWeightOptions[0].value,
await configStore.setFontWeight(target.value); set: async (value: string) => {
}; if (value) {
await configStore.setFontWeight(value);
}
}
});
// 行高控制 // 行高控制
const increaseLineHeight = async () => { const increaseLineHeight = async () => {
@@ -118,8 +123,7 @@ const handleAutoSaveDelayChange = async (event: Event) => {
> >
<select <select
class="font-family-select" class="font-family-select"
:value="currentFontFamily" v-model="fontFamilyModel"
@change="handleFontFamilyChange"
> >
<option <option
v-for="option in fontFamilyOptions" v-for="option in fontFamilyOptions"
@@ -146,8 +150,7 @@ const handleAutoSaveDelayChange = async (event: Event) => {
> >
<select <select
class="font-weight-select" class="font-weight-select"
:value="configStore.config.editing.fontWeight" v-model="fontWeightModel"
@change="handleFontWeightChange"
> >
<option <option
v-for="option in fontWeightOptions" v-for="option in fontWeightOptions"

View File

@@ -66,10 +66,12 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
if (!extension) return; if (!extension) return;
// 更新配置 // 更新配置
const updatedConfig = {...extension.config, [configKey]: value}; const updatedConfig = {...extension.config};
if (value === undefined) {
console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`); delete updatedConfig[configKey];
} else {
updatedConfig[configKey] = value;
}
// 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例 // 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig); await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
@@ -81,7 +83,7 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
// 重置扩展到默认配置 // 重置扩展到默认配置
const resetExtension = async (extensionId: ExtensionID) => { const resetExtension = async (extensionId: ExtensionID) => {
try { try {
// 重置到默认配置(后端) // 重置到默认配置
await ExtensionService.ResetExtensionToDefault(extensionId); await ExtensionService.ResetExtensionToDefault(extensionId);
// 重新加载扩展状态以获取最新配置 // 重新加载扩展状态以获取最新配置
@@ -92,63 +94,65 @@ const resetExtension = async (extensionId: ExtensionID) => {
if (extension) { if (extension) {
// 通过editorStore更新确保所有视图都能同步 // 通过editorStore更新确保所有视图都能同步
await editorStore.updateExtension(extensionId, extension.enabled, extension.config); await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`);
} }
} catch (error) { } catch (error) {
console.error('Failed to reset extension:', error); console.error('Failed to reset extension:', error);
} }
}; };
// 配置项类型定义 const getConfigValue = (
type ConfigItemType = 'toggle' | 'number' | 'text' | 'select' config: Record<string, any> | undefined,
configKey: string,
interface SelectOption { defaultValue: any
value: any ) => {
label: string if (config && Object.prototype.hasOwnProperty.call(config, configKey)) {
} return config[configKey];
interface ConfigItemMeta {
type: ConfigItemType
options?: SelectOption[]
}
// 只保留 select 类型的配置项元数据
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
[ExtensionID.ExtensionMinimap]: {
displayText: {
type: 'select',
options: [
{value: 'characters', label: 'Characters'},
{value: 'blocks', label: 'Blocks'}
]
},
showOverlay: {
type: 'select',
options: [
{value: 'always', label: 'Always'},
{value: 'mouse-over', label: 'Mouse Over'}
]
}
} }
return defaultValue;
}; };
// 获取配置项类型
const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => { const formatConfigValue = (value: any): string => {
const meta = extensionConfigMeta[extensionId]?.[configKey]; if (value === undefined) return '';
if (meta?.type) { try {
return meta.type; const serialized = JSON.stringify(value);
return serialized ?? '';
} catch (error) {
console.warn('Failed to stringify config value', error);
return '';
} }
// 根据默认值类型自动推断
if (typeof defaultValue === 'boolean') return 'toggle';
if (typeof defaultValue === 'number') return 'number';
return 'text';
}; };
// 获取选择框的选项列表
const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => { const handleConfigInput = async (
return extensionConfigMeta[extensionId]?.[configKey]?.options || []; extensionId: ExtensionID,
configKey: string,
defaultValue: any,
event: Event
) => {
const target = event.target as HTMLInputElement | null;
if (!target) return;
const rawValue = target.value;
const trimmedValue = rawValue.trim();
if (!trimmedValue.length) {
await updateExtensionConfig(extensionId, configKey, undefined);
return;
}
try {
const parsedValue = JSON.parse(trimmedValue);
await updateExtensionConfig(extensionId, configKey, parsedValue);
} catch (_error) {
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue);
target.value = formatConfigValue(fallbackValue);
}
}; };
</script> </script>
<template> <template>
@@ -204,58 +208,28 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
</button> </button>
</div> </div>
<div <div class="config-table-wrapper">
<table class="config-table">
<tbody>
<tr
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)" v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
:key="configKey" :key="configKey"
class="config-item"
> >
<SettingItem <th scope="row" class="config-table-key">
:title="configKey" {{ configKey }}
> </th>
<!-- 布尔值切换开关 --> <td class="config-table-value">
<ToggleSwitch
v-if="getConfigItemType(extension.id, configKey, configValue) === 'toggle'"
:model-value="extension.config[configKey] ?? configValue"
@update:model-value="updateExtensionConfig(extension.id, configKey, $event)"
/>
<!-- 数字输入框 -->
<input <input
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'number'" class="config-value-input"
type="number"
class="config-input"
:value="extension.config[configKey] ?? configValue"
:min="configKey === 'opacity' ? 0 : undefined"
:max="configKey === 'opacity' ? 1 : undefined"
:step="configKey === 'opacity' ? 0.1 : 1"
@input="updateExtensionConfig(extension.id, configKey, parseFloat(($event.target as HTMLInputElement).value))"
/>
<!-- 选择框 -->
<select
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'select'"
class="config-select"
:value="extension.config[configKey] ?? configValue"
@change="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLSelectElement).value)"
>
<option
v-for="option in getSelectOptions(extension.id, configKey)"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<!-- 文本输入框 -->
<input
v-else
type="text" type="text"
class="config-input" :value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
:value="extension.config[configKey] ?? configValue" @change="handleConfigInput(extension.id, configKey, configValue, $event)"
@input="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLInputElement).value)" @keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
/> />
</SettingItem> </td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
@@ -361,37 +335,65 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
} }
} }
.config-item { .config-table-wrapper {
&:not(:last-child) { border: 1px solid var(--settings-input-border);
margin-bottom: 12px; border-radius: 6px;
} margin-top: 8px;
overflow: hidden;
/* 配置项标题和描述字体大小 */ background-color: var(--settings-panel, var(--settings-input-bg));
:deep(.setting-item-title) {
font-size: 12px;
}
:deep(.setting-item-description) {
font-size: 11px;
}
} }
.config-input, .config-select { .config-table {
min-width: 120px; width: 100%;
padding: 4px 8px; border-collapse: collapse;
border: 1px solid var(--settings-input-border); font-size: 12px;
border-radius: 3px; }
background-color: var(--settings-input-bg);
color: var(--settings-text);
font-size: 11px;
&:focus { .config-table tr + tr {
border-top: 1px solid var(--settings-input-border);
}
.config-table th,
.config-table td {
padding: 10px 12px;
vertical-align: middle;
}
.config-table-key {
width: 36%;
text-align: left;
font-weight: 600;
color: var(--settings-text-secondary);
border-right: 1px solid var(--settings-input-border);
background-color: var(--settings-input-bg);
}
.config-table-value {
padding: 6px;
}
.config-value-input {
width: 100%;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--settings-text);
font-size: 12px;
line-height: 1.4;
box-sizing: border-box;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.config-value-input:hover {
border-color: var(--settings-hover-border, var(--settings-input-border));
background-color: var(--settings-hover);
}
.config-value-input:focus {
outline: none; outline: none;
border-color: var(--settings-accent); border-color: var(--settings-accent);
} background-color: var(--settings-input-bg);
}
.config-select {
cursor: pointer;
} }
</style> </style>

2
go.mod
View File

@@ -77,7 +77,7 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect github.com/xanzy/go-gitlab v0.115.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.44.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/image v0.33.0 // indirect golang.org/x/image v0.33.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect

4
go.sum
View File

@@ -176,8 +176,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=

View File

@@ -1,4 +1,4 @@
package services package helper
import ( import (
"strconv" "strconv"

View File

@@ -14,67 +14,21 @@ const (
ThemeTypeLight ThemeType = "light" ThemeTypeLight ThemeType = "light"
) )
// ThemeColorConfig 主题颜色配置(与前端 ThemeColors 接口保持一致) // ThemeColorConfig 使用与前端 ThemeColors 相同的结构,存储任意主题键值
type ThemeColorConfig struct { type ThemeColorConfig map[string]interface{}
// 主题基本信息
Name string `json:"name"` // 主题名称
Dark bool `json:"dark"` // 是否为深色主题
// 基础色调
Background string `json:"background"` // 主背景色
BackgroundSecondary string `json:"backgroundSecondary"` // 次要背景色(用于代码块交替背景)
Surface string `json:"surface"` // 面板背景
DropdownBackground string `json:"dropdownBackground"` // 下拉菜单背景
DropdownBorder string `json:"dropdownBorder"` // 下拉菜单边框
// 文本颜色
Foreground string `json:"foreground"` // 主文本色
ForegroundSecondary string `json:"foregroundSecondary"` // 次要文本色
Comment string `json:"comment"` // 注释色
// 语法高亮色 - 核心
Keyword string `json:"keyword"` // 关键字
String string `json:"string"` // 字符串
Function string `json:"function"` // 函数名
Number string `json:"number"` // 数字
Operator string `json:"operator"` // 操作符
Variable string `json:"variable"` // 变量
Type string `json:"type"` // 类型
// 语法高亮色 - 扩展
Constant string `json:"constant"` // 常量
Storage string `json:"storage"` // 存储类型(如 static, const
Parameter string `json:"parameter"` // 参数
Class string `json:"class"` // 类名
Heading string `json:"heading"` // 标题Markdown等
Invalid string `json:"invalid"` // 无效内容/错误
Regexp string `json:"regexp"` // 正则表达式
// 界面元素
Cursor string `json:"cursor"` // 光标
Selection string `json:"selection"` // 选中背景
SelectionBlur string `json:"selectionBlur"` // 失焦选中背景
ActiveLine string `json:"activeLine"` // 当前行高亮
LineNumber string `json:"lineNumber"` // 行号
ActiveLineNumber string `json:"activeLineNumber"` // 活动行号颜色
// 边框和分割线
BorderColor string `json:"borderColor"` // 边框色
BorderLight string `json:"borderLight"` // 浅色边框
// 搜索和匹配
SearchMatch string `json:"searchMatch"` // 搜索匹配
MatchingBracket string `json:"matchingBracket"` // 匹配括号
}
// Value 实现 driver.Valuer 接口,用于将 ThemeColorConfig 存储到数据库 // Value 实现 driver.Valuer 接口,用于将 ThemeColorConfig 存储到数据库
func (tc ThemeColorConfig) Value() (driver.Value, error) { func (tc ThemeColorConfig) Value() (driver.Value, error) {
if tc == nil {
return json.Marshal(map[string]interface{}{})
}
return json.Marshal(tc) return json.Marshal(tc)
} }
// Scan 实现 sql.Scanner 接口,用于从数据库读取 ThemeColorConfig // Scan 实现 sql.Scanner 接口,用于从数据库读取 ThemeColorConfig
func (tc *ThemeColorConfig) Scan(value interface{}) error { func (tc *ThemeColorConfig) Scan(value interface{}) error {
if value == nil { if value == nil {
*tc = ThemeColorConfig{}
return nil return nil
} }
@@ -88,7 +42,13 @@ func (tc *ThemeColorConfig) Scan(value interface{}) error {
return fmt.Errorf("cannot scan %T into ThemeColorConfig", value) return fmt.Errorf("cannot scan %T into ThemeColorConfig", value)
} }
return json.Unmarshal(bytes, tc) var data map[string]interface{}
if err := json.Unmarshal(bytes, &data); err != nil {
return err
}
*tc = data
return nil
} }
// Theme 主题数据库模型 // Theme 主题数据库模型
@@ -101,679 +61,3 @@ type Theme struct {
CreatedAt string `db:"created_at" json:"createdAt"` CreatedAt string `db:"created_at" json:"createdAt"`
UpdatedAt string `db:"updated_at" json:"updatedAt"` UpdatedAt string `db:"updated_at" json:"updatedAt"`
} }
// NewDefaultDarkTheme 创建默认深色主题配置(与前端 defaultDarkColors 完全一致)
func NewDefaultDarkTheme() *ThemeColorConfig {
return &ThemeColorConfig{
// 主题信息
Name: "default-dark",
Dark: true,
// 基础色调
Background: "#252B37",
BackgroundSecondary: "#213644",
Surface: "#474747",
DropdownBackground: "#252B37",
DropdownBorder: "#ffffff19",
// 文本颜色
Foreground: "#9BB586",
ForegroundSecondary: "#9c9c9c",
Comment: "#6272a4",
// 语法高亮色 - 核心
Keyword: "#ff79c6",
String: "#f1fa8c",
Function: "#50fa7b",
Number: "#bd93f9",
Operator: "#ff79c6",
Variable: "#8fbcbb",
Type: "#8be9fd",
// 语法高亮色 - 扩展
Constant: "#bd93f9",
Storage: "#ff79c6",
Parameter: "#8fbcbb",
Class: "#8be9fd",
Heading: "#ff79c6",
Invalid: "#d30102",
Regexp: "#f1fa8c",
// 界面元素
Cursor: "#ffffff",
Selection: "#0865a9",
SelectionBlur: "#225377",
ActiveLine: "#ffffff0a",
LineNumber: "#ffffff26",
ActiveLineNumber: "#ffffff99",
// 边框和分割线
BorderColor: "#1e222a",
BorderLight: "#ffffff19",
// 搜索和匹配
SearchMatch: "#8fbcbb",
MatchingBracket: "#ffffff19",
}
}
// NewDefaultLightTheme 创建默认浅色主题配置(与前端 defaultLightColors 完全一致)
func NewDefaultLightTheme() *ThemeColorConfig {
return &ThemeColorConfig{
// 主题信息
Name: "default-light",
Dark: false,
// 基础色调
Background: "#ffffff",
BackgroundSecondary: "#f1faf1",
Surface: "#f5f5f5",
DropdownBackground: "#ffffff",
DropdownBorder: "#e1e4e8",
// 文本颜色
Foreground: "#444d56",
ForegroundSecondary: "#6a737d",
Comment: "#6a737d",
// 语法高亮色 - 核心
Keyword: "#d73a49",
String: "#032f62",
Function: "#005cc5",
Number: "#005cc5",
Operator: "#d73a49",
Variable: "#24292e",
Type: "#6f42c1",
// 语法高亮色 - 扩展
Constant: "#005cc5",
Storage: "#d73a49",
Parameter: "#24292e",
Class: "#6f42c1",
Heading: "#d73a49",
Invalid: "#cb2431",
Regexp: "#032f62",
// 界面元素
Cursor: "#000000",
Selection: "#77baff",
SelectionBlur: "#b2c2ca",
ActiveLine: "#0000000a",
LineNumber: "#00000040",
ActiveLineNumber: "#000000aa",
// 边框和分割线
BorderColor: "#dfdfdf",
BorderLight: "#0000000c",
// 搜索和匹配
SearchMatch: "#005cc5",
MatchingBracket: "#00000019",
}
}
// NewDraculaTheme 创建 Dracula 深色主题配置
func NewDraculaTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "dracula",
Dark: true,
Background: "#282A36",
BackgroundSecondary: "#323543FF",
Surface: "#282A36",
DropdownBackground: "#282A36",
DropdownBorder: "#191A21",
Foreground: "#F8F8F2",
ForegroundSecondary: "#F8F8F2",
Comment: "#6272A4",
Keyword: "#FF79C6",
String: "#F1FA8C",
Function: "#50FA7B",
Number: "#BD93F9",
Operator: "#FF79C6",
Variable: "#F8F8F2",
Type: "#8BE9FD",
Constant: "#BD93F9",
Storage: "#FF79C6",
Parameter: "#F8F8F2",
Class: "#8BE9FD",
Heading: "#BD93F9",
Invalid: "#FF5555",
Regexp: "#F1FA8C",
Cursor: "#F8F8F2",
Selection: "#44475A",
SelectionBlur: "#44475A",
ActiveLine: "#53576c22",
LineNumber: "#6272A4",
ActiveLineNumber: "#F8F8F2",
BorderColor: "#191A21",
BorderLight: "#F8F8F219",
SearchMatch: "#50FA7B",
MatchingBracket: "#44475A",
}
}
// NewAuraTheme 创建 Aura 深色主题配置
func NewAuraTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "aura",
Dark: true,
Background: "#21202e",
BackgroundSecondary: "#2B2A3BFF",
Surface: "#21202e",
DropdownBackground: "#21202e",
DropdownBorder: "#3b334b",
Foreground: "#edecee",
ForegroundSecondary: "#edecee",
Comment: "#6d6d6d",
Keyword: "#a277ff",
String: "#61ffca",
Function: "#ffca85",
Number: "#61ffca",
Operator: "#a277ff",
Variable: "#edecee",
Type: "#82e2ff",
Constant: "#61ffca",
Storage: "#a277ff",
Parameter: "#edecee",
Class: "#82e2ff",
Heading: "#a277ff",
Invalid: "#ff6767",
Regexp: "#61ffca",
Cursor: "#a277ff",
Selection: "#3d375e7f",
SelectionBlur: "#3d375e7f",
ActiveLine: "#4d4b6622",
LineNumber: "#a394f033",
ActiveLineNumber: "#cdccce",
BorderColor: "#3b334b",
BorderLight: "#edecee19",
SearchMatch: "#61ffca",
MatchingBracket: "#a394f033",
}
}
// NewGitHubDarkTheme 创建 GitHub Dark 主题配置
func NewGitHubDarkTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "github-dark",
Dark: true,
Background: "#24292e",
BackgroundSecondary: "#2E343BFF",
Surface: "#24292e",
DropdownBackground: "#24292e",
DropdownBorder: "#1b1f23",
Foreground: "#d1d5da",
ForegroundSecondary: "#d1d5da",
Comment: "#6a737d",
Keyword: "#f97583",
String: "#9ecbff",
Function: "#79b8ff",
Number: "#79b8ff",
Operator: "#f97583",
Variable: "#ffab70",
Type: "#79b8ff",
Constant: "#79b8ff",
Storage: "#f97583",
Parameter: "#e1e4e8",
Class: "#b392f0",
Heading: "#79b8ff",
Invalid: "#f97583",
Regexp: "#9ecbff",
Cursor: "#c8e1ff",
Selection: "#3392FF44",
SelectionBlur: "#3392FF44",
ActiveLine: "#4d566022",
LineNumber: "#444d56",
ActiveLineNumber: "#e1e4e8",
BorderColor: "#1b1f23",
BorderLight: "#d1d5da19",
SearchMatch: "#79b8ff",
MatchingBracket: "#17E5E650",
}
}
// NewMaterialDarkTheme 创建 Material Dark 主题配置
func NewMaterialDarkTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "material-dark",
Dark: true,
Background: "#263238",
BackgroundSecondary: "#2D3E46FF",
Surface: "#263238",
DropdownBackground: "#263238",
DropdownBorder: "#FFFFFF10",
Foreground: "#EEFFFF",
ForegroundSecondary: "#EEFFFF",
Comment: "#546E7A",
Keyword: "#C792EA",
String: "#C3E88D",
Function: "#82AAFF",
Number: "#F78C6C",
Operator: "#C792EA",
Variable: "#EEFFFF",
Type: "#B2CCD6",
Constant: "#F78C6C",
Storage: "#C792EA",
Parameter: "#EEFFFF",
Class: "#FFCB6B",
Heading: "#C3E88D",
Invalid: "#FF5370",
Regexp: "#89DDFF",
Cursor: "#FFCC00",
Selection: "#80CBC420",
SelectionBlur: "#80CBC420",
ActiveLine: "#4c616c22",
LineNumber: "#37474F",
ActiveLineNumber: "#607a86",
BorderColor: "#FFFFFF10",
BorderLight: "#EEFFFF19",
SearchMatch: "#82AAFF",
MatchingBracket: "#263238",
}
}
// NewOneDarkTheme 创建 One Dark 主题配置
func NewOneDarkTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "one-dark",
Dark: true,
Background: "#282c34",
BackgroundSecondary: "#313949FF",
Surface: "#353a42",
DropdownBackground: "#21252b",
DropdownBorder: "#7d8799",
Foreground: "#abb2bf",
ForegroundSecondary: "#7d8799",
Comment: "#7d8799",
Keyword: "#c678dd",
String: "#98c379",
Function: "#61afef",
Number: "#e5c07b",
Operator: "#56b6c2",
Variable: "#e06c75",
Type: "#e5c07b",
Constant: "#d19a66",
Storage: "#c678dd",
Parameter: "#e06c75",
Class: "#e5c07b",
Heading: "#e06c75",
Invalid: "#ffffff",
Regexp: "#56b6c2",
Cursor: "#528bff",
Selection: "#3E4451",
SelectionBlur: "#3E4451",
ActiveLine: "#6699ff0b",
LineNumber: "#7d8799",
ActiveLineNumber: "#abb2bf",
BorderColor: "#21252b",
BorderLight: "#abb2bf19",
SearchMatch: "#61afef",
MatchingBracket: "#bad0f847",
}
}
// NewSolarizedDarkTheme 创建 Solarized Dark 主题配置
func NewSolarizedDarkTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "solarized-dark",
Dark: true,
Background: "#002B36",
BackgroundSecondary: "#003643FF",
Surface: "#002B36",
DropdownBackground: "#002B36",
DropdownBorder: "#2AA19899",
Foreground: "#93A1A1",
ForegroundSecondary: "#93A1A1",
Comment: "#586E75",
Keyword: "#859900",
String: "#2AA198",
Function: "#268BD2",
Number: "#D33682",
Operator: "#859900",
Variable: "#268BD2",
Type: "#CB4B16",
Constant: "#CB4B16",
Storage: "#93A1A1",
Parameter: "#268BD2",
Class: "#CB4B16",
Heading: "#268BD2",
Invalid: "#DC322F",
Regexp: "#DC322F",
Cursor: "#D30102",
Selection: "#274642",
SelectionBlur: "#274642",
ActiveLine: "#005b7022",
LineNumber: "#93A1A1",
ActiveLineNumber: "#949494",
BorderColor: "#073642",
BorderLight: "#93A1A119",
SearchMatch: "#2AA198",
MatchingBracket: "#073642",
}
}
// NewTokyoNightTheme 创建 Tokyo Night 主题配置
func NewTokyoNightTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "tokyo-night",
Dark: true,
Background: "#1a1b26",
BackgroundSecondary: "#272839FF",
Surface: "#1a1b26",
DropdownBackground: "#1a1b26",
DropdownBorder: "#787c99",
Foreground: "#787c99",
ForegroundSecondary: "#787c99",
Comment: "#444b6a",
Keyword: "#bb9af7",
String: "#9ece6a",
Function: "#7aa2f7",
Number: "#ff9e64",
Operator: "#bb9af7",
Variable: "#c0caf5",
Type: "#0db9d7",
Constant: "#bb9af7",
Storage: "#bb9af7",
Parameter: "#c0caf5",
Class: "#c0caf5",
Heading: "#89ddff",
Invalid: "#ff5370",
Regexp: "#b4f9f8",
Cursor: "#c0caf5",
Selection: "#515c7e40",
SelectionBlur: "#515c7e40",
ActiveLine: "#43455c22",
LineNumber: "#363b54",
ActiveLineNumber: "#737aa2",
BorderColor: "#16161e",
BorderLight: "#787c9919",
SearchMatch: "#7aa2f7",
MatchingBracket: "#16161e",
}
}
// NewTokyoNightStormTheme 创建 Tokyo Night Storm 主题配置
func NewTokyoNightStormTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "tokyo-night-storm",
Dark: true,
Background: "#24283b",
BackgroundSecondary: "#2B3151FF",
Surface: "#24283b",
DropdownBackground: "#24283b",
DropdownBorder: "#7982a9",
Foreground: "#7982a9",
ForegroundSecondary: "#7982a9",
Comment: "#565f89",
Keyword: "#bb9af7",
String: "#9ece6a",
Function: "#7aa2f7",
Number: "#ff9e64",
Operator: "#bb9af7",
Variable: "#c0caf5",
Type: "#2ac3de",
Constant: "#bb9af7",
Storage: "#bb9af7",
Parameter: "#c0caf5",
Class: "#c0caf5",
Heading: "#89ddff",
Invalid: "#ff5370",
Regexp: "#b4f9f8",
Cursor: "#c0caf5",
Selection: "#6f7bb630",
SelectionBlur: "#6f7bb630",
ActiveLine: "#4d547722",
LineNumber: "#3b4261",
ActiveLineNumber: "#737aa2",
BorderColor: "#1f2335",
BorderLight: "#7982a919",
SearchMatch: "#7aa2f7",
MatchingBracket: "#1f2335",
}
}
// 浅色主题预设配置
// NewGitHubLightTheme 创建 GitHub Light 主题配置
func NewGitHubLightTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "github-light",
Dark: false,
Background: "#fff",
BackgroundSecondary: "#f1faf1",
Surface: "#fff",
DropdownBackground: "#fff",
DropdownBorder: "#e1e4e8",
Foreground: "#444d56",
ForegroundSecondary: "#444d56",
Comment: "#6a737d",
Keyword: "#d73a49",
String: "#032f62",
Function: "#005cc5",
Number: "#005cc5",
Operator: "#d73a49",
Variable: "#e36209",
Type: "#005cc5",
Constant: "#005cc5",
Storage: "#d73a49",
Parameter: "#24292e",
Class: "#6f42c1",
Heading: "#005cc5",
Invalid: "#cb2431",
Regexp: "#032f62",
Cursor: "#044289",
Selection: "#0366d625",
SelectionBlur: "#0366d625",
ActiveLine: "#c6c6c622",
LineNumber: "#1b1f234d",
ActiveLineNumber: "#24292e",
BorderColor: "#e1e4e8",
BorderLight: "#444d5619",
SearchMatch: "#005cc5",
MatchingBracket: "#34d05840",
}
}
// NewMaterialLightTheme 创建 Material Light 主题配置
func NewMaterialLightTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "material-light",
Dark: false,
Background: "#FAFAFA",
BackgroundSecondary: "#f1faf1",
Surface: "#FAFAFA",
DropdownBackground: "#FAFAFA",
DropdownBorder: "#00000010",
Foreground: "#90A4AE",
ForegroundSecondary: "#90A4AE",
Comment: "#90A4AE",
Keyword: "#7C4DFF",
String: "#91B859",
Function: "#6182B8",
Number: "#F76D47",
Operator: "#7C4DFF",
Variable: "#90A4AE",
Type: "#8796B0",
Constant: "#F76D47",
Storage: "#7C4DFF",
Parameter: "#90A4AE",
Class: "#FFB62C",
Heading: "#91B859",
Invalid: "#E53935",
Regexp: "#39ADB5",
Cursor: "#272727",
Selection: "#80CBC440",
SelectionBlur: "#80CBC440",
ActiveLine: "#c2c2c222",
LineNumber: "#CFD8DC",
ActiveLineNumber: "#7E939E",
BorderColor: "#00000010",
BorderLight: "#90A4AE19",
SearchMatch: "#6182B8",
MatchingBracket: "#FAFAFA",
}
}
// NewSolarizedLightTheme 创建 Solarized Light 主题配置
func NewSolarizedLightTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "solarized-light",
Dark: false,
Background: "#FDF6E3",
BackgroundSecondary: "#FFEEBCD4",
Surface: "#FDF6E3",
DropdownBackground: "#FDF6E3",
DropdownBorder: "#D3AF86",
Foreground: "#586E75",
ForegroundSecondary: "#586E75",
Comment: "#93A1A1",
Keyword: "#859900",
String: "#2AA198",
Function: "#268BD2",
Number: "#D33682",
Operator: "#859900",
Variable: "#268BD2",
Type: "#CB4B16",
Constant: "#CB4B16",
Storage: "#586E75",
Parameter: "#268BD2",
Class: "#CB4B16",
Heading: "#268BD2",
Invalid: "#DC322F",
Regexp: "#DC322F",
Cursor: "#657B83",
Selection: "#EEE8D5",
SelectionBlur: "#EEE8D5",
ActiveLine: "#d5bd5c22",
LineNumber: "#586E75",
ActiveLineNumber: "#567983",
BorderColor: "#EEE8D5",
BorderLight: "#586E7519",
SearchMatch: "#268BD2",
MatchingBracket: "#EEE8D5",
}
}
// NewTokyoNightDayTheme 创建 Tokyo Night Day 主题配置
func NewTokyoNightDayTheme() *ThemeColorConfig {
return &ThemeColorConfig{
Name: "tokyo-night-day",
Dark: false,
Background: "#e1e2e7",
BackgroundSecondary: "#D2D8EFFF",
Surface: "#e1e2e7",
DropdownBackground: "#e1e2e7",
DropdownBorder: "#6a6f8e",
Foreground: "#6a6f8e",
ForegroundSecondary: "#6a6f8e",
Comment: "#9da3c2",
Keyword: "#9854f1",
String: "#587539",
Function: "#2e7de9",
Number: "#b15c00",
Operator: "#9854f1",
Variable: "#3760bf",
Type: "#07879d",
Constant: "#9854f1",
Storage: "#9854f1",
Parameter: "#3760bf",
Class: "#3760bf",
Heading: "#006a83",
Invalid: "#ff3e64",
Regexp: "#2e5857",
Cursor: "#3760bf",
Selection: "#8591b840",
SelectionBlur: "#8591b840",
ActiveLine: "#a7aaba22",
LineNumber: "#b3b6cd",
ActiveLineNumber: "#68709a",
BorderColor: "#e9e9ec",
BorderLight: "#6a6f8e19",
SearchMatch: "#2e7de9",
MatchingBracket: "#e9e9ec",
}
}

View File

@@ -3,12 +3,13 @@ package services
import ( import (
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
"voidraft/internal/common/helper"
) )
// DialogService 对话框服务,处理文件选择等对话框操作 // DialogService 对话框服务,处理文件选择等对话框操作
type DialogService struct { type DialogService struct {
logger *log.LogService logger *log.LogService
windowHelper *WindowHelper windowHelper *helper.WindowHelper
} }
// NewDialogService 创建新的对话框服务实例 // NewDialogService 创建新的对话框服务实例
@@ -19,7 +20,7 @@ func NewDialogService(logger *log.LogService) *DialogService {
return &DialogService{ return &DialogService{
logger: logger, logger: logger,
windowHelper: NewWindowHelper(), windowHelper: helper.NewWindowHelper(),
} }
} }

View File

@@ -7,6 +7,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"voidraft/internal/common/helper"
"voidraft/internal/common/hotkey" "voidraft/internal/common/hotkey"
"voidraft/internal/models" "voidraft/internal/models"
@@ -18,7 +19,7 @@ import (
type HotkeyService struct { type HotkeyService struct {
logger *log.LogService logger *log.LogService
configService *ConfigService configService *ConfigService
windowHelper *WindowHelper windowHelper *helper.WindowHelper
mu sync.RWMutex mu sync.RWMutex
currentHotkey *models.HotkeyCombo currentHotkey *models.HotkeyCombo
@@ -45,7 +46,7 @@ func NewHotkeyService(configService *ConfigService, logger *log.LogService) *Hot
return &HotkeyService{ return &HotkeyService{
logger: logger, logger: logger,
configService: configService, configService: configService,
windowHelper: NewWindowHelper(), windowHelper: helper.NewWindowHelper(),
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
} }

View File

@@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"strings"
"time" "time"
"voidraft/internal/models" "voidraft/internal/models"
@@ -31,12 +32,10 @@ func NewThemeService(databaseService *DatabaseService, logger *log.LogService) *
} }
} }
// ServiceStartup 服务启动时初始化 // ServiceStartup 服务启动
func (ts *ThemeService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { func (ts *ThemeService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
ts.ctx = ctx ts.ctx = ctx
return nil
// 初始化默认主题
return ts.initializeDefaultThemes()
} }
// getDB 获取数据库连接 // getDB 获取数据库连接
@@ -44,141 +43,27 @@ func (ts *ThemeService) getDB() *sql.DB {
return ts.databaseService.db return ts.databaseService.db
} }
// initializeDefaultThemes 初始化所有预设主题 // GetThemeByName 通过名称获取主题覆盖,若不存在则返回 nil
func (ts *ThemeService) initializeDefaultThemes() error { func (ts *ThemeService) GetThemeByName(name string) (*models.Theme, error) {
db := ts.getDB() db := ts.getDB()
if db == nil { if db == nil {
return fmt.Errorf("database not available") return nil, fmt.Errorf("database not available")
} }
// 获取所有已存在的主题名称 trimmed := strings.TrimSpace(name)
existingThemes := make(map[string]bool) if trimmed == "" {
rows, err := db.Query("SELECT name FROM themes") return nil, fmt.Errorf("theme name cannot be empty")
if err != nil {
return fmt.Errorf("failed to query existing themes: %w", err)
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return fmt.Errorf("failed to scan theme name: %w", err)
}
existingThemes[name] = true
} }
// 定义所有预设主题配置 query := `
now := time.Now().Format("2006-01-02 15:04:05")
presetThemes := []struct {
config *models.ThemeColorConfig
themeType models.ThemeType
isDefault bool
}{
// 默认主题
{models.NewDefaultDarkTheme(), models.ThemeTypeDark, true},
{models.NewDefaultLightTheme(), models.ThemeTypeLight, true},
// 深色主题预设
{models.NewDraculaTheme(), models.ThemeTypeDark, false},
{models.NewAuraTheme(), models.ThemeTypeDark, false},
{models.NewGitHubDarkTheme(), models.ThemeTypeDark, false},
{models.NewMaterialDarkTheme(), models.ThemeTypeDark, false},
{models.NewOneDarkTheme(), models.ThemeTypeDark, false},
{models.NewSolarizedDarkTheme(), models.ThemeTypeDark, false},
{models.NewTokyoNightTheme(), models.ThemeTypeDark, false},
{models.NewTokyoNightStormTheme(), models.ThemeTypeDark, false},
// 浅色主题预设
{models.NewGitHubLightTheme(), models.ThemeTypeLight, false},
{models.NewMaterialLightTheme(), models.ThemeTypeLight, false},
{models.NewSolarizedLightTheme(), models.ThemeTypeLight, false},
{models.NewTokyoNightDayTheme(), models.ThemeTypeLight, false},
}
// 筛选出需要创建的主题
var themesToCreate []*models.Theme
for _, preset := range presetThemes {
if !existingThemes[preset.config.Name] {
themesToCreate = append(themesToCreate, &models.Theme{
Name: preset.config.Name,
Type: preset.themeType,
Colors: *preset.config,
IsDefault: preset.isDefault,
CreatedAt: now,
UpdatedAt: now,
})
}
}
if len(themesToCreate) == 0 {
return nil
}
// 批量插入主题
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
stmt, err := tx.Prepare(`
INSERT INTO themes (name, type, colors, is_default, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`)
if err != nil {
return fmt.Errorf("failed to prepare statement: %w", err)
}
defer stmt.Close()
for _, theme := range themesToCreate {
_, err := stmt.Exec(
theme.Name,
theme.Type,
theme.Colors,
theme.IsDefault,
theme.CreatedAt,
theme.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to insert theme %s: %w", theme.Name, err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
// GetThemeByID 根据ID或名称获取主题
// 如果 id > 0按ID查询如果 id = 0按名称查询
func (ts *ThemeService) GetThemeByIdOrName(id int, name ...string) (*models.Theme, error) {
var query string
var args []interface{}
if id > 0 {
query = `
SELECT id, name, type, colors, is_default, created_at, updated_at
FROM themes
WHERE id = ?
LIMIT 1
`
args = []interface{}{id}
} else if len(name) > 0 && name[0] != "" {
query = `
SELECT id, name, type, colors, is_default, created_at, updated_at SELECT id, name, type, colors, is_default, created_at, updated_at
FROM themes FROM themes
WHERE name = ? WHERE name = ?
LIMIT 1 LIMIT 1
` `
args = []interface{}{name[0]}
} else {
return nil, fmt.Errorf("either id or name must be provided")
}
theme := &models.Theme{} theme := &models.Theme{}
db := ts.getDB() err := db.QueryRow(query, trimmed).Scan(
err := db.QueryRow(query, args...).Scan(
&theme.ID, &theme.ID,
&theme.Name, &theme.Name,
&theme.Type, &theme.Type,
@@ -190,133 +75,89 @@ func (ts *ThemeService) GetThemeByIdOrName(id int, name ...string) (*models.Them
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
if id > 0 { return nil, nil
return nil, fmt.Errorf("theme not found with id: %d", id)
} }
return nil, fmt.Errorf("theme not found with name: %s", name[0]) return nil, fmt.Errorf("failed to query theme: %w", err)
}
return nil, fmt.Errorf("failed to get theme: %w", err)
} }
return theme, nil return theme, nil
} }
// UpdateTheme 更新主题 // UpdateTheme 保存或更新主题覆盖
func (ts *ThemeService) UpdateTheme(id int, colors models.ThemeColorConfig) error { func (ts *ThemeService) UpdateTheme(name string, colors models.ThemeColorConfig) error {
query := `
UPDATE themes
SET colors = ?, updated_at = ?
WHERE id = ?
`
db := ts.getDB() db := ts.getDB()
result, err := db.Exec(query, colors, time.Now().Format("2006-01-02 15:04:05"), id) if db == nil {
return fmt.Errorf("database not available")
}
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return fmt.Errorf("theme name cannot be empty")
}
if colors == nil {
colors = models.ThemeColorConfig{}
}
colors["themeName"] = trimmed
themeType := models.ThemeTypeDark
if raw, ok := colors["dark"].(bool); ok && !raw {
themeType = models.ThemeTypeLight
}
now := time.Now().Format("2006-01-02 15:04:05")
existing, err := ts.GetThemeByName(trimmed)
if err != nil {
return err
}
if existing == nil {
_, err = db.Exec(
`INSERT INTO themes (name, type, colors, is_default, created_at, updated_at) VALUES (?, ?, ?, 0, ?, ?)`,
trimmed,
themeType,
colors,
now,
now,
)
if err != nil {
return fmt.Errorf("failed to insert theme: %w", err)
}
return nil
}
_, err = db.Exec(
`UPDATE themes SET type = ?, colors = ?, updated_at = ? WHERE name = ?`,
themeType,
colors,
now,
trimmed,
)
if err != nil { if err != nil {
return fmt.Errorf("failed to update theme: %w", err) return fmt.Errorf("failed to update theme: %w", err)
} }
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("theme not found with id: %d", id)
}
return nil return nil
} }
// ResetTheme 重置主题为预设配置 // ResetTheme 删除指定主题的覆盖配置
func (ts *ThemeService) ResetTheme(id int, name ...string) error { func (ts *ThemeService) ResetTheme(name string) error {
// 先获取主题信息
theme, err := ts.GetThemeByIdOrName(id, name...)
if err != nil {
return err
}
// 根据主题名称获取预设配置
var presetConfig *models.ThemeColorConfig
switch theme.Name {
// 默认主题
case "default-dark":
presetConfig = models.NewDefaultDarkTheme()
case "default-light":
presetConfig = models.NewDefaultLightTheme()
// 深色主题预设
case "dracula":
presetConfig = models.NewDraculaTheme()
case "aura":
presetConfig = models.NewAuraTheme()
case "github-dark":
presetConfig = models.NewGitHubDarkTheme()
case "material-dark":
presetConfig = models.NewMaterialDarkTheme()
case "one-dark":
presetConfig = models.NewOneDarkTheme()
case "solarized-dark":
presetConfig = models.NewSolarizedDarkTheme()
case "tokyo-night":
presetConfig = models.NewTokyoNightTheme()
case "tokyo-night-storm":
presetConfig = models.NewTokyoNightStormTheme()
// 浅色主题预设
case "github-light":
presetConfig = models.NewGitHubLightTheme()
case "material-light":
presetConfig = models.NewMaterialLightTheme()
case "solarized-light":
presetConfig = models.NewSolarizedLightTheme()
case "tokyo-night-day":
presetConfig = models.NewTokyoNightDayTheme()
default:
return fmt.Errorf("no preset configuration found for theme: %s", theme.Name)
}
return ts.UpdateTheme(id, *presetConfig)
}
// GetAllThemes 获取所有主题
func (ts *ThemeService) GetAllThemes() ([]*models.Theme, error) {
query := `
SELECT id, name, type, colors, is_default, created_at, updated_at
FROM themes
ORDER BY is_default DESC, type DESC, name ASC
`
db := ts.getDB() db := ts.getDB()
rows, err := db.Query(query) if db == nil {
if err != nil { return fmt.Errorf("database not available")
return nil, fmt.Errorf("failed to query themes: %w", err)
}
defer rows.Close()
var themes []*models.Theme
for rows.Next() {
theme := &models.Theme{}
err := rows.Scan(
&theme.ID,
&theme.Name,
&theme.Type,
&theme.Colors,
&theme.IsDefault,
&theme.CreatedAt,
&theme.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan theme: %w", err)
}
themes = append(themes, theme)
} }
if err := rows.Err(); err != nil { trimmed := strings.TrimSpace(name)
return nil, fmt.Errorf("failed to iterate themes: %w", err) if trimmed == "" {
return fmt.Errorf("theme name cannot be empty")
} }
return themes, nil if _, err := db.Exec(`DELETE FROM themes WHERE name = ?`, trimmed); err != nil {
return fmt.Errorf("failed to reset theme: %w", err)
}
return nil
} }
// ServiceShutdown 服务关闭 // ServiceShutdown 服务关闭

View File

@@ -3,13 +3,14 @@ package services
import ( import (
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
"voidraft/internal/common/helper"
) )
// TrayService 系统托盘服务 // TrayService 系统托盘服务
type TrayService struct { type TrayService struct {
logger *log.LogService logger *log.LogService
configService *ConfigService configService *ConfigService
windowHelper *WindowHelper windowHelper *helper.WindowHelper
} }
// NewTrayService 创建新的系统托盘服务实例 // NewTrayService 创建新的系统托盘服务实例
@@ -17,7 +18,7 @@ func NewTrayService(logger *log.LogService, configService *ConfigService) *TrayS
return &TrayService{ return &TrayService{
logger: logger, logger: logger,
configService: configService, configService: configService,
windowHelper: NewWindowHelper(), windowHelper: helper.NewWindowHelper(),
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"math" "math"
"sync" "sync"
"time" "time"
"voidraft/internal/common/helper"
"voidraft/internal/models" "voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
@@ -25,7 +26,7 @@ const (
type WindowSnapService struct { type WindowSnapService struct {
logger *log.LogService logger *log.LogService
configService *ConfigService configService *ConfigService
windowHelper *WindowHelper windowHelper *helper.WindowHelper
mu sync.RWMutex mu sync.RWMutex
// 吸附配置 // 吸附配置
@@ -75,7 +76,7 @@ func NewWindowSnapService(logger *log.LogService, configService *ConfigService)
wss := &WindowSnapService{ wss := &WindowSnapService{
logger: logger, logger: logger,
configService: configService, configService: configService,
windowHelper: NewWindowHelper(), windowHelper: helper.NewWindowHelper(),
snapEnabled: snapEnabled, snapEnabled: snapEnabled,
baseThresholdRatio: 0.025, // 2.5%的主窗口宽度作为基础阈值 baseThresholdRatio: 0.025, // 2.5%的主窗口宽度作为基础阈值
minThreshold: 8, // 最小8像素小屏幕保底 minThreshold: 8, // 最小8像素小屏幕保底

View File

@@ -4,6 +4,7 @@ import (
"sync" "sync"
"testing" "testing"
"time" "time"
"voidraft/internal/common/helper"
"voidraft/internal/models" "voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
@@ -42,7 +43,7 @@ func createTestService() *WindowSnapService {
service := &WindowSnapService{ service := &WindowSnapService{
logger: logger, logger: logger,
configService: nil, // 测试中不需要实际的配置服务 configService: nil, // 测试中不需要实际的配置服务
windowHelper: NewWindowHelper(), windowHelper: helper.NewWindowHelper(),
snapEnabled: true, snapEnabled: true,
baseThresholdRatio: 0.025, baseThresholdRatio: 0.025,
minThreshold: 8, minThreshold: 8,