Compare commits
8 Commits
24f1549730
...
ad24d3a140
| Author | SHA1 | Date | |
|---|---|---|---|
| ad24d3a140 | |||
| 4b0f39d747 | |||
| 096cc1da94 | |||
| 2d3200ad97 | |||
| 4e82e2f6f7 | |||
| 339ed53c2e | |||
| fc7c162e2f | |||
| 5584a46ca2 |
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
852
frontend/package-lock.json
generated
852
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
|||||||
"app:generate": "cd .. && wails3 generate bindings -ts"
|
"app:generate": "cd .. && wails3 generate bindings -ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.19.1",
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
"@codemirror/commands": "^6.10.0",
|
"@codemirror/commands": "^6.10.0",
|
||||||
"@codemirror/lang-angular": "^0.1.4",
|
"@codemirror/lang-angular": "^0.1.4",
|
||||||
"@codemirror/lang-cpp": "^6.0.3",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"@codemirror/lint": "^6.9.2",
|
"@codemirror/lint": "^6.9.2",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.38.6",
|
"@codemirror/view": "^6.38.8",
|
||||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.3",
|
"@lezer/lr": "^1.4.3",
|
||||||
@@ -72,37 +72,37 @@
|
|||||||
"linguist-languages": "^9.1.0",
|
"linguist-languages": "^9.1.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"mermaid": "^11.12.1",
|
"mermaid": "^11.12.1",
|
||||||
"npm": "^11.6.2",
|
"npm": "^11.6.3",
|
||||||
"php-parser": "^3.2.5",
|
"php-parser": "^3.2.5",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"sass": "^1.94.0",
|
"sass": "^1.94.2",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.2.1",
|
||||||
"vue-pick-colors": "^1.8.0",
|
"vue-pick-colors": "^1.8.0",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.8.0",
|
||||||
"@types/node": "^24.9.2",
|
"@types/node": "^24.10.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
"@wailsio/runtime": "latest",
|
"@wailsio/runtime": "latest",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-vue": "^10.5.1",
|
"eslint-plugin-vue": "^10.6.0",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"happy-dom": "^20.0.10",
|
"happy-dom": "^20.0.10",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.46.4",
|
"typescript-eslint": "^8.47.0",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
"vitepress": "^2.0.0-alpha.12",
|
"vitepress": "^2.0.0-alpha.12",
|
||||||
"vitest": "^4.0.8",
|
"vitest": "^4.0.13",
|
||||||
"vue-eslint-parser": "^10.2.0",
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-tsc": "^3.1.3"
|
"vue-tsc": "^3.1.4"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "npm:rolldown-vite@latest"
|
"vite": "npm:rolldown-vite@latest"
|
||||||
|
|||||||
@@ -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';
|
||||||
3
frontend/src/assets/styles/styles.css
Normal file
3
frontend/src/assets/styles/styles.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
body {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,45 +1,3 @@
|
|||||||
/**
|
|
||||||
* 默认翻译配置
|
|
||||||
*/
|
|
||||||
export const DEFAULT_TRANSLATION_CONFIG = {
|
|
||||||
minSelectionLength: 2,
|
|
||||||
maxTranslationLength: 5000,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 翻译相关的错误消息
|
|
||||||
*/
|
|
||||||
export const TRANSLATION_ERRORS = {
|
|
||||||
NO_TEXT: 'no text to translate',
|
|
||||||
TRANSLATION_FAILED: 'translation failed',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 翻译结果接口
|
|
||||||
*/
|
|
||||||
export interface TranslationResult {
|
|
||||||
translatedText: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 语言信息接口
|
|
||||||
*/
|
|
||||||
export interface LanguageInfo {
|
|
||||||
Code: string; // 语言代码
|
|
||||||
Name: string; // 语言名称
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 翻译器扩展配置
|
|
||||||
*/
|
|
||||||
export interface TranslatorConfig {
|
|
||||||
/** 最小选择字符数才显示翻译按钮 */
|
|
||||||
minSelectionLength: number;
|
|
||||||
/** 最大翻译字符数 */
|
|
||||||
maxTranslationLength: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译图标SVG
|
* 翻译图标SVG
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 & {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -30,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 {
|
||||||
@@ -642,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 => {
|
||||||
@@ -783,6 +790,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 配置更新方法
|
// 配置更新方法
|
||||||
applyFontSettings,
|
applyFontSettings,
|
||||||
applyThemeSettings,
|
applyThemeSettings,
|
||||||
|
applyPreviewThemeSettings,
|
||||||
applyTabSettings,
|
applyTabSettings,
|
||||||
applyKeymapSettings,
|
applyKeymapSettings,
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,7 +1,28 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {ref} from 'vue';
|
import {ref} from 'vue';
|
||||||
import {TranslationService} from '@/../bindings/voidraft/internal/services';
|
import {TranslationService} from '@/../bindings/voidraft/internal/services';
|
||||||
import {LanguageInfo, TRANSLATION_ERRORS, TranslationResult} from '@/common/constant/translation';
|
/**
|
||||||
|
* 翻译结果接口
|
||||||
|
*/
|
||||||
|
export interface TranslationResult {
|
||||||
|
translatedText: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语言信息接口
|
||||||
|
*/
|
||||||
|
export interface LanguageInfo {
|
||||||
|
Code: string; // 语言代码
|
||||||
|
Name: string; // 语言名称
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 翻译相关的错误消息
|
||||||
|
*/
|
||||||
|
export const TRANSLATION_ERRORS = {
|
||||||
|
NO_TEXT: 'no text to translate',
|
||||||
|
TRANSLATION_FAILED: 'translation failed',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const useTranslationStore = defineStore('translation', () => {
|
export const useTranslationStore = defineStore('translation', () => {
|
||||||
// 基础状态
|
// 基础状态
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<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 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';
|
||||||
|
import TranslatorDialog from './extensions/translator/TranslatorDialog.vue';
|
||||||
|
import { translatorManager } from './extensions/translator/manager';
|
||||||
|
|
||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
@@ -21,31 +25,35 @@ const enableLoadingAnimation = computed(() => configStore.config.general.enableL
|
|||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
editorStore.clearAllEditors();
|
contextMenuManager.destroy();
|
||||||
|
translatorManager.destroy();
|
||||||
});
|
});
|
||||||
</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" />
|
||||||
|
<!-- 翻译器弹窗 -->
|
||||||
|
<TranslatorDialog :portal-target="editorElement" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -62,6 +70,7 @@ onBeforeUnmount(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,4 +93,3 @@ onBeforeUnmount(() => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
181
frontend/src/views/editor/contextMenu/ContextMenu.vue
Normal file
181
frontend/src/views/editor/contextMenu/ContextMenu.vue
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onUnmounted, 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);
|
||||||
|
// 显示时添加 outside 点击监听
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
} else {
|
||||||
|
// 隐藏时移除监听
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 handleClickOutside(event: MouseEvent) {
|
||||||
|
// 如果点击在菜单内部,不关闭
|
||||||
|
if (menuRef.value?.contains(event.target as Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contextMenuManager.hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport :to="teleportTarget">
|
||||||
|
<template v-if="isVisible">
|
||||||
|
<div
|
||||||
|
ref="menuRef"
|
||||||
|
class="cm-context-menu show"
|
||||||
|
:style="menuStyle"
|
||||||
|
role="menu"
|
||||||
|
@contextmenu.prevent
|
||||||
|
>
|
||||||
|
<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-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>
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
108
frontend/src/views/editor/contextMenu/manager.ts
Normal file
108
frontend/src/views/editor/contextMenu/manager.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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 {
|
||||||
|
const currentState = this.state.value;
|
||||||
|
|
||||||
|
// 如果菜单已经显示,且位置很接近(20px范围内),则只更新内容,避免闪烁
|
||||||
|
if (currentState.visible) {
|
||||||
|
const dx = Math.abs(currentState.position.x - clientX);
|
||||||
|
const dy = Math.abs(currentState.position.y - clientY);
|
||||||
|
const isSamePosition = dx < 20 && dy < 20;
|
||||||
|
|
||||||
|
if (isSamePosition) {
|
||||||
|
// 只更新items和view,保持visible状态和位置
|
||||||
|
this.state.value = {
|
||||||
|
...currentState,
|
||||||
|
items,
|
||||||
|
view
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则正常显示菜单
|
||||||
|
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);
|
||||||
|
}
|
||||||
102
frontend/src/views/editor/contextMenu/menuSchema.ts
Normal file
102
frontend/src/views/editor/contextMenu/menuSchema.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
const newTheme = isDark ? "dark" : "default";
|
watch(() => themeStore.isDarkMode, (isDark) => {
|
||||||
updateMermaidTheme(newTheme);
|
const newTheme = isDark ? "dark" : "default";
|
||||||
this.lastRenderedContent = ""; // 清空缓存,强制重新渲染
|
updateMermaidTheme(newTheme);
|
||||||
});
|
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 = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,481 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, nextTick, onUnmounted, ref, watch} from 'vue';
|
||||||
|
import {translatorManager} from './manager';
|
||||||
|
import {useTranslationStore} from '@/stores/translationStore';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
portalTarget?: HTMLElement | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const state = translatorManager.useState();
|
||||||
|
const translationStore = useTranslationStore();
|
||||||
|
|
||||||
|
const dialogRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const adjustedPosition = ref({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const isVisible = computed(() => state.value.visible);
|
||||||
|
const sourceText = computed(() => state.value.sourceText);
|
||||||
|
const position = computed(() => state.value.position);
|
||||||
|
const teleportTarget = computed<HTMLElement | string>(() => props.portalTarget ?? 'body');
|
||||||
|
|
||||||
|
const sourceLangSelector = ref('');
|
||||||
|
const targetLangSelector = ref('');
|
||||||
|
const translatorSelector = ref('');
|
||||||
|
const translatedText = ref('');
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const dragStart = ref({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// 监听可见性变化
|
||||||
|
watch(isVisible, async (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
adjustedPosition.value = { ...position.value };
|
||||||
|
await nextTick();
|
||||||
|
adjustDialogPosition();
|
||||||
|
await initializeTranslation();
|
||||||
|
await nextTick();
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
isDragging.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogStyle = computed(() => ({
|
||||||
|
left: `${adjustedPosition.value.x}px`,
|
||||||
|
top: `${adjustedPosition.value.y}px`
|
||||||
|
}));
|
||||||
|
|
||||||
|
const availableLanguages = computed(() => {
|
||||||
|
const languageMap = translationStore.translatorLanguages[translatorSelector.value];
|
||||||
|
if (!languageMap) return [];
|
||||||
|
return Object.entries(languageMap).map(([code, info]: [string, any]) => ({
|
||||||
|
code,
|
||||||
|
name: info.Name || info.name || code
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableTranslators = computed(() => translationStore.translators);
|
||||||
|
|
||||||
|
function adjustDialogPosition() {
|
||||||
|
const dialogEl = dialogRef.value;
|
||||||
|
const container = props.portalTarget;
|
||||||
|
if (!dialogEl || !container) return;
|
||||||
|
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const dialogRect = dialogEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
let x = adjustedPosition.value.x;
|
||||||
|
let y = adjustedPosition.value.y;
|
||||||
|
|
||||||
|
// 限制在容器范围内
|
||||||
|
x = Math.max(containerRect.left, Math.min(x, containerRect.right - dialogRect.width - 8));
|
||||||
|
y = Math.max(containerRect.top, Math.min(y, containerRect.bottom - dialogRect.height - 8));
|
||||||
|
|
||||||
|
adjustedPosition.value = { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampPosition(x: number, y: number) {
|
||||||
|
const container = props.portalTarget;
|
||||||
|
const dialogEl = dialogRef.value;
|
||||||
|
if (!container || !dialogEl) return { x, y };
|
||||||
|
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const dialogRect = dialogEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.max(containerRect.left, Math.min(x, containerRect.right - dialogRect.width)),
|
||||||
|
y: Math.max(containerRect.top, Math.min(y, containerRect.bottom - dialogRect.height))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeTranslation() {
|
||||||
|
isLoading.value = true;
|
||||||
|
translatedText.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadTranslators();
|
||||||
|
await translate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize translation:', error);
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTranslators() {
|
||||||
|
const translators = translationStore.translators;
|
||||||
|
if (translators.length > 0) {
|
||||||
|
translatorSelector.value = translators[0];
|
||||||
|
}
|
||||||
|
resetLanguageSelectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLanguageSelectors() {
|
||||||
|
const languageMap = translationStore.translatorLanguages[translatorSelector.value];
|
||||||
|
if (!languageMap) return;
|
||||||
|
|
||||||
|
const languages = Object.keys(languageMap);
|
||||||
|
if (languages.length > 0) {
|
||||||
|
sourceLangSelector.value = languages[0];
|
||||||
|
targetLangSelector.value = languages[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTranslatorChange() {
|
||||||
|
resetLanguageSelectors();
|
||||||
|
translate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function swapLanguages() {
|
||||||
|
const temp = sourceLangSelector.value;
|
||||||
|
sourceLangSelector.value = targetLangSelector.value;
|
||||||
|
targetLangSelector.value = temp;
|
||||||
|
translate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function translate() {
|
||||||
|
const sourceLang = sourceLangSelector.value;
|
||||||
|
const targetLang = targetLangSelector.value;
|
||||||
|
const translatorType = translatorSelector.value;
|
||||||
|
|
||||||
|
if (!sourceLang || !targetLang || !translatorType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
translatedText.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await translationStore.translateText(
|
||||||
|
sourceText.value,
|
||||||
|
sourceLang,
|
||||||
|
targetLang,
|
||||||
|
translatorType
|
||||||
|
);
|
||||||
|
|
||||||
|
translatedText.value = result.translatedText || result.error || '';
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Translation failed:', err);
|
||||||
|
translatedText.value = 'Translation failed';
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrag(e: MouseEvent) {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.closest('select, button')) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const rect = dialogRef.value!.getBoundingClientRect();
|
||||||
|
dragStart.value = {
|
||||||
|
x: e.clientX - rect.left,
|
||||||
|
y: e.clientY - rect.top
|
||||||
|
};
|
||||||
|
|
||||||
|
isDragging.value = true;
|
||||||
|
document.addEventListener('mousemove', onDrag);
|
||||||
|
document.addEventListener('mouseup', endDrag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrag(e: MouseEvent) {
|
||||||
|
adjustedPosition.value = clampPosition(
|
||||||
|
e.clientX - dragStart.value.x,
|
||||||
|
e.clientY - dragStart.value.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDrag(e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
isDragging.value = false;
|
||||||
|
document.removeEventListener('mousemove', onDrag);
|
||||||
|
document.removeEventListener('mouseup', endDrag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(translatedText.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy text:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
if (isDragging.value) return;
|
||||||
|
if (dialogRef.value?.contains(e.target as Node)) return;
|
||||||
|
translatorManager.hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport :to="teleportTarget">
|
||||||
|
<template v-if="isVisible">
|
||||||
|
<div
|
||||||
|
ref="dialogRef"
|
||||||
|
class="cm-translation-tooltip"
|
||||||
|
:class="{ 'cm-translation-dragging': isDragging }"
|
||||||
|
:style="dialogStyle"
|
||||||
|
@mousedown="startDrag"
|
||||||
|
@keydown.esc="translatorManager.hide"
|
||||||
|
@contextmenu.prevent
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="cm-translation-header">
|
||||||
|
<div class="cm-translation-controls">
|
||||||
|
<select
|
||||||
|
v-model="sourceLangSelector"
|
||||||
|
class="cm-translation-select"
|
||||||
|
@change="translate"
|
||||||
|
@mousedown.stop
|
||||||
|
>
|
||||||
|
<option v-for="lang in availableLanguages" :key="lang.code" :value="lang.code">
|
||||||
|
{{ lang.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="cm-translation-swap" @click="swapLanguages" @mousedown.stop title="交换语言">
|
||||||
|
<svg viewBox="0 0 24 24" width="11" height="11">
|
||||||
|
<path fill="currentColor" d="M7.5 21L3 16.5L7.5 12L9 13.5L7 15.5H15V13H17V17.5H7L9 19.5L7.5 21M16.5 3L21 7.5L16.5 12L15 10.5L17 8.5H9V11H7V6.5H17L15 4.5L16.5 3Z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-model="targetLangSelector"
|
||||||
|
class="cm-translation-select"
|
||||||
|
@change="translate"
|
||||||
|
@mousedown.stop
|
||||||
|
>
|
||||||
|
<option v-for="lang in availableLanguages" :key="lang.code" :value="lang.code">
|
||||||
|
{{ lang.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-model="translatorSelector"
|
||||||
|
class="cm-translation-select"
|
||||||
|
@change="handleTranslatorChange"
|
||||||
|
@mousedown.stop
|
||||||
|
>
|
||||||
|
<option v-for="translator in availableTranslators" :key="translator" :value="translator">
|
||||||
|
{{ translator }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cm-translation-scroll-container">
|
||||||
|
<div v-if="isLoading" class="cm-translation-loading">
|
||||||
|
Translation...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="cm-translation-result">
|
||||||
|
<div class="cm-translation-result-wrapper">
|
||||||
|
<button
|
||||||
|
v-if="translatedText"
|
||||||
|
class="cm-translation-copy-btn"
|
||||||
|
@click="copyToClipboard"
|
||||||
|
@mousedown.stop
|
||||||
|
title="复制"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="cm-translation-target">{{ translatedText }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cm-translation-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
background: var(--settings-card-bg, #fff);
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 6px;
|
||||||
|
max-width: 240px;
|
||||||
|
max-height: 180px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--voidraft-font-mono, system-ui, -apple-system, sans-serif), serif;
|
||||||
|
font-size: 10px;
|
||||||
|
user-select: none;
|
||||||
|
cursor: grab;
|
||||||
|
z-index: 10000;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-dragging {
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 10001;
|
||||||
|
cursor: grabbing !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-header {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-select {
|
||||||
|
padding: 2px 3px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.12));
|
||||||
|
background: var(--bg-primary, #f8f8f8);
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 65px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--border-color, rgba(66, 133, 244, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-swap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.12));
|
||||||
|
background: var(--bg-primary, transparent);
|
||||||
|
color: var(--text-muted, #666);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-swap:hover {
|
||||||
|
background: var(--bg-hover, rgba(66, 133, 244, 0.08));
|
||||||
|
border-color: var(--border-color, rgba(66, 133, 244, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-scroll-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-scroll-container::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-scroll-container::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-scroll-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-result-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-copy-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
|
||||||
|
background: var(--bg-primary, rgba(255, 255, 255, 0.9));
|
||||||
|
color: var(--text-muted, #666);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
z-index: 2;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-copy-btn:hover {
|
||||||
|
background: var(--bg-hover, rgba(66, 133, 244, 0.1));
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-copy-btn svg {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-target {
|
||||||
|
padding: 5px;
|
||||||
|
padding-right: 24px;
|
||||||
|
background: var(--bg-primary, rgba(66, 133, 244, 0.03));
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.4;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-loading {
|
||||||
|
padding: 6px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted, #666);
|
||||||
|
font-size: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-translation-loading::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--text-muted, rgba(0, 0, 0, 0.2));
|
||||||
|
border-top-color: var(--text-muted, #666);
|
||||||
|
animation: cm-translation-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cm-translation-spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,355 +1,84 @@
|
|||||||
import { Extension, StateField, StateEffect, StateEffectType } from '@codemirror/state';
|
import { Extension, StateField } from '@codemirror/state';
|
||||||
import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
|
import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
|
||||||
import { createTranslationTooltip } from './tooltip';
|
import { translatorManager } from './manager';
|
||||||
import {
|
import { TRANSLATION_ICON_SVG } from '@/common/constant/translation';
|
||||||
TranslatorConfig,
|
|
||||||
DEFAULT_TRANSLATION_CONFIG,
|
|
||||||
TRANSLATION_ICON_SVG
|
|
||||||
} from '@/common/constant/translation';
|
|
||||||
|
|
||||||
|
function TranslationTooltips(state: any): readonly Tooltip[] {
|
||||||
|
const selection = state.selection.main;
|
||||||
|
if (selection.empty) return [];
|
||||||
|
|
||||||
class TranslatorExtension {
|
const selectedText = state.sliceDoc(selection.from, selection.to);
|
||||||
private config: TranslatorConfig;
|
if (!selectedText.trim()) return [];
|
||||||
private setTranslationTooltip: StateEffectType<Tooltip | null>;
|
|
||||||
private translationTooltipField: StateField<readonly Tooltip[]>;
|
|
||||||
private translationButtonField: StateField<readonly Tooltip[]>;
|
|
||||||
|
|
||||||
constructor(config?: Partial<TranslatorConfig>) {
|
return [{
|
||||||
// 初始化配置
|
pos: selection.to,
|
||||||
this.config = {
|
above: false,
|
||||||
minSelectionLength: DEFAULT_TRANSLATION_CONFIG.minSelectionLength,
|
strictSide: true,
|
||||||
maxTranslationLength: DEFAULT_TRANSLATION_CONFIG.maxTranslationLength,
|
arrow: false,
|
||||||
...config
|
create: (view) => {
|
||||||
};
|
const dom = document.createElement('div');
|
||||||
|
dom.className = 'cm-translator-button';
|
||||||
|
dom.innerHTML = TRANSLATION_ICON_SVG;
|
||||||
|
|
||||||
// 初始化状态效果
|
dom.addEventListener('mousedown', (e) => {
|
||||||
this.setTranslationTooltip = StateEffect.define<Tooltip | null>();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
showTranslatorDialog(view);
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化翻译气泡状态字段
|
return { dom };
|
||||||
this.translationTooltipField = StateField.define<readonly Tooltip[]>({
|
|
||||||
create: () => [],
|
|
||||||
update: (tooltips, tr) => {
|
|
||||||
// 检查是否有特定的状态效果来更新tooltips
|
|
||||||
for (const effect of tr.effects) {
|
|
||||||
if (effect.is(this.setTranslationTooltip)) {
|
|
||||||
return effect.value ? [effect.value] : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果文档或选择变化,隐藏气泡
|
|
||||||
if (tr.docChanged || tr.selection) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return tooltips;
|
|
||||||
},
|
|
||||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化翻译按钮状态字段
|
|
||||||
this.translationButtonField = StateField.define<readonly Tooltip[]>({
|
|
||||||
create: (state) => this.getTranslationButtonTooltips(state),
|
|
||||||
update: (tooltips, tr) => {
|
|
||||||
// 如果文档或选择变化,重新计算tooltip
|
|
||||||
if (tr.docChanged || tr.selection) {
|
|
||||||
return this.getTranslationButtonTooltips(tr.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有翻译气泡显示,如果有则不显示按钮
|
|
||||||
if (tr.state.field(this.translationTooltipField).length > 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return tooltips;
|
|
||||||
},
|
|
||||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据当前选择获取翻译按钮tooltip
|
|
||||||
*/
|
|
||||||
private getTranslationButtonTooltips(state: any): readonly Tooltip[] {
|
|
||||||
// 如果气泡已显示,则不显示按钮
|
|
||||||
if (state.field(this.translationTooltipField).length > 0) return [];
|
|
||||||
|
|
||||||
const selection = state.selection.main;
|
|
||||||
|
|
||||||
// 如果没有选中文本,不显示按钮
|
|
||||||
if (selection.empty) return [];
|
|
||||||
|
|
||||||
// 获取选中的文本
|
|
||||||
const selectedText = state.sliceDoc(selection.from, selection.to);
|
|
||||||
|
|
||||||
// 检查文本是否只包含空格
|
|
||||||
if (!selectedText.trim()) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
}];
|
||||||
// 检查文本长度条件
|
|
||||||
if (selectedText.length < this.config.minSelectionLength ||
|
|
||||||
selectedText.length > this.config.maxTranslationLength) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回翻译按钮tooltip配置
|
|
||||||
return [{
|
|
||||||
pos: selection.to,
|
|
||||||
above: false,
|
|
||||||
strictSide: true,
|
|
||||||
arrow: false,
|
|
||||||
create: (view) => {
|
|
||||||
// 创建按钮DOM
|
|
||||||
const dom = document.createElement('div');
|
|
||||||
dom.className = 'cm-translator-button';
|
|
||||||
dom.innerHTML = TRANSLATION_ICON_SVG;
|
|
||||||
|
|
||||||
// 点击事件
|
|
||||||
dom.addEventListener('mousedown', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// 显示翻译气泡
|
|
||||||
this.showTranslationTooltip(view);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { dom };
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示翻译气泡
|
|
||||||
*/
|
|
||||||
private showTranslationTooltip(view: EditorView) {
|
|
||||||
// 直接从当前选择获取文本
|
|
||||||
const selection = view.state.selection.main;
|
|
||||||
if (selection.empty) return;
|
|
||||||
|
|
||||||
const selectedText = view.state.sliceDoc(selection.from, selection.to);
|
|
||||||
if (!selectedText.trim()) return;
|
|
||||||
|
|
||||||
// 创建翻译气泡
|
|
||||||
const tooltip = createTranslationTooltip(view, selectedText);
|
|
||||||
|
|
||||||
// 更新状态以显示气泡
|
|
||||||
view.dispatch({
|
|
||||||
effects: this.setTranslationTooltip.of(tooltip)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建扩展
|
|
||||||
*/
|
|
||||||
createExtension(): Extension {
|
|
||||||
return [
|
|
||||||
// 翻译按钮tooltip
|
|
||||||
this.translationButtonField,
|
|
||||||
// 翻译气泡tooltip
|
|
||||||
this.translationTooltipField,
|
|
||||||
|
|
||||||
// 添加基础样式
|
|
||||||
EditorView.baseTheme({
|
|
||||||
".cm-translator-button": {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
cursor: "pointer",
|
|
||||||
background: "var(--bg-secondary, transparent)",
|
|
||||||
color: "var(--text-muted, #4285f4)",
|
|
||||||
border: "1px solid var(--border-color, #dadce0)",
|
|
||||||
borderRadius: "3px",
|
|
||||||
padding: "2px",
|
|
||||||
width: "24px",
|
|
||||||
height: "24px",
|
|
||||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
|
||||||
userSelect: "none",
|
|
||||||
"&:hover": {
|
|
||||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 翻译气泡样式
|
|
||||||
".cm-translation-tooltip": {
|
|
||||||
background: "var(--bg-secondary, #fff)",
|
|
||||||
color: "var(--text-primary, #333)",
|
|
||||||
border: "1px solid var(--border-color, #dadce0)",
|
|
||||||
borderRadius: "3px",
|
|
||||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
|
||||||
padding: "8px",
|
|
||||||
maxWidth: "300px",
|
|
||||||
maxHeight: "200px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
overflow: "hidden",
|
|
||||||
fontFamily: "var(--font-family, system-ui, -apple-system, sans-serif)",
|
|
||||||
fontSize: "11px",
|
|
||||||
userSelect: "none",
|
|
||||||
cursor: "grab"
|
|
||||||
},
|
|
||||||
|
|
||||||
// 拖拽状态样式
|
|
||||||
".cm-translation-dragging": {
|
|
||||||
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)",
|
|
||||||
zIndex: "1000",
|
|
||||||
cursor: "grabbing !important"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-header": {
|
|
||||||
marginBottom: "8px",
|
|
||||||
flexShrink: "0"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-controls": {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "4px",
|
|
||||||
flexWrap: "nowrap"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-select": {
|
|
||||||
padding: "2px 4px",
|
|
||||||
borderRadius: "3px",
|
|
||||||
border: "1px solid var(--border-color, #dadce0)",
|
|
||||||
background: "var(--bg-primary, #f5f5f5)",
|
|
||||||
fontSize: "11px",
|
|
||||||
color: "var(--text-primary, #333)",
|
|
||||||
flex: "1",
|
|
||||||
minWidth: "0",
|
|
||||||
maxWidth: "80px"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-swap": {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
|
||||||
borderRadius: "3px",
|
|
||||||
border: "1px solid var(--border-color, #dadce0)",
|
|
||||||
background: "var(--bg-primary, transparent)",
|
|
||||||
color: "var(--text-muted, #666)",
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "0",
|
|
||||||
flexShrink: "0",
|
|
||||||
"&:hover": {
|
|
||||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 滚动容器
|
|
||||||
".cm-translation-scroll-container": {
|
|
||||||
overflowY: "auto",
|
|
||||||
flex: "1",
|
|
||||||
minHeight: "0"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-result": {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-result-header": {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
marginBottom: "4px"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-result-wrapper": {
|
|
||||||
position: "relative",
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-copy-btn": {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
borderRadius: "3px",
|
|
||||||
border: "1px solid var(--border-color, #dadce0)",
|
|
||||||
background: "var(--bg-primary, transparent)",
|
|
||||||
color: "var(--text-muted, #666)",
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "0",
|
|
||||||
position: "absolute",
|
|
||||||
top: "4px",
|
|
||||||
right: "4px",
|
|
||||||
zIndex: "2",
|
|
||||||
opacity: "0.7",
|
|
||||||
"&:hover": {
|
|
||||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))",
|
|
||||||
opacity: "1"
|
|
||||||
},
|
|
||||||
"&.copied": {
|
|
||||||
background: "var(--bg-success, #4caf50)",
|
|
||||||
color: "white",
|
|
||||||
border: "1px solid var(--bg-success, #4caf50)",
|
|
||||||
opacity: "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-target": {
|
|
||||||
padding: "6px",
|
|
||||||
paddingRight: "28px", // 为复制按钮留出空间
|
|
||||||
background: "var(--bg-primary, rgba(66, 133, 244, 0.05))",
|
|
||||||
color: "var(--text-primary, #333)",
|
|
||||||
borderRadius: "3px",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
wordBreak: "break-word"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-notice": {
|
|
||||||
fontSize: "10px",
|
|
||||||
color: "var(--text-muted, #888)",
|
|
||||||
padding: "2px 0",
|
|
||||||
fontStyle: "italic",
|
|
||||||
textAlign: "center",
|
|
||||||
marginBottom: "2px"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-error": {
|
|
||||||
color: "var(--text-danger, #d32f2f)",
|
|
||||||
fontStyle: "italic"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-loading": {
|
|
||||||
padding: "8px",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "var(--text-muted, #666)",
|
|
||||||
fontSize: "11px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: "6px"
|
|
||||||
},
|
|
||||||
|
|
||||||
".cm-translation-loading::before": {
|
|
||||||
content: "''",
|
|
||||||
display: "inline-block",
|
|
||||||
width: "12px",
|
|
||||||
height: "12px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
border: "2px solid var(--text-muted, #666)",
|
|
||||||
borderTopColor: "transparent",
|
|
||||||
animation: "cm-translation-spin 1s linear infinite"
|
|
||||||
},
|
|
||||||
|
|
||||||
"@keyframes cm-translation-spin": {
|
|
||||||
"0%": { transform: "rotate(0deg)" },
|
|
||||||
"100%": { transform: "rotate(360deg)" }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function showTranslatorDialog(view: EditorView) {
|
||||||
* 创建翻译扩展
|
const selection = view.state.selection.main;
|
||||||
*/
|
if (selection.empty) return;
|
||||||
export function createTranslatorExtension(config?: Partial<TranslatorConfig>): Extension {
|
|
||||||
const translatorExtension = new TranslatorExtension(config);
|
const selectedText = view.state.sliceDoc(selection.from, selection.to);
|
||||||
return translatorExtension.createExtension();
|
if (!selectedText.trim()) return;
|
||||||
|
|
||||||
|
const coords = view.coordsAtPos(selection.to);
|
||||||
|
if (!coords) return;
|
||||||
|
|
||||||
|
translatorManager.show(view, coords.left, coords.bottom + 5, selectedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const translationButtonField = StateField.define<readonly Tooltip[]>({
|
||||||
|
create: (state) => TranslationTooltips(state),
|
||||||
|
update: (tooltips, tr) => {
|
||||||
|
if (tr.docChanged || tr.selection) {
|
||||||
|
return TranslationTooltips(tr.state);
|
||||||
|
}
|
||||||
|
return tooltips;
|
||||||
|
},
|
||||||
|
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createTranslatorExtension(): Extension {
|
||||||
|
return [
|
||||||
|
translationButtonField,
|
||||||
|
EditorView.baseTheme({
|
||||||
|
".cm-translator-button": {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
background: "var(--bg-secondary, transparent)",
|
||||||
|
color: "var(--text-muted, #4285f4)",
|
||||||
|
border: "1px solid var(--border-color, #dadce0)",
|
||||||
|
borderRadius: "3px",
|
||||||
|
padding: "2px",
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
||||||
|
userSelect: "none",
|
||||||
|
"&:hover": {
|
||||||
|
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createTranslatorExtension;
|
export default createTranslatorExtension;
|
||||||
66
frontend/src/views/editor/extensions/translator/manager.ts
Normal file
66
frontend/src/views/editor/extensions/translator/manager.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
import { readonly, shallowRef, type ShallowRef } from 'vue';
|
||||||
|
|
||||||
|
interface TranslatorPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TranslatorState {
|
||||||
|
visible: boolean;
|
||||||
|
position: TranslatorPosition;
|
||||||
|
sourceText: string;
|
||||||
|
view: EditorView | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TranslatorManager {
|
||||||
|
private state: ShallowRef<TranslatorState> = shallowRef({
|
||||||
|
visible: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
sourceText: '',
|
||||||
|
view: null
|
||||||
|
});
|
||||||
|
|
||||||
|
useState() {
|
||||||
|
return readonly(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
show(view: EditorView, clientX: number, clientY: number, text: string): void {
|
||||||
|
this.state.value = {
|
||||||
|
visible: true,
|
||||||
|
position: { x: clientX, y: clientY },
|
||||||
|
sourceText: text,
|
||||||
|
view
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(): void {
|
||||||
|
if (!this.state.value.visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = this.state.value.view;
|
||||||
|
this.state.value = {
|
||||||
|
visible: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
sourceText: '',
|
||||||
|
view: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (view) {
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.state.value = {
|
||||||
|
visible: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
sourceText: '',
|
||||||
|
view: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const translatorManager = new TranslatorManager();
|
||||||
|
|
||||||
@@ -1,598 +0,0 @@
|
|||||||
import {EditorView, Tooltip, TooltipView} from '@codemirror/view';
|
|
||||||
import {useTranslationStore} from '@/stores/translationStore';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 翻译气泡弹窗类
|
|
||||||
* 提供文本翻译功能的交互式界面
|
|
||||||
*/
|
|
||||||
export class TranslationTooltip implements TooltipView {
|
|
||||||
// ===== 核心属性 =====
|
|
||||||
dom!: HTMLElement;
|
|
||||||
sourceText: string;
|
|
||||||
translationStore: ReturnType<typeof useTranslationStore>;
|
|
||||||
|
|
||||||
// ===== UI 元素 =====
|
|
||||||
private translatorSelector!: HTMLSelectElement;
|
|
||||||
private sourceLangSelector!: HTMLSelectElement;
|
|
||||||
private targetLangSelector!: HTMLSelectElement;
|
|
||||||
private resultContainer!: HTMLDivElement;
|
|
||||||
private loadingIndicator!: HTMLDivElement;
|
|
||||||
private swapButton!: HTMLButtonElement;
|
|
||||||
|
|
||||||
// ===== 状态管理 =====
|
|
||||||
private translatedText: string = '';
|
|
||||||
private eventListeners: Array<{element: HTMLElement | Document, event: string, handler: EventListener}> = [];
|
|
||||||
|
|
||||||
// ===== 拖拽状态 =====
|
|
||||||
private isDragging: boolean = false;
|
|
||||||
private dragOffset: { x: number; y: number } = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
constructor(_view: EditorView, text: string) {
|
|
||||||
this.sourceText = text;
|
|
||||||
this.translationStore = useTranslationStore();
|
|
||||||
|
|
||||||
this.initializeDOM();
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.initializeTranslation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== DOM 初始化 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化DOM结构
|
|
||||||
*/
|
|
||||||
private initializeDOM(): void {
|
|
||||||
this.dom = this.createElement('div', 'cm-translation-tooltip');
|
|
||||||
// 设置为绝对定位,允许拖拽移动
|
|
||||||
this.dom.style.position = 'absolute';
|
|
||||||
|
|
||||||
const header = this.createHeader();
|
|
||||||
const scrollContainer = this.createScrollContainer();
|
|
||||||
|
|
||||||
this.dom.appendChild(header);
|
|
||||||
this.dom.appendChild(scrollContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建头部控制区域
|
|
||||||
*/
|
|
||||||
private createHeader(): HTMLElement {
|
|
||||||
const header = this.createElement('div', 'cm-translation-header');
|
|
||||||
|
|
||||||
const controlsContainer = this.createElement('div', 'cm-translation-controls');
|
|
||||||
|
|
||||||
// 创建所有控制元素
|
|
||||||
this.sourceLangSelector = this.createSelector('cm-translation-select');
|
|
||||||
this.swapButton = this.createSwapButton();
|
|
||||||
this.targetLangSelector = this.createSelector('cm-translation-select');
|
|
||||||
this.translatorSelector = this.createTranslatorSelector();
|
|
||||||
|
|
||||||
// 添加到控制容器
|
|
||||||
controlsContainer.appendChild(this.sourceLangSelector);
|
|
||||||
controlsContainer.appendChild(this.swapButton);
|
|
||||||
controlsContainer.appendChild(this.targetLangSelector);
|
|
||||||
controlsContainer.appendChild(this.translatorSelector);
|
|
||||||
|
|
||||||
header.appendChild(controlsContainer);
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建滚动容器
|
|
||||||
*/
|
|
||||||
private createScrollContainer(): HTMLElement {
|
|
||||||
const scrollContainer = this.createElement('div', 'cm-translation-scroll-container');
|
|
||||||
|
|
||||||
this.loadingIndicator = this.createElement('div', 'cm-translation-loading') as HTMLDivElement;
|
|
||||||
this.loadingIndicator.textContent = 'Translation...';
|
|
||||||
this.loadingIndicator.style.display = 'none';
|
|
||||||
|
|
||||||
this.resultContainer = this.createElement('div', 'cm-translation-result') as HTMLDivElement;
|
|
||||||
|
|
||||||
scrollContainer.appendChild(this.loadingIndicator);
|
|
||||||
scrollContainer.appendChild(this.resultContainer);
|
|
||||||
|
|
||||||
return scrollContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建选择器元素
|
|
||||||
*/
|
|
||||||
private createSelector(className: string): HTMLSelectElement {
|
|
||||||
const select = this.createElement('select', className) as HTMLSelectElement;
|
|
||||||
return select;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建语言交换按钮
|
|
||||||
*/
|
|
||||||
private createSwapButton(): HTMLButtonElement {
|
|
||||||
const button = this.createElement('button', 'cm-translation-swap') as HTMLButtonElement;
|
|
||||||
button.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M7.5 21L3 16.5L7.5 12L9 13.5L7 15.5H15V13H17V17.5H7L9 19.5L7.5 21M16.5 3L21 7.5L16.5 12L15 10.5L17 8.5H9V11H7V6.5H17L15 4.5L16.5 3Z"/></svg>`;
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建翻译器选择器
|
|
||||||
*/
|
|
||||||
private createTranslatorSelector(): HTMLSelectElement {
|
|
||||||
const select = this.createSelector('cm-translation-select');
|
|
||||||
const tempOption = this.createElement('option') as HTMLOptionElement;
|
|
||||||
tempOption.textContent = 'Loading...';
|
|
||||||
select.appendChild(tempOption);
|
|
||||||
return select;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通用DOM元素创建方法
|
|
||||||
*/
|
|
||||||
private createElement(tag: string, className?: string): HTMLElement {
|
|
||||||
const element = document.createElement(tag);
|
|
||||||
if (className) {
|
|
||||||
element.className = className;
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 事件管理 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置事件监听器
|
|
||||||
*/
|
|
||||||
private setupEventListeners(): void {
|
|
||||||
this.addEventListenerWithCleanup(this.sourceLangSelector, 'change', () => {
|
|
||||||
this.handleLanguageChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addEventListenerWithCleanup(this.targetLangSelector, 'change', () => {
|
|
||||||
this.handleLanguageChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addEventListenerWithCleanup(this.swapButton, 'click', () => {
|
|
||||||
this.swapLanguages();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加拖拽事件监听器
|
|
||||||
this.setupDragListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加事件监听器并记录以便清理
|
|
||||||
*/
|
|
||||||
private addEventListenerWithCleanup(element: HTMLElement | Document, event: string, handler: EventListener): void {
|
|
||||||
element.addEventListener(event, handler);
|
|
||||||
this.eventListeners.push({ element, event, handler });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理所有事件监听器
|
|
||||||
*/
|
|
||||||
private cleanupEventListeners(): void {
|
|
||||||
this.eventListeners.forEach(({ element, event, handler }) => {
|
|
||||||
element.removeEventListener(event, handler);
|
|
||||||
});
|
|
||||||
this.eventListeners = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 初始化和生命周期 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化翻译功能
|
|
||||||
*/
|
|
||||||
private async initializeTranslation(): Promise<void> {
|
|
||||||
this.showLoading();
|
|
||||||
this.resultContainer.innerHTML = '<div class="cm-translation-loading">Loading...</div>';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.loadTranslators();
|
|
||||||
await this.translate();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize translation:', error);
|
|
||||||
this.hideLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 语言管理 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置拖拽事件监听器
|
|
||||||
*/
|
|
||||||
private setupDragListeners(): void {
|
|
||||||
// 在整个翻译框上监听鼠标按下事件
|
|
||||||
this.addEventListenerWithCleanup(this.dom, 'mousedown', (e: Event) => {
|
|
||||||
const mouseEvent = e as MouseEvent;
|
|
||||||
const target = mouseEvent.target as HTMLElement;
|
|
||||||
|
|
||||||
// 如果点击的是交互元素(按钮、选择框等),不启动拖拽
|
|
||||||
if (target.tagName === 'SELECT' || target.tagName === 'BUTTON' ||
|
|
||||||
target.tagName === 'OPTION' || target.closest('select') || target.closest('button')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startDrag(mouseEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 鼠标移动
|
|
||||||
this.addEventListenerWithCleanup(document, 'mousemove', (e: Event) => {
|
|
||||||
const mouseEvent = e as MouseEvent;
|
|
||||||
this.onDrag(mouseEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 鼠标释放结束拖拽
|
|
||||||
this.addEventListenerWithCleanup(document, 'mouseup', () => {
|
|
||||||
this.endDrag();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开始拖拽
|
|
||||||
*/
|
|
||||||
private startDrag(e: MouseEvent): void {
|
|
||||||
e.preventDefault();
|
|
||||||
this.isDragging = true;
|
|
||||||
|
|
||||||
const rect = this.dom.getBoundingClientRect();
|
|
||||||
this.dragOffset = {
|
|
||||||
x: e.clientX - rect.left,
|
|
||||||
y: e.clientY - rect.top
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加拖拽状态样式
|
|
||||||
this.dom.classList.add('cm-translation-dragging');
|
|
||||||
this.dom.style.cursor = 'grabbing';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拖拽过程中
|
|
||||||
*/
|
|
||||||
private onDrag(e: MouseEvent): void {
|
|
||||||
if (!this.isDragging) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const newX = e.clientX - this.dragOffset.x;
|
|
||||||
const newY = e.clientY - this.dragOffset.y;
|
|
||||||
|
|
||||||
// 确保不会拖拽到视窗外
|
|
||||||
const maxX = window.innerWidth - this.dom.offsetWidth;
|
|
||||||
const maxY = window.innerHeight - this.dom.offsetHeight;
|
|
||||||
|
|
||||||
const clampedX = Math.max(0, Math.min(newX, maxX));
|
|
||||||
const clampedY = Math.max(0, Math.min(newY, maxY));
|
|
||||||
|
|
||||||
this.dom.style.left = `${clampedX}px`;
|
|
||||||
this.dom.style.top = `${clampedY}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 结束拖拽
|
|
||||||
*/
|
|
||||||
private endDrag(): void {
|
|
||||||
if (!this.isDragging) return;
|
|
||||||
|
|
||||||
this.isDragging = false;
|
|
||||||
|
|
||||||
// 移除拖拽状态样式
|
|
||||||
this.dom.classList.remove('cm-translation-dragging');
|
|
||||||
this.dom.style.cursor = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理语言变更
|
|
||||||
*/
|
|
||||||
private handleLanguageChange(): void {
|
|
||||||
// 语言变更后重新翻译,具体的语言限制逻辑在store中处理
|
|
||||||
this.translate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 交换源语言和目标语言
|
|
||||||
*/
|
|
||||||
private swapLanguages(): void {
|
|
||||||
const temp = this.sourceLangSelector.value;
|
|
||||||
this.sourceLangSelector.value = this.targetLangSelector.value;
|
|
||||||
this.targetLangSelector.value = temp;
|
|
||||||
this.translate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 翻译器管理 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载翻译器选项
|
|
||||||
*/
|
|
||||||
private async loadTranslators(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
this.clearSelectOptions(this.translatorSelector);
|
|
||||||
|
|
||||||
const translators = this.translationStore.translators;
|
|
||||||
this.populateTranslatorOptions(translators);
|
|
||||||
|
|
||||||
// 添加翻译器变更事件监听
|
|
||||||
this.addEventListenerWithCleanup(this.translatorSelector, 'change', () => {
|
|
||||||
this.handleTranslatorChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.updateLanguageSelectors();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load translators:', error);
|
|
||||||
this.loadDefaultTranslators();
|
|
||||||
await this.updateLanguageSelectors();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 填充翻译器选项
|
|
||||||
*/
|
|
||||||
private populateTranslatorOptions(translators: string[]): void {
|
|
||||||
translators.forEach((translator, index) => {
|
|
||||||
const option = this.createElement('option') as HTMLOptionElement;
|
|
||||||
option.value = translator;
|
|
||||||
option.textContent = translator;
|
|
||||||
option.selected = index === 0; // 选择第一个翻译器
|
|
||||||
this.translatorSelector.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载默认翻译器
|
|
||||||
*/
|
|
||||||
private loadDefaultTranslators(): void {
|
|
||||||
this.clearSelectOptions(this.translatorSelector);
|
|
||||||
|
|
||||||
// 使用从后端获取的翻译器列表
|
|
||||||
const translators = this.translationStore.translators;
|
|
||||||
this.populateTranslatorOptions(translators);
|
|
||||||
|
|
||||||
this.addEventListenerWithCleanup(this.translatorSelector, 'change', () => {
|
|
||||||
this.handleTranslatorChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理翻译器选择变化
|
|
||||||
*/
|
|
||||||
private async handleTranslatorChange(): Promise<void> {
|
|
||||||
await this.updateLanguageSelectors();
|
|
||||||
this.translate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 语言选择器管理 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新语言选择器
|
|
||||||
*/
|
|
||||||
private async updateLanguageSelectors(): Promise<void> {
|
|
||||||
const currentTranslator = this.translatorSelector.value;
|
|
||||||
|
|
||||||
// 保存当前选中的语言
|
|
||||||
const currentSourceLang = this.sourceLangSelector.value || '';
|
|
||||||
const currentTargetLang = this.targetLangSelector.value;
|
|
||||||
|
|
||||||
// 清空选择器
|
|
||||||
this.clearSelectOptions(this.sourceLangSelector);
|
|
||||||
this.clearSelectOptions(this.targetLangSelector);
|
|
||||||
|
|
||||||
// 直接使用预加载的语言映射
|
|
||||||
const languageMap = this.translationStore.translatorLanguages[currentTranslator];
|
|
||||||
|
|
||||||
if (!languageMap || Object.keys(languageMap).length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加语言选项
|
|
||||||
Object.entries(languageMap).forEach(([code, langInfo]) => {
|
|
||||||
this.addLanguageOption(code, langInfo);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 恢复之前的语言选择
|
|
||||||
this.restoreLanguageSelection(currentSourceLang, currentTargetLang);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空选择器选项
|
|
||||||
*/
|
|
||||||
private clearSelectOptions(selector: HTMLSelectElement): void {
|
|
||||||
while (selector.firstChild) {
|
|
||||||
selector.removeChild(selector.firstChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加语言选项到选择器
|
|
||||||
*/
|
|
||||||
private addLanguageOption(code: string, langInfo: any): void {
|
|
||||||
const displayName = langInfo.Name || langInfo.name || code;
|
|
||||||
|
|
||||||
// 添加源语言选项
|
|
||||||
const sourceOption = this.createElement('option') as HTMLOptionElement;
|
|
||||||
sourceOption.value = code;
|
|
||||||
sourceOption.textContent = displayName;
|
|
||||||
this.sourceLangSelector.appendChild(sourceOption);
|
|
||||||
|
|
||||||
// 添加目标语言选项
|
|
||||||
const targetOption = this.createElement('option') as HTMLOptionElement;
|
|
||||||
targetOption.value = code;
|
|
||||||
targetOption.textContent = displayName;
|
|
||||||
this.targetLangSelector.appendChild(targetOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 恢复语言选择
|
|
||||||
*/
|
|
||||||
private restoreLanguageSelection(sourceLang: string, targetLang: string): void {
|
|
||||||
// 设置源语言
|
|
||||||
if (sourceLang && this.hasLanguageOption(this.sourceLangSelector, sourceLang)) {
|
|
||||||
this.sourceLangSelector.value = sourceLang;
|
|
||||||
} else if (this.sourceLangSelector.options.length > 0) {
|
|
||||||
this.sourceLangSelector.selectedIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置目标语言
|
|
||||||
if (targetLang && this.hasLanguageOption(this.targetLangSelector, targetLang)) {
|
|
||||||
this.targetLangSelector.value = targetLang;
|
|
||||||
} else if (this.targetLangSelector.options.length > 0) {
|
|
||||||
this.targetLangSelector.selectedIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保源语言和目标语言不同
|
|
||||||
this.handleLanguageChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查选择器是否有指定语言选项
|
|
||||||
*/
|
|
||||||
private hasLanguageOption(selector: HTMLSelectElement, langCode: string): boolean {
|
|
||||||
return Array.from(selector.options).some(option => option.value === langCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 翻译功能 =====
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行翻译
|
|
||||||
*/
|
|
||||||
private async translate(): Promise<void> {
|
|
||||||
const sourceLang = this.sourceLangSelector.value;
|
|
||||||
const targetLang = this.targetLangSelector.value;
|
|
||||||
const translatorType = this.translatorSelector.value;
|
|
||||||
|
|
||||||
this.showLoading();
|
|
||||||
this.resultContainer.innerHTML = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await this.translationStore.translateText(
|
|
||||||
this.sourceText,
|
|
||||||
sourceLang,
|
|
||||||
targetLang,
|
|
||||||
translatorType
|
|
||||||
);
|
|
||||||
|
|
||||||
this.displayTranslationResult(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Translation failed:', err);
|
|
||||||
this.displayError('Translation failed');
|
|
||||||
} finally {
|
|
||||||
this.hideLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== UI 状态管理 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示加载状态
|
|
||||||
*/
|
|
||||||
private showLoading(): void {
|
|
||||||
this.loadingIndicator.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 隐藏加载状态
|
|
||||||
*/
|
|
||||||
private hideLoading(): void {
|
|
||||||
this.loadingIndicator.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示错误信息
|
|
||||||
*/
|
|
||||||
private displayError(message: string): void {
|
|
||||||
this.resultContainer.innerHTML = '';
|
|
||||||
this.translatedText = '';
|
|
||||||
|
|
||||||
const errorElement = this.createElement('div', 'cm-translation-error');
|
|
||||||
errorElement.textContent = message;
|
|
||||||
this.resultContainer.appendChild(errorElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 结果显示 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示翻译结果
|
|
||||||
*/
|
|
||||||
private displayTranslationResult(result: any): void {
|
|
||||||
this.resultContainer.innerHTML = '';
|
|
||||||
|
|
||||||
const resultWrapper = this.createElement('div', 'cm-translation-result-wrapper');
|
|
||||||
const translatedTextElem = this.createElement('div', 'cm-translation-target');
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
translatedTextElem.classList.add('cm-translation-error');
|
|
||||||
translatedTextElem.textContent = result.error;
|
|
||||||
this.translatedText = '';
|
|
||||||
} else {
|
|
||||||
this.translatedText = result.translatedText || '';
|
|
||||||
translatedTextElem.textContent = this.translatedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加复制按钮
|
|
||||||
if (this.translatedText) {
|
|
||||||
const copyButton = this.createCopyButton();
|
|
||||||
resultWrapper.appendChild(copyButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
resultWrapper.appendChild(translatedTextElem);
|
|
||||||
this.resultContainer.appendChild(resultWrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建复制按钮
|
|
||||||
*/
|
|
||||||
private createCopyButton(): HTMLButtonElement {
|
|
||||||
const copyButton = this.createElement('button', 'cm-translation-copy-btn') as HTMLButtonElement;
|
|
||||||
copyButton.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
|
|
||||||
|
|
||||||
this.addEventListenerWithCleanup(copyButton, 'click', () => {
|
|
||||||
this.copyToClipboard(copyButton);
|
|
||||||
});
|
|
||||||
|
|
||||||
return copyButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 复制文本到剪贴板
|
|
||||||
*/
|
|
||||||
private async copyToClipboard(button: HTMLButtonElement): Promise<void> {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(this.translatedText);
|
|
||||||
this.showCopySuccess(button);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to copy text:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示复制成功状态
|
|
||||||
*/
|
|
||||||
private showCopySuccess(button: HTMLButtonElement): void {
|
|
||||||
const originalHTML = button.innerHTML;
|
|
||||||
button.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`;
|
|
||||||
button.classList.add('copied');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
button.innerHTML = originalHTML;
|
|
||||||
button.classList.remove('copied');
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 生命周期管理 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁组件时的清理工作
|
|
||||||
*/
|
|
||||||
destroy(): void {
|
|
||||||
this.cleanupEventListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建翻译气泡
|
|
||||||
export function createTranslationTooltip(view: EditorView, text: string): Tooltip {
|
|
||||||
return {
|
|
||||||
pos: view.state.selection.main.to, // 紧贴文本末尾
|
|
||||||
above: false,
|
|
||||||
strictSide: false,
|
|
||||||
arrow: true,
|
|
||||||
create: () => new TranslationTooltip(view, text)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,7 @@ import {deleteLineCommand} from '../extensions/codeblock/deleteLine';
|
|||||||
import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines';
|
import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines';
|
||||||
import {transposeChars} from '../extensions/codeblock';
|
import {transposeChars} from '../extensions/codeblock';
|
||||||
import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste';
|
import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste';
|
||||||
import {textHighlightToggleCommand} from '../extensions/textHighlight/textHighlightExtension';
|
import {textHighlightToggleCommand} from '../extensions/textHighlight';
|
||||||
import {
|
import {
|
||||||
copyLineDown,
|
copyLineDown,
|
||||||
copyLineUp,
|
copyLineUp,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import i18n from '@/i18n';
|
|||||||
import {ExtensionDefinition} 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';
|
||||||
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';
|
||||||
@@ -43,13 +43,7 @@ const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
|
|||||||
descriptionKey: 'extensions.colorSelector.description'
|
descriptionKey: 'extensions.colorSelector.description'
|
||||||
},
|
},
|
||||||
[ExtensionID.ExtensionTranslator]: {
|
[ExtensionID.ExtensionTranslator]: {
|
||||||
definition: defineExtension((config: any) => createTranslatorExtension({
|
definition: defineExtension(() => 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'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
52
frontend/src/views/editor/theme/presets.ts
Normal file
52
frontend/src/views/editor/theme/presets.ts
Normal 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;
|
||||||
@@ -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; // 匹配括号颜色
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
14
go.mod
14
go.mod
@@ -10,11 +10,11 @@ require (
|
|||||||
github.com/knadh/koanf/providers/structs v1.0.0
|
github.com/knadh/koanf/providers/structs v1.0.0
|
||||||
github.com/knadh/koanf/v2 v2.3.0
|
github.com/knadh/koanf/v2 v2.3.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.40
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.41
|
||||||
golang.org/x/net v0.47.0
|
golang.org/x/net v0.47.0
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.38.0
|
||||||
golang.org/x/text v0.31.0
|
golang.org/x/text v0.31.0
|
||||||
modernc.org/sqlite v1.40.0
|
modernc.org/sqlite v1.40.1
|
||||||
resty.dev/v3 v3.0.0-beta.3
|
resty.dev/v3 v3.0.0-beta.3
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ require (
|
|||||||
github.com/adrg/xdg v0.5.3 // indirect
|
github.com/adrg/xdg v0.5.3 // indirect
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -42,7 +42,7 @@ require (
|
|||||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
@@ -73,19 +73,19 @@ require (
|
|||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||||
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.45.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-20251113190631-e25ba8c21ef6 // 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
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.66.10 // indirect
|
modernc.org/libc v1.67.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
48
go.sum
48
go.sum
@@ -25,8 +25,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
|
|||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk=
|
github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk=
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
|
github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -62,8 +62,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
@@ -87,6 +87,8 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU
|
|||||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||||
@@ -162,12 +164,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
|
||||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.40 h1:LY0hngVwihlSXveshL5LM8ivjLTHAN6VDjOSF6szI9k=
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.41 h1:DYcC1/vtO862sxnoyCOMfLLypbzpFWI257fR6zDYY+Y=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.40/go.mod h1:7i8tSuA74q97zZ5qEJlcVZdnO+IR7LT2KU8UpzYMPsw=
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.41/go.mod h1:7i8tSuA74q97zZ5qEJlcVZdnO+IR7LT2KU8UpzYMPsw=
|
||||||
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
@@ -178,12 +180,12 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
|
|||||||
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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
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-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -220,8 +222,8 @@ golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
|||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -236,18 +238,20 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||||
|
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
modernc.org/libc v1.67.0 h1:QzL4IrKab2OFmxA3/vRYl0tLXrIamwrhD6CKD4WBVjQ=
|
||||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
modernc.org/libc v1.67.0/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -256,8 +260,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
|
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
||||||
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package services
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
SELECT id, name, type, colors, is_default, created_at, updated_at
|
||||||
presetThemes := []struct {
|
FROM themes
|
||||||
config *models.ThemeColorConfig
|
WHERE name = ?
|
||||||
themeType models.ThemeType
|
LIMIT 1
|
||||||
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
|
|
||||||
FROM themes
|
|
||||||
WHERE name = ?
|
|
||||||
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 get theme: %w", err)
|
return nil, fmt.Errorf("failed to query 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 服务关闭
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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像素(小屏幕保底)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user