From a720a4cfb88f8e5033559325deae718ea068e8fa Mon Sep 17 00:00:00 2001 From: landaiqing Date: Fri, 11 Jul 2025 23:02:25 +0800 Subject: [PATCH] :sparkles: Complete the custom editor theme --- README.md | 3 +- README_ZH.md | 3 +- .../voidraft/internal/models/models.ts | 289 ++++++++++- frontend/components.d.ts | 1 + frontend/package-lock.json | 24 + frontend/package.json | 1 + frontend/src/assets/styles/variables.css | 38 ++ .../src/components/loading/LoadingScreen.vue | 177 +++++++ .../src/components/titlebar/LinuxTitleBar.vue | 2 +- .../components/titlebar/WindowsTitleBar.vue | 3 +- frontend/src/i18n/locales/en-US.ts | 38 +- frontend/src/i18n/locales/zh-CN.ts | 38 +- frontend/src/stores/configStore.ts | 122 ++++- frontend/src/stores/editorStore.ts | 17 +- frontend/src/stores/themeStore.ts | 113 ++++- frontend/src/views/editor/Editor.vue | 7 +- .../src/views/editor/basic/themeExtension.ts | 31 +- .../rainbowBracketsExtension.ts | 17 +- .../views/editor/manager/ExtensionManager.ts | 33 +- frontend/src/views/editor/theme/dark.ts | 200 ++++---- frontend/src/views/editor/theme/light.ts | 413 +++++++-------- .../settings/components/SettingSection.vue | 26 +- .../views/settings/pages/AppearancePage.vue | 472 +++++++++++++++++- internal/models/config.go | 10 +- internal/models/theme.go | 127 +++++ internal/services/config_migration_service.go | 2 +- 26 files changed, 1838 insertions(+), 369 deletions(-) create mode 100644 frontend/src/components/loading/LoadingScreen.vue create mode 100644 internal/models/theme.go diff --git a/README.md b/README.md index dd8d50e..6f0d7c2 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ VoidRaft is a modern developer-focused text editor that allows you to record, or - Code formatting - Built-in Prettier support for one-click code beautification - Block editing mode - Split content into independent code blocks, each with different language settings - Multi-window support - edit multiple documents at the same time +- Support for custom themes - Custom editor themes ### Modern Interface @@ -118,7 +119,7 @@ Voidraft/ | Linux | Planned | Future versions | ### Planned Features -- [ ] Custom themes - Customize editor themes +- ✅ Custom themes - Customize editor themes - ✅ Multi-window support - Support editing multiple documents simultaneously - [ ] Enhanced clipboard - Monitor and manage clipboard history - [ ] Data synchronization - Cloud backup for configurations and documents diff --git a/README_ZH.md b/README_ZH.md index 127e74a..66d7203 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -15,6 +15,7 @@ Voidraft 是一个现代化的开发者专用文本编辑器,让你能够随 - 代码格式化 - 内置 Prettier 支持,一键美化代码 - 块状编辑模式 - 将内容分割为独立的代码块,每个块可设置不同语言 - 支持多窗口 - 同时编辑多个文档 +- 支持自定义主题 - 自定义编辑器主题 ### 现代化界面 @@ -119,7 +120,7 @@ Voidraft/ | Linux | 计划中 | 后续版本 | ### 计划添加的功能 -- [ ] 自定义主题 - 自定义编辑器主题 +- ✅ 自定义主题 - 自定义编辑器主题 - ✅ 多窗口支持 - 支持同时编辑多个文档 - [ ] 剪切板增强 - 监听和管理剪切板历史 - [ ] 数据同步 - 配置和文档云端备份 diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 8d6c49f..ebc65d4 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -102,6 +102,11 @@ export class AppearanceConfig { */ "systemTheme": SystemThemeType; + /** + * 自定义主题配置 + */ + "customTheme": CustomThemeConfig; + /** Creates a new AppearanceConfig instance. */ constructor($$source: Partial = {}) { if (!("language" in $$source)) { @@ -110,6 +115,9 @@ export class AppearanceConfig { if (!("systemTheme" in $$source)) { this["systemTheme"] = ("" as SystemThemeType); } + if (!("customTheme" in $$source)) { + this["customTheme"] = (new CustomThemeConfig()); + } Object.assign(this, $$source); } @@ -118,7 +126,11 @@ export class AppearanceConfig { * Creates a new AppearanceConfig instance from a string or object. */ static createFrom($$source: any = {}): AppearanceConfig { + const $$createField2_0 = $$createType5; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("customTheme" in $$parsedSource) { + $$parsedSource["customTheme"] = $$createField2_0($$parsedSource["customTheme"]); + } return new AppearanceConfig($$parsedSource as Partial); } } @@ -158,6 +170,49 @@ export class ConfigMetadata { } } +/** + * CustomThemeConfig 自定义主题配置 + */ +export class CustomThemeConfig { + /** + * 深色主题配置 + */ + "darkTheme": ThemeColorConfig; + + /** + * 浅色主题配置 + */ + "lightTheme": ThemeColorConfig; + + /** Creates a new CustomThemeConfig instance. */ + constructor($$source: Partial = {}) { + if (!("darkTheme" in $$source)) { + this["darkTheme"] = (new ThemeColorConfig()); + } + if (!("lightTheme" in $$source)) { + this["lightTheme"] = (new ThemeColorConfig()); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new CustomThemeConfig instance from a string or object. + */ + static createFrom($$source: any = {}): CustomThemeConfig { + const $$createField0_0 = $$createType6; + const $$createField1_0 = $$createType6; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("darkTheme" in $$parsedSource) { + $$parsedSource["darkTheme"] = $$createField0_0($$parsedSource["darkTheme"]); + } + if ("lightTheme" in $$parsedSource) { + $$parsedSource["lightTheme"] = $$createField1_0($$parsedSource["lightTheme"]); + } + return new CustomThemeConfig($$parsedSource as Partial); + } +} + /** * Document represents a document in the system */ @@ -334,7 +389,7 @@ export class Extension { * Creates a new Extension instance from a string or object. */ static createFrom($$source: any = {}): Extension { - const $$createField3_0 = $$createType5; + const $$createField3_0 = $$createType7; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("config" in $$parsedSource) { $$parsedSource["config"] = $$createField3_0($$parsedSource["config"]); @@ -467,7 +522,7 @@ export class GeneralConfig { * Creates a new GeneralConfig instance from a string or object. */ static createFrom($$source: any = {}): GeneralConfig { - const $$createField5_0 = $$createType7; + const $$createField5_0 = $$createType9; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("globalHotkey" in $$parsedSource) { $$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); @@ -1028,6 +1083,214 @@ export enum TabType { TabTypeTab = "tab", }; +/** + * ThemeColorConfig 主题颜色配置 + */ +export class ThemeColorConfig { + /** + * 基础色调 + * 主背景色 + */ + "background": string; + + /** + * 次要背景色 + */ + "backgroundSecondary": string; + + /** + * 面板背景 + */ + "surface": string; + + /** + * 主文本色 + */ + "foreground": string; + + /** + * 次要文本色 + */ + "foregroundSecondary": string; + + /** + * 语法高亮 + * 注释色 + */ + "comment": string; + + /** + * 关键字 + */ + "keyword": string; + + /** + * 字符串 + */ + "string": string; + + /** + * 函数名 + */ + "function": string; + + /** + * 数字 + */ + "number": string; + + /** + * 操作符 + */ + "operator": string; + + /** + * 变量 + */ + "variable": string; + + /** + * 类型 + */ + "type": 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 = {}) { + if (!("background" in $$source)) { + this["background"] = ""; + } + if (!("backgroundSecondary" in $$source)) { + this["backgroundSecondary"] = ""; + } + if (!("surface" in $$source)) { + this["surface"] = ""; + } + 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 (!("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); + } +} + /** * UpdateSourceType 更新源类型 */ @@ -1126,8 +1389,8 @@ export class UpdatesConfig { * Creates a new UpdatesConfig instance from a string or object. */ static createFrom($$source: any = {}): UpdatesConfig { - const $$createField6_0 = $$createType8; - const $$createField7_0 = $$createType9; + const $$createField6_0 = $$createType10; + const $$createField7_0 = $$createType11; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("github" in $$parsedSource) { $$parsedSource["github"] = $$createField6_0($$parsedSource["github"]); @@ -1145,13 +1408,15 @@ const $$createType1 = EditingConfig.createFrom; const $$createType2 = AppearanceConfig.createFrom; const $$createType3 = UpdatesConfig.createFrom; const $$createType4 = ConfigMetadata.createFrom; -var $$createType5 = (function $$initCreateType5(...args): any { - if ($$createType5 === $$initCreateType5) { - $$createType5 = $$createType6; +const $$createType5 = CustomThemeConfig.createFrom; +const $$createType6 = ThemeColorConfig.createFrom; +var $$createType7 = (function $$initCreateType7(...args): any { + if ($$createType7 === $$initCreateType7) { + $$createType7 = $$createType8; } - return $$createType5(...args); + return $$createType7(...args); }); -const $$createType6 = $Create.Map($Create.Any, $Create.Any); -const $$createType7 = HotkeyCombo.createFrom; -const $$createType8 = GithubConfig.createFrom; -const $$createType9 = GiteaConfig.createFrom; +const $$createType8 = $Create.Map($Create.Any, $Create.Any); +const $$createType9 = HotkeyCombo.createFrom; +const $$createType10 = GithubConfig.createFrom; +const $$createType11 = GiteaConfig.createFrom; diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 961398e..223d937 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -11,6 +11,7 @@ declare module 'vue' { BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default'] DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default'] LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default'] + LoadingScreen: typeof import('./src/components/loading/LoadingScreen.vue')['default'] MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default'] MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 09751d7..040e827 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -54,6 +54,7 @@ "sass": "^1.89.2", "vue": "^3.5.17", "vue-i18n": "^11.1.9", + "vue-pick-colors": "^1.8.0", "vue-router": "^4.5.1" }, "devDependencies": { @@ -1822,6 +1823,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.19", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", @@ -5304,6 +5315,19 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/vue-pick-colors": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/vue-pick-colors/-/vue-pick-colors-1.8.0.tgz", + "integrity": "sha512-lIP28A1BZEPp0v0Y6m9lNbsC6jNM2LP+Dc2tJbUXiNRvDgXqBMe/msX3svqjspV4B+SZdPAjx75JY2zem0hA2Q==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.11.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.11.2", + "vue": "^3.2.26" + } + }, "node_modules/vue-router": { "version": "4.5.1", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 82e3c58..d37068d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,6 +58,7 @@ "sass": "^1.89.2", "vue": "^3.5.17", "vue-i18n": "^11.1.9", + "vue-pick-colors": "^1.8.0", "vue-router": "^4.5.1" }, "devDependencies": { diff --git a/frontend/src/assets/styles/variables.css b/frontend/src/assets/styles/variables.css index 81d4fbf..4ca9c89 100644 --- a/frontend/src/assets/styles/variables.css +++ b/frontend/src/assets/styles/variables.css @@ -28,6 +28,11 @@ --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; @@ -55,6 +60,11 @@ --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); @@ -83,6 +93,12 @@ --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; } @@ -116,6 +132,11 @@ --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); } } @@ -148,6 +169,11 @@ --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); } } @@ -179,6 +205,11 @@ --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); } /* 手动选择深色主题 */ @@ -207,4 +238,11 @@ --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); } \ No newline at end of file diff --git a/frontend/src/components/loading/LoadingScreen.vue b/frontend/src/components/loading/LoadingScreen.vue new file mode 100644 index 0000000..0f1b98b --- /dev/null +++ b/frontend/src/components/loading/LoadingScreen.vue @@ -0,0 +1,177 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/titlebar/LinuxTitleBar.vue b/frontend/src/components/titlebar/LinuxTitleBar.vue index a510402..e4d54be 100644 --- a/frontend/src/components/titlebar/LinuxTitleBar.vue +++ b/frontend/src/components/titlebar/LinuxTitleBar.vue @@ -1,5 +1,5 @@