Merge pull request #305 from ChandlerVer5/refactor-markdown

refactor(markdown): 💡 使用 markdown-it 替换 marked,获得更好的体验
main
Redon 2 years ago committed by GitHub
commit c8fa086565
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -29,10 +29,14 @@
"dockerhub",
"esno",
"GPTAPI",
"highlightjs",
"hljs",
"iconify",
"katex",
"katexmath",
"linkify",
"logprobs",
"mdhljs",
"nodata",
"OPENAI",
"pinia",
@ -40,6 +44,7 @@
"rushstack",
"Sider",
"tailwindcss",
"traptitech",
"tsup",
"Typecheck",
"unplugin",

@ -23,10 +23,11 @@
"common:prepare": "husky install"
},
"dependencies": {
"@traptitech/markdown-it-katex": "^3.6.0",
"@vueuse/core": "^9.13.0",
"highlight.js": "^11.7.0",
"katex": "^0.16.4",
"marked": "^4.2.12",
"markdown-it": "^13.0.1",
"naive-ui": "^2.34.3",
"pinia": "^2.0.32",
"vue": "^3.2.47",
@ -40,7 +41,7 @@
"@iconify/vue": "^4.1.0",
"@types/crypto-js": "^4.1.1",
"@types/katex": "^0.16.0",
"@types/marked": "^4.0.8",
"@types/markdown-it": "^12.2.3",
"@types/node": "^18.14.6",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",

@ -5,9 +5,10 @@ specifiers:
'@commitlint/cli': ^17.4.4
'@commitlint/config-conventional': ^17.4.4
'@iconify/vue': ^4.1.0
'@traptitech/markdown-it-katex': ^3.6.0
'@types/crypto-js': ^4.1.1
'@types/katex': ^0.16.0
'@types/marked': ^4.0.8
'@types/markdown-it': ^12.2.3
'@types/node': ^18.14.6
'@vitejs/plugin-vue': ^4.0.0
'@vueuse/core': ^9.13.0
@ -20,7 +21,7 @@ specifiers:
katex: ^0.16.4
less: ^4.1.3
lint-staged: ^13.1.2
marked: ^4.2.12
markdown-it: ^13.0.1
naive-ui: ^2.34.3
npm-run-all: ^4.1.5
pinia: ^2.0.32
@ -35,12 +36,13 @@ specifiers:
vue-tsc: ^1.2.0
dependencies:
'@traptitech/markdown-it-katex': 3.6.0
'@vueuse/core': 9.13.0_vue@3.2.47
highlight.js: 11.7.0
katex: 0.16.4
marked: 4.2.12
markdown-it: 13.0.1
naive-ui: 2.34.3_vue@3.2.47
pinia: 2.0.32_hmuptsblhheur2tugfgucj7gc4
pinia: 2.0.33_hmuptsblhheur2tugfgucj7gc4
vue: 3.2.47
vue-i18n: 9.2.2_vue@3.2.47
vue-router: 4.1.6_vue@3.2.47
@ -52,7 +54,7 @@ devDependencies:
'@iconify/vue': 4.1.0_vue@3.2.47
'@types/crypto-js': 4.1.1
'@types/katex': 0.16.0
'@types/marked': 4.0.8
'@types/markdown-it': 12.2.3
'@types/node': 18.14.6
'@vitejs/plugin-vue': 4.0.0_vite@4.1.4+vue@3.2.47
autoprefixer: 10.4.13_postcss@8.4.21
@ -64,7 +66,7 @@ devDependencies:
lint-staged: 13.1.2
npm-run-all: 4.1.5
postcss: 8.4.21
rimraf: 4.2.0
rimraf: 4.3.0
tailwindcss: 3.2.7_postcss@8.4.21
typescript: 4.9.5
vite: 4.1.4_4l5pdn5ozbjpiwj3fcgseihr44
@ -600,11 +602,11 @@ packages:
dev: true
optional: true
/@eslint-community/eslint-utils/4.1.2_eslint@8.35.0:
resolution: {integrity: sha512-7qELuQWWjVDdVsFQ5+beUl+KPczrEDA7S3zM4QUd/bJl7oXgsmpXaEVqrRTnOBqenOV4rWf2kVZk2Ot085zPWA==}
/@eslint-community/eslint-utils/4.2.0_eslint@8.35.0:
resolution: {integrity: sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
dependencies:
eslint: 8.35.0
eslint-visitor-keys: 3.3.0
@ -744,6 +746,12 @@ packages:
fastq: 1.15.0
dev: true
/@traptitech/markdown-it-katex/3.6.0:
resolution: {integrity: sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==}
dependencies:
katex: 0.16.4
dev: false
/@tsconfig/node10/1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
dev: true
@ -780,6 +788,10 @@ packages:
resolution: {integrity: sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==}
dev: true
/@types/linkify-it/3.0.2:
resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==}
dev: true
/@types/lodash-es/4.17.6:
resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==}
dependencies:
@ -790,8 +802,11 @@ packages:
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
dev: false
/@types/marked/4.0.8:
resolution: {integrity: sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==}
/@types/markdown-it/12.2.3:
resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==}
dependencies:
'@types/linkify-it': 3.0.2
'@types/mdurl': 1.0.2
dev: true
/@types/mdast/3.0.10:
@ -800,6 +815,10 @@ packages:
'@types/unist': 2.0.6
dev: true
/@types/mdurl/1.0.2:
resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
dev: true
/@types/minimist/1.2.2:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
dev: true
@ -1232,7 +1251,6 @@ packages:
/argparse/2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
/array-ify/1.0.0:
resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
@ -1300,7 +1318,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.21.5
caniuse-lite: 1.0.30001458
caniuse-lite: 1.0.30001460
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@ -1361,8 +1379,8 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001458
electron-to-chromium: 1.4.315
caniuse-lite: 1.0.30001460
electron-to-chromium: 1.4.320
node-releases: 2.0.10
update-browserslist-db: 1.0.10_browserslist@4.21.5
dev: true
@ -1409,8 +1427,8 @@ packages:
engines: {node: '>=6'}
dev: true
/caniuse-lite/1.0.30001458:
resolution: {integrity: sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==}
/caniuse-lite/1.0.30001460:
resolution: {integrity: sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==}
dev: true
/chalk/2.4.2:
@ -1829,8 +1847,8 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/electron-to-chromium/1.4.315:
resolution: {integrity: sha512-ndBQYz3Eyy3rASjjQ9poMJGoAlsZ/aZnq6GBsGL4w/4sWIAwiUHVSsMuADbxa8WJw7pZ0oxLpGbtoDt4vRTdCg==}
/electron-to-chromium/1.4.320:
resolution: {integrity: sha512-h70iRscrNluMZPVICXYl5SSB+rBKo22XfuIS1ER0OQxQZpKTnFpuS6coj7wY9M/3trv7OR88rRMOlKmRvDty7Q==}
dev: true
/emoji-regex/8.0.0:
@ -1841,6 +1859,11 @@ packages:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
dev: true
/entities/3.0.1:
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
engines: {node: '>=0.12'}
dev: false
/entities/4.4.0:
resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==}
engines: {node: '>=0.12'}
@ -2162,7 +2185,7 @@ packages:
eslint: '>=8.28.0'
dependencies:
'@babel/helper-validator-identifier': 7.19.1
'@eslint-community/eslint-utils': 4.1.2_eslint@8.35.0
'@eslint-community/eslint-utils': 4.2.0_eslint@8.35.0
ci-info: 3.8.0
clean-regexp: 1.0.0
eslint: 8.35.0
@ -3144,10 +3167,21 @@ packages:
engines: {node: '>=10'}
dev: true
/lilconfig/2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
dev: true
/lines-and-columns/1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
/linkify-it/4.0.1:
resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==}
dependencies:
uc.micro: 1.0.6
dev: false
/lint-staged/13.1.2:
resolution: {integrity: sha512-K9b4FPbWkpnupvK3WXZLbgu9pchUJ6N7TtVZjbaPsoizkqFUDkUReUL25xdrCljJs7uLUF3tZ7nVPeo/6lp+6w==}
engines: {node: ^14.13.1 || >=16.0.0}
@ -3283,8 +3317,8 @@ packages:
yallist: 4.0.0
dev: true
/lru-cache/7.18.1:
resolution: {integrity: sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg==}
/lru-cache/7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
dev: true
@ -3317,10 +3351,15 @@ packages:
engines: {node: '>=8'}
dev: true
/marked/4.2.12:
resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==}
engines: {node: '>= 12'}
/markdown-it/13.0.1:
resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==}
hasBin: true
dependencies:
argparse: 2.0.1
entities: 3.0.1
linkify-it: 4.0.1
mdurl: 1.0.1
uc.micro: 1.0.6
dev: false
/mdast-util-from-markdown/0.8.5:
@ -3339,6 +3378,10 @@ packages:
resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==}
dev: true
/mdurl/1.0.1:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
dev: false
/memorystream/0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'}
@ -3777,7 +3820,7 @@ packages:
resolution: {integrity: sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA==}
engines: {node: '>=14'}
dependencies:
lru-cache: 7.18.1
lru-cache: 7.18.3
minipass: 4.2.4
dev: true
@ -3829,8 +3872,8 @@ packages:
dev: true
optional: true
/pinia/2.0.32_hmuptsblhheur2tugfgucj7gc4:
resolution: {integrity: sha512-8Tw4OrpCSJ028UUyp0gYPP/wyjigLoEceuO/x1G+FlHVf73337e5vLm4uDmrRIoBG1hvaed/eSHnrCFjOc4nkA==}
/pinia/2.0.33_hmuptsblhheur2tugfgucj7gc4:
resolution: {integrity: sha512-HOj1yVV2itw6rNIrR2f7+MirGNxhORjrULL8GWgRwXsGSvEqIQ+SE0MYt6cwtpegzCda3i+rVTZM+AM7CG+kRg==}
peerDependencies:
'@vue/composition-api': ^1.4.0
typescript: '>=4.4.4'
@ -3886,7 +3929,7 @@ packages:
ts-node:
optional: true
dependencies:
lilconfig: 2.0.6
lilconfig: 2.1.0
postcss: 8.4.21
yaml: 1.10.2
dev: true
@ -4103,8 +4146,8 @@ packages:
glob: 7.2.3
dev: true
/rimraf/4.2.0:
resolution: {integrity: sha512-tPt+gLORNVqRCk0NwuJ5SlMEcOGvt4CCU8sUPqgCFtCbnoNCTd9Q6vq7JlBbxQlACiH14OR28y7piA2Bak9Sxw==}
/rimraf/4.3.0:
resolution: {integrity: sha512-5qVDXPbByA1qSJEWMv1qAwKsoS22vVpsL2QyxCKBw4gf6XiFo1K3uRLY6uSOOBFDwnqAZtnbILqWKKlzh8bkGg==}
engines: {node: '>=14'}
hasBin: true
dependencies:
@ -4422,7 +4465,7 @@ packages:
fast-glob: 3.2.12
glob-parent: 6.0.2
is-glob: 4.0.3
lilconfig: 2.0.6
lilconfig: 2.1.0
micromatch: 4.0.5
normalize-path: 3.0.0
object-hash: 3.0.0
@ -4582,6 +4625,10 @@ packages:
engines: {node: '>=4.2.0'}
hasBin: true
/uc.micro/1.0.6:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
dev: false
/unbox-primitive/1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies:

@ -1,23 +0,0 @@
import type { App, Directive } from 'vue'
import hljs from 'highlight.js'
import { includeCode } from '@/utils/format'
hljs.configure({ ignoreUnescapedHTML: true })
function highlightCode(el: HTMLElement) {
if (includeCode(el.textContent))
hljs.highlightBlock(el)
}
export default function setupHighlightDirective(app: App) {
const highLightDirective: Directive<HTMLElement> = {
mounted(el: HTMLElement) {
highlightCode(el)
},
updated(el: HTMLElement) {
highlightCode(el)
},
}
app.directive('highlight', highLightDirective)
}

@ -1,6 +0,0 @@
import type { App } from 'vue'
import setupHighlightDirective from './highlight'
export function setupDirectives(app: App) {
setupHighlightDirective(app)
}

@ -1,6 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import { setupDirectives } from './directives'
import { setupI18n } from './locales'
import { setupAssets } from './plugins'
import { setupStore } from './store'
@ -12,8 +11,6 @@ async function bootstrap() {
setupStore(app)
setupDirectives(app)
setupI18n(app)
await setupRouter(app)

@ -1,10 +1,9 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import katex from 'katex'
import { marked } from 'marked'
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import hljs from 'highlight.js'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { encodeHTML } from '@/utils/format'
import { t } from '@/locales'
interface Props {
@ -18,77 +17,21 @@ const props = defineProps<Props>()
const { isMobile } = useBasicLayout()
const renderer = new marked.Renderer()
const textRef = ref<HTMLElement>()
renderer.html = (html) => {
return `<p>${encodeHTML(html)}</p>`
}
renderer.code = (code, language) => {
const validLang = !!(language && hljs.getLanguage(language))
if (validLang) {
const lang = language ?? ''
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${language}">${hljs.highlight(code, { language: lang }).value}</code></pre>`
}
return `<pre style="background: none">${hljs.highlightAuto(code).value}</pre>`
}
marked.setOptions({
renderer,
highlight(code) {
return hljs.highlightAuto(code).value
},
})
const katexOptions = {
throwOnError: false,
}
const katexInline = {
name: 'katexInline',
level: 'inline',
start(src: string) {
return src.indexOf('$')
},
tokenizer(src: string) {
const match = src.match(/^\$+([^$\n]+?)\$+/)
if (match) {
return {
type: 'katexInline',
raw: match[0],
text: match[1].trim(),
}
}
},
renderer(token: marked.Tokens.Generic) {
return katex.renderToString(token.text, katexOptions)
},
}
const katexBlock = {
name: 'katexBlock',
level: 'block',
start(src: string) {
return src.indexOf('\n$$')
},
tokenizer(src: string) {
const match = src.match(/^\$\$+\n([^$]+?)\n\$\$+\n/)
if (match) {
return {
type: 'katexBlock',
raw: match[0],
text: match[1].trim(),
}
const mdi = new MarkdownIt({
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language))
if (validLang) {
const lang = language ?? ''
return highlightBlock(hljs.highlight(lang, code, true).value, lang)
}
return highlightBlock(hljs.highlightAuto(code).value, '')
},
renderer(token: marked.Tokens.Generic) {
return `<p>${katex.renderToString(token.text, katexOptions)}</p>`
},
}
})
marked.use({ extensions: [katexInline, katexBlock] })
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
const wrapClass = computed(() => {
return [
@ -105,10 +48,14 @@ const wrapClass = computed(() => {
const text = computed(() => {
const value = props.text ?? ''
if (!props.inversion)
return marked(value)
return mdi.render(value)
return value
})
function highlightBlock(str: string, lang?: string) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
}
defineExpose({ textRef })
</script>

@ -1,4 +1,5 @@
import { onMounted, onUpdated } from 'vue'
import { copyText } from '@/utils/format'
export function useCopyCode() {
function copyCodeBlock() {
@ -8,7 +9,10 @@ export function useCopyCode() {
const codeBlock = wrapper.querySelector('.code-block-body')
if (copyBtn && codeBlock) {
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(codeBlock.textContent ?? '')
if (navigator.clipboard?.writeText)
navigator.clipboard.writeText(codeBlock.textContent ?? '')
else
copyText({ text: codeBlock.textContent ?? '', origin: true })
})
}
})

@ -16,7 +16,7 @@ async function handleSelect({ uuid }: Chat.History) {
if (isActive(uuid))
return
if(chatStore.active)
if (chatStore.active)
chatStore.updateHistory(chatStore.active, { isEdit: false })
await chatStore.setActive(uuid)

Loading…
Cancel
Save