70 Commits

Author SHA1 Message Date
b6c325198d 🎨 Removed support for Gitea and optimized update timeouts 2026-01-03 23:19:43 +08:00
532d30aa93 Added code collapse state persistence 2026-01-03 23:06:08 +08:00
aae86d8b4e Merge branch 'master' into dev 2026-01-03 00:34:23 +08:00
4b1fb765b0 💄 Updated extended management interface style and keybinding management interface style 2026-01-03 00:32:08 +08:00
533f732c53 Added toast notification function and optimized related component styles 2026-01-02 01:27:51 +08:00
009274e4ad 🎨 Optimize code 2026-01-02 00:03:50 +08:00
76f6c30b9d 🎨 Optimize code & Upgrade dependencies 2026-01-01 02:27:21 +08:00
9ec22add55 🐛 Fixed theme invalidation issue 2026-01-01 00:02:34 +08:00
272227e4e3 Merge pull request #20 from landaiqing/dev
Added code block export image extension
2025-12-23 20:53:43 +08:00
ec8f8c1e2d ⬆️ Upgrade dependencies 2025-12-23 20:47:06 +08:00
1c14092068 🐛 Resolved a selection conflict issue during IME input method combination input 2025-12-23 00:37:10 +08:00
00bdafc621 Merge pull request #19 from landaiqing/issue-template
Update issue templates
2025-12-22 19:05:14 +08:00
78422899e4 Update issue templates 2025-12-22 19:02:07 +08:00
c47f7de5b8 Added code block export image extension 2025-12-22 00:13:55 +08:00
37aae9e03c Merge pull request #18 from landaiqing/dev
♻️ Refactor keybinding service
2025-12-20 17:21:13 +08:00
fa134d31d6 📝 Update README.md 2025-12-20 17:05:49 +08:00
d035dcd531 Merge branch 'master' into dev 2025-12-20 17:04:39 +08:00
c50bf452ca Merge pull request #16 from landaiqing/snyk-upgrade-84db4245b76a139f9893542518e26f7c
[Snyk] Upgrade @toml-tools/lexer from 1.0.0 to 1.0.1
2025-12-20 16:46:48 +08:00
dace5ce2b0 Merge branch 'master' into snyk-upgrade-84db4245b76a139f9893542518e26f7c 2025-12-20 16:46:29 +08:00
ef145169aa Merge pull request #17 from landaiqing/snyk-upgrade-d49a610d736beff91ab0d19edbc6eead
[Snyk] Upgrade @toml-tools/parser from 1.0.0 to 1.0.1
2025-12-20 16:44:52 +08:00
7b746155f7 ♻️ Refactor keybinding service 2025-12-20 16:43:04 +08:00
snyk-bot
b289f4054d fix: upgrade @toml-tools/parser from 1.0.0 to 1.0.1
Snyk has created this PR to upgrade @toml-tools/parser from 1.0.0 to 1.0.1.

See this package in npm:
@toml-tools/parser

See this project in Snyk:
https://app.snyk.io/org/landaiqing/project/27ce8f71-d823-4dce-84c2-bf6a1cf5aa6a?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-12-19 11:27:17 +00:00
snyk-bot
bdee1fdf84 fix: upgrade @toml-tools/lexer from 1.0.0 to 1.0.1
Snyk has created this PR to upgrade @toml-tools/lexer from 1.0.0 to 1.0.1.

See this package in npm:
@toml-tools/lexer

See this project in Snyk:
https://app.snyk.io/org/landaiqing/project/27ce8f71-d823-4dce-84c2-bf6a1cf5aa6a?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-12-19 11:27:13 +00:00
541e4e96cf Merge pull request #14 from landaiqing/dev
♻️ Refactor cursor position cache
2025-12-17 23:30:20 +08:00
401eb3ab39 ⬆️ Upgrade dependencies 2025-12-17 23:19:50 +08:00
d3eba96a29 🐛 Fixed assignment issues 2025-12-17 22:55:52 +08:00
81c02db00d Merge pull request #15 from fossabot/add-license-scan-badge
Add license scan report and status
2025-12-17 10:58:59 +08:00
fossabot
9cb2ccbb4e Add license scan report and status
Signed off by: fossabot <badges@fossa.com>
2025-12-16 21:39:50 -05:00
8a10b8fe0f ♻️ Refactor cursor position cache 2025-12-17 00:12:59 +08:00
8fce8bdca4 ♻️ Refactor backup service complete.
Some checks failed
CodeQL Advanced / Analyze (go) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (c-cpp) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
CodeQL Advanced / Analyze (rust) (push) Has been cancelled
2025-12-16 23:20:40 +08:00
1ab934cee9 Merge pull request #13 from landaiqing/alert-autofix-1
Some checks failed
CodeQL Advanced / Analyze (go) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (c-cpp) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
CodeQL Advanced / Analyze (rust) (push) Has been cancelled
Potential fix for code scanning alert no. 1: Workflow does not contain permissions
2025-12-16 15:20:27 +08:00
6659ac6fad 🐛 Potential fix for code scanning alert no. 1: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-16 15:15:36 +08:00
3a5ab1c614 Merge pull request #12 from landaiqing/alert-autofix-2
Potential fix for code scanning alert no. 2: Workflow does not contain permissions
2025-12-16 14:44:06 +08:00
1e07e1f833 🐛 Potential fix for code scanning alert no. 2: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-16 14:36:57 +08:00
e1e91a3683 👷 Change build mode for C/C++ in CodeQL workflow 2025-12-16 14:28:00 +08:00
c30d95a3e0 🐛 Potential fix for code scanning alert no. 3: Replacement of a substring with itself
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-16 14:14:52 +08:00
97f6fa843c 👷 Add CodeQL analysis workflow configuration 2025-12-16 14:01:02 +08:00
f43fc47539 Merge pull request #11 from landaiqing/dependabot/npm_and_yarn/frontend/mdast-util-to-hast-13.2.1
⬆️ Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /frontend
2025-12-16 13:48:54 +08:00
dependabot[bot]
c330de52fa ⬆️ Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /frontend
Bumps [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast) from 13.2.0 to 13.2.1.
- [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1)

---
updated-dependencies:
- dependency-name: mdast-util-to-hast
  dependency-version: 13.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 05:46:59 +00:00
67d35626cb 🚧 Refactor backup service 2025-12-14 23:48:52 +08:00
cc4c2189dc 🚧 Refactor basic services 2025-12-14 02:19:50 +08:00
d16905c0a3 🐛 Fixed some known issues 2025-12-12 22:56:22 +08:00
4e611db349 Optimize minmap extension completed 2025-12-11 23:59:06 +08:00
7e9fc0ac3f Optimize minmap extension 2025-12-10 22:25:19 +08:00
ff072d1a93 Optimize minmap performance 2025-12-10 00:41:24 +08:00
a9c81c878e 🚚 2025-12-08 23:28:36 +08:00
3660d13d7d ♻️ Refactor search 2025-12-08 23:20:37 +08:00
281f53c049 Optimized markdown preview performance 2025-12-07 00:09:52 +08:00
71ca541f78 🚧 Added support for markdown preview table 2025-12-04 00:47:51 +08:00
91f4f4afac Merge branch 'markdown'
# Conflicts:
#	frontend/package-lock.json
2025-12-03 00:46:17 +08:00
fc5639d7bd 🚧 Added support for markdown preview math 2025-12-03 00:45:01 +08:00
dependabot[bot]
6668c11846 ⬆️ Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /frontend
Bumps [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast) from 13.2.0 to 13.2.1.
- [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1)

---
updated-dependencies:
- dependency-name: mdast-util-to-hast
  dependency-version: 13.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 04:14:35 +00:00
17f3351cea 🚧 Added support for markdown preview footnotes 2025-12-02 00:22:22 +08:00
dd3dd4ddb2 🚧 Refactor markdown preview extension 2025-12-01 00:00:05 +08:00
60d1494d45 🚧 Refactor markdown preview extension 2025-11-30 01:09:31 +08:00
1ef5350b3f 🚧 Refactor markdown preview extension 2025-11-29 22:54:38 +08:00
3521e5787b 🚧 Refactor markdown preview extension 2025-11-29 19:24:20 +08:00
8d9bcdad7e 🚧 Refactor markdown preview extension 2025-11-28 00:38:38 +08:00
ac086db1ed ♻️ Updated markdown preview extension 2025-11-26 22:11:16 +08:00
6dff0181d2 ♻️ Refactored markdown preview extension 2025-11-24 00:10:28 +08:00
ad24d3a140 ♻️ Refactored translation extension 2025-11-23 18:45:49 +08:00
4b0f39d747 Merge branch 'master' into dev 2025-11-21 23:37:36 +08:00
096cc1da94 🎨 Optimize hyperlink extension 2025-11-21 23:35:42 +08:00
2d3200ad97 ♻️ Refactor context menu 2025-11-21 22:30:47 +08:00
4e82e2f6f7 ♻️ Refactor the Markdown preview theme application logic 2025-11-21 20:20:06 +08:00
339ed53c2e ♻️ Refactor theme module 2025-11-21 00:03:03 +08:00
fc7c162e2f ♻️ Refactor theme module 2025-11-20 23:07:12 +08:00
dependabot[bot]
24f1549730 ⬆️ Bump golang.org/x/crypto from 0.44.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

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

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

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View 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.

View File

@@ -28,6 +28,7 @@ env:
jobs: jobs:
# 准备构建配置 # 准备构建配置
prepare: prepare:
permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }} matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -84,6 +85,8 @@ jobs:
fi fi
build: build:
permissions:
contents: read
needs: prepare needs: prepare
if: ${{ fromJson(needs.prepare.outputs.matrix).include[0] != null }} if: ${{ fromJson(needs.prepare.outputs.matrix).include[0] != null }}
strategy: strategy:

109
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '29 8 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: c-cpp
build-mode: none
- language: go
build-mode: autobuild
- language: javascript-typescript
build-mode: none
- language: python
build-mode: none
- language: rust
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -61,5 +61,3 @@ export class ServiceOptions {
return new ServiceOptions($$parsedSource as Partial<ServiceOptions>); return new ServiceOptions($$parsedSource as Partial<ServiceOptions>);
} }
} }
export type Window = any;

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,17 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
/**
* CancelFunc 取消订阅函数
* 调用此函数可以取消对配置的监听
*/
export type CancelFunc = any;
/**
* ObserverCallback 观察者回调函数
*/
export type ObserverCallback = any;

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,370 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as theme$0 from "./theme/models.js";
/**
* Document is the model entity for the Document schema.
*/
export class Document {
/**
* ID of the ent.
*/
"id"?: number;
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
/**
* creation time
*/
"created_at": string;
/**
* update time
*/
"updated_at": string;
/**
* deleted at
*/
"deleted_at"?: string | null;
/**
* document title
*/
"title": string;
/**
* document content
*/
"content": string;
/**
* document locked status
*/
"locked": boolean;
/** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("title" in $$source)) {
this["title"] = "";
}
if (!("content" in $$source)) {
this["content"] = "";
}
if (!("locked" in $$source)) {
this["locked"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new Document instance from a string or object.
*/
static createFrom($$source: any = {}): Document {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Document($$parsedSource as Partial<Document>);
}
}
/**
* Extension is the model entity for the Extension schema.
*/
export class Extension {
/**
* ID of the ent.
*/
"id"?: number;
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
/**
* creation time
*/
"created_at": string;
/**
* update time
*/
"updated_at": string;
/**
* deleted at
*/
"deleted_at"?: string | null;
/**
* extension name
*/
"name": string;
/**
* extension enabled or not
*/
"enabled": boolean;
/**
* extension config
*/
"config": { [_: string]: any };
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("config" in $$source)) {
this["config"] = {};
}
Object.assign(this, $$source);
}
/**
* Creates a new Extension instance from a string or object.
*/
static createFrom($$source: any = {}): Extension {
const $$createField7_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField7_0($$parsedSource["config"]);
}
return new Extension($$parsedSource as Partial<Extension>);
}
}
/**
* KeyBinding is the model entity for the KeyBinding schema.
*/
export class KeyBinding {
/**
* ID of the ent.
*/
"id"?: number;
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
/**
* creation time
*/
"created_at": string;
/**
* update time
*/
"updated_at": string;
/**
* deleted at
*/
"deleted_at"?: string | null;
/**
* command identifier
*/
"name": string;
/**
* keybinding type: standard or emacs
*/
"type": string;
/**
* universal keybinding (cross-platform)
*/
"key"?: string;
/**
* 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"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("name" in $$source)) {
this["name"] = "";
}
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);
}
/**
* Creates a new KeyBinding instance from a string or object.
*/
static createFrom($$source: any = {}): KeyBinding {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new KeyBinding($$parsedSource as Partial<KeyBinding>);
}
}
/**
* Theme is the model entity for the Theme schema.
*/
export class Theme {
/**
* ID of the ent.
*/
"id"?: number;
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
/**
* creation time
*/
"created_at": string;
/**
* update time
*/
"updated_at": string;
/**
* deleted at
*/
"deleted_at"?: string | null;
/**
* theme name
*/
"name": string;
/**
* theme type
*/
"type": theme$0.Type;
/**
* theme colors
*/
"colors": { [_: string]: any };
/** Creates a new Theme instance. */
constructor($$source: Partial<Theme> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("type" in $$source)) {
this["type"] = ("" as theme$0.Type);
}
if (!("colors" in $$source)) {
this["colors"] = {};
}
Object.assign(this, $$source);
}
/**
* Creates a new Theme instance from a string or object.
*/
static createFrom($$source: any = {}): Theme {
const $$createField7_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("colors" in $$parsedSource) {
$$parsedSource["colors"] = $$createField7_0($$parsedSource["colors"]);
}
return new Theme($$parsedSource as Partial<Theme>);
}
}
// Private type creation functions
const $$createType0 = $Create.Map($Create.Any, $Create.Any);

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,22 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
/**
* Type defines the type for the "type" enum field.
*/
export enum Type {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Type values.
*/
TypeDark = "dark",
TypeLight = "light",
};

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
/** /**
* BackupService 提供基于Git的备份功能 * BackupService 提供基于Git的备份同步功能
* @module * @module
*/ */
@@ -18,7 +18,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
import * as models$0 from "../models/models.js"; import * as models$0 from "../models/models.js";
/** /**
* HandleConfigChange 处理备份配置变更 * HandleConfigChange 处理配置变更
*/ */
export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } { export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(395287784, config) as any; let $resultPromise = $Call.ByID(395287784, config) as any;
@@ -34,15 +34,7 @@ export function Initialize(): Promise<void> & { cancel(): void } {
} }
/** /**
* PushToRemote 推送本地更改到远程仓库 * Reinitialize 重新初始化
*/
export function PushToRemote(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(262644139) as any;
return $resultPromise;
}
/**
* Reinitialize 重新初始化备份服务,用于响应配置变更
*/ */
export function Reinitialize(): Promise<void> & { cancel(): void } { export function Reinitialize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(301562543) as any; let $resultPromise = $Call.ByID(301562543) as any;
@@ -50,7 +42,7 @@ export function Reinitialize(): Promise<void> & { cancel(): void } {
} }
/** /**
* ServiceShutdown 服务关闭时的清理工作 * ServiceShutdown 服务关闭
*/ */
export function ServiceShutdown(): Promise<void> & { cancel(): void } { export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(422131801) as any; let $resultPromise = $Call.ByID(422131801) as any;
@@ -63,7 +55,7 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
} }
/** /**
* StartAutoBackup 启动自动备份定时器 * StartAutoBackup 启动自动备份
*/ */
export function StartAutoBackup(): Promise<void> & { cancel(): void } { export function StartAutoBackup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3035755449) as any; let $resultPromise = $Call.ByID(3035755449) as any;
@@ -77,3 +69,11 @@ export function StopAutoBackup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2641894021) as any; let $resultPromise = $Call.ByID(2641894021) as any;
return $resultPromise; return $resultPromise;
} }
/**
* Sync 执行完整的同步流程:导出 -> commit -> pull -> 解决冲突 -> push -> 导入
*/
export function Sync(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3420383211) as any;
return $resultPromise;
}

View File

@@ -15,11 +15,10 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as models$0 from "../models/models.js"; import * as helper$0 from "../common/helper/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as $models from "./models.js"; import * as models$0 from "../models/models.js";
/** /**
* Get 获取配置项 * Get 获取配置项
@@ -50,7 +49,7 @@ export function MigrateConfig(): Promise<void> & { cancel(): void } {
} }
/** /**
* ResetConfig 强制重置所有配置为默认值 * ResetConfig 重置所有配置为默认值
*/ */
export function ResetConfig(): Promise<void> & { cancel(): void } { export function ResetConfig(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3593047389) as any; let $resultPromise = $Call.ByID(3593047389) as any;
@@ -66,7 +65,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
} }
/** /**
* ServiceStartup initializes the service when the application starts * ServiceStartup 服务启动时初始化
*/ */
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3311949428, options) as any; let $resultPromise = $Call.ByID(3311949428, options) as any;
@@ -84,7 +83,7 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
/** /**
* Watch 注册配置变更监听器 * Watch 注册配置变更监听器
*/ */
export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } { export function Watch(path: string, callback: helper$0.ObserverCallback): Promise<helper$0.CancelFunc> & { cancel(): void } {
let $resultPromise = $Call.ByID(1143583035, path, callback) as any; let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
return $resultPromise; return $resultPromise;
} }
@@ -92,7 +91,7 @@ export function Watch(path: string, callback: $models.ObserverCallback): Promise
/** /**
* WatchWithContext 使用 Context 注册监听器 * WatchWithContext 使用 Context 注册监听器
*/ */
export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise<void> & { cancel(): void } { export function WatchWithContext(path: string, callback: helper$0.ObserverCallback): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1454973098, path, callback) as any; let $resultPromise = $Call.ByID(1454973098, path, callback) as any;
return $resultPromise; return $resultPromise;
} }

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
/** /**
* DatabaseService provides shared database functionality * DatabaseService 数据库服务
* @module * @module
*/ */
@@ -15,15 +15,7 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/** /**
* RegisterModel 注册模型与表的映射关系 * ServiceShutdown 服务关闭
*/
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
return $resultPromise;
}
/**
* ServiceShutdown shuts down the service when the application closes
*/ */
export function ServiceShutdown(): Promise<void> & { cancel(): void } { export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3907893632) as any; let $resultPromise = $Call.ByID(3907893632) as any;
@@ -31,7 +23,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
} }
/** /**
* ServiceStartup initializes the service when the application starts * ServiceStartup 服务启动
*/ */
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2067840771, options) as any; let $resultPromise = $Call.ByID(2067840771, options) as any;

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
/** /**
* DocumentService provides document management functionality * DocumentService 文档服务
* @module * @module
*/ */
@@ -15,12 +15,12 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as models$0 from "../models/models.js"; import * as ent$0 from "../models/ent/models.js";
/** /**
* CreateDocument creates a new document and returns the created document with ID * CreateDocument 创建文档
*/ */
export function CreateDocument(title: string): Promise<models$0.Document | null> & { cancel(): void } { export function CreateDocument(title: string): Promise<ent$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3360680842, title) as any; let $resultPromise = $Call.ByID(3360680842, title) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType1($result);
@@ -30,7 +30,7 @@ export function CreateDocument(title: string): Promise<models$0.Document | null>
} }
/** /**
* DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted) * DeleteDocument 删除文档
*/ */
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } { export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(412287269, id) as any; let $resultPromise = $Call.ByID(412287269, id) as any;
@@ -38,9 +38,9 @@ export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
} }
/** /**
* GetDocumentByID gets a document by ID * GetDocumentByID 根据ID获取文档
*/ */
export function GetDocumentByID(id: number): Promise<models$0.Document | null> & { cancel(): void } { export function GetDocumentByID(id: number): Promise<ent$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3468193232, id) as any; let $resultPromise = $Call.ByID(3468193232, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType1($result);
@@ -50,9 +50,9 @@ export function GetDocumentByID(id: number): Promise<models$0.Document | null> &
} }
/** /**
* ListAllDocumentsMeta lists all active (non-deleted) document metadata * ListAllDocumentsMeta 列出所有文档
*/ */
export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } { export function ListAllDocumentsMeta(): Promise<(ent$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3073950297) as any; let $resultPromise = $Call.ByID(3073950297) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result); return $$createType2($result);
@@ -62,19 +62,7 @@ export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> &
} }
/** /**
* ListDeletedDocumentsMeta lists all deleted document metadata * LockDocument 锁定文档
*/
export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(490143787) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* LockDocument 锁定文档,防止删除
*/ */
export function LockDocument(id: number): Promise<void> & { cancel(): void } { export function LockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1889494473, id) as any; let $resultPromise = $Call.ByID(1889494473, id) as any;
@@ -82,15 +70,7 @@ export function LockDocument(id: number): Promise<void> & { cancel(): void } {
} }
/** /**
* RestoreDocument restores a deleted document * ServiceStartup 服务启动
*/
export function RestoreDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(784200778, id) as any;
return $resultPromise;
}
/**
* ServiceStartup initializes the service when the application starts
*/ */
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1474135487, options) as any; let $resultPromise = $Call.ByID(1474135487, options) as any;
@@ -106,7 +86,7 @@ export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
} }
/** /**
* UpdateDocumentContent updates the content of a document * UpdateDocumentContent 更新文档内容
*/ */
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } { export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3251897116, id, content) as any; let $resultPromise = $Call.ByID(3251897116, id, content) as any;
@@ -114,7 +94,7 @@ export function UpdateDocumentContent(id: number, content: string): Promise<void
} }
/** /**
* UpdateDocumentTitle updates the title of a document * UpdateDocumentTitle 更新文档标题
*/ */
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } { export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2045530459, id, title) as any; let $resultPromise = $Call.ByID(2045530459, id, title) as any;
@@ -122,6 +102,6 @@ export function UpdateDocumentTitle(id: number, title: string): Promise<void> &
} }
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.Document.createFrom; const $$createType0 = ent$0.Document.createFrom;
const $$createType1 = $Create.Nullable($$createType0); const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1); const $$createType2 = $Create.Array($$createType1);

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
/** /**
* ExtensionService 扩展管理服务 * ExtensionService 扩展服务
* @module * @module
*/ */
@@ -16,12 +16,15 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as models$0 from "../models/models.js"; import * as models$0 from "../models/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as ent$0 from "../models/ent/models.js";
/** /**
* GetAllExtensions 获取所有扩展配置 * GetDefaultExtensions 获取默认扩展配置(用于前端绑定生成)
*/ */
export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel(): void } { export function GetDefaultExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3094292124) as any; let $resultPromise = $Call.ByID(4036328166) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType1($result);
}) as any; }) as any;
@@ -30,23 +33,51 @@ export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel():
} }
/** /**
* ResetAllExtensionsToDefault 重置所有扩展到默认状态 * GetExtensionByID 根据ID获取扩展
*/ */
export function ResetAllExtensionsToDefault(): Promise<void> & { cancel(): void } { export function GetExtensionByID(id: number): Promise<ent$0.Extension | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(270611949) as any; 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(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3990780299, id) as any;
return $resultPromise; return $resultPromise;
} }
/** /**
* ResetExtensionToDefault 重置扩展到默认状态 * ServiceStartup 服务启动
*/
export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(868308101, id) as any;
return $resultPromise;
}
/**
* ServiceStartup 启动时调用
*/ */
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(40324057, options) as any; let $resultPromise = $Call.ByID(40324057, options) as any;
@@ -54,21 +85,33 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
} }
/** /**
* UpdateExtensionEnabled 更新扩展启用状态 * SyncExtensions 同步扩展配置
*/ */
export function UpdateExtensionEnabled(id: models$0.ExtensionID, enabled: boolean): Promise<void> & { cancel(): void } { export function SyncExtensions(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any; let $resultPromise = $Call.ByID(167560004) as any;
return $resultPromise; return $resultPromise;
} }
/** /**
* UpdateExtensionState 更新扩展状态 * UpdateExtensionConfig 更新扩展配置
*/ */
export function UpdateExtensionState(id: models$0.ExtensionID, enabled: boolean, config: models$0.ExtensionConfig): Promise<void> & { cancel(): void } { export function UpdateExtensionConfig(id: number, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2917946454, id, enabled, config) as any; let $resultPromise = $Call.ByID(3184142503, id, config) as any;
return $resultPromise;
}
/**
* UpdateExtensionEnabled 更新扩展启用状态
*/
export function UpdateExtensionEnabled(id: number, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
return $resultPromise; return $resultPromise;
} }
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.Extension.createFrom; const $$createType0 = models$0.Extension.createFrom;
const $$createType1 = $Create.Array($$createType0); 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);

View File

@@ -18,12 +18,12 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
import * as models$0 from "../models/models.js"; import * as models$0 from "../models/models.js";
/** /**
* GetCurrentHotkey 获取当前热键 * GetSupportedKeys 返回系统支持的快捷键列表
*/ */
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } { export function GetSupportedKeys(): Promise<string[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(2572811187) as any; let $resultPromise = $Call.ByID(1511528650) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType0($result);
}) as any; }) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise; return $typingPromise;
@@ -86,5 +86,4 @@ export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null
} }
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.HotkeyCombo.createFrom; const $$createType0 = $Create.Array($Create.Any);
const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -17,7 +17,6 @@ import * as SystemService from "./systemservice.js";
import * as TestService from "./testservice.js"; import * as TestService from "./testservice.js";
import * as ThemeService from "./themeservice.js"; import * as ThemeService from "./themeservice.js";
import * as TranslationService from "./translationservice.js"; import * as TranslationService from "./translationservice.js";
import * as TrayService from "./trayservice.js";
import * as WindowService from "./windowservice.js"; import * as WindowService from "./windowservice.js";
export { export {
BackupService, BackupService,
@@ -36,7 +35,6 @@ export {
TestService, TestService,
ThemeService, ThemeService,
TranslationService, TranslationService,
TrayService,
WindowService WindowService
}; };

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
/** /**
* KeyBindingService 快捷键管理服务 * KeyBindingService 快捷键服务
* @module * @module
*/ */
@@ -16,12 +16,15 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as models$0 from "../models/models.js"; import * as models$0 from "../models/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as ent$0 from "../models/ent/models.js";
/** /**
* GetAllKeyBindings 获取所有快捷键配置 * GetDefaultKeyBindings 获取默认快捷键配置
*/ */
export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } { export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1633502882) as any; let $resultPromise = $Call.ByID(3843471588) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType1($result);
}) as any; }) as any;
@@ -30,13 +33,80 @@ export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel()
} }
/** /**
* ServiceStartup 启动时调用 * GetKeyBindingByID 根据ID获取快捷键
*/
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;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ResetKeyBindings 重置所有快捷键到默认值
*/
export function ResetKeyBindings(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4251626010) as any;
return $resultPromise;
}
/**
* ServiceStartup 服务启动
*/ */
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2057121990, options) as any; let $resultPromise = $Call.ByID(2057121990, options) as any;
return $resultPromise; return $resultPromise;
} }
/**
* SyncKeyBindings 同步快捷键配置
*/
export function SyncKeyBindings(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1522202638) as any;
return $resultPromise;
}
/**
* UpdateKeyBindingEnabled 更新快捷键启用状态
*/
export function UpdateKeyBindingEnabled(id: number, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(843626124, id, enabled) as any;
return $resultPromise;
}
/**
* UpdateKeyBindingKeys 更新快捷键绑定
*/
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 // Private type creation functions
const $$createType0 = models$0.KeyBinding.createFrom; const $$createType0 = models$0.KeyBinding.createFrom;
const $$createType1 = $Create.Array($$createType0); const $$createType1 = $Create.Array($$createType0);
const $$createType2 = ent$0.KeyBinding.createFrom;
const $$createType3 = $Create.Nullable($$createType2);
const $$createType4 = $Create.Array($$createType3);

View File

@@ -12,12 +12,6 @@ import * as http$0 from "../../../net/http/models.js";
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as time$0 from "../../../time/models.js"; import * as time$0 from "../../../time/models.js";
/**
* CancelFunc 取消订阅函数
* 调用此函数可以取消对配置的监听
*/
export type CancelFunc = any;
/** /**
* HttpRequest HTTP请求结构 * HttpRequest HTTP请求结构
*/ */
@@ -191,15 +185,14 @@ export class MemoryStats {
* MigrationProgress 迁移进度信息 * MigrationProgress 迁移进度信息
*/ */
export class MigrationProgress { export class MigrationProgress {
"status": MigrationStatus; /**
* 0-100
*/
"progress": number; "progress": number;
"error"?: string; "error"?: string;
/** Creates a new MigrationProgress instance. */ /** Creates a new MigrationProgress instance. */
constructor($$source: Partial<MigrationProgress> = {}) { constructor($$source: Partial<MigrationProgress> = {}) {
if (!("status" in $$source)) {
this["status"] = ("" as MigrationStatus);
}
if (!("progress" in $$source)) { if (!("progress" in $$source)) {
this["progress"] = 0; this["progress"] = 0;
} }
@@ -216,20 +209,6 @@ export class MigrationProgress {
} }
} }
/**
* MigrationStatus 迁移状态
*/
export enum MigrationStatus {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
MigrationStatusMigrating = "migrating",
MigrationStatusCompleted = "completed",
MigrationStatusFailed = "failed",
};
/** /**
* OSInfo 操作系统信息 * OSInfo 操作系统信息
*/ */
@@ -266,11 +245,6 @@ export class OSInfo {
} }
} }
/**
* ObserverCallback 观察者回调函数
*/
export type ObserverCallback = any;
/** /**
* SelfUpdateResult 自我更新结果 * SelfUpdateResult 自我更新结果
*/ */
@@ -311,7 +285,7 @@ export class SelfUpdateResult {
"error": string; "error": string;
/** /**
* 更新源github/gitea * 更新源github
*/ */
"source": string; "source": string;

View File

@@ -15,26 +15,13 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as models$0 from "../models/models.js"; import * as ent$0 from "../models/ent/models.js";
/** /**
* GetAllThemes 获取所有主题 * GetThemeByName 根据Key获取主题
*/ */
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } { export function GetThemeByName(name: string): Promise<ent$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2425053076) as any; let $resultPromise = $Call.ByID(1938954770, name) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetThemeByID 根据ID或名称获取主题
* 如果 id > 0按ID查询如果 id = 0按名称查询
*/
export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(127385338, id, name) as any;
let $typingPromise = $resultPromise.then(($result: any) => { let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result); return $$createType1($result);
}) as any; }) as any;
@@ -43,23 +30,15 @@ export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<model
} }
/** /**
* ResetTheme 重置主题为预设配置 * ResetTheme 删除主题
*/ */
export function ResetTheme(id: number, ...name: string[]): Promise<void> & { cancel(): void } { export function ResetTheme(key: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1806334457, id, name) as any; let $resultPromise = $Call.ByID(1806334457, key) as any;
return $resultPromise; return $resultPromise;
} }
/** /**
* ServiceShutdown 服务关闭 * ServiceStartup 服务启动
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1676749034) as any;
return $resultPromise;
}
/**
* ServiceStartup 服务启动时初始化
*/ */
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } { export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2915959937, options) as any; let $resultPromise = $Call.ByID(2915959937, options) as any;
@@ -67,14 +46,13 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
} }
/** /**
* UpdateTheme 更新主题 * UpdateTheme 保存或更新主题
*/ */
export function UpdateTheme(id: number, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } { export function UpdateTheme(key: string, colors: { [_: string]: any }): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(70189749, id, colors) as any; let $resultPromise = $Call.ByID(70189749, key, colors) as any;
return $resultPromise; return $resultPromise;
} }
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.Theme.createFrom; const $$createType0 = ent$0.Theme.createFrom;
const $$createType1 = $Create.Nullable($$createType0); const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);

View File

@@ -1,59 +0,0 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* TrayService 系统托盘服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* AutoShowHide 自动显示/隐藏主窗口
*/
export function AutoShowHide(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4044219428) as any;
return $resultPromise;
}
/**
* HandleWindowClose 处理窗口关闭事件
*/
export function HandleWindowClose(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1824247204) as any;
return $resultPromise;
}
/**
* HandleWindowMinimize 处理窗口最小化事件
*/
export function HandleWindowMinimize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(178686624) as any;
return $resultPromise;
}
/**
* MinimizeButtonClicked 处理标题栏最小化按钮点击
*/
export function MinimizeButtonClicked(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2477618539) as any;
return $resultPromise;
}
/**
* ShouldMinimizeToTray 检查是否应该最小化到托盘
*/
export function ShouldMinimizeToTray(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(3403884012) as any;
return $resultPromise;
}
/**
* ShowWindow 显示主窗口
*/
export function ShowWindow(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1315913255) as any;
return $resultPromise;
}

View File

@@ -14,18 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* GetOpenWindows 获取所有打开的文档窗口
*/
export function GetOpenWindows(): Promise<application$0.Window[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1464997251) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/** /**
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开 * IsDocumentWindowOpen 检查指定文档的窗口是否已打开
*/ */
@@ -57,6 +45,3 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
let $resultPromise = $Call.ByID(2432987694, options) as any; let $resultPromise = $Call.ByID(2432987694, options) as any;
return $resultPromise; return $resultPromise;
} }
// Private type creation functions
const $$createType0 = $Create.Array($Create.Any);

View File

@@ -11,6 +11,8 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { 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'] BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default'] DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.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'] TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default']
TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default'] TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default']
TabItem: typeof import('./src/components/tabs/TabItem.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'] Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default'] WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default'] WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@
"app:generate": "cd .. && wails3 generate bindings -ts" "app:generate": "cd .. && wails3 generate bindings -ts"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.19.1", "@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.0", "@codemirror/commands": "^6.10.1",
"@codemirror/lang-angular": "^0.1.4", "@codemirror/lang-angular": "^0.1.4",
"@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
@@ -34,7 +34,7 @@
"@codemirror/lang-json": "^6.0.2", "@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-less": "^6.0.2", "@codemirror/lang-less": "^6.0.2",
"@codemirror/lang-lezer": "^6.0.2", "@codemirror/lang-lezer": "^6.0.2",
"@codemirror/lang-liquid": "^6.3.0", "@codemirror/lang-liquid": "^6.3.1",
"@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-php": "^6.0.2", "@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1", "@codemirror/lang-python": "^6.2.1",
@@ -44,65 +44,62 @@
"@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-wast": "^6.0.2",
"@codemirror/lang-yaml": "^6.1.2", "@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.3", "@codemirror/language": "^6.12.1",
"@codemirror/language-data": "^6.5.2", "@codemirror/language-data": "^6.5.2",
"@codemirror/legacy-modes": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.2", "@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.5.11", "@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2", "@codemirror/state": "^6.5.3",
"@codemirror/view": "^6.38.6", "@codemirror/view": "^6.39.8",
"@cospaia/prettier-plugin-clojure": "^0.0.2", "@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.3", "@lezer/lr": "^1.4.5",
"@mdit/plugin-katex": "^0.23.2",
"@mdit/plugin-tasklist": "^0.22.2",
"@prettier/plugin-xml": "^3.4.2", "@prettier/plugin-xml": "^3.4.2",
"@replit/codemirror-lang-svelte": "^6.0.0", "@replit/codemirror-lang-svelte": "^6.0.0",
"@toml-tools/lexer": "^1.0.0", "@toml-tools/lexer": "^1.0.1",
"@toml-tools/parser": "^1.0.0", "@toml-tools/parser": "^1.0.1",
"@types/markdown-it": "^14.1.2", "@types/katex": "^0.16.7",
"@zumer/snapdom": "^2.0.1",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"codemirror-lang-elixir": "^4.0.0", "codemirror-lang-elixir": "^4.0.0",
"colors-named": "^1.0.2", "colors-named": "^1.0.4",
"colors-named-hex": "^1.0.2", "colors-named-hex": "^1.0.3",
"groovy-beautify": "^0.0.17", "groovy-beautify": "^0.0.17",
"highlight.js": "^11.11.1",
"hsl-matcher": "^1.2.4", "hsl-matcher": "^1.2.4",
"java-parser": "^3.0.1", "java-parser": "^3.0.1",
"linguist-languages": "^9.1.0", "katex": "^0.16.27",
"markdown-it": "^14.1.0", "linguist-languages": "^9.1.11",
"mermaid": "^11.12.1", "marked": "^17.0.1",
"npm": "^11.6.2", "mermaid": "^11.12.2",
"php-parser": "^3.2.5", "php-parser": "^3.2.5",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"prettier": "^3.6.2", "prettier": "^3.7.4",
"sass": "^1.94.0", "sass": "^1.97.1",
"vue": "^3.5.24", "vue": "^3.5.26",
"vue-i18n": "^11.1.12", "vue-i18n": "^11.2.8",
"vue-pick-colors": "^1.8.0", "vue-pick-colors": "^1.8.0",
"vue-router": "^4.6.3" "vue-router": "^4.6.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.2",
"@lezer/generator": "^1.8.0", "@lezer/generator": "^1.8.0",
"@types/node": "^24.9.2", "@types/node": "^25.0.3",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.3",
"@wailsio/runtime": "latest", "@wailsio/runtime": "^3.0.0-alpha.77",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9.39.1", "eslint": "^9.39.2",
"eslint-plugin-vue": "^10.5.1", "eslint-plugin-vue": "^10.6.2",
"globals": "^16.5.0", "globals": "^16.5.0",
"happy-dom": "^20.0.10",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.46.4", "typescript-eslint": "^8.51.0",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^30.0.0",
"vite": "npm:rolldown-vite@latest", "vite": "npm:rolldown-vite@latest",
"vite-plugin-node-polyfills": "^0.24.0", "vite-plugin-node-polyfills": "^0.24.0",
"vitepress": "^2.0.0-alpha.12", "vitepress": "^2.0.0-alpha.12",
"vitest": "^4.0.8", "vitest": "^4.0.16",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.3" "vue-tsc": "^3.2.1"
}, },
"overrides": { "overrides": {
"vite": "npm:rolldown-vite@latest" "vite": "npm:rolldown-vite@latest"

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -6,7 +6,10 @@ import {useKeybindingStore} from '@/stores/keybindingStore';
import {useThemeStore} from '@/stores/themeStore'; import {useThemeStore} from '@/stores/themeStore';
import {useUpdateStore} from '@/stores/updateStore'; import {useUpdateStore} from '@/stores/updateStore';
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue'; import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
import ToastContainer from '@/components/toast/ToastContainer.vue';
import {useTranslationStore} from "@/stores/translationStore"; import {useTranslationStore} from "@/stores/translationStore";
import {useI18n} from "vue-i18n";
import {LanguageType} from "../bindings/voidraft/internal/models";
const configStore = useConfigStore(); const configStore = useConfigStore();
const systemStore = useSystemStore(); const systemStore = useSystemStore();
@@ -14,18 +17,18 @@ const keybindingStore = useKeybindingStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
const translationStore = useTranslationStore(); const translationStore = useTranslationStore();
const {locale} = useI18n();
onBeforeMount(async () => { onBeforeMount(async () => {
// 并行初始化配置、系统信息和快捷键配置 // 并行初始化配置、系统信息和快捷键配置
await Promise.all([ await Promise.all([
configStore.initConfig(), configStore.initConfig(),
systemStore.initializeSystemInfo(), systemStore.initSystemInfo(),
keybindingStore.loadKeyBindings(), keybindingStore.loadKeyBindings(),
]); ]);
// 初始化语言和主题 locale.value = configStore.config.appearance.language || LanguageType.LangEnUS;
await configStore.initializeLanguage(); await themeStore.initTheme();
await themeStore.initializeTheme();
await translationStore.loadTranslators(); await translationStore.loadTranslators();
// 启动时检查更新 // 启动时检查更新
@@ -39,6 +42,7 @@ onBeforeMount(async () => {
<div class="app-content"> <div class="app-content">
<router-view/> <router-view/>
</div> </div>
<ToastContainer/>
</div> </div>
</template> </template>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

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

View File

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

View File

@@ -1,255 +1,266 @@
:root { :root {
/* 编辑器区域 */ --voidraft-font-mono: "HarmonyOS", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
--text-primary: #9BB586; /* 内容区域字体颜色 */
/* 深色主题颜色变量 */
--dark-toolbar-bg: #2d2d2d;
--dark-toolbar-border: #404040;
--dark-toolbar-text: #ffffff;
--dark-toolbar-text-secondary: #cccccc;
--dark-toolbar-button-hover: #404040;
--dark-tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
--dark-bg-secondary: #0E1217;
--dark-text-secondary: #a0aec0;
--dark-text-muted: #666;
--dark-border-color: #2d3748;
--dark-settings-bg: #2a2a2a;
--dark-settings-card-bg: #333333;
--dark-settings-text: #ffffff;
--dark-settings-text-secondary: #cccccc;
--dark-settings-border: #444444;
--dark-settings-input-bg: #3a3a3a;
--dark-settings-input-border: #555555;
--dark-settings-hover: #404040;
--dark-scrollbar-track: #2a2a2a;
--dark-scrollbar-thumb: #555555;
--dark-scrollbar-thumb-hover: #666666;
--dark-selection-bg: rgba(181, 206, 168, 0.1);
--dark-selection-text: #b5cea8;
--dark-danger-color: #ff6b6b;
--dark-bg-primary: #1a1a1a;
--dark-bg-hover: #2a2a2a;
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
--dark-loading-color: #fff;
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--dark-loading-done-color: #6f6;
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
/* 浅色主题颜色变量 */
--light-toolbar-bg: #f8f9fa;
--light-toolbar-border: #e9ecef;
--light-toolbar-text: #212529;
--light-toolbar-text-secondary: #495057;
--light-toolbar-button-hover: #e9ecef;
--light-tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--light-bg-secondary: #f7fef7;
--light-text-secondary: #374151;
--light-text-muted: #6b7280;
--light-border-color: #e5e7eb;
--light-settings-bg: #ffffff;
--light-settings-card-bg: #f8f9fa;
--light-settings-text: #212529;
--light-settings-text-secondary: #6c757d;
--light-settings-border: #dee2e6;
--light-settings-input-bg: #ffffff;
--light-settings-input-border: #ced4da;
--light-settings-hover: #e9ecef;
--light-scrollbar-track: #f1f3f4;
--light-scrollbar-thumb: #c1c1c1;
--light-scrollbar-thumb-hover: #a8a8a8;
--light-selection-bg: rgba(59, 130, 246, 0.15);
--light-selection-text: #2563eb;
--light-danger-color: #dc3545;
--light-bg-primary: #ffffff;
--light-bg-hover: #f1f3f4;
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--light-loading-color: #1a3c1a;
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--light-loading-done-color: #008800;
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* 默认使用深色主题 */
--toolbar-bg: var(--dark-toolbar-bg);
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover);
--tab-active-line: var(--dark-tab-active-line);
--bg-secondary: var(--dark-bg-secondary);
--text-secondary: var(--dark-text-secondary);
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg);
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary);
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border);
--settings-hover: var(--dark-settings-hover);
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
color-scheme: light dark;
} }
/* 监听系统深色主题 */ /* 默认/暗色主题 */
@media (prefers-color-scheme: dark) { :root,
:root[data-theme="auto"] { :root[data-theme="dark"],
--toolbar-bg: var(--dark-toolbar-bg); :root[data-theme="auto"] {
--toolbar-border: var(--dark-toolbar-border); color-scheme: dark;
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary); --text-primary: #ffffff;
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover); --toolbar-bg: #2d2d2d;
--tab-active-line: var(--dark-tab-active-line); --toolbar-border: #404040;
--bg-secondary: var(--dark-bg-secondary); --toolbar-text: #ffffff;
--text-secondary: var(--dark-text-secondary); --toolbar-text-secondary: #cccccc;
--text-muted: var(--dark-text-muted); --toolbar-button-hover: #404040;
--border-color: var(--dark-border-color); --toolbar-separator: #404040;
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg); --tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
--settings-text: var(--dark-settings-text); --bg-secondary: #0e1217;
--settings-text-secondary: var(--dark-settings-text-secondary); --bg-primary: #1a1a1a;
--settings-border: var(--dark-settings-border); --bg-hover: #2a2a2a;
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border); --text-secondary: #a0aec0;
--settings-hover: var(--dark-settings-hover); --text-muted: #666666;
--scrollbar-track: var(--dark-scrollbar-track); --text-danger: #ff6b6b;
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover); --border-color: #2d3748;
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text); --settings-bg: #2a2a2a;
--text-danger: var(--dark-danger-color); --settings-card-bg: #333333;
--bg-primary: var(--dark-bg-primary); --settings-text: #ffffff;
--bg-hover: var(--dark-bg-hover); --settings-text-secondary: #cccccc;
--voidraft-bg-gradient: var(--dark-loading-bg-gradient); --settings-border: #444444;
--voidraft-loading-color: var(--dark-loading-color); --settings-input-bg: #3a3a3a;
--voidraft-loading-glow: var(--dark-loading-glow); --settings-input-border: #555555;
--voidraft-loading-done-color: var(--dark-loading-done-color); --settings-hover: #404040;
--voidraft-loading-overlay: var(--dark-loading-overlay);
} --scrollbar-track: #2a2a2a;
--scrollbar-thumb: #555555;
--scrollbar-thumb-hover: #666666;
--selection-bg: rgba(181, 206, 168, 0.1);
--selection-text: #b5cea8;
--voidraft-bg-gradient: radial-gradient(#222922, #000500);
--voidraft-loading-color: #ffffff;
--voidraft-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--voidraft-loading-done-color: #66ff66;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
/* Markdown 代码块样式 - 暗色主题 */
--cm-codeblock-bg: rgba(46, 51, 69, 0.8);
--cm-codeblock-radius: 0.4rem;
/* Markdown 内联代码样式 */
--cm-inline-code-bg: oklch(28% 0.02 255);
/* Markdown 上标/下标样式 */
--cm-superscript-color: inherit;
--cm-subscript-color: inherit;
/* Markdown 高亮样式 */
--cm-highlight-background: rgba(250, 204, 21, 0.35);
/* Markdown 表格样式 - 暗色主题 */
--cm-table-bg: rgba(35, 40, 52, 0.5);
--cm-table-header-bg: rgba(46, 51, 69, 0.7);
--cm-table-border: rgba(75, 85, 99, 0.35);
--cm-table-row-hover: rgba(55, 62, 78, 0.5);
/* Search Panel - Dark Theme */
--search-panel-bg: #252526;
--search-panel-text: #cccccc;
--search-panel-border: #454545;
--search-input-bg: #3c3c3c;
--search-input-text: #cccccc;
--search-input-border: #3c3c3c;
--search-focus-border: #0078d4;
--search-btn-hover: rgba(255, 255, 255, 0.1);
--search-btn-active-bg: rgba(0, 120, 212, 0.4);
--search-btn-active-text: #ffffff;
--search-error-border: #f14c4c;
--search-error-bg: #5a1d1d;
/* Search Match Highlight - Dark Theme (VSCode style) */
--search-match-bg: rgba(250, 220, 81, 0.85);
--search-match-selected-bg: rgba(81, 175, 255, 0.5);
--search-match-selected-border: #74b0f4;
} }
/* 监听系统浅色主题 */ /* 色主题 */
:root[data-theme="light"] {
color-scheme: light;
--text-primary: #000000;
--toolbar-bg: #f8f9fa;
--toolbar-border: #e9ecef;
--toolbar-text: #212529;
--toolbar-text-secondary: #495057;
--toolbar-button-hover: #e9ecef;
--toolbar-separator: #e9ecef;
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--bg-secondary: #f7fef7;
--bg-primary: #ffffff;
--bg-hover: #f1f3f4;
--text-secondary: #374151;
--text-muted: #6b7280;
--text-danger: #dc3545;
--border-color: #e5e7eb;
--settings-bg: #ffffff;
--settings-card-bg: #f8f9fa;
--settings-text: #212529;
--settings-text-secondary: #6c757d;
--settings-border: #dee2e6;
--settings-input-bg: #ffffff;
--settings-input-border: #ced4da;
--settings-hover: #e9ecef;
--scrollbar-track: #f1f3f4;
--scrollbar-thumb: #c1c1c1;
--scrollbar-thumb-hover: #a8a8a8;
--selection-bg: rgba(59, 130, 246, 0.15);
--selection-text: #2563eb;
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--voidraft-loading-color: #1a3c1a;
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--voidraft-loading-done-color: #008800;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* Markdown 代码块样式 - 亮色主题 */
--cm-codeblock-bg: #f3f3f3;
--cm-codeblock-radius: 0.4rem;
/* Markdown 内联代码样式 */
--cm-inline-code-bg: oklch(92.9% 0.013 255.508);
/* Markdown 上标/下标样式 */
--cm-superscript-color: inherit;
--cm-subscript-color: inherit;
/* Markdown 高亮样式 */
--cm-highlight-background: rgba(253, 224, 71, 0.45);
/* Markdown 表格样式 - 亮色主题 */
--cm-table-bg: oklch(97.5% 0.006 255);
--cm-table-header-bg: oklch(94% 0.01 255);
--cm-table-border: oklch(88% 0.008 255);
--cm-table-row-hover: oklch(95% 0.008 255);
/* Search Panel - Light Theme */
--search-panel-bg: #f3f3f3;
--search-panel-text: #616161;
--search-panel-border: #c8c8c8;
--search-input-bg: #ffffff;
--search-input-text: #616161;
--search-input-border: #cecece;
--search-focus-border: #0078d4;
--search-btn-hover: rgba(0, 0, 0, 0.1);
--search-btn-active-bg: rgba(0, 120, 212, 0.2);
--search-btn-active-text: #0078d4;
--search-error-border: #e51400;
--search-error-bg: #fdeceb;
/* Search Match Highlight - Light Theme (VSCode style) */
--search-match-bg: rgba(250, 220, 81, 0.85);
--search-match-selected-bg: rgba(38, 143, 255, 0.3);
--search-match-selected-border: #268fff;
}
/* 跟随系统的浅色偏好 */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root[data-theme="auto"] { :root[data-theme="auto"] {
--toolbar-bg: var(--light-toolbar-bg); color-scheme: light;
--toolbar-border: var(--light-toolbar-border);
--toolbar-text: var(--light-toolbar-text); --text-primary: #000000;
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
--toolbar-button-hover: var(--light-toolbar-button-hover); --toolbar-bg: #f8f9fa;
--toolbar-separator: var(--light-toolbar-button-hover); --toolbar-border: #e9ecef;
--tab-active-line: var(--light-tab-active-line); --toolbar-text: #212529;
--bg-secondary: var(--light-bg-secondary); --toolbar-text-secondary: #495057;
--text-secondary: var(--light-text-secondary); --toolbar-button-hover: #e9ecef;
--text-muted: var(--light-text-muted); --toolbar-separator: #e9ecef;
--border-color: var(--light-border-color);
--settings-bg: var(--light-settings-bg); --tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--settings-card-bg: var(--light-settings-card-bg); --bg-secondary: #f7fef7;
--settings-text: var(--light-settings-text); --bg-primary: #ffffff;
--settings-text-secondary: var(--light-settings-text-secondary); --bg-hover: #f1f3f4;
--settings-border: var(--light-settings-border);
--settings-input-bg: var(--light-settings-input-bg); --text-secondary: #374151;
--settings-input-border: var(--light-settings-input-border); --text-muted: #6b7280;
--settings-hover: var(--light-settings-hover); --text-danger: #dc3545;
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb); --border-color: #e5e7eb;
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg); --settings-bg: #ffffff;
--selection-text: var(--light-selection-text); --settings-card-bg: #f8f9fa;
--text-danger: var(--light-danger-color); --settings-text: #212529;
--bg-primary: var(--light-bg-primary); --settings-text-secondary: #6c757d;
--bg-hover: var(--light-bg-hover); --settings-border: #dee2e6;
--voidraft-bg-gradient: var(--light-loading-bg-gradient); --settings-input-bg: #ffffff;
--voidraft-loading-color: var(--light-loading-color); --settings-input-border: #ced4da;
--voidraft-loading-glow: var(--light-loading-glow); --settings-hover: #e9ecef;
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay); --scrollbar-track: #f1f3f4;
--scrollbar-thumb: #c1c1c1;
--scrollbar-thumb-hover: #a8a8a8;
--selection-bg: rgba(59, 130, 246, 0.15);
--selection-text: #2563eb;
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--voidraft-loading-color: #1a3c1a;
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--voidraft-loading-done-color: #008800;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* Markdown 代码块样式 - 亮色主题 */
--cm-codeblock-bg: oklch(92.9% 0.013 255.508);
--cm-codeblock-radius: 0.4rem;
/* Markdown 内联代码样式 */
--cm-inline-code-bg: oklch(92.9% 0.013 255.508);
/* Markdown 上标/下标样式 */
--cm-superscript-color: inherit;
--cm-subscript-color: inherit;
/* Markdown 高亮样式 */
--cm-highlight-background: rgba(253, 224, 71, 0.45);
/* Markdown 表格样式 - 亮色主题 */
--cm-table-bg: oklch(97.5% 0.006 255);
--cm-table-header-bg: oklch(94% 0.01 255);
--cm-table-border: oklch(88% 0.008 255);
--cm-table-row-hover: oklch(95% 0.008 255);
/* Search Panel - Light Theme (auto) */
--search-panel-bg: #f3f3f3;
--search-panel-text: #616161;
--search-panel-border: #c8c8c8;
--search-input-bg: #ffffff;
--search-input-text: #616161;
--search-input-border: #cecece;
--search-focus-border: #0078d4;
--search-btn-hover: rgba(0, 0, 0, 0.1);
--search-btn-active-bg: rgba(0, 120, 212, 0.2);
--search-btn-active-text: #0078d4;
--search-error-border: #e51400;
--search-error-bg: #fdeceb;
/* Search Match Highlight - Light Theme auto (VSCode style) */
--search-match-bg: rgba(250, 220, 81, 0.85);
--search-match-selected-bg: rgba(38, 143, 255, 0.3);
--search-match-selected-border: #268fff;
} }
} }
/* 手动选择浅色主题 */
:root[data-theme="light"] {
--toolbar-bg: var(--light-toolbar-bg);
--toolbar-border: var(--light-toolbar-border);
--toolbar-text: var(--light-toolbar-text);
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
--toolbar-button-hover: var(--light-toolbar-button-hover);
--toolbar-separator: var(--light-toolbar-button-hover);
--tab-active-line: var(--light-tab-active-line);
--bg-secondary: var(--light-bg-secondary);
--text-secondary: var(--light-text-secondary);
--text-muted: var(--light-text-muted);
--border-color: var(--light-border-color);
--settings-bg: var(--light-settings-bg);
--settings-card-bg: var(--light-settings-card-bg);
--settings-text: var(--light-settings-text);
--settings-text-secondary: var(--light-settings-text-secondary);
--settings-border: var(--light-settings-border);
--settings-input-bg: var(--light-settings-input-bg);
--settings-input-border: var(--light-settings-input-border);
--settings-hover: var(--light-settings-hover);
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb);
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg);
--selection-text: var(--light-selection-text);
--text-danger: var(--light-danger-color);
--bg-primary: var(--light-bg-primary);
--bg-hover: var(--light-bg-hover);
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
--voidraft-loading-color: var(--light-loading-color);
--voidraft-loading-glow: var(--light-loading-glow);
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay);
}
/* 手动选择深色主题 */
:root[data-theme="dark"] {
--toolbar-bg: var(--dark-toolbar-bg);
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover);
--tab-active-line: var(--dark-tab-active-line);
--bg-secondary: var(--dark-bg-secondary);
--text-secondary: var(--dark-text-secondary);
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg);
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary);
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border);
--settings-hover: var(--dark-settings-hover);
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
}

View File

@@ -1,43 +1,19 @@
import { import {
AppConfig, AppConfig,
AppearanceConfig, AuthMethod,
EditingConfig, KeyBindingType,
GeneralConfig,
LanguageType, LanguageType,
SystemThemeType, SystemThemeType,
TabType, TabType,
UpdatesConfig,
UpdateSourceType,
GitBackupConfig,
AuthMethod
} from '@/../bindings/voidraft/internal/models/models'; } from '@/../bindings/voidraft/internal/models/models';
import {FONT_OPTIONS} from './fonts'; import {FONT_OPTIONS} from './fonts';
// 配置键映射和限制的类型定义
export type GeneralConfigKeyMap = {
readonly [K in keyof GeneralConfig]: string;
};
export type EditingConfigKeyMap = {
readonly [K in keyof EditingConfig]: string;
};
export type AppearanceConfigKeyMap = {
readonly [K in keyof AppearanceConfig]: string;
};
export type UpdatesConfigKeyMap = {
readonly [K in keyof UpdatesConfig]: string;
};
export type BackupConfigKeyMap = {
readonly [K in keyof GitBackupConfig]: string;
};
export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight'; export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
export type ConfigSection = 'general' | 'editing' | 'appearance' | 'updates' | 'backup';
// 配置键映射 // 统一配置键映射(平级展开)
export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = { export const CONFIG_KEY_MAP = {
// general
alwaysOnTop: 'general.alwaysOnTop', alwaysOnTop: 'general.alwaysOnTop',
dataPath: 'general.dataPath', dataPath: 'general.dataPath',
enableSystemTray: 'general.enableSystemTray', enableSystemTray: 'general.enableSystemTray',
@@ -47,9 +23,8 @@ export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
enableWindowSnap: 'general.enableWindowSnap', enableWindowSnap: 'general.enableWindowSnap',
enableLoadingAnimation: 'general.enableLoadingAnimation', enableLoadingAnimation: 'general.enableLoadingAnimation',
enableTabs: 'general.enableTabs', enableTabs: 'general.enableTabs',
} as const; enableMemoryMonitor: 'general.enableMemoryMonitor',
// editing
export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
fontSize: 'editing.fontSize', fontSize: 'editing.fontSize',
fontFamily: 'editing.fontFamily', fontFamily: 'editing.fontFamily',
fontWeight: 'editing.fontWeight', fontWeight: 'editing.fontWeight',
@@ -57,16 +32,13 @@ export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
enableTabIndent: 'editing.enableTabIndent', enableTabIndent: 'editing.enableTabIndent',
tabSize: 'editing.tabSize', tabSize: 'editing.tabSize',
tabType: 'editing.tabType', tabType: 'editing.tabType',
autoSaveDelay: 'editing.autoSaveDelay' keymapMode: 'editing.keymapMode',
} as const; autoSaveDelay: 'editing.autoSaveDelay',
// appearance
export const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
language: 'appearance.language', language: 'appearance.language',
systemTheme: 'appearance.systemTheme', systemTheme: 'appearance.systemTheme',
currentTheme: 'appearance.currentTheme' currentTheme: 'appearance.currentTheme',
} as const; // updates
export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
version: 'updates.version', version: 'updates.version',
autoUpdate: 'updates.autoUpdate', autoUpdate: 'updates.autoUpdate',
primarySource: 'updates.primarySource', primarySource: 'updates.primarySource',
@@ -74,10 +46,8 @@ export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
backupBeforeUpdate: 'updates.backupBeforeUpdate', backupBeforeUpdate: 'updates.backupBeforeUpdate',
updateTimeout: 'updates.updateTimeout', updateTimeout: 'updates.updateTimeout',
github: 'updates.github', github: 'updates.github',
gitea: 'updates.gitea' gitea: 'updates.gitea',
} as const; // backup
export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
enabled: 'backup.enabled', enabled: 'backup.enabled',
repo_url: 'backup.repo_url', repo_url: 'backup.repo_url',
auth_method: 'backup.auth_method', auth_method: 'backup.auth_method',
@@ -90,6 +60,8 @@ export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
auto_backup: 'backup.auto_backup', auto_backup: 'backup.auto_backup',
} as const; } as const;
export type ConfigKey = keyof typeof CONFIG_KEY_MAP;
// 配置限制 // 配置限制
export const CONFIG_LIMITS = { export const CONFIG_LIMITS = {
fontSize: {min: 12, max: 28, default: 13}, fontSize: {min: 12, max: 28, default: 13},
@@ -116,6 +88,7 @@ export const DEFAULT_CONFIG: AppConfig = {
enableWindowSnap: true, enableWindowSnap: true,
enableLoadingAnimation: true, enableLoadingAnimation: true,
enableTabs: false, enableTabs: false,
enableMemoryMonitor: true,
}, },
editing: { editing: {
fontSize: CONFIG_LIMITS.fontSize.default, fontSize: CONFIG_LIMITS.fontSize.default,
@@ -125,29 +98,23 @@ export const DEFAULT_CONFIG: AppConfig = {
enableTabIndent: true, enableTabIndent: true,
tabSize: CONFIG_LIMITS.tabSize.default, tabSize: CONFIG_LIMITS.tabSize.default,
tabType: CONFIG_LIMITS.tabType.default, tabType: CONFIG_LIMITS.tabType.default,
keymapMode: KeyBindingType.Standard,
autoSaveDelay: 5000 autoSaveDelay: 5000
}, },
appearance: { appearance: {
language: LanguageType.LangZhCN, language: LanguageType.LangZhCN,
systemTheme: SystemThemeType.SystemThemeAuto, systemTheme: SystemThemeType.SystemThemeDark,
currentTheme: 'default-dark' currentTheme: 'default-dark'
}, },
updates: { updates: {
version: "1.0.0", version: "1.0.0",
autoUpdate: true, autoUpdate: true,
primarySource: UpdateSourceType.UpdateSourceGithub,
backupSource: UpdateSourceType.UpdateSourceGitea,
backupBeforeUpdate: true, backupBeforeUpdate: true,
updateTimeout: 30, updateTimeout: 120,
github: { github: {
owner: "landaiqing", owner: "landaiqing",
repo: "voidraft", repo: "voidraft",
}, },
gitea: {
baseURL: "https://git.landaiqing.cn",
owner: "landaiqing",
repo: "voidraft",
}
}, },
backup: { backup: {
enabled: false, enabled: false,

View File

@@ -5,7 +5,7 @@
// 编辑器实例管理 // 编辑器实例管理
export const EDITOR_CONFIG = { export const EDITOR_CONFIG = {
/** 最多缓存的编辑器实例数量 */ /** 最多缓存的编辑器实例数量 */
MAX_INSTANCES: 5, MAX_INSTANCES: 10,
/** 语法树缓存过期时间(毫秒) */ /** 语法树缓存过期时间(毫秒) */
SYNTAX_TREE_CACHE_TIMEOUT: 30000, SYNTAX_TREE_CACHE_TIMEOUT: 30000,
/** 加载状态延迟时间(毫秒) */ /** 加载状态延迟时间(毫秒) */

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +0,0 @@
/**
* 默认翻译配置
*/
export const DEFAULT_TRANSLATION_CONFIG = {
minSelectionLength: 2,
maxTranslationLength: 5000,
} as const;
/**
* 翻译相关的错误消息
*/
export const TRANSLATION_ERRORS = {
NO_TEXT: 'no text to translate',
TRANSLATION_FAILED: 'translation failed',
} as const;
/**
* 翻译结果接口
*/
export interface TranslationResult {
translatedText: string;
error?: string;
}
/**
* 语言信息接口
*/
export interface LanguageInfo {
Code: string; // 语言代码
Name: string; // 语言名称
}
/**
* 翻译器扩展配置
*/
export interface TranslatorConfig {
/** 最小选择字符数才显示翻译按钮 */
minSelectionLength: number;
/** 最大翻译字符数 */
maxTranslationLength: number;
}
/**
* 翻译图标SVG
*/
export const TRANSLATION_ICON_SVG = `
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
</svg>`;

View File

@@ -1,159 +0,0 @@
// Enclose abbreviations in <abbr> tags
//
import MarkdownIt, {StateBlock, StateCore, Token} from 'markdown-it';
/**
* 环境接口,包含缩写定义
*/
interface AbbrEnv {
abbreviations?: { [key: string]: string };
}
/**
* markdown-it-abbr 插件
* 用于支持缩写语法
*/
export default function abbr_plugin(md: MarkdownIt): void {
const escapeRE = md.utils.escapeRE;
const arrayReplaceAt = md.utils.arrayReplaceAt;
// ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on;
// you can check character classes here:
// http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
const OTHER_CHARS = ' \r\n$+<=>^`|~';
const UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source;
const UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source;
function abbr_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
let labelEnd: number;
let pos = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
if (pos + 2 >= max) { return false; }
if (state.src.charCodeAt(pos++) !== 0x2A/* * */) { return false; }
if (state.src.charCodeAt(pos++) !== 0x5B/* [ */) { return false; }
const labelStart = pos;
for (; pos < max; pos++) {
const ch = state.src.charCodeAt(pos);
if (ch === 0x5B /* [ */) {
return false;
} else if (ch === 0x5D /* ] */) {
labelEnd = pos;
break;
} else if (ch === 0x5C /* \ */) {
pos++;
}
}
if (labelEnd! < 0 || state.src.charCodeAt(labelEnd! + 1) !== 0x3A/* : */) {
return false;
}
if (silent) { return true; }
const label = state.src.slice(labelStart, labelEnd!).replace(/\\(.)/g, '$1');
const title = state.src.slice(labelEnd! + 2, max).trim();
if (label.length === 0) { return false; }
if (title.length === 0) { return false; }
const env = state.env as AbbrEnv;
if (!env.abbreviations) { env.abbreviations = {}; }
// prepend ':' to avoid conflict with Object.prototype members
if (typeof env.abbreviations[':' + label] === 'undefined') {
env.abbreviations[':' + label] = title;
}
state.line = startLine + 1;
return true;
}
function abbr_replace(state: StateCore): void {
const blockTokens = state.tokens;
const env = state.env as AbbrEnv;
if (!env.abbreviations) { return; }
const regSimple = new RegExp('(?:' +
Object.keys(env.abbreviations).map(function (x: string) {
return x.substr(1);
}).sort(function (a: string, b: string) {
return b.length - a.length;
}).map(escapeRE).join('|') +
')');
const regText = '(^|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE +
'|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' +
'(' + Object.keys(env.abbreviations).map(function (x: string) {
return x.substr(1);
}).sort(function (a: string, b: string) {
return b.length - a.length;
}).map(escapeRE).join('|') + ')' +
'($|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE +
'|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])'
const reg = new RegExp(regText, 'g');
for (let j = 0, l = blockTokens.length; j < l; j++) {
if (blockTokens[j].type !== 'inline') { continue; }
let tokens = blockTokens[j].children!;
// We scan from the end, to keep position when new tags added.
for (let i = tokens.length - 1; i >= 0; i--) {
const currentToken = tokens[i];
if (currentToken.type !== 'text') { continue; }
let pos = 0;
const text = currentToken.content;
reg.lastIndex = 0;
const nodes: Token[] = [];
// fast regexp run to determine whether there are any abbreviated words
// in the current token
if (!regSimple.test(text)) { continue; }
let m: RegExpExecArray | null;
while ((m = reg.exec(text))) {
if (m.index > 0 || m[1].length > 0) {
const token = new state.Token('text', '', 0);
token.content = text.slice(pos, m.index + m[1].length);
nodes.push(token);
}
const token_o = new state.Token('abbr_open', 'abbr', 1);
token_o.attrs = [['title', env.abbreviations[':' + m[2]]]];
nodes.push(token_o);
const token_t = new state.Token('text', '', 0);
token_t.content = m[2];
nodes.push(token_t);
const token_c = new state.Token('abbr_close', 'abbr', -1);
nodes.push(token_c);
reg.lastIndex -= m[3].length;
pos = reg.lastIndex;
}
if (!nodes.length) { continue; }
if (pos < text.length) {
const token = new state.Token('text', '', 0);
token.content = text.slice(pos);
nodes.push(token);
}
// replace current node
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
}
}
}
md.block.ruler.before('reference', 'abbr_def', abbr_def, { alt: ['paragraph', 'reference'] });
md.core.ruler.after('linkify', 'abbr_replace', abbr_replace);
}

View File

@@ -1,209 +0,0 @@
// Process definition lists
//
import MarkdownIt, { StateBlock, Token } from 'markdown-it';
/**
* markdown-it-deflist 插件
* 用于支持定义列表语法
*/
export default function deflist_plugin(md: MarkdownIt): void {
const isSpace = md.utils.isSpace;
// Search `[:~][\n ]`, returns next pos after marker on success
// or -1 on fail.
function skipMarker(state: StateBlock, line: number): number {
let start = state.bMarks[line] + state.tShift[line];
const max = state.eMarks[line];
if (start >= max) { return -1; }
// Check bullet
const marker = state.src.charCodeAt(start++);
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1; }
const pos = state.skipSpaces(start);
// require space after ":"
if (start === pos) { return -1; }
// no empty definitions, e.g. " : "
if (pos >= max) { return -1; }
return start;
}
function markTightParagraphs(state: StateBlock, idx: number): void {
const level = state.level + 2;
for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
state.tokens[i + 2].hidden = true;
state.tokens[i].hidden = true;
i += 2;
}
}
}
function deflist(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
if (silent) {
// quirk: validation mode validates a dd block only, not a whole deflist
if (state.ddIndent < 0) { return false; }
return skipMarker(state, startLine) >= 0;
}
let nextLine = startLine + 1;
if (nextLine >= endLine) { return false; }
if (state.isEmpty(nextLine)) {
nextLine++;
if (nextLine >= endLine) { return false; }
}
if (state.sCount[nextLine] < state.blkIndent) { return false; }
let contentStart = skipMarker(state, nextLine);
if (contentStart < 0) { return false; }
// Start list
const listTokIdx = state.tokens.length;
let tight = true;
const token_dl_o: Token = state.push('dl_open', 'dl', 1);
const listLines: [number, number] = [startLine, 0];
token_dl_o.map = listLines;
//
// Iterate list items
//
let dtLine = startLine;
let ddLine = nextLine;
// One definition list can contain multiple DTs,
// and one DT can be followed by multiple DDs.
//
// Thus, there is two loops here, and label is
// needed to break out of the second one
//
/* eslint no-labels:0,block-scoped-var:0 */
OUTER:
for (;;) {
let prevEmptyEnd = false;
const token_dt_o: Token = state.push('dt_open', 'dt', 1);
token_dt_o.map = [dtLine, dtLine];
const token_i: Token = state.push('inline', '', 0);
token_i.map = [dtLine, dtLine];
token_i.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim();
token_i.children = [];
state.push('dt_close', 'dt', -1);
for (;;) {
const token_dd_o: Token = state.push('dd_open', 'dd', 1);
const itemLines: [number, number] = [nextLine, 0];
token_dd_o.map = itemLines;
let pos = contentStart;
const max = state.eMarks[ddLine];
let offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]);
while (pos < max) {
const ch = state.src.charCodeAt(pos);
if (isSpace(ch)) {
if (ch === 0x09) {
offset += 4 - offset % 4;
} else {
offset++;
}
} else {
break;
}
pos++;
}
contentStart = pos;
const oldTight = state.tight;
const oldDDIndent = state.ddIndent;
const oldIndent = state.blkIndent;
const oldTShift = state.tShift[ddLine];
const oldSCount = state.sCount[ddLine];
const oldParentType = state.parentType;
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2;
state.tShift[ddLine] = contentStart - state.bMarks[ddLine];
state.sCount[ddLine] = offset;
state.tight = true;
state.parentType = 'deflist' as any;
state.md.block.tokenize(state, ddLine, endLine);
// If any of list item is tight, mark list as tight
if (!state.tight || prevEmptyEnd) {
tight = false;
}
// Item become loose if finish with empty line,
// but we should filter last element, because it means list finish
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1);
state.tShift[ddLine] = oldTShift;
state.sCount[ddLine] = oldSCount;
state.tight = oldTight;
state.parentType = oldParentType;
state.blkIndent = oldIndent;
state.ddIndent = oldDDIndent;
state.push('dd_close', 'dd', -1);
itemLines[1] = nextLine = state.line;
if (nextLine >= endLine) { break OUTER; }
if (state.sCount[nextLine] < state.blkIndent) { break OUTER; }
contentStart = skipMarker(state, nextLine);
if (contentStart < 0) { break; }
ddLine = nextLine;
// go to the next loop iteration:
// insert DD tag and repeat checking
}
if (nextLine >= endLine) { break; }
dtLine = nextLine;
if (state.isEmpty(dtLine)) { break; }
if (state.sCount[dtLine] < state.blkIndent) { break; }
ddLine = dtLine + 1;
if (ddLine >= endLine) { break; }
if (state.isEmpty(ddLine)) { ddLine++; }
if (ddLine >= endLine) { break; }
if (state.sCount[ddLine] < state.blkIndent) { break; }
contentStart = skipMarker(state, ddLine);
if (contentStart < 0) { break; }
// go to the next loop iteration:
// insert DT and DD tags and repeat checking
}
// Finilize list
state.push('dl_close', 'dl', -1);
listLines[1] = nextLine;
state.line = nextLine;
// mark paragraphs tight if needed
if (tight) {
markTightParagraphs(state, listTokIdx);
}
return true;
}
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: ['paragraph', 'reference', 'blockquote'] });
}

View File

@@ -1,4 +0,0 @@
export { default as bare } from './lib/bare';
export { default as light } from './lib/light';
export { default as full } from './lib/full';

View File

@@ -1,26 +0,0 @@
import MarkdownIt from 'markdown-it';
import emoji_html from './render';
import emoji_replace from './replace';
import normalize_opts, { EmojiOptions } from './normalize_opts';
/**
* Bare emoji 插件(不包含预定义的 emoji 数据)
*/
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
const defaults: EmojiOptions = {
defs: {},
shortcuts: {},
enabled: []
};
const opts = normalize_opts(md.utils.assign({}, defaults, options || {}) as EmojiOptions);
md.renderer.rules.emoji = emoji_html;
md.core.ruler.after(
'linkify',
'emoji',
emoji_replace(md, opts.defs, opts.shortcuts, opts.scanRE, opts.replaceRE)
);
}

View File

@@ -1,158 +0,0 @@
// Generated, don't edit
import { EmojiDefs } from '../normalize_opts';
const emojies: EmojiDefs = {
"grinning": "😀",
"smiley": "😃",
"smile": "😄",
"grin": "😁",
"laughing": "😆",
"satisfied": "😆",
"sweat_smile": "😅",
"joy": "😂",
"wink": "😉",
"blush": "😊",
"innocent": "😇",
"heart_eyes": "😍",
"kissing_heart": "😘",
"kissing": "😗",
"kissing_closed_eyes": "😚",
"kissing_smiling_eyes": "😙",
"yum": "😋",
"stuck_out_tongue": "😛",
"stuck_out_tongue_winking_eye": "😜",
"stuck_out_tongue_closed_eyes": "😝",
"neutral_face": "😐",
"expressionless": "😑",
"no_mouth": "😶",
"smirk": "😏",
"unamused": "😒",
"relieved": "😌",
"pensive": "😔",
"sleepy": "😪",
"sleeping": "😴",
"mask": "😷",
"dizzy_face": "😵",
"sunglasses": "😎",
"confused": "😕",
"worried": "😟",
"open_mouth": "😮",
"hushed": "😯",
"astonished": "😲",
"flushed": "😳",
"frowning": "😦",
"anguished": "😧",
"fearful": "😨",
"cold_sweat": "😰",
"disappointed_relieved": "😥",
"cry": "😢",
"sob": "😭",
"scream": "😱",
"confounded": "😖",
"persevere": "😣",
"disappointed": "😞",
"sweat": "😓",
"weary": "😩",
"tired_face": "😫",
"rage": "😡",
"pout": "😡",
"angry": "😠",
"smiling_imp": "😈",
"smiley_cat": "😺",
"smile_cat": "😸",
"joy_cat": "😹",
"heart_eyes_cat": "😻",
"smirk_cat": "😼",
"kissing_cat": "😽",
"scream_cat": "🙀",
"crying_cat_face": "😿",
"pouting_cat": "😾",
"heart": "❤️",
"hand": "✋",
"raised_hand": "✋",
"v": "✌️",
"point_up": "☝️",
"fist_raised": "✊",
"fist": "✊",
"monkey_face": "🐵",
"cat": "🐱",
"cow": "🐮",
"mouse": "🐭",
"coffee": "☕",
"hotsprings": "♨️",
"anchor": "⚓",
"airplane": "✈️",
"hourglass": "⌛",
"watch": "⌚",
"sunny": "☀️",
"star": "⭐",
"cloud": "☁️",
"umbrella": "☔",
"zap": "⚡",
"snowflake": "❄️",
"sparkles": "✨",
"black_joker": "🃏",
"mahjong": "🀄",
"phone": "☎️",
"telephone": "☎️",
"envelope": "✉️",
"pencil2": "✏️",
"black_nib": "✒️",
"scissors": "✂️",
"wheelchair": "♿",
"warning": "⚠️",
"aries": "♈",
"taurus": "♉",
"gemini": "♊",
"cancer": "♋",
"leo": "♌",
"virgo": "♍",
"libra": "♎",
"scorpius": "♏",
"sagittarius": "♐",
"capricorn": "♑",
"aquarius": "♒",
"pisces": "♓",
"heavy_multiplication_x": "✖️",
"heavy_plus_sign": "",
"heavy_minus_sign": "",
"heavy_division_sign": "➗",
"bangbang": "‼️",
"interrobang": "⁉️",
"question": "❓",
"grey_question": "❔",
"grey_exclamation": "❕",
"exclamation": "❗",
"heavy_exclamation_mark": "❗",
"wavy_dash": "〰️",
"recycle": "♻️",
"white_check_mark": "✅",
"ballot_box_with_check": "☑️",
"heavy_check_mark": "✔️",
"x": "❌",
"negative_squared_cross_mark": "❎",
"curly_loop": "➰",
"loop": "➿",
"part_alternation_mark": "〽️",
"eight_spoked_asterisk": "✳️",
"eight_pointed_black_star": "✴️",
"sparkle": "❇️",
"copyright": "©️",
"registered": "®️",
"tm": "™️",
"information_source": "",
"m": "Ⓜ️",
"black_circle": "⚫",
"white_circle": "⚪",
"black_large_square": "⬛",
"white_large_square": "⬜",
"black_medium_square": "◼️",
"white_medium_square": "◻️",
"black_medium_small_square": "◾",
"white_medium_small_square": "◽",
"black_small_square": "▪️",
"white_small_square": "▫️"
};
export default emojies;

View File

@@ -1,45 +0,0 @@
// Emoticons -> Emoji mapping.
//
// (!) Some patterns skipped, to avoid collisions
// without increase matcher complicity. Than can change in future.
//
// Places to look for more emoticons info:
//
// - http://en.wikipedia.org/wiki/List_of_emoticons#Western
// - https://github.com/wooorm/emoticon/blob/master/Support.md
// - http://factoryjoe.com/projects/emoticons/
//
import { EmojiShortcuts } from '../normalize_opts';
const shortcuts: EmojiShortcuts = {
angry: ['>:(', '>:-('],
blush: [':")', ':-")'],
broken_heart: ['</3', '<\\3'],
// :\ and :-\ not used because of conflict with markdown escaping
confused: [':/', ':-/'], // twemoji shows question
cry: [":'(", ":'-(", ':,(', ':,-('],
frowning: [':(', ':-('],
heart: ['<3'],
imp: [']:(', ']:-('],
innocent: ['o:)', 'O:)', 'o:-)', 'O:-)', '0:)', '0:-)'],
joy: [":')", ":'-)", ':,)', ':,-)', ":'D", ":'-D", ':,D', ':,-D'],
kissing: [':*', ':-*'],
laughing: ['x-)', 'X-)'],
neutral_face: [':|', ':-|'],
open_mouth: [':o', ':-o', ':O', ':-O'],
rage: [':@', ':-@'],
smile: [':D', ':-D'],
smiley: [':)', ':-)'],
smiling_imp: [']:)', ']:-)'],
sob: [":,'(", ":,'-(", ';(', ';-('],
stuck_out_tongue: [':P', ':-P'],
sunglasses: ['8-)', 'B-)'],
sweat: [',:(', ',:-('],
sweat_smile: [',:)', ',:-)'],
unamused: [':s', ':-S', ':z', ':-Z', ':$', ':-$'],
wink: [';)', ';-)']
};
export default shortcuts;

View File

@@ -1,21 +0,0 @@
import MarkdownIt from 'markdown-it';
import emojies_defs from './data/full';
import emojies_shortcuts from './data/shortcuts';
import bare_emoji_plugin from './bare';
import { EmojiOptions } from './normalize_opts';
/**
* Full emoji 插件(包含完整的 emoji 数据)
*/
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
const defaults: EmojiOptions = {
defs: emojies_defs,
shortcuts: emojies_shortcuts,
enabled: []
};
const opts = md.utils.assign({}, defaults, options || {}) as EmojiOptions;
bare_emoji_plugin(md, opts);
}

View File

@@ -1,21 +0,0 @@
import MarkdownIt from 'markdown-it';
import emojies_defs from './data/light';
import emojies_shortcuts from './data/shortcuts';
import bare_emoji_plugin from './bare';
import { EmojiOptions } from './normalize_opts';
/**
* Light emoji 插件(包含常用的 emoji 数据)
*/
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
const defaults: EmojiOptions = {
defs: emojies_defs,
shortcuts: emojies_shortcuts,
enabled: []
};
const opts = md.utils.assign({}, defaults, options || {}) as EmojiOptions;
bare_emoji_plugin(md, opts);
}

View File

@@ -1,95 +0,0 @@
/**
* Emoji 定义类型
*/
export interface EmojiDefs {
[key: string]: string;
}
/**
* Emoji 快捷方式类型
*/
export interface EmojiShortcuts {
[key: string]: string | string[];
}
/**
* 输入选项接口
*/
export interface EmojiOptions {
defs: EmojiDefs;
shortcuts: EmojiShortcuts;
enabled: string[];
}
/**
* 标准化后的选项接口
*/
export interface NormalizedEmojiOptions {
defs: EmojiDefs;
shortcuts: { [key: string]: string };
scanRE: RegExp;
replaceRE: RegExp;
}
/**
* 转义正则表达式特殊字符
*/
function quoteRE(str: string): string {
return str.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&');
}
/**
* 将输入选项转换为更可用的格式并编译搜索正则表达式
*/
export default function normalize_opts(options: EmojiOptions): NormalizedEmojiOptions {
let emojies = options.defs;
// Filter emojies by whitelist, if needed
if (options.enabled.length) {
emojies = Object.keys(emojies).reduce((acc: EmojiDefs, key: string) => {
if (options.enabled.indexOf(key) >= 0) acc[key] = emojies[key];
return acc;
}, {});
}
// Flatten shortcuts to simple object: { alias: emoji_name }
const shortcuts = Object.keys(options.shortcuts).reduce((acc: { [key: string]: string }, key: string) => {
// Skip aliases for filtered emojies, to reduce regexp
if (!emojies[key]) return acc;
if (Array.isArray(options.shortcuts[key])) {
(options.shortcuts[key] as string[]).forEach((alias: string) => { acc[alias] = key; });
return acc;
}
acc[options.shortcuts[key] as string] = key;
return acc;
}, {});
const keys = Object.keys(emojies);
let names: string;
// If no definitions are given, return empty regex to avoid replacements with 'undefined'.
if (keys.length === 0) {
names = '^$';
} else {
// Compile regexp
names = keys
.map((name: string) => { return `:${name}:`; })
.concat(Object.keys(shortcuts))
.sort()
.reverse()
.map((name: string) => { return quoteRE(name); })
.join('|');
}
const scanRE = RegExp(names);
const replaceRE = RegExp(names, 'g');
return {
defs: emojies,
shortcuts,
scanRE,
replaceRE
};
}

View File

@@ -1,9 +0,0 @@
import { Token } from 'markdown-it';
/**
* Emoji 渲染函数
*/
export default function emoji_html(tokens: Token[], idx: number): string {
return tokens[idx].content;
}

View File

@@ -1,97 +0,0 @@
import MarkdownIt, { StateCore, Token } from 'markdown-it';
import { EmojiDefs } from './normalize_opts';
/**
* Emoji 和快捷方式替换逻辑
*
* 注意:理论上,在内联链中解析 :smile: 并只留下快捷方式会更快。
* 但是,谁在乎呢...
*/
export default function create_rule(
md: MarkdownIt,
emojies: EmojiDefs,
shortcuts: { [key: string]: string },
scanRE: RegExp,
replaceRE: RegExp
) {
const arrayReplaceAt = md.utils.arrayReplaceAt;
const ucm = md.utils.lib.ucmicro;
const has = md.utils.has;
const ZPCc = new RegExp([ucm.Z.source, ucm.P.source, ucm.Cc.source].join('|'));
function splitTextToken(text: string, level: number, TokenConstructor: any): Token[] {
let last_pos = 0;
const nodes: Token[] = [];
text.replace(replaceRE, function (match: string, offset: number, src: string): string {
let emoji_name: string;
// Validate emoji name
if (has(shortcuts, match)) {
// replace shortcut with full name
emoji_name = shortcuts[match];
// Don't allow letters before any shortcut (as in no ":/" in http://)
if (offset > 0 && !ZPCc.test(src[offset - 1])) return '';
// Don't allow letters after any shortcut
if (offset + match.length < src.length && !ZPCc.test(src[offset + match.length])) {
return '';
}
} else {
emoji_name = match.slice(1, -1);
}
// Add new tokens to pending list
if (offset > last_pos) {
const token = new TokenConstructor('text', '', 0);
token.content = text.slice(last_pos, offset);
nodes.push(token);
}
const token = new TokenConstructor('emoji', '', 0);
token.markup = emoji_name;
token.content = emojies[emoji_name];
nodes.push(token);
last_pos = offset + match.length;
return '';
});
if (last_pos < text.length) {
const token = new TokenConstructor('text', '', 0);
token.content = text.slice(last_pos);
nodes.push(token);
}
return nodes;
}
return function emoji_replace(state: StateCore): void {
let token: Token;
const blockTokens = state.tokens;
let autolinkLevel = 0;
for (let j = 0, l = blockTokens.length; j < l; j++) {
if (blockTokens[j].type !== 'inline') { continue; }
let tokens = blockTokens[j].children!;
// We scan from the end, to keep position when new tags added.
// Use reversed logic in links start/end match
for (let i = tokens.length - 1; i >= 0; i--) {
token = tokens[i];
if (token.type === 'link_open' || token.type === 'link_close') {
if (token.info === 'auto') { autolinkLevel -= token.nesting; }
}
if (token.type === 'text' && autolinkLevel === 0 && scanRE.test(token.content)) {
// replace current node
blockTokens[j].children = tokens = arrayReplaceAt(
tokens, i, splitTextToken(token.content, token.level, state.Token)
);
}
}
}
};
}

View File

@@ -1,390 +0,0 @@
import MarkdownIt, {Renderer, StateBlock, StateCore, StateInline, Token} from 'markdown-it';
/**
* 脚注元数据接口
*/
interface FootnoteMeta {
id: number;
subId: number;
label: string;
}
/**
* 脚注列表项接口
*/
interface FootnoteItem {
label?: string;
content?: string;
tokens?: Token[];
count: number;
}
/**
* 环境接口
*/
interface FootnoteEnv {
footnotes?: {
refs?: { [key: string]: number };
list?: FootnoteItem[];
};
docId?: string;
}
/// /////////////////////////////////////////////////////////////////////////////
// Renderer partials
function render_footnote_anchor_name(tokens: Token[], idx: number, options: any, env: FootnoteEnv): string {
const n = Number(tokens[idx].meta.id + 1).toString();
let prefix = '';
if (typeof env.docId === 'string') prefix = `-${env.docId}-`;
return prefix + n;
}
function render_footnote_caption(tokens: Token[], idx: number): string {
let n = Number(tokens[idx].meta.id + 1).toString();
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
return `[${n}]`;
}
function render_footnote_ref(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
const id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
const caption = slf.rules.footnote_caption!(tokens, idx, options, env, slf);
let refid = id;
if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`;
}
function render_footnote_block_open(tokens: Token[], idx: number, options: any): string {
return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') +
'<section class="footnotes">\n' +
'<ol class="footnotes-list">\n';
}
function render_footnote_block_close(): string {
return '</ol>\n</section>\n';
}
function render_footnote_open(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
return `<li id="fn${id}" class="footnote-item">`;
}
function render_footnote_close(): string {
return '</li>\n';
}
function render_footnote_anchor(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\uFE0E</a>`;
}
/**
* markdown-it-footnote 插件
* 用于支持脚注语法
*/
export default function footnote_plugin(md: MarkdownIt): void {
const parseLinkLabel = md.helpers.parseLinkLabel;
const isSpace = md.utils.isSpace;
md.renderer.rules.footnote_ref = render_footnote_ref;
md.renderer.rules.footnote_block_open = render_footnote_block_open;
md.renderer.rules.footnote_block_close = render_footnote_block_close;
md.renderer.rules.footnote_open = render_footnote_open;
md.renderer.rules.footnote_close = render_footnote_close;
md.renderer.rules.footnote_anchor = render_footnote_anchor;
// helpers (only used in other rules, no tokens are attached to those)
md.renderer.rules.footnote_caption = render_footnote_caption;
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
// Process footnote block definition
function footnote_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
// line should be at least 5 chars - "[^x]:"
if (start + 4 > max) return false;
if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false;
if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) return false;
let pos: number;
for (pos = start + 2; pos < max; pos++) {
if (state.src.charCodeAt(pos) === 0x20) return false;
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
break;
}
}
if (pos === start + 2) return false; // no empty footnote labels
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) return false;
if (silent) return true;
pos++;
const env = state.env as FootnoteEnv;
if (!env.footnotes) env.footnotes = {};
if (!env.footnotes.refs) env.footnotes.refs = {};
const label = state.src.slice(start + 2, pos - 2);
env.footnotes.refs[`:${label}`] = -1;
const token_fref_o = new state.Token('footnote_reference_open', '', 1);
token_fref_o.meta = { label };
token_fref_o.level = state.level++;
state.tokens.push(token_fref_o);
const oldBMark = state.bMarks[startLine];
const oldTShift = state.tShift[startLine];
const oldSCount = state.sCount[startLine];
const oldParentType = state.parentType;
const posAfterColon = pos;
const initial = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
let offset = initial;
while (pos < max) {
const ch = state.src.charCodeAt(pos);
if (isSpace(ch)) {
if (ch === 0x09) {
offset += 4 - offset % 4;
} else {
offset++;
}
} else {
break;
}
pos++;
}
state.tShift[startLine] = pos - posAfterColon;
state.sCount[startLine] = offset - initial;
state.bMarks[startLine] = posAfterColon;
state.blkIndent += 4;
state.parentType = 'footnote' as any;
if (state.sCount[startLine] < state.blkIndent) {
state.sCount[startLine] += state.blkIndent;
}
state.md.block.tokenize(state, startLine, endLine);
state.parentType = oldParentType;
state.blkIndent -= 4;
state.tShift[startLine] = oldTShift;
state.sCount[startLine] = oldSCount;
state.bMarks[startLine] = oldBMark;
const token_fref_c = new state.Token('footnote_reference_close', '', -1);
token_fref_c.level = --state.level;
state.tokens.push(token_fref_c);
return true;
}
// Process inline footnotes (^[...])
function footnote_inline(state: StateInline, silent: boolean): boolean {
const max = state.posMax;
const start = state.pos;
if (start + 2 >= max) return false;
if (state.src.charCodeAt(start) !== 0x5E/* ^ */) return false;
if (state.src.charCodeAt(start + 1) !== 0x5B/* [ */) return false;
const labelStart = start + 2;
const labelEnd = parseLinkLabel(state, start + 1);
// parser failed to find ']', so it's not a valid note
if (labelEnd < 0) return false;
// We found the end of the link, and know for a fact it's a valid link;
// so all that's left to do is to call tokenizer.
//
if (!silent) {
const env = state.env as FootnoteEnv;
if (!env.footnotes) env.footnotes = {};
if (!env.footnotes.list) env.footnotes.list = [];
const footnoteId = env.footnotes.list.length;
const tokens: Token[] = [];
state.md.inline.parse(
state.src.slice(labelStart, labelEnd),
state.md,
state.env,
tokens
);
const token = state.push('footnote_ref', '', 0);
token.meta = { id: footnoteId };
env.footnotes.list[footnoteId] = {
content: state.src.slice(labelStart, labelEnd),
tokens,
count: 0
};
}
state.pos = labelEnd + 1;
state.posMax = max;
return true;
}
// Process footnote references ([^...])
function footnote_ref(state: StateInline, silent: boolean): boolean {
const max = state.posMax;
const start = state.pos;
// should be at least 4 chars - "[^x]"
if (start + 3 > max) return false;
const env = state.env as FootnoteEnv;
if (!env.footnotes || !env.footnotes.refs) return false;
if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false;
if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) return false;
let pos: number;
for (pos = start + 2; pos < max; pos++) {
if (state.src.charCodeAt(pos) === 0x20) return false;
if (state.src.charCodeAt(pos) === 0x0A) return false;
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
break;
}
}
if (pos === start + 2) return false; // no empty footnote labels
if (pos >= max) return false;
pos++;
const label = state.src.slice(start + 2, pos - 1);
if (typeof env.footnotes.refs[`:${label}`] === 'undefined') return false;
if (!silent) {
if (!env.footnotes.list) env.footnotes.list = [];
let footnoteId: number;
if (env.footnotes.refs[`:${label}`] < 0) {
footnoteId = env.footnotes.list.length;
env.footnotes.list[footnoteId] = { label, count: 0 };
env.footnotes.refs[`:${label}`] = footnoteId;
} else {
footnoteId = env.footnotes.refs[`:${label}`];
}
const footnoteSubId = env.footnotes.list[footnoteId].count;
env.footnotes.list[footnoteId].count++;
const token = state.push('footnote_ref', '', 0);
token.meta = { id: footnoteId, subId: footnoteSubId, label };
}
state.pos = pos;
state.posMax = max;
return true;
}
// Glue footnote tokens to end of token stream
function footnote_tail(state: StateCore): void {
let tokens: Token[] | null = null;
let current: Token[];
let currentLabel: string;
let insideRef = false;
const refTokens: { [key: string]: Token[] } = {};
const env = state.env as FootnoteEnv;
if (!env.footnotes) { return; }
state.tokens = state.tokens.filter(function (tok) {
if (tok.type === 'footnote_reference_open') {
insideRef = true;
current = [];
currentLabel = tok.meta.label;
return false;
}
if (tok.type === 'footnote_reference_close') {
insideRef = false;
// prepend ':' to avoid conflict with Object.prototype members
refTokens[':' + currentLabel] = current;
return false;
}
if (insideRef) { current.push(tok); }
return !insideRef;
});
if (!env.footnotes.list) { return; }
const list = env.footnotes.list;
state.tokens.push(new state.Token('footnote_block_open', '', 1));
for (let i = 0, l = list.length; i < l; i++) {
const token_fo = new state.Token('footnote_open', '', 1);
token_fo.meta = { id: i, label: list[i].label };
state.tokens.push(token_fo);
if (list[i].tokens) {
tokens = [];
const token_po = new state.Token('paragraph_open', 'p', 1);
token_po.block = true;
tokens.push(token_po);
const token_i = new state.Token('inline', '', 0);
token_i.children = list[i].tokens || null;
token_i.content = list[i].content || '';
tokens.push(token_i);
const token_pc = new state.Token('paragraph_close', 'p', -1);
token_pc.block = true;
tokens.push(token_pc);
} else if (list[i].label) {
tokens = refTokens[`:${list[i].label}`] || null;
}
if (tokens) state.tokens = state.tokens.concat(tokens);
let lastParagraph: Token | null;
if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
lastParagraph = state.tokens.pop()!;
} else {
lastParagraph = null;
}
const t = list[i].count > 0 ? list[i].count : 1;
for (let j = 0; j < t; j++) {
const token_a = new state.Token('footnote_anchor', '', 0);
token_a.meta = { id: i, subId: j, label: list[i].label };
state.tokens.push(token_a);
}
if (lastParagraph) {
state.tokens.push(lastParagraph);
}
state.tokens.push(new state.Token('footnote_close', '', -1));
}
state.tokens.push(new state.Token('footnote_block_close', '', -1));
}
md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: ['paragraph', 'reference'] });
md.inline.ruler.after('image', 'footnote_inline', footnote_inline);
md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref);
md.core.ruler.after('inline', 'footnote_tail', footnote_tail);
}

View File

@@ -1,160 +0,0 @@
import MarkdownIt, { StateInline, Token } from 'markdown-it';
/**
* 分隔符接口定义
*/
interface Delimiter {
marker: number;
length: number;
jump: number;
token: number;
end: number;
open: boolean;
close: boolean;
}
/**
* 扫描结果接口定义
*/
interface ScanResult {
can_open: boolean;
can_close: boolean;
length: number;
}
/**
* Token 元数据接口定义
*/
interface TokenMeta {
delimiters?: Delimiter[];
}
/**
* markdown-it-ins 插件
* 用于支持插入文本语法 ++text++
*/
export default function ins_plugin(md: MarkdownIt): void {
// Insert each marker as a separate text token, and add it to delimiter list
//
function tokenize(state: StateInline, silent: boolean): boolean {
const start = state.pos;
const marker = state.src.charCodeAt(start);
if (silent) { return false; }
if (marker !== 0x2B/* + */) { return false; }
const scanned = state.scanDelims(state.pos, true) as ScanResult;
let len = scanned.length;
const ch = String.fromCharCode(marker);
if (len < 2) { return false; }
if (len % 2) {
const token: Token = state.push('text', '', 0);
token.content = ch;
len--;
}
for (let i = 0; i < len; i += 2) {
const token: Token = state.push('text', '', 0);
token.content = ch + ch;
if (!scanned.can_open && !scanned.can_close) { continue; }
state.delimiters.push({
marker,
length: 0, // disable "rule of 3" length checks meant for emphasis
jump: i / 2, // 1 delimiter = 2 characters
token: state.tokens.length - 1,
end: -1,
open: scanned.can_open,
close: scanned.can_close
} as Delimiter);
}
state.pos += scanned.length;
return true;
}
// Walk through delimiter list and replace text tokens with tags
//
function postProcess(state: StateInline, delimiters: Delimiter[]): void {
let token: Token;
const loneMarkers: number[] = [];
const max = delimiters.length;
for (let i = 0; i < max; i++) {
const startDelim = delimiters[i];
if (startDelim.marker !== 0x2B/* + */) {
continue;
}
if (startDelim.end === -1) {
continue;
}
const endDelim = delimiters[startDelim.end];
token = state.tokens[startDelim.token];
token.type = 'ins_open';
token.tag = 'ins';
token.nesting = 1;
token.markup = '++';
token.content = '';
token = state.tokens[endDelim.token];
token.type = 'ins_close';
token.tag = 'ins';
token.nesting = -1;
token.markup = '++';
token.content = '';
if (state.tokens[endDelim.token - 1].type === 'text' &&
state.tokens[endDelim.token - 1].content === '+') {
loneMarkers.push(endDelim.token - 1);
}
}
// If a marker sequence has an odd number of characters, it's splitted
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
// start of the sequence.
//
// So, we have to move all those markers after subsequent s_close tags.
//
while (loneMarkers.length) {
const i = loneMarkers.pop()!;
let j = i + 1;
while (j < state.tokens.length && state.tokens[j].type === 'ins_close') {
j++;
}
j--;
if (i !== j) {
token = state.tokens[j];
state.tokens[j] = state.tokens[i];
state.tokens[i] = token;
}
}
}
md.inline.ruler.before('emphasis', 'ins', tokenize);
md.inline.ruler2.before('emphasis', 'ins', function (state: StateInline): boolean {
const tokens_meta = state.tokens_meta as TokenMeta[];
const max = (state.tokens_meta || []).length;
postProcess(state, state.delimiters as Delimiter[]);
for (let curr = 0; curr < max; curr++) {
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
postProcess(state, tokens_meta[curr].delimiters!);
}
}
return true;
});
}

View File

@@ -1,160 +0,0 @@
import MarkdownIt, {StateInline, Token} from 'markdown-it';
/**
* 分隔符接口定义
*/
interface Delimiter {
marker: number;
length: number;
jump: number;
token: number;
end: number;
open: boolean;
close: boolean;
}
/**
* 扫描结果接口定义
*/
interface ScanResult {
can_open: boolean;
can_close: boolean;
length: number;
}
/**
* Token 元数据接口定义
*/
interface TokenMeta {
delimiters?: Delimiter[];
}
/**
* markdown-it-mark 插件
* 用于支持 ==标记文本== 语法
*/
export default function markPlugin(md: MarkdownIt): void {
// Insert each marker as a separate text token, and add it to delimiter list
//
function tokenize(state: StateInline, silent: boolean): boolean {
const start = state.pos;
const marker = state.src.charCodeAt(start);
if (silent) { return false; }
if (marker !== 0x3D/* = */) { return false; }
const scanned = state.scanDelims(state.pos, true) as ScanResult;
let len = scanned.length;
const ch = String.fromCharCode(marker);
if (len < 2) { return false; }
if (len % 2) {
const token: Token = state.push('text', '', 0);
token.content = ch;
len--;
}
for (let i = 0; i < len; i += 2) {
const token: Token = state.push('text', '', 0);
token.content = ch + ch;
if (!scanned.can_open && !scanned.can_close) { continue; }
state.delimiters.push({
marker,
length: 0, // disable "rule of 3" length checks meant for emphasis
jump: i / 2, // 1 delimiter = 2 characters
token: state.tokens.length - 1,
end: -1,
open: scanned.can_open,
close: scanned.can_close
} as Delimiter);
}
state.pos += scanned.length;
return true;
}
// Walk through delimiter list and replace text tokens with tags
//
function postProcess(state: StateInline, delimiters: Delimiter[]): void {
const loneMarkers: number[] = [];
const max = delimiters.length;
for (let i = 0; i < max; i++) {
const startDelim = delimiters[i];
if (startDelim.marker !== 0x3D/* = */) {
continue;
}
if (startDelim.end === -1) {
continue;
}
const endDelim = delimiters[startDelim.end];
const token_o = state.tokens[startDelim.token];
token_o.type = 'mark_open';
token_o.tag = 'mark';
token_o.nesting = 1;
token_o.markup = '==';
token_o.content = '';
const token_c = state.tokens[endDelim.token];
token_c.type = 'mark_close';
token_c.tag = 'mark';
token_c.nesting = -1;
token_c.markup = '==';
token_c.content = '';
if (state.tokens[endDelim.token - 1].type === 'text' &&
state.tokens[endDelim.token - 1].content === '=') {
loneMarkers.push(endDelim.token - 1);
}
}
// If a marker sequence has an odd number of characters, it's splitted
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
// start of the sequence.
//
// So, we have to move all those markers after subsequent s_close tags.
//
while (loneMarkers.length) {
const i = loneMarkers.pop()!;
let j = i + 1;
while (j < state.tokens.length && state.tokens[j].type === 'mark_close') {
j++;
}
j--;
if (i !== j) {
const token = state.tokens[j];
state.tokens[j] = state.tokens[i];
state.tokens[i] = token;
}
}
}
md.inline.ruler.before('emphasis', 'mark', tokenize);
md.inline.ruler2.before('emphasis', 'mark', function (state: StateInline): boolean {
let curr: number;
const tokens_meta = state.tokens_meta as TokenMeta[];
const max = (state.tokens_meta || []).length;
postProcess(state, state.delimiters as Delimiter[]);
for (curr = 0; curr < max; curr++) {
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
postProcess(state, tokens_meta[curr].delimiters!);
}
}
return true;
});
}

View File

@@ -1,106 +0,0 @@
import mermaid from "mermaid";
import {genUid, hashCode, sleep} from "./utils";
const mermaidCache = new Map<string, HTMLElement>();
// 缓存计数器,用于清除缓存
const mermaidCacheCount = new Map<string, number>();
let count = 0;
let countTmo = setTimeout(() => undefined, 0);
const addCount = () => {
clearTimeout(countTmo);
countTmo = setTimeout(() => {
count++;
clearCache();
}, 500);
};
const clearCache = () => {
for (const key of mermaidCacheCount.keys()) {
const value = mermaidCacheCount.get(key)!;
if (value + 3 < count) {
mermaidCache.delete(key);
mermaidCacheCount.delete(key);
}
}
};
/**
* 渲染 mermaid
* @param code mermaid 代码
* @param targetId 目标 id
* @param count 计数器
*/
const renderMermaid = async (code: string, targetId: string, count: number) => {
let limit = 100;
while (limit-- > 0) {
const container = document.getElementById(targetId);
if (!container) {
await sleep(100);
continue;
}
try {
const {svg} = await mermaid.render("mermaid-svg-" + genUid(), code, container);
container.innerHTML = svg;
mermaidCache.set(targetId, container);
mermaidCacheCount.set(targetId, count);
} catch (e) {
}
break;
}
};
export interface MermaidItOptions {
theme?: "default" | "dark" | "forest" | "neutral" | "base";
}
/**
* 更新 mermaid 主题
*/
export const updateMermaidTheme = (theme: "default" | "dark" | "forest" | "neutral" | "base") => {
mermaid.initialize({
startOnLoad: false,
theme: theme
});
// 清空缓存,强制重新渲染
mermaidCache.clear();
mermaidCacheCount.clear();
};
/**
* mermaid 插件
* @param md markdown-it
* @param options 配置选项
* @constructor MermaidIt
*/
export const MermaidIt = function (md: any, options?: MermaidItOptions): void {
const theme = options?.theme || "default";
mermaid.initialize({
startOnLoad: false,
theme: theme
});
const defaultRenderer = md.renderer.rules.fence.bind(md.renderer.rules);
md.renderer.rules.fence = (tokens: any, idx: any, options: any, env: any, self: any) => {
addCount();
const token = tokens[idx];
const info = token.info.trim();
if (info === "mermaid") {
const containerId = "mermaid-container-" + hashCode(token.content);
const container = document.createElement("div");
container.id = containerId;
if (mermaidCache.has(containerId)) {
container.innerHTML = mermaidCache.get(containerId)!.innerHTML;
mermaidCacheCount.set(containerId, count);
} else {
renderMermaid(token.content, containerId, count).then();
}
return container.outerHTML;
}
// 使用默认的渲染规则
return defaultRenderer(tokens, idx, options, env, self);
};
};

View File

@@ -1,49 +0,0 @@
import { v4 as uuidv4 } from "uuid";
/**
* uuid 生成函数
* @param split 分隔符
*/
export const genUid = (split = "") => {
return uuidv4().split("-").join(split);
};
/**
* 一个简易的sleep函数
*/
export const sleep = async (ms: number) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
/**
* 计算字符串的hash值
* 返回一个数字
* @param str
*/
export const hashCode = (str: string) => {
let hash = 0;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
};
/**
* 一个简易的阻塞函数
*/
export const awaitFor = async (cb: () => boolean, timeout = 0, errText = "超时暂停阻塞") => {
const start = Date.now();
while (true) {
if (cb()) return true;
if (timeout && Date.now() - start > timeout) {
console.error("阻塞超时: " + errText);
return false;
}
await sleep(100);
}
};

View File

@@ -1,66 +0,0 @@
// Process ~subscript~
import MarkdownIt, { StateInline, Token } from 'markdown-it';
// same as UNESCAPE_MD_RE plus a space
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
function subscript(state: StateInline, silent: boolean): boolean {
const max = state.posMax;
const start = state.pos;
if (state.src.charCodeAt(start) !== 0x7E/* ~ */) { return false; }
if (silent) { return false; } // don't run any pairs in validation mode
if (start + 2 >= max) { return false; }
state.pos = start + 1;
let found = false;
while (state.pos < max) {
if (state.src.charCodeAt(state.pos) === 0x7E/* ~ */) {
found = true;
break;
}
state.md.inline.skipToken(state);
}
if (!found || start + 1 === state.pos) {
state.pos = start;
return false;
}
const content = state.src.slice(start + 1, state.pos);
// don't allow unescaped spaces/newlines inside
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
state.pos = start;
return false;
}
// found!
state.posMax = state.pos;
state.pos = start + 1;
// Earlier we checked !silent, but this implementation does not need it
const token_so: Token = state.push('sub_open', 'sub', 1);
token_so.markup = '~';
const token_t: Token = state.push('text', '', 0);
token_t.content = content.replace(UNESCAPE_RE, '$1');
const token_sc: Token = state.push('sub_close', 'sub', -1);
token_sc.markup = '~';
state.pos = state.posMax + 1;
state.posMax = max;
return true;
}
/**
* markdown-it-sub 插件
* 用于支持下标语法 ~text~
*/
export default function sub_plugin(md: MarkdownIt): void {
md.inline.ruler.after('emphasis', 'sub', subscript);
}

View File

@@ -1,66 +0,0 @@
// Process ^superscript^
import MarkdownIt, { StateInline, Token } from 'markdown-it';
// same as UNESCAPE_MD_RE plus a space
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
function superscript(state: StateInline, silent: boolean): boolean {
const max = state.posMax;
const start = state.pos;
if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; }
if (silent) { return false; } // don't run any pairs in validation mode
if (start + 2 >= max) { return false; }
state.pos = start + 1;
let found = false;
while (state.pos < max) {
if (state.src.charCodeAt(state.pos) === 0x5E/* ^ */) {
found = true;
break;
}
state.md.inline.skipToken(state);
}
if (!found || start + 1 === state.pos) {
state.pos = start;
return false;
}
const content = state.src.slice(start + 1, state.pos);
// don't allow unescaped spaces/newlines inside
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
state.pos = start;
return false;
}
// found!
state.posMax = state.pos;
state.pos = start + 1;
// Earlier we checked !silent, but this implementation does not need it
const token_so: Token = state.push('sup_open', 'sup', 1);
token_so.markup = '^';
const token_t: Token = state.push('text', '', 0);
token_t.content = content.replace(UNESCAPE_RE, '$1');
const token_sc: Token = state.push('sup_close', 'sup', -1);
token_sc.markup = '^';
state.pos = state.posMax + 1;
state.posMax = max;
return true;
}
/**
* markdown-it-sup 插件
* 用于支持上标语法 ^text^
*/
export default function sup_plugin(md: MarkdownIt): void {
md.inline.ruler.after('emphasis', 'sup', superscript);
}

View File

@@ -35,7 +35,6 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
// Helper methods // Helper methods
public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[]; public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[];
public visitSingle: (ctx: TomlContext) => Doc | string; public visitSingle: (ctx: TomlContext) => Doc | string;
public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string;
constructor() { constructor() {
super(); super();
@@ -57,26 +56,38 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
const singleElement = getSingle(ctx); const singleElement = getSingle(ctx);
return this.visit(singleElement); return this.visit(singleElement);
}; };
}
// Store reference to inherited visit method and override it /**
const originalVisit = Object.getPrototypeOf(this).visit?.bind(this); * Override visit method to handle TOML CST nodes
this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => { * Accepts both single node and array of nodes as per base class signature
if (!ctx) { */
return ''; 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 // Try to use the inherited visit method first
const originalVisit = super.visit;
if (originalVisit) { if (originalVisit) {
try { try {
return originalVisit(ctx, inParam); return originalVisit.call(this, ctx, param);
} catch (error) { } catch (error) {
console.warn('Original visit method failed:', error); // Fallback to manual dispatch
} }
} }
// Fallback: manually dispatch based on node name/type // Fallback: manually dispatch based on node name/type
const methodName = ctx.name; 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]; const visitMethod = (this as any)[methodName];
try { try {
if (ctx.children) { if (ctx.children) {
@@ -88,16 +99,16 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
console.warn(`Visit method ${methodName} failed:`, error); 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 * Visit the root TOML document
*/ */
toml(ctx: TomlDocument): Doc { toml(ctx: any): Doc {
// Handle empty toml document // Handle empty toml document
if (!ctx.expression) { if (!ctx.expression) {
return [line]; return [line];
@@ -164,7 +175,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
/** /**
* Visit an expression (keyval, table, or comment) * Visit an expression (keyval, table, or comment)
*/ */
expression(ctx: TomlExpression): Doc | string { expression(ctx: any): Doc | string {
if (ctx.keyval) { if (ctx.keyval) {
let keyValDoc = this.visit(ctx.keyval[0]); let keyValDoc = this.visit(ctx.keyval[0]);
if (ctx.Comment) { if (ctx.Comment) {
@@ -189,7 +200,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
/** /**
* Visit a key-value pair * Visit a key-value pair
*/ */
keyval(ctx: TomlKeyVal): Doc { keyval(ctx: any): Doc {
const keyDoc = this.visit(ctx.key[0]); const keyDoc = this.visit(ctx.key[0]);
const valueDoc = this.visit(ctx.val[0]); const valueDoc = this.visit(ctx.val[0]);
return [keyDoc, ' = ', valueDoc]; return [keyDoc, ' = ', valueDoc];

View File

@@ -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;
}
}

View File

@@ -1,42 +1,13 @@
import { LanguageType } from '@/../bindings/voidraft/internal/models/models';
import type { SupportedLocaleType } from '@/common/constant/locales';
/** /**
* 配置工具类 * 配置工具类
*/ */
export class ConfigUtils { export class ConfigUtils {
/**
* 将后端语言类型转换为前端语言代码
*/
static backendLanguageToFrontend(language: LanguageType): SupportedLocaleType {
return language === LanguageType.LangZhCN ? 'zh-CN' : 'en-US';
}
/** /**
* 将前端语言代码转换为后端语言类型 * 验证数值是否在指定范围内
*/ */
static frontendLanguageToBackend(locale: SupportedLocaleType): LanguageType { static clamp(value: number, min: number, max: number): number {
return locale === 'zh-CN' ? LanguageType.LangZhCN : LanguageType.LangEnUS; 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;
}
} }

View File

@@ -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');
});
});
});

View File

@@ -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);
}

View File

@@ -0,0 +1,65 @@
/**
* Formatter utility functions
*/
export interface DateTimeFormatOptions {
locale?: string;
includeTime?: boolean;
hour12?: boolean;
}
/**
* Format date time string to localized format
* @param dateString - ISO date string or null
* @param options - Formatting options
* @returns Formatted date string or error message
*/
export const formatDateTime = (
dateString: string | null,
options: DateTimeFormatOptions = {}
): string => {
const {
locale = 'en-US',
includeTime = true,
hour12 = false
} = options;
if (!dateString) {
return 'Unknown time';
}
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return 'Invalid date';
}
const formatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
};
if (includeTime) {
formatOptions.hour = '2-digit';
formatOptions.minute = '2-digit';
formatOptions.hour12 = hour12;
}
return date.toLocaleString(locale, formatOptions);
} catch {
return 'Time error';
}
};
/**
* Truncate string with ellipsis
* @param str - String to truncate
* @param maxLength - Maximum length before truncation
* @returns Truncated string with ellipsis if needed
*/
export const truncateString = (str: string, maxLength: number): string => {
if (!str) return '';
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
};

View File

@@ -0,0 +1,42 @@
/**
* Validation utility functions
*/
/**
* Validate document title
* @param title - The title to validate
* @param maxLength - Maximum allowed length (default: 50)
* @returns Error message if invalid, null if valid
*/
export const validateDocumentTitle = (title: string, maxLength: number = 50): string | null => {
const trimmed = title.trim();
if (!trimmed) {
return 'Document name cannot be empty';
}
if (trimmed.length > maxLength) {
return `Document name cannot exceed ${maxLength} characters`;
}
return null;
};
/**
* Check if a string is empty or whitespace only
* @param value - The string to check
* @returns true if empty or whitespace only
*/
export const isEmpty = (value: string | null | undefined): boolean => {
return !value || value.trim().length === 0;
};
/**
* Check if a string exceeds max length
* @param value - The string to check
* @param maxLength - Maximum allowed length
* @returns true if exceeds max length
*/
export const exceedsMaxLength = (value: string, maxLength: number): boolean => {
return value.trim().length > maxLength;
};

View 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>

View 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>

View File

@@ -0,0 +1,3 @@
export { default as AccordionContainer } from './AccordionContainer.vue';
export { default as AccordionItem } from './AccordionItem.vue';

View File

@@ -142,7 +142,7 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-family: var(--voidraft-mono-font, monospace),serif; font-family: Menlo, monospace,serif;
} }
.loading-word { .loading-word {
@@ -175,4 +175,4 @@ onBeforeUnmount(() => {
top: 0; top: 0;
pointer-events: none; pointer-events: none;
} }
</style> </style>

View File

@@ -7,7 +7,7 @@
v-for="tab in tabStore.tabs" v-for="tab in tabStore.tabs"
:key="tab.documentId" :key="tab.documentId"
:tab="tab" :tab="tab"
:isActive="tab.documentId === tabStore.currentDocumentId" :isActive="tab.documentId === documentStore.currentDocumentId"
:canClose="tabStore.canCloseTab" :canClose="tabStore.canCloseTab"
@click="switchToTab" @click="switchToTab"
@close="closeTab" @close="closeTab"
@@ -35,8 +35,14 @@ import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import TabItem from './TabItem.vue'; import TabItem from './TabItem.vue';
import TabContextMenu from './TabContextMenu.vue'; import TabContextMenu from './TabContextMenu.vue';
import { useTabStore } from '@/stores/tabStore'; import { useTabStore } from '@/stores/tabStore';
import { useDocumentStore } from '@/stores/documentStore';
import { useEditorStore } from '@/stores/editorStore';
import { useEditorStateStore } from '@/stores/editorStateStore';
const tabStore = useTabStore(); const tabStore = useTabStore();
const documentStore = useDocumentStore();
const editorStore = useEditorStore();
const editorStateStore = useEditorStateStore();
// DOM 引用 // DOM 引用
const tabBarRef = ref<HTMLElement>(); const tabBarRef = ref<HTMLElement>();
@@ -50,8 +56,36 @@ const contextMenuTargetId = ref<number | null>(null);
// 标签页操作 // 标签页操作
const switchToTab = (documentId: number) => { const switchToTab = async (documentId: number) => {
tabStore.switchToTabAndDocument(documentId);
// 保存旧文档的光标位置
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) => { const closeTab = (documentId: number) => {
@@ -150,7 +184,7 @@ onUnmounted(() => {
}); });
// 监听当前活跃标签页的变化 // 监听当前活跃标签页的变化
watch(() => tabStore.currentDocumentId, () => { watch(() => documentStore.currentDocumentId, () => {
scrollToActiveTab(); scrollToActiveTab();
}); });

View File

@@ -1,35 +1,40 @@
<template> <template>
<div <div
v-if="visible && canClose" v-if="visible && canClose"
class="tab-context-menu" v-click-outside="handleClose"
:style="{ class="tab-context-menu"
left: position.x + 'px', :style="{
top: position.y + 'px' left: position.x + 'px',
}" top: position.y + 'px'
@click.stop }"
@click.stop
> >
<div v-if="canClose" class="menu-item" @click="handleMenuClick('close')"> <div v-if="canClose" class="menu-item" @click="handleMenuClick('close')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18M6 6l12 12"/> <path d="M18 6L6 18M6 6l12 12"/>
</svg> </svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeTab') }}</span> <span class="menu-text">{{ t('tabs.contextMenu.closeTab') }}</span>
</div> </div>
<div v-if="hasOtherTabs" class="menu-item" @click="handleMenuClick('closeOthers')"> <div v-if="hasOtherTabs" class="menu-item" @click="handleMenuClick('closeOthers')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<path d="M9 9l6 6M15 9l-6 6"/> <path d="M9 9l6 6M15 9l-6 6"/>
</svg> </svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeOthers') }}</span> <span class="menu-text">{{ t('tabs.contextMenu.closeOthers') }}</span>
</div> </div>
<div v-if="hasTabsToLeft" class="menu-item" @click="handleMenuClick('closeLeft')"> <div v-if="hasTabsToLeft" class="menu-item" @click="handleMenuClick('closeLeft')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6"/> <path d="M15 18l-6-6 6-6"/>
<path d="M9 18l-6-6 6-6"/> <path d="M9 18l-6-6 6-6"/>
</svg> </svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeLeft') }}</span> <span class="menu-text">{{ t('tabs.contextMenu.closeLeft') }}</span>
</div> </div>
<div v-if="hasTabsToRight" class="menu-item" @click="handleMenuClick('closeRight')"> <div v-if="hasTabsToRight" class="menu-item" @click="handleMenuClick('closeRight')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6"/> <path d="M9 18l6-6-6-6"/>
<path d="M15 18l6-6-6-6"/> <path d="M15 18l6-6-6-6"/>
</svg> </svg>
@@ -39,9 +44,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'; import {computed, onMounted, onUnmounted} from 'vue';
import { useI18n } from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import { useTabStore } from '@/stores/tabStore'; import {useTabStore} from '@/stores/tabStore';
interface Props { interface Props {
visible: boolean; visible: boolean;
@@ -54,7 +59,7 @@ const emit = defineEmits<{
close: []; close: [];
}>(); }>();
const { t } = useI18n(); const {t} = useI18n();
const tabStore = useTabStore(); const tabStore = useTabStore();
// 计算属性 // 计算属性
@@ -79,6 +84,9 @@ const hasTabsToLeft = computed(() => {
return index > 0; return index > 0;
}); });
const handleClose = () => {
emit('close');
};
// 处理菜单项点击 // 处理菜单项点击
const handleMenuClick = (action: string) => { const handleMenuClick = (action: string) => {
if (!props.targetDocumentId) return; if (!props.targetDocumentId) return;
@@ -97,34 +105,9 @@ const handleMenuClick = (action: string) => {
tabStore.closeTabsToRight(props.targetDocumentId); tabStore.closeTabsToRight(props.targetDocumentId);
break; break;
} }
emit('close'); handleClose();
}; };
// 处理外部点击
const handleClickOutside = (_event: MouseEvent) => {
if (props.visible) {
emit('close');
}
};
// 处理ESC键
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && props.visible) {
emit('close');
}
};
// 生命周期
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleEscapeKey);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleEscapeKey);
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -147,15 +130,15 @@ onUnmounted(() => {
padding: 8px 12px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
color: var(--text-muted); color: var(--text-primary);
transition: all 0.15s ease; transition: all 0.15s ease;
gap: 8px; gap: 8px;
&:hover { &:hover {
background-color: var(--toolbar-button-hover); background-color: var(--toolbar-button-hover);
color: var(--text-primary); color: var(--text-primary);
} }
&:active { &:active {
background-color: var(--border-color); background-color: var(--border-color);
} }
@@ -165,9 +148,9 @@ onUnmounted(() => {
flex-shrink: 0; flex-shrink: 0;
width: 12px; width: 12px;
height: 12px; height: 12px;
color: var(--text-muted); color: var(--text-primary);
transition: color 0.15s ease; transition: color 0.15s ease;
.menu-item:hover & { .menu-item:hover & {
color: var(--text-primary); color: var(--text-primary);
} }
@@ -178,4 +161,4 @@ onUnmounted(() => {
font-weight: 400; font-weight: 400;
flex: 1; flex: 1;
} }
</style> </style>

View File

@@ -1,13 +1,15 @@
<template> <template>
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent> <div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent> <div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon"> <div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft"/> <img src="/appicon.png" alt="voidraft"/>
</div> </div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div> <div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">
{{ titleText }}
</div>
<!-- 标签页容器区域 --> <!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag"> <div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer /> <TabContainer/>
</div> </div>
<!-- 设置页面标题 --> <!-- 设置页面标题 -->
<div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div> <div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
@@ -26,7 +28,7 @@
<button <button
class="titlebar-button maximize-button" class="titlebar-button maximize-button"
@click="toggleMaximize" @click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')" :title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
> >
<svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized"> <svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized">
@@ -55,81 +57,43 @@
import {computed, onMounted, ref} from 'vue'; import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {useRoute} from 'vue-router'; import {useRoute} from 'vue-router';
import * as runtime from '@wailsio/runtime';
import {useDocumentStore} from '@/stores/documentStore'; import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue'; import TabContainer from '@/components/tabs/TabContainer.vue';
import {useTabStore} from "@/stores/tabStore"; import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore();
const {t} = useI18n(); const {t} = useI18n();
const route = useRoute(); const route = useRoute();
const isMaximized = ref(false); const tabStore = useTabStore();
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
// 判断是否在设置页面 const isMaximized = ref(false);
const isInSettings = computed(() => route.path.startsWith('/settings')); const isInSettings = computed(() => route.path.startsWith('/settings'));
// 计算标题文本
const titleText = computed(() => { const titleText = computed(() => {
if (isInSettings.value) { if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return `voidraft - ` + t('settings.title'); return generateTitleText(documentStore.currentDocument?.title);
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
}); });
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => { const fullTitleText = computed(() => {
if (isInSettings.value) { if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return `voidraft - ` + t('settings.title'); return generateFullTitleText(documentStore.currentDocument?.title);
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
}); });
const minimizeWindow = async () => { const handleToggleMaximize = async () => {
try { await toggleMaximize();
await runtime.Window.Minimise(); isMaximized.value = await getMaximizedState();
} catch (error) {
console.error(error);
}
};
const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
console.error(error);
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error(error);
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
console.error(error);
}
}; };
onMounted(async () => { onMounted(async () => {
await checkMaximizedState(); isMaximized.value = await getMaximizedState();
}); });
</script> </script>
@@ -160,7 +124,7 @@ onMounted(async () => {
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
cursor: default; cursor: default;
min-width: 0; /* 允许内容收缩 */ min-width: 0;
-webkit-context-menu: none; -webkit-context-menu: none;
-moz-context-menu: none; -moz-context-menu: none;
@@ -310,4 +274,4 @@ onMounted(async () => {
} }
} }
} }
</style> </style>

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="macos-titlebar" style="--wails-draggable:drag" @contextmenu.prevent> <div class="macos-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent> <div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
<button <button
class="titlebar-button close-button" class="titlebar-button close-button"
@click="closeWindow" @click="closeWindow"
:title="t('titlebar.close')" :title="t('titlebar.close')"
> >
<div class="button-icon"> <div class="button-icon">
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons"> <svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons">
@@ -12,11 +12,11 @@
</svg> </svg>
</div> </div>
</button> </button>
<button <button
class="titlebar-button minimize-button" class="titlebar-button minimize-button"
@click="minimizeWindow" @click="minimizeWindow"
:title="t('titlebar.minimize')" :title="t('titlebar.minimize')"
> >
<div class="button-icon"> <div class="button-icon">
<svg width="8" height="1" viewBox="0 0 8 1" v-show="showControlIcons"> <svg width="8" height="1" viewBox="0 0 8 1" v-show="showControlIcons">
@@ -24,11 +24,11 @@
</svg> </svg>
</div> </div>
</button> </button>
<button <button
class="titlebar-button maximize-button" class="titlebar-button maximize-button"
@click="toggleMaximize" @click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')" :title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
> >
<div class="button-icon"> <div class="button-icon">
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons && !isMaximized"> <svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons && !isMaximized">
@@ -42,98 +42,61 @@
</div> </div>
</button> </button>
</div> </div>
<!-- 标签页容器区域 --> <!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag"> <div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer /> <TabContainer/>
</div> </div>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent v-if="!tabStore.isTabsEnabled || isInSettings"> <div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent
v-if="!tabStore.isTabsEnabled || isInSettings">
<div class="titlebar-title" :title="fullTitleText">{{ titleText }}</div> <div class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import {computed, onMounted, ref} from 'vue';
import { useI18n } from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import { useRoute } from 'vue-router'; import {useRoute} from 'vue-router';
import * as runtime from '@wailsio/runtime'; import {useDocumentStore} from '@/stores/documentStore';
import { useDocumentStore } from '@/stores/documentStore'; import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue'; import TabContainer from '@/components/tabs/TabContainer.vue';
import { useTabStore } from "@/stores/tabStore"; import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore(); const {t} = useI18n();
const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const isMaximized = ref(false); const tabStore = useTabStore();
const showControlIcons = ref(false);
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
// 判断是否在设置页面 const isMaximized = ref(false);
const showControlIcons = ref(false);
const isInSettings = computed(() => route.path.startsWith('/settings')); const isInSettings = computed(() => route.path.startsWith('/settings'));
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
console.error(error);
}
};
const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
console.error(error);
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error(error);
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
console.error(error);
}
};
// 计算标题文本
const titleText = computed(() => { const titleText = computed(() => {
if (isInSettings.value) { if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return `voidraft - ` + t('settings.title'); return generateTitleText(documentStore.currentDocument?.title);
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
}); });
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => { const fullTitleText = computed(() => {
if (isInSettings.value) { if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return `voidraft - ` + t('settings.title'); return generateFullTitleText(documentStore.currentDocument?.title);
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
}); });
const handleToggleMaximize = async () => {
await toggleMaximize();
isMaximized.value = await getMaximizedState();
};
onMounted(async () => { onMounted(async () => {
await checkMaximizedState(); isMaximized.value = await getMaximizedState();
}); });
</script> </script>
@@ -147,11 +110,11 @@ onMounted(async () => {
-webkit-user-select: none; -webkit-user-select: none;
width: 100%; width: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
-webkit-context-menu: none; -webkit-context-menu: none;
-moz-context-menu: none; -moz-context-menu: none;
context-menu: none; context-menu: none;
&:hover { &:hover {
.titlebar-button { .titlebar-button {
.button-icon { .button-icon {
@@ -168,7 +131,7 @@ onMounted(async () => {
padding-left: 8px; padding-left: 8px;
gap: 8px; gap: 8px;
flex-shrink: 0; flex-shrink: 0;
-webkit-context-menu: none; -webkit-context-menu: none;
-moz-context-menu: none; -moz-context-menu: none;
context-menu: none; context-menu: none;
@@ -187,7 +150,7 @@ onMounted(async () => {
padding: 0; padding: 0;
margin: 0; margin: 0;
position: relative; position: relative;
.button-icon { .button-icon {
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
@@ -198,7 +161,7 @@ onMounted(async () => {
height: 100%; height: 100%;
color: rgba(0, 0, 0, 0.7); color: rgba(0, 0, 0, 0.7);
} }
&:hover .button-icon { &:hover .button-icon {
opacity: 1; opacity: 1;
} }
@@ -206,11 +169,11 @@ onMounted(async () => {
.close-button { .close-button {
background: #ff5f57; background: #ff5f57;
&:hover { &:hover {
background: #ff453a; background: #ff453a;
} }
&:active { &:active {
background: #d7463f; background: #d7463f;
} }
@@ -218,11 +181,11 @@ onMounted(async () => {
.minimize-button { .minimize-button {
background: #ffbd2e; background: #ffbd2e;
&:hover { &:hover {
background: #ffb524; background: #ffb524;
} }
&:active { &:active {
background: #e6a220; background: #e6a220;
} }
@@ -230,11 +193,11 @@ onMounted(async () => {
.maximize-button { .maximize-button {
background: #28ca42; background: #28ca42;
&:hover { &:hover {
background: #1ebe36; background: #1ebe36;
} }
&:active { &:active {
background: #1ba932; background: #1ba932;
} }
@@ -247,7 +210,7 @@ onMounted(async () => {
flex: 1; flex: 1;
cursor: default; cursor: default;
min-width: 0; min-width: 0;
-webkit-context-menu: none; -webkit-context-menu: none;
-moz-context-menu: none; -moz-context-menu: none;
context-menu: none; context-menu: none;
@@ -261,34 +224,32 @@ onMounted(async () => {
margin-left: 8px; margin-left: 8px;
margin-right: 8px; margin-right: 8px;
min-width: 0; min-width: 0;
overflow: visible; /* 允许TabContainer内部处理滚动 */ overflow: visible;
/* 确保TabContainer能够正确处理滚动 */
:deep(.tab-container) { :deep(.tab-container) {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
:deep(.tab-bar) { :deep(.tab-bar) {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
:deep(.tab-scroll-wrapper) { :deep(.tab-scroll-wrapper) {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
} }
/* 确保底部线条能够正确显示 */
:deep(.tab-item) { :deep(.tab-item) {
position: relative; position: relative;
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -319,13 +280,13 @@ onMounted(async () => {
background: var(--toolbar-bg, #2d2d2d); background: var(--toolbar-bg, #2d2d2d);
border-bottom-color: var(--toolbar-border, rgba(255, 255, 255, 0.1)); border-bottom-color: var(--toolbar-border, rgba(255, 255, 255, 0.1));
} }
.titlebar-title { .titlebar-title {
color: var(--toolbar-text, #fff); color: var(--toolbar-text, #fff);
} }
.titlebar-button .button-icon { .titlebar-button .button-icon {
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
} }
</style> </style>

View File

@@ -1,13 +1,15 @@
<template> <template>
<div class="windows-titlebar" style="--wails-draggable:drag"> <div class="windows-titlebar" style="--wails-draggable:drag">
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent> <div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon"> <div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft"/> <img src="/appicon.png" alt="voidraft"/>
</div> </div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div> <div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">
{{ titleText }}
</div>
<!-- 标签页容器区域 --> <!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag"> <div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer /> <TabContainer/>
</div> </div>
<!-- 设置页面标题 --> <!-- 设置页面标题 -->
<div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div> <div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
@@ -24,7 +26,7 @@
<button <button
class="titlebar-button maximize-button" class="titlebar-button maximize-button"
@click="toggleMaximize" @click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')" :title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
> >
<span class="titlebar-icon" v-html="maximizeIcon"></span> <span class="titlebar-icon" v-html="maximizeIcon"></span>
@@ -45,84 +47,44 @@
import {computed, onMounted, ref} from 'vue'; import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {useRoute} from 'vue-router'; import {useRoute} from 'vue-router';
import * as runtime from '@wailsio/runtime';
import {useDocumentStore} from '@/stores/documentStore'; import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue'; import TabContainer from '@/components/tabs/TabContainer.vue';
import {useTabStore} from "@/stores/tabStore"; import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore();
const {t} = useI18n(); const {t} = useI18n();
const route = useRoute(); const route = useRoute();
const isMaximized = ref(false); const tabStore = useTabStore();
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
// 计算属性用于图标,减少重复渲染 const isMaximized = ref(false);
const maximizeIcon = computed(() => isMaximized.value ? '&#xE923;' : '&#xE922;'); const maximizeIcon = computed(() => isMaximized.value ? '&#xE923;' : '&#xE922;');
// 判断是否在设置页面
const isInSettings = computed(() => route.path.startsWith('/settings')); const isInSettings = computed(() => route.path.startsWith('/settings'));
// 计算标题文本
const titleText = computed(() => { const titleText = computed(() => {
if (isInSettings.value) { if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return `voidraft - ` + t('settings.title'); return generateTitleText(documentStore.currentDocument?.title);
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
}); });
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => { const fullTitleText = computed(() => {
if (isInSettings.value) { if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return `voidraft - ` + t('settings.title'); return generateFullTitleText(documentStore.currentDocument?.title);
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
}); });
const minimizeWindow = async () => { const handleToggleMaximize = async () => {
try { await toggleMaximize();
await runtime.Window.Minimise(); isMaximized.value = await getMaximizedState();
} catch (error) {
console.error(error);
}
};
const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
console.error(error);
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error(error);
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
console.error(error);
}
}; };
onMounted(async () => { onMounted(async () => {
await checkMaximizedState(); isMaximized.value = await getMaximizedState();
}); });
</script> </script>
@@ -152,7 +114,7 @@ onMounted(async () => {
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
cursor: default; cursor: default;
min-width: 0; /* 允许内容收缩 */ min-width: 0;
-webkit-context-menu: none; -webkit-context-menu: none;
-moz-context-menu: none; -moz-context-menu: none;
@@ -178,7 +140,6 @@ onMounted(async () => {
overflow: hidden; overflow: hidden;
margin-left: 8px; margin-left: 8px;
min-width: 0; min-width: 0;
//margin-right: 8px;
} }
.titlebar-controls { .titlebar-controls {
@@ -254,4 +215,4 @@ onMounted(async () => {
opacity: 1; opacity: 1;
} }
} }
</style> </style>

View File

@@ -0,0 +1,60 @@
import * as runtime from '@wailsio/runtime';
/**
* Titlebar utility functions
*/
// Window control functions
export const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
console.error('Failed to minimize window:', error);
}
};
export const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
} catch (error) {
console.error('Failed to toggle maximize:', error);
}
};
export const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error('Failed to close window:', error);
}
};
export const getMaximizedState = async (): Promise<boolean> => {
try {
return await runtime.Window.IsMaximised();
} catch (error) {
console.error('Failed to check maximized state:', error);
return false;
}
};
/**
* Generate title text with optional truncation
*/
export const generateTitleText = (
title: string | undefined,
maxLength: number = 30
): string => {
if (!title) return 'voidraft';
const truncated = title.length > maxLength
? title.substring(0, maxLength) + '...'
: title;
return `voidraft - ${truncated}`;
};
/**
* Generate full title text (no truncation)
*/
export const generateFullTitleText = (title: string | undefined): string => {
return title ? `voidraft - ${title}` : 'voidraft';
};

View 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>

View 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>

View 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;

View 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,
};
});

View 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;
}

View File

@@ -8,7 +8,7 @@ import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state';
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands'; import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
const { t } = useI18n(); const { t } = useI18n();
const editorStore = readonly(useEditorStore()); const editorStore = useEditorStore();
// 组件状态 // 组件状态
const showLanguageMenu = shallowRef(false); const showLanguageMenu = shallowRef(false);
@@ -51,13 +51,13 @@ let editorScope: ReturnType<typeof effectScope> | null = null;
// 更新当前块语言信息 // 更新当前块语言信息
const updateCurrentBlockLanguage = () => { const updateCurrentBlockLanguage = () => {
if (!editorStore.editorView) { if (!editorStore.currentEditor) {
currentBlockLanguage.value = { name: 'text', auto: false }; currentBlockLanguage.value = { name: 'text', auto: false };
return; return;
} }
try { try {
const state = editorStore.editorView.state; const state = editorStore.currentEditor.state;
const activeBlock = getActiveNoteBlock(state as any); const activeBlock = getActiveNoteBlock(state as any);
if (activeBlock) { if (activeBlock) {
const newLanguage = { const newLanguage = {
@@ -128,7 +128,7 @@ const setupEventListeners = (view: any) => {
// 监听编辑器状态变化 // 监听编辑器状态变化
watch( watch(
() => editorStore.editorView, () => editorStore.currentEditor,
(newView) => { (newView) => {
if (newView) { if (newView) {
setupEventListeners(newView); setupEventListeners(newView);
@@ -175,13 +175,13 @@ const closeLanguageMenu = () => {
// 选择语言 - 优化性能 // 选择语言 - 优化性能
const selectLanguage = (languageId: SupportedLanguage) => { const selectLanguage = (languageId: SupportedLanguage) => {
if (!editorStore.editorView) { if (!editorStore.currentEditor) {
closeLanguageMenu(); closeLanguageMenu();
return; return;
} }
try { try {
const view = editorStore.editorView; const view = editorStore.currentEditor;
const state = view.state; const state = view.state;
const dispatch = view.dispatch; const dispatch = view.dispatch;
@@ -294,9 +294,11 @@ const scrollToCurrentLanguage = () => {
<span class="arrow" :class="{ 'open': showLanguageMenu }"></span> <span class="arrow" :class="{ 'open': showLanguageMenu }"></span>
</button> </button>
<div class="language-menu" v-if="showLanguageMenu"> <!-- 菜单 -->
<!-- 搜索框 --> <Transition name="slide-up">
<div class="search-container"> <div class="language-menu" v-if="showLanguageMenu">
<!-- 搜索框 -->
<div class="search-container">
<input <input
ref="searchInputRef" ref="searchInputRef"
v-model="searchQuery" v-model="searchQuery"
@@ -330,11 +332,23 @@ const scrollToCurrentLanguage = () => {
{{ t('toolbar.noLanguageFound') }} {{ t('toolbar.noLanguageFound') }}
</div> </div>
</div> </div>
</div> </div>
</Transition>
</div> </div>
</template> </template>
<style scoped lang="scss"> <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 { .block-language-selector {
position: relative; position: relative;
@@ -386,15 +400,17 @@ const scrollToCurrentLanguage = () => {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 3px; border-radius: 3px;
margin-bottom: 4px; margin-bottom: 4px;
width: 220px; width: 280px;
max-height: 280px; max-height: 400px;
z-index: 1000; z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
.search-container { .search-container {
position: relative; position: relative;
padding: 8px; padding: 10px;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
.search-input { .search-input {
@@ -403,11 +419,11 @@ const scrollToCurrentLanguage = () => {
background-color: var(--bg-primary); background-color: var(--bg-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 2px; border-radius: 2px;
padding: 5px 8px 5px 26px; padding: 6px 10px 6px 30px;
font-size: 11px; font-size: 12px;
color: var(--text-primary); color: var(--text-primary);
outline: none; outline: none;
line-height: 1.2; line-height: 1.4;
&:focus { &:focus {
border-color: var(--text-muted); border-color: var(--text-muted);
@@ -420,7 +436,7 @@ const scrollToCurrentLanguage = () => {
.search-icon { .search-icon {
position: absolute; position: absolute;
left: 14px; left: 16px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: var(--text-muted); color: var(--text-muted);
@@ -429,20 +445,21 @@ const scrollToCurrentLanguage = () => {
} }
.language-list { .language-list {
max-height: 200px; max-height: 320px;
overflow-y: auto; overflow-y: auto;
flex: 1;
.language-option { .language-option {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 6px 8px; padding: 8px 10px;
cursor: pointer; cursor: pointer;
font-size: 11px; font-size: 12px;
border-bottom: 1px solid var(--border-color);
&:hover { &:hover {
background-color: var(--border-color); background-color: var(--bg-hover);
opacity: 0.8;
} }
&.active { &.active {
@@ -460,17 +477,17 @@ const scrollToCurrentLanguage = () => {
} }
.language-alias { .language-alias {
font-size: 10px; font-size: 11px;
color: var(--text-muted); color: var(--text-muted);
opacity: 0.6; opacity: 0.6;
} }
} }
.no-results { .no-results {
padding: 12px 8px; padding: 14px 10px;
text-align: center; text-align: center;
color: var(--text-muted); color: var(--text-muted);
font-size: 11px; font-size: 12px;
} }
} }
} }
@@ -478,7 +495,7 @@ const scrollToCurrentLanguage = () => {
/* 自定义滚动条 */ /* 自定义滚动条 */
.language-list::-webkit-scrollbar { .language-list::-webkit-scrollbar {
width: 4px; width: 6px;
} }
.language-list::-webkit-scrollbar-track { .language-list::-webkit-scrollbar-track {
@@ -487,7 +504,7 @@ const scrollToCurrentLanguage = () => {
.language-list::-webkit-scrollbar-thumb { .language-list::-webkit-scrollbar-thumb {
background-color: var(--border-color); background-color: var(--border-color);
border-radius: 2px; border-radius: 3px;
&:hover { &:hover {
background-color: var(--text-muted); background-color: var(--text-muted);

View File

@@ -1,49 +1,69 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; import {computed, nextTick, reactive, ref, watch} from 'vue';
import { useDocumentStore } from '@/stores/documentStore'; import {useDocumentStore} from '@/stores/documentStore';
import { useTabStore } from '@/stores/tabStore'; import {useTabStore} from '@/stores/tabStore';
import { useWindowStore } from '@/stores/windowStore'; import {useEditorStore} from '@/stores/editorStore';
import { useI18n } from 'vue-i18n'; import {useEditorStateStore} from '@/stores/editorStateStore';
import type { Document } from '@/../bindings/voidraft/internal/models/models'; 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 {Document} from '@/../bindings/voidraft/internal/models/ent/models';
import toast from '@/components/toast';
// 类型定义
interface DocumentItem extends Document {
isCreateOption?: boolean;
}
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
const tabStore = useTabStore(); const tabStore = useTabStore();
const editorStore = useEditorStore();
const editorStateStore = useEditorStateStore();
const windowStore = useWindowStore(); const windowStore = useWindowStore();
const { t } = useI18n(); const {t} = useI18n();
// DOM 引用
const inputRef = ref<HTMLInputElement>();
const editInputRef = ref<HTMLInputElement>();
// 组件状态 // 组件状态
const inputValue = ref(''); const state = reactive({
const inputRef = ref<HTMLInputElement>(); isLoaded: false,
const editingId = ref<number | null>(null); searchQuery: '',
const editingTitle = ref(''); documentList: [] as Document[], // 缓存文档列表
const editInputRef = ref<HTMLInputElement>(); editing: {
const deleteConfirmId = ref<number | null>(null); id: null as number | null,
title: ''
}
});
// 常量 // 常量
const MAX_TITLE_LENGTH = 50; const MAX_TITLE_LENGTH = 50;
const DELETE_CONFIRM_TIMEOUT = 3000;
// 计算属性 // 计算属性
const currentDocName = computed(() => { const currentDocName = computed(() => {
if (!documentStore.currentDocument) return t('toolbar.selectDocument'); if (!documentStore.currentDocument) return t('toolbar.selectDocument');
const title = documentStore.currentDocument.title; return truncateString(documentStore.currentDocument.title || '', 12);
return title.length > 12 ? title.substring(0, 12) + '...' : title;
}); });
const filteredItems = computed(() => { const filteredItems = computed<DocumentItem[]>(() => {
const docs = documentStore.documentList; const docs = state.documentList;
const query = inputValue.value.trim(); const query = state.searchQuery.trim();
if (!query) return docs; if (!query) return docs;
const filtered = docs.filter(doc => const filtered = docs.filter(doc =>
doc.title.toLowerCase().includes(query.toLowerCase()) (doc.title || '').toLowerCase().includes(query.toLowerCase())
); );
// 如果输入的不是已存在文档的完整标题,添加创建选项 // 如果输入的不是已存在文档的完整标题,添加创建选项
const exactMatch = docs.some(doc => doc.title.toLowerCase() === query.toLowerCase()); const exactMatch = docs.some(doc => (doc.title || '').toLowerCase() === query.toLowerCase());
if (!exactMatch && query.length > 0) { if (!exactMatch && query.length > 0) {
return [ return [
{ id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true } as any, {id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true} as DocumentItem,
...filtered ...filtered
]; ];
} }
@@ -51,54 +71,33 @@ const filteredItems = computed(() => {
return filtered; return filtered;
}); });
// 工具函数
const validateTitle = (title: string): string | null => {
if (!title.trim()) return t('toolbar.documentNameRequired');
if (title.trim().length > MAX_TITLE_LENGTH) {
return t('toolbar.documentNameTooLong', { max: MAX_TITLE_LENGTH });
}
return null;
};
const formatTime = (dateString: string | null) => {
if (!dateString) return t('toolbar.unknownTime');
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
return date.toLocaleString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
} catch {
return t('toolbar.timeError');
}
};
// 核心操作 // 核心操作
const openMenu = async () => { const openMenu = async () => {
state.documentList = await documentStore.getDocumentList();
documentStore.openDocumentSelector(); documentStore.openDocumentSelector();
await documentStore.getDocumentMetaList(); state.isLoaded = true;
await nextTick(); await nextTick();
inputRef.value?.focus(); inputRef.value?.focus();
}; };
// 删除确认
const {isConfirming: isDeleting, startConfirm: startDeleteConfirm, reset: resetDeleteConfirm} = useConfirm({
timeout: DELETE_CONFIRM_TIMEOUT
});
const closeMenu = () => { const closeMenu = () => {
state.isLoaded = false;
documentStore.closeDocumentSelector(); documentStore.closeDocumentSelector();
inputValue.value = ''; state.searchQuery = '';
editingId.value = null; state.editing.id = null;
editingTitle.value = ''; state.editing.title = '';
deleteConfirmId.value = null; resetDeleteConfirm();
}; };
const selectDoc = async (doc: Document) => { const selectDoc = async (doc: DocumentItem) => {
// 如果选择的就是当前文档,直接关闭菜单 if (doc.id === undefined) return;
// 如果选择的就是当前文档,直接关闭菜单
if (documentStore.currentDocument?.id === doc.id) { if (documentStore.currentDocument?.id === doc.id) {
closeMenu(); closeMenu();
return; return;
@@ -106,46 +105,79 @@ const selectDoc = async (doc: Document) => {
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id); const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) { if (hasOpen) {
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow')); toast.warning(t('toolbar.alreadyOpenInNewWindow'));
return; return;
} }
const success = await documentStore.openDocument(doc.id);
if (success) { // 保存旧文档的光标位置
if (tabStore.isTabsEnabled) { const oldDocId = documentStore.currentDocumentId;
tabStore.addOrActivateTab(doc); if (oldDocId) {
} const cursorPos = editorStore.getCurrentCursorPosition();
closeMenu(); 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) => { const createDoc = async (title: string) => {
const trimmedTitle = title.trim(); const trimmedTitle = title.trim();
const error = validateTitle(trimmedTitle); const error = validateDocumentTitle(trimmedTitle, MAX_TITLE_LENGTH);
if (error) return; if (error) return;
try { try {
const newDoc = await documentStore.createNewDocument(trimmedTitle); const newDoc = await documentStore.createNewDocument(trimmedTitle);
if (newDoc) await selectDoc(newDoc); if (newDoc && newDoc.id) {
await selectDoc(newDoc);
}
} catch (error) { } catch (error) {
console.error('Failed to create document:', error); console.error('Failed to create document:', error);
} }
}; };
const selectItem = async (item: any) => { const selectDocItem = async (item: any) => {
if (item.isCreateOption) { if (item.isCreateOption) {
await createDoc(inputValue.value.trim()); await createDoc(state.searchQuery.trim());
} else { } else {
await selectDoc(item); await selectDoc(item);
} }
}; };
// 搜索框回车处理
const handleSearchEnter = () => {
const query = state.searchQuery.trim();
if (query && filteredItems.value.length > 0) {
selectDocItem(filteredItems.value[0]);
}
};
// 编辑操作 // 编辑操作
const startRename = (doc: Document, event: Event) => { const renameDoc = (doc: DocumentItem, event: Event) => {
event.stopPropagation(); event.stopPropagation();
editingId.value = doc.id; state.editing.id = doc.id ?? null;
editingTitle.value = doc.title; state.editing.title = doc.title || '';
deleteConfirmId.value = null; resetDeleteConfirm();
nextTick(() => { nextTick(() => {
editInputRef.value?.focus(); editInputRef.value?.focus();
editInputRef.value?.select(); editInputRef.value?.select();
@@ -153,285 +185,245 @@ const startRename = (doc: Document, event: Event) => {
}; };
const saveEdit = async () => { const saveEdit = async () => {
if (!editingId.value || !editingTitle.value.trim()) { if (!state.editing.id || !state.editing.title.trim()) {
editingId.value = null; state.editing.id = null;
editingTitle.value = ''; state.editing.title = '';
return; return;
} }
const trimmedTitle = editingTitle.value.trim(); const trimmedTitle = state.editing.title.trim();
const error = validateTitle(trimmedTitle); const error = validateDocumentTitle(trimmedTitle, MAX_TITLE_LENGTH);
if (error) return; if (error) return;
try { try {
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle); await documentStore.updateDocumentTitle(state.editing.id, trimmedTitle);
await documentStore.getDocumentMetaList(); state.documentList = await documentStore.getDocumentList();
// 如果tabs功能开启且该文档有标签页更新标签页标题 // 如果tabs功能开启且该文档有标签页更新标签页标题
if (tabStore.isTabsEnabled && tabStore.hasTab(editingId.value)) { if (tabStore.isTabsEnabled && tabStore.hasTab(state.editing.id)) {
tabStore.updateTabTitle(editingId.value, trimmedTitle); tabStore.updateTabTitle(state.editing.id, trimmedTitle);
} }
} catch (error) { } catch (error) {
console.error('Failed to update document:', error); console.error('Failed to update document:', error);
} finally { } finally {
editingId.value = null; state.editing.id = null;
editingTitle.value = ''; state.editing.title = '';
} }
}; };
const cancelEdit = () => {
state.editing.id = null;
state.editing.title = '';
};
// 其他操作 // 其他操作
const openInNewWindow = async (doc: Document, event: Event) => { const openInNewWindow = async (doc: DocumentItem, event: Event) => {
event.stopPropagation(); event.stopPropagation();
if (doc.id === undefined) return;
try { try {
// 在打开新窗口前,如果启用了标签且该文档有标签,先关闭标签
if (tabStore.isTabsEnabled && tabStore.hasTab(doc.id)) {
await tabStore.closeTab(doc.id);
}
await documentStore.openDocumentInNewWindow(doc.id); await documentStore.openDocumentInNewWindow(doc.id);
} catch (error) { } catch (error) {
console.error('Failed to open document in new window:', 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(); event.stopPropagation();
if (doc.id === undefined) return;
if (deleteConfirmId.value === doc.id) { if (isDeleting(doc.id)) {
// 确认删除前检查文档是否在其他窗口打开 // 确认删除前检查文档是否在其他窗口打开
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id); const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) { if (hasOpen) {
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow')); toast.warning(t('toolbar.alreadyOpenInNewWindow'));
deleteConfirmId.value = null; resetDeleteConfirm();
return; return;
} }
const deleteSuccess = await documentStore.deleteDocument(doc.id); const deleteSuccess = await documentStore.deleteDocument(doc.id);
if (deleteSuccess) { if (deleteSuccess) {
await documentStore.getDocumentMetaList(); state.documentList = await documentStore.getDocumentList();
// 如果删除的是当前文档切换到第一个文档 // 如果删除的是当前文档,切换到第一个文档
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) { if (documentStore.currentDocument?.id === doc.id && state.documentList.length > 0) {
const firstDoc = documentStore.documentList[0]; const firstDoc = state.documentList[0];
if (firstDoc) await selectDoc(firstDoc); if (firstDoc) await selectDoc(firstDoc);
} }
} }
deleteConfirmId.value = null; resetDeleteConfirm();
} else { } else {
// 进入确认状态 // 进入确认状态
deleteConfirmId.value = doc.id; startDeleteConfirm(doc.id);
editingId.value = null; state.editing.id = null;
// 3秒后自动取消确认状态
setTimeout(() => {
if (deleteConfirmId.value === doc.id) {
deleteConfirmId.value = null;
}
}, 3000);
} }
}; };
// 键盘事件处理 // 切换菜单
const createKeyHandler = (handlers: Record<string, () => void>) => (event: KeyboardEvent) => { const toggleMenu = () => {
const handler = handlers[event.key]; if (documentStore.showDocumentSelector) {
if (handler) {
event.preventDefault();
event.stopPropagation();
handler();
}
};
const handleGlobalKeydown = createKeyHandler({
Escape: () => {
if (editingId.value) {
editingId.value = null;
editingTitle.value = '';
} else if (deleteConfirmId.value) {
deleteConfirmId.value = null;
} else {
closeMenu();
}
}
});
const handleInputKeydown = createKeyHandler({
Enter: () => {
const query = inputValue.value.trim();
if (query && filteredItems.value.length > 0) {
selectItem(filteredItems.value[0]);
}
},
Escape: closeMenu
});
const handleEditKeydown = createKeyHandler({
Enter: saveEdit,
Escape: () => {
editingId.value = null;
editingTitle.value = '';
}
});
// 点击外部关闭
const handleClickOutside = (event: Event) => {
const target = event.target as HTMLElement;
if (!target.closest('.document-selector')) {
closeMenu(); closeMenu();
} else {
openMenu();
} }
}; };
// 生命周期
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleGlobalKeydown);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleGlobalKeydown);
});
// 监听菜单状态变化 // 监听菜单状态变化
watch(() => documentStore.showDocumentSelector, (isOpen) => { watch(() => documentStore.showDocumentSelector, (isOpen) => {
if (isOpen) { if (isOpen && !state.isLoaded) {
openMenu(); openMenu();
} }
}); });
</script> </script>
<template> <template>
<div class="document-selector"> <div class="document-selector" v-click-outside="closeMenu">
<!-- 选择器按钮 --> <!-- 选择器按钮 -->
<button class="doc-btn" @click="documentStore.toggleDocumentSelector"> <button class="doc-btn" @click="toggleMenu">
<span class="doc-icon"> <span class="doc-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path> <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14,2 14,8 20,8"></polyline> <polyline points="14,2 14,8 20,8"></polyline>
</svg> </svg>
</span> </span>
<span class="doc-name">{{ currentDocName }}</span> <span class="doc-name">{{ currentDocName }}</span>
<span class="arrow" :class="{ open: documentStore.showDocumentSelector }"></span> <span class="arrow" :class="{ open: state.isLoaded }"></span>
</button> </button>
<!-- 菜单 --> <!-- 菜单 -->
<div v-if="documentStore.showDocumentSelector" class="doc-menu"> <Transition name="slide-up">
<!-- 输入框 --> <div v-if="state.isLoaded" class="doc-menu">
<div class="input-box"> <!-- 输入框 -->
<input <div class="input-box">
ref="inputRef" <input
v-model="inputValue" ref="inputRef"
type="text" v-model="state.searchQuery"
class="main-input" type="text"
:placeholder="t('toolbar.searchOrCreateDocument')" class="main-input"
:maxlength="MAX_TITLE_LENGTH" :placeholder="t('toolbar.searchOrCreateDocument')"
@keydown="handleInputKeydown" :maxlength="MAX_TITLE_LENGTH"
/> @keydown.enter="handleSearchEnter"
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" @keydown.esc="closeMenu"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> />
<circle cx="11" cy="11" r="8"></circle> <svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
<path d="m21 21-4.35-4.35"></path> fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
</svg> <circle cx="11" cy="11" r="8"></circle>
</div> <path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<!-- 项目列表 --> <!-- 项目列表 -->
<div class="item-list"> <div class="item-list">
<div <div
v-for="item in filteredItems" v-for="item in filteredItems"
:key="item.id" :key="item.id"
class="list-item" class="list-item"
:class="{ :class="{
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id, 'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
'create-item': item.isCreateOption 'create-item': item.isCreateOption
}" }"
@click="selectItem(item)" @click="selectDocItem(item)"
> >
<!-- 创建选项 --> <!-- 创建选项 -->
<div v-if="item.isCreateOption" class="create-option"> <div v-if="item.isCreateOption" class="create-option">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path> <path d="M5 12h14"></path>
<path d="M12 5v14"></path> <path d="M12 5v14"></path>
</svg> </svg>
<span>{{ item.title }}</span> <span>{{ item.title }}</span>
</div>
<!-- 文档项 -->
<div v-else class="doc-item-content">
<!-- 普通显示 -->
<div v-if="editingId !== 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">{{ formatTime(item.updatedAt) }}</div>
</div> </div>
<!-- 编辑状态 --> <!-- 文档项 -->
<div v-else class="doc-edit"> <div v-else class="doc-item-content">
<input <!-- 普通显示 -->
:ref="el => editInputRef = el as HTMLInputElement" <div v-if="state.editing.id !== item.id" class="doc-info">
v-model="editingTitle" <div class="doc-title">{{ item.title }}</div>
type="text" <div class="doc-date">{{ formatDateTime(item.updated_at) }}</div>
class="edit-input" </div>
:maxlength="MAX_TITLE_LENGTH"
@keydown="handleEditKeydown"
@blur="saveEdit"
@click.stop
/>
</div>
<!-- 操作按钮 --> <!-- 编辑状态 -->
<div v-if="editingId !== item.id" class="doc-actions"> <div v-else class="doc-edit">
<!-- 只有非当前文档才显示在新窗口打开按钮 --> <input
<button :ref="el => editInputRef = el as HTMLInputElement"
v-if="documentStore.currentDocument?.id !== item.id" v-model="state.editing.title"
class="action-btn" type="text"
@click="openInNewWindow(item, $event)" class="edit-input"
:title="t('toolbar.openInNewWindow')" :maxlength="MAX_TITLE_LENGTH"
> @keydown.enter="saveEdit"
<svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" @keydown.esc="cancelEdit"
fill="currentColor"> @blur="saveEdit"
<path @click.stop
d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path> />
<path </div>
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
</svg> <!-- 操作按钮 -->
</button> <div v-if="state.editing.id !== item.id" class="doc-actions">
<button class="action-btn" @click="startRename(item, $event)" :title="t('toolbar.rename')"> <!-- 只有非当前文档才显示在新窗口打开按钮 -->
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" <button
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> v-if="documentStore.currentDocument?.id !== item.id"
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path> class="action-btn"
</svg> @click="openInNewWindow(item, $event)"
</button> :title="t('toolbar.openInNewWindow')"
<button >
v-if="documentStore.documentList.length > 1 && item.id !== 1" <svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
class="action-btn delete-btn" fill="currentColor">
:class="{ 'delete-confirm': deleteConfirmId === item.id }" <path
@click="handleDelete(item, $event)" d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path>
:title="deleteConfirmId === item.id ? t('toolbar.confirmDelete') : t('toolbar.delete')" <path
> d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
<svg v-if="deleteConfirmId !== item.id" xmlns="http://www.w3.org/2000/svg" width="12" height="12" </svg>
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" </button>
stroke-linejoin="round"> <button class="action-btn" @click="renameDoc(item, $event)" :title="t('toolbar.rename')">
<polyline points="3,6 5,6 21,6"></polyline> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
</svg> <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
<span v-else class="confirm-text">{{ t('toolbar.confirm') }}</span> </svg>
</button> </button>
<button
v-if="state.documentList.length > 1"
class="action-btn delete-btn"
:class="{ 'delete-confirm': isDeleting(item.id!) }"
@click="handleDelete(item, $event)"
:title="isDeleting(item.id!) ? t('toolbar.confirmDelete') : t('toolbar.delete')"
>
<svg v-if="!isDeleting(item.id!)" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="3,6 5,6 21,6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
<span v-else class="confirm-text">{{ t('toolbar.confirm') }}</span>
</button>
</div>
</div> </div>
</div> </div>
</div>
<!-- 空状态 --> <!-- 空状态 -->
<div v-if="filteredItems.length === 0" class="empty"> <div v-if="filteredItems.length === 0" class="empty">
{{ t('toolbar.noDocumentFound') }} {{ t('toolbar.noDocumentFound') }}
</div> </div>
<!-- 加载状态 -->
<div v-if="documentStore.isLoading" class="loading">
{{ t('toolbar.loading') }}
</div> </div>
</div> </div>
</div> </Transition>
</div> </div>
</template> </template>
<style scoped lang="scss"> <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);
}
.document-selector { .document-selector {
position: relative; position: relative;
@@ -483,8 +475,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 3px; border-radius: 3px;
margin-bottom: 4px; margin-bottom: 4px;
width: 260px; width: 340px;
max-height: calc(100vh - 40px); // 限制最大高度留出titlebar空间(32px)和一些边距 max-height: calc(100vh - 40px);
z-index: 1000; z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden; overflow: hidden;
@@ -493,7 +485,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
.input-box { .input-box {
position: relative; position: relative;
padding: 8px; padding: 10px;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
.main-input { .main-input {
@@ -502,8 +494,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
background-color: var(--bg-primary); background-color: var(--bg-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 2px; border-radius: 2px;
padding: 5px 8px 5px 26px; padding: 6px 10px 6px 30px;
font-size: 11px; font-size: 12px;
color: var(--text-primary); color: var(--text-primary);
outline: none; outline: none;
@@ -518,7 +510,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
.input-icon { .input-icon {
position: absolute; position: absolute;
left: 14px; left: 16px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: var(--text-muted); color: var(--text-muted);
@@ -527,7 +519,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
} }
.item-list { .item-list {
max-height: calc(100vh - 100px); // 为输入框和边距预留空间 max-height: calc(100vh - 100px);
overflow-y: auto; overflow-y: auto;
flex: 1; flex: 1;
@@ -539,7 +531,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
background-color: var(--bg-hover); background-color: var(--bg-hover);
} }
&.active { &.active {
background-color: var(--selection-bg); background-color: var(--selection-bg);
.doc-item-content .doc-info { .doc-item-content .doc-info {
@@ -547,7 +539,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
color: var(--selection-text); color: var(--selection-text);
} }
.doc-date, .doc-error { .doc-date {
color: var(--selection-text); color: var(--selection-text);
opacity: 0.7; opacity: 0.7;
} }
@@ -559,8 +551,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px 8px; padding: 10px 10px;
font-size: 11px; font-size: 12px;
font-weight: normal; font-weight: normal;
svg { svg {
@@ -574,15 +566,15 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 8px 8px; padding: 10px 10px;
.doc-info { .doc-info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
.doc-title { .doc-title {
font-size: 12px; font-size: 13px;
margin-bottom: 2px; margin-bottom: 3px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -590,17 +582,10 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
} }
.doc-date { .doc-date {
font-size: 10px; font-size: 11px;
color: var(--text-muted); color: var(--text-muted);
opacity: 0.6; opacity: 0.6;
} }
.doc-error {
font-size: 10px;
color: var(--text-danger);
font-weight: 500;
animation: fadeInOut 3s forwards;
}
} }
.doc-edit { .doc-edit {
@@ -612,8 +597,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
background-color: var(--bg-primary); background-color: var(--bg-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 2px; border-radius: 2px;
padding: 4px 6px; padding: 5px 8px;
font-size: 11px; font-size: 12px;
color: var(--text-primary); color: var(--text-primary);
outline: none; outline: none;
@@ -625,7 +610,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
.doc-actions { .doc-actions {
display: flex; display: flex;
gap: 6px; gap: 8px;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
@@ -634,7 +619,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
border: none; border: none;
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
padding: 4px; padding: 5px;
border-radius: 2px; border-radius: 2px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -655,7 +640,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
color: white; color: white;
.confirm-text { .confirm-text {
font-size: 9px; font-size: 10px;
font-weight: 500; font-weight: 500;
} }
} }
@@ -669,20 +654,13 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
} }
} }
.empty, .loading { .empty {
padding: 16px 8px; padding: 18px 10px;
text-align: center; text-align: center;
font-size: 11px; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);
} }
} }
} }
} }
</style>
@keyframes fadeInOut {
0% { opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; }
}
</style>

View File

@@ -3,6 +3,7 @@ import {useI18n} from 'vue-i18n';
import {computed, onMounted, onUnmounted, ref, watch, shallowRef, readonly, toRefs, effectScope, onScopeDispose} from 'vue'; import {computed, onMounted, onUnmounted, ref, watch, shallowRef, readonly, toRefs, effectScope, onScopeDispose} from 'vue';
import {useConfigStore} from '@/stores/configStore'; import {useConfigStore} from '@/stores/configStore';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
import {useEditorStateStore} from '@/stores/editorStateStore';
import {useUpdateStore} from '@/stores/updateStore'; import {useUpdateStore} from '@/stores/updateStore';
import {useWindowStore} from '@/stores/windowStore'; import {useWindowStore} from '@/stores/windowStore';
import {useSystemStore} from '@/stores/systemStore'; import {useSystemStore} from '@/stores/systemStore';
@@ -13,23 +14,19 @@ import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages'; import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages';
import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode'; import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode';
import {createDebounce} from '@/common/utils/debounce'; import {createDebounce} from '@/common/utils/debounce';
import {toggleMarkdownPreview} from '@/views/editor/extensions/markdownPreview';
import {usePanelStore} from '@/stores/panelStore';
const editorStore = readonly(useEditorStore()); const editorStore = useEditorStore();
const configStore = readonly(useConfigStore()); const editorStateStore = useEditorStateStore();
const updateStore = readonly(useUpdateStore()); const configStore = useConfigStore();
const windowStore = readonly(useWindowStore()); const updateStore = useUpdateStore();
const systemStore = readonly(useSystemStore()); const windowStore = useWindowStore();
const panelStore = readonly(usePanelStore()); const systemStore = useSystemStore();
const {t} = useI18n(); const {t} = useI18n();
const router = useRouter(); const router = useRouter();
const canFormatCurrentBlock = ref(false); const canFormatCurrentBlock = ref(false);
const canPreviewMarkdown = ref(false);
const isLoaded = shallowRef(false); const isLoaded = shallowRef(false);
const { documentStats } = toRefs(editorStore);
const { config } = toRefs(configStore); const { config } = toRefs(configStore);
// 窗口置顶状态 // 窗口置顶状态
@@ -37,10 +34,6 @@ const isCurrentWindowOnTop = computed(() => {
return config.value.general.alwaysOnTop || systemStore.isWindowOnTop; return config.value.general.alwaysOnTop || systemStore.isWindowOnTop;
}); });
// 当前文档的预览是否打开
const isCurrentBlockPreviewing = computed(() => {
return panelStore.markdownPreview.isOpen && !panelStore.markdownPreview.isClosing;
});
// 切换窗口置顶状态 // 切换窗口置顶状态
const toggleAlwaysOnTop = async () => { const toggleAlwaysOnTop = async () => {
@@ -65,26 +58,16 @@ const goToSettings = () => {
// 执行格式化 // 执行格式化
const formatCurrentBlock = () => { const formatCurrentBlock = () => {
if (!canFormatCurrentBlock.value || !editorStore.editorView) return; if (!canFormatCurrentBlock.value || !editorStore.currentEditor) return;
formatBlockContent(editorStore.editorView); formatBlockContent(editorStore.currentEditor);
}; };
// 切换 Markdown 预览
const { debouncedFn: debouncedTogglePreview } = createDebounce(() => {
if (!canPreviewMarkdown.value || !editorStore.editorView) return;
toggleMarkdownPreview(editorStore.editorView as any);
}, { delay: 200 });
const togglePreview = () => {
debouncedTogglePreview();
};
// 统一更新按钮状态 // 统一更新按钮状态
const updateButtonStates = () => { const updateButtonStates = () => {
const view: any = editorStore.editorView; const view: any = editorStore.currentEditor;
if (!view) { if (!view) {
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
canPreviewMarkdown.value = false;
return; return;
} }
@@ -95,7 +78,6 @@ const updateButtonStates = () => {
// 提前返回,减少不必要的计算 // 提前返回,减少不必要的计算
if (!activeBlock) { if (!activeBlock) {
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
canPreviewMarkdown.value = false;
return; return;
} }
@@ -103,11 +85,9 @@ const updateButtonStates = () => {
const language = getLanguage(languageName as any); const language = getLanguage(languageName as any);
canFormatCurrentBlock.value = Boolean(language?.prettier); canFormatCurrentBlock.value = Boolean(language?.prettier);
canPreviewMarkdown.value = languageName.toLowerCase() === 'md';
} catch (error) { } catch (error) {
console.warn('Error checking block capabilities:', error); console.warn('Error checking block capabilities:', error);
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
canPreviewMarkdown.value = false;
} }
}; };
@@ -146,7 +126,7 @@ const setupEditorListeners = (view: any) => {
// 监听编辑器视图变化 // 监听编辑器视图变化
watch( watch(
() => editorStore.editorView, () => editorStore.currentEditor,
(newView) => { (newView) => {
// 在 scope 中管理副作用 // 在 scope 中管理副作用
editorScope.run(() => { editorScope.run(() => {
@@ -161,7 +141,6 @@ watch(
cleanupListeners = setupEditorListeners(newView); cleanupListeners = setupEditorListeners(newView);
} else { } else {
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
canPreviewMarkdown.value = false;
} }
}); });
}, },
@@ -213,11 +192,13 @@ const updateButtonTitle = computed(() => {
}); });
// 统计数据的计算属性 // 统计数据的计算属性
const statsData = computed(() => ({ const statsData = computed(() => {
lines: documentStats.value.lines, const docId = editorStore.currentEditorId;
characters: documentStats.value.characters, if (!docId) {
selectedCharacters: documentStats.value.selectedCharacters return { lines: 0, characters: 0, selectedCharacters: 0 };
})); }
return editorStateStore.getDocumentStats(docId);
});
</script> </script>
<template> <template>
@@ -255,21 +236,6 @@ const statsData = computed(() => ({
<!-- 块语言选择器 --> <!-- 块语言选择器 -->
<BlockLanguageSelector/> <BlockLanguageSelector/>
<!-- Markdown预览按钮 -->
<div
v-if="canPreviewMarkdown"
class="preview-button"
:class="{ 'active': isCurrentBlockPreviewing }"
:title="isCurrentBlockPreviewing ? t('toolbar.closePreview') : t('toolbar.previewMarkdown')"
@click="togglePreview"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</div>
<!-- 格式化按钮 - 支持点击操作 --> <!-- 格式化按钮 - 支持点击操作 -->
<div <div
v-if="canFormatCurrentBlock" v-if="canFormatCurrentBlock"

View File

@@ -0,0 +1,5 @@
export { useConfirm } from './useConfirm';
export type { UseConfirmOptions } from './useConfirm';
export { usePolling } from './usePolling';
export type { UsePollingOptions, UsePollingReturn } from './usePolling';

View File

@@ -0,0 +1,174 @@
import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
export interface UseConfirmOptions<T extends string | number = string | number> {
/** Auto cancel timeout in ms (default: 3000, set 0 to disable) */
timeout?: number;
/** Callback when confirmed */
onConfirm?: (id: T) => void | Promise<void>;
/** Callback when cancelled (timeout or manual) */
onCancel?: (id: T) => void;
}
export interface UseConfirmReturn<T extends string | number = string | number> {
/** Current confirming id (readonly) */
confirmId: DeepReadonly<Ref<T | null>>;
/** Whether confirm action is executing */
isPending: DeepReadonly<Ref<boolean>>;
/** Check if a specific id is in confirming state */
isConfirming: (id: T) => boolean;
/** Start confirming state (with auto timeout) */
startConfirm: (id: T) => void;
/** Request confirmation (toggle between request and execute) */
requestConfirm: (id: T) => Promise<boolean>;
/** Manually confirm current id */
confirm: () => Promise<void>;
/** Cancel confirmation */
cancel: () => void;
/** Reset without triggering callbacks */
reset: () => void;
}
/**
* Composable for handling confirm actions (e.g., delete confirmation)
*
* @example
* ```ts
* // Basic usage
* const { isConfirming, requestConfirm } = useConfirm({
* timeout: 3000,
* onConfirm: async (id) => { await deleteItem(id) }
* })
*
* // In template
* <button @click="requestConfirm('delete')">
* {{ isConfirming('delete') ? 'Confirm?' : 'Delete' }}
* </button>
*
* // With loading state
* const { isPending, requestConfirm } = useConfirm({ ... })
* <button :disabled="isPending" @click="requestConfirm('id')">
* {{ isPending ? 'Processing...' : 'Delete' }}
* </button>
* ```
*/
export function useConfirm<T extends string | number = string | number>(
options: UseConfirmOptions<T> = {}
): UseConfirmReturn<T> {
const { timeout = 3000, onConfirm, onCancel } = options;
const confirmId = ref<T | null>(null) as Ref<T | null>;
const isPending = ref(false);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const clearTimer = (): void => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
/**
* Check if a specific id is in confirming state
*/
const isConfirming = (id: T): boolean => {
return confirmId.value === id;
};
/**
* Start confirming state for an id (with auto timeout)
*/
const startConfirm = (id: T): void => {
clearTimer();
confirmId.value = id;
// Auto cancel after timeout (0 = disabled)
if (timeout > 0) {
timeoutId = setTimeout(() => {
if (confirmId.value === id) {
confirmId.value = null;
onCancel?.(id);
}
}, timeout);
}
};
/**
* Request confirmation for an id
* - First click: enter confirming state
* - Second click: execute confirm action
* @returns true if confirmed, false if entered confirming state
*/
const requestConfirm = async (id: T): Promise<boolean> => {
// Prevent action while pending
if (isPending.value) return false;
if (confirmId.value === id) {
// Already confirming, execute action
clearTimer();
isPending.value = true;
try {
await onConfirm?.(id);
return true;
} finally {
confirmId.value = null;
isPending.value = false;
}
} else {
// Enter confirming state
startConfirm(id);
return false;
}
};
/**
* Manually confirm the current id
*/
const confirm = async (): Promise<void> => {
if (confirmId.value === null || isPending.value) return;
clearTimer();
const id = confirmId.value;
isPending.value = true;
try {
await onConfirm?.(id);
} finally {
confirmId.value = null;
isPending.value = false;
}
};
/**
* Cancel the confirming state
*/
const cancel = (): void => {
if (confirmId.value === null) return;
const id = confirmId.value;
clearTimer();
confirmId.value = null;
onCancel?.(id);
};
/**
* Reset state without triggering callbacks
*/
const reset = (): void => {
clearTimer();
confirmId.value = null;
isPending.value = false;
};
// Cleanup on unmount
onUnmounted(clearTimer);
return {
confirmId: readonly(confirmId),
isPending: readonly(isPending),
isConfirming,
startConfirm,
requestConfirm,
confirm,
cancel,
reset
};
}

View File

@@ -0,0 +1,147 @@
import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
export interface UsePollingOptions<T> {
/** Polling interval in ms (default: 500) */
interval?: number;
/** Execute immediately when started (default: true) */
immediate?: boolean;
/** Auto-stop condition, return true to stop polling */
shouldStop?: (data: T) => boolean;
/** Callback on each successful poll */
onSuccess?: (data: T) => void;
/** Callback when error occurs */
onError?: (error: unknown) => void;
/** Callback when polling stops (either manual or auto) */
onStop?: () => void;
}
export interface UsePollingReturn<T> {
/** Latest fetched data (readonly) */
data: DeepReadonly<Ref<T | null>>;
/** Error message if any (readonly) */
error: DeepReadonly<Ref<string>>;
/** Whether polling is active (readonly) */
isActive: DeepReadonly<Ref<boolean>>;
/** Start polling */
start: () => void;
/** Stop polling */
stop: () => void;
/** Reset all state (also stops polling) */
reset: () => void;
}
/**
* Composable for polling async operations with auto-stop support
*
* @example
* ```ts
* // Basic usage
* const { data, isActive, start, stop } = usePolling(
* () => api.getProgress(),
* {
* interval: 200,
* shouldStop: (d) => d.progress >= 100 || !!d.error,
* onSuccess: (d) => console.log('Progress:', d.progress)
* }
* )
*
* // Start polling
* start()
*
* // With reactive data binding
* <div>{{ isActive ? `${data?.progress}%` : 'Idle' }}</div>
* ```
*/
export function usePolling<T>(
fetcher: () => Promise<T>,
options: UsePollingOptions<T> = {}
): UsePollingReturn<T> {
const {
interval = 500,
immediate = true,
shouldStop,
onSuccess,
onError,
onStop
} = options;
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref('');
const isActive = ref(false);
let timerId = 0;
const clearTimer = (): void => {
if (timerId) {
clearInterval(timerId);
timerId = 0;
}
};
const poll = async (): Promise<void> => {
try {
const result = await fetcher();
data.value = result;
error.value = '';
onSuccess?.(result);
// Check auto-stop condition
if (shouldStop?.(result)) {
stop();
}
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
onError?.(e);
stop();
}
};
/**
* Start polling
*/
const start = (): void => {
if (isActive.value) return;
isActive.value = true;
error.value = '';
// Execute immediately if configured
if (immediate) {
poll();
}
timerId = window.setInterval(poll, interval);
};
/**
* Stop polling
*/
const stop = (): void => {
if (!isActive.value) return;
clearTimer();
isActive.value = false;
onStop?.();
};
/**
* Reset all state to initial values
*/
const reset = (): void => {
clearTimer();
data.value = null;
error.value = '';
isActive.value = false;
};
// Cleanup on unmount
onUnmounted(clearTimer);
return {
data: readonly(data),
error: readonly(error),
isActive: readonly(isActive),
start,
stop,
reset
};
}

View File

@@ -0,0 +1,37 @@
import type { Directive, DirectiveBinding } from 'vue';
type ClickOutsideHandler = (event: MouseEvent) => void;
interface ClickOutsideElement extends HTMLElement {
_clickOutsideHandler?: (event: MouseEvent) => void;
}
/**
* v-click-outside directive
* Triggers a callback when clicking outside the element
*
* Usage:
* <div v-click-outside="handleClickOutside">...</div>
*/
export const clickOutside: Directive<ClickOutsideElement, ClickOutsideHandler> = {
mounted(el: ClickOutsideElement, binding: DirectiveBinding<ClickOutsideHandler>) {
const handler = (event: MouseEvent) => {
const target = event.target as Node;
if (el && !el.contains(target)) {
binding.value(event);
}
};
el._clickOutsideHandler = handler;
document.addEventListener('click', handler);
},
unmounted(el: ClickOutsideElement) {
if (el._clickOutsideHandler) {
document.removeEventListener('click', el._clickOutsideHandler);
delete el._clickOutsideHandler;
}
}
};
export default clickOutside;

View File

@@ -0,0 +1,13 @@
import type { App } from 'vue';
import { clickOutside } from './clickOutside';
export { clickOutside };
/**
* Register all custom directives
*/
export function registerDirectives(app: App) {
app.directive('click-outside', clickOutside);
}
export default registerDirectives;

View File

@@ -1,5 +1,14 @@
export default { export default {
locale: 'en-US', locale: 'en-US',
common: {
ok: 'OK',
cancel: 'Cancel',
edit: 'Edit',
delete: 'Delete',
confirm: 'Confirm',
save: 'Save',
reset: 'Reset'
},
titlebar: { titlebar: {
minimize: 'Minimize', minimize: 'Minimize',
maximize: 'Maximize', maximize: 'Maximize',
@@ -33,13 +42,6 @@ export default {
confirmDelete: 'Click again to confirm delete', confirmDelete: 'Click again to confirm delete',
openInNewWindow: 'Open in New Window', openInNewWindow: 'Open in New Window',
alreadyOpenInNewWindow: 'Already open in another window', alreadyOpenInNewWindow: 'Already open in another window',
documentNameTooLong: 'Document name cannot exceed {max} characters',
documentNameRequired: 'Document name cannot be empty',
cannotDeleteLastDocument: 'Cannot delete the last document',
cannotDeleteDefaultDocument: 'Cannot delete the default document',
unknownTime: 'Unknown time',
invalidDate: 'Invalid date',
timeError: 'Time error',
}, },
languages: { languages: {
'zh-CN': 'Chinese', 'zh-CN': 'Chinese',
@@ -51,11 +53,31 @@ export default {
auto: 'Follow System' auto: 'Follow System'
}, },
keybindings: { keybindings: {
keymapMode: 'Keymap Mode',
modes: {
standard: 'Standard Mode',
emacs: 'Emacs Mode'
},
headers: { headers: {
shortcut: 'Shortcut', shortcut: 'Shortcut',
category: 'Category', extension: 'Extension',
description: 'Description' 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: { commands: {
showSearch: 'Show search panel', showSearch: 'Show search panel',
hideSearch: 'Hide search panel', hideSearch: 'Hide search panel',
@@ -83,6 +105,7 @@ export default {
blockCopy: 'Copy', blockCopy: 'Copy',
blockCut: 'Cut', blockCut: 'Cut',
blockPaste: 'Paste', blockPaste: 'Paste',
copyBlockImage: 'Copy block image',
historyUndo: 'Undo', historyUndo: 'Undo',
historyRedo: 'Redo', historyRedo: 'Redo',
historyUndoSelection: 'Undo selection', historyUndoSelection: 'Undo selection',
@@ -100,6 +123,25 @@ export default {
insertBlankLine: 'Insert blank line', insertBlankLine: 'Insert blank line',
selectLine: 'Select line', selectLine: 'Select line',
selectParentSyntax: 'Select parent syntax', 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', indentLess: 'Indent less',
indentMore: 'Indent more', indentMore: 'Indent more',
indentSelection: 'Indent selection', indentSelection: 'Indent selection',
@@ -111,7 +153,18 @@ export default {
deleteCharForward: 'Delete character forward', deleteCharForward: 'Delete character forward',
deleteGroupBackward: 'Delete group backward', deleteGroupBackward: 'Delete group backward',
deleteGroupForward: 'Delete group forward', deleteGroupForward: 'Delete group forward',
textHighlightToggle: 'Toggle text highlight',
// 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: { tabs: {
@@ -147,6 +200,7 @@ export default {
enableWindowSnap: 'Enable Window Snapping', enableWindowSnap: 'Enable Window Snapping',
enableLoadingAnimation: 'Enable Loading Animation', enableLoadingAnimation: 'Enable Loading Animation',
enableTabs: 'Enable Tabs', enableTabs: 'Enable Tabs',
enableMemoryMonitor: 'Enable Memory Monitor',
startup: 'Startup Settings', startup: 'Startup Settings',
startAtLogin: 'Start at Login', startAtLogin: 'Start at Login',
dataStorage: 'Data Storage', dataStorage: 'Data Storage',
@@ -161,53 +215,6 @@ export default {
customThemeColors: 'Custom Theme Colors', customThemeColors: 'Custom Theme Colors',
resetToDefault: 'Reset to Default', resetToDefault: 'Reset to Default',
colorValue: 'Color Value', colorValue: 'Color Value',
themeColors: {
basic: 'Basic Colors',
text: 'Text Colors',
syntax: 'Syntax Highlighting',
interface: 'Interface Elements',
border: 'Borders & Dividers',
search: 'Search & Matching',
// Base Colors
background: 'Main Background',
backgroundSecondary: 'Secondary Background',
surface: 'Panel Background',
dropdownBackground: 'Dropdown Background',
dropdownBorder: 'Dropdown Border',
// Text Colors
foreground: 'Primary Text',
foregroundSecondary: 'Secondary Text',
comment: 'Comments',
// Syntax Highlighting - Core
keyword: 'Keywords',
string: 'Strings',
function: 'Functions',
number: 'Numbers',
operator: 'Operators',
variable: 'Variables',
type: 'Types',
// Syntax Highlighting - Extended
constant: 'Constants',
storage: 'Storage Type',
parameter: 'Parameters',
class: 'Class Names',
heading: 'Headings',
invalid: 'Invalid/Error',
regexp: 'Regular Expressions',
// Interface Elements
cursor: 'Cursor',
selection: 'Selection Background',
selectionBlur: 'Unfocused Selection',
activeLine: 'Active Line Highlight',
lineNumber: 'Line Numbers',
activeLineNumber: 'Active Line Number',
// Borders & Dividers
borderColor: 'Border Color',
borderLight: 'Light Border',
// Search & Matching
searchMatch: 'Search Match',
matchingBracket: 'Matching Bracket'
},
lineHeight: 'Line Height', lineHeight: 'Line Height',
tabSettings: 'Tab Settings', tabSettings: 'Tab Settings',
tabSize: 'Tab Size', tabSize: 'Tab Size',
@@ -239,6 +246,7 @@ export default {
categoryEditing: 'Editing Enhancement', categoryEditing: 'Editing Enhancement',
categoryUI: 'UI Enhancement', categoryUI: 'UI Enhancement',
categoryTools: 'Tools', categoryTools: 'Tools',
enabled: 'Enabled',
configuration: 'Configuration', configuration: 'Configuration',
resetToDefault: 'Reset to Default Configuration', resetToDefault: 'Reset to Default Configuration',
}, },
@@ -282,14 +290,10 @@ export default {
sshKeyPassphrase: 'SSH Key Passphrase', sshKeyPassphrase: 'SSH Key Passphrase',
sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase', sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase',
backupOperations: 'Backup Operations', backupOperations: 'Backup Operations',
pushToRemote: 'Push to Remote', syncToRemote: 'Sync to Remote',
pushing: 'Pushing...', syncing: 'Syncing...',
actions: { actions: {
push: 'Push', sync: 'Sync',
},
status: {
success: 'Success',
failed: 'Failed'
} }
}, },
}, },
@@ -304,7 +308,7 @@ export default {
}, },
colorSelector: { colorSelector: {
name: 'Color Selector', name: 'Color Selector',
description: 'Visual color picker and color value display' description: 'CSS code block visual color picker and color value display'
}, },
translator: { translator: {
name: 'Text Translator', name: 'Text Translator',
@@ -322,23 +326,42 @@ export default {
name: 'Code Folding', name: 'Code Folding',
description: 'Collapse and expand code sections for better readability' description: 'Collapse and expand code sections for better readability'
}, },
textHighlight: { markdown: {
name: 'Text Highlight', name: 'Markdown Renderer',
description: 'Highlight selected text content (Ctrl+Shift+H to toggle highlight)', description: 'Render Markdown elements, "what you see is what you get"'
backgroundColor: 'Background Color',
opacity: 'Opacity'
},
checkbox: {
name: 'Checkbox',
description: 'Render [x] and [ ] as interactive checkboxes'
}, },
codeblock: { codeblock: {
name: 'Code Block', name: 'Code Block',
description: 'Code block related functionality' description: 'Code block related functionality'
},
lineNumbers: {
name: 'Line Numbers',
description: 'Display line numbers on the left side of the editor and highlight the current line'
},
contextMenu: {
name: 'Context Menu',
description: 'Show context menu when right-clicking in the editor'
},
highlightWhitespace: {
name: 'Highlight Whitespace',
description: 'Display whitespace characters such as spaces and tabs in the editor'
},
highlightTrailingWhitespace: {
name: 'Highlight Trailing Whitespace',
description: 'Highlight trailing whitespace at the end of lines'
},
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: { monitor: {
memory: 'Memory', memory: 'Memory',
clickToClean: 'Click to clean memory' clickToClean: 'Click to clean memory'
} }
}; };

Some files were not shown because too many files have changed in this diff Show More