@esershnr/artalk-sidebar 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/auto-imports.d.ts +76 -0
- package/components.d.ts +31 -0
- package/env.d.ts +2 -0
- package/index.html +15 -0
- package/package.json +32 -0
- package/public/favicon.png +0 -0
- package/public/robots.txt +2 -0
- package/src/App.vue +89 -0
- package/src/artalk.ts +82 -0
- package/src/assets/favicon.png +0 -0
- package/src/assets/icon-darkmode-off.svg +1 -0
- package/src/assets/icon-darkmode-on.svg +1 -0
- package/src/assets/icon-eye-off.svg +1 -0
- package/src/assets/icon-eye-on.svg +1 -0
- package/src/assets/nav-icon-comments.svg +1 -0
- package/src/assets/nav-icon-pages.svg +1 -0
- package/src/assets/nav-icon-search.svg +1 -0
- package/src/assets/nav-icon-settings.svg +1 -0
- package/src/assets/nav-icon-sites.svg +1 -0
- package/src/assets/nav-icon-transfer.svg +1 -0
- package/src/assets/nav-icon-users.svg +1 -0
- package/src/components/AppHeader.vue +235 -0
- package/src/components/AppNavigation.vue +11 -0
- package/src/components/AppNavigationDesktop.vue +176 -0
- package/src/components/AppNavigationMenu.ts +152 -0
- package/src/components/AppNavigationMobile.vue +187 -0
- package/src/components/AppNavigationSearch.vue +137 -0
- package/src/components/FileUploader.vue +149 -0
- package/src/components/ItemTextEditor.vue +130 -0
- package/src/components/LoadingLayer.vue +37 -0
- package/src/components/LogTerminal.vue +89 -0
- package/src/components/PageEditor.vue +171 -0
- package/src/components/Pagination.vue +253 -0
- package/src/components/PreferenceArr.vue +105 -0
- package/src/components/PreferenceGrp.vue +153 -0
- package/src/components/PreferenceItem.vue +159 -0
- package/src/components/SiteCreate.vue +96 -0
- package/src/components/SiteEditor.vue +138 -0
- package/src/components/SiteSwitcher.vue +184 -0
- package/src/components/UserEditor.vue +229 -0
- package/src/global.ts +62 -0
- package/src/hooks/MobileWidth.ts +27 -0
- package/src/i18n/fr.ts +103 -0
- package/src/i18n/ja.ts +100 -0
- package/src/i18n/ko.ts +99 -0
- package/src/i18n/ru.ts +102 -0
- package/src/i18n/tr.ts +102 -0
- package/src/i18n/zh-CN.ts +97 -0
- package/src/i18n/zh-TW.ts +97 -0
- package/src/i18n-en.ts +99 -0
- package/src/i18n.ts +37 -0
- package/src/lib/promise-polyfill.ts +9 -0
- package/src/lib/settings-option.ts +186 -0
- package/src/lib/settings-sensitive.ts +44 -0
- package/src/lib/settings.ts +94 -0
- package/src/main.ts +65 -0
- package/src/pages/comments.vue +110 -0
- package/src/pages/index.vue +33 -0
- package/src/pages/login.vue +245 -0
- package/src/pages/pages.vue +309 -0
- package/src/pages/settings.vue +181 -0
- package/src/pages/sites.vue +353 -0
- package/src/pages/transfer.vue +204 -0
- package/src/pages/users.vue +271 -0
- package/src/stores/nav.ts +114 -0
- package/src/stores/user.ts +48 -0
- package/src/style/_extends.scss +100 -0
- package/src/style/_variables.scss +18 -0
- package/src/style.scss +245 -0
- package/src/vue-i18n.d.ts +7 -0
- package/tsconfig.json +40 -0
- package/tsconfig.node.json +11 -0
- package/typed-router.d.ts +30 -0
- package/vite.config.ts +71 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { MessageSchema } from '../i18n'
|
|
2
|
+
|
|
3
|
+
export const zhTW: MessageSchema = {
|
|
4
|
+
ctrlCenter: '控制中心',
|
|
5
|
+
msgCenter: '訊息中心',
|
|
6
|
+
noContent: '無內容',
|
|
7
|
+
searchHint: '關鍵字搜尋...',
|
|
8
|
+
allSites: '所有網站',
|
|
9
|
+
siteManage: '網站管理',
|
|
10
|
+
comment: '評論',
|
|
11
|
+
page: '頁面',
|
|
12
|
+
user: '用戶',
|
|
13
|
+
site: '網站',
|
|
14
|
+
transfer: '轉移',
|
|
15
|
+
settings: '設置',
|
|
16
|
+
all: '全部',
|
|
17
|
+
pending: '待審',
|
|
18
|
+
personal: '個人',
|
|
19
|
+
mentions: '提及',
|
|
20
|
+
mine: '我的',
|
|
21
|
+
admin: '管理員',
|
|
22
|
+
create: '建立',
|
|
23
|
+
import: '匯入',
|
|
24
|
+
export: '匯出',
|
|
25
|
+
settingSaved: '設定已儲存',
|
|
26
|
+
settingSaveFailed: '設定儲存失敗',
|
|
27
|
+
settingNotice: '注:某些設定選項可能需要手動重啟才能生效',
|
|
28
|
+
apply: '套用',
|
|
29
|
+
updateComplete: '更新完畢',
|
|
30
|
+
updateReady: '準備更新...',
|
|
31
|
+
opFailed: '操作失敗',
|
|
32
|
+
updateTitle: '擷取標題',
|
|
33
|
+
uploading: '上傳中',
|
|
34
|
+
cancel: '取消',
|
|
35
|
+
back: '返回',
|
|
36
|
+
cacheClear: '清除快取',
|
|
37
|
+
cacheWarm: '預熱快取',
|
|
38
|
+
editTitle: '編輯標題',
|
|
39
|
+
switchKey: 'KEY 變更',
|
|
40
|
+
commentAllowAll: '允許任何人評論',
|
|
41
|
+
commentOnlyAdmin: '僅允許管理員評論',
|
|
42
|
+
config: '配置文件',
|
|
43
|
+
envVarControlHint: '由環境變數 {key} 參照',
|
|
44
|
+
userAdminHint: '該用戶具有管理員權限',
|
|
45
|
+
userInConfHint: '該用戶存在於配置文件中',
|
|
46
|
+
userInConfCannotEditHint: '暫不支持線上編輯配置文件中的用戶,請手動修改配置文件',
|
|
47
|
+
userDeleteConfirm:
|
|
48
|
+
'該操作將刪除 用戶:"{name}" 郵箱:"{email}" 所有評論,包括其評論下面他人的回覆評論,是否繼續?',
|
|
49
|
+
userDeleteManuallyHint: '用戶已從數據庫刪除,請手動編輯配置文件並刪除用戶',
|
|
50
|
+
pageDeleteConfirm: '確認刪除頁面 "{title}"?將會刪除所有相關數據',
|
|
51
|
+
siteDeleteConfirm: '該操作將刪除網站:"{name}" 及其下所有數據,是否繼續?',
|
|
52
|
+
siteNameInputHint: '請輸入網站名稱',
|
|
53
|
+
edit: '編輯',
|
|
54
|
+
delete: '刪除',
|
|
55
|
+
siteCount: '共 {count} 個網站',
|
|
56
|
+
createSite: '建立網站',
|
|
57
|
+
siteName: '網站名稱',
|
|
58
|
+
siteUrls: '網站 URLs',
|
|
59
|
+
multiSepHint: '使用逗號分隔多個',
|
|
60
|
+
add: '新增',
|
|
61
|
+
rename: '重命名',
|
|
62
|
+
inputHint: '輸入文字...',
|
|
63
|
+
userCreate: '建立用戶',
|
|
64
|
+
userEdit: '用戶編輯',
|
|
65
|
+
comments: '評論',
|
|
66
|
+
last: '最後',
|
|
67
|
+
show: '展開',
|
|
68
|
+
username: '用戶名',
|
|
69
|
+
email: '郵箱',
|
|
70
|
+
link: '連結',
|
|
71
|
+
badgeText: '徽章文字',
|
|
72
|
+
badgeColor: '徽章顏色',
|
|
73
|
+
role: '身份角色',
|
|
74
|
+
normal: '一般',
|
|
75
|
+
password: '密碼',
|
|
76
|
+
passwordEmptyHint: '留空表示不變更密碼',
|
|
77
|
+
emailNotify: '郵件通知',
|
|
78
|
+
enabled: '啟用',
|
|
79
|
+
disabled: '停用',
|
|
80
|
+
save: '儲存',
|
|
81
|
+
dataFile: '資料檔案',
|
|
82
|
+
artransfer: '轉移工具',
|
|
83
|
+
targetSiteName: '目標網站名稱',
|
|
84
|
+
targetSiteURL: '目標網站 URL',
|
|
85
|
+
payload: '有效載荷',
|
|
86
|
+
optional: '選填',
|
|
87
|
+
uploadReadyToImport: '檔案已上傳並準備匯入',
|
|
88
|
+
artransferToolHint: '使用 {link} 將評論數據轉換為 Artrans 格式。',
|
|
89
|
+
moreDetails: '查看詳情',
|
|
90
|
+
loginFailure: '登入失敗',
|
|
91
|
+
login: '登入',
|
|
92
|
+
logout: '登出',
|
|
93
|
+
logoutConfirm: '確定要登出嗎?',
|
|
94
|
+
loginSelectHint: '請選擇您要登入的帳號:',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default zhTW
|
package/src/i18n-en.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const en = {
|
|
2
|
+
ctrlCenter: 'Admin',
|
|
3
|
+
msgCenter: 'Messages',
|
|
4
|
+
noContent: 'No Content',
|
|
5
|
+
searchHint: 'Search by keywords...',
|
|
6
|
+
allSites: 'All Sites',
|
|
7
|
+
siteManage: 'Site Management',
|
|
8
|
+
comment: 'Comment',
|
|
9
|
+
page: 'Page',
|
|
10
|
+
user: 'User',
|
|
11
|
+
site: 'Site',
|
|
12
|
+
transfer: 'Transfer',
|
|
13
|
+
settings: 'Settings',
|
|
14
|
+
all: 'All',
|
|
15
|
+
pending: 'Pending',
|
|
16
|
+
personal: 'Personal',
|
|
17
|
+
mentions: 'Mentions',
|
|
18
|
+
mine: 'Mine',
|
|
19
|
+
admin: 'Admin',
|
|
20
|
+
create: 'Create',
|
|
21
|
+
import: 'Import',
|
|
22
|
+
export: 'Export',
|
|
23
|
+
settingSaved: 'Setting saved',
|
|
24
|
+
settingSaveFailed: 'Setting save failed',
|
|
25
|
+
settingNotice: 'Note: Some config options may require a manual reboot to take effect.',
|
|
26
|
+
apply: 'Apply',
|
|
27
|
+
updateComplete: 'Update complete',
|
|
28
|
+
updateReady: 'Ready to update...',
|
|
29
|
+
opFailed: 'Operation failed',
|
|
30
|
+
updateTitle: 'Fetch Title',
|
|
31
|
+
uploading: 'Uploading',
|
|
32
|
+
cancel: 'Cancel',
|
|
33
|
+
back: 'Back',
|
|
34
|
+
cacheClear: 'Cache Clear',
|
|
35
|
+
cacheWarm: 'Cache Warm',
|
|
36
|
+
editTitle: 'Edit Title',
|
|
37
|
+
switchKey: 'Switch Key',
|
|
38
|
+
commentAllowAll: 'Anyone Comment',
|
|
39
|
+
commentOnlyAdmin: 'Admin Comment Only',
|
|
40
|
+
config: 'Config',
|
|
41
|
+
envVarControlHint: 'Referenced by the environment variable {key}',
|
|
42
|
+
userAdminHint: 'Admin user',
|
|
43
|
+
userInConfHint: 'This user is defined in config file',
|
|
44
|
+
edit: 'Edit',
|
|
45
|
+
delete: 'Delete',
|
|
46
|
+
siteCount: 'Total {count} Sites',
|
|
47
|
+
createSite: 'Create Site',
|
|
48
|
+
siteName: 'Site Name',
|
|
49
|
+
siteUrls: 'Site URLs',
|
|
50
|
+
multiSepHint: 'multiple separated by commas',
|
|
51
|
+
add: 'Add',
|
|
52
|
+
rename: 'Rename',
|
|
53
|
+
inputHint: 'Input text...',
|
|
54
|
+
userCreate: 'Create User',
|
|
55
|
+
userEdit: 'Edit User',
|
|
56
|
+
userInConfCannotEditHint:
|
|
57
|
+
'Cannot edit user in config file, please modify the config file manually',
|
|
58
|
+
userDeleteConfirm:
|
|
59
|
+
'This operation will delete all comments of user: "{name}" email: "{email}", including the reply comments under his comments. Continue?',
|
|
60
|
+
userDeleteManuallyHint:
|
|
61
|
+
'User has been deleted from the database, please manually edit the config file and delete the user',
|
|
62
|
+
pageDeleteConfirm:
|
|
63
|
+
'This operation will delete the page: "{title}" and all data under it. Continue?',
|
|
64
|
+
siteDeleteConfirm:
|
|
65
|
+
'This operation will delete the site: "{name}" and all data under it. Continue?',
|
|
66
|
+
siteNameInputHint: 'Please enter the site name',
|
|
67
|
+
comments: 'Comments',
|
|
68
|
+
last: 'Last',
|
|
69
|
+
show: 'Show',
|
|
70
|
+
username: 'Username',
|
|
71
|
+
email: 'Email',
|
|
72
|
+
link: 'Link',
|
|
73
|
+
badgeText: 'Badge Text',
|
|
74
|
+
badgeColor: 'Badge Color',
|
|
75
|
+
role: 'Role',
|
|
76
|
+
normal: 'Normal',
|
|
77
|
+
password: 'Password',
|
|
78
|
+
passwordEmptyHint: 'leave blank not change your password',
|
|
79
|
+
emailNotify: 'Email notification',
|
|
80
|
+
enabled: 'Enabled',
|
|
81
|
+
disabled: 'Disabled',
|
|
82
|
+
save: 'Save',
|
|
83
|
+
dataFile: 'Data File',
|
|
84
|
+
artransfer: 'Artransfer',
|
|
85
|
+
targetSiteName: 'Target Site Name',
|
|
86
|
+
targetSiteURL: 'Target Site URL',
|
|
87
|
+
payload: 'Payload',
|
|
88
|
+
optional: 'Optional',
|
|
89
|
+
uploadReadyToImport: 'File uploaded and is ready for import',
|
|
90
|
+
artransferToolHint: 'Use the {link} to convert data to Artrans format.',
|
|
91
|
+
moreDetails: 'More details',
|
|
92
|
+
loginFailure: 'Login failure',
|
|
93
|
+
login: 'Login',
|
|
94
|
+
logout: 'Logout',
|
|
95
|
+
logoutConfirm: 'Are you sure you want to log out?',
|
|
96
|
+
loginSelectHint: 'Please select the account you wish to log into:',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default en
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createI18n, type I18n, type Locale } from 'vue-i18n'
|
|
2
|
+
import { en } from './i18n-en'
|
|
3
|
+
|
|
4
|
+
export type MessageSchema = typeof en
|
|
5
|
+
|
|
6
|
+
export function setupI18n() {
|
|
7
|
+
const i18n = createI18n({
|
|
8
|
+
legacy: false, // use i18n in Composition API
|
|
9
|
+
locale: 'en',
|
|
10
|
+
fallbackLocale: 'en',
|
|
11
|
+
messages: { en } as any,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const setLocale = async (value: string) => {
|
|
15
|
+
await loadLocaleMessages(i18n, value)
|
|
16
|
+
i18n.global.locale.value = value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { i18n, setLocale }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function loadLocaleMessages(i18n: I18n, locale: Locale) {
|
|
23
|
+
if (i18n.global.availableLocales.includes(locale)) return
|
|
24
|
+
|
|
25
|
+
// Load locale messages with dynamic import
|
|
26
|
+
// @see https://vitejs.dev/guide/features#dynamic-import
|
|
27
|
+
const messages = await import(`./i18n/${locale}.ts`)
|
|
28
|
+
.then((r: any) => r.default || r)
|
|
29
|
+
.catch(() => {
|
|
30
|
+
console.error(`Failed to load locale messages for "${locale}"`)
|
|
31
|
+
return
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Set locale and locale message
|
|
35
|
+
i18n.global.setLocaleMessage(locale, messages)
|
|
36
|
+
return nextTick()
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Promise.withResolvers ??= function <T>() {
|
|
2
|
+
let resolve: PromiseWithResolvers<T>['resolve']
|
|
3
|
+
let reject: PromiseWithResolvers<T>['reject']
|
|
4
|
+
const promise = new Promise<T>((res, rej) => {
|
|
5
|
+
resolve = res
|
|
6
|
+
reject = rej
|
|
7
|
+
})
|
|
8
|
+
return { promise, resolve: resolve!, reject: reject! }
|
|
9
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import YAML from 'yaml'
|
|
2
|
+
type Pair = YAML.Pair<YAML.Scalar<any>, YAML.Scalar<any> & YAML.YAMLMap<any, any>>
|
|
3
|
+
|
|
4
|
+
export interface OptionNode {
|
|
5
|
+
name: string
|
|
6
|
+
path: string
|
|
7
|
+
level: number
|
|
8
|
+
default?: string | number | boolean
|
|
9
|
+
selector?: string[]
|
|
10
|
+
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
|
|
11
|
+
title: string
|
|
12
|
+
subTitle?: string
|
|
13
|
+
items?: OptionNode[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractItemComment(item: Pair, index: number, parentPair?: Pair): string {
|
|
17
|
+
let comment = ''
|
|
18
|
+
if (index === 0 && parentPair) comment = parentPair?.value?.commentBefore || ''
|
|
19
|
+
else comment = item?.key?.commentBefore || ''
|
|
20
|
+
return comment
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getTree(yamlObj: YAML.Document.Parsed): OptionNode {
|
|
24
|
+
const tree: OptionNode = {
|
|
25
|
+
name: '',
|
|
26
|
+
path: '',
|
|
27
|
+
title: '',
|
|
28
|
+
level: 0,
|
|
29
|
+
type: 'object',
|
|
30
|
+
items: [],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const traverse = (
|
|
34
|
+
pairs: Pair[],
|
|
35
|
+
parentNode: OptionNode = tree,
|
|
36
|
+
parentPath: string[] = [],
|
|
37
|
+
parentPair?: Pair,
|
|
38
|
+
) => {
|
|
39
|
+
pairs.forEach((item, index) => {
|
|
40
|
+
// get key and value
|
|
41
|
+
const key = item.key?.value
|
|
42
|
+
const value = item.value?.toJSON ? item.value.toJSON() : undefined
|
|
43
|
+
if (!key) return
|
|
44
|
+
|
|
45
|
+
// get path
|
|
46
|
+
const path = [...parentPath, key]
|
|
47
|
+
|
|
48
|
+
// get comment
|
|
49
|
+
const comment = extractItemComment(item, index, parentPair)
|
|
50
|
+
|
|
51
|
+
// get type
|
|
52
|
+
const probablyTypes = ['string', 'number', 'boolean', 'object']
|
|
53
|
+
const type =
|
|
54
|
+
(Array.isArray(value) ? 'array' : probablyTypes.find((t) => typeof value === t)) ||
|
|
55
|
+
undefined
|
|
56
|
+
|
|
57
|
+
if (!type) return
|
|
58
|
+
|
|
59
|
+
// get default value
|
|
60
|
+
const defaultValue = type !== 'object' ? value : undefined
|
|
61
|
+
|
|
62
|
+
// create new node
|
|
63
|
+
const node: OptionNode = {
|
|
64
|
+
name: key,
|
|
65
|
+
path: path.join('.'),
|
|
66
|
+
level: parentNode ? parentNode.level + 1 : 0,
|
|
67
|
+
...extractComment(key, comment),
|
|
68
|
+
default: defaultValue,
|
|
69
|
+
type: type as any,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// traverse children
|
|
73
|
+
if (type === 'object' && item.value?.items) {
|
|
74
|
+
node.items = []
|
|
75
|
+
traverse(item.value.items, node, path, item)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// add to parent
|
|
79
|
+
if (!parentNode.items) parentNode.items = []
|
|
80
|
+
parentNode.items.push(node)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
traverse((yamlObj.contents as YAML.YAMLMap<any, any>)?.items)
|
|
85
|
+
|
|
86
|
+
return tree
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get flatten meta data from yaml object
|
|
91
|
+
*
|
|
92
|
+
* @param yamlObj
|
|
93
|
+
* @returns
|
|
94
|
+
*/
|
|
95
|
+
export function getFlattenNodes(tree: OptionNode): {
|
|
96
|
+
[path: string]: OptionNode
|
|
97
|
+
} {
|
|
98
|
+
const metas: { [path: string]: OptionNode } = {}
|
|
99
|
+
|
|
100
|
+
const traverse = (node: OptionNode) => {
|
|
101
|
+
metas[node.path] = node
|
|
102
|
+
if (node.items) node.items.forEach(traverse)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
traverse(tree)
|
|
106
|
+
|
|
107
|
+
return metas
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extract option info from comment
|
|
112
|
+
*
|
|
113
|
+
* @param name Option name
|
|
114
|
+
* @param comment Option comment in YAML
|
|
115
|
+
* @returns Option info
|
|
116
|
+
*/
|
|
117
|
+
function extractComment(name: string, comment: string) {
|
|
118
|
+
comment = comment.trim()
|
|
119
|
+
|
|
120
|
+
// ignore comments begin and end with `--`
|
|
121
|
+
comment = comment.replace(/--(.*?)--/gm, '')
|
|
122
|
+
|
|
123
|
+
let title = ''
|
|
124
|
+
let subTitle = ''
|
|
125
|
+
let selector: string[] | undefined
|
|
126
|
+
|
|
127
|
+
const stReg = /\(.*?\)/gm
|
|
128
|
+
title = comment.replace(stReg, '').trim()
|
|
129
|
+
const stFind = stReg.exec(comment)
|
|
130
|
+
subTitle = stFind ? stFind[0].substring(1, stFind[0].length - 1) : ''
|
|
131
|
+
if (!title) {
|
|
132
|
+
title = snakeToCamel(name)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const optReg = /\[.*?\]/gm
|
|
136
|
+
const optFind = optReg.exec(title)
|
|
137
|
+
if (optFind) {
|
|
138
|
+
try {
|
|
139
|
+
selector = JSON.parse(optFind[0])
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error(err)
|
|
142
|
+
}
|
|
143
|
+
title = title.replace(optReg, '').trim()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
title,
|
|
148
|
+
subTitle,
|
|
149
|
+
selector,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function snakeToCamel(str: string) {
|
|
154
|
+
return str.toLowerCase().replace(/([_][a-z]|^[a-z])/g, (group) => group.slice(-1).toUpperCase())
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Patch the option value by meta data
|
|
159
|
+
*
|
|
160
|
+
* @param value User custom value
|
|
161
|
+
* @param meta Option meta data
|
|
162
|
+
* @returns Patched value
|
|
163
|
+
*/
|
|
164
|
+
export function patchOptionValue(value: any, node: OptionNode) {
|
|
165
|
+
// console.log(value, node)
|
|
166
|
+
switch (node.type) {
|
|
167
|
+
case 'boolean':
|
|
168
|
+
if (value === 'true') value = true
|
|
169
|
+
else if (value === 'false') value = false
|
|
170
|
+
break
|
|
171
|
+
case 'string':
|
|
172
|
+
if (!node.selector)
|
|
173
|
+
// ignore option item
|
|
174
|
+
value = String(value).trim()
|
|
175
|
+
break
|
|
176
|
+
case 'number':
|
|
177
|
+
if (!isNaN(Number(value))) value = Number(value)
|
|
178
|
+
break
|
|
179
|
+
case 'array':
|
|
180
|
+
// trim string array
|
|
181
|
+
if (Array.isArray(value)) value = value.map((v) => (typeof v === 'string' ? v.trim() : v))
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return value
|
|
186
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List of sensitive config paths.
|
|
3
|
+
*
|
|
4
|
+
* (which should be hidden in the UI)
|
|
5
|
+
*/
|
|
6
|
+
export const SensitiveConfigPaths = [
|
|
7
|
+
'app_key',
|
|
8
|
+
'admin_notify.ding_talk.secret',
|
|
9
|
+
'admin_notify.line.channel_access_token',
|
|
10
|
+
'admin_notify.line.channel_secret',
|
|
11
|
+
'admin_notify.lark.webhook_url',
|
|
12
|
+
'admin_notify.slack.oauth_token',
|
|
13
|
+
'admin_notify.telegram.api_token',
|
|
14
|
+
'admin_notify.webhook.url',
|
|
15
|
+
'admin.notify.bark.server',
|
|
16
|
+
'auth.apple.client_secret',
|
|
17
|
+
'auth.auth0.client_secret',
|
|
18
|
+
'auth.discord.client_secret',
|
|
19
|
+
'auth.facebook.client_secret',
|
|
20
|
+
'auth.gitea.client_secret',
|
|
21
|
+
'auth.github.client_secret',
|
|
22
|
+
'auth.gitlab.client_secret',
|
|
23
|
+
'auth.google.client_secret',
|
|
24
|
+
'auth.line.client_secret',
|
|
25
|
+
'auth.mastodon.client_secret',
|
|
26
|
+
'auth.microsoft.client_secret',
|
|
27
|
+
'auth.patreon.client_secret',
|
|
28
|
+
'auth.slack.client_secret',
|
|
29
|
+
'auth.tiktok.client_secret',
|
|
30
|
+
'auth.twitter.client_secret',
|
|
31
|
+
'auth.wechat.client_secret',
|
|
32
|
+
'auth.steam.api_key',
|
|
33
|
+
'captcha.geetest.captcha_key',
|
|
34
|
+
'captcha.hcaptcha.secret_key',
|
|
35
|
+
'captcha.recaptcha.secret_key',
|
|
36
|
+
'captcha.turnstile.secret_key',
|
|
37
|
+
'db.password',
|
|
38
|
+
'email.ali_dm.access_key_secret',
|
|
39
|
+
'email.smtp.password',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
export function isSensitiveConfigPath(path: string) {
|
|
43
|
+
return SensitiveConfigPaths.includes(path)
|
|
44
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import YAML from 'yaml'
|
|
2
|
+
import { getFlattenNodes, getTree, type OptionNode } from './settings-option'
|
|
3
|
+
|
|
4
|
+
export class Settings {
|
|
5
|
+
private tree: OptionNode
|
|
6
|
+
private flatten: { [path: string]: OptionNode }
|
|
7
|
+
private customs = shallowRef<YAML.Document.Parsed<YAML.ParsedNode>>()
|
|
8
|
+
private envs = shallowRef<{ [key: string]: string }>()
|
|
9
|
+
|
|
10
|
+
constructor(yamlObj: YAML.Document.Parsed) {
|
|
11
|
+
this.tree = getTree(yamlObj)
|
|
12
|
+
this.flatten = getFlattenNodes(this.tree)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getTree() {
|
|
16
|
+
return this.tree
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getNode(path: string) {
|
|
20
|
+
return this.flatten[path]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getCustoms() {
|
|
24
|
+
return this.customs
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setCustoms(yamlStr: string) {
|
|
28
|
+
this.customs.value = YAML.parseDocument(yamlStr)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setEnvs(envs: string[]) {
|
|
32
|
+
const envsObj: { [key: string]: string } = {}
|
|
33
|
+
envs.forEach((env) => {
|
|
34
|
+
const [key, value] = env.split('=')
|
|
35
|
+
envsObj[key] = value
|
|
36
|
+
})
|
|
37
|
+
this.envs.value = envsObj
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getEnv(key: string) {
|
|
41
|
+
return this.envs.value?.[key] || null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getEnvByPath(path: string) {
|
|
45
|
+
// replace `.` to `_` and uppercase
|
|
46
|
+
// replace `ATK_TRUSTED_DOMAINS_0` to `ATK_TRUSTED_DOMAINS`
|
|
47
|
+
// replace `ATK_ADMIN_USERS_0_NAME` to `ATK_ADMIN_USERS`
|
|
48
|
+
return this.getEnv(
|
|
49
|
+
'ATK_' +
|
|
50
|
+
path
|
|
51
|
+
.replace(/\./g, '_')
|
|
52
|
+
.toUpperCase()
|
|
53
|
+
.replace(/(_\d+?_\w+|_\d+)$/, ''),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getCustom(path: string) {
|
|
58
|
+
const env = this.getEnvByPath(path)
|
|
59
|
+
if (env) return env
|
|
60
|
+
return this.customs.value?.getIn(path.split('.')) as any
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setCustom(path: string, value: any) {
|
|
64
|
+
const pathArr = path.split('.')
|
|
65
|
+
|
|
66
|
+
this.makeSureObject(pathArr)
|
|
67
|
+
|
|
68
|
+
this.customs.value?.setIn(pathArr, value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// @see https://github.com/eemeli/yaml/issues/174#issuecomment-632281283
|
|
72
|
+
private makeSureObject(pathArr: string[]) {
|
|
73
|
+
for (let i = pathArr.length - 1; i >= 1; i--) {
|
|
74
|
+
const parentPath = pathArr.slice(0, -i)
|
|
75
|
+
|
|
76
|
+
const parentNode = this.customs.value?.getIn(parentPath)
|
|
77
|
+
if (!parentNode) {
|
|
78
|
+
this.customs.value?.setIn(parentPath, new YAML.YAMLMap())
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// -------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export * from './settings-option'
|
|
87
|
+
|
|
88
|
+
// Singleton instance
|
|
89
|
+
let instance: Settings
|
|
90
|
+
|
|
91
|
+
export default {
|
|
92
|
+
init: (yamlObj: YAML.Document.Parsed) => (instance = new Settings(yamlObj)),
|
|
93
|
+
get: () => instance,
|
|
94
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import { createPinia } from 'pinia'
|
|
3
|
+
import Artalk from '@esershnr/artalk'
|
|
4
|
+
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
5
|
+
import { routes } from 'vue-router/auto-routes'
|
|
6
|
+
import { setupI18n } from './i18n'
|
|
7
|
+
import '@esershnr/artalk/Artalk.css'
|
|
8
|
+
import './style.scss'
|
|
9
|
+
import App from './App.vue'
|
|
10
|
+
import { setArtalk } from './global'
|
|
11
|
+
import { setupArtalk, syncArtalkUser } from './artalk'
|
|
12
|
+
import './lib/promise-polyfill'
|
|
13
|
+
|
|
14
|
+
// I18n
|
|
15
|
+
// @see https://vue-i18n.intlify.dev
|
|
16
|
+
const { i18n, setLocale } = setupI18n()
|
|
17
|
+
|
|
18
|
+
// Router
|
|
19
|
+
// @see https://github.com/posva/unplugin-vue-router
|
|
20
|
+
const router = createRouter({
|
|
21
|
+
history: createWebHashHistory(),
|
|
22
|
+
routes,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Pinia
|
|
26
|
+
// @see https://pinia.vuejs.org
|
|
27
|
+
const pinia = createPinia()
|
|
28
|
+
|
|
29
|
+
// Artalk
|
|
30
|
+
// @see https://artalk.js.org
|
|
31
|
+
const artalkLoader = () =>
|
|
32
|
+
new Promise<Artalk>((notifyArtalkLoaded) => {
|
|
33
|
+
let artalkLoaded = false
|
|
34
|
+
let artalk: Artalk | null = null
|
|
35
|
+
|
|
36
|
+
Artalk.use((ctx) => {
|
|
37
|
+
// When artalk is ready, notify the loader and load the locale
|
|
38
|
+
ctx.watchConf(['locale'], async (conf) => {
|
|
39
|
+
if (typeof conf.locale === 'string' && conf.locale !== 'auto') await setLocale(conf.locale) // update i18n locale
|
|
40
|
+
|
|
41
|
+
if (!artalkLoaded) {
|
|
42
|
+
artalkLoaded = true
|
|
43
|
+
notifyArtalkLoaded(artalk!)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
artalk = setupArtalk()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Mount Vue app
|
|
52
|
+
;(async () => {
|
|
53
|
+
const artalk = await artalkLoader()
|
|
54
|
+
setArtalk(artalk)
|
|
55
|
+
|
|
56
|
+
const app = createApp(App)
|
|
57
|
+
app.use(i18n)
|
|
58
|
+
app.use(router)
|
|
59
|
+
app.use(pinia)
|
|
60
|
+
|
|
61
|
+
// user sync from artalk to sidebar
|
|
62
|
+
await syncArtalkUser(artalk.ctx, router)
|
|
63
|
+
|
|
64
|
+
app.mount('#app')
|
|
65
|
+
})()
|