Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a92935dc6 | |||
| 6e49516962 | |||
| b6c325198d | |||
| 532d30aa93 | |||
| aae86d8b4e | |||
| 0b91447b05 | |||
| 4b1fb765b0 | |||
|
|
aa5ce2b038 | ||
| 533f732c53 | |||
| 009274e4ad | |||
| 76f6c30b9d | |||
| 9ec22add55 | |||
| 272227e4e3 | |||
| ec8f8c1e2d | |||
| 1c14092068 | |||
| 00bdafc621 | |||
| 78422899e4 | |||
| c47f7de5b8 | |||
| 37aae9e03c | |||
| fa134d31d6 | |||
| d035dcd531 | |||
| c50bf452ca | |||
| dace5ce2b0 | |||
| ef145169aa | |||
| 7b746155f7 | |||
|
|
b289f4054d | ||
|
|
bdee1fdf84 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -159,8 +159,5 @@ Welcome to Fork, Star, and contribute code.
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/yourusername/voidraft)
|
||||
[](https://github.com/yourusername/voidraft)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Flandaiqing%2Fvoidraft?ref=badge_shield)
|
||||
|
||||
*Made with ❤️ by landaiqing*
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Flandaiqing%2Fvoidraft?ref=badge_large)
|
||||
@@ -21,7 +21,7 @@ export class Document {
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string | null;
|
||||
"uuid": string;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
@@ -56,7 +56,7 @@ export class Document {
|
||||
/** Creates a new Document instance. */
|
||||
constructor($$source: Partial<Document> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = null;
|
||||
this["uuid"] = "";
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
@@ -98,7 +98,7 @@ export class Extension {
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string | null;
|
||||
"uuid": string;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
@@ -116,9 +116,9 @@ export class Extension {
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* extension key
|
||||
* extension name
|
||||
*/
|
||||
"key": string;
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* extension enabled or not
|
||||
@@ -133,7 +133,7 @@ export class Extension {
|
||||
/** Creates a new Extension instance. */
|
||||
constructor($$source: Partial<Extension> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = null;
|
||||
this["uuid"] = "";
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
@@ -141,8 +141,8 @@ export class Extension {
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = "";
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
@@ -179,7 +179,7 @@ export class KeyBinding {
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string | null;
|
||||
"uuid": string;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
@@ -197,29 +197,59 @@ export class KeyBinding {
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* key binding key
|
||||
* command identifier
|
||||
*/
|
||||
"key": string;
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* key binding command
|
||||
* keybinding type: standard or emacs
|
||||
*/
|
||||
"command": string;
|
||||
"type": string;
|
||||
|
||||
/**
|
||||
* key binding extension
|
||||
* universal keybinding (cross-platform)
|
||||
*/
|
||||
"extension"?: string;
|
||||
"key"?: string;
|
||||
|
||||
/**
|
||||
* key binding enabled
|
||||
* macOS specific keybinding
|
||||
*/
|
||||
"macos"?: string;
|
||||
|
||||
/**
|
||||
* Windows specific keybinding
|
||||
*/
|
||||
"windows"?: string;
|
||||
|
||||
/**
|
||||
* Linux specific keybinding
|
||||
*/
|
||||
"linux"?: string;
|
||||
|
||||
/**
|
||||
* extension name (functional category)
|
||||
*/
|
||||
"extension": string;
|
||||
|
||||
/**
|
||||
* whether this keybinding is enabled
|
||||
*/
|
||||
"enabled": boolean;
|
||||
|
||||
/**
|
||||
* prevent browser default behavior
|
||||
*/
|
||||
"preventDefault": boolean;
|
||||
|
||||
/**
|
||||
* keybinding scope (default: editor)
|
||||
*/
|
||||
"scope"?: string;
|
||||
|
||||
/** Creates a new KeyBinding instance. */
|
||||
constructor($$source: Partial<KeyBinding> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = null;
|
||||
this["uuid"] = "";
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
@@ -227,15 +257,21 @@ export class KeyBinding {
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = "";
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("command" in $$source)) {
|
||||
this["command"] = "";
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = "";
|
||||
}
|
||||
if (!("extension" in $$source)) {
|
||||
this["extension"] = "";
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
if (!("preventDefault" in $$source)) {
|
||||
this["preventDefault"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -261,7 +297,7 @@ export class Theme {
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string | null;
|
||||
"uuid": string;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
@@ -279,9 +315,9 @@ export class Theme {
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* theme key
|
||||
* theme name
|
||||
*/
|
||||
"key": string;
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* theme type
|
||||
@@ -296,7 +332,7 @@ export class Theme {
|
||||
/** Creates a new Theme instance. */
|
||||
constructor($$source: Partial<Theme> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = null;
|
||||
this["uuid"] = "";
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
@@ -304,8 +340,8 @@ export class Theme {
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = "";
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = ("" as theme$0.Type);
|
||||
|
||||
@@ -234,6 +234,12 @@ export class EditingConfig {
|
||||
*/
|
||||
"tabType": TabType;
|
||||
|
||||
/**
|
||||
* 快捷键模式
|
||||
* 快捷键模式(standard 或 emacs)
|
||||
*/
|
||||
"keymapMode": KeyBindingType;
|
||||
|
||||
/**
|
||||
* 保存选项
|
||||
* 自动保存延迟(毫秒)
|
||||
@@ -263,6 +269,9 @@ export class EditingConfig {
|
||||
if (!("tabType" in $$source)) {
|
||||
this["tabType"] = ("" as TabType);
|
||||
}
|
||||
if (!("keymapMode" in $$source)) {
|
||||
this["keymapMode"] = ("" as KeyBindingType);
|
||||
}
|
||||
if (!("autoSaveDelay" in $$source)) {
|
||||
this["autoSaveDelay"] = 0;
|
||||
}
|
||||
@@ -283,14 +292,14 @@ export class EditingConfig {
|
||||
* Extension 扩展配置
|
||||
*/
|
||||
export class Extension {
|
||||
"key": ExtensionKey;
|
||||
"key": ExtensionName;
|
||||
"enabled": boolean;
|
||||
"config": ExtensionConfig;
|
||||
|
||||
/** Creates a new Extension instance. */
|
||||
constructor($$source: Partial<Extension> = {}) {
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = ("" as ExtensionKey);
|
||||
this["key"] = ("" as ExtensionName);
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
@@ -321,79 +330,83 @@ export class Extension {
|
||||
export type ExtensionConfig = { [_: string]: any };
|
||||
|
||||
/**
|
||||
* ExtensionKey 扩展标识符
|
||||
* ExtensionName 扩展标识符
|
||||
*/
|
||||
export enum ExtensionKey {
|
||||
export enum ExtensionName {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* 编辑增强扩展
|
||||
* 彩虹括号
|
||||
*/
|
||||
ExtensionRainbowBrackets = "rainbowBrackets",
|
||||
RainbowBrackets = "rainbowBrackets",
|
||||
|
||||
/**
|
||||
* 超链接
|
||||
*/
|
||||
ExtensionHyperlink = "hyperlink",
|
||||
Hyperlink = "hyperlink",
|
||||
|
||||
/**
|
||||
* 颜色选择器
|
||||
*/
|
||||
ExtensionColorSelector = "colorSelector",
|
||||
ColorSelector = "colorSelector",
|
||||
|
||||
/**
|
||||
* 代码折叠
|
||||
*/
|
||||
ExtensionFold = "fold",
|
||||
Fold = "fold",
|
||||
|
||||
/**
|
||||
* 划词翻译
|
||||
*/
|
||||
ExtensionTranslator = "translator",
|
||||
Translator = "translator",
|
||||
|
||||
/**
|
||||
* Markdown渲染
|
||||
*/
|
||||
ExtensionMarkdown = "markdown",
|
||||
Markdown = "markdown",
|
||||
|
||||
/**
|
||||
* 显示空白字符
|
||||
*/
|
||||
ExtensionHighlightWhitespace = "highlightWhitespace",
|
||||
HighlightWhitespace = "highlightWhitespace",
|
||||
|
||||
/**
|
||||
* 高亮行尾空白
|
||||
*/
|
||||
ExtensionHighlightTrailingWhitespace = "highlightTrailingWhitespace",
|
||||
HighlightTrailingWhitespace = "highlightTrailingWhitespace",
|
||||
|
||||
/**
|
||||
* 小地图
|
||||
*/
|
||||
ExtensionMinimap = "minimap",
|
||||
Minimap = "minimap",
|
||||
|
||||
/**
|
||||
* 行号显示
|
||||
*/
|
||||
ExtensionLineNumbers = "lineNumbers",
|
||||
LineNumbers = "lineNumbers",
|
||||
|
||||
/**
|
||||
* 上下文菜单
|
||||
*/
|
||||
ExtensionContextMenu = "contextMenu",
|
||||
ContextMenu = "contextMenu",
|
||||
|
||||
/**
|
||||
* 搜索功能
|
||||
*/
|
||||
ExtensionSearch = "search",
|
||||
Search = "search",
|
||||
|
||||
/**
|
||||
* HTTP 客户端
|
||||
*/
|
||||
ExtensionHttpClient = "httpClient",
|
||||
HttpClient = "httpClient",
|
||||
|
||||
/**
|
||||
* 代码块导出图片
|
||||
*/
|
||||
BlockImage = "blockImage",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -448,6 +461,11 @@ export class GeneralConfig {
|
||||
*/
|
||||
"enableTabs": boolean;
|
||||
|
||||
/**
|
||||
* 是否启用内存监视器
|
||||
*/
|
||||
"enableMemoryMonitor": boolean;
|
||||
|
||||
/** Creates a new GeneralConfig instance. */
|
||||
constructor($$source: Partial<GeneralConfig> = {}) {
|
||||
if (!("alwaysOnTop" in $$source)) {
|
||||
@@ -477,6 +495,9 @@ export class GeneralConfig {
|
||||
if (!("enableTabs" in $$source)) {
|
||||
this["enableTabs"] = false;
|
||||
}
|
||||
if (!("enableMemoryMonitor" in $$source)) {
|
||||
this["enableMemoryMonitor"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -543,49 +564,6 @@ export class GitBackupConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GiteaConfig Gitea配置
|
||||
*/
|
||||
export class GiteaConfig {
|
||||
/**
|
||||
* Gitea服务器URL
|
||||
*/
|
||||
"baseURL": string;
|
||||
|
||||
/**
|
||||
* 仓库所有者
|
||||
*/
|
||||
"owner": string;
|
||||
|
||||
/**
|
||||
* 仓库名称
|
||||
*/
|
||||
"repo": string;
|
||||
|
||||
/** Creates a new GiteaConfig instance. */
|
||||
constructor($$source: Partial<GiteaConfig> = {}) {
|
||||
if (!("baseURL" in $$source)) {
|
||||
this["baseURL"] = "";
|
||||
}
|
||||
if (!("owner" in $$source)) {
|
||||
this["owner"] = "";
|
||||
}
|
||||
if (!("repo" in $$source)) {
|
||||
this["repo"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new GiteaConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): GiteaConfig {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new GiteaConfig($$parsedSource as Partial<GiteaConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GithubConfig GitHub配置
|
||||
*/
|
||||
@@ -684,25 +662,73 @@ export class HotkeyCombo {
|
||||
* KeyBinding 单个快捷键绑定
|
||||
*/
|
||||
export class KeyBinding {
|
||||
"key": KeyBindingKey;
|
||||
"command": string;
|
||||
"extension": ExtensionKey;
|
||||
/**
|
||||
* 命令唯一标识符
|
||||
*/
|
||||
"name": KeyBindingName;
|
||||
|
||||
/**
|
||||
* 快捷键类型(standard 或 "emacs")
|
||||
*/
|
||||
"type": KeyBindingType;
|
||||
|
||||
/**
|
||||
* 通用快捷键(跨平台)
|
||||
*/
|
||||
"key"?: string;
|
||||
|
||||
/**
|
||||
* macOS 专用快捷键
|
||||
*/
|
||||
"macos"?: string;
|
||||
|
||||
/**
|
||||
* windows 专用快捷键
|
||||
*/
|
||||
"win"?: string;
|
||||
|
||||
/**
|
||||
* Linux 专用快捷键
|
||||
*/
|
||||
"linux"?: string;
|
||||
|
||||
/**
|
||||
* 所属扩展
|
||||
*/
|
||||
"extension": ExtensionName;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
"enabled": boolean;
|
||||
|
||||
/**
|
||||
* 阻止浏览器默认行为
|
||||
*/
|
||||
"preventDefault": boolean;
|
||||
|
||||
/**
|
||||
* 作用域(默认 "editor")
|
||||
*/
|
||||
"scope"?: string;
|
||||
|
||||
/** Creates a new KeyBinding instance. */
|
||||
constructor($$source: Partial<KeyBinding> = {}) {
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = ("" as KeyBindingKey);
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = ("" as KeyBindingName);
|
||||
}
|
||||
if (!("command" in $$source)) {
|
||||
this["command"] = "";
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = ("" as KeyBindingType);
|
||||
}
|
||||
if (!("extension" in $$source)) {
|
||||
this["extension"] = ("" as ExtensionKey);
|
||||
this["extension"] = ("" as ExtensionName);
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
if (!("preventDefault" in $$source)) {
|
||||
this["preventDefault"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -717,9 +743,9 @@ export class KeyBinding {
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyBindingKey 快捷键命令
|
||||
* KeyBindingName 快捷键命令标识符
|
||||
*/
|
||||
export enum KeyBindingKey {
|
||||
export enum KeyBindingName {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
@@ -728,247 +754,414 @@ export enum KeyBindingKey {
|
||||
/**
|
||||
* 显示搜索
|
||||
*/
|
||||
ShowSearchKeyBindingKey = "showSearch",
|
||||
ShowSearch = "showSearch",
|
||||
|
||||
/**
|
||||
* 隐藏搜索
|
||||
*/
|
||||
HideSearchKeyBindingKey = "hideSearch",
|
||||
HideSearch = "hideSearch",
|
||||
|
||||
/**
|
||||
* 块内选择全部
|
||||
*/
|
||||
BlockSelectAllKeyBindingKey = "blockSelectAll",
|
||||
BlockSelectAll = "blockSelectAll",
|
||||
|
||||
/**
|
||||
* 在当前块后添加新块
|
||||
*/
|
||||
BlockAddAfterCurrentKeyBindingKey = "blockAddAfterCurrent",
|
||||
BlockAddAfterCurrent = "blockAddAfterCurrent",
|
||||
|
||||
/**
|
||||
* 在最后添加新块
|
||||
*/
|
||||
BlockAddAfterLastKeyBindingKey = "blockAddAfterLast",
|
||||
BlockAddAfterLast = "blockAddAfterLast",
|
||||
|
||||
/**
|
||||
* 在当前块前添加新块
|
||||
*/
|
||||
BlockAddBeforeCurrentKeyBindingKey = "blockAddBeforeCurrent",
|
||||
BlockAddBeforeCurrent = "blockAddBeforeCurrent",
|
||||
|
||||
/**
|
||||
* 跳转到上一个块
|
||||
*/
|
||||
BlockGotoPreviousKeyBindingKey = "blockGotoPrevious",
|
||||
BlockGotoPrevious = "blockGotoPrevious",
|
||||
|
||||
/**
|
||||
* 跳转到下一个块
|
||||
*/
|
||||
BlockGotoNextKeyBindingKey = "blockGotoNext",
|
||||
BlockGotoNext = "blockGotoNext",
|
||||
|
||||
/**
|
||||
* 选择上一个块
|
||||
*/
|
||||
BlockSelectPreviousKeyBindingKey = "blockSelectPrevious",
|
||||
BlockSelectPrevious = "blockSelectPrevious",
|
||||
|
||||
/**
|
||||
* 选择下一个块
|
||||
*/
|
||||
BlockSelectNextKeyBindingKey = "blockSelectNext",
|
||||
BlockSelectNext = "blockSelectNext",
|
||||
|
||||
/**
|
||||
* 删除当前块
|
||||
*/
|
||||
BlockDeleteKeyBindingKey = "blockDelete",
|
||||
BlockDelete = "blockDelete",
|
||||
|
||||
/**
|
||||
* 向上移动当前块
|
||||
*/
|
||||
BlockMoveUpKeyBindingKey = "blockMoveUp",
|
||||
BlockMoveUp = "blockMoveUp",
|
||||
|
||||
/**
|
||||
* 向下移动当前块
|
||||
*/
|
||||
BlockMoveDownKeyBindingKey = "blockMoveDown",
|
||||
BlockMoveDown = "blockMoveDown",
|
||||
|
||||
/**
|
||||
* 删除行
|
||||
*/
|
||||
BlockDeleteLineKeyBindingKey = "blockDeleteLine",
|
||||
BlockDeleteLine = "blockDeleteLine",
|
||||
|
||||
/**
|
||||
* 向上移动行
|
||||
*/
|
||||
BlockMoveLineUpKeyBindingKey = "blockMoveLineUp",
|
||||
BlockMoveLineUp = "blockMoveLineUp",
|
||||
|
||||
/**
|
||||
* 向下移动行
|
||||
*/
|
||||
BlockMoveLineDownKeyBindingKey = "blockMoveLineDown",
|
||||
BlockMoveLineDown = "blockMoveLineDown",
|
||||
|
||||
/**
|
||||
* 字符转置
|
||||
*/
|
||||
BlockTransposeCharsKeyBindingKey = "blockTransposeChars",
|
||||
BlockTransposeChars = "blockTransposeChars",
|
||||
|
||||
/**
|
||||
* 格式化代码块
|
||||
*/
|
||||
BlockFormatKeyBindingKey = "blockFormat",
|
||||
BlockFormat = "blockFormat",
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
BlockCopyKeyBindingKey = "blockCopy",
|
||||
BlockCopy = "blockCopy",
|
||||
|
||||
/**
|
||||
* 剪切
|
||||
*/
|
||||
BlockCutKeyBindingKey = "blockCut",
|
||||
BlockCut = "blockCut",
|
||||
|
||||
/**
|
||||
* 粘贴
|
||||
*/
|
||||
BlockPasteKeyBindingKey = "blockPaste",
|
||||
BlockPaste = "blockPaste",
|
||||
|
||||
/**
|
||||
* 折叠代码
|
||||
*/
|
||||
FoldCodeKeyBindingKey = "foldCode",
|
||||
FoldCode = "foldCode",
|
||||
|
||||
/**
|
||||
* 展开代码
|
||||
*/
|
||||
UnfoldCodeKeyBindingKey = "unfoldCode",
|
||||
UnfoldCode = "unfoldCode",
|
||||
|
||||
/**
|
||||
* 折叠全部
|
||||
*/
|
||||
FoldAllKeyBindingKey = "foldAll",
|
||||
FoldAll = "foldAll",
|
||||
|
||||
/**
|
||||
* 展开全部
|
||||
*/
|
||||
UnfoldAllKeyBindingKey = "unfoldAll",
|
||||
UnfoldAll = "unfoldAll",
|
||||
|
||||
/**
|
||||
* 光标按语法左移
|
||||
*/
|
||||
CursorSyntaxLeftKeyBindingKey = "cursorSyntaxLeft",
|
||||
CursorSyntaxLeft = "cursorSyntaxLeft",
|
||||
|
||||
/**
|
||||
* 光标按语法右移
|
||||
*/
|
||||
CursorSyntaxRightKeyBindingKey = "cursorSyntaxRight",
|
||||
CursorSyntaxRight = "cursorSyntaxRight",
|
||||
|
||||
/**
|
||||
* 按语法选择左侧
|
||||
*/
|
||||
SelectSyntaxLeftKeyBindingKey = "selectSyntaxLeft",
|
||||
SelectSyntaxLeft = "selectSyntaxLeft",
|
||||
|
||||
/**
|
||||
* 按语法选择右侧
|
||||
*/
|
||||
SelectSyntaxRightKeyBindingKey = "selectSyntaxRight",
|
||||
SelectSyntaxRight = "selectSyntaxRight",
|
||||
|
||||
/**
|
||||
* 向上复制行
|
||||
*/
|
||||
CopyLineUpKeyBindingKey = "copyLineUp",
|
||||
CopyLineUp = "copyLineUp",
|
||||
|
||||
/**
|
||||
* 向下复制行
|
||||
*/
|
||||
CopyLineDownKeyBindingKey = "copyLineDown",
|
||||
CopyLineDown = "copyLineDown",
|
||||
|
||||
/**
|
||||
* 插入空行
|
||||
*/
|
||||
InsertBlankLineKeyBindingKey = "insertBlankLine",
|
||||
InsertBlankLine = "insertBlankLine",
|
||||
|
||||
/**
|
||||
* 选择行
|
||||
*/
|
||||
SelectLineKeyBindingKey = "selectLine",
|
||||
SelectLine = "selectLine",
|
||||
|
||||
/**
|
||||
* 选择父级语法
|
||||
*/
|
||||
SelectParentSyntaxKeyBindingKey = "selectParentSyntax",
|
||||
SelectParentSyntax = "selectParentSyntax",
|
||||
|
||||
/**
|
||||
* 简化选择
|
||||
*/
|
||||
SimplifySelection = "simplifySelection",
|
||||
|
||||
/**
|
||||
* 在上方添加光标
|
||||
*/
|
||||
AddCursorAbove = "addCursorAbove",
|
||||
|
||||
/**
|
||||
* 在下方添加光标
|
||||
*/
|
||||
AddCursorBelow = "addCursorBelow",
|
||||
|
||||
/**
|
||||
* 光标按单词左移
|
||||
*/
|
||||
CursorGroupLeft = "cursorGroupLeft",
|
||||
|
||||
/**
|
||||
* 光标按单词右移
|
||||
*/
|
||||
CursorGroupRight = "cursorGroupRight",
|
||||
|
||||
/**
|
||||
* 按单词选择左侧
|
||||
*/
|
||||
SelectGroupLeft = "selectGroupLeft",
|
||||
|
||||
/**
|
||||
* 按单词选择右侧
|
||||
*/
|
||||
SelectGroupRight = "selectGroupRight",
|
||||
|
||||
/**
|
||||
* 删除到行尾
|
||||
*/
|
||||
DeleteToLineEnd = "deleteToLineEnd",
|
||||
|
||||
/**
|
||||
* 删除到行首
|
||||
*/
|
||||
DeleteToLineStart = "deleteToLineStart",
|
||||
|
||||
/**
|
||||
* 移动到行首
|
||||
*/
|
||||
CursorLineStart = "cursorLineStart",
|
||||
|
||||
/**
|
||||
* 移动到行尾
|
||||
*/
|
||||
CursorLineEnd = "cursorLineEnd",
|
||||
|
||||
/**
|
||||
* 选择到行首
|
||||
*/
|
||||
SelectLineStart = "selectLineStart",
|
||||
|
||||
/**
|
||||
* 选择到行尾
|
||||
*/
|
||||
SelectLineEnd = "selectLineEnd",
|
||||
|
||||
/**
|
||||
* 跳转到文档开头
|
||||
*/
|
||||
CursorDocStart = "cursorDocStart",
|
||||
|
||||
/**
|
||||
* 跳转到文档结尾
|
||||
*/
|
||||
CursorDocEnd = "cursorDocEnd",
|
||||
|
||||
/**
|
||||
* 选择到文档开头
|
||||
*/
|
||||
SelectDocStart = "selectDocStart",
|
||||
|
||||
/**
|
||||
* 选择到文档结尾
|
||||
*/
|
||||
SelectDocEnd = "selectDocEnd",
|
||||
|
||||
/**
|
||||
* 选择到匹配括号
|
||||
*/
|
||||
SelectMatchingBracket = "selectMatchingBracket",
|
||||
|
||||
/**
|
||||
* 分割行
|
||||
*/
|
||||
SplitLine = "splitLine",
|
||||
|
||||
/**
|
||||
* 光标左移一个字符
|
||||
*/
|
||||
CursorCharLeft = "cursorCharLeft",
|
||||
|
||||
/**
|
||||
* 光标右移一个字符
|
||||
*/
|
||||
CursorCharRight = "cursorCharRight",
|
||||
|
||||
/**
|
||||
* 光标上移一行
|
||||
*/
|
||||
CursorLineUp = "cursorLineUp",
|
||||
|
||||
/**
|
||||
* 光标下移一行
|
||||
*/
|
||||
CursorLineDown = "cursorLineDown",
|
||||
|
||||
/**
|
||||
* 向上翻页
|
||||
*/
|
||||
CursorPageUp = "cursorPageUp",
|
||||
|
||||
/**
|
||||
* 向下翻页
|
||||
*/
|
||||
CursorPageDown = "cursorPageDown",
|
||||
|
||||
/**
|
||||
* 选择左移一个字符
|
||||
*/
|
||||
SelectCharLeft = "selectCharLeft",
|
||||
|
||||
/**
|
||||
* 选择右移一个字符
|
||||
*/
|
||||
SelectCharRight = "selectCharRight",
|
||||
|
||||
/**
|
||||
* 选择上移一行
|
||||
*/
|
||||
SelectLineUp = "selectLineUp",
|
||||
|
||||
/**
|
||||
* 选择下移一行
|
||||
*/
|
||||
SelectLineDown = "selectLineDown",
|
||||
|
||||
/**
|
||||
* 减少缩进
|
||||
*/
|
||||
IndentLessKeyBindingKey = "indentLess",
|
||||
IndentLess = "indentLess",
|
||||
|
||||
/**
|
||||
* 增加缩进
|
||||
*/
|
||||
IndentMoreKeyBindingKey = "indentMore",
|
||||
IndentMore = "indentMore",
|
||||
|
||||
/**
|
||||
* 缩进选择
|
||||
*/
|
||||
IndentSelectionKeyBindingKey = "indentSelection",
|
||||
IndentSelection = "indentSelection",
|
||||
|
||||
/**
|
||||
* 光标到匹配括号
|
||||
*/
|
||||
CursorMatchingBracketKeyBindingKey = "cursorMatchingBracket",
|
||||
CursorMatchingBracket = "cursorMatchingBracket",
|
||||
|
||||
/**
|
||||
* 切换注释
|
||||
*/
|
||||
ToggleCommentKeyBindingKey = "toggleComment",
|
||||
ToggleComment = "toggleComment",
|
||||
|
||||
/**
|
||||
* 切换块注释
|
||||
*/
|
||||
ToggleBlockCommentKeyBindingKey = "toggleBlockComment",
|
||||
ToggleBlockComment = "toggleBlockComment",
|
||||
|
||||
/**
|
||||
* 插入新行并缩进
|
||||
*/
|
||||
InsertNewlineAndIndentKeyBindingKey = "insertNewlineAndIndent",
|
||||
InsertNewlineAndIndent = "insertNewlineAndIndent",
|
||||
|
||||
/**
|
||||
* 向后删除字符
|
||||
*/
|
||||
DeleteCharBackwardKeyBindingKey = "deleteCharBackward",
|
||||
DeleteCharBackward = "deleteCharBackward",
|
||||
|
||||
/**
|
||||
* 向前删除字符
|
||||
*/
|
||||
DeleteCharForwardKeyBindingKey = "deleteCharForward",
|
||||
DeleteCharForward = "deleteCharForward",
|
||||
|
||||
/**
|
||||
* 向后删除组
|
||||
*/
|
||||
DeleteGroupBackwardKeyBindingKey = "deleteGroupBackward",
|
||||
DeleteGroupBackward = "deleteGroupBackward",
|
||||
|
||||
/**
|
||||
* 向前删除组
|
||||
*/
|
||||
DeleteGroupForwardKeyBindingKey = "deleteGroupForward",
|
||||
DeleteGroupForward = "deleteGroupForward",
|
||||
|
||||
/**
|
||||
* 撤销
|
||||
*/
|
||||
HistoryUndoKeyBindingKey = "historyUndo",
|
||||
HistoryUndo = "historyUndo",
|
||||
|
||||
/**
|
||||
* 重做
|
||||
*/
|
||||
HistoryRedoKeyBindingKey = "historyRedo",
|
||||
HistoryRedo = "historyRedo",
|
||||
|
||||
/**
|
||||
* 撤销选择
|
||||
*/
|
||||
HistoryUndoSelectionKeyBindingKey = "historyUndoSelection",
|
||||
HistoryUndoSelection = "historyUndoSelection",
|
||||
|
||||
/**
|
||||
* 重做选择
|
||||
*/
|
||||
HistoryRedoSelectionKeyBindingKey = "historyRedoSelection",
|
||||
HistoryRedoSelection = "historyRedoSelection",
|
||||
|
||||
/**
|
||||
* 复制块为图片
|
||||
*/
|
||||
CopyBlockImage = "copyBlockImage",
|
||||
};
|
||||
|
||||
export enum KeyBindingType {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* standard 标准快捷键
|
||||
*/
|
||||
Standard = "standard",
|
||||
|
||||
/**
|
||||
* emacs 快捷键
|
||||
*/
|
||||
Emacs = "emacs",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1036,26 +1229,6 @@ export enum TabType {
|
||||
TabTypeTab = "tab",
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdateSourceType 更新源类型
|
||||
*/
|
||||
export enum UpdateSourceType {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* UpdateSourceGithub GitHub更新源
|
||||
*/
|
||||
UpdateSourceGithub = "github",
|
||||
|
||||
/**
|
||||
* UpdateSourceGitea Gitea更新源
|
||||
*/
|
||||
UpdateSourceGitea = "gitea",
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdatesConfig 更新设置配置
|
||||
*/
|
||||
@@ -1070,16 +1243,6 @@ export class UpdatesConfig {
|
||||
*/
|
||||
"autoUpdate": boolean;
|
||||
|
||||
/**
|
||||
* 主要更新源
|
||||
*/
|
||||
"primarySource": UpdateSourceType;
|
||||
|
||||
/**
|
||||
* 备用更新源
|
||||
*/
|
||||
"backupSource": UpdateSourceType;
|
||||
|
||||
/**
|
||||
* 更新前是否备份
|
||||
*/
|
||||
@@ -1095,11 +1258,6 @@ export class UpdatesConfig {
|
||||
*/
|
||||
"github": GithubConfig;
|
||||
|
||||
/**
|
||||
* Gitea配置
|
||||
*/
|
||||
"gitea": GiteaConfig;
|
||||
|
||||
/** Creates a new UpdatesConfig instance. */
|
||||
constructor($$source: Partial<UpdatesConfig> = {}) {
|
||||
if (!("version" in $$source)) {
|
||||
@@ -1108,12 +1266,6 @@ export class UpdatesConfig {
|
||||
if (!("autoUpdate" in $$source)) {
|
||||
this["autoUpdate"] = false;
|
||||
}
|
||||
if (!("primarySource" in $$source)) {
|
||||
this["primarySource"] = ("" as UpdateSourceType);
|
||||
}
|
||||
if (!("backupSource" in $$source)) {
|
||||
this["backupSource"] = ("" as UpdateSourceType);
|
||||
}
|
||||
if (!("backupBeforeUpdate" in $$source)) {
|
||||
this["backupBeforeUpdate"] = false;
|
||||
}
|
||||
@@ -1123,9 +1275,6 @@ export class UpdatesConfig {
|
||||
if (!("github" in $$source)) {
|
||||
this["github"] = (new GithubConfig());
|
||||
}
|
||||
if (!("gitea" in $$source)) {
|
||||
this["gitea"] = (new GiteaConfig());
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -1134,14 +1283,10 @@ export class UpdatesConfig {
|
||||
* Creates a new UpdatesConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): UpdatesConfig {
|
||||
const $$createField6_0 = $$createType9;
|
||||
const $$createField7_0 = $$createType10;
|
||||
const $$createField4_0 = $$createType9;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("github" in $$parsedSource) {
|
||||
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
|
||||
}
|
||||
if ("gitea" in $$parsedSource) {
|
||||
$$parsedSource["gitea"] = $$createField7_0($$parsedSource["gitea"]);
|
||||
$$parsedSource["github"] = $$createField4_0($$parsedSource["github"]);
|
||||
}
|
||||
return new UpdatesConfig($$parsedSource as Partial<UpdatesConfig>);
|
||||
}
|
||||
@@ -1163,4 +1308,3 @@ var $$createType6 = (function $$initCreateType6(...args): any {
|
||||
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType8 = HotkeyCombo.createFrom;
|
||||
const $$createType9 = GithubConfig.createFrom;
|
||||
const $$createType10 = GiteaConfig.createFrom;
|
||||
|
||||
@@ -20,35 +20,11 @@ import * as models$0 from "../models/models.js";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetAllExtensions 获取所有扩展
|
||||
*/
|
||||
export function GetAllExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3094292124) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDefaultExtensions 获取默认扩展配置(用于前端绑定生成)
|
||||
*/
|
||||
export function GetDefaultExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4036328166) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType4($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetExtensionByKey 根据Key获取扩展
|
||||
*/
|
||||
export function GetExtensionByKey(key: string): Promise<ent$0.Extension | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2551065776, key) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
@@ -56,11 +32,47 @@ export function GetExtensionByKey(key: string): Promise<ent$0.Extension | null>
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetExtensionByID 根据ID获取扩展
|
||||
*/
|
||||
export function GetExtensionByID(id: number): Promise<ent$0.Extension | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1521424252, id) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType3($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetExtensionConfig 获取扩展配置
|
||||
*/
|
||||
export function GetExtensionConfig(id: number): Promise<{ [_: string]: any }> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1629559882, id) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType4($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetExtensions 获取所有扩展
|
||||
*/
|
||||
export function GetExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3179289021) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType5($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetExtensionConfig 重置单个扩展到默认状态
|
||||
*/
|
||||
export function ResetExtensionConfig(key: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3990780299, key) as any;
|
||||
export function ResetExtensionConfig(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3990780299, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
@@ -83,22 +95,23 @@ export function SyncExtensions(): Promise<void> & { cancel(): void } {
|
||||
/**
|
||||
* UpdateExtensionConfig 更新扩展配置
|
||||
*/
|
||||
export function UpdateExtensionConfig(key: string, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3184142503, key, config) as any;
|
||||
export function UpdateExtensionConfig(id: number, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3184142503, id, config) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateExtensionEnabled 更新扩展启用状态
|
||||
*/
|
||||
export function UpdateExtensionEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1067300094, key, enabled) as any;
|
||||
export function UpdateExtensionEnabled(id: number, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = ent$0.Extension.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
const $$createType3 = models$0.Extension.createFrom;
|
||||
const $$createType4 = $Create.Array($$createType3);
|
||||
const $$createType0 = models$0.Extension.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType2 = ent$0.Extension.createFrom;
|
||||
const $$createType3 = $Create.Nullable($$createType2);
|
||||
const $$createType4 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType5 = $Create.Array($$createType3);
|
||||
|
||||
@@ -21,22 +21,34 @@ import * as models$0 from "../models/models.js";
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetAllKeyBindings 获取所有快捷键
|
||||
* GetDefaultKeyBindings 获取默认快捷键配置
|
||||
*/
|
||||
export function GetAllKeyBindings(): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1633502882) as any;
|
||||
export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3843471588) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDefaultKeyBindings 获取默认快捷键配置
|
||||
* GetKeyBindingByID 根据ID获取快捷键
|
||||
*/
|
||||
export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3843471588) as any;
|
||||
export function GetKeyBindingByID(id: number): Promise<ent$0.KeyBinding | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1578192526, id) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType3($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetKeyBindings 根据类型获取快捷键
|
||||
*/
|
||||
export function GetKeyBindings(kbType: models$0.KeyBindingType): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4253885163, kbType) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType4($result);
|
||||
}) as any;
|
||||
@@ -45,15 +57,11 @@ export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { canc
|
||||
}
|
||||
|
||||
/**
|
||||
* GetKeyBindingByKey 根据Key获取快捷键
|
||||
* ResetKeyBindings 重置所有快捷键到默认值
|
||||
*/
|
||||
export function GetKeyBindingByKey(key: string): Promise<ent$0.KeyBinding | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(852938650, key) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
export function ResetKeyBindings(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4251626010) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,24 +81,32 @@ export function SyncKeyBindings(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateKeyBindingCommand 更新快捷键命令
|
||||
* UpdateKeyBindingEnabled 更新快捷键启用状态
|
||||
*/
|
||||
export function UpdateKeyBindingCommand(key: string, command: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1293670628, key, command) as any;
|
||||
export function UpdateKeyBindingEnabled(id: number, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(843626124, id, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateKeyBindingEnabled 更新快捷键启用状态
|
||||
* UpdateKeyBindingKeys 更新快捷键绑定
|
||||
*/
|
||||
export function UpdateKeyBindingEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(843626124, key, enabled) as any;
|
||||
export function UpdateKeyBindingKeys(id: number, key: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3432755175, id, key) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateKeyBindingPreventDefault 更新快捷键 PreventDefault 状态
|
||||
*/
|
||||
export function UpdateKeyBindingPreventDefault(id: number, preventDefault: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(202386744, id, preventDefault) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = ent$0.KeyBinding.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
const $$createType3 = models$0.KeyBinding.createFrom;
|
||||
const $$createType0 = models$0.KeyBinding.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType2 = ent$0.KeyBinding.createFrom;
|
||||
const $$createType3 = $Create.Nullable($$createType2);
|
||||
const $$createType4 = $Create.Array($$createType3);
|
||||
|
||||
@@ -285,7 +285,7 @@ export class SelfUpdateResult {
|
||||
"error": string;
|
||||
|
||||
/**
|
||||
* 更新源(github/gitea)
|
||||
* 更新源(github)
|
||||
*/
|
||||
"source": string;
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetThemeByKey 根据Key获取主题
|
||||
* GetThemeByName 根据Key获取主题
|
||||
*/
|
||||
export function GetThemeByKey(key: string): Promise<ent$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(808794256, key) as any;
|
||||
export function GetThemeByName(name: string): Promise<ent$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1938954770, name) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
|
||||
4
frontend/components.d.ts
vendored
@@ -11,6 +11,8 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AccordionContainer: typeof import('./src/components/accordion/AccordionContainer.vue')['default']
|
||||
AccordionItem: typeof import('./src/components/accordion/AccordionItem.vue')['default']
|
||||
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']
|
||||
@@ -22,6 +24,8 @@ declare module 'vue' {
|
||||
TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default']
|
||||
TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default']
|
||||
TabItem: typeof import('./src/components/tabs/TabItem.vue')['default']
|
||||
Toast: typeof import('./src/components/toast/Toast.vue')['default']
|
||||
ToastContainer: typeof import('./src/components/toast/ToastContainer.vue')['default']
|
||||
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
|
||||
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
|
||||
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']
|
||||
|
||||
383
frontend/package-lock.json
generated
@@ -30,21 +30,22 @@
|
||||
"@codemirror/lang-vue": "^0.1.3",
|
||||
"@codemirror/lang-wast": "^6.0.2",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.39.4",
|
||||
"@codemirror/state": "^6.5.3",
|
||||
"@codemirror/view": "^6.39.8",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/lr": "^1.4.5",
|
||||
"@prettier/plugin-xml": "^3.4.2",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
"@toml-tools/lexer": "^1.0.0",
|
||||
"@toml-tools/parser": "^1.0.0",
|
||||
"@toml-tools/lexer": "^1.0.1",
|
||||
"@toml-tools/parser": "^1.0.1",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@zumer/snapdom": "^2.0.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.4",
|
||||
@@ -60,9 +61,9 @@
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"prettier": "^3.7.4",
|
||||
"sass": "^1.97.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"sass": "^1.97.1",
|
||||
"vue": "^3.5.26",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
@@ -71,21 +72,20 @@
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^25.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.76",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.77",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"globals": "^16.5.0",
|
||||
"happy-dom": "^20.0.11",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"typescript-eslint": "^8.50.1",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitepress": "^2.0.0-alpha.12",
|
||||
"vitest": "^4.0.16",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.1.8"
|
||||
"vue-tsc": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antfu/install-pkg": {
|
||||
@@ -142,6 +142,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.5"
|
||||
},
|
||||
@@ -524,14 +525,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.11.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.3.tgz",
|
||||
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
||||
"version": "6.12.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.1.tgz",
|
||||
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
@@ -600,18 +601,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.3.tgz",
|
||||
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.4",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.4.tgz",
|
||||
"integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==",
|
||||
"version": "6.39.6",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.6.tgz",
|
||||
"integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
@@ -1340,13 +1341,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.2.tgz",
|
||||
"integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==",
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.7.tgz",
|
||||
"integrity": "sha512-+Ra9I/LAzXDnmv/IrTO03WMCiLya7pHRmGJvNl9fKwx/W4REJ0xaMk2PxCRqnxcBsX443amEMdebQ3R1geiuIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.2.2",
|
||||
"@intlify/shared": "11.2.2"
|
||||
"@intlify/message-compiler": "11.2.7",
|
||||
"@intlify/shared": "11.2.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -1356,12 +1357,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.2.tgz",
|
||||
"integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==",
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.7.tgz",
|
||||
"integrity": "sha512-TFamC+GzJAotAFwUNvbtRVBgvuSn2nCwKNresmPUHv3IIVMmXJt7QQJj/DORI1h8hs46ZF6L0Fs2xBohSOE4iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@intlify/shared": "11.2.7",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1372,9 +1373,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.2.tgz",
|
||||
"integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==",
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.7.tgz",
|
||||
"integrity": "sha512-uvlkvc/0uQ4FDlHQZccpUnmcOwNcaI3i+69ck2YJ+GqM35AoVbuS63b+YfirV4G0SZh64Ij2UMcFRMmB4nr95w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -1433,9 +1434,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.3.0.tgz",
|
||||
"integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.0.tgz",
|
||||
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/cpp": {
|
||||
@@ -2867,17 +2868,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
|
||||
"integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz",
|
||||
"integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/type-utils": "8.50.0",
|
||||
"@typescript-eslint/utils": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.1",
|
||||
"@typescript-eslint/type-utils": "8.50.1",
|
||||
"@typescript-eslint/utils": "8.50.1",
|
||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
@@ -2890,7 +2891,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.50.0",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -2906,16 +2907,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.50.0.tgz",
|
||||
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.50.1.tgz",
|
||||
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.1",
|
||||
"@typescript-eslint/types": "8.50.1",
|
||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2931,14 +2932,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
|
||||
"integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.50.1.tgz",
|
||||
"integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.50.0",
|
||||
"@typescript-eslint/types": "^8.50.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.50.1",
|
||||
"@typescript-eslint/types": "^8.50.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2953,14 +2954,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
|
||||
"integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz",
|
||||
"integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0"
|
||||
"@typescript-eslint/types": "8.50.1",
|
||||
"@typescript-eslint/visitor-keys": "8.50.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2971,9 +2972,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
|
||||
"integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz",
|
||||
"integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2988,15 +2989,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
|
||||
"integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz",
|
||||
"integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
||||
"@typescript-eslint/utils": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.1",
|
||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
||||
"@typescript-eslint/utils": "8.50.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -3013,9 +3014,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.50.0.tgz",
|
||||
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.50.1.tgz",
|
||||
"integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3027,16 +3028,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
|
||||
"integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz",
|
||||
"integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.50.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"@typescript-eslint/project-service": "8.50.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.50.1",
|
||||
"@typescript-eslint/types": "8.50.1",
|
||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
||||
"debug": "^4.3.4",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
@@ -3081,16 +3082,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.50.0.tgz",
|
||||
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.50.1.tgz",
|
||||
"integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0"
|
||||
"@typescript-eslint/scope-manager": "8.50.1",
|
||||
"@typescript-eslint/types": "8.50.1",
|
||||
"@typescript-eslint/typescript-estree": "8.50.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3105,13 +3106,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
|
||||
"integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz",
|
||||
"integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.1",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3268,68 +3269,68 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "2.4.26",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.26.tgz",
|
||||
"integrity": "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==",
|
||||
"version": "2.4.27",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.27.tgz",
|
||||
"integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/source-map": "2.4.26"
|
||||
"@volar/source-map": "2.4.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "2.4.26",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.26.tgz",
|
||||
"integrity": "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==",
|
||||
"version": "2.4.27",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.27.tgz",
|
||||
"integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "2.4.26",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.26.tgz",
|
||||
"integrity": "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==",
|
||||
"version": "2.4.27",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.27.tgz",
|
||||
"integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.26",
|
||||
"@volar/language-core": "2.4.27",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vscode-uri": "^3.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
||||
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
|
||||
"integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.25",
|
||||
"entities": "^4.5.0",
|
||||
"@vue/shared": "3.5.26",
|
||||
"entities": "^7.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
|
||||
"integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-core": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz",
|
||||
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
|
||||
"integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"@vue/compiler-core": "3.5.26",
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-ssr": "3.5.26",
|
||||
"@vue/shared": "3.5.26",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
@@ -3337,13 +3338,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
|
||||
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
|
||||
"integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
@@ -3380,27 +3381,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.1.8.tgz",
|
||||
"integrity": "sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.2.1.tgz",
|
||||
"integrity": "sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.26",
|
||||
"@volar/language-core": "2.4.27",
|
||||
"@vue/compiler-dom": "^3.5.0",
|
||||
"@vue/shared": "^3.5.0",
|
||||
"alien-signals": "^3.0.0",
|
||||
"muggle-string": "^0.4.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/picomatch": {
|
||||
@@ -3417,53 +3410,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.25.tgz",
|
||||
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.26.tgz",
|
||||
"integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.25.tgz",
|
||||
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.26.tgz",
|
||||
"integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/reactivity": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz",
|
||||
"integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/runtime-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"csstype": "^3.1.3"
|
||||
"@vue/reactivity": "3.5.26",
|
||||
"@vue/runtime-core": "3.5.26",
|
||||
"@vue/shared": "3.5.26",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.25.tgz",
|
||||
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.26.tgz",
|
||||
"integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-ssr": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.25"
|
||||
"vue": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.25.tgz",
|
||||
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.26.tgz",
|
||||
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
@@ -3599,6 +3592,12 @@
|
||||
"regexp-to-ast": "0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zumer/snapdom": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@zumer/snapdom/-/snapdom-2.0.1.tgz",
|
||||
"integrity": "sha512-78/qbYl2FTv4H6qaXcNfAujfIOSzdvs83NW63VbyC9QA3sqNPfPvhn4xYMO6Gy11hXwJUEhd0z65yKiNzDwy9w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -5168,9 +5167,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.0.tgz",
|
||||
"integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
@@ -7656,9 +7655,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.0.tgz",
|
||||
"integrity": "sha512-KR0igP1z4avUJetEuIeOdDlwaUDvkH8wSx7FdSjyYBS3dpyX3TzHfAMO0G1Q4/3cdjcmi3r7idh+KCmKqS+KeQ==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.1.tgz",
|
||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
@@ -8232,16 +8231,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.50.0.tgz",
|
||||
"integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==",
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.50.1.tgz",
|
||||
"integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||
"@typescript-eslint/parser": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
||||
"@typescript-eslint/utils": "8.50.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
||||
"@typescript-eslint/utils": "8.50.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -8902,16 +8901,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz",
|
||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz",
|
||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
"@vue/runtime-dom": "3.5.25",
|
||||
"@vue/server-renderer": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-sfc": "3.5.26",
|
||||
"@vue/runtime-dom": "3.5.26",
|
||||
"@vue/server-renderer": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -8947,13 +8946,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.2.tgz",
|
||||
"integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==",
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.7.tgz",
|
||||
"integrity": "sha512-LPv8bAY5OA0UvFEXl4vBQOBqJzRrlExy92tWgRuwW7tbykHf7CH71G2Y4TM2OwGcIS4+hyqKHS2EVBqaYwPY9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.2.2",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@intlify/core-base": "11.2.7",
|
||||
"@intlify/shared": "11.2.7",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -9007,14 +9006,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.1.8.tgz",
|
||||
"integrity": "sha512-deKgwx6exIHeZwF601P1ktZKNF0bepaSN4jBU3AsbldPx9gylUc1JDxYppl82yxgkAgaz0Y0LCLOi+cXe9HMYA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.2.1.tgz",
|
||||
"integrity": "sha512-I23Rk8dkQfmcSbxDO0dmg9ioMLjKA1pjlU3Lz6Jfk2pMGu3Uryu9810XkcZH24IzPbhzPCnkKo2rEMRX0skSrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/typescript": "2.4.26",
|
||||
"@vue/language-core": "3.1.8"
|
||||
"@volar/typescript": "2.4.27",
|
||||
"@vue/language-core": "3.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"vue-tsc": "bin/vue-tsc.js"
|
||||
|
||||
@@ -44,21 +44,22 @@
|
||||
"@codemirror/lang-vue": "^0.1.3",
|
||||
"@codemirror/lang-wast": "^6.0.2",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.39.4",
|
||||
"@codemirror/state": "^6.5.3",
|
||||
"@codemirror/view": "^6.39.8",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/lr": "^1.4.5",
|
||||
"@prettier/plugin-xml": "^3.4.2",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
"@toml-tools/lexer": "^1.0.0",
|
||||
"@toml-tools/parser": "^1.0.0",
|
||||
"@toml-tools/lexer": "^1.0.1",
|
||||
"@toml-tools/parser": "^1.0.1",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@zumer/snapdom": "^2.0.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.4",
|
||||
@@ -74,9 +75,9 @@
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"prettier": "^3.7.4",
|
||||
"sass": "^1.97.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"sass": "^1.97.1",
|
||||
"vue": "^3.5.26",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
@@ -85,21 +86,20 @@
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^25.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.76",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.77",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"globals": "^16.5.0",
|
||||
"happy-dom": "^20.0.11",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"typescript-eslint": "^8.51.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitepress": "^2.0.0-alpha.12",
|
||||
"vitest": "^4.0.16",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.1.8"
|
||||
"vue-tsc": "^3.2.1"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@latest"
|
||||
|
||||
1
frontend/public/images/blockImage.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366893329" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="106998" width="200" height="200"><path d="M344.064 36.571429v95.085714H155.428571a23.771429 23.771429 0 0 0-23.259428 19.017143l-0.512 4.754285-0.073143 509.952L306.176 505.417143a95.085714 95.085714 0 0 1 121.270857-5.997714l4.827429 3.876571 152.137143 130.413714 102.546285-102.4a95.085714 95.085714 0 0 1 119.003429-12.507428l5.266286 3.657143 81.042285 60.781714 0.073143-118.784 0.512-7.021714a47.542857 47.542857 0 0 1 94.061714 0l0.512 7.021714v404.114286c0 62.171429-47.762286 113.225143-108.617142 118.418285l-10.24 0.438857h-713.142858l-10.24-0.438857a118.857143 118.857143 0 0 1-108.105142-107.52L36.571429 868.498286v-713.142857l0.438857-10.24A118.857143 118.857143 0 0 1 155.428571 36.571429h188.708572z m26.331429 538.916571L131.657143 794.404571l0.073143 74.166858c0 11.483429 8.118857 21.065143 19.017143 23.259428l4.754285 0.512h713.142857a23.771429 23.771429 0 0 0 23.259429-19.017143l0.512-4.754285-0.073143-166.473143-138.093714-103.570286-97.353143 97.28 76.288 65.316571a47.542857 47.542857 0 0 1-58.002286 75.190858l-3.876571-2.925715-300.836572-257.901714zM649.069714 60.269714a47.542857 47.542857 0 0 1 3.291429 63.634286l-3.291429 3.657143-61.44 61.44 61.44 61.44a47.542857 47.542857 0 0 1 3.291429 63.634286l-3.291429 3.657142a47.542857 47.542857 0 0 1-63.634285 3.218286l-3.584-3.291428-95.085715-95.085715a47.542857 47.542857 0 0 1-3.291428-63.634285l3.291428-3.584 95.085715-95.085715a47.542857 47.542857 0 0 1 67.291428 0zM855.259429 57.051429l3.584 3.218285 95.085714 95.085715a47.542857 47.542857 0 0 1 3.291428 63.634285l-3.291428 3.657143-95.085714 95.085714a47.542857 47.542857 0 0 1-70.509715-63.634285l3.291429-3.657143 61.44-61.44-61.44-61.44a47.542857 47.542857 0 0 1-3.291429-63.634286l3.291429-3.657143a47.542857 47.542857 0 0 1 63.634286-3.218285zM344.210286 36.571429a47.542857 47.542857 0 0 1 7.021714 94.573714l-7.021714 0.512V36.571429z" p-id="106999" fill="#e0620d"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
frontend/public/images/colorSelector.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767367606621" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15611" width="200" height="200"><path d="M511.966509 0.066982c96.036384 0 185.886507 26.451603 262.724147 72.443261L767.949763 68.670494l-127.948963 221.638835A254.788666 254.788666 0 0 0 511.966509 256.050237V0.066982z" fill="#E70212" p-id="15612"></path><path d="M767.949763 68.670494a509.577332 509.577332 0 0 1 191.304819 194.120635L955.329506 256.050237l-221.638835 127.991627a257.263171 257.263171 0 0 0-93.689871-93.689871L767.949763 68.670494z" fill="#EA6101" p-id="15613"></path><path d="M955.329506 256.050237A509.577332 509.577332 0 0 1 1023.933018 519.798317V512.033491h-255.983255a254.788666 254.788666 0 0 0-34.259092-127.991627l221.638835-127.991627z" fill="#F39801" p-id="15614"></path><path d="M1023.933018 512.033491c0 90.020778-23.251812 174.665907-64.038478 248.175765l-4.565034 7.80749-221.638835-127.948964c21.758577-37.672202 34.259092-81.402675 34.259092-128.034291v-0.042664L1023.933018 512.033491z" fill="#FCC902" p-id="15615"></path><path d="M733.690671 640.025118l221.638835 127.991628a509.66266 509.66266 0 0 1-179.52959 182.900035l-7.850153 4.479707-127.991627-221.638835A257.263171 257.263171 0 0 0 733.690671 640.025118z" fill="#FEF200" p-id="15616"></path><path d="M640.0008 733.757653L767.949763 955.396488A509.66266 509.66266 0 0 1 521.011251 1024H511.966509v-255.983254a254.788666 254.788666 0 0 0 128.034291-34.259093z" fill="#90C320" p-id="15617"></path><path d="M511.966509 768.016746v255.983254c-90.020778 0-174.665907-23.251812-248.175765-64.038477L255.983254 955.396488l127.991628-221.638835c37.672202 21.758577 81.402675 34.259092 128.034291 34.259093z" fill="#019A44" p-id="15618"></path><path d="M383.974882 733.757653l-127.991628 221.638835a509.66266 509.66266 0 0 1-182.900035-179.529589L68.603512 768.016746l221.638835-127.991628A257.263171 257.263171 0 0 0 383.974882 733.757653z" fill="#019E97" p-id="15619"></path><path d="M255.983254 512.033491c0 46.631616 12.457852 90.362089 34.259093 128.034291L68.603512 768.016746A509.66266 509.66266 0 0 1 0 521.078233V512.033491h255.983254z" fill="#0169B8" p-id="15620"></path><path d="M68.603512 256.050237l221.638835 127.991627A254.788666 254.788666 0 0 0 255.983254 512.033491H0c0-90.020778 23.251812-174.665907 64.038477-248.175765L68.603512 256.050237z" fill="#1C2089" p-id="15621"></path><path d="M262.681483 64.745418L255.983254 68.670494l127.991628 221.638835A257.263171 257.263171 0 0 0 290.242347 384.041864L68.603512 256.050237a509.577332 509.577332 0 0 1 194.120635-191.304819z" fill="#621988" p-id="15622"></path><path d="M519.731334 0.066982H511.966509v255.983255a254.788666 254.788666 0 0 0-128.034291 34.259092L255.983254 68.670494A509.577332 509.577332 0 0 1 519.731334 0.066982z" fill="#910783" p-id="15623"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
frontend/public/images/contextMenu.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366808037" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="103196" width="200" height="200"><path d="M878.921143 96c27.099429 0 49.078857 21.942857 49.078857 49.078857v349.842286c0 27.099429-21.942857 49.078857-49.078857 49.078857h-221.842286c-27.099429 0-49.078857-21.942857-49.078857-49.078857V145.078857c0-27.099429 21.942857-49.078857 49.078857-49.078857z m-14.921143 320h-192v64h192v-64z m0-128h-192v64h192v-64z m0-128h-192v64h192v-64zM384 309.321143A202.678857 202.678857 0 0 1 586.678857 512v213.321143a202.678857 202.678857 0 0 1-202.678857 202.678857H298.678857a202.678857 202.678857 0 0 1-202.678857-202.678857V512a202.678857 202.678857 0 0 1 202.678857-202.678857z m138.642286 298.642286H160v117.394285a138.678857 138.678857 0 0 0 131.547429 138.459429l7.131428 0.182857H384a138.678857 138.678857 0 0 0 138.678857-138.678857l-0.036571-117.357714z m-213.321143-234.642286h-10.642286A138.678857 138.678857 0 0 0 160 512v31.963429h149.321143v-170.642286z" p-id="103197" fill="#8992c8"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
frontend/public/images/fold.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366707029" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="100833" width="200" height="200"><path d="M533.333 54c16.138 0 29.75 12.016 31.753 28.03l17.828 142.569L896 224.6c17.496 0 31.713 14.042 31.996 31.47l0.004 0.53V939c0 17.673-14.327 32-32 32H469.735a31.94 31.94 0 0 1-3.051-0.102l-0.09-0.009c-10.834-0.868-19.434-6.86-24.45-14.999a31.766 31.766 0 0 1-3.374-7.39 32.348 32.348 0 0 1-1.405-7.246L419.752 800.4H128c-17.496 0-31.713-14.042-31.996-31.47L96 768.4V86c0-17.673 14.327-32 32-32zM864 288.599H590.917l13.33 106.6L704 395.2c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004-91.75-0.001 13.331 106.6L704 565.8c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004-70.416-0.001 16.548 132.332 0.287 2.298c0.024 0.188 0.046 0.376 0.066 0.565 0.986 9.171-2.002 17.793-7.523 24.232l-0.217 0.25L539.872 907H864v-618.4zM548.127 800.4H484.25l7.985 63.851 55.892-63.851zM505.085 118H160v618.4h287.598c0.302-0.004 0.603-0.004 0.904 0h133.913l-0.001-0.004L569.01 629.21l-3.386-27.076c-0.03-0.225-0.058-0.45-0.084-0.676l-21.256-169.977c-0.03-0.219-0.056-0.438-0.081-0.659L505.085 118zM448 565.8c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h192z m-21.333-170.6c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.471 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h170.667z m-21.334-170.6c17.673 0 32 14.327 32 32 0 17.496-14.041 31.713-31.47 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h149.333z" p-id="100834" fill="#1aaba8"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
frontend/public/images/highlightTrailingWhitespace.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767367226608" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5585" width="200" height="200"><path d="M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z" fill="#a4579d" p-id="5586"></path></svg>
|
||||
|
After Width: | Height: | Size: 330 B |
1
frontend/public/images/highlightWhitespace.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767366207284" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="95138" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M753.5 130.4v763.3h70.1v66.4H477v-66.4h70.1V547.1H443.9c-44.2 0-84.8-11.1-121.7-33.2-36.9-22.1-66.4-51.6-88.5-88.5s-33.2-76.8-33.2-119.8 11.1-83.6 33.2-121.7c22.1-38.1 51.6-67.6 88.5-88.5s77.4-31.3 121.7-31.3h379.8v66.4h-70.1z m-206.5 0H443.8c-49.2 0-90.3 17.2-123.5 51.6-33.2 34.4-49.8 75.6-49.8 123.5s16.6 88.5 49.8 121.7c33.2 33.2 74.4 49.8 123.5 49.8H547V130.4z m140.1 0H617v763.3h70.1V130.4z" p-id="95139"></path></svg>
|
||||
|
After Width: | Height: | Size: 758 B |
1
frontend/public/images/httpClient.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365935803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="81379" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M896 64a64 64 0 0 1 64 64v613.952l-121.28-95.68a83.2 83.2 0 0 0-110.848 7.04l-6.016 6.784-5.312 7.616a83.2 83.2 0 0 0-12.032 34.56l-0.064 1.728H595.2A83.2 83.2 0 0 0 512 787.2v44.8H128a64 64 0 0 1-64-64V128a64 64 0 0 1 64-64h768z" fill="#B5E3CC" p-id="81380"></path><path d="M640 256a32 32 0 1 1 0 64h-32v224a32 32 0 0 1-64 0V320h-64v224a32 32 0 0 1-64 0V320H384a32 32 0 0 1 0-64h256z m160 0a96 96 0 0 1 0 192H768v96a32 32 0 0 1-26.24 31.488L736 576a32 32 0 0 1-32-32v-256a32 32 0 0 1 32-32h64zM768 384h32a32 32 0 1 0 0-64H768v64zM288 256a32 32 0 0 1 32 32v256a32 32 0 0 1-64 0V448H192v96a32 32 0 0 1-64 0v-256a32 32 0 0 1 64 0V384h64V288a32 32 0 0 1 32-32zM772.096 699.712a19.2 19.2 0 0 0-4.096 11.904v56.32L595.2 768a19.2 19.2 0 0 0-19.2 19.2v89.6a19.2 19.2 0 0 0 19.2 19.2H768v56.384a19.2 19.2 0 0 0 31.104 15.104l152.576-120.384a19.2 19.2 0 0 0 0-30.208l-152.576-120.32a19.2 19.2 0 0 0-27.008 3.136z" fill="#129250" p-id="81381"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/images/hyperlink.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365120766" class="icon" viewBox="0 0 1160 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12589" xmlns:xlink="http://www.w3.org/1999/xlink" width="226.5625" height="200"><path d="M398.5408 736.142222l435.768889-384.568889a44.009244 44.009244 0 0 0 0-67.675022 59.483022 59.483022 0 0 0-76.731733 0l-435.768889 384.568889a43.872711 43.872711 0 0 0 0 67.675022 59.164444 59.164444 0 0 0 76.731733 0z m143.7696-66.582755a39.867733 39.867733 0 0 1 9.102222 25.031111 38.866489 38.866489 0 0 1-13.653333 30.128355L308.929422 926.151111a52.8384 52.8384 0 0 1-68.266666 0L111.047111 811.781689a39.139556 39.139556 0 0 1 0-60.302222l228.192711-201.9328a50.335289 50.335289 0 0 1 34.178845-11.969423 53.748622 53.748622 0 0 1 22.755555 4.551112l69.632-60.848356c-57.617067-41.824711-141.7216-38.365867-194.696533 7.964444L42.052267 690.631111c-56.069689 50.3808-56.069689 131.117511 0 181.4528l130.207289 114.323911a158.651733 158.651733 0 0 0 204.8 0L605.297778 785.066667c54.613333-48.196267 57.025422-125.474133 5.779911-176.355556zM1117.980444 151.916089L988.410311 37.546667c-56.797867-50.062222-148.821333-50.062222-205.664711 0L554.552889 238.933333c-52.519822 46.739911-56.388267 120.968533-9.102222 171.804445L614.4 349.889422a41.688178 41.688178 0 0 1-5.142756-20.48 38.866489 38.866489 0 0 1 13.653334-30.128355l228.875378-201.386667a50.335289 50.335289 0 0 1 34.178844-12.515556 52.383289 52.383289 0 0 1 34.178844 12.515556l129.570134 114.323911a39.139556 39.139556 0 0 1 0 60.302222l-228.192711 201.9328a50.335289 50.335289 0 0 1-34.178845 11.969423 57.7536 57.7536 0 0 1-28.353422-7.418312l-68.266667 60.302223c57.389511 45.101511 144.543289 43.099022 199.202134-4.551111l228.192711-201.9328a117.418667 117.418667 0 0 0 0-180.906667z" fill="#1A97F0" p-id="12590"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
frontend/public/images/lineNumbers.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366749042" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="102155" width="200" height="200"><path d="M426.7008 256c0-23.552 19.0976-42.6496 42.6496-42.6496h384a42.6496 42.6496 0 1 1 0 85.2992h-384A42.6496 42.6496 0 0 1 426.7008 256zM426.7008 512c0-23.552 19.0976-42.6496 42.6496-42.6496h384a42.6496 42.6496 0 1 1 0 85.2992h-384A42.6496 42.6496 0 0 1 426.7008 512zM469.2992 768c0-23.552 19.1488-42.6496 42.7008-42.6496h341.2992a42.6496 42.6496 0 0 1 0 85.2992H512A42.6496 42.6496 0 0 1 469.2992 768zM256 640a42.6496 42.6496 0 0 0-42.6496 42.6496 42.6496 42.6496 0 0 1-85.3504 0 128 128 0 1 1 256 0c0 25.856-11.264 45.6192-22.4256 59.8528-8.0384 10.1888-18.5856 20.48-26.8288 28.5184l-5.888 5.8368a42.4448 42.4448 0 0 1-2.8672 2.56l-37.4784 31.232h52.8384a42.6496 42.6496 0 1 1 0 85.3504H170.6496a42.6496 42.6496 0 0 1-27.2896-75.4688l126.5152-105.472 6.5024-6.3488 0.4096-0.4096 7.2704-7.168c4.608-4.608 7.936-8.192 10.3936-11.3664a28.672 28.672 0 0 0 3.9424-6.0928c0.3072-0.7168 0.256-0.9728 0.256-1.024A42.6496 42.6496 0 0 0 256 640zM272.3328 131.2256a42.6496 42.6496 0 0 1 26.3168 39.424v256a42.6496 42.6496 0 0 1-85.2992 0V273.664l-12.4928 12.4928a42.6496 42.6496 0 1 1-60.3648-60.3136l85.3504-85.3504a42.6496 42.6496 0 0 1 46.4896-9.216z" p-id="102156" fill="#87c38f"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/images/markdown.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365625477" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="51837" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M98.649225 16.619164h734.636904v990.162784h-734.636904z" fill="#FF8A90" p-id="51838"></path><path d="M881.197231 128.411736h-31.940735V32.589531c0-17.642265-14.29847-31.940735-31.940735-31.940735H114.619592c-17.642265 0-31.940735 14.29847-31.940735 31.940735v958.222049c0 17.642265 14.29847 31.940735 31.940735 31.940735h702.696169c17.642265 0 31.940735-14.29847 31.940735-31.940735V415.878351h31.940735c17.642265 0 31.940735-14.29847 31.940735-31.940735V160.352471c0-17.642265-14.29847-31.940735-31.940735-31.940735z m-63.88147 0h-383.288819c-17.642265 0-31.940735 14.29847-31.940735 31.940735V383.937616c0 17.642265 14.29847 31.940735 31.940735 31.940735h383.288819v574.933229H114.619592v-958.222049h702.696169v95.822205z" fill="#2B3139" p-id="51839"></path><path d="M434.026942 160.352471h447.170289V383.937616h-447.170289z" fill="#FFFFFF" p-id="51840"></path><path d="M544.808889 215.175748h49.969783l16.294765 67.025636h0.311922l16.294765-67.025636h49.957306v113.963544h-33.18842v-73.101861h-0.336875l-19.80076 73.101861h-26.176431l-19.788283-73.101861h-0.336875v73.101861h-33.200897zM698.947889 215.175748h57.618092c37.992007 0 51.392143 28.097865 51.392143 56.819573 0 34.947656-18.490691 57.143971-58.241934 57.143971h-50.768301v-113.963544z" fill="#2B3139" p-id="51841"></path><path d="M726.584111 299.918511h13.712058c21.85944 0 25.065991-17.717126 25.065991-28.409787 0-7.174189-2.233356-27.112194-27.611269-27.112194h-11.154303v55.521981z" fill="#FFFFFF" p-id="51842"></path><path d="M630.512369 785.841895l-94.998733-129.709328h57.318647V544.888976h76.445658v111.243591h57.30617L630.487415 785.841895z" fill="#1EB9B0" p-id="51843"></path><path d="M508.139428 785.754557l-76.445657 0.087338v-120.439029l-57.331124 77.194268-57.343601-77.194268v120.439029H240.573389V544.976313h76.445657l57.343601 80.288528 57.331124-80.288528 76.445657-0.087337z" fill="#1EB9B0" p-id="51844"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
frontend/public/images/minimap.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767367796950" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18541" width="200" height="200"><path d="M880.489336 512H993.882278v455.107765A56.786824 56.786824 0 0 1 937.185807 1024h-56.681412V512z" fill="#4AC3BB" fill-opacity=".603" p-id="18542"></path><path d="M143.510513 1024a113.182118 113.182118 0 0 1-80.188235-33.325176A113.980235 113.980235 0 0 1 30.117572 910.215529V113.784471C30.117572 83.606588 42.059219 54.663529 63.322278 33.325176A113.182118 113.182118 0 0 1 143.510513 0h510.238118C778.977807 0 880.489336 101.872941 880.489336 227.553882v739.553883A56.786824 56.786824 0 0 0 937.185807 1024H143.510513z" fill="#4AC3BB" p-id="18543"></path><path d="M575.653572 335.329882c64.331294 65.024 66.529882 169.758118 4.954353 237.477647l-4.954353 5.240471-98.063059 122.473412a28.175059 28.175059 0 0 1-21.985882 10.586353 28.175059 28.175059 0 0 1-21.985883-10.586353l-98.078117-122.473412-4.412236-4.638118c-63.503059-68.487529-60.777412-175.841882 6.098824-240.941176a168.478118 168.478118 0 0 1 238.426353 2.861176z m-120.048941 64.150589a56.470588 56.470588 0 0 0-49.016471 28.611764 57.735529 57.735529 0 0 0 0 57.193412 56.470588 56.470588 0 0 0 49.016471 28.611765c31.247059-0.015059 56.576-25.630118 56.576-57.22353 0-31.578353-25.328941-57.193412-56.576-57.193411z" fill="#FFFFFF" fill-opacity=".95" p-id="18544"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/public/images/rainbowBrackets.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767364585914" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8006" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960 128c0-35.36-28.704-64.224-63.936-61.728A896.48 896.48 0 0 0 326.4 326.4a896.48 896.48 0 0 0-260.128 569.664C63.776 931.296 92.64 960 128 960c0 0 19.52 0.576 32 0 64-272 84.48-383.68 234.24-533.76C544.32 276.16 752 192 960 160V128z" fill="#F8312F" p-id="8007"></path><path d="M960 256V160a800 800 0 0 0-565.76 234.24A801.344 801.344 0 0 0 160 960h96c32-128 106.24-333.76 238.08-465.92C625.92 362.24 752 304 960 256z" fill="#FFB02E" p-id="8008"></path><path d="M960 256v96c-160 16-284.16 96-398.08 209.92C448 676.16 384 800 352 960H256c0-186.56 74.24-365.76 206.08-497.92A704.32 704.32 0 0 1 960 256z" fill="#FFF478" p-id="8009"></path><path d="M630.08 630.08C534.08 726.08 480 832 448 960h-96c0-161.28 64-315.84 177.92-430.08C643.84 416 798.72 352 960 352v96c-144 32-233.92 86.08-329.92 182.08z" fill="#00D26A" p-id="8010"></path><path d="M960 544v-96c-135.68 0-265.92 54.08-361.92 150.08-96 96-150.08 226.24-150.08 361.92h96c0-110.4 43.84-216.32 121.92-294.08C744 587.84 849.92 544 960 544z" fill="#3F5FFF" p-id="8011"></path><path d="M960 576c0 35.36-28.928 63.36-63.584 70.336a320.384 320.384 0 0 0-250.112 250.08C639.36 931.04 611.392 960 576 960h-32c0-110.4 43.84-216.32 121.92-294.08C744 587.84 849.92 544 960 544v32z" fill="#8D65C5" p-id="8012"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
frontend/public/images/search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366992790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="111373" width="200" height="200"><path d="M581.973333 846.933333a380.8 380.8 0 1 1 380.8-380.8A381.226667 381.226667 0 0 1 581.973333 846.933333z m0-688a307.2 307.2 0 1 0 307.2 307.2 307.413333 307.413333 0 0 0-307.2-307.2z" fill="#FA6302" p-id="111374"></path><path d="M146.56 938.666667a36.906667 36.906667 0 0 1-26.026667-64l192-190.933334a36.906667 36.906667 0 0 1 52.053334 52.266667l-192 192a37.333333 37.333333 0 0 1-26.026667 10.666667z" fill="#43D7B4" p-id="111375"></path><path d="M470.826667 274.773333m-49.066667 0a49.066667 49.066667 0 1 0 98.133333 0 49.066667 49.066667 0 1 0-98.133333 0Z" fill="#43D7B4" p-id="111376"></path><path d="M312.106667 684.8l-23.68 23.466667A388.693333 388.693333 0 0 0 341.333333 760.32l23.466667-23.253333a36.906667 36.906667 0 0 0-52.053333-52.266667z" fill="#425300" p-id="111377"></path></svg>
|
||||
|
After Width: | Height: | Size: 956 B |
1
frontend/public/images/translator.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
@@ -6,7 +6,10 @@ import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||
import {useThemeStore} from '@/stores/themeStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||
import ToastContainer from '@/components/toast/ToastContainer.vue';
|
||||
import {useTranslationStore} from "@/stores/translationStore";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {LanguageType} from "../bindings/voidraft/internal/models";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const systemStore = useSystemStore();
|
||||
@@ -14,6 +17,7 @@ const keybindingStore = useKeybindingStore();
|
||||
const themeStore = useThemeStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const translationStore = useTranslationStore();
|
||||
const {locale} = useI18n();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
// 并行初始化配置、系统信息和快捷键配置
|
||||
@@ -23,8 +27,7 @@ onBeforeMount(async () => {
|
||||
keybindingStore.loadKeyBindings(),
|
||||
]);
|
||||
|
||||
// 初始化语言和主题
|
||||
await configStore.initLanguage();
|
||||
locale.value = configStore.config.appearance.language || LanguageType.LangEnUS;
|
||||
await themeStore.initTheme();
|
||||
await translationStore.loadTranslators();
|
||||
|
||||
@@ -39,6 +42,7 @@ onBeforeMount(async () => {
|
||||
<div class="app-content">
|
||||
<router-view/>
|
||||
</div>
|
||||
<ToastContainer/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
1
frontend/src/assets/images/translator.svg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
AppConfig,
|
||||
AuthMethod,
|
||||
KeyBindingType,
|
||||
LanguageType,
|
||||
SystemThemeType,
|
||||
TabType,
|
||||
UpdateSourceType
|
||||
} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {FONT_OPTIONS} from './fonts';
|
||||
|
||||
@@ -23,6 +23,7 @@ export const CONFIG_KEY_MAP = {
|
||||
enableWindowSnap: 'general.enableWindowSnap',
|
||||
enableLoadingAnimation: 'general.enableLoadingAnimation',
|
||||
enableTabs: 'general.enableTabs',
|
||||
enableMemoryMonitor: 'general.enableMemoryMonitor',
|
||||
// editing
|
||||
fontSize: 'editing.fontSize',
|
||||
fontFamily: 'editing.fontFamily',
|
||||
@@ -31,6 +32,7 @@ export const CONFIG_KEY_MAP = {
|
||||
enableTabIndent: 'editing.enableTabIndent',
|
||||
tabSize: 'editing.tabSize',
|
||||
tabType: 'editing.tabType',
|
||||
keymapMode: 'editing.keymapMode',
|
||||
autoSaveDelay: 'editing.autoSaveDelay',
|
||||
// appearance
|
||||
language: 'appearance.language',
|
||||
@@ -86,6 +88,7 @@ export const DEFAULT_CONFIG: AppConfig = {
|
||||
enableWindowSnap: true,
|
||||
enableLoadingAnimation: true,
|
||||
enableTabs: false,
|
||||
enableMemoryMonitor: true,
|
||||
},
|
||||
editing: {
|
||||
fontSize: CONFIG_LIMITS.fontSize.default,
|
||||
@@ -95,29 +98,23 @@ export const DEFAULT_CONFIG: AppConfig = {
|
||||
enableTabIndent: true,
|
||||
tabSize: CONFIG_LIMITS.tabSize.default,
|
||||
tabType: CONFIG_LIMITS.tabType.default,
|
||||
keymapMode: KeyBindingType.Standard,
|
||||
autoSaveDelay: 5000
|
||||
},
|
||||
appearance: {
|
||||
language: LanguageType.LangZhCN,
|
||||
systemTheme: SystemThemeType.SystemThemeAuto,
|
||||
systemTheme: SystemThemeType.SystemThemeDark,
|
||||
currentTheme: 'default-dark'
|
||||
},
|
||||
updates: {
|
||||
version: "1.0.0",
|
||||
autoUpdate: true,
|
||||
primarySource: UpdateSourceType.UpdateSourceGithub,
|
||||
backupSource: UpdateSourceType.UpdateSourceGitea,
|
||||
backupBeforeUpdate: true,
|
||||
updateTimeout: 30,
|
||||
updateTimeout: 120,
|
||||
github: {
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
},
|
||||
gitea: {
|
||||
baseURL: "https://git.landaiqing.cn",
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
}
|
||||
},
|
||||
backup: {
|
||||
enabled: false,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// 编辑器实例管理
|
||||
export const EDITOR_CONFIG = {
|
||||
/** 最多缓存的编辑器实例数量 */
|
||||
MAX_INSTANCES: 5,
|
||||
MAX_INSTANCES: 10,
|
||||
/** 语法树缓存过期时间(毫秒) */
|
||||
SYNTAX_TREE_CACHE_TIMEOUT: 30000,
|
||||
/** 加载状态延迟时间(毫秒) */
|
||||
|
||||
@@ -35,7 +35,6 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
// Helper methods
|
||||
public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[];
|
||||
public visitSingle: (ctx: TomlContext) => Doc | string;
|
||||
public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -57,26 +56,38 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
const singleElement = getSingle(ctx);
|
||||
return this.visit(singleElement);
|
||||
};
|
||||
}
|
||||
|
||||
// Store reference to inherited visit method and override it
|
||||
const originalVisit = Object.getPrototypeOf(this).visit?.bind(this);
|
||||
this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => {
|
||||
if (!ctx) {
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* Override visit method to handle TOML CST nodes
|
||||
* Accepts both single node and array of nodes as per base class signature
|
||||
*/
|
||||
visit(cstNode: any, param?: any): any {
|
||||
// Handle array of nodes
|
||||
if (Array.isArray(cstNode)) {
|
||||
return cstNode.map(node => this.visit(node, param));
|
||||
}
|
||||
|
||||
const ctx = cstNode;
|
||||
if (!ctx) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 确保节点有name属性才调用基类方法
|
||||
if (ctx.name) {
|
||||
// Try to use the inherited visit method first
|
||||
const originalVisit = super.visit;
|
||||
if (originalVisit) {
|
||||
try {
|
||||
return originalVisit(ctx, inParam);
|
||||
return originalVisit.call(this, ctx, param);
|
||||
} catch (error) {
|
||||
console.warn('Original visit method failed:', error);
|
||||
// Fallback to manual dispatch
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: manually dispatch based on node name/type
|
||||
const methodName = ctx.name;
|
||||
if (methodName && typeof (this as any)[methodName] === 'function') {
|
||||
if (typeof (this as any)[methodName] === 'function') {
|
||||
const visitMethod = (this as any)[methodName];
|
||||
try {
|
||||
if (ctx.children) {
|
||||
@@ -88,16 +99,16 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
console.warn(`Visit method ${methodName} failed:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: return image if available
|
||||
return ctx.image || '';
|
||||
};
|
||||
// Final fallback: return image if available
|
||||
return ctx.image || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit the root TOML document
|
||||
*/
|
||||
toml(ctx: TomlDocument): Doc {
|
||||
toml(ctx: any): Doc {
|
||||
// Handle empty toml document
|
||||
if (!ctx.expression) {
|
||||
return [line];
|
||||
@@ -164,7 +175,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
/**
|
||||
* Visit an expression (keyval, table, or comment)
|
||||
*/
|
||||
expression(ctx: TomlExpression): Doc | string {
|
||||
expression(ctx: any): Doc | string {
|
||||
if (ctx.keyval) {
|
||||
let keyValDoc = this.visit(ctx.keyval[0]);
|
||||
if (ctx.Comment) {
|
||||
@@ -189,7 +200,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
/**
|
||||
* Visit a key-value pair
|
||||
*/
|
||||
keyval(ctx: TomlKeyVal): Doc {
|
||||
keyval(ctx: any): Doc {
|
||||
const keyDoc = this.visit(ctx.key[0]);
|
||||
const valueDoc = this.visit(ctx.val[0]);
|
||||
return [keyDoc, ' = ', valueDoc];
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
/**
|
||||
* 操作信息接口
|
||||
*/
|
||||
interface OperationInfo {
|
||||
controller: AbortController;
|
||||
createdAt: number;
|
||||
timeout?: number;
|
||||
timeoutId?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步操作管理器
|
||||
* 用于管理异步操作的竞态条件,确保只有最新的操作有效
|
||||
* 支持操作超时和自动清理机制
|
||||
*
|
||||
* @template T 操作上下文的类型
|
||||
*/
|
||||
export class AsyncManager<T = any> {
|
||||
private operationSequence = 0;
|
||||
private pendingOperations = new Map<number, OperationInfo>();
|
||||
private currentContext: T | null = null;
|
||||
private defaultTimeout: number;
|
||||
|
||||
/**
|
||||
* 创建异步操作管理器
|
||||
*
|
||||
* @param defaultTimeout 默认超时时间(毫秒),0表示不设置超时
|
||||
*/
|
||||
constructor(defaultTimeout: number = 0) {
|
||||
this.defaultTimeout = defaultTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新的操作ID
|
||||
*
|
||||
* @returns 新的操作ID
|
||||
*/
|
||||
getNextOperationId(): number {
|
||||
return ++this.operationSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始新的操作
|
||||
*
|
||||
* @param context 操作上下文
|
||||
* @param options 操作选项
|
||||
* @returns 操作ID和AbortController
|
||||
*/
|
||||
startOperation(
|
||||
context: T,
|
||||
options?: {
|
||||
excludeId?: number;
|
||||
timeout?: number;
|
||||
}
|
||||
): { operationId: number; abortController: AbortController } {
|
||||
const operationId = this.getNextOperationId();
|
||||
const abortController = new AbortController();
|
||||
const timeout = options?.timeout ?? this.defaultTimeout;
|
||||
|
||||
// 取消之前的操作
|
||||
this.cancelPreviousOperations(options?.excludeId);
|
||||
|
||||
// 创建操作信息
|
||||
const operationInfo: OperationInfo = {
|
||||
controller: abortController,
|
||||
createdAt: Date.now(),
|
||||
timeout: timeout > 0 ? timeout : undefined
|
||||
};
|
||||
|
||||
// 设置超时处理
|
||||
if (timeout > 0) {
|
||||
operationInfo.timeoutId = setTimeout(() => {
|
||||
this.cancelOperation(operationId, 'timeout');
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
// 设置当前上下文和操作
|
||||
this.currentContext = context;
|
||||
this.pendingOperations.set(operationId, operationInfo);
|
||||
|
||||
return { operationId, abortController };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查操作是否仍然有效
|
||||
*
|
||||
* @param operationId 操作ID
|
||||
* @param context 操作上下文
|
||||
* @returns 操作是否有效
|
||||
*/
|
||||
isOperationValid(operationId: number, context?: T): boolean {
|
||||
const operationInfo = this.pendingOperations.get(operationId);
|
||||
const contextValid = context === undefined || this.currentContext === context;
|
||||
|
||||
return (
|
||||
operationInfo !== undefined &&
|
||||
!operationInfo.controller.signal.aborted &&
|
||||
contextValid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成操作
|
||||
*
|
||||
* @param operationId 操作ID
|
||||
*/
|
||||
completeOperation(operationId: number): void {
|
||||
const operationInfo = this.pendingOperations.get(operationId);
|
||||
if (operationInfo) {
|
||||
// 清理超时定时器
|
||||
if (operationInfo.timeoutId) {
|
||||
clearTimeout(operationInfo.timeoutId);
|
||||
}
|
||||
this.pendingOperations.delete(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消指定操作
|
||||
*
|
||||
* @param operationId 操作ID
|
||||
* @param reason 取消原因
|
||||
*/
|
||||
cancelOperation(operationId: number, reason?: string): void {
|
||||
const operationInfo = this.pendingOperations.get(operationId);
|
||||
if (operationInfo) {
|
||||
// 清理超时定时器
|
||||
if (operationInfo.timeoutId) {
|
||||
clearTimeout(operationInfo.timeoutId);
|
||||
}
|
||||
// 取消操作
|
||||
operationInfo.controller.abort(reason);
|
||||
this.pendingOperations.delete(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消之前的操作(修复并发bug)
|
||||
*
|
||||
* @param excludeId 要排除的操作ID(不取消该操作)
|
||||
*/
|
||||
cancelPreviousOperations(excludeId?: number): void {
|
||||
// 创建要取消的操作ID数组,避免在遍历时修改Map
|
||||
const operationIdsToCancel: number[] = [];
|
||||
|
||||
for (const [operationId] of this.pendingOperations) {
|
||||
if (excludeId === undefined || operationId !== excludeId) {
|
||||
operationIdsToCancel.push(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量取消操作
|
||||
for (const operationId of operationIdsToCancel) {
|
||||
this.cancelOperation(operationId, 'superseded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有操作
|
||||
*/
|
||||
cancelAllOperations(): void {
|
||||
// 创建要取消的操作ID数组,避免在遍历时修改Map
|
||||
const operationIdsToCancel = Array.from(this.pendingOperations.keys());
|
||||
|
||||
// 批量取消操作
|
||||
for (const operationId of operationIdsToCancel) {
|
||||
this.cancelOperation(operationId, 'cancelled');
|
||||
}
|
||||
this.currentContext = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期操作(手动清理超时操作)
|
||||
*
|
||||
* @param maxAge 最大存活时间(毫秒)
|
||||
* @returns 清理的操作数量
|
||||
*/
|
||||
cleanupExpiredOperations(maxAge: number): number {
|
||||
const now = Date.now();
|
||||
const expiredOperationIds: number[] = [];
|
||||
|
||||
for (const [operationId, operationInfo] of this.pendingOperations) {
|
||||
if (now - operationInfo.createdAt > maxAge) {
|
||||
expiredOperationIds.push(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量取消过期操作
|
||||
for (const operationId of expiredOperationIds) {
|
||||
this.cancelOperation(operationId, 'expired');
|
||||
}
|
||||
|
||||
return expiredOperationIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作统计信息
|
||||
*
|
||||
* @returns 操作统计信息
|
||||
*/
|
||||
getOperationStats(): {
|
||||
total: number;
|
||||
withTimeout: number;
|
||||
averageAge: number;
|
||||
oldestAge: number;
|
||||
} {
|
||||
const now = Date.now();
|
||||
let withTimeout = 0;
|
||||
let totalAge = 0;
|
||||
let oldestAge = 0;
|
||||
|
||||
for (const operationInfo of this.pendingOperations.values()) {
|
||||
const age = now - operationInfo.createdAt;
|
||||
totalAge += age;
|
||||
oldestAge = Math.max(oldestAge, age);
|
||||
|
||||
if (operationInfo.timeout) {
|
||||
withTimeout++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total: this.pendingOperations.size,
|
||||
withTimeout,
|
||||
averageAge: this.pendingOperations.size > 0 ? totalAge / this.pendingOperations.size : 0,
|
||||
oldestAge
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前上下文
|
||||
*
|
||||
* @returns 当前上下文
|
||||
*/
|
||||
getCurrentContext(): T | null {
|
||||
return this.currentContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前上下文
|
||||
*
|
||||
* @param context 新的上下文
|
||||
*/
|
||||
setCurrentContext(context: T | null): void {
|
||||
this.currentContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待处理操作数量
|
||||
*
|
||||
* @returns 待处理操作数量
|
||||
*/
|
||||
get pendingCount(): number {
|
||||
return this.pendingOperations.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的操作
|
||||
*
|
||||
* @returns 是否有待处理的操作
|
||||
*/
|
||||
hasPendingOperations(): boolean {
|
||||
return this.pendingOperations.size > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,13 @@
|
||||
import { LanguageType } from '@/../bindings/voidraft/internal/models/models';
|
||||
import type { SupportedLocaleType } from '@/common/constant/locales';
|
||||
|
||||
/**
|
||||
* 配置工具类
|
||||
*/
|
||||
export class ConfigUtils {
|
||||
/**
|
||||
* 将后端语言类型转换为前端语言代码
|
||||
*/
|
||||
static backendLanguageToFrontend(language: LanguageType): SupportedLocaleType {
|
||||
return language === LanguageType.LangZhCN ? 'zh-CN' : 'en-US';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端语言代码转换为后端语言类型
|
||||
*/
|
||||
static frontendLanguageToBackend(locale: SupportedLocaleType): LanguageType {
|
||||
return locale === 'zh-CN' ? LanguageType.LangZhCN : LanguageType.LangEnUS;
|
||||
}
|
||||
/**
|
||||
* 验证数值是否在指定范围内
|
||||
*/
|
||||
static clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数值是否在指定范围内
|
||||
*/
|
||||
static clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置值是否有效
|
||||
*/
|
||||
static isValidConfigValue<T>(value: T, validValues: readonly T[]): boolean {
|
||||
return validValues.includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置的默认值
|
||||
*/
|
||||
static getDefaultValue<T>(key: string, defaults: Record<string, { default: T }>): T {
|
||||
return defaults[key]?.default;
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
/**
|
||||
* DOM Diff 算法单元测试
|
||||
*/
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { morphNode, morphHTML, morphWithKeys } from './domDiff';
|
||||
|
||||
describe('DOM Diff Algorithm', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
describe('morphNode - 基础功能', () => {
|
||||
test('应该更新文本节点内容', () => {
|
||||
const fromNode = document.createTextNode('Hello');
|
||||
const toNode = document.createTextNode('World');
|
||||
container.appendChild(fromNode);
|
||||
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(fromNode.nodeValue).toBe('World');
|
||||
});
|
||||
|
||||
test('应该保持相同的文本节点不变', () => {
|
||||
const fromNode = document.createTextNode('Hello');
|
||||
const toNode = document.createTextNode('Hello');
|
||||
container.appendChild(fromNode);
|
||||
|
||||
const originalNode = fromNode;
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(fromNode).toBe(originalNode);
|
||||
expect(fromNode.nodeValue).toBe('Hello');
|
||||
});
|
||||
|
||||
test('应该替换不同类型的节点', () => {
|
||||
const fromNode = document.createElement('span');
|
||||
fromNode.textContent = 'Hello';
|
||||
const toNode = document.createElement('div');
|
||||
toNode.textContent = 'World';
|
||||
container.appendChild(fromNode);
|
||||
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(container.firstChild?.nodeName).toBe('DIV');
|
||||
expect(container.firstChild?.textContent).toBe('World');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphNode - 属性更新', () => {
|
||||
test('应该添加新属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'test');
|
||||
toEl.setAttribute('id', 'myid');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('test');
|
||||
expect(fromEl.getAttribute('id')).toBe('myid');
|
||||
});
|
||||
|
||||
test('应该更新已存在的属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.setAttribute('class', 'old');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'new');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('new');
|
||||
});
|
||||
|
||||
test('应该删除不存在的属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.setAttribute('class', 'test');
|
||||
fromEl.setAttribute('id', 'myid');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'test');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('test');
|
||||
expect(fromEl.hasAttribute('id')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphNode - 子节点更新', () => {
|
||||
test('应该添加新子节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = '<li>1</li><li>2</li>';
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(3);
|
||||
expect(fromEl.children[2].textContent).toBe('3');
|
||||
});
|
||||
|
||||
test('应该删除多余的子节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = '<li>1</li><li>2</li>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(2);
|
||||
expect(fromEl.textContent).toBe('12');
|
||||
});
|
||||
|
||||
test('应该更新子节点内容', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.innerHTML = '<p>Old</p>';
|
||||
const toEl = document.createElement('div');
|
||||
toEl.innerHTML = '<p>New</p>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const originalP = fromEl.querySelector('p');
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
// 应该保持同一个 p 元素,只更新内容
|
||||
expect(fromEl.querySelector('p')).toBe(originalP);
|
||||
expect(fromEl.querySelector('p')?.textContent).toBe('New');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphHTML - HTML 字符串更新', () => {
|
||||
test('应该从 HTML 字符串更新元素', () => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = '<p>Old</p>';
|
||||
container.appendChild(element);
|
||||
|
||||
morphHTML(element, '<p>New</p>');
|
||||
|
||||
expect(element.innerHTML).toBe('<p>New</p>');
|
||||
});
|
||||
|
||||
test('应该处理复杂的 HTML 结构', () => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = '<h1>Title</h1><p>Paragraph</p>';
|
||||
container.appendChild(element);
|
||||
|
||||
morphHTML(element, '<h1>New Title</h1><p>New Paragraph</p><span>Extra</span>');
|
||||
|
||||
expect(element.children.length).toBe(3);
|
||||
expect(element.querySelector('h1')?.textContent).toBe('New Title');
|
||||
expect(element.querySelector('p')?.textContent).toBe('New Paragraph');
|
||||
expect(element.querySelector('span')?.textContent).toBe('Extra');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphWithKeys - 基于 key 的智能 diff', () => {
|
||||
test('应该保持相同 key 的节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A Updated</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const originalA = fromEl.querySelector('[data-key="a"]');
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.querySelector('[data-key="a"]')).toBe(originalA);
|
||||
expect(originalA?.textContent).toBe('A Updated');
|
||||
});
|
||||
|
||||
test('应该重新排序节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="c">C</li>
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
const keys = Array.from(fromEl.children).map(child => child.getAttribute('data-key'));
|
||||
expect(keys).toEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
test('应该添加新的 key 节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(3);
|
||||
expect(fromEl.querySelector('[data-key="c"]')?.textContent).toBe('C');
|
||||
});
|
||||
|
||||
test('应该删除不存在的 key 节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(2);
|
||||
expect(fromEl.querySelector('[data-key="b"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能测试', () => {
|
||||
test('应该高效处理大量节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `Item ${i}`;
|
||||
fromEl.appendChild(li);
|
||||
}
|
||||
|
||||
const toEl = document.createElement('ul');
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `Updated Item ${i}`;
|
||||
toEl.appendChild(li);
|
||||
}
|
||||
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const startTime = performance.now();
|
||||
morphNode(fromEl, toEl);
|
||||
const endTime = performance.now();
|
||||
|
||||
expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成
|
||||
expect(fromEl.children.length).toBe(1000);
|
||||
expect(fromEl.children[0].textContent).toBe('Updated Item 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
test('应该处理空节点', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
const toEl = document.createElement('div');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
expect(() => morphNode(fromEl, toEl)).not.toThrow();
|
||||
});
|
||||
|
||||
test('应该处理只有文本的节点', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.textContent = 'Hello';
|
||||
const toEl = document.createElement('div');
|
||||
toEl.textContent = 'World';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.textContent).toBe('World');
|
||||
});
|
||||
|
||||
test('应该处理嵌套的复杂结构', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.innerHTML = `
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<span>Text</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const toEl = document.createElement('div');
|
||||
toEl.innerHTML = `
|
||||
<div class="outer modified">
|
||||
<div class="inner">
|
||||
<span>Updated Text</span>
|
||||
<strong>New</strong>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.querySelector('.outer')?.classList.contains('modified')).toBe(true);
|
||||
expect(fromEl.querySelector('span')?.textContent).toBe('Updated Text');
|
||||
expect(fromEl.querySelector('strong')?.textContent).toBe('New');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/**
|
||||
* 轻量级 DOM Diff 算法实现
|
||||
* 基于 morphdom 思路,只更新变化的节点,保持未变化的节点不动
|
||||
*/
|
||||
|
||||
/**
|
||||
* 比较并更新两个 DOM 节点
|
||||
* @param fromNode 原节点
|
||||
* @param toNode 目标节点
|
||||
*/
|
||||
export function morphNode(fromNode: Node, toNode: Node): void {
|
||||
// 节点类型不同,直接替换
|
||||
if (fromNode.nodeType !== toNode.nodeType || fromNode.nodeName !== toNode.nodeName) {
|
||||
fromNode.parentNode?.replaceChild(toNode.cloneNode(true), fromNode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 文本节点:比较内容
|
||||
if (fromNode.nodeType === Node.TEXT_NODE) {
|
||||
if (fromNode.nodeValue !== toNode.nodeValue) {
|
||||
fromNode.nodeValue = toNode.nodeValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 元素节点:更新属性和子节点
|
||||
if (fromNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const fromEl = fromNode as Element;
|
||||
const toEl = toNode as Element;
|
||||
|
||||
// 更新属性
|
||||
morphAttributes(fromEl, toEl);
|
||||
|
||||
// 更新子节点
|
||||
morphChildren(fromEl, toEl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新元素属性
|
||||
*/
|
||||
function morphAttributes(fromEl: Element, toEl: Element): void {
|
||||
// 移除旧属性
|
||||
const fromAttrs = fromEl.attributes;
|
||||
for (let i = fromAttrs.length - 1; i >= 0; i--) {
|
||||
const attr = fromAttrs[i];
|
||||
if (!toEl.hasAttribute(attr.name)) {
|
||||
fromEl.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加/更新新属性
|
||||
const toAttrs = toEl.attributes;
|
||||
for (let i = 0; i < toAttrs.length; i++) {
|
||||
const attr = toAttrs[i];
|
||||
const fromValue = fromEl.getAttribute(attr.name);
|
||||
if (fromValue !== attr.value) {
|
||||
fromEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新子节点(核心 diff 算法)
|
||||
*/
|
||||
function morphChildren(fromEl: Element, toEl: Element): void {
|
||||
const fromChildren = Array.from(fromEl.childNodes);
|
||||
const toChildren = Array.from(toEl.childNodes);
|
||||
|
||||
const fromLen = fromChildren.length;
|
||||
const toLen = toChildren.length;
|
||||
const minLen = Math.min(fromLen, toLen);
|
||||
|
||||
// 1. 更新公共部分
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
morphNode(fromChildren[i], toChildren[i]);
|
||||
}
|
||||
|
||||
// 2. 移除多余的旧节点
|
||||
if (fromLen > toLen) {
|
||||
for (let i = fromLen - 1; i >= toLen; i--) {
|
||||
fromEl.removeChild(fromChildren[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 添加新节点
|
||||
if (toLen > fromLen) {
|
||||
for (let i = fromLen; i < toLen; i++) {
|
||||
fromEl.appendChild(toChildren[i].cloneNode(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化版:使用 key 进行更智能的 diff(可选)
|
||||
* 适用于有 data-key 属性的元素
|
||||
*/
|
||||
export function morphWithKeys(fromEl: Element, toEl: Element): void {
|
||||
const toChildren = Array.from(toEl.children) as Element[];
|
||||
|
||||
// 构建 from 的 key 映射
|
||||
const fromKeyMap = new Map<string, Element>();
|
||||
Array.from(fromEl.children).forEach((child) => {
|
||||
const key = child.getAttribute('data-key');
|
||||
if (key) {
|
||||
fromKeyMap.set(key, child);
|
||||
}
|
||||
});
|
||||
|
||||
const processedKeys = new Set<string>();
|
||||
|
||||
// 按照 toChildren 的顺序处理
|
||||
toChildren.forEach((toChild, toIndex) => {
|
||||
const key = toChild.getAttribute('data-key');
|
||||
if (!key) return;
|
||||
|
||||
processedKeys.add(key);
|
||||
const fromChild = fromKeyMap.get(key);
|
||||
|
||||
if (fromChild) {
|
||||
// 找到对应节点,更新内容
|
||||
morphNode(fromChild, toChild);
|
||||
|
||||
// 确保节点在正确的位置
|
||||
const currentNode = fromEl.children[toIndex];
|
||||
if (currentNode !== fromChild) {
|
||||
// 将 fromChild 移动到正确位置
|
||||
fromEl.insertBefore(fromChild, currentNode);
|
||||
}
|
||||
} else {
|
||||
// 新节点,插入到正确位置
|
||||
const currentNode = fromEl.children[toIndex];
|
||||
fromEl.insertBefore(toChild.cloneNode(true), currentNode || null);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除不再存在的节点(从后往前删除,避免索引问题)
|
||||
const childrenToRemove: Element[] = [];
|
||||
fromKeyMap.forEach((child, key) => {
|
||||
if (!processedKeys.has(key)) {
|
||||
childrenToRemove.push(child);
|
||||
}
|
||||
});
|
||||
childrenToRemove.forEach(child => {
|
||||
fromEl.removeChild(child);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级 API:直接从 HTML 字符串更新元素
|
||||
*/
|
||||
export function morphHTML(element: Element, htmlString: string): void {
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = htmlString;
|
||||
|
||||
// 更新元素的子节点列表
|
||||
morphChildren(element, tempContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新(使用 DocumentFragment)
|
||||
*/
|
||||
export function batchMorph(element: Element, htmlString: string): void {
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = htmlString;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
Array.from(tempContainer.childNodes).forEach(node => {
|
||||
fragment.appendChild(node);
|
||||
});
|
||||
|
||||
// 清空原内容
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild);
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
element.appendChild(fragment);
|
||||
}
|
||||
|
||||
105
frontend/src/components/accordion/AccordionContainer.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { provide, ref } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 是否允许多个面板同时展开
|
||||
* @default false - 单选模式(手风琴效果)
|
||||
*/
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
// 当前展开的项(单选模式)或展开项列表(多选模式)
|
||||
const expandedItems = ref<Set<string | number>>(new Set());
|
||||
|
||||
/**
|
||||
* 切换展开状态
|
||||
*/
|
||||
const toggleItem = (id: string | number) => {
|
||||
if (props.multiple) {
|
||||
// 多选模式:切换单个项
|
||||
if (expandedItems.value.has(id)) {
|
||||
expandedItems.value.delete(id);
|
||||
} else {
|
||||
expandedItems.value.add(id);
|
||||
}
|
||||
} else {
|
||||
// 单选模式:只能展开一个
|
||||
if (expandedItems.value.has(id)) {
|
||||
expandedItems.value.clear();
|
||||
} else {
|
||||
expandedItems.value.clear();
|
||||
expandedItems.value.add(id);
|
||||
}
|
||||
}
|
||||
// 触发响应式更新
|
||||
expandedItems.value = new Set(expandedItems.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查项是否展开
|
||||
*/
|
||||
const isExpanded = (id: string | number): boolean => {
|
||||
return expandedItems.value.has(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 展开指定项
|
||||
*/
|
||||
const expand = (id: string | number) => {
|
||||
if (!props.multiple) {
|
||||
expandedItems.value.clear();
|
||||
}
|
||||
expandedItems.value.add(id);
|
||||
expandedItems.value = new Set(expandedItems.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 收起指定项
|
||||
*/
|
||||
const collapse = (id: string | number) => {
|
||||
expandedItems.value.delete(id);
|
||||
expandedItems.value = new Set(expandedItems.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 收起所有项
|
||||
*/
|
||||
const collapseAll = () => {
|
||||
expandedItems.value.clear();
|
||||
expandedItems.value = new Set(expandedItems.value);
|
||||
};
|
||||
|
||||
// 通过 provide 向子组件提供状态和方法
|
||||
provide('accordion', {
|
||||
toggleItem,
|
||||
isExpanded,
|
||||
expand,
|
||||
collapse,
|
||||
});
|
||||
|
||||
// 暴露方法供父组件使用
|
||||
defineExpose({
|
||||
expand,
|
||||
collapse,
|
||||
collapseAll,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="accordion-container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.accordion-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
187
frontend/src/components/accordion/AccordionItem.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, computed, ref } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
id: string | number;
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const accordion = inject<{
|
||||
toggleItem: (id: string | number) => void;
|
||||
isExpanded: (id: string | number) => boolean;
|
||||
}>('accordion');
|
||||
|
||||
if (!accordion) {
|
||||
throw new Error('AccordionItem must be used within AccordionContainer');
|
||||
}
|
||||
|
||||
const isExpanded = computed(() => accordion.isExpanded(props.id));
|
||||
|
||||
const toggle = () => {
|
||||
if (!props.disabled) {
|
||||
accordion.toggleItem(props.id);
|
||||
}
|
||||
};
|
||||
|
||||
// 内容容器的引用,用于计算高度
|
||||
const contentRef = ref<HTMLElement>();
|
||||
const contentHeight = computed(() => {
|
||||
if (!contentRef.value) return '0px';
|
||||
return isExpanded.value ? `${contentRef.value.scrollHeight}px` : '0px';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="accordion-item"
|
||||
:class="{
|
||||
'is-expanded': isExpanded,
|
||||
'is-disabled': disabled
|
||||
}"
|
||||
>
|
||||
<!-- 标题栏 -->
|
||||
<div
|
||||
class="accordion-header"
|
||||
@click="toggle"
|
||||
:aria-expanded="isExpanded"
|
||||
:aria-disabled="disabled"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keydown.enter="toggle"
|
||||
@keydown.space.prevent="toggle"
|
||||
>
|
||||
<div class="accordion-title">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</div>
|
||||
<div class="accordion-icon">
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 4.5L6 7.5L9 4.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div
|
||||
class="accordion-content-wrapper"
|
||||
:style="{ height: contentHeight }"
|
||||
>
|
||||
<div
|
||||
ref="contentRef"
|
||||
class="accordion-content"
|
||||
>
|
||||
<div class="accordion-content-inner">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.accordion-item {
|
||||
border-bottom: 1px solid var(--settings-border);
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.is-expanded {
|
||||
background-color: var(--settings-hover);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
.accordion-header {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover:not([aria-disabled="true"]) {
|
||||
background-color: var(--settings-hover);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid #4a9eff;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--settings-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.accordion-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-muted);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
.is-expanded & {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-content-wrapper {
|
||||
overflow: hidden;
|
||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
// 用于测量实际内容高度
|
||||
}
|
||||
|
||||
.accordion-content-inner {
|
||||
padding: 0 16px 12px 16px;
|
||||
color: var(--settings-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
3
frontend/src/components/accordion/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as AccordionContainer } from './AccordionContainer.vue';
|
||||
export { default as AccordionItem } from './AccordionItem.vue';
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-for="tab in tabStore.tabs"
|
||||
:key="tab.documentId"
|
||||
:tab="tab"
|
||||
:isActive="tab.documentId === tabStore.currentDocumentId"
|
||||
:isActive="tab.documentId === documentStore.currentDocumentId"
|
||||
:canClose="tabStore.canCloseTab"
|
||||
@click="switchToTab"
|
||||
@close="closeTab"
|
||||
@@ -35,8 +35,14 @@ import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import TabItem from './TabItem.vue';
|
||||
import TabContextMenu from './TabContextMenu.vue';
|
||||
import { useTabStore } from '@/stores/tabStore';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
import { useEditorStore } from '@/stores/editorStore';
|
||||
import { useEditorStateStore } from '@/stores/editorStateStore';
|
||||
|
||||
const tabStore = useTabStore();
|
||||
const documentStore = useDocumentStore();
|
||||
const editorStore = useEditorStore();
|
||||
const editorStateStore = useEditorStateStore();
|
||||
|
||||
// DOM 引用
|
||||
const tabBarRef = ref<HTMLElement>();
|
||||
@@ -50,8 +56,36 @@ const contextMenuTargetId = ref<number | null>(null);
|
||||
|
||||
|
||||
// 标签页操作
|
||||
const switchToTab = (documentId: number) => {
|
||||
tabStore.switchToTabAndDocument(documentId);
|
||||
const switchToTab = async (documentId: number) => {
|
||||
|
||||
// 保存旧文档的光标位置
|
||||
const oldDocId = documentStore.currentDocumentId;
|
||||
if (oldDocId) {
|
||||
const cursorPos = editorStore.getCurrentCursorPosition();
|
||||
editorStateStore.saveCursorPosition(oldDocId, cursorPos);
|
||||
}
|
||||
|
||||
// 如果旧文档有未保存修改,保存它
|
||||
if (oldDocId && editorStore.hasUnsavedChanges(oldDocId)) {
|
||||
try {
|
||||
const content = editorStore.getCurrentContent();
|
||||
await documentStore.saveDocument(oldDocId, content);
|
||||
editorStore.syncAfterSave(oldDocId);
|
||||
} catch (error) {
|
||||
console.error('save document error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换文档
|
||||
await tabStore.switchToTabAndDocument(documentId);
|
||||
|
||||
// 切换到新编辑器
|
||||
await editorStore.switchToEditor(documentId);
|
||||
|
||||
// 更新标签页
|
||||
if (documentStore.currentDocument && tabStore.isTabsEnabled) {
|
||||
tabStore.addOrActivateTab(documentStore.currentDocument);
|
||||
}
|
||||
};
|
||||
|
||||
const closeTab = (documentId: number) => {
|
||||
@@ -150,7 +184,7 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
// 监听当前活跃标签页的变化
|
||||
watch(() => tabStore.currentDocumentId, () => {
|
||||
watch(() => documentStore.currentDocumentId, () => {
|
||||
scrollToActiveTab();
|
||||
});
|
||||
|
||||
|
||||
292
frontend/src/components/toast/Toast.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['toast-item', `toast-${type}`]"
|
||||
@mouseenter="pauseTimer"
|
||||
@mouseleave="resumeTimer"
|
||||
>
|
||||
<!-- 图标 -->
|
||||
<div class="toast-icon">
|
||||
<svg v-if="type === 'success'" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm-2 15l-5-5 1.41-1.41L8 12.17l7.59-7.59L17 6l-9 9z" fill="currentColor"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'error'" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm1 15H9v-2h2v2zm0-4H9V5h2v6z" fill="currentColor"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'warning'" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M1 19h18L10 1 1 19zm10-3H9v-2h2v2zm0-4H9v-4h2v4z" fill="currentColor"/>
|
||||
</svg>
|
||||
<svg v-else-if="type === 'info'" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm1 15H9V9h2v6zm0-8H9V5h2v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="toast-content">
|
||||
<div v-if="title" class="toast-title">{{ title }}</div>
|
||||
<div class="toast-message">{{ message }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
v-if="closable"
|
||||
class="toast-close"
|
||||
@click="close"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M12 4L4 12M4 4L12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import type { Toast } from './types';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
toast: Toast;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [id: string];
|
||||
}>();
|
||||
|
||||
const timer = ref<number | null>(null);
|
||||
const remainingTime = ref(props.toast.duration);
|
||||
const pausedAt = ref<number | null>(null);
|
||||
|
||||
const { id, message, title, type, duration, closable } = props.toast;
|
||||
|
||||
const close = () => {
|
||||
emit('close', id);
|
||||
};
|
||||
|
||||
const startTimer = () => {
|
||||
if (duration > 0) {
|
||||
timer.value = window.setTimeout(() => {
|
||||
close();
|
||||
}, remainingTime.value);
|
||||
}
|
||||
};
|
||||
|
||||
const clearTimer = () => {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const pauseTimer = () => {
|
||||
if (timer.value && duration > 0) {
|
||||
clearTimer();
|
||||
pausedAt.value = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
const resumeTimer = () => {
|
||||
if (pausedAt.value && duration > 0) {
|
||||
const elapsed = Date.now() - pausedAt.value;
|
||||
remainingTime.value = Math.max(0, remainingTime.value - elapsed);
|
||||
pausedAt.value = null;
|
||||
startTimer();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
startTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.toast-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
min-width: 300px;
|
||||
max-width: 420px;
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
transform-origin: center center;
|
||||
|
||||
// 毛玻璃效果
|
||||
// 亮色主题
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.08),
|
||||
0 1px 3px rgba(0, 0, 0, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
|
||||
cursor: default;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 6px 16px rgba(0, 0, 0, 0.1),
|
||||
0 2px 6px rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题适配 - 使用应用的 data-theme 属性
|
||||
:root[data-theme="dark"] .toast-item,
|
||||
:root[data-theme="auto"] .toast-item {
|
||||
background: rgba(45, 45, 45, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.3),
|
||||
0 1px 3px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 6px 16px rgba(0, 0, 0, 0.4),
|
||||
0 2px 6px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
// 跟随系统主题时的浅色偏好
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root[data-theme="auto"] .toast-item {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.08),
|
||||
0 1px 3px rgba(0, 0, 0, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 6px 16px rgba(0, 0, 0, 0.1),
|
||||
0 2px 6px rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
}
|
||||
|
||||
// 有标题时,图标与标题对齐(不需要 margin-top)
|
||||
.toast-item:has(.toast-title) .toast-icon {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.toast-warning .toast-icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--settings-text);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 12px;
|
||||
color: var(--settings-text-secondary);
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: 0px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--settings-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: var(--settings-text);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: rotate(90deg) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .toast-close,
|
||||
:root[data-theme="auto"] .toast-close {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root[data-theme="auto"] .toast-close {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
168
frontend/src/components/toast/ToastContainer.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<TransitionGroup
|
||||
v-for="position in positions"
|
||||
:key="position"
|
||||
:class="['toast-container', `toast-container-${position}`]"
|
||||
name="toast-list"
|
||||
tag="div"
|
||||
>
|
||||
<ToastItem
|
||||
v-for="toast in getToastsByPosition(position)"
|
||||
:key="toast.id"
|
||||
:toast="toast"
|
||||
@close="removeToast"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ToastItem from './Toast.vue';
|
||||
import { useToastStore } from './toastStore';
|
||||
import type { ToastPosition } from './types';
|
||||
|
||||
const toastStore = useToastStore();
|
||||
|
||||
const positions: ToastPosition[] = [
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
];
|
||||
|
||||
const getToastsByPosition = (position: ToastPosition) => {
|
||||
return toastStore.toasts.filter(toast => toast.position === position);
|
||||
};
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
toastStore.remove(id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部位置 - 增加间距避免覆盖标题栏
|
||||
.toast-container-top-left {
|
||||
top: 35px;
|
||||
left: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.toast-container-top-center {
|
||||
top: 35px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toast-container-top-right {
|
||||
top: 35px;
|
||||
right: 20px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
// 底部位置
|
||||
.toast-container-bottom-left {
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
align-items: flex-start;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.toast-container-bottom-center {
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.toast-container-bottom-right {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
align-items: flex-end;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
// TransitionGroup 列表动画 - 从哪来回哪去,收缩滑出
|
||||
.toast-list-move {
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.toast-list-enter-active {
|
||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.toast-list-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.6, 0, 0.8, 0.4);
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
// 右侧位置:从右滑入,收缩向右滑出
|
||||
.toast-container-top-right,
|
||||
.toast-container-bottom-right {
|
||||
.toast-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) scale(0.8);
|
||||
}
|
||||
|
||||
.toast-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// 左侧位置:从左滑入,收缩向左滑出
|
||||
.toast-container-top-left,
|
||||
.toast-container-bottom-left {
|
||||
.toast-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
}
|
||||
|
||||
.toast-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// 居中位置:从上/下滑入,收缩向上/下滑出
|
||||
.toast-container-top-center {
|
||||
.toast-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-100%) scale(0.8);
|
||||
}
|
||||
|
||||
.toast-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-100%) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container-bottom-center {
|
||||
.toast-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(100%) scale(0.8);
|
||||
}
|
||||
|
||||
.toast-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(100%) scale(0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
80
frontend/src/components/toast/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useToastStore } from './toastStore';
|
||||
import type { ToastOptions } from './types';
|
||||
|
||||
class ToastService {
|
||||
private getStore() {
|
||||
return useToastStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示一个通知
|
||||
*/
|
||||
show(options: ToastOptions): string {
|
||||
return this.getStore().add(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成功通知
|
||||
*/
|
||||
success(message: string, title?: string, options?: Partial<ToastOptions>): string {
|
||||
return this.show({
|
||||
message,
|
||||
title,
|
||||
type: 'success',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误通知
|
||||
*/
|
||||
error(message: string, title?: string, options?: Partial<ToastOptions>): string {
|
||||
return this.show({
|
||||
message,
|
||||
title,
|
||||
type: 'error',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示警告通知
|
||||
*/
|
||||
warning(message: string, title?: string, options?: Partial<ToastOptions>): string {
|
||||
return this.show({
|
||||
message,
|
||||
title,
|
||||
type: 'warning',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示信息通知
|
||||
*/
|
||||
info(message: string, title?: string, options?: Partial<ToastOptions>): string {
|
||||
return this.show({
|
||||
message,
|
||||
title,
|
||||
type: 'info',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭指定的通知
|
||||
*/
|
||||
close(id: string): void {
|
||||
this.getStore().remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有通知
|
||||
*/
|
||||
clear(): void {
|
||||
this.getStore().clear();
|
||||
}
|
||||
}
|
||||
export const toast = new ToastService();
|
||||
export default toast;
|
||||
|
||||
55
frontend/src/components/toast/toastStore.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { Toast, ToastOptions } from './types';
|
||||
|
||||
export const useToastStore = defineStore('toast', () => {
|
||||
const toasts = ref<Toast[]>([]);
|
||||
let idCounter = 0;
|
||||
|
||||
/**
|
||||
* 添加一个 Toast
|
||||
*/
|
||||
const add = (options: ToastOptions): string => {
|
||||
const id = `toast-${Date.now()}-${idCounter++}`;
|
||||
|
||||
const toast: Toast = {
|
||||
id,
|
||||
message: options.message,
|
||||
type: options.type || 'info',
|
||||
title: options.title,
|
||||
duration: options.duration ?? 4000,
|
||||
position: options.position || 'top-right',
|
||||
closable: options.closable ?? true,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
toasts.value.push(toast);
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除指定 Toast
|
||||
*/
|
||||
const remove = (id: string) => {
|
||||
const index = toasts.value.findIndex(t => t.id === id);
|
||||
if (index > -1) {
|
||||
toasts.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空所有 Toast
|
||||
*/
|
||||
const clear = () => {
|
||||
toasts.value = [];
|
||||
};
|
||||
|
||||
return {
|
||||
toasts,
|
||||
add,
|
||||
remove,
|
||||
clear,
|
||||
};
|
||||
});
|
||||
|
||||
52
frontend/src/components/toast/types.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Toast 通知类型定义
|
||||
*/
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export type ToastPosition =
|
||||
| 'top-right'
|
||||
| 'top-left'
|
||||
| 'bottom-right'
|
||||
| 'bottom-left'
|
||||
| 'top-center'
|
||||
| 'bottom-center';
|
||||
|
||||
export interface ToastOptions {
|
||||
/**
|
||||
* Toast 消息内容
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* Toast 类型
|
||||
*/
|
||||
type?: ToastType;
|
||||
|
||||
/**
|
||||
* 标题(可选)
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* 持续时间(毫秒),0 表示不自动关闭
|
||||
*/
|
||||
duration?: number;
|
||||
|
||||
/**
|
||||
* 显示位置
|
||||
*/
|
||||
position?: ToastPosition;
|
||||
|
||||
/**
|
||||
* 是否可关闭
|
||||
*/
|
||||
closable?: boolean;
|
||||
}
|
||||
|
||||
export interface Toast extends Required<Omit<ToastOptions, 'title'>> {
|
||||
id: string;
|
||||
title?: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
@@ -51,13 +51,13 @@ let editorScope: ReturnType<typeof effectScope> | null = null;
|
||||
|
||||
// 更新当前块语言信息
|
||||
const updateCurrentBlockLanguage = () => {
|
||||
if (!editorStore.editorView) {
|
||||
if (!editorStore.currentEditor) {
|
||||
currentBlockLanguage.value = { name: 'text', auto: false };
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = editorStore.editorView.state;
|
||||
const state = editorStore.currentEditor.state;
|
||||
const activeBlock = getActiveNoteBlock(state as any);
|
||||
if (activeBlock) {
|
||||
const newLanguage = {
|
||||
@@ -128,7 +128,7 @@ const setupEventListeners = (view: any) => {
|
||||
|
||||
// 监听编辑器状态变化
|
||||
watch(
|
||||
() => editorStore.editorView,
|
||||
() => editorStore.currentEditor,
|
||||
(newView) => {
|
||||
if (newView) {
|
||||
setupEventListeners(newView);
|
||||
@@ -175,13 +175,13 @@ const closeLanguageMenu = () => {
|
||||
|
||||
// 选择语言 - 优化性能
|
||||
const selectLanguage = (languageId: SupportedLanguage) => {
|
||||
if (!editorStore.editorView) {
|
||||
if (!editorStore.currentEditor) {
|
||||
closeLanguageMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const view = editorStore.editorView;
|
||||
const view = editorStore.currentEditor;
|
||||
const state = view.state;
|
||||
const dispatch = view.dispatch;
|
||||
|
||||
@@ -294,9 +294,11 @@ const scrollToCurrentLanguage = () => {
|
||||
<span class="arrow" :class="{ 'open': showLanguageMenu }">▲</span>
|
||||
</button>
|
||||
|
||||
<div class="language-menu" v-if="showLanguageMenu">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-container">
|
||||
<!-- 菜单 -->
|
||||
<Transition name="slide-up">
|
||||
<div class="language-menu" v-if="showLanguageMenu">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-container">
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
v-model="searchQuery"
|
||||
@@ -330,11 +332,23 @@ const scrollToCurrentLanguage = () => {
|
||||
{{ t('toolbar.noLanguageFound') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.block-language-selector {
|
||||
position: relative;
|
||||
|
||||
@@ -386,15 +400,17 @@ const scrollToCurrentLanguage = () => {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 4px;
|
||||
width: 220px;
|
||||
max-height: 280px;
|
||||
width: 280px;
|
||||
max-height: 400px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.search-input {
|
||||
@@ -403,11 +419,11 @@ const scrollToCurrentLanguage = () => {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px 5px 26px;
|
||||
font-size: 11px;
|
||||
padding: 6px 10px 6px 30px;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
line-height: 1.2;
|
||||
line-height: 1.4;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--text-muted);
|
||||
@@ -420,7 +436,7 @@ const scrollToCurrentLanguage = () => {
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
@@ -429,20 +445,21 @@ const scrollToCurrentLanguage = () => {
|
||||
}
|
||||
|
||||
.language-list {
|
||||
max-height: 200px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
.language-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
opacity: 0.8;
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -460,17 +477,17 @@ const scrollToCurrentLanguage = () => {
|
||||
}
|
||||
|
||||
.language-alias {
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 12px 8px;
|
||||
padding: 14px 10px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,7 +495,7 @@ const scrollToCurrentLanguage = () => {
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.language-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.language-list::-webkit-scrollbar-track {
|
||||
@@ -487,7 +504,7 @@ const scrollToCurrentLanguage = () => {
|
||||
|
||||
.language-list::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-color);
|
||||
border-radius: 2px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-muted);
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import {computed, nextTick, reactive, ref, watch} from 'vue';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useTabStore} from '@/stores/tabStore';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useEditorStateStore} from '@/stores/editorStateStore';
|
||||
import {useWindowStore} from '@/stores/windowStore';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useConfirm} from '@/composables';
|
||||
import {validateDocumentTitle} from '@/common/utils/validation';
|
||||
import {formatDateTime, truncateString} from '@/common/utils/formatter';
|
||||
import type {Document} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import toast from '@/components/toast';
|
||||
|
||||
// 类型定义
|
||||
interface DocumentItem extends Document {
|
||||
@@ -16,6 +19,8 @@ interface DocumentItem extends Document {
|
||||
|
||||
const documentStore = useDocumentStore();
|
||||
const tabStore = useTabStore();
|
||||
const editorStore = useEditorStore();
|
||||
const editorStateStore = useEditorStateStore();
|
||||
const windowStore = useWindowStore();
|
||||
const {t} = useI18n();
|
||||
|
||||
@@ -27,6 +32,7 @@ const editInputRef = ref<HTMLInputElement>();
|
||||
const state = reactive({
|
||||
isLoaded: false,
|
||||
searchQuery: '',
|
||||
documentList: [] as Document[], // 缓存文档列表
|
||||
editing: {
|
||||
id: null as number | null,
|
||||
title: ''
|
||||
@@ -44,7 +50,7 @@ const currentDocName = computed(() => {
|
||||
});
|
||||
|
||||
const filteredItems = computed<DocumentItem[]>(() => {
|
||||
const docs = documentStore.documentList;
|
||||
const docs = state.documentList;
|
||||
const query = state.searchQuery.trim();
|
||||
|
||||
if (!query) return docs;
|
||||
@@ -67,7 +73,7 @@ const filteredItems = computed<DocumentItem[]>(() => {
|
||||
|
||||
// 核心操作
|
||||
const openMenu = async () => {
|
||||
await documentStore.getDocumentMetaList();
|
||||
state.documentList = await documentStore.getDocumentList();
|
||||
documentStore.openDocumentSelector();
|
||||
state.isLoaded = true;
|
||||
await nextTick();
|
||||
@@ -88,10 +94,10 @@ const closeMenu = () => {
|
||||
resetDeleteConfirm();
|
||||
};
|
||||
|
||||
const selectDoc = async (doc: Document) => {
|
||||
const selectDoc = async (doc: DocumentItem) => {
|
||||
if (doc.id === undefined) return;
|
||||
|
||||
// 如果选择的就是当前文档,直接关闭菜单
|
||||
// 如果选择的就是当前文档,直接关闭菜单
|
||||
if (documentStore.currentDocument?.id === doc.id) {
|
||||
closeMenu();
|
||||
return;
|
||||
@@ -99,17 +105,40 @@ const selectDoc = async (doc: Document) => {
|
||||
|
||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||
if (hasOpen) {
|
||||
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow'));
|
||||
toast.warning(t('toolbar.alreadyOpenInNewWindow'));
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await documentStore.openDocument(doc.id);
|
||||
if (success) {
|
||||
if (tabStore.isTabsEnabled) {
|
||||
tabStore.addOrActivateTab(doc);
|
||||
}
|
||||
closeMenu();
|
||||
|
||||
// 保存旧文档的光标位置
|
||||
const oldDocId = documentStore.currentDocumentId;
|
||||
if (oldDocId) {
|
||||
const cursorPos = editorStore.getCurrentCursorPosition();
|
||||
editorStateStore.saveCursorPosition(oldDocId, cursorPos);
|
||||
}
|
||||
|
||||
// 如果旧文档有未保存修改,保存它
|
||||
if (oldDocId && editorStore.hasUnsavedChanges(oldDocId)) {
|
||||
|
||||
const content = editorStore.getCurrentContent();
|
||||
await documentStore.saveDocument(oldDocId, content);
|
||||
editorStore.syncAfterSave(oldDocId);
|
||||
|
||||
}
|
||||
|
||||
// 打开新文档
|
||||
const success = await documentStore.openDocument(doc.id);
|
||||
if (!success) return;
|
||||
|
||||
// 切换到新编辑器
|
||||
await editorStore.switchToEditor(doc.id);
|
||||
|
||||
// 更新标签页
|
||||
if (documentStore.currentDocument && tabStore.isTabsEnabled) {
|
||||
tabStore.addOrActivateTab(documentStore.currentDocument);
|
||||
}
|
||||
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const createDoc = async (title: string) => {
|
||||
@@ -119,7 +148,9 @@ const createDoc = async (title: string) => {
|
||||
|
||||
try {
|
||||
const newDoc = await documentStore.createNewDocument(trimmedTitle);
|
||||
if (newDoc) await selectDoc(newDoc);
|
||||
if (newDoc && newDoc.id) {
|
||||
await selectDoc(newDoc);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create document:', error);
|
||||
}
|
||||
@@ -142,7 +173,7 @@ const handleSearchEnter = () => {
|
||||
};
|
||||
|
||||
// 编辑操作
|
||||
const renameDoc = (doc: Document, event: Event) => {
|
||||
const renameDoc = (doc: DocumentItem, event: Event) => {
|
||||
event.stopPropagation();
|
||||
state.editing.id = doc.id ?? null;
|
||||
state.editing.title = doc.title || '';
|
||||
@@ -165,8 +196,8 @@ const saveEdit = async () => {
|
||||
if (error) return;
|
||||
|
||||
try {
|
||||
await documentStore.updateDocumentMetadata(state.editing.id, trimmedTitle);
|
||||
await documentStore.getDocumentMetaList();
|
||||
await documentStore.updateDocumentTitle(state.editing.id, trimmedTitle);
|
||||
state.documentList = await documentStore.getDocumentList();
|
||||
|
||||
// 如果tabs功能开启且该文档有标签页,更新标签页标题
|
||||
if (tabStore.isTabsEnabled && tabStore.hasTab(state.editing.id)) {
|
||||
@@ -186,17 +217,21 @@ const cancelEdit = () => {
|
||||
};
|
||||
|
||||
// 其他操作
|
||||
const openInNewWindow = async (doc: Document, event: Event) => {
|
||||
const openInNewWindow = async (doc: DocumentItem, event: Event) => {
|
||||
event.stopPropagation();
|
||||
if (doc.id === undefined) return;
|
||||
try {
|
||||
// 在打开新窗口前,如果启用了标签且该文档有标签,先关闭标签
|
||||
if (tabStore.isTabsEnabled && tabStore.hasTab(doc.id)) {
|
||||
await tabStore.closeTab(doc.id);
|
||||
}
|
||||
await documentStore.openDocumentInNewWindow(doc.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to open document in new window:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (doc: Document, event: Event) => {
|
||||
const handleDelete = async (doc: DocumentItem, event: Event) => {
|
||||
event.stopPropagation();
|
||||
if (doc.id === undefined) return;
|
||||
|
||||
@@ -204,17 +239,17 @@ const handleDelete = async (doc: Document, event: Event) => {
|
||||
// 确认删除前检查文档是否在其他窗口打开
|
||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||
if (hasOpen) {
|
||||
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow'));
|
||||
toast.warning(t('toolbar.alreadyOpenInNewWindow'));
|
||||
resetDeleteConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteSuccess = await documentStore.deleteDocument(doc.id);
|
||||
if (deleteSuccess) {
|
||||
await documentStore.getDocumentMetaList();
|
||||
// 如果删除的是当前文档,切换到第一个文档
|
||||
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||
const firstDoc = documentStore.documentList[0];
|
||||
state.documentList = await documentStore.getDocumentList();
|
||||
// 如果删除的是当前文档,切换到第一个文档
|
||||
if (documentStore.currentDocument?.id === doc.id && state.documentList.length > 0) {
|
||||
const firstDoc = state.documentList[0];
|
||||
if (firstDoc) await selectDoc(firstDoc);
|
||||
}
|
||||
}
|
||||
@@ -307,11 +342,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
<!-- 普通显示 -->
|
||||
<div v-if="state.editing.id !== item.id" class="doc-info">
|
||||
<div class="doc-title">{{ item.title }}</div>
|
||||
<!-- 根据状态显示错误信息或时间 -->
|
||||
<div v-if="documentStore.selectorError?.docId === item.id" class="doc-error">
|
||||
{{ documentStore.selectorError?.message }}
|
||||
</div>
|
||||
<div v-else class="doc-date">{{ formatDateTime(item.updated_at) }}</div>
|
||||
<div class="doc-date">{{ formatDateTime(item.updated_at) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑状态 -->
|
||||
@@ -353,7 +384,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="documentStore.documentList.length > 1 && item.id !== 1"
|
||||
v-if="state.documentList.length > 1"
|
||||
class="action-btn delete-btn"
|
||||
:class="{ 'delete-confirm': isDeleting(item.id!) }"
|
||||
@click="handleDelete(item, $event)"
|
||||
@@ -444,7 +475,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 4px;
|
||||
width: 300px;
|
||||
width: 340px;
|
||||
max-height: calc(100vh - 40px);
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
@@ -454,7 +485,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
|
||||
.input-box {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.main-input {
|
||||
@@ -463,8 +494,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px 5px 26px;
|
||||
font-size: 11px;
|
||||
padding: 6px 10px 6px 30px;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
|
||||
@@ -479,7 +510,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
@@ -500,7 +531,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
&.active {
|
||||
background-color: var(--selection-bg);
|
||||
|
||||
.doc-item-content .doc-info {
|
||||
@@ -508,7 +539,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
color: var(--selection-text);
|
||||
}
|
||||
|
||||
.doc-date, .doc-error {
|
||||
.doc-date {
|
||||
color: var(--selection-text);
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -520,8 +551,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 8px;
|
||||
font-size: 11px;
|
||||
padding: 10px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
svg {
|
||||
@@ -535,15 +566,15 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 8px;
|
||||
padding: 10px 10px;
|
||||
|
||||
.doc-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.doc-title {
|
||||
font-size: 12px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -551,17 +582,10 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
}
|
||||
|
||||
.doc-date {
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.doc-error {
|
||||
font-size: 10px;
|
||||
color: var(--text-danger);
|
||||
font-weight: 500;
|
||||
animation: fadeInOut 3s forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-edit {
|
||||
@@ -573,8 +597,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
|
||||
@@ -586,7 +610,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
|
||||
.doc-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
@@ -595,7 +619,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding: 5px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -616,7 +640,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
color: white;
|
||||
|
||||
.confirm-text {
|
||||
font-size: 9px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -631,27 +655,12 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 16px 8px;
|
||||
padding: 18px 10px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {useI18n} from 'vue-i18n';
|
||||
import {computed, onMounted, onUnmounted, ref, watch, shallowRef, readonly, toRefs, effectScope, onScopeDispose} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useEditorStateStore} from '@/stores/editorStateStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import {useWindowStore} from '@/stores/windowStore';
|
||||
import {useSystemStore} from '@/stores/systemStore';
|
||||
@@ -15,6 +16,7 @@ import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
const editorStore = useEditorStore();
|
||||
const editorStateStore = useEditorStateStore();
|
||||
const configStore = useConfigStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const windowStore = useWindowStore();
|
||||
@@ -25,7 +27,6 @@ const router = useRouter();
|
||||
const canFormatCurrentBlock = ref(false);
|
||||
const isLoaded = shallowRef(false);
|
||||
|
||||
const { documentStats } = toRefs(editorStore);
|
||||
const { config } = toRefs(configStore);
|
||||
|
||||
// 窗口置顶状态
|
||||
@@ -57,14 +58,14 @@ const goToSettings = () => {
|
||||
|
||||
// 执行格式化
|
||||
const formatCurrentBlock = () => {
|
||||
if (!canFormatCurrentBlock.value || !editorStore.editorView) return;
|
||||
formatBlockContent(editorStore.editorView);
|
||||
if (!canFormatCurrentBlock.value || !editorStore.currentEditor) return;
|
||||
formatBlockContent(editorStore.currentEditor);
|
||||
};
|
||||
|
||||
|
||||
// 统一更新按钮状态
|
||||
const updateButtonStates = () => {
|
||||
const view: any = editorStore.editorView;
|
||||
const view: any = editorStore.currentEditor;
|
||||
if (!view) {
|
||||
canFormatCurrentBlock.value = false;
|
||||
return;
|
||||
@@ -125,7 +126,7 @@ const setupEditorListeners = (view: any) => {
|
||||
|
||||
// 监听编辑器视图变化
|
||||
watch(
|
||||
() => editorStore.editorView,
|
||||
() => editorStore.currentEditor,
|
||||
(newView) => {
|
||||
// 在 scope 中管理副作用
|
||||
editorScope.run(() => {
|
||||
@@ -191,11 +192,13 @@ const updateButtonTitle = computed(() => {
|
||||
});
|
||||
|
||||
// 统计数据的计算属性
|
||||
const statsData = computed(() => ({
|
||||
lines: documentStats.value.lines,
|
||||
characters: documentStats.value.characters,
|
||||
selectedCharacters: documentStats.value.selectedCharacters
|
||||
}));
|
||||
const statsData = computed(() => {
|
||||
const docId = editorStore.currentEditorId;
|
||||
if (!docId) {
|
||||
return { lines: 0, characters: 0, selectedCharacters: 0 };
|
||||
}
|
||||
return editorStateStore.getDocumentStats(docId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
export default {
|
||||
locale: 'en-US',
|
||||
common: {
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
confirm: 'Confirm',
|
||||
save: 'Save',
|
||||
reset: 'Reset'
|
||||
},
|
||||
titlebar: {
|
||||
minimize: 'Minimize',
|
||||
maximize: 'Maximize',
|
||||
@@ -44,11 +53,31 @@ export default {
|
||||
auto: 'Follow System'
|
||||
},
|
||||
keybindings: {
|
||||
keymapMode: 'Keymap Mode',
|
||||
modes: {
|
||||
standard: 'Standard Mode',
|
||||
emacs: 'Emacs Mode'
|
||||
},
|
||||
headers: {
|
||||
shortcut: 'Shortcut',
|
||||
extension: 'Extension',
|
||||
description: 'Description'
|
||||
},
|
||||
resetToDefault: 'Reset to Default',
|
||||
confirmReset: 'Confirm Reset?',
|
||||
noKeybinding: 'Not Set',
|
||||
waitingForKey: 'Waiting...',
|
||||
clickToSet: 'Click to set keybinding',
|
||||
editKeybinding: 'Edit keybinding',
|
||||
config: {
|
||||
enabled: 'Enabled',
|
||||
preventDefault: 'Prevent Default',
|
||||
keybinding: 'Keybinding'
|
||||
},
|
||||
keyPlaceholder: 'Enter key, press Enter to add',
|
||||
invalidFormat: 'Invalid format',
|
||||
conflict: 'Conflict: {command}',
|
||||
maxKeysReached: 'Maximum 4 keys allowed',
|
||||
commands: {
|
||||
showSearch: 'Show search panel',
|
||||
hideSearch: 'Hide search panel',
|
||||
@@ -76,6 +105,7 @@ export default {
|
||||
blockCopy: 'Copy',
|
||||
blockCut: 'Cut',
|
||||
blockPaste: 'Paste',
|
||||
copyBlockImage: 'Copy block image',
|
||||
historyUndo: 'Undo',
|
||||
historyRedo: 'Redo',
|
||||
historyUndoSelection: 'Undo selection',
|
||||
@@ -93,6 +123,25 @@ export default {
|
||||
insertBlankLine: 'Insert blank line',
|
||||
selectLine: 'Select line',
|
||||
selectParentSyntax: 'Select parent syntax',
|
||||
simplifySelection: 'Simplify selection',
|
||||
addCursorAbove: 'Add cursor above',
|
||||
addCursorBelow: 'Add cursor below',
|
||||
cursorGroupLeft: 'Cursor word left',
|
||||
cursorGroupRight: 'Cursor word right',
|
||||
selectGroupLeft: 'Select word left',
|
||||
selectGroupRight: 'Select word right',
|
||||
deleteToLineEnd: 'Delete to line end',
|
||||
deleteToLineStart: 'Delete to line start',
|
||||
cursorLineStart: 'Cursor to line start',
|
||||
cursorLineEnd: 'Cursor to line end',
|
||||
selectLineStart: 'Select to line start',
|
||||
selectLineEnd: 'Select to line end',
|
||||
cursorDocStart: 'Cursor to document start',
|
||||
cursorDocEnd: 'Cursor to document end',
|
||||
selectDocStart: 'Select to document start',
|
||||
selectDocEnd: 'Select to document end',
|
||||
selectMatchingBracket: 'Select to matching bracket',
|
||||
splitLine: 'Split line',
|
||||
indentLess: 'Indent less',
|
||||
indentMore: 'Indent more',
|
||||
indentSelection: 'Indent selection',
|
||||
@@ -104,6 +153,18 @@ export default {
|
||||
deleteCharForward: 'Delete character forward',
|
||||
deleteGroupBackward: 'Delete group backward',
|
||||
deleteGroupForward: 'Delete group forward',
|
||||
|
||||
// Emacs mode additional basic navigation commands
|
||||
cursorCharLeft: 'Cursor left one character',
|
||||
cursorCharRight: 'Cursor right one character',
|
||||
cursorLineUp: 'Cursor up one line',
|
||||
cursorLineDown: 'Cursor down one line',
|
||||
cursorPageUp: 'Page up',
|
||||
cursorPageDown: 'Page down',
|
||||
selectCharLeft: 'Select left one character',
|
||||
selectCharRight: 'Select right one character',
|
||||
selectLineUp: 'Select up one line',
|
||||
selectLineDown: 'Select down one line',
|
||||
}
|
||||
},
|
||||
tabs: {
|
||||
@@ -139,6 +200,7 @@ export default {
|
||||
enableWindowSnap: 'Enable Window Snapping',
|
||||
enableLoadingAnimation: 'Enable Loading Animation',
|
||||
enableTabs: 'Enable Tabs',
|
||||
enableMemoryMonitor: 'Enable Memory Monitor',
|
||||
startup: 'Startup Settings',
|
||||
startAtLogin: 'Start at Login',
|
||||
dataStorage: 'Data Storage',
|
||||
@@ -184,6 +246,7 @@ export default {
|
||||
categoryEditing: 'Editing Enhancement',
|
||||
categoryUI: 'UI Enhancement',
|
||||
categoryTools: 'Tools',
|
||||
enabled: 'Enabled',
|
||||
configuration: 'Configuration',
|
||||
resetToDefault: 'Reset to Default Configuration',
|
||||
},
|
||||
@@ -290,6 +353,11 @@ export default {
|
||||
httpClient: {
|
||||
name: 'HTTP Client',
|
||||
description: 'Send HTTP requests directly in the editor and view responses'
|
||||
},
|
||||
blockImage: {
|
||||
name: 'Block Image Export',
|
||||
description: 'Render the current code block to an image and copy it to the clipboard',
|
||||
copyMenu: 'Copy block as image'
|
||||
}
|
||||
},
|
||||
monitor: {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
export default {
|
||||
locale: 'zh-CN',
|
||||
common: {
|
||||
ok: '确定',
|
||||
cancel: '取消',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
confirm: '确认',
|
||||
save: '保存',
|
||||
reset: '重置'
|
||||
},
|
||||
titlebar: {
|
||||
minimize: '最小化',
|
||||
maximize: '最大化',
|
||||
@@ -44,11 +53,31 @@ export default {
|
||||
auto: '跟随系统'
|
||||
},
|
||||
keybindings: {
|
||||
keymapMode: '快捷键模式',
|
||||
modes: {
|
||||
standard: '标准模式',
|
||||
emacs: 'Emacs 模式'
|
||||
},
|
||||
headers: {
|
||||
shortcut: '快捷键',
|
||||
extension: '扩展',
|
||||
description: '描述'
|
||||
},
|
||||
resetToDefault: '重置为默认',
|
||||
confirmReset: '确认重置?',
|
||||
noKeybinding: '未设置',
|
||||
waitingForKey: '等待输入...',
|
||||
clickToSet: '点击设置快捷键',
|
||||
editKeybinding: '编辑快捷键',
|
||||
config: {
|
||||
enabled: '启用',
|
||||
preventDefault: '阻止默认',
|
||||
keybinding: '快捷键'
|
||||
},
|
||||
keyPlaceholder: '输入键名, 回车添加',
|
||||
invalidFormat: '格式错误',
|
||||
conflict: '冲突: {command}',
|
||||
maxKeysReached: '最多只能添加4个键',
|
||||
commands: {
|
||||
showSearch: '显示搜索面板',
|
||||
hideSearch: '隐藏搜索面板',
|
||||
@@ -76,6 +105,7 @@ export default {
|
||||
blockCopy: '复制',
|
||||
blockCut: '剪切',
|
||||
blockPaste: '粘贴',
|
||||
copyBlockImage: '复制块图片',
|
||||
historyUndo: '撤销',
|
||||
historyRedo: '重做',
|
||||
historyUndoSelection: '撤销选择',
|
||||
@@ -93,6 +123,25 @@ export default {
|
||||
insertBlankLine: '插入空行',
|
||||
selectLine: '选择行',
|
||||
selectParentSyntax: '选择父级语法',
|
||||
simplifySelection: '简化选择',
|
||||
addCursorAbove: '在上方添加光标',
|
||||
addCursorBelow: '在下方添加光标',
|
||||
cursorGroupLeft: '光标按单词左移',
|
||||
cursorGroupRight: '光标按单词右移',
|
||||
selectGroupLeft: '按单词选择左侧',
|
||||
selectGroupRight: '按单词选择右侧',
|
||||
deleteToLineEnd: '删除到行尾',
|
||||
deleteToLineStart: '删除到行首',
|
||||
cursorLineStart: '移动到行首',
|
||||
cursorLineEnd: '移动到行尾',
|
||||
selectLineStart: '选择到行首',
|
||||
selectLineEnd: '选择到行尾',
|
||||
cursorDocStart: '跳转到文档开头',
|
||||
cursorDocEnd: '跳转到文档结尾',
|
||||
selectDocStart: '选择到文档开头',
|
||||
selectDocEnd: '选择到文档结尾',
|
||||
selectMatchingBracket: '选择到匹配括号',
|
||||
splitLine: '分割行',
|
||||
indentLess: '减少缩进',
|
||||
indentMore: '增加缩进',
|
||||
indentSelection: '缩进选择',
|
||||
@@ -104,6 +153,18 @@ export default {
|
||||
deleteCharForward: '向前删除字符',
|
||||
deleteGroupBackward: '向后删除组',
|
||||
deleteGroupForward: '向前删除组',
|
||||
|
||||
// Emacs 模式额外的基础导航命令
|
||||
cursorCharLeft: '光标左移一个字符',
|
||||
cursorCharRight: '光标右移一个字符',
|
||||
cursorLineUp: '光标上移一行',
|
||||
cursorLineDown: '光标下移一行',
|
||||
cursorPageUp: '向上翻页',
|
||||
cursorPageDown: '向下翻页',
|
||||
selectCharLeft: '选择左移一个字符',
|
||||
selectCharRight: '选择右移一个字符',
|
||||
selectLineUp: '选择上移一行',
|
||||
selectLineDown: '选择下移一行',
|
||||
}
|
||||
},
|
||||
tabs: {
|
||||
@@ -140,6 +201,7 @@ export default {
|
||||
enableWindowSnap: '启用窗口吸附',
|
||||
enableLoadingAnimation: '启用加载动画',
|
||||
enableTabs: '启用标签页',
|
||||
enableMemoryMonitor: '启用内存监视器',
|
||||
startup: '启动设置',
|
||||
startAtLogin: '开机自启动',
|
||||
dataStorage: '数据存储',
|
||||
@@ -187,6 +249,7 @@ export default {
|
||||
categoryEditing: '编辑增强',
|
||||
categoryUI: '界面增强',
|
||||
categoryTools: '工具扩展',
|
||||
enabled: '启用',
|
||||
configuration: '配置',
|
||||
resetToDefault: '重置为默认配置',
|
||||
},
|
||||
@@ -292,6 +355,11 @@ export default {
|
||||
httpClient: {
|
||||
name: 'HTTP 客户端',
|
||||
description: '在编辑器中直接发送 HTTP 请求并查看响应'
|
||||
},
|
||||
blockImage: {
|
||||
name: '代码块导出图片',
|
||||
description: '将当前代码块渲染为图片并复制到剪贴板',
|
||||
copyMenu: '复制块为图片'
|
||||
}
|
||||
},
|
||||
monitor: {
|
||||
|
||||
@@ -6,6 +6,9 @@ import i18n from './i18n';
|
||||
import router from './router';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
import { registerDirectives } from './directives';
|
||||
import {EditorView} from "@codemirror/view";
|
||||
|
||||
(EditorView as any).EDIT_CONTEXT = false;
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { BackupService } from '@/../bindings/voidraft/internal/services';
|
||||
|
||||
export const useBackupStore = defineStore('backup', () => {
|
||||
const isSyncing = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const sync = async (): Promise<void> => {
|
||||
if (isSyncing.value) {
|
||||
@@ -12,12 +11,11 @@ export const useBackupStore = defineStore('backup', () => {
|
||||
}
|
||||
|
||||
isSyncing.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
await BackupService.Sync();
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
throw e;
|
||||
} finally {
|
||||
isSyncing.value = false;
|
||||
}
|
||||
@@ -25,7 +23,6 @@ export const useBackupStore = defineStore('backup', () => {
|
||||
|
||||
return {
|
||||
isSyncing,
|
||||
error,
|
||||
sync
|
||||
};
|
||||
});
|
||||
@@ -1,18 +1,10 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, reactive} from 'vue';
|
||||
import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/services';
|
||||
import {
|
||||
AppConfig,
|
||||
AuthMethod,
|
||||
EditingConfig,
|
||||
LanguageType,
|
||||
SystemThemeType,
|
||||
TabType
|
||||
} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {AppConfig, AuthMethod, LanguageType, SystemThemeType, TabType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {ConfigUtils} from '@/common/utils/configUtils';
|
||||
import {FONT_OPTIONS} from '@/common/constant/fonts';
|
||||
import {SUPPORTED_LOCALES} from '@/common/constant/locales';
|
||||
import {
|
||||
CONFIG_KEY_MAP,
|
||||
CONFIG_LIMITS,
|
||||
@@ -36,12 +28,6 @@ export const useConfigStore = defineStore('config', () => {
|
||||
// Font options (no longer localized)
|
||||
const fontOptions = computed(() => FONT_OPTIONS);
|
||||
|
||||
// 计算属性
|
||||
const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]);
|
||||
const limits = Object.fromEntries(
|
||||
(['fontSize', 'tabSize', 'lineHeight'] as const).map(key => [key, createLimitComputed(key)])
|
||||
) as Record<NumberConfigKey, ReturnType<typeof createLimitComputed>>;
|
||||
|
||||
// 统一配置更新方法
|
||||
const updateConfig = async <K extends ConfigKey>(key: K, value: any): Promise<void> => {
|
||||
if (!state.configLoaded && !state.isLoading) {
|
||||
@@ -99,39 +85,12 @@ export const useConfigStore = defineStore('config', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通用数值调整器工厂
|
||||
const createAdjuster = <T extends NumberConfigKey>(key: T) => {
|
||||
const limit = CONFIG_LIMITS[key];
|
||||
const clamp = (value: number) => ConfigUtils.clamp(value, limit.min, limit.max);
|
||||
|
||||
return {
|
||||
increase: async () => await updateConfig(key, clamp(state.config.editing[key] + 1)),
|
||||
decrease: async () => await updateConfig(key, clamp(state.config.editing[key] - 1)),
|
||||
set: async (value: number) => await updateConfig(key, clamp(value)),
|
||||
reset: async () => await updateConfig(key, limit.default),
|
||||
increaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] + 1)),
|
||||
decreaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] - 1))
|
||||
};
|
||||
};
|
||||
|
||||
const createEditingToggler = <T extends keyof EditingConfig>(key: T) =>
|
||||
async () => await updateConfig(key as ConfigKey, !state.config.editing[key] as EditingConfig[T]);
|
||||
|
||||
// 枚举值切换器
|
||||
const createEnumToggler = <T extends TabType>(key: 'tabType', values: readonly T[]) =>
|
||||
async () => {
|
||||
const currentIndex = values.indexOf(state.config.editing[key] as T);
|
||||
const nextIndex = (currentIndex + 1) % values.length;
|
||||
return await updateConfig(key, values[nextIndex]);
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const resetConfig = async (): Promise<void> => {
|
||||
if (state.isLoading) return;
|
||||
|
||||
state.isLoading = true;
|
||||
try {
|
||||
|
||||
await ConfigService.ResetConfig();
|
||||
const appConfig = await ConfigService.GetConfig();
|
||||
if (appConfig) {
|
||||
@@ -142,57 +101,25 @@ export const useConfigStore = defineStore('config', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 语言设置方法
|
||||
const setLanguage = async (language: LanguageType): Promise<void> => {
|
||||
await updateConfig('language', language);
|
||||
const frontendLocale = ConfigUtils.backendLanguageToFrontend(language);
|
||||
locale.value = frontendLocale as any;
|
||||
// 辅助函数:限制数值范围
|
||||
const clampValue = (value: number, key: NumberConfigKey): number => {
|
||||
const limit = CONFIG_LIMITS[key];
|
||||
return ConfigUtils.clamp(value, limit.min, limit.max);
|
||||
};
|
||||
|
||||
// 系统主题设置方法
|
||||
const setSystemTheme = async (systemTheme: SystemThemeType): Promise<void> => {
|
||||
await updateConfig('systemTheme', systemTheme);
|
||||
};
|
||||
// 计算属性
|
||||
const fontConfig = computed(() => ({
|
||||
fontSize: state.config.editing.fontSize,
|
||||
fontFamily: state.config.editing.fontFamily,
|
||||
lineHeight: state.config.editing.lineHeight,
|
||||
fontWeight: state.config.editing.fontWeight
|
||||
}));
|
||||
|
||||
// 当前主题设置方法
|
||||
const setCurrentTheme = async (themeName: string): Promise<void> => {
|
||||
await updateConfig('currentTheme', themeName);
|
||||
};
|
||||
|
||||
|
||||
// 初始化语言设置
|
||||
const initLanguage = async (): Promise<void> => {
|
||||
try {
|
||||
// 如果配置未加载,先加载配置
|
||||
if (!state.configLoaded) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
// 同步前端语言设置
|
||||
const frontendLocale = ConfigUtils.backendLanguageToFrontend(state.config.appearance.language);
|
||||
locale.value = frontendLocale as any;
|
||||
} catch (_error) {
|
||||
const browserLang = SUPPORTED_LOCALES[0].code;
|
||||
locale.value = browserLang as any;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建数值调整器实例
|
||||
const adjusters = {
|
||||
fontSize: createAdjuster('fontSize'),
|
||||
tabSize: createAdjuster('tabSize'),
|
||||
lineHeight: createAdjuster('lineHeight')
|
||||
};
|
||||
|
||||
// 创建切换器实例
|
||||
const togglers = {
|
||||
tabIndent: createEditingToggler('enableTabIndent'),
|
||||
alwaysOnTop: async () => {
|
||||
await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
|
||||
await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop);
|
||||
},
|
||||
tabType: createEnumToggler('tabType', CONFIG_LIMITS.tabType.values)
|
||||
};
|
||||
const tabConfig = computed(() => ({
|
||||
tabSize: state.config.editing.tabSize,
|
||||
enableTabIndent: state.config.editing.enableTabIndent,
|
||||
tabType: state.config.editing.tabType
|
||||
}));
|
||||
|
||||
return {
|
||||
// 状态
|
||||
@@ -200,53 +127,84 @@ export const useConfigStore = defineStore('config', () => {
|
||||
configLoaded: computed(() => state.configLoaded),
|
||||
isLoading: computed(() => state.isLoading),
|
||||
fontOptions,
|
||||
|
||||
// 限制常量
|
||||
...limits,
|
||||
fontConfig,
|
||||
tabConfig,
|
||||
|
||||
// 核心方法
|
||||
initConfig,
|
||||
resetConfig,
|
||||
|
||||
// 语言相关方法
|
||||
setLanguage,
|
||||
initLanguage,
|
||||
setLanguage: (value: LanguageType) => {
|
||||
updateConfig('language', value);
|
||||
locale.value = value as any;
|
||||
},
|
||||
|
||||
// 主题相关方法
|
||||
setSystemTheme,
|
||||
setCurrentTheme,
|
||||
setSystemTheme: (value: SystemThemeType) => updateConfig('systemTheme', value),
|
||||
setCurrentTheme: (value: string) => updateConfig('currentTheme', value),
|
||||
|
||||
// 字体大小操作
|
||||
...adjusters.fontSize,
|
||||
increaseFontSize: adjusters.fontSize.increase,
|
||||
decreaseFontSize: adjusters.fontSize.decrease,
|
||||
resetFontSize: adjusters.fontSize.reset,
|
||||
setFontSize: adjusters.fontSize.set,
|
||||
// 字体大小操作
|
||||
increaseFontSizeLocal: adjusters.fontSize.increaseLocal,
|
||||
decreaseFontSizeLocal: adjusters.fontSize.decreaseLocal,
|
||||
saveFontSize: () => saveConfig('fontSize'),
|
||||
|
||||
// Tab操作
|
||||
toggleTabIndent: togglers.tabIndent,
|
||||
setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value),
|
||||
...adjusters.tabSize,
|
||||
increaseTabSize: adjusters.tabSize.increase,
|
||||
decreaseTabSize: adjusters.tabSize.decrease,
|
||||
setTabSize: adjusters.tabSize.set,
|
||||
toggleTabType: togglers.tabType,
|
||||
|
||||
// 行高操作
|
||||
setLineHeight: adjusters.lineHeight.set,
|
||||
|
||||
// 窗口操作
|
||||
toggleAlwaysOnTop: togglers.alwaysOnTop,
|
||||
setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value),
|
||||
setFontSize: async (value: number) => {
|
||||
await updateConfig('fontSize', clampValue(value, 'fontSize'));
|
||||
},
|
||||
increaseFontSize: async () => {
|
||||
const newValue = state.config.editing.fontSize + 1;
|
||||
await updateConfig('fontSize', clampValue(newValue, 'fontSize'));
|
||||
},
|
||||
decreaseFontSize: async () => {
|
||||
const newValue = state.config.editing.fontSize - 1;
|
||||
await updateConfig('fontSize', clampValue(newValue, 'fontSize'));
|
||||
},
|
||||
resetFontSize: async () => {
|
||||
await updateConfig('fontSize', CONFIG_LIMITS.fontSize.default);
|
||||
},
|
||||
increaseFontSizeLocal: () => {
|
||||
updateConfigLocal('fontSize', clampValue(state.config.editing.fontSize + 1, 'fontSize'));
|
||||
},
|
||||
decreaseFontSizeLocal: () => {
|
||||
updateConfigLocal('fontSize', clampValue(state.config.editing.fontSize - 1, 'fontSize'));
|
||||
},
|
||||
saveFontSize: async () => {
|
||||
await saveConfig('fontSize');
|
||||
},
|
||||
|
||||
// 字体操作
|
||||
setFontFamily: (value: string) => updateConfig('fontFamily', value),
|
||||
setFontWeight: (value: string) => updateConfig('fontWeight', value),
|
||||
|
||||
// 行高操作
|
||||
setLineHeight: async (value: number) => {
|
||||
await updateConfig('lineHeight', clampValue(value, 'lineHeight'));
|
||||
},
|
||||
|
||||
// Tab操作
|
||||
setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value),
|
||||
setTabSize: async (value: number) => {
|
||||
await updateConfig('tabSize', clampValue(value, 'tabSize'));
|
||||
},
|
||||
increaseTabSize: async () => {
|
||||
const newValue = state.config.editing.tabSize + 1;
|
||||
await updateConfig('tabSize', clampValue(newValue, 'tabSize'));
|
||||
},
|
||||
decreaseTabSize: async () => {
|
||||
const newValue = state.config.editing.tabSize - 1;
|
||||
await updateConfig('tabSize', clampValue(newValue, 'tabSize'));
|
||||
},
|
||||
toggleTabType: async () => {
|
||||
const values = CONFIG_LIMITS.tabType.values;
|
||||
const currentIndex = values.indexOf(state.config.editing.tabType as typeof values[number]);
|
||||
const nextIndex = (currentIndex + 1) % values.length;
|
||||
await updateConfig('tabType', values[nextIndex]);
|
||||
},
|
||||
|
||||
// 窗口操作
|
||||
toggleAlwaysOnTop: async () => {
|
||||
await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
|
||||
await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop);
|
||||
},
|
||||
setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value),
|
||||
|
||||
// 路径操作
|
||||
setDataPath: (value: string) => updateConfigLocal('dataPath', value),
|
||||
|
||||
@@ -275,6 +233,12 @@ export const useConfigStore = defineStore('config', () => {
|
||||
// 标签页配置相关方法
|
||||
setEnableTabs: (value: boolean) => updateConfig('enableTabs', value),
|
||||
|
||||
// 内存监视器配置相关方法
|
||||
setEnableMemoryMonitor: (value: boolean) => updateConfig('enableMemoryMonitor', value),
|
||||
|
||||
// 快捷键模式配置相关方法
|
||||
setKeymapMode: (value: any) => updateConfig('keymapMode', value),
|
||||
|
||||
// 更新配置相关方法
|
||||
setAutoUpdate: (value: boolean) => updateConfig('autoUpdate', value),
|
||||
|
||||
|
||||
@@ -1,79 +1,70 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, ref} from 'vue';
|
||||
import {ref} from 'vue';
|
||||
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import {useTabStore} from "@/stores/tabStore";
|
||||
import type {EditorViewState} from '@/stores/editorStore';
|
||||
import type {TimerManager} from '@/common/utils/timerUtils';
|
||||
import {createTimerManager} from '@/common/utils/timerUtils';
|
||||
|
||||
export const useDocumentStore = defineStore('document', () => {
|
||||
|
||||
// === 核心状态 ===
|
||||
const documents = ref<Record<number, Document>>({});
|
||||
const currentDocumentId = ref<number | null>(null);
|
||||
const currentDocument = ref<Document | null>(null);
|
||||
|
||||
// === 编辑器状态持久化 ===
|
||||
const documentStates = ref<Record<number, EditorViewState>>({});
|
||||
// 自动保存定时器
|
||||
const autoSaveTimers = ref<Map<number, TimerManager>>(new Map());
|
||||
|
||||
// === UI状态 ===
|
||||
const showDocumentSelector = ref(false);
|
||||
const selectorError = ref<{ docId: number; message: string } | null>(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// === 计算属性 ===
|
||||
const documentList = computed(() =>
|
||||
Object.values(documents.value).sort((a, b) => {
|
||||
const timeA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
|
||||
const timeB = b.updated_at ? new Date(b.updated_at).getTime() : 0;
|
||||
return timeB - timeA;
|
||||
})
|
||||
);
|
||||
|
||||
const setDocuments = (docs: Document[]) => {
|
||||
documents.value = {};
|
||||
docs.forEach(doc => {
|
||||
if (doc.id !== undefined) {
|
||||
documents.value[doc.id] = doc;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// === 错误处理 ===
|
||||
const setError = (docId: number, message: string) => {
|
||||
selectorError.value = {docId, message};
|
||||
// 3秒后自动清除错误状态
|
||||
setTimeout(() => {
|
||||
if (selectorError.value?.docId === docId) {
|
||||
selectorError.value = null;
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
selectorError.value = null;
|
||||
};
|
||||
|
||||
// === UI控制方法 ===
|
||||
const openDocumentSelector = () => {
|
||||
showDocumentSelector.value = true;
|
||||
clearError();
|
||||
};
|
||||
|
||||
const closeDocumentSelector = () => {
|
||||
showDocumentSelector.value = false;
|
||||
clearError();
|
||||
};
|
||||
|
||||
|
||||
// 获取文档列表
|
||||
const getDocumentList = async (): Promise<Document[]> => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const docs = await DocumentService.ListAllDocumentsMeta();
|
||||
return docs?.filter((doc): doc is Document => doc !== null) || [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取单个文档
|
||||
const getDocument = async (docId: number): Promise<Document | null> => {
|
||||
try {
|
||||
return await DocumentService.GetDocumentByID(docId);
|
||||
} catch (error) {
|
||||
console.error('Failed to get document:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存文档内容
|
||||
const saveDocument = async (docId: number, content: string): Promise<Document | null> => {
|
||||
try {
|
||||
await DocumentService.UpdateDocumentContent(docId, content);
|
||||
return await DocumentService.GetDocumentByID(docId);
|
||||
} catch (error) {
|
||||
console.error('Failed to save document:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 在新窗口中打开文档
|
||||
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
const tabStore = useTabStore();
|
||||
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
|
||||
tabStore.closeTab(docId);
|
||||
}
|
||||
await OpenDocumentWindow(docId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -86,36 +77,16 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
const createNewDocument = async (title: string): Promise<Document | null> => {
|
||||
try {
|
||||
const doc = await DocumentService.CreateDocument(title);
|
||||
if (doc && doc.id !== undefined) {
|
||||
documents.value[doc.id] = doc;
|
||||
return doc;
|
||||
}
|
||||
return null;
|
||||
return doc || null;
|
||||
} catch (error) {
|
||||
console.error('Failed to create document:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取文档列表
|
||||
const getDocumentMetaList = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const docs = await DocumentService.ListAllDocumentsMeta();
|
||||
if (docs) {
|
||||
setDocuments(docs.filter((doc): doc is Document => doc !== null));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update documents:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开文档
|
||||
const openDocument = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
// 获取完整文档数据
|
||||
const doc = await DocumentService.GetDocumentByID(docId);
|
||||
if (!doc) {
|
||||
throw new Error(`Document ${docId} not found`);
|
||||
@@ -123,7 +94,6 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
|
||||
currentDocumentId.value = docId;
|
||||
currentDocument.value = doc;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to open document:', error);
|
||||
@@ -131,30 +101,20 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 更新文档元数据
|
||||
const updateDocumentMetadata = async (docId: number, title: string): Promise<boolean> => {
|
||||
// 更新文档标题
|
||||
const updateDocumentTitle = async (docId: number, title: string): Promise<boolean> => {
|
||||
try {
|
||||
await DocumentService.UpdateDocumentTitle(docId, title);
|
||||
|
||||
// 更新本地状态
|
||||
const doc = documents.value[docId];
|
||||
if (doc) {
|
||||
doc.title = title;
|
||||
doc.updated_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
// 更新当前文档状态
|
||||
if (currentDocument.value?.id === docId) {
|
||||
currentDocument.value.title = title;
|
||||
currentDocument.value.updated_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
// 同步更新标签页标题
|
||||
const tabStore = useTabStore();
|
||||
tabStore.updateTabTitle(docId, title);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update document metadata:', error);
|
||||
console.error('Failed to update document title:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -164,20 +124,18 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
try {
|
||||
await DocumentService.DeleteDocument(docId);
|
||||
|
||||
// 更新本地状态
|
||||
delete documents.value[docId];
|
||||
|
||||
// 同步清理标签页
|
||||
const tabStore = useTabStore();
|
||||
if (tabStore.hasTab(docId)) {
|
||||
tabStore.closeTab(docId);
|
||||
// 清理定时器
|
||||
const timer = autoSaveTimers.value.get(docId);
|
||||
if (timer) {
|
||||
timer.clear();
|
||||
autoSaveTimers.value.delete(docId);
|
||||
}
|
||||
|
||||
// 如果删除的是当前文档,切换到第一个可用文档
|
||||
if (currentDocumentId.value === docId) {
|
||||
const availableDocs = Object.values(documents.value);
|
||||
if (availableDocs.length > 0 && availableDocs[0].id !== undefined) {
|
||||
await openDocument(availableDocs[0].id);
|
||||
const docs = await getDocumentList();
|
||||
if (docs.length > 0 && docs[0].id !== undefined) {
|
||||
await openDocument(docs[0].id);
|
||||
} else {
|
||||
currentDocumentId.value = null;
|
||||
currentDocument.value = null;
|
||||
@@ -191,22 +149,45 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// === 初始化 ===
|
||||
const initialize = async (urlDocumentId?: number): Promise<void> => {
|
||||
// 调度自动保存
|
||||
const scheduleAutoSave = (docId: number, saveCallback: () => Promise<void>, delay: number = 2000) => {
|
||||
let timer = autoSaveTimers.value.get(docId);
|
||||
if (!timer) {
|
||||
timer = createTimerManager();
|
||||
autoSaveTimers.value.set(docId, timer);
|
||||
}
|
||||
|
||||
timer.set(async () => {
|
||||
try {
|
||||
await saveCallback();
|
||||
} catch (error) {
|
||||
console.error(`auto save for document ${docId} failed:`, error);
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
// 取消自动保存
|
||||
const cancelAutoSave = (docId: number) => {
|
||||
const timer = autoSaveTimers.value.get(docId);
|
||||
if (timer) {
|
||||
timer.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化文档
|
||||
const initDocument = async (urlDocumentId?: number): Promise<void> => {
|
||||
try {
|
||||
await getDocumentMetaList();
|
||||
const docs = await getDocumentList();
|
||||
|
||||
// 优先使用URL参数中的文档ID
|
||||
if (urlDocumentId && documents.value[urlDocumentId]) {
|
||||
if (urlDocumentId) {
|
||||
await openDocument(urlDocumentId);
|
||||
} else if (currentDocumentId.value && documents.value[currentDocumentId.value]) {
|
||||
// 如果URL中没有指定文档ID,则使用持久化的文档ID
|
||||
} else if (currentDocumentId.value) {
|
||||
// 使用持久化的文档ID
|
||||
await openDocument(currentDocumentId.value);
|
||||
} else {
|
||||
// 否则打开第一个文档
|
||||
if (documents.value[0].id) {
|
||||
await openDocument(documents.value[0].id);
|
||||
}
|
||||
} else if (docs.length > 0 && docs[0].id !== undefined) {
|
||||
// 打开第一个文档
|
||||
await openDocument(docs[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize document store:', error);
|
||||
@@ -215,32 +196,35 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
|
||||
return {
|
||||
// 状态
|
||||
documents,
|
||||
documentList,
|
||||
currentDocumentId,
|
||||
currentDocument,
|
||||
documentStates,
|
||||
showDocumentSelector,
|
||||
selectorError,
|
||||
isLoading,
|
||||
|
||||
// 方法
|
||||
getDocumentMetaList,
|
||||
getDocumentList,
|
||||
getDocument,
|
||||
saveDocument,
|
||||
createNewDocument,
|
||||
updateDocumentTitle,
|
||||
deleteDocument,
|
||||
openDocument,
|
||||
openDocumentInNewWindow,
|
||||
createNewDocument,
|
||||
updateDocumentMetadata,
|
||||
deleteDocument,
|
||||
|
||||
// 自动保存
|
||||
scheduleAutoSave,
|
||||
cancelAutoSave,
|
||||
|
||||
// UI 控制
|
||||
openDocumentSelector,
|
||||
closeDocumentSelector,
|
||||
setError,
|
||||
clearError,
|
||||
initialize,
|
||||
|
||||
// 初始化
|
||||
initDocument,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'voidraft-document',
|
||||
storage: localStorage,
|
||||
pick: ['currentDocumentId', 'documents', 'documentStates']
|
||||
pick: ['currentDocumentId']
|
||||
}
|
||||
});
|
||||
98
frontend/src/stores/editorStateStore.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {ref} from 'vue';
|
||||
|
||||
export interface DocumentStats {
|
||||
lines: number;
|
||||
characters: number;
|
||||
selectedCharacters: number;
|
||||
}
|
||||
|
||||
export interface FoldRange {
|
||||
// 字符偏移(备用)
|
||||
from: number;
|
||||
to: number;
|
||||
|
||||
// 行号
|
||||
fromLine: number;
|
||||
toLine: number;
|
||||
}
|
||||
|
||||
export const useEditorStateStore = defineStore('editorState', () => {
|
||||
// 光标位置存储 Record<docId, cursorPosition>
|
||||
const cursorPositions = ref<Record<number, number>>({});
|
||||
|
||||
// 文档统计数据存储 Record<docId, DocumentStats>
|
||||
const documentStats = ref<Record<number, DocumentStats>>({});
|
||||
|
||||
// 折叠状态存储 Record<docId, FoldRange[]>
|
||||
const foldStates = ref<Record<number, FoldRange[]>>({});
|
||||
|
||||
// 保存光标位置
|
||||
const saveCursorPosition = (docId: number, position: number) => {
|
||||
cursorPositions.value[docId] = position;
|
||||
};
|
||||
|
||||
// 获取光标位置
|
||||
const getCursorPosition = (docId: number): number | undefined => {
|
||||
return cursorPositions.value[docId];
|
||||
};
|
||||
|
||||
// 保存文档统计数据
|
||||
const saveDocumentStats = (docId: number, stats: DocumentStats) => {
|
||||
documentStats.value[docId] = stats;
|
||||
};
|
||||
|
||||
// 获取文档统计数据
|
||||
const getDocumentStats = (docId: number): DocumentStats => {
|
||||
return documentStats.value[docId] || {
|
||||
lines: 0,
|
||||
characters: 0,
|
||||
selectedCharacters: 0
|
||||
};
|
||||
};
|
||||
|
||||
// 保存折叠状态
|
||||
const saveFoldState = (docId: number, foldRanges: FoldRange[]) => {
|
||||
foldStates.value[docId] = foldRanges;
|
||||
};
|
||||
|
||||
// 获取折叠状态
|
||||
const getFoldState = (docId: number): FoldRange[] => {
|
||||
return foldStates.value[docId] || [];
|
||||
};
|
||||
|
||||
// 清除文档状态
|
||||
const clearDocumentState = (docId: number) => {
|
||||
delete cursorPositions.value[docId];
|
||||
delete documentStats.value[docId];
|
||||
delete foldStates.value[docId];
|
||||
};
|
||||
|
||||
// 清除所有状态
|
||||
const clearAllStates = () => {
|
||||
cursorPositions.value = {};
|
||||
documentStats.value = {};
|
||||
foldStates.value = {};
|
||||
};
|
||||
|
||||
return {
|
||||
cursorPositions,
|
||||
documentStats,
|
||||
foldStates,
|
||||
saveCursorPosition,
|
||||
getCursorPosition,
|
||||
saveDocumentStats,
|
||||
getDocumentStats,
|
||||
saveFoldState,
|
||||
getFoldState,
|
||||
clearDocumentState,
|
||||
clearAllStates
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'voidraft-editor-state',
|
||||
storage: localStorage,
|
||||
pick: ['cursorPositions', 'foldStates']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, nextTick, ref, watch} from 'vue';
|
||||
import {computed, readonly, ref} from 'vue';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {EditorState, Extension} from '@codemirror/state';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useDocumentStore} from './documentStore';
|
||||
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
|
||||
import {ensureSyntaxTree} from "@codemirror/language";
|
||||
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
|
||||
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
|
||||
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
|
||||
@@ -14,140 +13,69 @@ import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
||||
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
|
||||
import {createCursorPositionExtension, scrollToCursor} from '@/views/editor/basic/cursorPositionExtension';
|
||||
import {createFoldStateExtension, restoreFoldState} from '@/views/editor/basic/foldStateExtension';
|
||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||
import {
|
||||
createDynamicExtensions,
|
||||
getExtensionManager,
|
||||
removeExtensionManagerView,
|
||||
setExtensionManagerView
|
||||
} from '@/views/editor/manager';
|
||||
import {useExtensionStore} from './extensionStore';
|
||||
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
||||
import {LruCache} from '@/common/utils/lruCache';
|
||||
import {AsyncManager} from '@/common/utils/asyncManager';
|
||||
import {generateContentHash} from "@/common/utils/hashUtils";
|
||||
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
|
||||
import {EDITOR_CONFIG} from '@/common/constant/editor';
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
export interface DocumentStats {
|
||||
lines: number;
|
||||
characters: number;
|
||||
selectedCharacters: number;
|
||||
}
|
||||
|
||||
// 修复:只保存光标位置,恢复时自动滚动到光标处
|
||||
export interface EditorViewState {
|
||||
cursorPos: number;
|
||||
}
|
||||
import {useEditorStateStore, type DocumentStats} from './editorStateStore';
|
||||
|
||||
// 编辑器实例
|
||||
interface EditorInstance {
|
||||
view: EditorView;
|
||||
documentId: number;
|
||||
content: string;
|
||||
contentTimestamp: string; // 文档时间戳
|
||||
contentLength: number; // 内容长度
|
||||
isDirty: boolean;
|
||||
lastModified: Date;
|
||||
autoSaveTimer: TimerManager;
|
||||
syntaxTreeCache: {
|
||||
lastDocLength: number;
|
||||
lastContentHash: string;
|
||||
lastParsed: Date;
|
||||
} | null;
|
||||
// 修复:使用统一的类型,可选但不是 undefined | {...}
|
||||
editorState?: EditorViewState;
|
||||
lastModified: number;
|
||||
}
|
||||
|
||||
export const useEditorStore = defineStore('editor', () => {
|
||||
// === 依赖store ===
|
||||
const configStore = useConfigStore();
|
||||
const documentStore = useDocumentStore();
|
||||
const extensionStore = useExtensionStore();
|
||||
const editorStateStore = useEditorStateStore();
|
||||
|
||||
// === 核心状态 ===
|
||||
const editorCache = new LruCache<number, EditorInstance>(EDITOR_CONFIG.MAX_INSTANCES);
|
||||
const containerElement = ref<HTMLElement | null>(null);
|
||||
|
||||
const currentEditor = ref<EditorView | null>(null);
|
||||
const documentStats = ref<DocumentStats>({
|
||||
lines: 0,
|
||||
characters: 0,
|
||||
selectedCharacters: 0
|
||||
});
|
||||
|
||||
// 编辑器加载状态
|
||||
const currentEditorId = ref<number | null>(null);
|
||||
const isLoading = ref(false);
|
||||
// 修复:使用操作计数器精确管理加载状态
|
||||
const loadingOperations = ref(0);
|
||||
|
||||
// 异步操作管理器
|
||||
const operationManager = new AsyncManager<number>();
|
||||
|
||||
// 自动保存设置 - 从配置动态获取
|
||||
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
|
||||
|
||||
// 创建防抖的语法树缓存清理函数
|
||||
const debouncedClearSyntaxCache = createDebounce((instance) => {
|
||||
if (instance) {
|
||||
instance.syntaxTreeCache = null;
|
||||
}
|
||||
}, { delay: 500 }); // 500ms 内的多次输入只清理一次
|
||||
|
||||
|
||||
// 缓存化的语法树确保方法
|
||||
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
||||
const instance = editorCache.get(documentId);
|
||||
if (!instance) return;
|
||||
// 验证缓存是否有效
|
||||
const isCacheValid = (cached: EditorInstance, doc: Document): boolean => {
|
||||
return cached.contentTimestamp === doc.updated_at
|
||||
&& cached.contentLength === (doc.content || '').length;
|
||||
};
|
||||
|
||||
const docLength = view.state.doc.length;
|
||||
const content = view.state.doc.toString();
|
||||
const contentHash = generateContentHash(content);
|
||||
const now = new Date();
|
||||
|
||||
// 检查是否需要重新构建语法树
|
||||
const cache = instance.syntaxTreeCache;
|
||||
const shouldRebuild = !cache ||
|
||||
cache.lastDocLength !== docLength ||
|
||||
cache.lastContentHash !== contentHash ||
|
||||
(now.getTime() - cache.lastParsed.getTime()) > EDITOR_CONFIG.SYNTAX_TREE_CACHE_TIMEOUT;
|
||||
|
||||
if (shouldRebuild) {
|
||||
try {
|
||||
ensureSyntaxTree(view.state, docLength, 5000);
|
||||
|
||||
// 更新缓存
|
||||
instance.syntaxTreeCache = {
|
||||
lastDocLength: docLength,
|
||||
lastContentHash: contentHash,
|
||||
lastParsed: now
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to ensure syntax tree:', error);
|
||||
}
|
||||
}
|
||||
// 检查内容是否真的不同
|
||||
const hasContentChanged = (cached: EditorInstance, doc: Document): boolean => {
|
||||
const currentContent = cached.view.state.doc.toString();
|
||||
return currentContent !== (doc.content || '');
|
||||
};
|
||||
|
||||
// 创建编辑器实例
|
||||
const createEditorInstance = async (
|
||||
content: string,
|
||||
operationId: number,
|
||||
documentId: number
|
||||
): Promise<EditorView> => {
|
||||
docId: number,
|
||||
doc: Document
|
||||
): Promise<EditorInstance> => {
|
||||
if (!containerElement.value) {
|
||||
throw new Error('Editor container not set');
|
||||
}
|
||||
|
||||
// 检查操作是否仍然有效
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
const content = doc.content || '';
|
||||
|
||||
// 获取基本扩展
|
||||
// 基本扩展
|
||||
const basicExtensions = createBasicSetup();
|
||||
|
||||
// 获取主题扩展
|
||||
// 主题扩展
|
||||
const themeExtension = createThemeExtension();
|
||||
|
||||
// Tab相关扩展
|
||||
// Tab 扩展
|
||||
const tabExtensions = getTabExtensions(
|
||||
configStore.config.editing.tabSize,
|
||||
configStore.config.editing.enableTabIndent,
|
||||
@@ -162,6 +90,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
fontWeight: configStore.config.editing.fontWeight
|
||||
});
|
||||
|
||||
// 滚轮缩放扩展
|
||||
const wheelZoomExtension = createWheelZoomExtension({
|
||||
increaseFontSize: () => configStore.increaseFontSizeLocal(),
|
||||
decreaseFontSize: () => configStore.decreaseFontSizeLocal(),
|
||||
@@ -170,10 +99,16 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
});
|
||||
|
||||
// 统计扩展
|
||||
const statsExtension = createStatsUpdateExtension(updateDocumentStats);
|
||||
const statsExtension = createStatsUpdateExtension((stats: DocumentStats) => {
|
||||
if (currentEditorId.value) {
|
||||
editorStateStore.saveDocumentStats(currentEditorId.value, stats);
|
||||
}
|
||||
});
|
||||
|
||||
// 内容变化扩展
|
||||
const contentChangeExtension = createContentChangePlugin();
|
||||
const contentChangeExtension = createContentChangePlugin(() => {
|
||||
handleContentChange(docId);
|
||||
});
|
||||
|
||||
// 代码块扩展
|
||||
const codeBlockExtension = createCodeBlockExtension({
|
||||
@@ -182,29 +117,17 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
});
|
||||
|
||||
// 光标位置持久化扩展
|
||||
const cursorPositionExtension = createCursorPositionExtension(documentId);
|
||||
const cursorPositionExtension = createCursorPositionExtension(docId);
|
||||
|
||||
// 再次检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
// 折叠状态持久化扩展
|
||||
const foldStateExtension = createFoldStateExtension(docId);
|
||||
|
||||
// 快捷键扩展
|
||||
const keymapExtension = await createDynamicKeymapExtension();
|
||||
|
||||
// 检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
// 动态扩展,传递文档ID以便扩展管理器可以预初始化
|
||||
// 动态扩展
|
||||
const dynamicExtensions = await createDynamicExtensions();
|
||||
|
||||
// 最终检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
// 组合所有扩展
|
||||
const extensions: Extension[] = [
|
||||
keymapExtension,
|
||||
@@ -217,327 +140,303 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
contentChangeExtension,
|
||||
codeBlockExtension,
|
||||
cursorPositionExtension,
|
||||
foldStateExtension,
|
||||
...dynamicExtensions,
|
||||
];
|
||||
|
||||
// 获取保存的光标位置
|
||||
const savedState = documentStore.documentStates[documentId];
|
||||
const savedCursorPos = editorStateStore.getCursorPosition(docId);
|
||||
const docLength = content.length;
|
||||
const initialCursorPos = savedState?.cursorPos !== undefined
|
||||
? Math.min(savedState.cursorPos, docLength)
|
||||
const initialCursorPos = savedCursorPos !== undefined
|
||||
? Math.min(savedCursorPos, docLength)
|
||||
: docLength;
|
||||
|
||||
|
||||
// 创建编辑器状态,设置初始光标位置
|
||||
// 创建编辑器状态
|
||||
const state = EditorState.create({
|
||||
doc: content,
|
||||
extensions,
|
||||
selection: { anchor: initialCursorPos, head: initialCursorPos }
|
||||
selection: {anchor: initialCursorPos, head: initialCursorPos}
|
||||
});
|
||||
|
||||
return new EditorView({
|
||||
state
|
||||
});
|
||||
};
|
||||
const view = new EditorView({state});
|
||||
|
||||
// 添加编辑器到缓存
|
||||
const addEditorToCache = (documentId: number, view: EditorView, content: string) => {
|
||||
const instance: EditorInstance = {
|
||||
return {
|
||||
view,
|
||||
documentId,
|
||||
content,
|
||||
documentId: docId,
|
||||
contentTimestamp: doc.updated_at || '',
|
||||
contentLength: content.length,
|
||||
isDirty: false,
|
||||
lastModified: new Date(),
|
||||
autoSaveTimer: createTimerManager(),
|
||||
syntaxTreeCache: null,
|
||||
editorState: documentStore.documentStates[documentId]
|
||||
lastModified: Date.now()
|
||||
};
|
||||
|
||||
// 使用LRU缓存的onEvict回调处理被驱逐的实例
|
||||
editorCache.set(documentId, instance, (_evictedKey, evictedInstance) => {
|
||||
// 清除自动保存定时器
|
||||
evictedInstance.autoSaveTimer.clear();
|
||||
// 移除DOM元素
|
||||
if (evictedInstance.view.dom.parentElement) {
|
||||
evictedInstance.view.dom.remove();
|
||||
}
|
||||
evictedInstance.view.destroy();
|
||||
});
|
||||
|
||||
// 初始化语法树缓存
|
||||
ensureSyntaxTreeCached(view, documentId);
|
||||
};
|
||||
|
||||
// 获取或创建编辑器
|
||||
const getOrCreateEditor = async (
|
||||
documentId: number,
|
||||
content: string,
|
||||
operationId: number
|
||||
): Promise<EditorView> => {
|
||||
// 检查缓存
|
||||
const cached = editorCache.get(documentId);
|
||||
if (cached) {
|
||||
return cached.view;
|
||||
// 更新编辑器内容
|
||||
const updateEditorContent = (instance: EditorInstance, doc: Document) => {
|
||||
const currentContent = instance.view.state.doc.toString();
|
||||
const newContent = doc.content || '';
|
||||
|
||||
// 如果内容相同,只更新元数据
|
||||
if (currentContent === newContent) {
|
||||
instance.contentTimestamp = doc.updated_at || '';
|
||||
instance.contentLength = newContent.length;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查操作是否仍然有效
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
}
|
||||
// 保存当前光标位置
|
||||
const currentCursorPos = instance.view.state.selection.main.head;
|
||||
|
||||
// 创建新的编辑器实例
|
||||
const view = await createEditorInstance(content, operationId, documentId);
|
||||
|
||||
// 完善取消操作时的清理逻辑
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
// 如果操作已取消,彻底清理创建的实例
|
||||
try {
|
||||
// 移除 DOM 元素(如果已添加到文档)
|
||||
if (view.dom && view.dom.parentElement) {
|
||||
view.dom.remove();
|
||||
}
|
||||
// 销毁编辑器视图
|
||||
view.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up cancelled editor:', error);
|
||||
// 更新内容
|
||||
instance.view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: instance.view.state.doc.length,
|
||||
insert: newContent
|
||||
}
|
||||
throw new Error('Operation cancelled');
|
||||
});
|
||||
|
||||
// 智能恢复光标位置
|
||||
const newContentLength = newContent.length;
|
||||
const safeCursorPos = Math.min(currentCursorPos, newContentLength);
|
||||
|
||||
if (safeCursorPos > 0 && safeCursorPos < newContentLength) {
|
||||
instance.view.dispatch({
|
||||
selection: {anchor: safeCursorPos, head: safeCursorPos}
|
||||
});
|
||||
}
|
||||
|
||||
addEditorToCache(documentId, view, content);
|
||||
|
||||
return view;
|
||||
// 同步元数据
|
||||
instance.contentTimestamp = doc.updated_at || '';
|
||||
instance.contentLength = newContent.length;
|
||||
instance.isDirty = false;
|
||||
};
|
||||
|
||||
// 显示编辑器
|
||||
const showEditor = (documentId: number) => {
|
||||
const instance = editorCache.get(documentId);
|
||||
if (!instance || !containerElement.value) return;
|
||||
const showEditor = (instance: EditorInstance) => {
|
||||
if (!containerElement.value) return;
|
||||
|
||||
try {
|
||||
// 移除当前编辑器DOM
|
||||
if (currentEditor.value && currentEditor.value.dom && currentEditor.value.dom.parentElement) {
|
||||
currentEditor.value.dom.remove();
|
||||
// 移除当前编辑器 DOM
|
||||
const currentEditor = editorCache.get(currentEditorId.value || 0);
|
||||
if (currentEditor && currentEditor.view.dom && currentEditor.view.dom.parentElement) {
|
||||
currentEditor.view.dom.remove();
|
||||
}
|
||||
|
||||
// 将目标编辑器DOM添加到容器
|
||||
// 添加目标编辑器 DOM
|
||||
containerElement.value.appendChild(instance.view.dom);
|
||||
currentEditor.value = instance.view;
|
||||
currentEditorId.value = instance.documentId;
|
||||
|
||||
// 设置扩展管理器视图
|
||||
setExtensionManagerView(instance.view, documentId);
|
||||
setExtensionManagerView(instance.view, instance.documentId);
|
||||
|
||||
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染
|
||||
nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// 滚动到当前光标位置
|
||||
scrollToCursor(instance.view);
|
||||
// 使用 requestAnimationFrame 确保 DOM 渲染
|
||||
requestAnimationFrame(() => {
|
||||
scrollToCursor(instance.view);
|
||||
instance.view.focus();
|
||||
|
||||
// 聚焦编辑器
|
||||
instance.view.focus();
|
||||
|
||||
// 使用缓存的语法树确保方法
|
||||
ensureSyntaxTreeCached(instance.view, documentId);
|
||||
});
|
||||
// 恢复折叠状态
|
||||
const savedFoldState = editorStateStore.getFoldState(instance.documentId);
|
||||
if (savedFoldState.length > 0) {
|
||||
restoreFoldState(instance.view, savedFoldState);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error showing editor:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存编辑器内容
|
||||
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
||||
const instance = editorCache.get(documentId);
|
||||
if (!instance || !instance.isDirty) return true;
|
||||
|
||||
try {
|
||||
const content = instance.view.state.doc.toString();
|
||||
const lastModified = instance.lastModified;
|
||||
|
||||
await DocumentService.UpdateDocumentContent(documentId, content);
|
||||
|
||||
// 检查在保存期间内容是否又被修改了
|
||||
if (instance.lastModified === lastModified) {
|
||||
instance.content = content;
|
||||
instance.isDirty = false;
|
||||
instance.lastModified = new Date();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to save editor content:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 内容变化处理
|
||||
const onContentChange = () => {
|
||||
const documentId = documentStore.currentDocumentId;
|
||||
if (!documentId) return;
|
||||
const instance = editorCache.get(documentId);
|
||||
const handleContentChange = (docId: number) => {
|
||||
const instance = editorCache.get(docId);
|
||||
if (!instance) return;
|
||||
|
||||
// 立即设置脏标记和修改时间(切换文档时需要判断)
|
||||
// 标记为脏数据
|
||||
instance.isDirty = true;
|
||||
instance.lastModified = new Date();
|
||||
instance.lastModified = Date.now();
|
||||
|
||||
// 优使用防抖清理语法树缓存
|
||||
debouncedClearSyntaxCache.debouncedFn(instance);
|
||||
// 调度自动保存
|
||||
const autoSaveDelay = configStore.config.editing.autoSaveDelay;
|
||||
documentStore.scheduleAutoSave(
|
||||
docId,
|
||||
async () => {
|
||||
const content = instance.view.state.doc.toString();
|
||||
const savedDoc = await documentStore.saveDocument(docId, content);
|
||||
|
||||
// 设置自动保存定时器(已经是防抖效果:每次重置定时器)
|
||||
instance.autoSaveTimer.set(() => {
|
||||
saveEditorContent(documentId);
|
||||
}, getAutoSaveDelay());
|
||||
// 同步版本信息
|
||||
if (savedDoc) {
|
||||
instance.contentTimestamp = savedDoc.updated_at || '';
|
||||
instance.contentLength = (savedDoc.content || '').length;
|
||||
instance.isDirty = false;
|
||||
}
|
||||
},
|
||||
autoSaveDelay
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// 设置编辑器容器
|
||||
const setEditorContainer = (container: HTMLElement | null) => {
|
||||
containerElement.value = container;
|
||||
|
||||
// 如果设置容器时已有当前文档,立即加载编辑器
|
||||
if (container && documentStore.currentDocument && documentStore.currentDocument.id !== undefined) {
|
||||
loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content || '');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载编辑器
|
||||
const loadEditor = async (documentId: number, content: string) => {
|
||||
// 修复:使用计数器精确管理加载状态
|
||||
loadingOperations.value++;
|
||||
// 切换到指定编辑器
|
||||
const switchToEditor = async (docId: number) => {
|
||||
isLoading.value = true;
|
||||
|
||||
// 开始新的操作
|
||||
const { operationId } = operationManager.startOperation(documentId);
|
||||
|
||||
try {
|
||||
// 验证参数
|
||||
if (!documentId) {
|
||||
throw new Error('Invalid parameters for loadEditor');
|
||||
// 直接从后端获取文档
|
||||
const doc = await documentStore.getDocument(docId);
|
||||
if (!doc) {
|
||||
throw new Error(`Failed to load document ${docId}`);
|
||||
}
|
||||
|
||||
// 保存当前编辑器内容
|
||||
if (currentEditor.value) {
|
||||
const currentDocId = documentStore.currentDocumentId;
|
||||
if (currentDocId && currentDocId !== documentId) {
|
||||
await saveEditorContent(currentDocId);
|
||||
const cached = editorCache.get(docId);
|
||||
|
||||
// 检查操作是否仍然有效
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
return;
|
||||
if (cached) {
|
||||
// 场景1:缓存有效
|
||||
if (isCacheValid(cached, doc)) {
|
||||
showEditor(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
// 场景2:有未保存修改
|
||||
if (cached.isDirty) {
|
||||
// 检查内容是否真的不同
|
||||
if (!hasContentChanged(cached, doc)) {
|
||||
// 内容实际相同,只是元数据变了,同步元数据
|
||||
cached.contentTimestamp = doc.updated_at || '';
|
||||
cached.contentLength = (doc.content || '').length;
|
||||
cached.isDirty = false;
|
||||
}
|
||||
// 内容不同,保留用户编辑
|
||||
showEditor(cached);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取或创建编辑器
|
||||
const view = await getOrCreateEditor(documentId, content, operationId);
|
||||
|
||||
// 检查操作是否仍然有效
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新内容(如果需要)
|
||||
const instance = editorCache.get(documentId);
|
||||
if (instance && instance.content !== content) {
|
||||
// 确保编辑器视图有效
|
||||
if (view && view.state && view.dispatch) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: content
|
||||
}
|
||||
});
|
||||
instance.content = content;
|
||||
instance.isDirty = false;
|
||||
// 清理语法树缓存,因为内容已更新
|
||||
instance.syntaxTreeCache = null;
|
||||
// 修复:内容变了,清空光标位置,避免越界
|
||||
instance.editorState = undefined;
|
||||
delete documentStore.documentStates[documentId];
|
||||
}
|
||||
}
|
||||
|
||||
// 最终检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示编辑器
|
||||
showEditor(documentId);
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Operation cancelled') {
|
||||
console.log(`Editor loading cancelled for document ${documentId}`);
|
||||
// 场景3:缓存失效且无脏数据,更新内容
|
||||
updateEditorContent(cached, doc);
|
||||
showEditor(cached);
|
||||
} else {
|
||||
console.error('Failed to load editor:', error);
|
||||
}
|
||||
} finally {
|
||||
// 完成操作
|
||||
operationManager.completeOperation(operationId);
|
||||
// 场景4:创建新编辑器
|
||||
const editor = await createEditorInstance(docId, doc);
|
||||
|
||||
// 修复:使用计数器精确管理加载状态,避免快速切换时状态不准确
|
||||
loadingOperations.value--;
|
||||
// 延迟一段时间后再取消加载状态,但要确保所有操作都完成了
|
||||
// 添加到缓存
|
||||
editorCache.set(docId, editor, (_evictedKey, evictedInstance) => {
|
||||
// 保存光标位置
|
||||
const cursorPos = evictedInstance.view.state.selection.main.head;
|
||||
editorStateStore.saveCursorPosition(evictedInstance.documentId, cursorPos);
|
||||
|
||||
// 从扩展管理器移除
|
||||
removeExtensionManagerView(evictedInstance.documentId);
|
||||
|
||||
// 移除 DOM
|
||||
if (evictedInstance.view.dom.parentElement) {
|
||||
evictedInstance.view.dom.remove();
|
||||
}
|
||||
|
||||
// 销毁编辑器
|
||||
evictedInstance.view.destroy();
|
||||
});
|
||||
|
||||
showEditor(editor);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to switch editor:', error);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
if (loadingOperations.value <= 0) {
|
||||
loadingOperations.value = 0;
|
||||
isLoading.value = false;
|
||||
}
|
||||
isLoading.value = false;
|
||||
}, EDITOR_CONFIG.LOADING_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
// 移除编辑器
|
||||
const removeEditor = async (documentId: number) => {
|
||||
const instance = editorCache.get(documentId);
|
||||
if (instance) {
|
||||
try {
|
||||
// 如果正在加载这个文档,取消操作
|
||||
if (operationManager.getCurrentContext() === documentId) {
|
||||
operationManager.cancelAllOperations();
|
||||
}
|
||||
// 获取当前内容
|
||||
const getCurrentContent = (): string => {
|
||||
if (!currentEditorId.value) return '';
|
||||
const instance = editorCache.get(currentEditorId.value);
|
||||
return instance ? instance.view.state.doc.toString() : '';
|
||||
};
|
||||
|
||||
// 修复:移除前先保存内容(如果有未保存的修改)
|
||||
if (instance.isDirty) {
|
||||
await saveEditorContent(documentId);
|
||||
}
|
||||
// 获取当前光标位置
|
||||
const getCurrentCursorPosition = (): number => {
|
||||
if (!currentEditorId.value) return 0;
|
||||
const instance = editorCache.get(currentEditorId.value);
|
||||
return instance ? instance.view.state.selection.main.head : 0;
|
||||
};
|
||||
|
||||
// 清除自动保存定时器
|
||||
instance.autoSaveTimer.clear();
|
||||
// 检查是否有未保存修改
|
||||
const hasUnsavedChanges = (docId: number): boolean => {
|
||||
const instance = editorCache.get(docId);
|
||||
return instance?.isDirty || false;
|
||||
};
|
||||
|
||||
// 从扩展管理器中移除视图
|
||||
removeExtensionManagerView(documentId);
|
||||
// 同步保存后的版本信息
|
||||
const syncAfterSave = async (docId: number) => {
|
||||
const instance = editorCache.get(docId);
|
||||
if (!instance) return;
|
||||
|
||||
// 移除DOM元素
|
||||
if (instance.view && instance.view.dom && instance.view.dom.parentElement) {
|
||||
instance.view.dom.remove();
|
||||
}
|
||||
|
||||
// 销毁编辑器
|
||||
if (instance.view && instance.view.destroy) {
|
||||
instance.view.destroy();
|
||||
}
|
||||
|
||||
// 清理引用
|
||||
if (currentEditor.value === instance.view) {
|
||||
currentEditor.value = null;
|
||||
}
|
||||
|
||||
// 从缓存中删除
|
||||
editorCache.delete(documentId);
|
||||
} catch (error) {
|
||||
console.error('Error removing editor:', error);
|
||||
}
|
||||
const doc = await documentStore.getDocument(docId);
|
||||
if (doc) {
|
||||
instance.contentTimestamp = doc.updated_at || '';
|
||||
instance.contentLength = (doc.content || '').length;
|
||||
instance.isDirty = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新文档统计
|
||||
const updateDocumentStats = (stats: DocumentStats) => {
|
||||
documentStats.value = stats;
|
||||
// 销毁编辑器
|
||||
const destroyEditor = async (docId: number) => {
|
||||
const instance = editorCache.get(docId);
|
||||
if (!instance) return;
|
||||
|
||||
try {
|
||||
// 保存光标位置
|
||||
const cursorPos = instance.view.state.selection.main.head;
|
||||
editorStateStore.saveCursorPosition(docId, cursorPos);
|
||||
|
||||
// 从扩展管理器移除
|
||||
removeExtensionManagerView(docId);
|
||||
|
||||
// 移除 DOM
|
||||
if (instance.view.dom && instance.view.dom.parentElement) {
|
||||
instance.view.dom.remove();
|
||||
}
|
||||
|
||||
// 销毁编辑器
|
||||
instance.view.destroy();
|
||||
|
||||
// 从缓存删除
|
||||
editorCache.delete(docId);
|
||||
|
||||
// 清空当前编辑器引用
|
||||
if (currentEditorId.value === docId) {
|
||||
currentEditorId.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error destroying editor:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 清空所有编辑器
|
||||
const destroyAllEditors = () => {
|
||||
editorCache.clear((_documentId, instance) => {
|
||||
// 保存光标位置
|
||||
const cursorPos = instance.view.state.selection.main.head;
|
||||
editorStateStore.saveCursorPosition(instance.documentId, cursorPos);
|
||||
|
||||
// 从扩展管理器移除
|
||||
removeExtensionManagerView(instance.documentId);
|
||||
|
||||
// 移除 DOM
|
||||
if (instance.view.dom.parentElement) {
|
||||
instance.view.dom.remove();
|
||||
}
|
||||
|
||||
// 销毁编辑器
|
||||
instance.view.destroy();
|
||||
});
|
||||
|
||||
currentEditorId.value = null;
|
||||
};
|
||||
|
||||
// 设置编辑器容器
|
||||
const setEditorContainer = (container: HTMLElement | null) => {
|
||||
containerElement.value = container;
|
||||
};
|
||||
|
||||
|
||||
// 应用字体设置
|
||||
const applyFontSettings = () => {
|
||||
editorCache.values().forEach(instance => {
|
||||
@@ -557,8 +456,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 应用Tab设置
|
||||
// 应用 Tab 设置
|
||||
const applyTabSettings = () => {
|
||||
editorCache.values().forEach(instance => {
|
||||
updateTabConfig(
|
||||
@@ -572,7 +470,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 应用快捷键设置
|
||||
const applyKeymapSettings = async () => {
|
||||
// 确保所有编辑器实例的快捷键都更新
|
||||
await Promise.all(
|
||||
editorCache.values().map(instance =>
|
||||
updateKeymapExtension(instance.view)
|
||||
@@ -580,102 +477,36 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 清空所有编辑器
|
||||
const clearAllEditors = () => {
|
||||
// 取消所有挂起的操作
|
||||
operationManager.cancelAllOperations();
|
||||
|
||||
editorCache.clear((_documentId, instance) => {
|
||||
// 清除自动保存定时器
|
||||
instance.autoSaveTimer.clear();
|
||||
// 从扩展管理器移除
|
||||
removeExtensionManagerView(instance.documentId);
|
||||
// 移除DOM元素
|
||||
if (instance.view.dom.parentElement) {
|
||||
instance.view.dom.remove();
|
||||
}
|
||||
// 销毁编辑器
|
||||
instance.view.destroy();
|
||||
});
|
||||
|
||||
currentEditor.value = null;
|
||||
};
|
||||
|
||||
// 更新扩展
|
||||
const updateExtension = async (key: string, enabled: boolean, config?: any) => {
|
||||
// 更新启用状态
|
||||
await ExtensionService.UpdateExtensionEnabled(key, enabled);
|
||||
|
||||
// 如果需要更新配置
|
||||
if (config !== undefined) {
|
||||
await ExtensionService.UpdateExtensionConfig(key, config);
|
||||
}
|
||||
|
||||
// 更新前端编辑器扩展 - 应用于所有实例
|
||||
const manager = getExtensionManager();
|
||||
if (manager) {
|
||||
// 直接更新前端扩展至所有视图
|
||||
manager.updateExtension(key, enabled, config);
|
||||
}
|
||||
|
||||
// 重新加载扩展配置
|
||||
await extensionStore.loadExtensions();
|
||||
if (manager) {
|
||||
manager.initExtensions(extensionStore.extensions);
|
||||
}
|
||||
|
||||
await applyKeymapSettings();
|
||||
};
|
||||
|
||||
// 监听文档切换
|
||||
watch(() => documentStore.currentDocument, async (newDoc, oldDoc) => {
|
||||
if (newDoc && newDoc.id !== undefined && containerElement.value) {
|
||||
// 等待 DOM 更新完成,再加载新文档的编辑器
|
||||
await nextTick();
|
||||
loadEditor(newDoc.id, newDoc.content || '');
|
||||
}
|
||||
const hasContainer = computed(() => containerElement.value !== null);
|
||||
const currentEditor = computed(() => {
|
||||
if (!currentEditorId.value) return null;
|
||||
const instance = editorCache.get(currentEditorId.value);
|
||||
return instance ? instance.view : null;
|
||||
});
|
||||
|
||||
// 创建字体配置的计算属性
|
||||
const fontConfig = computed(() => ({
|
||||
fontSize: configStore.config.editing.fontSize,
|
||||
fontFamily: configStore.config.editing.fontFamily,
|
||||
lineHeight: configStore.config.editing.lineHeight,
|
||||
fontWeight: configStore.config.editing.fontWeight
|
||||
}));
|
||||
// 创建Tab配置的计算属性
|
||||
const tabConfig = computed(() => ({
|
||||
tabSize: configStore.config.editing.tabSize,
|
||||
enableTabIndent: configStore.config.editing.enableTabIndent,
|
||||
tabType: configStore.config.editing.tabType
|
||||
}));
|
||||
// 监听字体配置变化
|
||||
watch(fontConfig, applyFontSettings, { deep: true });
|
||||
// 监听Tab配置变化
|
||||
watch(tabConfig, applyTabSettings, { deep: true });
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentEditorId: readonly(currentEditorId),
|
||||
currentEditor,
|
||||
documentStats,
|
||||
isLoading,
|
||||
isLoading: readonly(isLoading),
|
||||
hasContainer,
|
||||
|
||||
// 方法
|
||||
// 编辑器管理
|
||||
setEditorContainer,
|
||||
loadEditor,
|
||||
removeEditor,
|
||||
clearAllEditors,
|
||||
onContentChange,
|
||||
switchToEditor,
|
||||
destroyEditor,
|
||||
destroyAllEditors,
|
||||
|
||||
// 配置更新方法
|
||||
// 查询方法
|
||||
getCurrentContent,
|
||||
getCurrentCursorPosition,
|
||||
hasUnsavedChanges,
|
||||
syncAfterSave,
|
||||
|
||||
// 配置应用
|
||||
applyFontSettings,
|
||||
applyThemeSettings,
|
||||
applyTabSettings,
|
||||
applyKeymapSettings,
|
||||
|
||||
// 扩展管理方法
|
||||
updateExtension,
|
||||
|
||||
editorView: currentEditor,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,22 +7,12 @@ export const useExtensionStore = defineStore('extension', () => {
|
||||
// 扩展配置数据
|
||||
const extensions = ref<Extension[]>([]);
|
||||
|
||||
// 获取启用的扩展
|
||||
const enabledExtensions = computed(() =>
|
||||
extensions.value.filter(ext => ext.enabled)
|
||||
);
|
||||
|
||||
// 获取启用的扩展ID列表 (key)
|
||||
const enabledExtensionIds = computed(() =>
|
||||
enabledExtensions.value.map(ext => ext.key).filter((k): k is string => k !== undefined)
|
||||
);
|
||||
|
||||
/**
|
||||
* 从后端加载扩展配置
|
||||
*/
|
||||
const loadExtensions = async (): Promise<void> => {
|
||||
try {
|
||||
const result = await ExtensionService.GetAllExtensions();
|
||||
const result = await ExtensionService.GetExtensions();
|
||||
extensions.value = result.filter((ext): ext is Extension => ext !== null);
|
||||
} catch (err) {
|
||||
console.error('[ExtensionStore] Failed to load extensions:', err);
|
||||
@@ -32,17 +22,19 @@ export const useExtensionStore = defineStore('extension', () => {
|
||||
/**
|
||||
* 获取扩展配置
|
||||
*/
|
||||
const getExtensionConfig = (key: string): any => {
|
||||
const extension = extensions.value.find(ext => ext.key === key);
|
||||
return extension?.config ?? {};
|
||||
const getExtensionConfig = async (id: number): Promise<any> => {
|
||||
try {
|
||||
const config = await ExtensionService.GetExtensionConfig(id);
|
||||
return config ?? {};
|
||||
} catch (err) {
|
||||
console.error('[ExtensionStore] Failed to get extension config:', err);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
extensions,
|
||||
enabledExtensions,
|
||||
enabledExtensionIds,
|
||||
|
||||
// 方法
|
||||
loadExtensions,
|
||||
getExtensionConfig,
|
||||
|
||||
@@ -1,86 +1,38 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, ref} from 'vue';
|
||||
import {KeyBinding} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import {GetAllKeyBindings} from '@/../bindings/voidraft/internal/services/keybindingservice';
|
||||
import {KeyBindingService} from '@/../bindings/voidraft/internal/services';
|
||||
import {KeyBindingType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useConfigStore} from './configStore';
|
||||
|
||||
export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 快捷键配置数据
|
||||
const keyBindings = ref<KeyBinding[]>([]);
|
||||
|
||||
// 获取启用的快捷键
|
||||
const enabledKeyBindings = computed(() =>
|
||||
keyBindings.value.filter(kb => kb.enabled)
|
||||
);
|
||||
|
||||
// 按扩展分组的快捷键
|
||||
const keyBindingsByExtension = computed(() => {
|
||||
const groups = new Map<string, KeyBinding[]>();
|
||||
|
||||
for (const binding of keyBindings.value) {
|
||||
const ext = binding.extension || '';
|
||||
if (!groups.has(ext)) {
|
||||
groups.set(ext, []);
|
||||
}
|
||||
groups.get(ext)!.push(binding);
|
||||
}
|
||||
|
||||
return groups;
|
||||
});
|
||||
|
||||
// 获取指定扩展的快捷键
|
||||
const getKeyBindingsByExtension = computed(() =>
|
||||
(extension: string) =>
|
||||
keyBindings.value.filter(kb => kb.extension === extension)
|
||||
);
|
||||
|
||||
// 按命令获取快捷键
|
||||
const getKeyBindingByCommand = computed(() =>
|
||||
(command: string) =>
|
||||
keyBindings.value.find(kb => kb.command === command)
|
||||
);
|
||||
|
||||
/**
|
||||
* 从后端加载快捷键配置
|
||||
* 从后端加载快捷键配置(根据当前配置的模式)
|
||||
*/
|
||||
const loadKeyBindings = async (): Promise<void> => {
|
||||
const result = await GetAllKeyBindings();
|
||||
const keymapMode = configStore.config.editing.keymapMode || KeyBindingType.Standard;
|
||||
const result = await KeyBindingService.GetKeyBindings(keymapMode);
|
||||
keyBindings.value = result.filter((kb): kb is KeyBinding => kb !== null);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否存在指定命令的快捷键
|
||||
* 更新快捷键绑定
|
||||
*/
|
||||
const hasCommand = (command: string): boolean => {
|
||||
return keyBindings.value.some(kb => kb.command === command && kb.enabled);
|
||||
const updateKeyBinding = async (id: number, key: string): Promise<void> => {
|
||||
await KeyBindingService.UpdateKeyBindingKeys(id, key);
|
||||
await loadKeyBindings();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 获取扩展相关的所有扩展ID
|
||||
*/
|
||||
const getAllExtensionIds = computed(() => {
|
||||
const extensionIds = new Set<string>();
|
||||
for (const binding of keyBindings.value) {
|
||||
if (binding.extension) {
|
||||
extensionIds.add(binding.extension);
|
||||
}
|
||||
}
|
||||
return Array.from(extensionIds);
|
||||
});
|
||||
|
||||
return {
|
||||
// 状态
|
||||
keyBindings,
|
||||
enabledKeyBindings,
|
||||
keyBindingsByExtension,
|
||||
getAllExtensionIds,
|
||||
|
||||
// 计算属性
|
||||
getKeyBindingByCommand,
|
||||
getKeyBindingsByExtension,
|
||||
|
||||
// 方法
|
||||
loadKeyBindings,
|
||||
hasCommand,
|
||||
updateKeyBinding,
|
||||
};
|
||||
});
|
||||
@@ -19,11 +19,8 @@ export const useTabStore = defineStore('tab', () => {
|
||||
const tabOrder = ref<number[]>([]); // 维护标签页顺序
|
||||
const draggedTabId = ref<number | null>(null);
|
||||
|
||||
// === 计算属性 ===
|
||||
|
||||
const isTabsEnabled = computed(() => configStore.config.general.enableTabs);
|
||||
const canCloseTab = computed(() => tabOrder.value.length > 1);
|
||||
const currentDocumentId = computed(() => documentStore.currentDocumentId);
|
||||
|
||||
// 按顺序返回标签页数组(用于UI渲染)
|
||||
const tabs = computed(() => {
|
||||
@@ -75,7 +72,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
/**
|
||||
* 关闭标签页
|
||||
*/
|
||||
const closeTab = (documentId: number) => {
|
||||
const closeTab = async (documentId: number) => {
|
||||
if (!hasTab(documentId)) return;
|
||||
|
||||
const tabIndex = tabOrder.value.indexOf(documentId);
|
||||
@@ -95,7 +92,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
|
||||
if (nextIndex >= 0 && tabOrder.value[nextIndex]) {
|
||||
const nextDocumentId = tabOrder.value[nextIndex];
|
||||
switchToTabAndDocument(nextDocumentId);
|
||||
await switchToTabAndDocument(nextDocumentId);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -120,7 +117,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
/**
|
||||
* 切换到指定标签页并打开对应文档
|
||||
*/
|
||||
const switchToTabAndDocument = (documentId: number) => {
|
||||
const switchToTabAndDocument = async (documentId: number) => {
|
||||
if (!hasTab(documentId)) return;
|
||||
|
||||
// 如果点击的是当前已激活的文档,不需要重复请求
|
||||
@@ -128,7 +125,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
documentStore.openDocument(documentId);
|
||||
await documentStore.openDocument(documentId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -154,8 +151,9 @@ export const useTabStore = defineStore('tab', () => {
|
||||
/**
|
||||
* 验证并清理无效的标签页
|
||||
*/
|
||||
const validateTabs = () => {
|
||||
const validDocIds = Object.keys(documentStore.documents).map(Number);
|
||||
const validateTabs = async () => {
|
||||
const docs = await documentStore.getDocumentList();
|
||||
const validDocIds = docs.map(doc => doc.id).filter((id): id is number => id !== undefined);
|
||||
|
||||
// 找出无效的标签页(文档已被删除)
|
||||
const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId));
|
||||
@@ -172,9 +170,9 @@ export const useTabStore = defineStore('tab', () => {
|
||||
/**
|
||||
* 初始化标签页(当前文档)
|
||||
*/
|
||||
const initializeTab = () => {
|
||||
// 先验证并清理无效的标签页(处理持久化的脏数据)
|
||||
validateTabs();
|
||||
const initTab = async () => {
|
||||
// 先验证并清理无效的标签页
|
||||
await validateTabs();
|
||||
|
||||
if (isTabsEnabled.value) {
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
@@ -189,7 +187,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
/**
|
||||
* 关闭其他标签页(除了指定的标签页)
|
||||
*/
|
||||
const closeOtherTabs = (keepDocumentId: number) => {
|
||||
const closeOtherTabs = async (keepDocumentId: number) => {
|
||||
if (!hasTab(keepDocumentId)) return;
|
||||
|
||||
// 获取所有其他标签页的ID
|
||||
@@ -200,14 +198,14 @@ export const useTabStore = defineStore('tab', () => {
|
||||
|
||||
// 如果当前打开的文档在被关闭的标签中,需要切换到保留的文档
|
||||
if (otherTabIds.includes(documentStore.currentDocumentId!)) {
|
||||
switchToTabAndDocument(keepDocumentId);
|
||||
await switchToTabAndDocument(keepDocumentId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭指定标签页右侧的所有标签页
|
||||
*/
|
||||
const closeTabsToRight = (documentId: number) => {
|
||||
const closeTabsToRight = async (documentId: number) => {
|
||||
const index = getTabIndex(documentId);
|
||||
if (index === -1) return;
|
||||
|
||||
@@ -219,14 +217,14 @@ export const useTabStore = defineStore('tab', () => {
|
||||
|
||||
// 如果当前打开的文档在被关闭的右侧标签中,需要切换到指定的文档
|
||||
if (rightTabIds.includes(documentStore.currentDocumentId!)) {
|
||||
switchToTabAndDocument(documentId);
|
||||
await switchToTabAndDocument(documentId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭指定标签页左侧的所有标签页
|
||||
*/
|
||||
const closeTabsToLeft = (documentId: number) => {
|
||||
const closeTabsToLeft = async (documentId: number) => {
|
||||
const index = getTabIndex(documentId);
|
||||
if (index <= 0) return;
|
||||
|
||||
@@ -238,7 +236,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
|
||||
// 如果当前打开的文档在被关闭的左侧标签中,需要切换到指定的文档
|
||||
if (leftTabIds.includes(documentStore.currentDocumentId!)) {
|
||||
switchToTabAndDocument(documentId);
|
||||
await switchToTabAndDocument(documentId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -262,7 +260,6 @@ export const useTabStore = defineStore('tab', () => {
|
||||
// 计算属性
|
||||
isTabsEnabled,
|
||||
canCloseTab,
|
||||
currentDocumentId,
|
||||
|
||||
// 方法
|
||||
addOrActivateTab,
|
||||
@@ -273,7 +270,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
switchToTabAndDocument,
|
||||
moveTab,
|
||||
getTabIndex,
|
||||
initializeTab,
|
||||
initTab,
|
||||
clearAllTabs,
|
||||
updateTabTitle,
|
||||
validateTabs,
|
||||
|
||||
@@ -4,49 +4,23 @@ import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {Type as ThemeType} from '@/../bindings/voidraft/internal/models/ent/theme/models';
|
||||
import {ThemeService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useEditorStore} from './editorStore';
|
||||
import type {ThemeColors} from '@/views/editor/theme/types';
|
||||
import {cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap} from '@/views/editor/theme/presets';
|
||||
import {useEditorStore} from "@/stores/editorStore";
|
||||
|
||||
type ThemeColorConfig = { [_: string]: any };
|
||||
// 类型定义
|
||||
type ThemeOption = { name: string; type: ThemeType };
|
||||
|
||||
const resolveThemeName = (name?: string) =>
|
||||
// 解析主题名称,确保返回有效的主题
|
||||
const resolveThemeName = (name?: string): 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.TypeDark);
|
||||
const lightThemeOptions = createThemeOptions(ThemeType.TypeLight);
|
||||
|
||||
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.GetThemeByKey(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);
|
||||
};
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const configStore = useConfigStore();
|
||||
const currentColors = ref<ThemeColors | null>(null);
|
||||
@@ -62,10 +36,12 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
);
|
||||
|
||||
// 根据当前模式动态计算可用主题列表
|
||||
const availableThemes = computed<ThemeOption[]>(() =>
|
||||
isDarkMode.value ? darkThemeOptions : lightThemeOptions
|
||||
createThemeOptions(isDarkMode.value ? ThemeType.TypeDark : ThemeType.TypeLight)
|
||||
);
|
||||
|
||||
// 应用主题到 DOM
|
||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||
const themeMap = {
|
||||
[SystemThemeType.SystemThemeAuto]: 'auto',
|
||||
@@ -75,24 +51,64 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
document.documentElement.setAttribute('data-theme', themeMap[theme]);
|
||||
};
|
||||
|
||||
// 获取预设主题颜色
|
||||
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);
|
||||
const theme = await ThemeService.GetThemeByName(safeName);
|
||||
if (theme?.colors) {
|
||||
const colors = cloneThemeColors(theme.colors as ThemeColors);
|
||||
colors.themeName = safeName;
|
||||
return colors;
|
||||
}
|
||||
return getPresetColors(safeName);
|
||||
};
|
||||
|
||||
// 加载主题颜色
|
||||
const loadThemeColors = async (themeName?: string) => {
|
||||
const targetName = resolveThemeName(
|
||||
themeName || configStore.config?.appearance?.currentTheme
|
||||
);
|
||||
currentColors.value = getPresetColors(targetName);
|
||||
currentColors.value = await fetchThemeColors(targetName);
|
||||
|
||||
};
|
||||
|
||||
const initTheme = async () => {
|
||||
// 获取可用的主题颜色
|
||||
const getEffectiveColors = (): ThemeColors => {
|
||||
const targetName = resolveThemeName(
|
||||
currentColors.value?.themeName || configStore.config?.appearance?.currentTheme
|
||||
);
|
||||
return currentColors.value ?? getPresetColors(targetName);
|
||||
};
|
||||
|
||||
// 同步应用到 DOM 与编辑器
|
||||
const applyAllThemes = () => {
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
await loadThemeColors();
|
||||
const editorStore = useEditorStore();
|
||||
editorStore.applyThemeSettings();
|
||||
};
|
||||
|
||||
// 初始化主题
|
||||
const initTheme = async () => {
|
||||
await loadThemeColors();
|
||||
applyAllThemes();
|
||||
};
|
||||
|
||||
// 设置系统主题
|
||||
const setTheme = async (theme: SystemThemeType) => {
|
||||
await configStore.setSystemTheme(theme);
|
||||
applyThemeToDOM(theme);
|
||||
refreshEditorTheme();
|
||||
applyAllThemes();
|
||||
};
|
||||
|
||||
// 切换到指定主题
|
||||
const switchToTheme = async (themeName: string) => {
|
||||
if (!themePresetMap[themeName]) {
|
||||
console.error('Theme not found:', themeName);
|
||||
@@ -101,15 +117,17 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
await configStore.setCurrentTheme(themeName);
|
||||
refreshEditorTheme();
|
||||
applyAllThemes();
|
||||
return true;
|
||||
};
|
||||
|
||||
// 更新当前主题颜色
|
||||
const updateCurrentColors = (colors: Partial<ThemeColors>) => {
|
||||
if (!currentColors.value) return;
|
||||
Object.assign(currentColors.value, colors);
|
||||
};
|
||||
|
||||
// 保存当前主题
|
||||
const saveCurrentTheme = async () => {
|
||||
if (!currentColors.value) {
|
||||
throw new Error('No theme selected');
|
||||
@@ -118,13 +136,14 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
const themeName = resolveThemeName(currentColors.value.themeName);
|
||||
currentColors.value.themeName = themeName;
|
||||
|
||||
await ThemeService.UpdateTheme(themeName, currentColors.value as ThemeColorConfig);
|
||||
await ThemeService.UpdateTheme(themeName, currentColors.value);
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
refreshEditorTheme();
|
||||
applyAllThemes();
|
||||
return true;
|
||||
};
|
||||
|
||||
// 重置当前主题到默认值
|
||||
const resetCurrentTheme = async () => {
|
||||
if (!currentColors.value) {
|
||||
throw new Error('No theme selected');
|
||||
@@ -134,15 +153,10 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
await ThemeService.ResetTheme(themeName);
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
refreshEditorTheme();
|
||||
applyAllThemes();
|
||||
return true;
|
||||
};
|
||||
|
||||
const refreshEditorTheme = () => {
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
const editorStore = useEditorStore();
|
||||
editorStore?.applyThemeSettings();
|
||||
};
|
||||
|
||||
return {
|
||||
availableThemes,
|
||||
@@ -155,7 +169,8 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
updateCurrentColors,
|
||||
saveCurrentTheme,
|
||||
resetCurrentTheme,
|
||||
refreshEditorTheme,
|
||||
applyThemeToDOM,
|
||||
applyAllThemes,
|
||||
getEffectiveColors,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -28,11 +28,16 @@ onMounted(async () => {
|
||||
|
||||
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
|
||||
|
||||
await documentStore.initialize(urlDocumentId);
|
||||
await documentStore.initDocument(urlDocumentId);
|
||||
|
||||
editorStore.setEditorContainer(editorElement.value);
|
||||
|
||||
await tabStore.initializeTab();
|
||||
const currentDocId = documentStore.currentDocumentId;
|
||||
if (currentDocId) {
|
||||
await editorStore.switchToEditor(currentDocId);
|
||||
}
|
||||
|
||||
await tabStore.initTab();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
|
||||
import type {Text} from '@codemirror/state';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
|
||||
/**
|
||||
* 内容变化监听扩展
|
||||
* 通过回调函数解耦,不直接依赖 Store
|
||||
*/
|
||||
export function createContentChangePlugin() {
|
||||
export function createContentChangePlugin(onContentChange: () => void) {
|
||||
return ViewPlugin.fromClass(
|
||||
class ContentChangePlugin {
|
||||
private readonly editorStore = useEditorStore();
|
||||
private lastDoc: Text;
|
||||
private rafId: number | null = null;
|
||||
private pendingNotification = false;
|
||||
@@ -40,7 +40,7 @@ export function createContentChangePlugin() {
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
this.pendingNotification = false;
|
||||
this.rafId = null;
|
||||
this.editorStore.onContentChange();
|
||||
onContentChange(); // 调用注入的回调
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useEditorStateStore} from '@/stores/editorStateStore';
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
/**
|
||||
* 光标位置持久化扩展
|
||||
* 实时监听光标位置变化并持久化到 documentStore
|
||||
* 实时监听光标位置变化并持久化到 editorStateStore
|
||||
*/
|
||||
export function createCursorPositionExtension(documentId: number) {
|
||||
return ViewPlugin.fromClass(
|
||||
class CursorPositionPlugin {
|
||||
private readonly documentStore = useDocumentStore();
|
||||
private readonly editorStateStore = useEditorStateStore();
|
||||
private readonly debouncedSave;
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
const {debouncedFn, flush} = createDebounce(
|
||||
() => this.saveCursorPosition(),
|
||||
{delay: 400}
|
||||
{delay: 1000}
|
||||
);
|
||||
this.debouncedSave = {fn: debouncedFn, flush};
|
||||
|
||||
@@ -42,11 +42,7 @@ export function createCursorPositionExtension(documentId: number) {
|
||||
|
||||
private saveCursorPosition() {
|
||||
const cursorPos = this.view.state.selection.main.head;
|
||||
if (!this.documentStore.documentStates[documentId]) {
|
||||
this.documentStore.documentStates[documentId] = {cursorPos};
|
||||
} else {
|
||||
this.documentStore.documentStates[documentId].cursorPos = cursorPos;
|
||||
}
|
||||
this.editorStateStore.saveCursorPosition(documentId, cursorPos);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
113
frontend/src/views/editor/basic/foldStateExtension.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
|
||||
import {foldedRanges, foldEffect, unfoldEffect} from '@codemirror/language';
|
||||
import {StateEffect} from '@codemirror/state';
|
||||
import {useEditorStateStore, type FoldRange} from '@/stores/editorStateStore';
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
/**
|
||||
* 折叠状态持久化扩展
|
||||
*/
|
||||
export function createFoldStateExtension(documentId: number) {
|
||||
return ViewPlugin.fromClass(
|
||||
class FoldStatePlugin {
|
||||
private readonly editorStateStore = useEditorStateStore();
|
||||
private readonly debouncedSave;
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
const {debouncedFn, flush} = createDebounce(
|
||||
() => this.saveFoldState(),
|
||||
{delay: 500}
|
||||
);
|
||||
this.debouncedSave = {fn: debouncedFn, flush};
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
// 检查是否有折叠/展开操作
|
||||
const hasFoldChange = update.transactions.some(tr =>
|
||||
tr.effects.some(effect =>
|
||||
effect.is(foldEffect) || effect.is(unfoldEffect)
|
||||
)
|
||||
);
|
||||
|
||||
if (hasFoldChange) {
|
||||
this.debouncedSave.fn();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 销毁时立即执行待保存的操作
|
||||
this.debouncedSave.flush();
|
||||
// 再保存一次确保最新状态
|
||||
this.saveFoldState();
|
||||
}
|
||||
|
||||
private saveFoldState() {
|
||||
const foldRanges: FoldRange[] = [];
|
||||
const foldCursor = foldedRanges(this.view.state).iter();
|
||||
const doc = this.view.state.doc;
|
||||
|
||||
// 遍历所有折叠区间
|
||||
while (foldCursor.value !== null) {
|
||||
const from = foldCursor.from;
|
||||
const to = foldCursor.to;
|
||||
|
||||
// 同时记录字符偏移和行号
|
||||
const fromLine = doc.lineAt(from).number;
|
||||
const toLine = doc.lineAt(to).number;
|
||||
|
||||
foldRanges.push({
|
||||
from,
|
||||
to,
|
||||
fromLine,
|
||||
toLine
|
||||
});
|
||||
|
||||
foldCursor.next();
|
||||
}
|
||||
|
||||
this.editorStateStore.saveFoldState(documentId, foldRanges);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复折叠状态(基于行号,更稳定)
|
||||
* @param view 编辑器视图
|
||||
* @param foldRanges 要恢复的折叠区间
|
||||
*/
|
||||
export function restoreFoldState(view: EditorView, foldRanges: FoldRange[]) {
|
||||
if (foldRanges.length === 0) return;
|
||||
|
||||
const doc = view.state.doc;
|
||||
const effects: StateEffect<any>[] = [];
|
||||
|
||||
for (const range of foldRanges) {
|
||||
try {
|
||||
// 优先使用行号恢复
|
||||
if (range.fromLine && range.toLine) {
|
||||
// 确保行号在有效范围内
|
||||
if (range.fromLine >= 1 && range.toLine <= doc.lines && range.fromLine <= range.toLine) {
|
||||
const fromPos = doc.line(range.fromLine).from;
|
||||
const toPos = doc.line(range.toLine).to;
|
||||
|
||||
effects.push(foldEffect.of({from: fromPos, to: toPos}));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用字符偏移
|
||||
if (range.from >= 0 && range.to <= doc.length && range.from < range.to) {
|
||||
effects.push(foldEffect.of({from: range.from, to: range.to}));
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略无效的折叠区间
|
||||
console.warn('Failed to restore fold range:', range, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (effects.length > 0) {
|
||||
view.dispatch({effects});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Extension} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {DocumentStats} from '@/stores/editorStore';
|
||||
import {DocumentStats} from '@/stores/editorStateStore';
|
||||
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
|
||||
|
||||
// 更新编辑器文档统计信息
|
||||
|
||||
@@ -9,15 +9,11 @@ export const themeCompartment = new Compartment();
|
||||
/**
|
||||
* 根据主题类型获取主题扩展
|
||||
*/
|
||||
const getThemeExtension = (): Extension | null => {
|
||||
const getThemeExtension = (): Extension => {
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// 直接获取当前主题颜色配置
|
||||
const colors = themeStore.currentColors;
|
||||
|
||||
if (!colors) {
|
||||
return null;
|
||||
}
|
||||
// 获取有效主题颜色
|
||||
const colors = themeStore.getEffectiveColors();
|
||||
|
||||
// 使用颜色配置创建主题
|
||||
return createThemeByColors(colors);
|
||||
@@ -28,12 +24,6 @@ const getThemeExtension = (): Extension | null => {
|
||||
*/
|
||||
export const createThemeExtension = (): Extension => {
|
||||
const extension = getThemeExtension();
|
||||
|
||||
// 如果主题未加载,返回空扩展
|
||||
if (!extension) {
|
||||
return themeCompartment.of([]);
|
||||
}
|
||||
|
||||
return themeCompartment.of(extension);
|
||||
};
|
||||
|
||||
@@ -48,11 +38,6 @@ export const updateEditorTheme = (view: EditorView): void => {
|
||||
try {
|
||||
const extension = getThemeExtension();
|
||||
|
||||
// 如果主题未加载,不更新
|
||||
if (!extension) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
effects: themeCompartment.reconfigure(extension)
|
||||
});
|
||||
@@ -60,4 +45,3 @@ export const updateEditorTheme = (view: EditorView): void => {
|
||||
console.error('Failed to update editor theme:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {MenuSchemaNode} from '../contextMenu/menuSchema';
|
||||
import {getActiveNoteBlock} from '../codeblock/state';
|
||||
import {blockImageEnabledFacet, copyBlockImageCommand} from './index';
|
||||
|
||||
|
||||
export const blockImageMenuNodes: MenuSchemaNode[] = [
|
||||
{
|
||||
id: 'copy-block-image',
|
||||
labelKey: 'extensions.blockImage.copyMenu',
|
||||
command: copyBlockImageCommand,
|
||||
visible: context =>
|
||||
context.view.state.facet(blockImageEnabledFacet) &&
|
||||
Boolean(getActiveNoteBlock(context.view.state)),
|
||||
enabled: context =>
|
||||
context.view.state.facet(blockImageEnabledFacet) &&
|
||||
Boolean(getActiveNoteBlock(context.view.state)),
|
||||
},
|
||||
];
|
||||
|
||||
319
frontend/src/views/editor/extensions/blockImage/index.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import {snapdom} from '@zumer/snapdom';
|
||||
import {syntaxTree, highlightingFor} from '@codemirror/language';
|
||||
import {Highlighter, highlightTree} from '@lezer/highlight';
|
||||
import {Facet, type Extension} from '@codemirror/state';
|
||||
import {EditorView, Command} from '@codemirror/view';
|
||||
import type {Block} from '../codeblock/types';
|
||||
import {blockState, getActiveNoteBlock} from '../codeblock/state';
|
||||
|
||||
/**
|
||||
* 高亮片段信息
|
||||
*/
|
||||
interface HighlightSpan {
|
||||
from: number;
|
||||
to: number;
|
||||
cssClass: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从语法树获取指定范围的高亮信息
|
||||
*/
|
||||
function getHighlights(view: EditorView, from: number, to: number): HighlightSpan[] {
|
||||
const tree = syntaxTree(view.state);
|
||||
const highlights: HighlightSpan[] = [];
|
||||
|
||||
if (tree.length === 0) {
|
||||
return highlights;
|
||||
}
|
||||
|
||||
const highlighter: Highlighter = {
|
||||
style: tags => highlightingFor(view.state, tags),
|
||||
};
|
||||
|
||||
highlightTree(
|
||||
tree,
|
||||
highlighter,
|
||||
(hlFrom, hlTo, cssClass) => {
|
||||
if (hlFrom < to && hlTo > from) {
|
||||
highlights.push({
|
||||
from: Math.max(hlFrom, from),
|
||||
to: Math.min(hlTo, to),
|
||||
cssClass: cssClass || '',
|
||||
});
|
||||
}
|
||||
},
|
||||
from,
|
||||
to,
|
||||
);
|
||||
|
||||
return highlights;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建带高亮的单行元素
|
||||
*/
|
||||
function createHighlightedLine(
|
||||
lineText: string,
|
||||
lineFrom: number,
|
||||
lineTo: number,
|
||||
highlights: HighlightSpan[],
|
||||
): HTMLElement {
|
||||
const lineElement = document.createElement('div');
|
||||
lineElement.className = 'cm-line';
|
||||
lineElement.style.whiteSpace = 'pre';
|
||||
|
||||
if (highlights.length === 0 || lineText.length === 0) {
|
||||
lineElement.textContent = lineText || ' ';
|
||||
return lineElement;
|
||||
}
|
||||
|
||||
const spans: Array<{text: string; cssClass: string}> = [];
|
||||
let pos = lineFrom;
|
||||
|
||||
const lineHighlights = highlights
|
||||
.filter(h => h.from < lineTo && h.to > lineFrom)
|
||||
.sort((a, b) => a.from - b.from);
|
||||
|
||||
for (const hl of lineHighlights) {
|
||||
if (hl.from > pos) {
|
||||
spans.push({
|
||||
text: lineText.slice(pos - lineFrom, hl.from - lineFrom),
|
||||
cssClass: '',
|
||||
});
|
||||
}
|
||||
|
||||
const hlStart = Math.max(hl.from, lineFrom);
|
||||
const hlEnd = Math.min(hl.to, lineTo);
|
||||
spans.push({
|
||||
text: lineText.slice(hlStart - lineFrom, hlEnd - lineFrom),
|
||||
cssClass: hl.cssClass,
|
||||
});
|
||||
|
||||
pos = hlEnd;
|
||||
}
|
||||
|
||||
if (pos < lineTo) {
|
||||
spans.push({
|
||||
text: lineText.slice(pos - lineFrom),
|
||||
cssClass: '',
|
||||
});
|
||||
}
|
||||
|
||||
for (const span of spans) {
|
||||
if (span.cssClass) {
|
||||
const spanElement = document.createElement('span');
|
||||
spanElement.className = span.cssClass;
|
||||
spanElement.textContent = span.text;
|
||||
lineElement.appendChild(spanElement);
|
||||
} else {
|
||||
lineElement.appendChild(document.createTextNode(span.text));
|
||||
}
|
||||
}
|
||||
|
||||
return lineElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用于截图的块 DOM
|
||||
*/
|
||||
function inlineStyle(style: CSSStyleDeclaration, props: string[]): string {
|
||||
return props
|
||||
.map(prop => {
|
||||
const val = style.getPropertyValue(prop);
|
||||
return val ? `${prop}:${val};` : '';
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function getBlockDomElement(view: EditorView, block: Block): HTMLElement | null {
|
||||
try {
|
||||
const blocks = view.state.field(blockState, false);
|
||||
if (!blocks) return null;
|
||||
|
||||
const blockIndex = blocks.indexOf(block);
|
||||
const isEvenBlock = blockIndex % 2 === 0;
|
||||
|
||||
const blockLayerElem = view.dom.querySelector(
|
||||
`.code-blocks-layer .${isEvenBlock ? 'block-even' : 'block-odd'}`,
|
||||
) as HTMLElement | null;
|
||||
const backgroundColor =
|
||||
blockLayerElem?.ownerDocument
|
||||
? getComputedStyle(blockLayerElem).backgroundColor
|
||||
: isEvenBlock
|
||||
? '#252B37'
|
||||
: '#213644';
|
||||
|
||||
const contentDom = view.dom.querySelector('.cm-content') as HTMLElement | null;
|
||||
const sourceStyle = contentDom ? getComputedStyle(contentDom) : getComputedStyle(view.dom);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'cm-editor cm-focused block-export-wrapper';
|
||||
container.style.cssText = `
|
||||
padding: 18px 22px;
|
||||
background-color: ${backgroundColor};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
|
||||
display: inline-block;
|
||||
min-width: 360px;
|
||||
max-width: 960px;
|
||||
color: ${sourceStyle.color};
|
||||
font-family: ${sourceStyle.fontFamily};
|
||||
font-size: ${sourceStyle.fontSize};
|
||||
line-height: ${sourceStyle.lineHeight};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const contentWrapper = document.createElement('div');
|
||||
contentWrapper.className = 'cm-content';
|
||||
contentWrapper.style.whiteSpace = 'pre';
|
||||
contentWrapper.style.cssText += inlineStyle(sourceStyle, [
|
||||
'color',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-weight',
|
||||
'font-style',
|
||||
'line-height',
|
||||
'letter-spacing',
|
||||
'tab-size',
|
||||
'text-rendering',
|
||||
'background',
|
||||
'background-color',
|
||||
'text-shadow',
|
||||
]);
|
||||
|
||||
const highlights = getHighlights(view, block.content.from, block.content.to);
|
||||
const fromLine = view.state.doc.lineAt(block.content.from);
|
||||
const toLine = view.state.doc.lineAt(block.content.to);
|
||||
for (let lineNum = fromLine.number; lineNum <= toLine.number; lineNum++) {
|
||||
const line = view.state.doc.line(lineNum);
|
||||
const lineElement = createHighlightedLine(line.text, line.from, line.to, highlights);
|
||||
contentWrapper.appendChild(lineElement);
|
||||
}
|
||||
|
||||
if (block.language.name && block.language.name !== 'text') {
|
||||
const langLabel = document.createElement('div');
|
||||
langLabel.className = 'block-language-label';
|
||||
langLabel.textContent = block.language.name;
|
||||
langLabel.style.cssText = `
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 10px;
|
||||
padding: 3px 8px;
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 11px;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
pointer-events: none;
|
||||
`;
|
||||
container.appendChild(langLabel);
|
||||
}
|
||||
|
||||
container.appendChild(contentWrapper);
|
||||
return container;
|
||||
} catch (error) {
|
||||
console.error('[blockImage] Failed to build block DOM:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Canvas 转换为 PNG Blob
|
||||
*/
|
||||
function canvasToPngBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(blob => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Canvas toBlob returned null'));
|
||||
}
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入剪贴板(PNG)
|
||||
*/
|
||||
async function writeImageToClipboard(blob: Blob): Promise<void> {
|
||||
const ClipboardItemCtor = (window as any).ClipboardItem;
|
||||
if (ClipboardItemCtor && navigator.clipboard?.write) {
|
||||
const item = new ClipboardItemCtor({'image/png': blob});
|
||||
await navigator.clipboard.write([item]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前活动块导出为图片并复制到剪贴板
|
||||
*/
|
||||
async function copyActiveBlockAsImage(view: EditorView): Promise<boolean> {
|
||||
const activeBlock = getActiveNoteBlock(view.state);
|
||||
if (!activeBlock) {
|
||||
console.warn('[blockImage] No active block found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetDom = view.scrollDOM || document.body;
|
||||
const prevCursor = (targetDom as HTMLElement).style.cursor;
|
||||
(targetDom as HTMLElement).style.cursor = 'progress';
|
||||
|
||||
const blockDom = getBlockDomElement(view, activeBlock);
|
||||
if (!blockDom) {
|
||||
console.warn('[blockImage] Cannot create block DOM');
|
||||
(targetDom as HTMLElement).style.cursor = prevCursor;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将节点挂到文档外层,确保样式可用
|
||||
const mount = document.createElement('div');
|
||||
mount.style.cssText = 'position: fixed; left: -10000px; top: -10000px; pointer-events: none; z-index: -1;';
|
||||
mount.appendChild(blockDom);
|
||||
document.body.appendChild(mount);
|
||||
|
||||
try {
|
||||
const canvas = await snapdom.toCanvas(blockDom, {
|
||||
scale: 2,
|
||||
dpr: window.devicePixelRatio || 1,
|
||||
cache: 'auto',
|
||||
backgroundColor: getComputedStyle(blockDom).backgroundColor,
|
||||
outerShadows: false,
|
||||
});
|
||||
|
||||
const blob = await canvasToPngBlob(canvas);
|
||||
await writeImageToClipboard(blob);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[blockImage] Failed to copy block image:', error);
|
||||
return false;
|
||||
} finally {
|
||||
mount.remove();
|
||||
(targetDom as HTMLElement).style.cursor = prevCursor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令:复制当前块为图片
|
||||
*/
|
||||
export const copyBlockImageCommand: Command = view => {
|
||||
void copyActiveBlockAsImage(view);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const blockImageEnabledFacet = Facet.define<boolean, boolean>({
|
||||
combine: values => values.some(Boolean),
|
||||
});
|
||||
|
||||
/**
|
||||
* BlockImage 扩展入口
|
||||
*/
|
||||
export function createBlockImageExtension(): Extension {
|
||||
return [
|
||||
blockImageEnabledFacet.of(true),
|
||||
];
|
||||
}
|
||||
|
||||
export default createBlockImageExtension;
|
||||
@@ -7,6 +7,9 @@ import { StateField, RangeSetBuilder, EditorState, Transaction } from "@codemirr
|
||||
import { blockState } from "./state";
|
||||
import { codeBlockEvent, USER_EVENTS } from "./annotation";
|
||||
|
||||
// IME 输入状态
|
||||
let isComposing = false;
|
||||
|
||||
/**
|
||||
* 块开始装饰组件
|
||||
*/
|
||||
@@ -222,9 +225,10 @@ const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any)
|
||||
|
||||
/**
|
||||
* 防止选择在第一个块之前
|
||||
* 使用 transactionFilter 来确保选择不会在第一个块之前
|
||||
*/
|
||||
const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr: any) => {
|
||||
if (isComposing) return tr;
|
||||
|
||||
if (tr.annotation(codeBlockEvent)) {
|
||||
return tr;
|
||||
}
|
||||
@@ -256,6 +260,24 @@ const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr: a
|
||||
return tr;
|
||||
});
|
||||
|
||||
// IME 状态同步
|
||||
const imeStateSynchronizer = ViewPlugin.fromClass(
|
||||
class {
|
||||
constructor(view: EditorView) {
|
||||
isComposing = view.composing || view.compositionStarted;
|
||||
}
|
||||
|
||||
update(update: any) {
|
||||
const view = update.view as EditorView;
|
||||
isComposing = view.composing || view.compositionStarted;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
isComposing = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取块装饰扩展 - 简化选项
|
||||
*/
|
||||
@@ -271,6 +293,7 @@ export function getBlockDecorationExtensions(options: {
|
||||
atomicNoteBlock,
|
||||
preventFirstBlockFromBeingDeleted,
|
||||
preventSelectionBeforeFirstBlock,
|
||||
imeStateSynchronizer,
|
||||
];
|
||||
|
||||
if (showBackground) {
|
||||
|
||||
@@ -39,6 +39,9 @@ export interface CodeBlockOptions {
|
||||
|
||||
/** 新建块时的默认语言 */
|
||||
defaultLanguage?: SupportedLanguage;
|
||||
|
||||
/** 分隔符高度(像素) */
|
||||
separatorHeight?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,140 +1,137 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { copyCommand, cutCommand, pasteCommand } from '../codeblock/copyPaste';
|
||||
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore';
|
||||
import { undo, redo } from '@codemirror/commands';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {Extension} from '@codemirror/state';
|
||||
import {copyCommand, cutCommand, pasteCommand} from '../codeblock/copyPaste';
|
||||
import {KeyBindingName} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||
import {redo, undo} from '@codemirror/commands';
|
||||
import i18n from '@/i18n';
|
||||
import { useSystemStore } from '@/stores/systemStore';
|
||||
import { showContextMenu } from './manager';
|
||||
import {
|
||||
buildRegisteredMenu,
|
||||
createMenuContext,
|
||||
registerMenuNodes
|
||||
} from './menuSchema';
|
||||
import type { MenuSchemaNode } from './menuSchema';
|
||||
import {useSystemStore} from '@/stores/systemStore';
|
||||
import {showContextMenu} from './manager';
|
||||
import type {MenuSchemaNode} from './menuSchema';
|
||||
import {buildRegisteredMenu, createMenuContext, registerMenuNodes} from './menuSchema';
|
||||
import {blockImageMenuNodes} from '../blockImage/contextMenu';
|
||||
|
||||
|
||||
function t(key: string): string {
|
||||
return i18n.global.t(key);
|
||||
return i18n.global.t(key);
|
||||
}
|
||||
|
||||
|
||||
function formatKeyBinding(keyBinding: string): string {
|
||||
const systemStore = useSystemStore();
|
||||
const isMac = systemStore.isMacOS;
|
||||
const systemStore = useSystemStore();
|
||||
const isMac = systemStore.isMacOS;
|
||||
|
||||
return keyBinding
|
||||
.replace("Mod", isMac ? "Cmd" : "Ctrl")
|
||||
.replace("Alt", isMac ? "Option" : "Alt")
|
||||
.replace(/-/g, " + ");
|
||||
return keyBinding
|
||||
.replace("Mod", isMac ? "Cmd" : "Ctrl")
|
||||
.replace("Alt", isMac ? "Option" : "Alt")
|
||||
.replace(/-/g, " + ");
|
||||
}
|
||||
|
||||
const shortcutCache = new Map<KeyBindingKey, string>();
|
||||
const shortcutCache = new Map<KeyBindingName, string>();
|
||||
|
||||
|
||||
function getShortcutText(keyBindingKey?: KeyBindingKey): string {
|
||||
if (keyBindingKey === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cached = shortcutCache.get(keyBindingKey);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const keybindingStore = useKeybindingStore();
|
||||
// binding.key 是命令标识符,binding.command 是快捷键组合
|
||||
const binding = keybindingStore.keyBindings.find(
|
||||
(kb) => kb.key === keyBindingKey && kb.enabled
|
||||
);
|
||||
|
||||
if (binding?.command) {
|
||||
const formatted = formatKeyBinding(binding.command);
|
||||
shortcutCache.set(keyBindingKey, formatted);
|
||||
return formatted;
|
||||
function getShortcutText(keyBindingKey?: KeyBindingName): string {
|
||||
if (keyBindingKey === undefined) {
|
||||
return "";
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("An error occurred while getting the shortcut:", error);
|
||||
}
|
||||
|
||||
shortcutCache.set(keyBindingKey, "");
|
||||
return "";
|
||||
const cached = shortcutCache.get(keyBindingKey);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const keybindingStore = useKeybindingStore();
|
||||
// binding.key 是命令标识符,binding.command 是快捷键组合
|
||||
const binding = keybindingStore.keyBindings.find(
|
||||
(kb) => kb.key === keyBindingKey && kb.enabled
|
||||
);
|
||||
|
||||
if (binding?.key) {
|
||||
const formatted = formatKeyBinding(binding.key);
|
||||
shortcutCache.set(keyBindingKey, formatted);
|
||||
return formatted;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("An error occurred while getting the shortcut:", error);
|
||||
}
|
||||
|
||||
shortcutCache.set(keyBindingKey, "");
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
function builtinMenuNodes(): MenuSchemaNode[] {
|
||||
return [
|
||||
{
|
||||
id: "copy",
|
||||
labelKey: "keybindings.commands.blockCopy",
|
||||
command: copyCommand,
|
||||
keyBindingKey: KeyBindingKey.BlockCopyKeyBindingKey,
|
||||
enabled: (context) => context.hasSelection
|
||||
},
|
||||
{
|
||||
id: "cut",
|
||||
labelKey: "keybindings.commands.blockCut",
|
||||
command: cutCommand,
|
||||
keyBindingKey: KeyBindingKey.BlockCutKeyBindingKey,
|
||||
visible: (context) => context.isEditable,
|
||||
enabled: (context) => context.hasSelection && context.isEditable
|
||||
},
|
||||
{
|
||||
id: "paste",
|
||||
labelKey: "keybindings.commands.blockPaste",
|
||||
command: pasteCommand,
|
||||
keyBindingKey: KeyBindingKey.BlockPasteKeyBindingKey,
|
||||
visible: (context) => context.isEditable
|
||||
},
|
||||
{
|
||||
id: "undo",
|
||||
labelKey: "keybindings.commands.historyUndo",
|
||||
command: undo,
|
||||
keyBindingKey: KeyBindingKey.HistoryUndoKeyBindingKey,
|
||||
visible: (context) => context.isEditable
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
labelKey: "keybindings.commands.historyRedo",
|
||||
command: redo,
|
||||
keyBindingKey: KeyBindingKey.HistoryRedoKeyBindingKey,
|
||||
visible: (context) => context.isEditable
|
||||
}
|
||||
];
|
||||
return [
|
||||
{
|
||||
id: "copy",
|
||||
labelKey: "keybindings.commands.blockCopy",
|
||||
command: copyCommand,
|
||||
keyBindingName: KeyBindingName.BlockCopy,
|
||||
enabled: (context) => context.hasSelection
|
||||
},
|
||||
{
|
||||
id: "cut",
|
||||
labelKey: "keybindings.commands.blockCut",
|
||||
command: cutCommand,
|
||||
keyBindingName: KeyBindingName.BlockCut,
|
||||
visible: (context) => context.isEditable,
|
||||
enabled: (context) => context.hasSelection && context.isEditable
|
||||
},
|
||||
{
|
||||
id: "paste",
|
||||
labelKey: "keybindings.commands.blockPaste",
|
||||
command: pasteCommand,
|
||||
keyBindingName: KeyBindingName.BlockPaste,
|
||||
visible: (context) => context.isEditable
|
||||
},
|
||||
{
|
||||
id: "undo",
|
||||
labelKey: "keybindings.commands.historyUndo",
|
||||
command: undo,
|
||||
keyBindingName: KeyBindingName.HistoryUndo,
|
||||
visible: (context) => context.isEditable
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
labelKey: "keybindings.commands.historyRedo",
|
||||
command: redo,
|
||||
keyBindingName: KeyBindingName.HistoryRedo,
|
||||
visible: (context) => context.isEditable
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
let builtinMenuRegistered = false;
|
||||
|
||||
function ensureBuiltinMenuRegistered(): void {
|
||||
if (builtinMenuRegistered) return;
|
||||
registerMenuNodes(builtinMenuNodes());
|
||||
builtinMenuRegistered = true;
|
||||
if (builtinMenuRegistered) return;
|
||||
registerMenuNodes([...builtinMenuNodes(), ...blockImageMenuNodes]);
|
||||
builtinMenuRegistered = true;
|
||||
}
|
||||
|
||||
|
||||
export function createEditorContextMenu(): Extension {
|
||||
ensureBuiltinMenuRegistered();
|
||||
ensureBuiltinMenuRegistered();
|
||||
|
||||
return EditorView.domEventHandlers({
|
||||
contextmenu: (event, view) => {
|
||||
event.preventDefault();
|
||||
return EditorView.domEventHandlers({
|
||||
contextmenu: (event, view) => {
|
||||
event.preventDefault();
|
||||
|
||||
const context = createMenuContext(view, event as MouseEvent);
|
||||
const menuItems = buildRegisteredMenu(context, {
|
||||
translate: t,
|
||||
formatShortcut: getShortcutText
|
||||
});
|
||||
const context = createMenuContext(view, event as MouseEvent);
|
||||
const menuItems = buildRegisteredMenu(context, {
|
||||
translate: t,
|
||||
formatShortcut: getShortcutText
|
||||
});
|
||||
|
||||
if (menuItems.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (menuItems.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
showContextMenu(view, event.clientX, event.clientY, menuItems);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
showContextMenu(view, event.clientX, event.clientY, menuItems);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default createEditorContextMenu;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { KeyBindingName } from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
export interface MenuContext {
|
||||
view: EditorView;
|
||||
@@ -16,7 +16,7 @@ export type MenuSchemaNode =
|
||||
type?: "action";
|
||||
labelKey: string;
|
||||
command?: (view: EditorView) => boolean;
|
||||
keyBindingKey?: KeyBindingKey;
|
||||
keyBindingName?: KeyBindingName;
|
||||
visible?: (context: MenuContext) => boolean;
|
||||
enabled?: (context: MenuContext) => boolean;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export interface RenderMenuItem {
|
||||
|
||||
interface MenuBuildOptions {
|
||||
translate: (key: string) => string;
|
||||
formatShortcut: (keyBindingKey?: KeyBindingKey) => string;
|
||||
formatShortcut: (keyBindingKey?: KeyBindingName) => string;
|
||||
}
|
||||
|
||||
const menuRegistry: MenuSchemaNode[] = [];
|
||||
@@ -89,7 +89,7 @@ function convertNode(
|
||||
}
|
||||
|
||||
const disabled = node.enabled ? !node.enabled(context) : false;
|
||||
const shortcut = options.formatShortcut(node.keyBindingKey);
|
||||
const shortcut = options.formatShortcut(node.keyBindingName);
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
|
||||
@@ -66,8 +66,14 @@ export function handleCodeBlock(
|
||||
ctx.seen.add(nf);
|
||||
ranges.push([nf, nt]);
|
||||
|
||||
// When cursor/selection is in this code block, don't add any decorations
|
||||
// This allows the selection background to be visible
|
||||
if (inCursor) return;
|
||||
|
||||
const startLine = ctx.view.state.doc.lineAt(nf);
|
||||
const endLine = ctx.view.state.doc.lineAt(nt);
|
||||
|
||||
// Add background decorations for each line
|
||||
for (let num = startLine.number; num <= endLine.number; num++) {
|
||||
const line = ctx.view.state.doc.line(num);
|
||||
let deco = DECO_CODEBLOCK_LINE;
|
||||
@@ -76,14 +82,14 @@ export function handleCodeBlock(
|
||||
else if (num === endLine.number) deco = DECO_CODEBLOCK_END;
|
||||
ctx.items.push({ from: line.from, to: line.from, deco });
|
||||
}
|
||||
if (!inCursor) {
|
||||
const codeInfo = node.getChild('CodeInfo');
|
||||
const codeMarks = node.getChildren('CodeMark');
|
||||
const language = codeInfo ? ctx.view.state.doc.sliceString(codeInfo.from, codeInfo.to).trim() : null;
|
||||
ctx.items.push({ from: startLine.to, to: startLine.to, deco: Decoration.widget({ widget: new CodeBlockInfoWidget(nf, nt, language), side: 1 }), priority: 1 });
|
||||
if (codeInfo) ctx.items.push({ from: codeInfo.from, to: codeInfo.to, deco: invisibleDecoration });
|
||||
for (const mark of codeMarks) ctx.items.push({ from: mark.from, to: mark.to, deco: invisibleDecoration });
|
||||
}
|
||||
|
||||
// Add language info widget and hide code marks
|
||||
const codeInfo = node.getChild('CodeInfo');
|
||||
const codeMarks = node.getChildren('CodeMark');
|
||||
const language = codeInfo ? ctx.view.state.doc.sliceString(codeInfo.from, codeInfo.to).trim() : null;
|
||||
ctx.items.push({ from: startLine.to, to: startLine.to, deco: Decoration.widget({ widget: new CodeBlockInfoWidget(nf, nt, language), side: 1 }), priority: 1 });
|
||||
if (codeInfo) ctx.items.push({ from: codeInfo.from, to: codeInfo.to, deco: invisibleDecoration });
|
||||
for (const mark of codeMarks) ctx.items.push({ from: mark.from, to: mark.to, deco: invisibleDecoration });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,8 +18,22 @@ import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines';
|
||||
import {transposeChars} from '../extensions/codeblock';
|
||||
import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste';
|
||||
import {
|
||||
addCursorAbove,
|
||||
addCursorBelow,
|
||||
copyLineDown,
|
||||
copyLineUp,
|
||||
cursorCharLeft,
|
||||
cursorCharRight,
|
||||
cursorLineDown,
|
||||
cursorLineUp,
|
||||
cursorPageDown,
|
||||
cursorPageUp,
|
||||
cursorDocEnd,
|
||||
cursorDocStart,
|
||||
cursorGroupLeft,
|
||||
cursorGroupRight,
|
||||
cursorLineEnd,
|
||||
cursorLineStart,
|
||||
cursorMatchingBracket,
|
||||
cursorSyntaxLeft,
|
||||
cursorSyntaxRight,
|
||||
@@ -27,6 +41,8 @@ import {
|
||||
deleteCharForward,
|
||||
deleteGroupBackward,
|
||||
deleteGroupForward,
|
||||
deleteToLineEnd,
|
||||
deleteToLineStart,
|
||||
indentLess,
|
||||
indentMore,
|
||||
indentSelection,
|
||||
@@ -34,10 +50,23 @@ import {
|
||||
insertNewlineAndIndent,
|
||||
redo,
|
||||
redoSelection,
|
||||
selectCharLeft,
|
||||
selectCharRight,
|
||||
selectLineDown,
|
||||
selectLineUp,
|
||||
selectDocEnd,
|
||||
selectDocStart,
|
||||
selectGroupLeft,
|
||||
selectGroupRight,
|
||||
selectLine,
|
||||
selectLineEnd,
|
||||
selectLineStart,
|
||||
selectMatchingBracket,
|
||||
selectParentSyntax,
|
||||
selectSyntaxLeft,
|
||||
selectSyntaxRight,
|
||||
simplifySelection,
|
||||
splitLine,
|
||||
toggleBlockComment,
|
||||
toggleComment,
|
||||
undo,
|
||||
@@ -45,9 +74,9 @@ import {
|
||||
} from '@codemirror/commands';
|
||||
import {foldAll, foldCode, unfoldAll, unfoldCode} from '@codemirror/language';
|
||||
import i18n from '@/i18n';
|
||||
import {KeyBindingKey} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {KeyBindingName} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {copyBlockImageCommand} from '../extensions/blockImage';
|
||||
|
||||
// 默认代码块扩展选项
|
||||
const defaultBlockExtensionOptions = {
|
||||
defaultBlockToken: 'text',
|
||||
defaultBlockAutoDetect: true,
|
||||
@@ -58,202 +87,324 @@ const defaultBlockExtensionOptions = {
|
||||
* 将后端定义的key字段映射到具体的前端方法和翻译键
|
||||
*/
|
||||
export const commands: Record<string, { handler: any; descriptionKey: string }> = {
|
||||
[KeyBindingKey.ShowSearchKeyBindingKey]: {
|
||||
[KeyBindingName.ShowSearch]: {
|
||||
handler: openSearchPanel,
|
||||
descriptionKey: 'keybindings.commands.showSearch'
|
||||
},
|
||||
[KeyBindingKey.HideSearchKeyBindingKey]: {
|
||||
[KeyBindingName.HideSearch]: {
|
||||
handler: closeSearchPanel,
|
||||
descriptionKey: 'keybindings.commands.hideSearch'
|
||||
},
|
||||
[KeyBindingKey.BlockSelectAllKeyBindingKey]: {
|
||||
[KeyBindingName.BlockSelectAll]: {
|
||||
handler: selectAll,
|
||||
descriptionKey: 'keybindings.commands.blockSelectAll'
|
||||
},
|
||||
[KeyBindingKey.BlockAddAfterCurrentKeyBindingKey]: {
|
||||
[KeyBindingName.BlockAddAfterCurrent]: {
|
||||
handler: addNewBlockAfterCurrent(defaultBlockExtensionOptions),
|
||||
descriptionKey: 'keybindings.commands.blockAddAfterCurrent'
|
||||
},
|
||||
[KeyBindingKey.BlockAddAfterLastKeyBindingKey]: {
|
||||
[KeyBindingName.BlockAddAfterLast]: {
|
||||
handler: addNewBlockAfterLast(defaultBlockExtensionOptions),
|
||||
descriptionKey: 'keybindings.commands.blockAddAfterLast'
|
||||
},
|
||||
[KeyBindingKey.BlockAddBeforeCurrentKeyBindingKey]: {
|
||||
[KeyBindingName.BlockAddBeforeCurrent]: {
|
||||
handler: addNewBlockBeforeCurrent(defaultBlockExtensionOptions),
|
||||
descriptionKey: 'keybindings.commands.blockAddBeforeCurrent'
|
||||
},
|
||||
[KeyBindingKey.BlockGotoPreviousKeyBindingKey]: {
|
||||
[KeyBindingName.BlockGotoPrevious]: {
|
||||
handler: gotoPreviousBlock,
|
||||
descriptionKey: 'keybindings.commands.blockGotoPrevious'
|
||||
},
|
||||
[KeyBindingKey.BlockGotoNextKeyBindingKey]: {
|
||||
[KeyBindingName.BlockGotoNext]: {
|
||||
handler: gotoNextBlock,
|
||||
descriptionKey: 'keybindings.commands.blockGotoNext'
|
||||
},
|
||||
[KeyBindingKey.BlockSelectPreviousKeyBindingKey]: {
|
||||
[KeyBindingName.BlockSelectPrevious]: {
|
||||
handler: selectPreviousBlock,
|
||||
descriptionKey: 'keybindings.commands.blockSelectPrevious'
|
||||
},
|
||||
[KeyBindingKey.BlockSelectNextKeyBindingKey]: {
|
||||
[KeyBindingName.BlockSelectNext]: {
|
||||
handler: selectNextBlock,
|
||||
descriptionKey: 'keybindings.commands.blockSelectNext'
|
||||
},
|
||||
[KeyBindingKey.BlockDeleteKeyBindingKey]: {
|
||||
[KeyBindingName.BlockDelete]: {
|
||||
handler: deleteBlock(defaultBlockExtensionOptions),
|
||||
descriptionKey: 'keybindings.commands.blockDelete'
|
||||
},
|
||||
[KeyBindingKey.BlockMoveUpKeyBindingKey]: {
|
||||
[KeyBindingName.BlockMoveUp]: {
|
||||
handler: moveCurrentBlockUp,
|
||||
descriptionKey: 'keybindings.commands.blockMoveUp'
|
||||
},
|
||||
[KeyBindingKey.BlockMoveDownKeyBindingKey]: {
|
||||
[KeyBindingName.BlockMoveDown]: {
|
||||
handler: moveCurrentBlockDown,
|
||||
descriptionKey: 'keybindings.commands.blockMoveDown'
|
||||
},
|
||||
[KeyBindingKey.BlockDeleteLineKeyBindingKey]: {
|
||||
[KeyBindingName.BlockDeleteLine]: {
|
||||
handler: deleteLineCommand,
|
||||
descriptionKey: 'keybindings.commands.blockDeleteLine'
|
||||
},
|
||||
[KeyBindingKey.BlockMoveLineUpKeyBindingKey]: {
|
||||
[KeyBindingName.BlockMoveLineUp]: {
|
||||
handler: moveLineUp,
|
||||
descriptionKey: 'keybindings.commands.blockMoveLineUp'
|
||||
},
|
||||
[KeyBindingKey.BlockMoveLineDownKeyBindingKey]: {
|
||||
[KeyBindingName.BlockMoveLineDown]: {
|
||||
handler: moveLineDown,
|
||||
descriptionKey: 'keybindings.commands.blockMoveLineDown'
|
||||
},
|
||||
[KeyBindingKey.BlockTransposeCharsKeyBindingKey]: {
|
||||
[KeyBindingName.BlockTransposeChars]: {
|
||||
handler: transposeChars,
|
||||
descriptionKey: 'keybindings.commands.blockTransposeChars'
|
||||
},
|
||||
[KeyBindingKey.BlockFormatKeyBindingKey]: {
|
||||
[KeyBindingName.BlockFormat]: {
|
||||
handler: formatCurrentBlock,
|
||||
descriptionKey: 'keybindings.commands.blockFormat'
|
||||
},
|
||||
[KeyBindingKey.BlockCopyKeyBindingKey]: {
|
||||
[KeyBindingName.BlockCopy]: {
|
||||
handler: copyCommand,
|
||||
descriptionKey: 'keybindings.commands.blockCopy'
|
||||
},
|
||||
[KeyBindingKey.BlockCutKeyBindingKey]: {
|
||||
[KeyBindingName.BlockCut]: {
|
||||
handler: cutCommand,
|
||||
descriptionKey: 'keybindings.commands.blockCut'
|
||||
},
|
||||
[KeyBindingKey.BlockPasteKeyBindingKey]: {
|
||||
[KeyBindingName.BlockPaste]: {
|
||||
handler: pasteCommand,
|
||||
descriptionKey: 'keybindings.commands.blockPaste'
|
||||
},
|
||||
[KeyBindingKey.HistoryUndoKeyBindingKey]: {
|
||||
[KeyBindingName.CopyBlockImage]: {
|
||||
handler: copyBlockImageCommand,
|
||||
descriptionKey: 'keybindings.commands.copyBlockImage'
|
||||
},
|
||||
[KeyBindingName.HistoryUndo]: {
|
||||
handler: undo,
|
||||
descriptionKey: 'keybindings.commands.historyUndo'
|
||||
},
|
||||
[KeyBindingKey.HistoryRedoKeyBindingKey]: {
|
||||
[KeyBindingName.HistoryRedo]: {
|
||||
handler: redo,
|
||||
descriptionKey: 'keybindings.commands.historyRedo'
|
||||
},
|
||||
[KeyBindingKey.HistoryUndoSelectionKeyBindingKey]: {
|
||||
[KeyBindingName.HistoryUndoSelection]: {
|
||||
handler: undoSelection,
|
||||
descriptionKey: 'keybindings.commands.historyUndoSelection'
|
||||
},
|
||||
[KeyBindingKey.HistoryRedoSelectionKeyBindingKey]: {
|
||||
[KeyBindingName.HistoryRedoSelection]: {
|
||||
handler: redoSelection,
|
||||
descriptionKey: 'keybindings.commands.historyRedoSelection'
|
||||
},
|
||||
[KeyBindingKey.FoldCodeKeyBindingKey]: {
|
||||
[KeyBindingName.FoldCode]: {
|
||||
handler: foldCode,
|
||||
descriptionKey: 'keybindings.commands.foldCode'
|
||||
},
|
||||
[KeyBindingKey.UnfoldCodeKeyBindingKey]: {
|
||||
[KeyBindingName.UnfoldCode]: {
|
||||
handler: unfoldCode,
|
||||
descriptionKey: 'keybindings.commands.unfoldCode'
|
||||
},
|
||||
[KeyBindingKey.FoldAllKeyBindingKey]: {
|
||||
[KeyBindingName.FoldAll]: {
|
||||
handler: foldAll,
|
||||
descriptionKey: 'keybindings.commands.foldAll'
|
||||
},
|
||||
[KeyBindingKey.UnfoldAllKeyBindingKey]: {
|
||||
[KeyBindingName.UnfoldAll]: {
|
||||
handler: unfoldAll,
|
||||
descriptionKey: 'keybindings.commands.unfoldAll'
|
||||
},
|
||||
[KeyBindingKey.CursorSyntaxLeftKeyBindingKey]: {
|
||||
[KeyBindingName.CursorSyntaxLeft]: {
|
||||
handler: cursorSyntaxLeft,
|
||||
descriptionKey: 'keybindings.commands.cursorSyntaxLeft'
|
||||
},
|
||||
[KeyBindingKey.CursorSyntaxRightKeyBindingKey]: {
|
||||
[KeyBindingName.CursorSyntaxRight]: {
|
||||
handler: cursorSyntaxRight,
|
||||
descriptionKey: 'keybindings.commands.cursorSyntaxRight'
|
||||
},
|
||||
[KeyBindingKey.SelectSyntaxLeftKeyBindingKey]: {
|
||||
[KeyBindingName.SelectSyntaxLeft]: {
|
||||
handler: selectSyntaxLeft,
|
||||
descriptionKey: 'keybindings.commands.selectSyntaxLeft'
|
||||
},
|
||||
[KeyBindingKey.SelectSyntaxRightKeyBindingKey]: {
|
||||
[KeyBindingName.SelectSyntaxRight]: {
|
||||
handler: selectSyntaxRight,
|
||||
descriptionKey: 'keybindings.commands.selectSyntaxRight'
|
||||
},
|
||||
[KeyBindingKey.CopyLineUpKeyBindingKey]: {
|
||||
[KeyBindingName.CopyLineUp]: {
|
||||
handler: copyLineUp,
|
||||
descriptionKey: 'keybindings.commands.copyLineUp'
|
||||
},
|
||||
[KeyBindingKey.CopyLineDownKeyBindingKey]: {
|
||||
[KeyBindingName.CopyLineDown]: {
|
||||
handler: copyLineDown,
|
||||
descriptionKey: 'keybindings.commands.copyLineDown'
|
||||
},
|
||||
[KeyBindingKey.InsertBlankLineKeyBindingKey]: {
|
||||
[KeyBindingName.InsertBlankLine]: {
|
||||
handler: insertBlankLine,
|
||||
descriptionKey: 'keybindings.commands.insertBlankLine'
|
||||
},
|
||||
[KeyBindingKey.SelectLineKeyBindingKey]: {
|
||||
[KeyBindingName.SelectLine]: {
|
||||
handler: selectLine,
|
||||
descriptionKey: 'keybindings.commands.selectLine'
|
||||
},
|
||||
[KeyBindingKey.SelectParentSyntaxKeyBindingKey]: {
|
||||
[KeyBindingName.SelectParentSyntax]: {
|
||||
handler: selectParentSyntax,
|
||||
descriptionKey: 'keybindings.commands.selectParentSyntax'
|
||||
},
|
||||
[KeyBindingKey.IndentLessKeyBindingKey]: {
|
||||
[KeyBindingName.SimplifySelection]: {
|
||||
handler: simplifySelection,
|
||||
descriptionKey: 'keybindings.commands.simplifySelection'
|
||||
},
|
||||
[KeyBindingName.AddCursorAbove]: {
|
||||
handler: addCursorAbove,
|
||||
descriptionKey: 'keybindings.commands.addCursorAbove'
|
||||
},
|
||||
[KeyBindingName.AddCursorBelow]: {
|
||||
handler: addCursorBelow,
|
||||
descriptionKey: 'keybindings.commands.addCursorBelow'
|
||||
},
|
||||
[KeyBindingName.CursorGroupLeft]: {
|
||||
handler: cursorGroupLeft,
|
||||
descriptionKey: 'keybindings.commands.cursorGroupLeft'
|
||||
},
|
||||
[KeyBindingName.CursorGroupRight]: {
|
||||
handler: cursorGroupRight,
|
||||
descriptionKey: 'keybindings.commands.cursorGroupRight'
|
||||
},
|
||||
[KeyBindingName.SelectGroupLeft]: {
|
||||
handler: selectGroupLeft,
|
||||
descriptionKey: 'keybindings.commands.selectGroupLeft'
|
||||
},
|
||||
[KeyBindingName.SelectGroupRight]: {
|
||||
handler: selectGroupRight,
|
||||
descriptionKey: 'keybindings.commands.selectGroupRight'
|
||||
},
|
||||
[KeyBindingName.DeleteToLineEnd]: {
|
||||
handler: deleteToLineEnd,
|
||||
descriptionKey: 'keybindings.commands.deleteToLineEnd'
|
||||
},
|
||||
[KeyBindingName.DeleteToLineStart]: {
|
||||
handler: deleteToLineStart,
|
||||
descriptionKey: 'keybindings.commands.deleteToLineStart'
|
||||
},
|
||||
[KeyBindingName.CursorLineStart]: {
|
||||
handler: cursorLineStart,
|
||||
descriptionKey: 'keybindings.commands.cursorLineStart'
|
||||
},
|
||||
[KeyBindingName.CursorLineEnd]: {
|
||||
handler: cursorLineEnd,
|
||||
descriptionKey: 'keybindings.commands.cursorLineEnd'
|
||||
},
|
||||
[KeyBindingName.SelectLineStart]: {
|
||||
handler: selectLineStart,
|
||||
descriptionKey: 'keybindings.commands.selectLineStart'
|
||||
},
|
||||
[KeyBindingName.SelectLineEnd]: {
|
||||
handler: selectLineEnd,
|
||||
descriptionKey: 'keybindings.commands.selectLineEnd'
|
||||
},
|
||||
[KeyBindingName.CursorDocStart]: {
|
||||
handler: cursorDocStart,
|
||||
descriptionKey: 'keybindings.commands.cursorDocStart'
|
||||
},
|
||||
[KeyBindingName.CursorDocEnd]: {
|
||||
handler: cursorDocEnd,
|
||||
descriptionKey: 'keybindings.commands.cursorDocEnd'
|
||||
},
|
||||
[KeyBindingName.SelectDocStart]: {
|
||||
handler: selectDocStart,
|
||||
descriptionKey: 'keybindings.commands.selectDocStart'
|
||||
},
|
||||
[KeyBindingName.SelectDocEnd]: {
|
||||
handler: selectDocEnd,
|
||||
descriptionKey: 'keybindings.commands.selectDocEnd'
|
||||
},
|
||||
[KeyBindingName.SelectMatchingBracket]: {
|
||||
handler: selectMatchingBracket,
|
||||
descriptionKey: 'keybindings.commands.selectMatchingBracket'
|
||||
},
|
||||
[KeyBindingName.SplitLine]: {
|
||||
handler: splitLine,
|
||||
descriptionKey: 'keybindings.commands.splitLine'
|
||||
},
|
||||
[KeyBindingName.IndentLess]: {
|
||||
handler: indentLess,
|
||||
descriptionKey: 'keybindings.commands.indentLess'
|
||||
},
|
||||
[KeyBindingKey.IndentMoreKeyBindingKey]: {
|
||||
[KeyBindingName.IndentMore]: {
|
||||
handler: indentMore,
|
||||
descriptionKey: 'keybindings.commands.indentMore'
|
||||
},
|
||||
[KeyBindingKey.IndentSelectionKeyBindingKey]: {
|
||||
[KeyBindingName.IndentSelection]: {
|
||||
handler: indentSelection,
|
||||
descriptionKey: 'keybindings.commands.indentSelection'
|
||||
},
|
||||
[KeyBindingKey.CursorMatchingBracketKeyBindingKey]: {
|
||||
[KeyBindingName.CursorMatchingBracket]: {
|
||||
handler: cursorMatchingBracket,
|
||||
descriptionKey: 'keybindings.commands.cursorMatchingBracket'
|
||||
},
|
||||
[KeyBindingKey.ToggleCommentKeyBindingKey]: {
|
||||
[KeyBindingName.ToggleComment]: {
|
||||
handler: toggleComment,
|
||||
descriptionKey: 'keybindings.commands.toggleComment'
|
||||
},
|
||||
[KeyBindingKey.ToggleBlockCommentKeyBindingKey]: {
|
||||
[KeyBindingName.ToggleBlockComment]: {
|
||||
handler: toggleBlockComment,
|
||||
descriptionKey: 'keybindings.commands.toggleBlockComment'
|
||||
},
|
||||
[KeyBindingKey.InsertNewlineAndIndentKeyBindingKey]: {
|
||||
[KeyBindingName.InsertNewlineAndIndent]: {
|
||||
handler: insertNewlineAndIndent,
|
||||
descriptionKey: 'keybindings.commands.insertNewlineAndIndent'
|
||||
},
|
||||
[KeyBindingKey.DeleteCharBackwardKeyBindingKey]: {
|
||||
[KeyBindingName.DeleteCharBackward]: {
|
||||
handler: deleteCharBackward,
|
||||
descriptionKey: 'keybindings.commands.deleteCharBackward'
|
||||
},
|
||||
[KeyBindingKey.DeleteCharForwardKeyBindingKey]: {
|
||||
[KeyBindingName.DeleteCharForward]: {
|
||||
handler: deleteCharForward,
|
||||
descriptionKey: 'keybindings.commands.deleteCharForward'
|
||||
},
|
||||
[KeyBindingKey.DeleteGroupBackwardKeyBindingKey]: {
|
||||
[KeyBindingName.DeleteGroupBackward]: {
|
||||
handler: deleteGroupBackward,
|
||||
descriptionKey: 'keybindings.commands.deleteGroupBackward'
|
||||
},
|
||||
[KeyBindingKey.DeleteGroupForwardKeyBindingKey]: {
|
||||
[KeyBindingName.DeleteGroupForward]: {
|
||||
handler: deleteGroupForward,
|
||||
descriptionKey: 'keybindings.commands.deleteGroupForward'
|
||||
},
|
||||
|
||||
// Emacs 模式额外的基础导航命令
|
||||
[KeyBindingName.CursorCharLeft]: {
|
||||
handler: cursorCharLeft,
|
||||
descriptionKey: 'keybindings.commands.cursorCharLeft'
|
||||
},
|
||||
[KeyBindingName.CursorCharRight]: {
|
||||
handler: cursorCharRight,
|
||||
descriptionKey: 'keybindings.commands.cursorCharRight'
|
||||
},
|
||||
[KeyBindingName.CursorLineUp]: {
|
||||
handler: cursorLineUp,
|
||||
descriptionKey: 'keybindings.commands.cursorLineUp'
|
||||
},
|
||||
[KeyBindingName.CursorLineDown]: {
|
||||
handler: cursorLineDown,
|
||||
descriptionKey: 'keybindings.commands.cursorLineDown'
|
||||
},
|
||||
[KeyBindingName.CursorPageUp]: {
|
||||
handler: cursorPageUp,
|
||||
descriptionKey: 'keybindings.commands.cursorPageUp'
|
||||
},
|
||||
[KeyBindingName.CursorPageDown]: {
|
||||
handler: cursorPageDown,
|
||||
descriptionKey: 'keybindings.commands.cursorPageDown'
|
||||
},
|
||||
[KeyBindingName.SelectCharLeft]: {
|
||||
handler: selectCharLeft,
|
||||
descriptionKey: 'keybindings.commands.selectCharLeft'
|
||||
},
|
||||
[KeyBindingName.SelectCharRight]: {
|
||||
handler: selectCharRight,
|
||||
descriptionKey: 'keybindings.commands.selectCharRight'
|
||||
},
|
||||
[KeyBindingName.SelectLineUp]: {
|
||||
handler: selectLineUp,
|
||||
descriptionKey: 'keybindings.commands.selectLineUp'
|
||||
},
|
||||
[KeyBindingName.SelectLineDown]: {
|
||||
handler: selectLineDown,
|
||||
descriptionKey: 'keybindings.commands.selectLineDown'
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,4 +28,3 @@ export const updateKeymapExtension = (view: any): void => {
|
||||
// 导出相关模块
|
||||
export { Manager } from './manager';
|
||||
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
|
||||
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';
|
||||
@@ -1,7 +1,6 @@
|
||||
import {keymap} from '@codemirror/view';
|
||||
import {Extension, Compartment} from '@codemirror/state';
|
||||
import {KeyBinding, keymap} from '@codemirror/view';
|
||||
import {Compartment, Extension} from '@codemirror/state';
|
||||
import {KeyBinding as KeyBindingConfig} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
import {KeyBinding, KeymapResult} from './types';
|
||||
import {getCommandHandler, isCommandRegistered} from './commands';
|
||||
|
||||
/**
|
||||
@@ -16,7 +15,7 @@ export class Manager {
|
||||
* @param keyBindings 后端快捷键配置列表
|
||||
* @returns 转换结果
|
||||
*/
|
||||
static convertToKeyBindings(keyBindings: KeyBindingConfig[]): KeymapResult {
|
||||
static convertToKeyBindings(keyBindings: KeyBindingConfig[]): KeyBinding[] {
|
||||
const result: KeyBinding[] = [];
|
||||
|
||||
for (const binding of keyBindings) {
|
||||
@@ -25,29 +24,31 @@ export class Manager {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查命令是否已注册(使用 key 字段作为命令标识符)
|
||||
if (!binding.key || !isCommandRegistered(binding.key)) {
|
||||
// 检查命令是否已注册
|
||||
if (!binding.name || !isCommandRegistered(binding.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取命令处理函数
|
||||
const handler = getCommandHandler(binding.key);
|
||||
const handler = getCommandHandler(binding.name);
|
||||
if (!handler) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 转换为CodeMirror快捷键格式
|
||||
// binding.command 是快捷键组合 (如 "Mod-f"),binding.key 是命令标识符
|
||||
const keyBinding: KeyBinding = {
|
||||
key: binding.command || '',
|
||||
key: binding.key || '',
|
||||
mac: binding.macos || undefined,
|
||||
win: binding.windows || undefined,
|
||||
linux: binding.linux || undefined,
|
||||
run: handler,
|
||||
preventDefault: true
|
||||
preventDefault: binding.preventDefault,
|
||||
scope: binding.scope || undefined
|
||||
};
|
||||
|
||||
result.push(keyBinding);
|
||||
}
|
||||
|
||||
return {keyBindings: result};
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +57,7 @@ export class Manager {
|
||||
* @returns CodeMirror扩展
|
||||
*/
|
||||
static createKeymapExtension(keyBindings: KeyBindingConfig[]): Extension {
|
||||
const {keyBindings: cmKeyBindings} = this.convertToKeyBindings(keyBindings);
|
||||
const cmKeyBindings = this.convertToKeyBindings(keyBindings);
|
||||
return this.compartment.of(keymap.of(cmKeyBindings));
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ export class Manager {
|
||||
* @param keyBindings 后端快捷键配置列表
|
||||
*/
|
||||
static updateKeymap(view: any, keyBindings: KeyBindingConfig[]): void {
|
||||
const {keyBindings: cmKeyBindings} = this.convertToKeyBindings(keyBindings);
|
||||
const cmKeyBindings = this.convertToKeyBindings(keyBindings);
|
||||
view.dispatch({
|
||||
effects: this.compartment.reconfigure(keymap.of(cmKeyBindings))
|
||||
});
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import {Command} from '@codemirror/view';
|
||||
|
||||
/**
|
||||
* CodeMirror快捷键绑定格式
|
||||
*/
|
||||
export interface KeyBinding {
|
||||
key: string
|
||||
run: Command
|
||||
preventDefault?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令处理函数类型
|
||||
*/
|
||||
export type CommandHandler = Command
|
||||
|
||||
/**
|
||||
* 命令定义接口
|
||||
*/
|
||||
export interface CommandDefinition {
|
||||
handler: CommandHandler
|
||||
descriptionKey: string // 翻译键
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷键转换结果
|
||||
*/
|
||||
export interface KeymapResult {
|
||||
keyBindings: KeyBinding[]
|
||||
}
|
||||
@@ -15,7 +15,8 @@ import {highlightActiveLineGutter, highlightWhitespace, highlightTrailingWhitesp
|
||||
import createEditorContextMenu from '../extensions/contextMenu';
|
||||
import {blockLineNumbers} from '../extensions/codeblock';
|
||||
import {createHttpClientExtension} from '../extensions/httpclient';
|
||||
import {ExtensionKey} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {createBlockImageExtension} from '../extensions/blockImage';
|
||||
import {ExtensionName} from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
type ExtensionEntry = {
|
||||
definition: ExtensionDefinition
|
||||
@@ -24,35 +25,35 @@ type ExtensionEntry = {
|
||||
};
|
||||
|
||||
// 排除 $zero 的有效扩展 Key 类型
|
||||
type ValidExtensionKey = Exclude<ExtensionKey, ExtensionKey.$zero>;
|
||||
type ValidExtensionName = Exclude<ExtensionName, ExtensionName.$zero>;
|
||||
|
||||
const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
|
||||
create,
|
||||
defaultConfig
|
||||
});
|
||||
|
||||
const EXTENSION_REGISTRY: Record<ValidExtensionKey, ExtensionEntry> = {
|
||||
[ExtensionKey.ExtensionRainbowBrackets]: {
|
||||
const EXTENSION_REGISTRY: Record<ValidExtensionName, ExtensionEntry> = {
|
||||
[ExtensionName.RainbowBrackets]: {
|
||||
definition: defineExtension(() => rainbowBrackets()),
|
||||
displayNameKey: 'extensions.rainbowBrackets.name',
|
||||
descriptionKey: 'extensions.rainbowBrackets.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionHyperlink]: {
|
||||
[ExtensionName.Hyperlink]: {
|
||||
definition: defineExtension(() => hyperLink),
|
||||
displayNameKey: 'extensions.hyperlink.name',
|
||||
descriptionKey: 'extensions.hyperlink.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionColorSelector]: {
|
||||
[ExtensionName.ColorSelector]: {
|
||||
definition: defineExtension(() => color),
|
||||
displayNameKey: 'extensions.colorSelector.name',
|
||||
descriptionKey: 'extensions.colorSelector.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionTranslator]: {
|
||||
[ExtensionName.Translator]: {
|
||||
definition: defineExtension(() => createTranslatorExtension()),
|
||||
displayNameKey: 'extensions.translator.name',
|
||||
descriptionKey: 'extensions.translator.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionMinimap]: {
|
||||
[ExtensionName.Minimap]: {
|
||||
definition: defineExtension((config: any) => minimap({
|
||||
displayText: config?.displayText ?? 'characters',
|
||||
showOverlay: config?.showOverlay ?? 'always',
|
||||
@@ -65,49 +66,54 @@ const EXTENSION_REGISTRY: Record<ValidExtensionKey, ExtensionEntry> = {
|
||||
displayNameKey: 'extensions.minimap.name',
|
||||
descriptionKey: 'extensions.minimap.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionSearch]: {
|
||||
[ExtensionName.Search]: {
|
||||
definition: defineExtension(() => vscodeSearch),
|
||||
displayNameKey: 'extensions.search.name',
|
||||
descriptionKey: 'extensions.search.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionFold]: {
|
||||
[ExtensionName.Fold]: {
|
||||
definition: defineExtension(() => Prec.low(foldGutter())),
|
||||
displayNameKey: 'extensions.fold.name',
|
||||
descriptionKey: 'extensions.fold.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionMarkdown]: {
|
||||
[ExtensionName.Markdown]: {
|
||||
definition: defineExtension(() => markdownExtensions),
|
||||
displayNameKey: 'extensions.markdown.name',
|
||||
descriptionKey: 'extensions.markdown.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionLineNumbers]: {
|
||||
[ExtensionName.LineNumbers]: {
|
||||
definition: defineExtension(() => Prec.high([blockLineNumbers, highlightActiveLineGutter()])),
|
||||
displayNameKey: 'extensions.lineNumbers.name',
|
||||
descriptionKey: 'extensions.lineNumbers.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionContextMenu]: {
|
||||
[ExtensionName.ContextMenu]: {
|
||||
definition: defineExtension(() => createEditorContextMenu()),
|
||||
displayNameKey: 'extensions.contextMenu.name',
|
||||
descriptionKey: 'extensions.contextMenu.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionHighlightWhitespace]: {
|
||||
[ExtensionName.HighlightWhitespace]: {
|
||||
definition: defineExtension(() => highlightWhitespace()),
|
||||
displayNameKey: 'extensions.highlightWhitespace.name',
|
||||
descriptionKey: 'extensions.highlightWhitespace.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionHighlightTrailingWhitespace]: {
|
||||
[ExtensionName.HighlightTrailingWhitespace]: {
|
||||
definition: defineExtension(() => highlightTrailingWhitespace()),
|
||||
displayNameKey: 'extensions.highlightTrailingWhitespace.name',
|
||||
descriptionKey: 'extensions.highlightTrailingWhitespace.description'
|
||||
},
|
||||
[ExtensionKey.ExtensionHttpClient]: {
|
||||
[ExtensionName.HttpClient]: {
|
||||
definition: defineExtension(() => createHttpClientExtension()),
|
||||
displayNameKey: 'extensions.httpClient.name',
|
||||
descriptionKey: 'extensions.httpClient.description'
|
||||
},
|
||||
[ExtensionName.BlockImage]: {
|
||||
definition: defineExtension(() => createBlockImageExtension()),
|
||||
displayNameKey: 'extensions.blockImage.name',
|
||||
descriptionKey: 'extensions.blockImage.description'
|
||||
}
|
||||
};
|
||||
|
||||
const isRegisteredExtension = (key: string): key is ValidExtensionKey =>
|
||||
const isRegisteredExtension = (key: string): key is ValidExtensionName =>
|
||||
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, key);
|
||||
|
||||
const getRegistryEntry = (key: string): ExtensionEntry | undefined => {
|
||||
@@ -118,7 +124,7 @@ const getRegistryEntry = (key: string): ExtensionEntry | undefined => {
|
||||
};
|
||||
|
||||
export function registerAllExtensions(manager: Manager): void {
|
||||
(Object.entries(EXTENSION_REGISTRY) as [ValidExtensionKey, ExtensionEntry][]).forEach(([id, entry]) => {
|
||||
(Object.entries(EXTENSION_REGISTRY) as [ValidExtensionName, ExtensionEntry][]).forEach(([id, entry]) => {
|
||||
manager.registerExtension(id, entry.definition);
|
||||
});
|
||||
}
|
||||
@@ -147,7 +153,7 @@ export function hasExtensionConfig(key: string): boolean {
|
||||
return Object.keys(getExtensionDefaultConfig(key)).length > 0;
|
||||
}
|
||||
|
||||
export function getAllExtensionIds(): string[] {
|
||||
export function getExtensionsMap(): string[] {
|
||||
return Object.keys(EXTENSION_REGISTRY);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ export class Manager {
|
||||
private extensionStates = new Map<string, ExtensionState>();
|
||||
private views = new Map<number, EditorView>();
|
||||
|
||||
registerExtension(id: string, definition: ExtensionDefinition): void {
|
||||
const existingState = this.extensionStates.get(id);
|
||||
registerExtension(name: string, definition: ExtensionDefinition): void {
|
||||
const existingState = this.extensionStates.get(name);
|
||||
if (existingState) {
|
||||
existingState.definition = definition;
|
||||
if (existingState.config === undefined) {
|
||||
@@ -21,8 +21,8 @@ export class Manager {
|
||||
} else {
|
||||
const compartment = new Compartment();
|
||||
const defaultConfig = this.cloneConfig(definition.defaultConfig ?? {});
|
||||
this.extensionStates.set(id, {
|
||||
id,
|
||||
this.extensionStates.set(name, {
|
||||
name,
|
||||
definition,
|
||||
config: defaultConfig,
|
||||
enabled: false,
|
||||
@@ -34,8 +34,8 @@ export class Manager {
|
||||
|
||||
initExtensions(extensionConfigs: ExtensionConfig[]): void {
|
||||
for (const config of extensionConfigs) {
|
||||
if (!config.key) continue;
|
||||
const state = this.extensionStates.get(config.key);
|
||||
if (!config.name) continue;
|
||||
const state = this.extensionStates.get(config.name);
|
||||
if (!state) continue;
|
||||
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
|
||||
this.commitExtensionState(state, config.enabled ?? false, resolvedConfig);
|
||||
@@ -88,9 +88,9 @@ export class Manager {
|
||||
state.enabled = enabled;
|
||||
state.config = config;
|
||||
state.extension = runtimeExtension;
|
||||
this.applyExtensionToAllViews(state.id);
|
||||
this.applyExtensionToAllViews(state.name);
|
||||
} catch (error) {
|
||||
console.error(`Failed to update extension ${state.id}:`, error);
|
||||
console.error(`Failed to update extension ${state.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface ExtensionDefinition {
|
||||
* 扩展运行时状态
|
||||
*/
|
||||
export interface ExtensionState {
|
||||
id: string // 扩展 key
|
||||
name: string
|
||||
definition: ExtensionDefinition
|
||||
config: any
|
||||
enabled: boolean
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import MemoryMonitor from '@/components/monitor/MemoryMonitor.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 计算属性
|
||||
const enableMemoryMonitor = computed(() => configStore.config.general.enableMemoryMonitor);
|
||||
|
||||
// 导航配置
|
||||
const navItems = [
|
||||
@@ -64,7 +69,7 @@ const goBackToEditor = async () => {
|
||||
<span class="nav-text">{{ item.id === 'test' ? 'Test' : t(`settings.${item.id}`) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<div class="settings-footer" v-if="enableMemoryMonitor">
|
||||
<div class="memory-info-section">
|
||||
<div class="section-title">{{ t('settings.systemInfo') }}</div>
|
||||
<MemoryMonitor />
|
||||
|
||||
@@ -140,9 +140,6 @@ const applyChanges = async () => {
|
||||
// 保存到数据库
|
||||
await themeStore.saveCurrentTheme();
|
||||
|
||||
// 刷新编辑器主题
|
||||
themeStore.refreshEditorTheme();
|
||||
|
||||
// 清除未保存标记
|
||||
hasUnsavedChanges.value = false;
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,48 +2,18 @@
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useBackupStore} from '@/stores/backupStore';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {computed, ref, watch, onUnmounted} from 'vue';
|
||||
import {computed} from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
import {AuthMethod} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {DialogService} from '@/../bindings/voidraft/internal/services';
|
||||
import toast from '@/components/toast';
|
||||
|
||||
const {t} = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const backupStore = useBackupStore();
|
||||
|
||||
// 消息显示状态
|
||||
const message = ref<string | null>(null);
|
||||
const isError = ref(false);
|
||||
let messageTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clearMessage = () => {
|
||||
if (messageTimer) {
|
||||
clearTimeout(messageTimer);
|
||||
messageTimer = null;
|
||||
}
|
||||
message.value = null;
|
||||
};
|
||||
|
||||
// 监听同步完成,显示消息并自动消失
|
||||
watch(() => backupStore.isSyncing, (syncing, wasSyncing) => {
|
||||
if (wasSyncing && !syncing) {
|
||||
clearMessage();
|
||||
if (backupStore.error) {
|
||||
message.value = backupStore.error;
|
||||
isError.value = true;
|
||||
messageTimer = setTimeout(clearMessage, 5000);
|
||||
} else {
|
||||
message.value = 'Sync successful';
|
||||
isError.value = false;
|
||||
messageTimer = setTimeout(clearMessage, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(clearMessage);
|
||||
|
||||
const authMethodOptions = computed(() => [
|
||||
{value: AuthMethod.Token, label: t('settings.backup.authMethods.token')},
|
||||
{value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')},
|
||||
@@ -64,6 +34,15 @@ const selectSshKeyFile = async () => {
|
||||
configStore.setSshKeyPath(selectedPath.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
await backupStore.sync();
|
||||
toast.success('Sync successful');
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -202,14 +181,10 @@ const selectSshKeyFile = async () => {
|
||||
|
||||
<!-- 备份操作 -->
|
||||
<SettingSection :title="t('settings.backup.backupOperations')">
|
||||
<SettingItem
|
||||
:title="t('settings.backup.syncToRemote')"
|
||||
:description="message || undefined"
|
||||
:descriptionType="message ? (isError ? 'error' : 'success') : 'default'"
|
||||
>
|
||||
<SettingItem :title="t('settings.backup.syncToRemote')">
|
||||
<button
|
||||
class="sync-button"
|
||||
@click="backupStore.sync"
|
||||
@click="handleSync"
|
||||
:disabled="!configStore.config.backup.enabled || !configStore.config.backup.repo_url || backupStore.isSyncing"
|
||||
:class="{ 'syncing': backupStore.isSyncing }"
|
||||
>
|
||||
@@ -222,10 +197,6 @@ const selectSshKeyFile = async () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
//max-width: 800px;
|
||||
}
|
||||
|
||||
// 统一的输入控件样式
|
||||
.repo-url-input,
|
||||
.branch-input,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useEditorStore } from '@/stores/editorStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {computed, onMounted } from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
@@ -9,6 +10,7 @@ import { TabType } from '@/../bindings/voidraft/internal/models/';
|
||||
|
||||
const { t } = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const editorStore = useEditorStore();
|
||||
|
||||
// 确保配置已加载
|
||||
onMounted(async () => {
|
||||
@@ -27,6 +29,7 @@ const fontFamilyModel = computed({
|
||||
set: async (fontFamily: string) => {
|
||||
if (fontFamily) {
|
||||
await configStore.setFontFamily(fontFamily);
|
||||
editorStore.applyFontSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -50,6 +53,7 @@ const fontWeightModel = computed({
|
||||
set: async (value: string) => {
|
||||
if (value) {
|
||||
await configStore.setFontWeight(value);
|
||||
editorStore.applyFontSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -58,20 +62,24 @@ const fontWeightModel = computed({
|
||||
const increaseLineHeight = async () => {
|
||||
const newLineHeight = Math.min(3.0, configStore.config.editing.lineHeight + 0.1);
|
||||
await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10);
|
||||
editorStore.applyFontSettings();
|
||||
};
|
||||
|
||||
const decreaseLineHeight = async () => {
|
||||
const newLineHeight = Math.max(1.0, configStore.config.editing.lineHeight - 0.1);
|
||||
await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10);
|
||||
editorStore.applyFontSettings();
|
||||
};
|
||||
|
||||
// 字体大小控制
|
||||
const increaseFontSize = async () => {
|
||||
await configStore.increaseFontSize();
|
||||
editorStore.applyFontSettings();
|
||||
};
|
||||
|
||||
const decreaseFontSize = async () => {
|
||||
await configStore.decreaseFontSize();
|
||||
editorStore.applyFontSettings();
|
||||
};
|
||||
|
||||
// Tab类型切换
|
||||
@@ -84,15 +92,18 @@ const tabTypeText = computed(() => {
|
||||
// Tab大小增减
|
||||
const increaseTabSize = async () => {
|
||||
await configStore.increaseTabSize();
|
||||
editorStore.applyTabSettings();
|
||||
};
|
||||
|
||||
const decreaseTabSize = async () => {
|
||||
await configStore.decreaseTabSize();
|
||||
editorStore.applyTabSettings();
|
||||
};
|
||||
|
||||
// Tab相关操作
|
||||
const handleToggleTabType = async () => {
|
||||
await configStore.toggleTabType();
|
||||
editorStore.applyTabSettings();
|
||||
};
|
||||
|
||||
// 创建双向绑定的计算属性
|
||||
@@ -100,6 +111,7 @@ const enableTabIndent = computed({
|
||||
get: () => configStore.config.editing.enableTabIndent,
|
||||
set: async (value: boolean) => {
|
||||
await configStore.setEnableTabIndent(value);
|
||||
editorStore.applyTabSettings();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -187,13 +199,13 @@ const handleAutoSaveDelayChange = async (event: Event) => {
|
||||
<button
|
||||
@click="decreaseTabSize"
|
||||
class="control-button"
|
||||
:disabled="!enableTabIndent || configStore.config.editing.tabSize <= configStore.tabSize.min"
|
||||
:disabled="!enableTabIndent || configStore.config.editing.tabSize <= 2"
|
||||
>-</button>
|
||||
<span>{{ configStore.config.editing.tabSize }}</span>
|
||||
<button
|
||||
@click="increaseTabSize"
|
||||
class="control-button"
|
||||
:disabled="!enableTabIndent || configStore.config.editing.tabSize >= configStore.tabSize.max"
|
||||
:disabled="!enableTabIndent || configStore.config.editing.tabSize >= 8"
|
||||
>+</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
@@ -1,66 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from 'vue';
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useExtensionStore} from '@/stores/extensionStore';
|
||||
import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||
import {ExtensionService} from '@/../bindings/voidraft/internal/services';
|
||||
import {
|
||||
getAllExtensionIds,
|
||||
getExtensionDefaultConfig,
|
||||
getExtensionDescription,
|
||||
getExtensionDisplayName,
|
||||
getExtensionDisplayName, getExtensionsMap,
|
||||
hasExtensionConfig
|
||||
} from '@/views/editor/manager/extensions';
|
||||
import {getExtensionManager} from '@/views/editor/manager';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import AccordionContainer from '@/components/accordion/AccordionContainer.vue';
|
||||
import AccordionItem from '@/components/accordion/AccordionItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
|
||||
const {t} = useI18n();
|
||||
const editorStore = useEditorStore();
|
||||
const extensionStore = useExtensionStore();
|
||||
const keybindingStore = useKeybindingStore();
|
||||
|
||||
// 页面初始化时加载扩展数据
|
||||
onMounted(async () => {
|
||||
await extensionStore.loadExtensions();
|
||||
});
|
||||
|
||||
// 展开状态管理
|
||||
const expandedExtensions = ref<Set<string>>(new Set());
|
||||
const expandedExtensions = ref<number[]>([]);
|
||||
|
||||
// 获取所有可用的扩展
|
||||
const availableExtensions = computed(() => {
|
||||
return getAllExtensionIds().map(key => {
|
||||
const extension = extensionStore.extensions.find(ext => ext.key === key);
|
||||
const extensions = getExtensionsMap().map(name => {
|
||||
const extension = extensionStore.extensions.find(ext => ext.name === name);
|
||||
return {
|
||||
id: key,
|
||||
displayName: getExtensionDisplayName(key),
|
||||
description: getExtensionDescription(key),
|
||||
id: extension?.id ?? 0,
|
||||
name: name,
|
||||
displayName: getExtensionDisplayName(name),
|
||||
description: getExtensionDescription(name),
|
||||
enabled: extension?.enabled || false,
|
||||
hasConfig: hasExtensionConfig(key),
|
||||
hasConfig: hasExtensionConfig(name),
|
||||
config: extension?.config || {},
|
||||
defaultConfig: getExtensionDefaultConfig(key)
|
||||
defaultConfig: getExtensionDefaultConfig(name)
|
||||
};
|
||||
});
|
||||
console.log('Available Extensions:', extensions);
|
||||
return extensions;
|
||||
});
|
||||
|
||||
// 切换展开状态
|
||||
const toggleExpanded = (extensionKey: string) => {
|
||||
if (expandedExtensions.value.has(extensionKey)) {
|
||||
expandedExtensions.value.delete(extensionKey);
|
||||
} else {
|
||||
expandedExtensions.value.add(extensionKey);
|
||||
}
|
||||
// 获取扩展图标路径(直接使用扩展名称作为文件名)
|
||||
const getExtensionIcon = (name: string): string => {
|
||||
return `/images/${name}.svg`;
|
||||
};
|
||||
|
||||
// 更新扩展状态
|
||||
const updateExtension = async (extensionKey: string, enabled: boolean) => {
|
||||
const updateExtension = async (extensionId: number, enabled: boolean) => {
|
||||
try {
|
||||
await editorStore.updateExtension(extensionKey, enabled);
|
||||
// 更新后端
|
||||
await ExtensionService.UpdateExtensionEnabled(extensionId, enabled);
|
||||
|
||||
// 重新加载各个 Store 的状态
|
||||
await extensionStore.loadExtensions();
|
||||
await keybindingStore.loadKeyBindings();
|
||||
|
||||
// 获取更新后的扩展
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
|
||||
if (!extension) return;
|
||||
|
||||
// 应用到编辑器
|
||||
const manager = getExtensionManager();
|
||||
if (manager) {
|
||||
manager.updateExtension(extension.name, enabled, extension.config);
|
||||
}
|
||||
|
||||
// 更新快捷键
|
||||
await editorStore.applyKeymapSettings();
|
||||
} catch (error) {
|
||||
console.error('Failed to update extension:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新扩展配置
|
||||
const updateExtensionConfig = async (extensionKey: string, configKey: string, value: any) => {
|
||||
const updateExtensionConfig = async (extensionId: number, configKey: string, value: any) => {
|
||||
try {
|
||||
// 获取当前扩展状态
|
||||
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
|
||||
if (!extension) return;
|
||||
|
||||
// 更新配置
|
||||
@@ -70,28 +95,39 @@ const updateExtensionConfig = async (extensionKey: string, configKey: string, va
|
||||
} else {
|
||||
updatedConfig[configKey] = value;
|
||||
}
|
||||
// 使用editorStore的updateExtension方法更新,确保应用到所有编辑器实例
|
||||
await editorStore.updateExtension(extensionKey, extension.enabled ?? false, updatedConfig);
|
||||
|
||||
// 更新后端配置
|
||||
await ExtensionService.UpdateExtensionConfig(extensionId, updatedConfig);
|
||||
|
||||
// 重新加载状态
|
||||
await extensionStore.loadExtensions();
|
||||
|
||||
// 应用到编辑器
|
||||
const manager = getExtensionManager();
|
||||
if (manager) {
|
||||
manager.updateExtension(extension.name, extension.enabled ?? false, updatedConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update extension config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置扩展到默认配置
|
||||
const resetExtension = async (extensionKey: string) => {
|
||||
const resetExtension = async (extensionId: number) => {
|
||||
try {
|
||||
// 重置到默认配置
|
||||
await ExtensionService.ResetExtensionConfig(extensionKey);
|
||||
await ExtensionService.ResetExtensionConfig(extensionId);
|
||||
|
||||
// 重新加载扩展状态以获取最新配置
|
||||
// 重新加载扩展状态
|
||||
await extensionStore.loadExtensions();
|
||||
|
||||
// 获取重置后的状态,立即应用到所有编辑器视图
|
||||
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
|
||||
// 获取重置后的状态,应用到编辑器
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
|
||||
if (extension) {
|
||||
// 通过editorStore更新,确保所有视图都能同步
|
||||
await editorStore.updateExtension(extensionKey, extension.enabled ?? false, extension.config);
|
||||
const manager = getExtensionManager();
|
||||
if (manager) {
|
||||
manager.updateExtension(extension.name, extension.enabled ?? false, extension.config);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reset extension:', error);
|
||||
@@ -125,7 +161,7 @@ const formatConfigValue = (value: any): string => {
|
||||
|
||||
|
||||
const handleConfigInput = async (
|
||||
extensionKey: string,
|
||||
extensionId: number,
|
||||
configKey: string,
|
||||
defaultValue: any,
|
||||
event: Event
|
||||
@@ -135,15 +171,15 @@ const handleConfigInput = async (
|
||||
const rawValue = target.value;
|
||||
const trimmedValue = rawValue.trim();
|
||||
if (!trimmedValue.length) {
|
||||
await updateExtensionConfig(extensionKey, configKey, undefined);
|
||||
await updateExtensionConfig(extensionId, configKey, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedValue = JSON.parse(trimmedValue);
|
||||
await updateExtensionConfig(extensionKey, configKey, parsedValue);
|
||||
await updateExtensionConfig(extensionId, configKey, parsedValue);
|
||||
} catch (_error) {
|
||||
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
|
||||
const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue);
|
||||
target.value = formatConfigValue(fallbackValue);
|
||||
|
||||
@@ -156,152 +192,227 @@ const handleConfigInput = async (
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<SettingSection :title="t('settings.extensions')">
|
||||
<div
|
||||
v-for="extension in availableExtensions"
|
||||
:key="extension.id"
|
||||
class="extension-item"
|
||||
>
|
||||
<!-- 扩展主项 -->
|
||||
<SettingItem
|
||||
:title="extension.displayName"
|
||||
:description="extension.description"
|
||||
>
|
||||
<div class="extension-controls">
|
||||
<button
|
||||
v-if="extension.hasConfig"
|
||||
class="config-button"
|
||||
@click="toggleExpanded(extension.id)"
|
||||
:class="{ expanded: expandedExtensions.has(extension.id) }"
|
||||
:title="t('settings.extensionsPage.configuration')"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path
|
||||
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-else class="config-placeholder"></div>
|
||||
<ToggleSwitch
|
||||
:model-value="extension.enabled"
|
||||
@update:model-value="updateExtension(extension.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 可展开的配置区域 -->
|
||||
<div
|
||||
v-if="extension.hasConfig && expandedExtensions.has(extension.id)"
|
||||
class="extension-config"
|
||||
>
|
||||
<!-- 配置项标题和重置按钮 -->
|
||||
<div class="config-header">
|
||||
<h4 class="config-title">{{ t('settings.extensionsPage.configuration') }}</h4>
|
||||
<button
|
||||
class="reset-button"
|
||||
@click="resetExtension(extension.id)"
|
||||
:title="t('settings.extensionsPage.resetToDefault')"
|
||||
>
|
||||
{{ t('settings.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="config-table-wrapper">
|
||||
<table class="config-table">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
|
||||
:key="configKey"
|
||||
>
|
||||
<th scope="row" class="config-table-key">
|
||||
{{ configKey }}
|
||||
</th>
|
||||
<td class="config-table-value">
|
||||
<input
|
||||
class="config-value-input"
|
||||
type="text"
|
||||
:value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
|
||||
@change="handleConfigInput(extension.id, configKey, configValue, $event)"
|
||||
@keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空状态提示 -->
|
||||
<div v-if="availableExtensions.length === 0" class="empty-state">
|
||||
<p>{{ t('settings.extensionsPage.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 扩展列表 -->
|
||||
<AccordionContainer v-else v-model="expandedExtensions" :multiple="false">
|
||||
<AccordionItem
|
||||
v-for="extension in availableExtensions"
|
||||
:key="extension.id"
|
||||
:id="extension.id"
|
||||
:class="{ 'extension-disabled': !extension.enabled }"
|
||||
>
|
||||
<!-- 标题插槽:显示图标和扩展名称 -->
|
||||
<template #title>
|
||||
<div class="extension-header">
|
||||
<div class="extension-icon-wrapper">
|
||||
<div class="extension-icon-placeholder" :class="{ 'disabled': !extension.enabled }">
|
||||
<!-- 直接使用扩展名称作为图标文件名 -->
|
||||
<img
|
||||
:src="getExtensionIcon(extension.name)"
|
||||
:alt="extension.displayName"
|
||||
class="extension-icon-img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extension-info">
|
||||
<div class="extension-name">{{ extension.displayName }}</div>
|
||||
<div class="extension-description">{{ extension.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 默认插槽:显示开关和配置项 -->
|
||||
<div class="extension-content">
|
||||
<!-- 启用开关 -->
|
||||
<div class="extension-toggle-section">
|
||||
<label class="toggle-label">{{ t('settings.extensionsPage.enabled') }}</label>
|
||||
<ToggleSwitch
|
||||
:model-value="extension.enabled"
|
||||
@update:model-value="updateExtension(extension.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 配置项 -->
|
||||
<div v-if="extension.hasConfig" class="extension-config-section">
|
||||
<div class="config-header">
|
||||
<h4 class="config-title">{{ t('settings.extensionsPage.configuration') }}</h4>
|
||||
<button
|
||||
class="reset-button"
|
||||
@click="resetExtension(extension.id)"
|
||||
:title="t('settings.extensionsPage.resetToDefault')"
|
||||
>
|
||||
{{ t('settings.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="config-table-wrapper">
|
||||
<table class="config-table">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
|
||||
:key="configKey"
|
||||
>
|
||||
<th scope="row" class="config-table-key">
|
||||
{{ configKey }}
|
||||
</th>
|
||||
<td class="config-table-value">
|
||||
<input
|
||||
class="config-value-input"
|
||||
type="text"
|
||||
:value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
|
||||
@change="handleConfigInput(extension.id, configKey, configValue, $event)"
|
||||
@keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</AccordionContainer>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.extension-item {
|
||||
border-bottom: 1px solid var(--settings-input-border);
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: var(--settings-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
// 禁用状态的扩展项
|
||||
:deep(.extension-disabled) {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
|
||||
.accordion-header {
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-expanded {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
|
||||
.accordion-header {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.extension-description {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.extension-controls {
|
||||
.extension-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 140px;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-button {
|
||||
padding: 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--settings-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
.extension-icon-wrapper {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.extension-icon-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, rgba(var(--settings-accent-rgb, 74, 158, 255), 0.12), rgba(var(--settings-accent-rgb, 74, 158, 255), 0.06));
|
||||
border: 1px solid rgba(var(--settings-accent-rgb, 74, 158, 255), 0.15);
|
||||
color: white;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--settings-hover);
|
||||
color: var(--settings-text);
|
||||
}
|
||||
&.disabled {
|
||||
background: linear-gradient(135deg, rgba(136, 136, 136, 0.08), rgba(136, 136, 136, 0.04));
|
||||
border-color: rgba(136, 136, 136, 0.1);
|
||||
box-shadow: none;
|
||||
|
||||
&.expanded {
|
||||
color: var(--settings-accent);
|
||||
background-color: var(--settings-hover);
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: all 0.2s ease;
|
||||
.extension-icon-img {
|
||||
opacity: 0.4;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-placeholder {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
.extension-icon-img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.extension-config {
|
||||
background-color: var(--settings-input-bg);
|
||||
border-left: 2px solid var(--settings-accent);
|
||||
margin: 4px 0 12px 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 2px;
|
||||
.extension-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.extension-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--settings-text);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.extension-description {
|
||||
font-size: 12px;
|
||||
color: var(--settings-text-secondary);
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.extension-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.extension-toggle-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--settings-input-bg);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--settings-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.extension-config-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
@@ -314,10 +425,10 @@ const handleConfigInput = async (
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
padding: 3px 8px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 2px;
|
||||
border-radius: 3px;
|
||||
background-color: transparent;
|
||||
color: var(--settings-text-secondary);
|
||||
cursor: pointer;
|
||||
@@ -333,7 +444,7 @@ const handleConfigInput = async (
|
||||
|
||||
.config-table-wrapper {
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: var(--settings-panel, var(--settings-input-bg));
|
||||
}
|
||||
@@ -341,7 +452,7 @@ const handleConfigInput = async (
|
||||
.config-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-table tr + tr {
|
||||
@@ -350,7 +461,7 @@ const handleConfigInput = async (
|
||||
|
||||
.config-table th,
|
||||
.config-table td {
|
||||
padding: 5px 8px;
|
||||
padding: 6px 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -362,36 +473,36 @@ const handleConfigInput = async (
|
||||
border-right: 1px solid var(--settings-input-border);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-table-value {
|
||||
padding: 3px 4px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.config-value-input {
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: var(--settings-text);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace;
|
||||
line-height: 1.3;
|
||||
line-height: 1.4;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.config-value-input:hover {
|
||||
border-color: var(--settings-input-border);
|
||||
background-color: var(--settings-hover);
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--settings-input-border);
|
||||
background-color: var(--settings-hover);
|
||||
}
|
||||
|
||||
.config-value-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--settings-accent);
|
||||
background-color: var(--settings-input-bg);
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--settings-accent);
|
||||
background-color: var(--settings-input-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
import {DialogService, HotkeyService, MigrationService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useSystemStore} from "@/stores/systemStore";
|
||||
import {useConfirm, usePolling} from '@/composables';
|
||||
import toast from '@/components/toast';
|
||||
|
||||
const {t} = useI18n();
|
||||
const {
|
||||
@@ -18,6 +19,7 @@ const {
|
||||
setDataPath,
|
||||
setEnableGlobalHotkey,
|
||||
setEnableLoadingAnimation,
|
||||
setEnableMemoryMonitor,
|
||||
setEnableSystemTray,
|
||||
setEnableTabs,
|
||||
setEnableWindowSnap,
|
||||
@@ -29,7 +31,6 @@ const tabStore = useTabStore();
|
||||
|
||||
// 进度条显示控制
|
||||
const showBar = ref(false);
|
||||
const manualError = ref(''); // 用于捕获 MigrateDirectory 抛出的错误
|
||||
let hideTimer = 0;
|
||||
|
||||
// 轮询迁移进度
|
||||
@@ -39,15 +40,20 @@ const {data: progress, error: pollError, isActive: migrating, start, stop, reset
|
||||
interval: 300,
|
||||
shouldStop: ({progress, error}) => !!error || progress >= 100,
|
||||
onStop: () => {
|
||||
const hasError = pollError.value || progress.value?.error;
|
||||
hideTimer = window.setTimeout(hideAll, hasError ? 5000 : 3000);
|
||||
const error = pollError.value || progress.value?.error;
|
||||
if (error) {
|
||||
toast.error(error);
|
||||
} else if ((progress.value?.progress ?? 0) >= 100) {
|
||||
toast.success('Migration successful');
|
||||
}
|
||||
hideTimer = window.setTimeout(hideAll, 3000);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 派生状态
|
||||
const migrationError = computed(() => manualError.value || pollError.value || progress.value?.error || '');
|
||||
const currentProgress = computed(() => progress.value?.progress ?? 0);
|
||||
const migrationError = computed(() => pollError.value || progress.value?.error || '');
|
||||
|
||||
const barClass = computed(() => {
|
||||
if (!showBar.value) return '';
|
||||
@@ -64,8 +70,7 @@ const hideAll = () => {
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = 0;
|
||||
showBar.value = false;
|
||||
manualError.value = '';
|
||||
reset(); // 清除轮询状态
|
||||
reset();
|
||||
};
|
||||
|
||||
// 重置设置确认
|
||||
@@ -125,7 +130,7 @@ const enableTabs = computed({
|
||||
await setEnableTabs(value);
|
||||
if (value) {
|
||||
// 开启tabs功能时,初始化当前文档到标签页
|
||||
tabStore.initializeTab();
|
||||
tabStore.initTab();
|
||||
} else {
|
||||
// 关闭tabs功能时,清空所有标签页
|
||||
tabStore.clearAllTabs();
|
||||
@@ -133,6 +138,12 @@ const enableTabs = computed({
|
||||
}
|
||||
});
|
||||
|
||||
// 计算属性 - 启用内存监视器
|
||||
const enableMemoryMonitor = computed({
|
||||
get: () => general.enableMemoryMonitor,
|
||||
set: (value: boolean) => setEnableMemoryMonitor(value)
|
||||
});
|
||||
|
||||
// 计算属性 - 开机启动
|
||||
const startAtLogin = computed({
|
||||
get: () => general.startAtLogin,
|
||||
@@ -193,10 +204,8 @@ const selectDataDirectory = async () => {
|
||||
|
||||
const [oldPath, newPath] = [currentDataPath.value, selectedPath.trim()];
|
||||
|
||||
// 清除之前的状态并开始轮询
|
||||
hideAll();
|
||||
showBar.value = true;
|
||||
manualError.value = '';
|
||||
start();
|
||||
|
||||
try {
|
||||
@@ -204,10 +213,9 @@ const selectDataDirectory = async () => {
|
||||
await setDataPath(newPath);
|
||||
} catch (e) {
|
||||
stop();
|
||||
// 设置手动捕获的错误(当轮询还没获取到错误时)
|
||||
manualError.value = String(e).replace(/^Error:\s*/i, '') || 'Migration failed';
|
||||
toast.error(String(e).replace(/^Error:\s*/i, '') || 'Migration failed');
|
||||
showBar.value = true;
|
||||
hideTimer = window.setTimeout(hideAll, 5000);
|
||||
hideTimer = window.setTimeout(hideAll, 3000);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -272,6 +280,9 @@ const selectDataDirectory = async () => {
|
||||
<SettingItem :title="t('settings.enableTabs')">
|
||||
<ToggleSwitch v-model="enableTabs"/>
|
||||
</SettingItem>
|
||||
<SettingItem :title="t('settings.enableMemoryMonitor')">
|
||||
<ToggleSwitch v-model="enableMemoryMonitor"/>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection :title="t('settings.startup')">
|
||||
@@ -300,11 +311,6 @@ const selectDataDirectory = async () => {
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-bar" :class="[{'active': showBar}, barClass]" :style="{width: barWidth}"/>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<Transition name="error-fade">
|
||||
<div v-if="migrationError" class="progress-error">{{ migrationError }}</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</SettingSection>
|
||||
@@ -537,13 +543,6 @@ const selectDataDirectory = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-error {
|
||||
font-size: 12px;
|
||||
color: #ef4444;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
@@ -602,35 +601,4 @@ const selectDataDirectory = async () => {
|
||||
box-shadow: 0 0 0 0 rgba(255, 71, 87, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 消息点脉冲动画
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误提示动画
|
||||
.error-fade-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.error-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.error-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.error-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
</style>
|
||||
@@ -1,297 +1,722 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { onMounted, computed } from 'vue';
|
||||
import { onMounted, computed, ref, nextTick } from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import { AccordionContainer, AccordionItem } from '@/components/accordion';
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore';
|
||||
import { useSystemStore } from '@/stores/systemStore';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useEditorStore } from '@/stores/editorStore';
|
||||
import { getCommandDescription } from '@/views/editor/keymap/commands';
|
||||
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { KeyBindingType } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { KeyBindingService } from '@/../bindings/voidraft/internal/services';
|
||||
import { useConfirm } from '@/composables/useConfirm';
|
||||
import toast from '@/components/toast';
|
||||
|
||||
const { t } = useI18n();
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const systemStore = useSystemStore();
|
||||
const configStore = useConfigStore();
|
||||
const editorStore = useEditorStore();
|
||||
|
||||
interface EditingState {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const editingBinding = ref<EditingState | null>(null);
|
||||
const inputKey = ref('');
|
||||
|
||||
// 将快捷键字符串拆分为独立的键
|
||||
const splitKeys = (keyStr: string): string[] => {
|
||||
if (!keyStr) return [];
|
||||
return keyStr.split(/[-+]/).filter(Boolean);
|
||||
};
|
||||
|
||||
// 动态设置 ref 并自动聚焦
|
||||
const setInputRef = (el: any) => {
|
||||
if (el && el instanceof HTMLInputElement) {
|
||||
// 使用 nextTick 确保 DOM 完全渲染后再聚焦
|
||||
nextTick(() => {
|
||||
el.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
onMounted(async () => {
|
||||
await keybindingStore.loadKeyBindings();
|
||||
});
|
||||
|
||||
// 从store中获取快捷键数据并转换为显示格式
|
||||
const keyBindings = computed(() => {
|
||||
return keybindingStore.keyBindings
|
||||
.filter(kb => kb.enabled)
|
||||
.map(kb => ({
|
||||
key: kb.key,
|
||||
command: parseKeyBinding(kb.command || '', kb.key),
|
||||
extension: kb.extension || '',
|
||||
description: kb.key ? (getCommandDescription(kb.key) || kb.key) : ''
|
||||
}));
|
||||
const keymapModeOptions = [
|
||||
{ label: t('keybindings.modes.standard'), value: KeyBindingType.Standard },
|
||||
{ label: t('keybindings.modes.emacs'), value: KeyBindingType.Emacs }
|
||||
];
|
||||
|
||||
const updateKeymapMode = async (mode: KeyBindingType) => {
|
||||
await configStore.setKeymapMode(mode);
|
||||
await keybindingStore.loadKeyBindings();
|
||||
await editorStore.applyKeymapSettings();
|
||||
};
|
||||
|
||||
// 重置快捷键确认
|
||||
const { isConfirming: isResetConfirming, requestConfirm: requestResetConfirm } = useConfirm({
|
||||
timeout: 3000,
|
||||
onConfirm: async () => {
|
||||
await KeyBindingService.ResetKeyBindings();
|
||||
await keybindingStore.loadKeyBindings();
|
||||
await editorStore.applyKeymapSettings();
|
||||
}
|
||||
});
|
||||
|
||||
// 解析快捷键字符串为显示数组
|
||||
const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
|
||||
const keyBindings = computed(() =>
|
||||
keybindingStore.keyBindings.map(kb => ({
|
||||
id: kb.id,
|
||||
name: kb.name,
|
||||
command: getDisplayKeybinding(kb),
|
||||
rawKey: getRawKey(kb),
|
||||
extension: kb.extension || '',
|
||||
description: getCommandDescription(kb.name) || kb.name || '',
|
||||
enabled: kb.enabled,
|
||||
preventDefault: kb.preventDefault,
|
||||
originalData: kb
|
||||
}))
|
||||
);
|
||||
|
||||
const getRawKey = (kb: any): string => {
|
||||
const platformKey = systemStore.isMacOS ? kb.macos
|
||||
: systemStore.isWindows ? kb.windows
|
||||
: systemStore.isLinux ? kb.linux
|
||||
: kb.key;
|
||||
|
||||
return platformKey || kb.key || '';
|
||||
};
|
||||
|
||||
const getDisplayKeybinding = (kb: any): string[] => {
|
||||
const keyStr = getRawKey(kb);
|
||||
return keyStr ? parseKeyString(keyStr) : [];
|
||||
};
|
||||
|
||||
const parseKeyString = (keyStr: string): string[] => {
|
||||
if (!keyStr) return [];
|
||||
|
||||
// 特殊处理重做快捷键的操作系统差异
|
||||
if (keyBindingKey === KeyBindingKey.HistoryRedoKeyBindingKey && keyStr === 'Mod-Shift-z') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', '⇧', 'Z']; // macOS: Cmd+Shift+Z
|
||||
} else {
|
||||
return ['Ctrl', 'Y']; // Windows/Linux: Ctrl+Y
|
||||
}
|
||||
}
|
||||
const symbolMap: Record<string, string> = {
|
||||
'Mod': systemStore.isMacOS ? '⌘' : 'Ctrl',
|
||||
'Cmd': '⌘',
|
||||
...(systemStore.isMacOS ? {
|
||||
'Alt': '⌥',
|
||||
'Shift': '⇧',
|
||||
'Ctrl': '⌃'
|
||||
} : {}),
|
||||
'ArrowUp': '↑',
|
||||
'ArrowDown': '↓',
|
||||
'ArrowLeft': '←',
|
||||
'ArrowRight': '→'
|
||||
};
|
||||
|
||||
// 特殊处理重做选择快捷键的操作系统差异
|
||||
if (keyBindingKey === KeyBindingKey.HistoryRedoSelectionKeyBindingKey && keyStr === 'Mod-Shift-u') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', '⇧', 'U']; // macOS: Cmd+Shift+U
|
||||
} else {
|
||||
return ['Alt', 'U']; // Windows/Linux: Alt+U
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理代码折叠快捷键的操作系统差异
|
||||
if (keyBindingKey === KeyBindingKey.FoldCodeKeyBindingKey && keyStr === 'Ctrl-Shift-[') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', '⌥', '[']; // macOS: Cmd+Alt+[
|
||||
} else {
|
||||
return ['Ctrl', 'Shift', '[']; // Windows/Linux: Ctrl+Shift+[
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.UnfoldCodeKeyBindingKey && keyStr === 'Ctrl-Shift-]') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', '⌥', ']']; // macOS: Cmd+Alt+]
|
||||
} else {
|
||||
return ['Ctrl', 'Shift', ']']; // Windows/Linux: Ctrl+Shift+]
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理编辑快捷键的操作系统差异
|
||||
if (keyBindingKey === KeyBindingKey.CursorSyntaxLeftKeyBindingKey && keyStr === 'Alt-ArrowLeft') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['Ctrl', '←']; // macOS: Ctrl+ArrowLeft
|
||||
} else {
|
||||
return ['Alt', '←']; // Windows/Linux: Alt+ArrowLeft
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.CursorSyntaxRightKeyBindingKey && keyStr === 'Alt-ArrowRight') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['Ctrl', '→']; // macOS: Ctrl+ArrowRight
|
||||
} else {
|
||||
return ['Alt', '→']; // Windows/Linux: Alt+ArrowRight
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.InsertBlankLineKeyBindingKey && keyStr === 'Ctrl-Enter') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', 'Enter']; // macOS: Cmd+Enter
|
||||
} else {
|
||||
return ['Ctrl', 'Enter']; // Windows/Linux: Ctrl+Enter
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.SelectLineKeyBindingKey && keyStr === 'Alt-l') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['Ctrl', 'L']; // macOS: Ctrl+l
|
||||
} else {
|
||||
return ['Alt', 'L']; // Windows/Linux: Alt+l
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.SelectParentSyntaxKeyBindingKey && keyStr === 'Ctrl-i') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', 'I']; // macOS: Cmd+i
|
||||
} else {
|
||||
return ['Ctrl', 'I']; // Windows/Linux: Ctrl+i
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.IndentLessKeyBindingKey && keyStr === 'Ctrl-[') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', '[']; // macOS: Cmd+[
|
||||
} else {
|
||||
return ['Ctrl', '[']; // Windows/Linux: Ctrl+[
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.IndentMoreKeyBindingKey && keyStr === 'Ctrl-]') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', ']']; // macOS: Cmd+]
|
||||
} else {
|
||||
return ['Ctrl', ']']; // Windows/Linux: Ctrl+]
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.IndentSelectionKeyBindingKey && keyStr === 'Ctrl-Alt-\\') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', '⌥', '\\']; // macOS: Cmd+Alt+\
|
||||
} else {
|
||||
return ['Ctrl', 'Alt', '\\']; // Windows/Linux: Ctrl+Alt+\
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.CursorMatchingBracketKeyBindingKey && keyStr === 'Shift-Ctrl-\\') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⇧', '⌘', '\\']; // macOS: Shift+Cmd+\
|
||||
} else {
|
||||
return ['Shift', 'Ctrl', '\\']; // Windows/Linux: Shift+Ctrl+\
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.ToggleCommentKeyBindingKey && keyStr === 'Ctrl-/') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', '/']; // macOS: Cmd+/
|
||||
} else {
|
||||
return ['Ctrl', '/']; // Windows/Linux: Ctrl+/
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理删除快捷键的操作系统差异
|
||||
if (keyBindingKey === KeyBindingKey.DeleteGroupBackwardKeyBindingKey && keyStr === 'Ctrl-Backspace') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', 'Backspace']; // macOS: Cmd+Backspace
|
||||
} else {
|
||||
return ['Ctrl', 'Backspace']; // Windows/Linux: Ctrl+Backspace
|
||||
}
|
||||
}
|
||||
|
||||
if (keyBindingKey === KeyBindingKey.DeleteGroupForwardKeyBindingKey && keyStr === 'Ctrl-Delete') {
|
||||
if (systemStore.isMacOS) {
|
||||
return ['⌘', 'Delete']; // macOS: Cmd+Delete
|
||||
} else {
|
||||
return ['Ctrl', 'Delete']; // Windows/Linux: Ctrl+Delete
|
||||
}
|
||||
}
|
||||
|
||||
// 处理常见的快捷键格式
|
||||
const parts = keyStr.split(/[-+]/);
|
||||
return parts.map(part => {
|
||||
// 根据操作系统将 Mod 替换为相应的键
|
||||
if (part === 'Mod') {
|
||||
if (systemStore.isMacOS) {
|
||||
return '⌘'; // macOS 使用 Command 键符号
|
||||
} else {
|
||||
return 'Ctrl'; // Windows/Linux 使用 Ctrl
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他键名的操作系统差异
|
||||
if (part === 'Alt' && systemStore.isMacOS) {
|
||||
return '⌥'; // macOS 使用 Option 键符号
|
||||
}
|
||||
|
||||
if (part === 'Shift') {
|
||||
return systemStore.isMacOS ? '⇧' : 'Shift'; // macOS 使用符号
|
||||
}
|
||||
|
||||
// 首字母大写
|
||||
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
||||
}).filter(part => part.length > 0);
|
||||
return keyStr
|
||||
.split(/[-+]/)
|
||||
.map(part => symbolMap[part] ?? part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
// 切换启用状态
|
||||
const toggleEnabled = async (binding: any) => {
|
||||
try {
|
||||
await KeyBindingService.UpdateKeyBindingEnabled(binding.id, !binding.enabled);
|
||||
await keybindingStore.loadKeyBindings();
|
||||
await editorStore.applyKeymapSettings();
|
||||
} catch (error) {
|
||||
console.error('Failed to update enabled status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换 PreventDefault
|
||||
const togglePreventDefault = async (binding: any) => {
|
||||
try {
|
||||
await KeyBindingService.UpdateKeyBindingPreventDefault(binding.id, !binding.preventDefault);
|
||||
await keybindingStore.loadKeyBindings();
|
||||
await editorStore.applyKeymapSettings();
|
||||
} catch (error) {
|
||||
console.error('Failed to update preventDefault:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始添加快捷键
|
||||
const startAddKey = (bindingId: number) => {
|
||||
editingBinding.value = {
|
||||
id: bindingId
|
||||
};
|
||||
inputKey.value = '';
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
editingBinding.value = null;
|
||||
inputKey.value = '';
|
||||
};
|
||||
|
||||
// 验证快捷键格式
|
||||
const validateKeyFormat = (key: string): boolean => {
|
||||
if (!key || key.trim() === '') return false;
|
||||
|
||||
// 基本格式验证:允许 Mod/Ctrl/Alt/Shift + 其他键
|
||||
const validPattern = /^(Mod|Ctrl|Alt|Shift|Cmd)(-[A-Za-z0-9\[\]\\/;',.\-=`]|-(ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Enter|Tab|Backspace|Delete|Home|End|PageUp|PageDown|Space|Escape))+$/;
|
||||
const simpleKeyPattern = /^[A-Za-z0-9]$/;
|
||||
const specialKeyPattern = /^(ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Enter|Tab|Backspace|Delete|Home|End|PageUp|PageDown|Space|Escape)$/;
|
||||
|
||||
return validPattern.test(key) || simpleKeyPattern.test(key) || specialKeyPattern.test(key);
|
||||
};
|
||||
|
||||
// 检查快捷键冲突
|
||||
const checkConflict = (newKey: string, currentBindingId: number): { conflict: boolean; conflictWith?: string } => {
|
||||
const conflictBinding = keyBindings.value.find(kb =>
|
||||
kb.rawKey === newKey && kb.id !== currentBindingId
|
||||
);
|
||||
|
||||
if (conflictBinding) {
|
||||
return {
|
||||
conflict: true,
|
||||
conflictWith: conflictBinding.description
|
||||
};
|
||||
}
|
||||
|
||||
return { conflict: false };
|
||||
};
|
||||
|
||||
// 添加新键到快捷键
|
||||
const addKeyPart = async () => {
|
||||
if (!editingBinding.value || !inputKey.value.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPart = inputKey.value.trim();
|
||||
const binding = keyBindings.value.find(kb => kb.id === editingBinding.value!.id);
|
||||
if (!binding) return;
|
||||
|
||||
// 检查键数量限制(最多4个)
|
||||
const currentParts = splitKeys(binding.rawKey);
|
||||
if (currentParts.length >= 4) {
|
||||
toast.error(t('keybindings.maxKeysReached'));
|
||||
inputKey.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取现有的键
|
||||
const currentKey = binding.rawKey;
|
||||
const newKey = currentKey ? `${currentKey}-${newPart}` : newPart;
|
||||
|
||||
// 验证格式
|
||||
if (!validateKeyFormat(newKey)) {
|
||||
toast.error(t('keybindings.invalidFormat'));
|
||||
inputKey.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查冲突
|
||||
const conflictCheck = checkConflict(newKey, editingBinding.value.id);
|
||||
if (conflictCheck.conflict) {
|
||||
toast.error(t('keybindings.conflict', { command: conflictCheck.conflictWith }));
|
||||
inputKey.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await keybindingStore.updateKeyBinding(editingBinding.value.id, newKey);
|
||||
await editorStore.applyKeymapSettings();
|
||||
inputKey.value = '';
|
||||
} catch (error) {
|
||||
console.error('Failed to add key part:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除快捷键的某个部分
|
||||
const removeKeyPart = async (bindingId: number, index: number) => {
|
||||
const binding = keyBindings.value.find(kb => kb.id === bindingId);
|
||||
if (!binding) return;
|
||||
|
||||
const parts = splitKeys(binding.rawKey);
|
||||
parts.splice(index, 1);
|
||||
|
||||
const newKey = parts.join('-');
|
||||
|
||||
try {
|
||||
await keybindingStore.updateKeyBinding(bindingId, newKey);
|
||||
await editorStore.applyKeymapSettings();
|
||||
} catch (error) {
|
||||
console.error('Failed to remove key part:', error);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<SettingSection :title="t('settings.keyBindings')">
|
||||
<div class="key-bindings-container">
|
||||
<div class="key-bindings-header">
|
||||
<div class="keybinding-col">{{ t('keybindings.headers.shortcut') }}</div>
|
||||
<div class="extension-col">{{ t('keybindings.headers.extension') }}</div>
|
||||
<div class="description-col">{{ t('keybindings.headers.description') }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="binding in keyBindings"
|
||||
:key="binding.key"
|
||||
class="key-binding-row"
|
||||
<!-- 快捷键模式设置 -->
|
||||
<SettingSection :title="t('keybindings.keymapMode')">
|
||||
<SettingItem :title="t('keybindings.keymapMode')">
|
||||
<select
|
||||
:value="configStore.config.editing.keymapMode"
|
||||
@change="updateKeymapMode(($event.target as HTMLSelectElement).value as KeyBindingType)"
|
||||
class="select-input"
|
||||
>
|
||||
<div class="keybinding-col">
|
||||
<span
|
||||
v-for="(key, index) in binding.command"
|
||||
:key="index"
|
||||
class="key-badge"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
<option
|
||||
v-for="option in keymapModeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<!-- 快捷键列表 -->
|
||||
<SettingSection :title="t('settings.keyBindings')">
|
||||
<template #title-right>
|
||||
<button
|
||||
:class="['reset-button', isResetConfirming('keybindings') ? 'reset-button-confirming' : '']"
|
||||
@click="requestResetConfirm('keybindings')"
|
||||
>
|
||||
{{ isResetConfirming('keybindings') ? t('keybindings.confirmReset') : t('keybindings.resetToDefault') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<AccordionContainer :multiple="false">
|
||||
<AccordionItem
|
||||
v-for="binding in keyBindings"
|
||||
:key="binding.id"
|
||||
:id="binding.id!"
|
||||
>
|
||||
<!-- 标题插槽 -->
|
||||
<template #title>
|
||||
<div class="binding-title" :class="{ 'disabled': !binding.enabled }">
|
||||
<div class="binding-name">
|
||||
<span class="binding-description">{{ binding.description }}</span>
|
||||
<span class="binding-extension">{{ binding.extension }}</span>
|
||||
</div>
|
||||
<div class="binding-keys">
|
||||
<span
|
||||
v-for="(key, index) in binding.command"
|
||||
:key="index"
|
||||
class="key-badge"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
<span v-if="!binding.command.length" class="key-badge-empty">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 展开内容 -->
|
||||
<div class="binding-config">
|
||||
<!-- Enabled 配置 -->
|
||||
<div class="config-row">
|
||||
<span class="config-label">{{ t('keybindings.config.enabled') }}</span>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="binding.enabled"
|
||||
@change="toggleEnabled(binding)"
|
||||
>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- PreventDefault 配置 -->
|
||||
<div class="config-row">
|
||||
<span class="config-label">{{ t('keybindings.config.preventDefault') }}</span>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="binding.preventDefault"
|
||||
@change="togglePreventDefault(binding)"
|
||||
>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Key 配置 -->
|
||||
<div class="config-row">
|
||||
<span class="config-label">{{ t('keybindings.config.keybinding') }}</span>
|
||||
<div class="key-input-wrapper">
|
||||
<div class="key-tags">
|
||||
<!-- 显示现有快捷键的每个部分 -->
|
||||
<template v-if="binding.rawKey">
|
||||
<span
|
||||
v-for="(keyPart, index) in splitKeys(binding.rawKey)"
|
||||
:key="index"
|
||||
class="key-tag"
|
||||
>
|
||||
<span class="key-tag-text">{{ keyPart }}</span>
|
||||
<button
|
||||
class="key-tag-remove"
|
||||
@click="removeKeyPart(binding.id!, index)"
|
||||
>×</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 添加输入框 -->
|
||||
<template v-if="editingBinding?.id === binding.id">
|
||||
<input
|
||||
:ref="setInputRef"
|
||||
v-model="inputKey"
|
||||
type="text"
|
||||
class="key-input"
|
||||
:placeholder="t('keybindings.keyPlaceholder')"
|
||||
@keydown.enter="addKeyPart"
|
||||
@keydown.escape="cancelEdit"
|
||||
@blur="cancelEdit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<template v-else>
|
||||
<button
|
||||
class="key-tag-add"
|
||||
@click="startAddKey(binding.id!)"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 1V11M1 6H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extension-col">{{ binding.extension }}</div>
|
||||
<div class="description-col">{{ binding.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</AccordionContainer>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
//max-width: 800px;
|
||||
}
|
||||
.select-input {
|
||||
min-width: 140px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--settings-input-bg);
|
||||
color: var(--settings-text);
|
||||
font-size: 12px;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 6px center;
|
||||
background-size: 14px;
|
||||
padding-right: 26px;
|
||||
transition: border-color 0.2s ease;
|
||||
|
||||
.key-bindings-container {
|
||||
padding: 10px 16px;
|
||||
|
||||
.key-bindings-header {
|
||||
display: flex;
|
||||
padding: 0 0 10px 0;
|
||||
border-bottom: 1px solid var(--settings-border);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.key-binding-row {
|
||||
display: flex;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--settings-border);
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
option {
|
||||
background-color: var(--settings-card-bg);
|
||||
color: var(--settings-text);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: var(--settings-button-bg);
|
||||
color: var(--settings-button-text);
|
||||
|
||||
&:hover {
|
||||
border-color: #4a9eff;
|
||||
background-color: var(--settings-button-hover-bg);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&.reset-button-confirming {
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
border-color: #c0392b;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--settings-hover);
|
||||
background-color: #c0392b;
|
||||
}
|
||||
}
|
||||
|
||||
.keybinding-col {
|
||||
width: 150px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 0 10px 0 0;
|
||||
color: var(--settings-text);
|
||||
|
||||
.key-badge {
|
||||
background-color: var(--settings-input-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
color: var(--settings-text);
|
||||
}
|
||||
}
|
||||
|
||||
.extension-col {
|
||||
width: 80px;
|
||||
padding: 0 10px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--settings-text);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.description-col {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--settings-text);
|
||||
}
|
||||
}
|
||||
|
||||
.coming-soon-placeholder {
|
||||
padding: 20px;
|
||||
background-color: var(--settings-card-bg);
|
||||
border-radius: 6px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
.binding-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.binding-name {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.binding-description {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--settings-text);
|
||||
}
|
||||
|
||||
.binding-extension {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.binding-keys {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.key-badge {
|
||||
background-color: var(--settings-input-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
color: var(--settings-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.key-badge-empty {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.binding-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
font-size: 13px;
|
||||
color: var(--settings-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// Switch 开关样式
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + .slider {
|
||||
background-color: #4a9eff;
|
||||
|
||||
&:before {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus + .slider {
|
||||
box-shadow: 0 0 1px #4a9eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--settings-input-border);
|
||||
transition: 0.3s;
|
||||
border-radius: 20px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.key-input-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.key-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.key-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
height: 28px;
|
||||
background-color: var(--settings-input-bg);
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--settings-text);
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
border-color: #4a9eff;
|
||||
|
||||
.key-tag-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key-tag-text {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.key-tag-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #e74c3c;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.key-tag-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border: 1px dashed var(--settings-input-border);
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
border-color: #4a9eff;
|
||||
background-color: var(--settings-input-bg);
|
||||
color: #4a9eff;
|
||||
}
|
||||
}
|
||||
|
||||
.key-input {
|
||||
padding: 4px 8px;
|
||||
height: 28px;
|
||||
border: 1px solid #4a9eff;
|
||||
border-radius: 4px;
|
||||
background-color: var(--settings-input-bg);
|
||||
color: var(--settings-text);
|
||||
font-size: 12px;
|
||||
width: 60px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-mini {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.btn-confirm {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--settings-input-border);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-cancel {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -72,6 +72,72 @@
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
||||
<!-- Toast 通知测试区域 -->
|
||||
<SettingSection title="Toast Notification Test">
|
||||
<SettingItem title="Toast Message">
|
||||
<input
|
||||
v-model="toastMessage"
|
||||
type="text"
|
||||
placeholder="Enter toast message"
|
||||
class="select-input"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="Toast Title (Optional)">
|
||||
<input
|
||||
v-model="toastTitle"
|
||||
type="text"
|
||||
placeholder="Enter toast title"
|
||||
class="select-input"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="Position">
|
||||
<select v-model="toastPosition" class="select-input">
|
||||
<option value="top-right">Top Right</option>
|
||||
<option value="top-left">Top Left</option>
|
||||
<option value="top-center">Top Center</option>
|
||||
<option value="bottom-right">Bottom Right</option>
|
||||
<option value="bottom-left">Bottom Left</option>
|
||||
<option value="bottom-center">Bottom Center</option>
|
||||
</select>
|
||||
</SettingItem>
|
||||
<SettingItem title="Duration (ms)">
|
||||
<input
|
||||
v-model.number="toastDuration"
|
||||
type="number"
|
||||
min="0"
|
||||
step="500"
|
||||
placeholder="4000"
|
||||
class="select-input"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="Toast Types">
|
||||
<div class="button-group">
|
||||
<button @click="showToast('success')" class="test-button toast-success-btn">
|
||||
Success
|
||||
</button>
|
||||
<button @click="showToast('error')" class="test-button toast-error-btn">
|
||||
Error
|
||||
</button>
|
||||
<button @click="showToast('warning')" class="test-button toast-warning-btn">
|
||||
Warning
|
||||
</button>
|
||||
<button @click="showToast('info')" class="test-button toast-info-btn">
|
||||
Info
|
||||
</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="Quick Tests">
|
||||
<div class="button-group">
|
||||
<button @click="showMultipleToasts" class="test-button">
|
||||
Show Multiple Toasts
|
||||
</button>
|
||||
<button @click="clearAllToasts" class="test-button">
|
||||
Clear All Toasts
|
||||
</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<!-- 清除所有测试状态 -->
|
||||
<SettingSection title="Cleanup">
|
||||
<SettingItem title="Clear All">
|
||||
@@ -91,6 +157,8 @@ import { ref } from 'vue';
|
||||
import * as TestService from '@/../bindings/voidraft/internal/services/testservice';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import toast from '@/components/toast';
|
||||
import type { ToastPosition, ToastType } from '@/components/toast/types';
|
||||
|
||||
// Badge测试状态
|
||||
const badgeText = ref('');
|
||||
@@ -102,6 +170,12 @@ const notificationSubtitle = ref('');
|
||||
const notificationBody = ref('');
|
||||
const notificationStatus = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
// Toast 测试状态
|
||||
const toastMessage = ref('This is a test toast notification!');
|
||||
const toastTitle = ref('');
|
||||
const toastPosition = ref<ToastPosition>('top-right');
|
||||
const toastDuration = ref(4000);
|
||||
|
||||
// 清除状态
|
||||
const clearStatus = ref<{ type: string; message: string } | null>(null);
|
||||
|
||||
@@ -172,13 +246,57 @@ const clearAll = async () => {
|
||||
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Toast 相关函数
|
||||
const showToast = (type: ToastType) => {
|
||||
const message = toastMessage.value || `This is a ${type} toast notification!`;
|
||||
const title = toastTitle.value || undefined;
|
||||
|
||||
const options = {
|
||||
position: toastPosition.value,
|
||||
duration: toastDuration.value,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
toast.success(message, title, options);
|
||||
break;
|
||||
case 'error':
|
||||
toast.error(message, title, options);
|
||||
break;
|
||||
case 'warning':
|
||||
toast.warning(message, title, options);
|
||||
break;
|
||||
case 'info':
|
||||
toast.info(message, title, options);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const showMultipleToasts = () => {
|
||||
const positions: ToastPosition[] = ['top-right', 'top-left', 'bottom-right', 'bottom-left'];
|
||||
const types: ToastType[] = ['success', 'error', 'warning', 'info'];
|
||||
|
||||
positions.forEach((position, index) => {
|
||||
setTimeout(() => {
|
||||
const type = types[index % types.length];
|
||||
toast.show({
|
||||
type,
|
||||
message: `Toast from ${position}`,
|
||||
title: `${type.charAt(0).toUpperCase() + type.slice(1)} Toast`,
|
||||
position,
|
||||
duration: 5000,
|
||||
});
|
||||
}, index * 200);
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllToasts = () => {
|
||||
toast.clear();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
//padding: 20px 0 20px 0;
|
||||
}
|
||||
|
||||
.dev-description {
|
||||
color: var(--settings-text-secondary);
|
||||
font-size: 12px;
|
||||
@@ -249,6 +367,50 @@ const clearAll = async () => {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-success-btn {
|
||||
background-color: #16a34a;
|
||||
color: white;
|
||||
border-color: #16a34a;
|
||||
|
||||
&:hover {
|
||||
background-color: #15803d;
|
||||
border-color: #15803d;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-error-btn {
|
||||
background-color: #dc2626;
|
||||
color: white;
|
||||
border-color: #dc2626;
|
||||
|
||||
&:hover {
|
||||
background-color: #b91c1c;
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-warning-btn {
|
||||
background-color: #f59e0b;
|
||||
color: white;
|
||||
border-color: #f59e0b;
|
||||
|
||||
&:hover {
|
||||
background-color: #d97706;
|
||||
border-color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-info-btn {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border-color: #3b82f6;
|
||||
|
||||
&:hover {
|
||||
background-color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.test-status {
|
||||
|
||||
27
go.mod
@@ -4,24 +4,24 @@ go 1.25
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
github.com/creativeprojects/go-selfupdate v1.5.1
|
||||
github.com/creativeprojects/go-selfupdate v1.5.2
|
||||
github.com/go-git/go-git/v5 v5.16.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/knadh/koanf/parsers/json v1.0.0
|
||||
github.com/knadh/koanf/providers/file v1.2.0
|
||||
github.com/knadh/koanf/providers/file v1.2.1
|
||||
github.com/knadh/koanf/providers/structs v1.0.0
|
||||
github.com/knadh/koanf/v2 v2.3.0
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.48
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.55
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0
|
||||
resty.dev/v3 v3.0.0-beta.5
|
||||
resty.dev/v3 v3.0.0-beta.6
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.38.0 // indirect
|
||||
ariga.io/atlas v1.0.0 // indirect
|
||||
code.gitea.io/sdk/gitea v0.22.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
@@ -34,7 +34,7 @@ require (
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.2 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
@@ -48,11 +48,12 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/inflect v0.21.5 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-cmp v0.7.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-github/v74 v74.0.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
@@ -79,11 +80,10 @@ require (
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/zclconf/go-cty v1.17.0 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.2.0 // indirect
|
||||
gitlab.com/gitlab-org/api/client-go v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
@@ -92,7 +92,6 @@ require (
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
83
go.sum
@@ -1,5 +1,5 @@
|
||||
ariga.io/atlas v0.38.0 h1:MwbtwVtDWJFq+ECyeTAz2ArvewDnpeiw/t/sgNdDsdo=
|
||||
ariga.io/atlas v0.38.0/go.mod h1:D7XMK6ei3GvfDqvzk+2VId78j77LdqHrqPOWamn51/s=
|
||||
ariga.io/atlas v1.0.0 h1:v9DQH49xK+SM2kKwk4OQBjfz/KNRMUR+pvDiEIxSJto=
|
||||
ariga.io/atlas v1.0.0/go.mod h1:esBbk3F+pi/mM2PvbCymDm+kWhaOk4PaaiegQdNELk8=
|
||||
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
|
||||
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
@@ -33,10 +33,18 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
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/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.2 h1:3KR3JLrq70oplb9yZzbmJ89qRP78D1AN/9u+l3k0LJ4=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.2/go.mod h1:BCOuwIl1dRRCmPNRPH0amULeZqayhKyY2mH/h4va7Dk=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
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=
|
||||
@@ -68,6 +76,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
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-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA=
|
||||
@@ -76,19 +86,19 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
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/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/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/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=
|
||||
github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
@@ -105,6 +115,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
|
||||
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/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
@@ -113,8 +125,8 @@ github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpb
|
||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/json v1.0.0 h1:1pVR1JhMwbqSg5ICzU+surJmeBbdT4bQm7jjgnA+f8o=
|
||||
github.com/knadh/koanf/parsers/json v1.0.0/go.mod h1:zb5WtibRdpxSoSJfXysqGbVxvbszdlroWDHGdDkkEYU=
|
||||
github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U=
|
||||
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
||||
github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM=
|
||||
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
||||
github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4=
|
||||
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
||||
@@ -139,14 +151,18 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
@@ -174,24 +190,26 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/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/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
|
||||
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/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.48 h1:m22PcankYJI/lKbv7NnNekxsEJYPbvIUnvHvH4WD1xQ=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.48/go.mod h1:yaz8baG0+YzoiN8J6osn0wKiEi0iUux0ZU5NsZFu6OQ=
|
||||
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/wailsapp/wails/v3 v3.0.0-alpha.55 h1:Wxwxc4EN6axDAvH/O5n3uoZQ+XRY/HQZ5rMdn0npq78=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.55/go.mod h1:AyH9vRcseorpL3p5XvxKgK0Lv/agJ7pTmcPdy25xZPo=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
|
||||
github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||
github.com/zclconf/go-cty-yaml v1.2.0 h1:GDyL4+e/Qe/S0B7YaecMLbVvAR/Mp21CXMOSiCTOi1M=
|
||||
github.com/zclconf/go-cty-yaml v1.2.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||
gitlab.com/gitlab-org/api/client-go v1.10.0 h1:VlB9gXQdG6w643lH53VduUHVnCWQG5Ty86VbXnyi70A=
|
||||
gitlab.com/gitlab-org/api/client-go v1.10.0/go.mod h1:U3QKvjbT1J1FrgLsA7w/XlhoBIendUqB4o3/Ht3UhEQ=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
@@ -204,14 +222,11 @@ golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
@@ -242,19 +257,15 @@ 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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
resty.dev/v3 v3.0.0-beta.5 h1:NV1xbqOLzSq7XMTs1t/HLPvu7xrxoXzF90SR4OO6faQ=
|
||||
resty.dev/v3 v3.0.0-beta.5/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
resty.dev/v3 v3.0.0-beta.6 h1:ghRdNpoE8/wBCv+kTKIOauW1aCrSIeTq7GxtfYgtevU=
|
||||
resty.dev/v3 v3.0.0-beta.6/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
|
||||
@@ -39,29 +39,12 @@ const (
|
||||
SystemThemeAuto SystemThemeType = "auto"
|
||||
)
|
||||
|
||||
// UpdateSourceType 更新源类型
|
||||
type UpdateSourceType string
|
||||
|
||||
const (
|
||||
// UpdateSourceGithub GitHub更新源
|
||||
UpdateSourceGithub UpdateSourceType = "github"
|
||||
// UpdateSourceGitea Gitea更新源
|
||||
UpdateSourceGitea UpdateSourceType = "gitea"
|
||||
)
|
||||
|
||||
// GithubConfig GitHub配置
|
||||
type GithubConfig struct {
|
||||
Owner string `json:"owner"` // 仓库所有者
|
||||
Repo string `json:"repo"` // 仓库名称
|
||||
}
|
||||
|
||||
// GiteaConfig Gitea配置
|
||||
type GiteaConfig struct {
|
||||
BaseURL string `json:"baseURL"` // Gitea服务器URL
|
||||
Owner string `json:"owner"` // 仓库所有者
|
||||
Repo string `json:"repo"` // 仓库名称
|
||||
}
|
||||
|
||||
// GeneralConfig 通用设置配置
|
||||
type GeneralConfig struct {
|
||||
AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶
|
||||
@@ -79,6 +62,7 @@ type GeneralConfig struct {
|
||||
// 界面设置
|
||||
EnableLoadingAnimation bool `json:"enableLoadingAnimation"` // 是否启用加载动画
|
||||
EnableTabs bool `json:"enableTabs"` // 是否启用标签页模式
|
||||
EnableMemoryMonitor bool `json:"enableMemoryMonitor"` // 是否启用内存监视器
|
||||
}
|
||||
|
||||
// HotkeyCombo 热键组合定义
|
||||
@@ -103,6 +87,9 @@ type EditingConfig struct {
|
||||
TabSize int `json:"tabSize"` // Tab大小
|
||||
TabType TabType `json:"tabType"` // Tab类型(空格或Tab)
|
||||
|
||||
// 快捷键模式
|
||||
KeymapMode KeyBindingType `json:"keymapMode"` // 快捷键模式(standard 或 emacs)
|
||||
|
||||
// 保存选项
|
||||
AutoSaveDelay int `json:"autoSaveDelay"` // 自动保存延迟(毫秒)
|
||||
}
|
||||
@@ -116,14 +103,11 @@ type AppearanceConfig struct {
|
||||
|
||||
// UpdatesConfig 更新设置配置
|
||||
type UpdatesConfig struct {
|
||||
Version string `json:"version"` // 当前版本号
|
||||
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
|
||||
PrimarySource UpdateSourceType `json:"primarySource"` // 主要更新源
|
||||
BackupSource UpdateSourceType `json:"backupSource"` // 备用更新源
|
||||
BackupBeforeUpdate bool `json:"backupBeforeUpdate"` // 更新前是否备份
|
||||
UpdateTimeout int `json:"updateTimeout"` // 更新超时时间(秒)
|
||||
Github GithubConfig `json:"github"` // GitHub配置
|
||||
Gitea GiteaConfig `json:"gitea"` // Gitea配置
|
||||
Version string `json:"version"` // 当前版本号
|
||||
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
|
||||
BackupBeforeUpdate bool `json:"backupBeforeUpdate"` // 更新前是否备份
|
||||
UpdateTimeout int `json:"updateTimeout"` // 更新超时时间(秒)
|
||||
Github GithubConfig `json:"github"` // GitHub配置
|
||||
}
|
||||
|
||||
// Git备份相关类型定义
|
||||
@@ -185,6 +169,7 @@ func NewDefaultAppConfig() *AppConfig {
|
||||
EnableGlobalHotkey: false,
|
||||
EnableLoadingAnimation: true, // 默认启用加载动画
|
||||
EnableTabs: false, // 默认不启用标签页模式
|
||||
EnableMemoryMonitor: true, // 默认启用内存监视器
|
||||
GlobalHotkey: HotkeyCombo{
|
||||
Ctrl: false,
|
||||
Shift: false,
|
||||
@@ -203,6 +188,8 @@ func NewDefaultAppConfig() *AppConfig {
|
||||
EnableTabIndent: true,
|
||||
TabSize: 4,
|
||||
TabType: TabTypeTab,
|
||||
// 快捷键模式
|
||||
KeymapMode: Standard, // 默认使用标准模式
|
||||
// 保存选项
|
||||
AutoSaveDelay: 2000,
|
||||
},
|
||||
@@ -214,19 +201,12 @@ func NewDefaultAppConfig() *AppConfig {
|
||||
Updates: UpdatesConfig{
|
||||
Version: version.Version,
|
||||
AutoUpdate: true,
|
||||
PrimarySource: UpdateSourceGitea,
|
||||
BackupSource: UpdateSourceGithub,
|
||||
BackupBeforeUpdate: true,
|
||||
UpdateTimeout: 30,
|
||||
UpdateTimeout: 120,
|
||||
Github: GithubConfig{
|
||||
Owner: "landaiqing",
|
||||
Repo: "voidraft",
|
||||
},
|
||||
Gitea: GiteaConfig{
|
||||
BaseURL: "https://git.landaiqing.cn",
|
||||
Owner: "landaiqing",
|
||||
Repo: "voidraft",
|
||||
},
|
||||
},
|
||||
Backup: GitBackupConfig{
|
||||
Enabled: false,
|
||||
|
||||
@@ -17,7 +17,7 @@ type Document struct {
|
||||
// ID of the ent.
|
||||
ID int `json:"id,omitempty"`
|
||||
// UUID for cross-device sync (UUIDv7)
|
||||
UUID *string `json:"uuid"`
|
||||
UUID string `json:"uuid"`
|
||||
// creation time
|
||||
CreatedAt string `json:"created_at"`
|
||||
// update time
|
||||
@@ -69,8 +69,7 @@ func (_m *Document) assignValues(columns []string, values []any) error {
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UUID = new(string)
|
||||
*_m.UUID = value.String
|
||||
_m.UUID = value.String
|
||||
}
|
||||
case document.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
@@ -145,10 +144,8 @@ func (_m *Document) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Document(")
|
||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||
if v := _m.UUID; v != nil {
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(_m.UUID)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(_m.CreatedAt)
|
||||
|
||||
@@ -143,16 +143,6 @@ func UUIDHasSuffix(v string) predicate.Document {
|
||||
return predicate.Document(sql.FieldHasSuffix(FieldUUID, v))
|
||||
}
|
||||
|
||||
// UUIDIsNil applies the IsNil predicate on the "uuid" field.
|
||||
func UUIDIsNil() predicate.Document {
|
||||
return predicate.Document(sql.FieldIsNull(FieldUUID))
|
||||
}
|
||||
|
||||
// UUIDNotNil applies the NotNil predicate on the "uuid" field.
|
||||
func UUIDNotNil() predicate.Document {
|
||||
return predicate.Document(sql.FieldNotNull(FieldUUID))
|
||||
}
|
||||
|
||||
// UUIDEqualFold applies the EqualFold predicate on the "uuid" field.
|
||||
func UUIDEqualFold(v string) predicate.Document {
|
||||
return predicate.Document(sql.FieldEqualFold(FieldUUID, v))
|
||||
|
||||
@@ -180,6 +180,9 @@ func (_c *DocumentCreate) defaults() error {
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (_c *DocumentCreate) check() error {
|
||||
if _, ok := _c.mutation.UUID(); !ok {
|
||||
return &ValidationError{Name: "uuid", err: errors.New(`ent: missing required field "Document.uuid"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.CreatedAt(); !ok {
|
||||
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "Document.created_at"`)}
|
||||
}
|
||||
@@ -225,7 +228,7 @@ func (_c *DocumentCreate) createSpec() (*Document, *sqlgraph.CreateSpec) {
|
||||
)
|
||||
if value, ok := _c.mutation.UUID(); ok {
|
||||
_spec.SetField(document.FieldUUID, field.TypeString, value)
|
||||
_node.UUID = &value
|
||||
_node.UUID = value
|
||||
}
|
||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(document.FieldCreatedAt, field.TypeString, value)
|
||||
|
||||
@@ -170,9 +170,6 @@ func (_u *DocumentUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if _u.mutation.UUIDCleared() {
|
||||
_spec.ClearField(document.FieldUUID, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UpdatedAt(); ok {
|
||||
_spec.SetField(document.FieldUpdatedAt, field.TypeString, value)
|
||||
}
|
||||
@@ -388,9 +385,6 @@ func (_u *DocumentUpdateOne) sqlSave(ctx context.Context) (_node *Document, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if _u.mutation.UUIDCleared() {
|
||||
_spec.ClearField(document.FieldUUID, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UpdatedAt(); ok {
|
||||
_spec.SetField(document.FieldUpdatedAt, field.TypeString, value)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ var schemaGraph = func() *sqlgraph.Schema {
|
||||
extension.FieldCreatedAt: {Type: field.TypeString, Column: extension.FieldCreatedAt},
|
||||
extension.FieldUpdatedAt: {Type: field.TypeString, Column: extension.FieldUpdatedAt},
|
||||
extension.FieldDeletedAt: {Type: field.TypeString, Column: extension.FieldDeletedAt},
|
||||
extension.FieldKey: {Type: field.TypeString, Column: extension.FieldKey},
|
||||
extension.FieldName: {Type: field.TypeString, Column: extension.FieldName},
|
||||
extension.FieldEnabled: {Type: field.TypeBool, Column: extension.FieldEnabled},
|
||||
extension.FieldConfig: {Type: field.TypeJSON, Column: extension.FieldConfig},
|
||||
},
|
||||
@@ -68,14 +68,20 @@ var schemaGraph = func() *sqlgraph.Schema {
|
||||
},
|
||||
Type: "KeyBinding",
|
||||
Fields: map[string]*sqlgraph.FieldSpec{
|
||||
keybinding.FieldUUID: {Type: field.TypeString, Column: keybinding.FieldUUID},
|
||||
keybinding.FieldCreatedAt: {Type: field.TypeString, Column: keybinding.FieldCreatedAt},
|
||||
keybinding.FieldUpdatedAt: {Type: field.TypeString, Column: keybinding.FieldUpdatedAt},
|
||||
keybinding.FieldDeletedAt: {Type: field.TypeString, Column: keybinding.FieldDeletedAt},
|
||||
keybinding.FieldKey: {Type: field.TypeString, Column: keybinding.FieldKey},
|
||||
keybinding.FieldCommand: {Type: field.TypeString, Column: keybinding.FieldCommand},
|
||||
keybinding.FieldExtension: {Type: field.TypeString, Column: keybinding.FieldExtension},
|
||||
keybinding.FieldEnabled: {Type: field.TypeBool, Column: keybinding.FieldEnabled},
|
||||
keybinding.FieldUUID: {Type: field.TypeString, Column: keybinding.FieldUUID},
|
||||
keybinding.FieldCreatedAt: {Type: field.TypeString, Column: keybinding.FieldCreatedAt},
|
||||
keybinding.FieldUpdatedAt: {Type: field.TypeString, Column: keybinding.FieldUpdatedAt},
|
||||
keybinding.FieldDeletedAt: {Type: field.TypeString, Column: keybinding.FieldDeletedAt},
|
||||
keybinding.FieldName: {Type: field.TypeString, Column: keybinding.FieldName},
|
||||
keybinding.FieldType: {Type: field.TypeString, Column: keybinding.FieldType},
|
||||
keybinding.FieldKey: {Type: field.TypeString, Column: keybinding.FieldKey},
|
||||
keybinding.FieldMacos: {Type: field.TypeString, Column: keybinding.FieldMacos},
|
||||
keybinding.FieldWindows: {Type: field.TypeString, Column: keybinding.FieldWindows},
|
||||
keybinding.FieldLinux: {Type: field.TypeString, Column: keybinding.FieldLinux},
|
||||
keybinding.FieldExtension: {Type: field.TypeString, Column: keybinding.FieldExtension},
|
||||
keybinding.FieldEnabled: {Type: field.TypeBool, Column: keybinding.FieldEnabled},
|
||||
keybinding.FieldPreventDefault: {Type: field.TypeBool, Column: keybinding.FieldPreventDefault},
|
||||
keybinding.FieldScope: {Type: field.TypeString, Column: keybinding.FieldScope},
|
||||
},
|
||||
}
|
||||
graph.Nodes[3] = &sqlgraph.Node{
|
||||
@@ -93,7 +99,7 @@ var schemaGraph = func() *sqlgraph.Schema {
|
||||
theme.FieldCreatedAt: {Type: field.TypeString, Column: theme.FieldCreatedAt},
|
||||
theme.FieldUpdatedAt: {Type: field.TypeString, Column: theme.FieldUpdatedAt},
|
||||
theme.FieldDeletedAt: {Type: field.TypeString, Column: theme.FieldDeletedAt},
|
||||
theme.FieldKey: {Type: field.TypeString, Column: theme.FieldKey},
|
||||
theme.FieldName: {Type: field.TypeString, Column: theme.FieldName},
|
||||
theme.FieldType: {Type: field.TypeEnum, Column: theme.FieldType},
|
||||
theme.FieldColors: {Type: field.TypeJSON, Column: theme.FieldColors},
|
||||
},
|
||||
@@ -242,9 +248,9 @@ func (f *ExtensionFilter) WhereDeletedAt(p entql.StringP) {
|
||||
f.Where(p.Field(extension.FieldDeletedAt))
|
||||
}
|
||||
|
||||
// WhereKey applies the entql string predicate on the key field.
|
||||
func (f *ExtensionFilter) WhereKey(p entql.StringP) {
|
||||
f.Where(p.Field(extension.FieldKey))
|
||||
// WhereName applies the entql string predicate on the name field.
|
||||
func (f *ExtensionFilter) WhereName(p entql.StringP) {
|
||||
f.Where(p.Field(extension.FieldName))
|
||||
}
|
||||
|
||||
// WhereEnabled applies the entql bool predicate on the enabled field.
|
||||
@@ -317,14 +323,34 @@ func (f *KeyBindingFilter) WhereDeletedAt(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldDeletedAt))
|
||||
}
|
||||
|
||||
// WhereName applies the entql string predicate on the name field.
|
||||
func (f *KeyBindingFilter) WhereName(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldName))
|
||||
}
|
||||
|
||||
// WhereType applies the entql string predicate on the type field.
|
||||
func (f *KeyBindingFilter) WhereType(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldType))
|
||||
}
|
||||
|
||||
// WhereKey applies the entql string predicate on the key field.
|
||||
func (f *KeyBindingFilter) WhereKey(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldKey))
|
||||
}
|
||||
|
||||
// WhereCommand applies the entql string predicate on the command field.
|
||||
func (f *KeyBindingFilter) WhereCommand(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldCommand))
|
||||
// WhereMacos applies the entql string predicate on the macos field.
|
||||
func (f *KeyBindingFilter) WhereMacos(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldMacos))
|
||||
}
|
||||
|
||||
// WhereWindows applies the entql string predicate on the windows field.
|
||||
func (f *KeyBindingFilter) WhereWindows(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldWindows))
|
||||
}
|
||||
|
||||
// WhereLinux applies the entql string predicate on the linux field.
|
||||
func (f *KeyBindingFilter) WhereLinux(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldLinux))
|
||||
}
|
||||
|
||||
// WhereExtension applies the entql string predicate on the extension field.
|
||||
@@ -337,6 +363,16 @@ func (f *KeyBindingFilter) WhereEnabled(p entql.BoolP) {
|
||||
f.Where(p.Field(keybinding.FieldEnabled))
|
||||
}
|
||||
|
||||
// WherePreventDefault applies the entql bool predicate on the prevent_default field.
|
||||
func (f *KeyBindingFilter) WherePreventDefault(p entql.BoolP) {
|
||||
f.Where(p.Field(keybinding.FieldPreventDefault))
|
||||
}
|
||||
|
||||
// WhereScope applies the entql string predicate on the scope field.
|
||||
func (f *KeyBindingFilter) WhereScope(p entql.StringP) {
|
||||
f.Where(p.Field(keybinding.FieldScope))
|
||||
}
|
||||
|
||||
// addPredicate implements the predicateAdder interface.
|
||||
func (_q *ThemeQuery) addPredicate(pred func(s *sql.Selector)) {
|
||||
_q.predicates = append(_q.predicates, pred)
|
||||
@@ -397,9 +433,9 @@ func (f *ThemeFilter) WhereDeletedAt(p entql.StringP) {
|
||||
f.Where(p.Field(theme.FieldDeletedAt))
|
||||
}
|
||||
|
||||
// WhereKey applies the entql string predicate on the key field.
|
||||
func (f *ThemeFilter) WhereKey(p entql.StringP) {
|
||||
f.Where(p.Field(theme.FieldKey))
|
||||
// WhereName applies the entql string predicate on the name field.
|
||||
func (f *ThemeFilter) WhereName(p entql.StringP) {
|
||||
f.Where(p.Field(theme.FieldName))
|
||||
}
|
||||
|
||||
// WhereType applies the entql string predicate on the type field.
|
||||
|
||||
@@ -18,15 +18,15 @@ type Extension struct {
|
||||
// ID of the ent.
|
||||
ID int `json:"id,omitempty"`
|
||||
// UUID for cross-device sync (UUIDv7)
|
||||
UUID *string `json:"uuid"`
|
||||
UUID string `json:"uuid"`
|
||||
// creation time
|
||||
CreatedAt string `json:"created_at"`
|
||||
// update time
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
// deleted at
|
||||
DeletedAt *string `json:"deleted_at,omitempty"`
|
||||
// extension key
|
||||
Key string `json:"key"`
|
||||
// extension name
|
||||
Name string `json:"name"`
|
||||
// extension enabled or not
|
||||
Enabled bool `json:"enabled"`
|
||||
// extension config
|
||||
@@ -45,7 +45,7 @@ func (*Extension) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new(sql.NullBool)
|
||||
case extension.FieldID:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case extension.FieldUUID, extension.FieldCreatedAt, extension.FieldUpdatedAt, extension.FieldDeletedAt, extension.FieldKey:
|
||||
case extension.FieldUUID, extension.FieldCreatedAt, extension.FieldUpdatedAt, extension.FieldDeletedAt, extension.FieldName:
|
||||
values[i] = new(sql.NullString)
|
||||
default:
|
||||
values[i] = new(sql.UnknownType)
|
||||
@@ -72,8 +72,7 @@ func (_m *Extension) assignValues(columns []string, values []any) error {
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UUID = new(string)
|
||||
*_m.UUID = value.String
|
||||
_m.UUID = value.String
|
||||
}
|
||||
case extension.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
@@ -94,11 +93,11 @@ func (_m *Extension) assignValues(columns []string, values []any) error {
|
||||
_m.DeletedAt = new(string)
|
||||
*_m.DeletedAt = value.String
|
||||
}
|
||||
case extension.FieldKey:
|
||||
case extension.FieldName:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field key", values[i])
|
||||
return fmt.Errorf("unexpected type %T for field name", values[i])
|
||||
} else if value.Valid {
|
||||
_m.Key = value.String
|
||||
_m.Name = value.String
|
||||
}
|
||||
case extension.FieldEnabled:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
@@ -150,10 +149,8 @@ func (_m *Extension) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Extension(")
|
||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||
if v := _m.UUID; v != nil {
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(_m.UUID)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(_m.CreatedAt)
|
||||
@@ -166,8 +163,8 @@ func (_m *Extension) String() string {
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("key=")
|
||||
builder.WriteString(_m.Key)
|
||||
builder.WriteString("name=")
|
||||
builder.WriteString(_m.Name)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("enabled=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Enabled))
|
||||
|
||||
@@ -20,8 +20,8 @@ const (
|
||||
FieldUpdatedAt = "updated_at"
|
||||
// FieldDeletedAt holds the string denoting the deleted_at field in the database.
|
||||
FieldDeletedAt = "deleted_at"
|
||||
// FieldKey holds the string denoting the key field in the database.
|
||||
FieldKey = "key"
|
||||
// FieldName holds the string denoting the name field in the database.
|
||||
FieldName = "name"
|
||||
// FieldEnabled holds the string denoting the enabled field in the database.
|
||||
FieldEnabled = "enabled"
|
||||
// FieldConfig holds the string denoting the config field in the database.
|
||||
@@ -37,7 +37,7 @@ var Columns = []string{
|
||||
FieldCreatedAt,
|
||||
FieldUpdatedAt,
|
||||
FieldDeletedAt,
|
||||
FieldKey,
|
||||
FieldName,
|
||||
FieldEnabled,
|
||||
FieldConfig,
|
||||
}
|
||||
@@ -66,8 +66,8 @@ var (
|
||||
DefaultCreatedAt func() string
|
||||
// DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
|
||||
DefaultUpdatedAt func() string
|
||||
// KeyValidator is a validator for the "key" field. It is called by the builders before save.
|
||||
KeyValidator func(string) error
|
||||
// NameValidator is a validator for the "name" field. It is called by the builders before save.
|
||||
NameValidator func(string) error
|
||||
// DefaultEnabled holds the default value on creation for the "enabled" field.
|
||||
DefaultEnabled bool
|
||||
)
|
||||
@@ -100,9 +100,9 @@ func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldDeletedAt, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByKey orders the results by the key field.
|
||||
func ByKey(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldKey, opts...).ToFunc()
|
||||
// ByName orders the results by the name field.
|
||||
func ByName(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldName, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByEnabled orders the results by the enabled field.
|
||||
|
||||
@@ -73,9 +73,9 @@ func DeletedAt(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldEQ(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// Key applies equality check predicate on the "key" field. It's identical to KeyEQ.
|
||||
func Key(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldEQ(FieldKey, v))
|
||||
// Name applies equality check predicate on the "name" field. It's identical to NameEQ.
|
||||
func Name(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldEQ(FieldName, v))
|
||||
}
|
||||
|
||||
// Enabled applies equality check predicate on the "enabled" field. It's identical to EnabledEQ.
|
||||
@@ -138,16 +138,6 @@ func UUIDHasSuffix(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldHasSuffix(FieldUUID, v))
|
||||
}
|
||||
|
||||
// UUIDIsNil applies the IsNil predicate on the "uuid" field.
|
||||
func UUIDIsNil() predicate.Extension {
|
||||
return predicate.Extension(sql.FieldIsNull(FieldUUID))
|
||||
}
|
||||
|
||||
// UUIDNotNil applies the NotNil predicate on the "uuid" field.
|
||||
func UUIDNotNil() predicate.Extension {
|
||||
return predicate.Extension(sql.FieldNotNull(FieldUUID))
|
||||
}
|
||||
|
||||
// UUIDEqualFold applies the EqualFold predicate on the "uuid" field.
|
||||
func UUIDEqualFold(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldEqualFold(FieldUUID, v))
|
||||
@@ -363,69 +353,69 @@ func DeletedAtContainsFold(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldContainsFold(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// KeyEQ applies the EQ predicate on the "key" field.
|
||||
func KeyEQ(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldEQ(FieldKey, v))
|
||||
// NameEQ applies the EQ predicate on the "name" field.
|
||||
func NameEQ(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldEQ(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyNEQ applies the NEQ predicate on the "key" field.
|
||||
func KeyNEQ(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldNEQ(FieldKey, v))
|
||||
// NameNEQ applies the NEQ predicate on the "name" field.
|
||||
func NameNEQ(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldNEQ(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyIn applies the In predicate on the "key" field.
|
||||
func KeyIn(vs ...string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldIn(FieldKey, vs...))
|
||||
// NameIn applies the In predicate on the "name" field.
|
||||
func NameIn(vs ...string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldIn(FieldName, vs...))
|
||||
}
|
||||
|
||||
// KeyNotIn applies the NotIn predicate on the "key" field.
|
||||
func KeyNotIn(vs ...string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldNotIn(FieldKey, vs...))
|
||||
// NameNotIn applies the NotIn predicate on the "name" field.
|
||||
func NameNotIn(vs ...string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldNotIn(FieldName, vs...))
|
||||
}
|
||||
|
||||
// KeyGT applies the GT predicate on the "key" field.
|
||||
func KeyGT(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldGT(FieldKey, v))
|
||||
// NameGT applies the GT predicate on the "name" field.
|
||||
func NameGT(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldGT(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyGTE applies the GTE predicate on the "key" field.
|
||||
func KeyGTE(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldGTE(FieldKey, v))
|
||||
// NameGTE applies the GTE predicate on the "name" field.
|
||||
func NameGTE(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldGTE(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyLT applies the LT predicate on the "key" field.
|
||||
func KeyLT(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldLT(FieldKey, v))
|
||||
// NameLT applies the LT predicate on the "name" field.
|
||||
func NameLT(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldLT(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyLTE applies the LTE predicate on the "key" field.
|
||||
func KeyLTE(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldLTE(FieldKey, v))
|
||||
// NameLTE applies the LTE predicate on the "name" field.
|
||||
func NameLTE(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldLTE(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyContains applies the Contains predicate on the "key" field.
|
||||
func KeyContains(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldContains(FieldKey, v))
|
||||
// NameContains applies the Contains predicate on the "name" field.
|
||||
func NameContains(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldContains(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyHasPrefix applies the HasPrefix predicate on the "key" field.
|
||||
func KeyHasPrefix(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldHasPrefix(FieldKey, v))
|
||||
// NameHasPrefix applies the HasPrefix predicate on the "name" field.
|
||||
func NameHasPrefix(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldHasPrefix(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyHasSuffix applies the HasSuffix predicate on the "key" field.
|
||||
func KeyHasSuffix(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldHasSuffix(FieldKey, v))
|
||||
// NameHasSuffix applies the HasSuffix predicate on the "name" field.
|
||||
func NameHasSuffix(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldHasSuffix(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyEqualFold applies the EqualFold predicate on the "key" field.
|
||||
func KeyEqualFold(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldEqualFold(FieldKey, v))
|
||||
// NameEqualFold applies the EqualFold predicate on the "name" field.
|
||||
func NameEqualFold(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldEqualFold(FieldName, v))
|
||||
}
|
||||
|
||||
// KeyContainsFold applies the ContainsFold predicate on the "key" field.
|
||||
func KeyContainsFold(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldContainsFold(FieldKey, v))
|
||||
// NameContainsFold applies the ContainsFold predicate on the "name" field.
|
||||
func NameContainsFold(v string) predicate.Extension {
|
||||
return predicate.Extension(sql.FieldContainsFold(FieldName, v))
|
||||
}
|
||||
|
||||
// EnabledEQ applies the EQ predicate on the "enabled" field.
|
||||
|
||||
@@ -75,9 +75,9 @@ func (_c *ExtensionCreate) SetNillableDeletedAt(v *string) *ExtensionCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetKey sets the "key" field.
|
||||
func (_c *ExtensionCreate) SetKey(v string) *ExtensionCreate {
|
||||
_c.mutation.SetKey(v)
|
||||
// SetName sets the "name" field.
|
||||
func (_c *ExtensionCreate) SetName(v string) *ExtensionCreate {
|
||||
_c.mutation.SetName(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -168,18 +168,21 @@ func (_c *ExtensionCreate) defaults() error {
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (_c *ExtensionCreate) check() error {
|
||||
if _, ok := _c.mutation.UUID(); !ok {
|
||||
return &ValidationError{Name: "uuid", err: errors.New(`ent: missing required field "Extension.uuid"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.CreatedAt(); !ok {
|
||||
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "Extension.created_at"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.UpdatedAt(); !ok {
|
||||
return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "Extension.updated_at"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.Key(); !ok {
|
||||
return &ValidationError{Name: "key", err: errors.New(`ent: missing required field "Extension.key"`)}
|
||||
if _, ok := _c.mutation.Name(); !ok {
|
||||
return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "Extension.name"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.Key(); ok {
|
||||
if err := extension.KeyValidator(v); err != nil {
|
||||
return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "Extension.key": %w`, err)}
|
||||
if v, ok := _c.mutation.Name(); ok {
|
||||
if err := extension.NameValidator(v); err != nil {
|
||||
return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Extension.name": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.Enabled(); !ok {
|
||||
@@ -213,7 +216,7 @@ func (_c *ExtensionCreate) createSpec() (*Extension, *sqlgraph.CreateSpec) {
|
||||
)
|
||||
if value, ok := _c.mutation.UUID(); ok {
|
||||
_spec.SetField(extension.FieldUUID, field.TypeString, value)
|
||||
_node.UUID = &value
|
||||
_node.UUID = value
|
||||
}
|
||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(extension.FieldCreatedAt, field.TypeString, value)
|
||||
@@ -227,9 +230,9 @@ func (_c *ExtensionCreate) createSpec() (*Extension, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(extension.FieldDeletedAt, field.TypeString, value)
|
||||
_node.DeletedAt = &value
|
||||
}
|
||||
if value, ok := _c.mutation.Key(); ok {
|
||||
_spec.SetField(extension.FieldKey, field.TypeString, value)
|
||||
_node.Key = value
|
||||
if value, ok := _c.mutation.Name(); ok {
|
||||
_spec.SetField(extension.FieldName, field.TypeString, value)
|
||||
_node.Name = value
|
||||
}
|
||||
if value, ok := _c.mutation.Enabled(); ok {
|
||||
_spec.SetField(extension.FieldEnabled, field.TypeBool, value)
|
||||
|
||||
@@ -62,16 +62,16 @@ func (_u *ExtensionUpdate) ClearDeletedAt() *ExtensionUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetKey sets the "key" field.
|
||||
func (_u *ExtensionUpdate) SetKey(v string) *ExtensionUpdate {
|
||||
_u.mutation.SetKey(v)
|
||||
// SetName sets the "name" field.
|
||||
func (_u *ExtensionUpdate) SetName(v string) *ExtensionUpdate {
|
||||
_u.mutation.SetName(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableKey sets the "key" field if the given value is not nil.
|
||||
func (_u *ExtensionUpdate) SetNillableKey(v *string) *ExtensionUpdate {
|
||||
// SetNillableName sets the "name" field if the given value is not nil.
|
||||
func (_u *ExtensionUpdate) SetNillableName(v *string) *ExtensionUpdate {
|
||||
if v != nil {
|
||||
_u.SetKey(*v)
|
||||
_u.SetName(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
@@ -136,9 +136,9 @@ func (_u *ExtensionUpdate) ExecX(ctx context.Context) {
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (_u *ExtensionUpdate) check() error {
|
||||
if v, ok := _u.mutation.Key(); ok {
|
||||
if err := extension.KeyValidator(v); err != nil {
|
||||
return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "Extension.key": %w`, err)}
|
||||
if v, ok := _u.mutation.Name(); ok {
|
||||
if err := extension.NameValidator(v); err != nil {
|
||||
return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Extension.name": %w`, err)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -162,9 +162,6 @@ func (_u *ExtensionUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if _u.mutation.UUIDCleared() {
|
||||
_spec.ClearField(extension.FieldUUID, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UpdatedAt(); ok {
|
||||
_spec.SetField(extension.FieldUpdatedAt, field.TypeString, value)
|
||||
}
|
||||
@@ -174,8 +171,8 @@ func (_u *ExtensionUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.DeletedAtCleared() {
|
||||
_spec.ClearField(extension.FieldDeletedAt, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Key(); ok {
|
||||
_spec.SetField(extension.FieldKey, field.TypeString, value)
|
||||
if value, ok := _u.mutation.Name(); ok {
|
||||
_spec.SetField(extension.FieldName, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Enabled(); ok {
|
||||
_spec.SetField(extension.FieldEnabled, field.TypeBool, value)
|
||||
@@ -242,16 +239,16 @@ func (_u *ExtensionUpdateOne) ClearDeletedAt() *ExtensionUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetKey sets the "key" field.
|
||||
func (_u *ExtensionUpdateOne) SetKey(v string) *ExtensionUpdateOne {
|
||||
_u.mutation.SetKey(v)
|
||||
// SetName sets the "name" field.
|
||||
func (_u *ExtensionUpdateOne) SetName(v string) *ExtensionUpdateOne {
|
||||
_u.mutation.SetName(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableKey sets the "key" field if the given value is not nil.
|
||||
func (_u *ExtensionUpdateOne) SetNillableKey(v *string) *ExtensionUpdateOne {
|
||||
// SetNillableName sets the "name" field if the given value is not nil.
|
||||
func (_u *ExtensionUpdateOne) SetNillableName(v *string) *ExtensionUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetKey(*v)
|
||||
_u.SetName(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
@@ -329,9 +326,9 @@ func (_u *ExtensionUpdateOne) ExecX(ctx context.Context) {
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (_u *ExtensionUpdateOne) check() error {
|
||||
if v, ok := _u.mutation.Key(); ok {
|
||||
if err := extension.KeyValidator(v); err != nil {
|
||||
return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "Extension.key": %w`, err)}
|
||||
if v, ok := _u.mutation.Name(); ok {
|
||||
if err := extension.NameValidator(v); err != nil {
|
||||
return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Extension.name": %w`, err)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -372,9 +369,6 @@ func (_u *ExtensionUpdateOne) sqlSave(ctx context.Context) (_node *Extension, er
|
||||
}
|
||||
}
|
||||
}
|
||||
if _u.mutation.UUIDCleared() {
|
||||
_spec.ClearField(extension.FieldUUID, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UpdatedAt(); ok {
|
||||
_spec.SetField(extension.FieldUpdatedAt, field.TypeString, value)
|
||||
}
|
||||
@@ -384,8 +378,8 @@ func (_u *ExtensionUpdateOne) sqlSave(ctx context.Context) (_node *Extension, er
|
||||
if _u.mutation.DeletedAtCleared() {
|
||||
_spec.ClearField(extension.FieldDeletedAt, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Key(); ok {
|
||||
_spec.SetField(extension.FieldKey, field.TypeString, value)
|
||||
if value, ok := _u.mutation.Name(); ok {
|
||||
_spec.SetField(extension.FieldName, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Enabled(); ok {
|
||||
_spec.SetField(extension.FieldEnabled, field.TypeBool, value)
|
||||
|
||||