artalk 2.2.11 → 2.3.1

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.
Files changed (78) hide show
  1. package/dist/Artalk.css +1 -1
  2. package/dist/Artalk.es.js +1872 -1143
  3. package/dist/Artalk.iife.js +1 -1
  4. package/dist/Artalk.js +1 -1
  5. package/dist/ArtalkLite.css +1 -0
  6. package/dist/ArtalkLite.es.js +5123 -0
  7. package/dist/ArtalkLite.iife.js +1 -0
  8. package/dist/ArtalkLite.js +1 -0
  9. package/index.html +5 -3
  10. package/package.json +7 -5
  11. package/src/api/index.ts +24 -4
  12. package/src/api/request.ts +50 -81
  13. package/src/artalk.ts +131 -127
  14. package/src/comment/actions.ts +86 -0
  15. package/src/{components/html → comment}/comment.html +1 -1
  16. package/src/comment/comment.ts +191 -0
  17. package/src/comment/index.ts +3 -0
  18. package/src/comment/render.ts +425 -0
  19. package/src/components/action-btn.ts +6 -3
  20. package/src/components/dialog.ts +7 -3
  21. package/src/components/read-more-btn.ts +6 -2
  22. package/src/context.ts +204 -13
  23. package/src/defaults.ts +15 -4
  24. package/src/editor/editor.html +18 -0
  25. package/src/editor/editor.ts +419 -0
  26. package/src/editor/index.ts +3 -0
  27. package/src/editor/plugs/editor-plug.ts +51 -0
  28. package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.less +2 -1
  29. package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.ts +65 -63
  30. package/src/editor/plugs/header-input-plug.ts +78 -0
  31. package/src/{components/editor-plugs → editor/plugs}/preview-plug.less +0 -0
  32. package/src/editor/plugs/preview-plug.ts +40 -0
  33. package/src/editor/plugs/upload-plug.ts +124 -0
  34. package/src/i18n/en.ts +78 -0
  35. package/src/i18n/index.ts +21 -0
  36. package/src/i18n/zh-CN.ts +82 -0
  37. package/src/layer/index.ts +4 -0
  38. package/src/{components → layer}/layer.ts +9 -9
  39. package/src/{components/html → layer}/sidebar-layer.html +0 -0
  40. package/src/{components → layer}/sidebar-layer.ts +17 -47
  41. package/src/lib/checker/admin-checker.ts +7 -7
  42. package/src/lib/checker/captcha-checker.ts +5 -6
  43. package/src/lib/checker/index.ts +22 -17
  44. package/src/lib/component.ts +7 -2
  45. package/src/lib/stat.ts +63 -0
  46. package/src/lib/ui.ts +3 -10
  47. package/src/lib/user.ts +20 -3
  48. package/src/lib/utils.ts +23 -9
  49. package/src/list/index.ts +3 -0
  50. package/src/{components → list}/list-lite.ts +83 -111
  51. package/src/{components → list}/list-nest.ts +0 -0
  52. package/src/{components/html → list}/list.html +3 -6
  53. package/src/{components → list}/list.ts +44 -42
  54. package/src/style/_variables.less +1 -2
  55. package/src/style/comment.less +26 -13
  56. package/src/style/editor.less +22 -14
  57. package/src/style/list.less +6 -9
  58. package/src/style/main.less +9 -8
  59. package/src/style/pagination.less +1 -1
  60. package/test/ArtalkCount.html +16 -0
  61. package/test/ArtalkI18n.html +15 -0
  62. package/test/ArtalkLite.html +15 -0
  63. package/types/artalk-config.d.ts +50 -32
  64. package/types/context.d.ts +128 -0
  65. package/types/event.d.ts +6 -66
  66. package/types/global.d.ts +1 -0
  67. package/types/plug.d.ts +4 -0
  68. package/{vite-common.config.ts → vite-base.config.ts} +6 -4
  69. package/vite-demo.config.ts +2 -4
  70. package/vite-lite.config.ts +22 -0
  71. package/vite.config.ts +9 -18
  72. package/src/components/_component.ts.txt +0 -10
  73. package/src/components/comment.ts +0 -605
  74. package/src/components/editor-plugs/_example-plug.ts.txt +0 -29
  75. package/src/components/editor-plugs/editor-plug.ts +0 -20
  76. package/src/components/editor-plugs/preview-plug.ts +0 -48
  77. package/src/components/editor.ts +0 -604
  78. package/src/components/html/editor.html +0 -18
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.2.11",
4
- "backendMinVersion": "2.1.4",
3
+ "version": "2.3.1",
4
+ "backendMinVersion": "2.1.7",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
@@ -24,16 +24,18 @@
24
24
  "module": "./dist/Artalk.es.js",
25
25
  "types": "./types/index.d.ts",
26
26
  "devDependencies": {
27
- "@types/marked": "^4.0.3"
27
+ "@types/marked": "4.0.3"
28
28
  },
29
29
  "dependencies": {
30
30
  "hanabi": "^0.4.0",
31
31
  "insane": "^2.6.2",
32
- "marked": "^4.0.14"
32
+ "marked": "^4.0.16",
33
+ "abortcontroller-polyfill": "^1.7.3"
33
34
  },
34
35
  "scripts": {
35
36
  "dev": "vite",
36
- "build": "pnpm lint && vite build",
37
+ "build": "pnpm lint && vite build && pnpm build:lite",
38
+ "build:lite": "vite build --config vite-lite.config.ts",
37
39
  "build:demo": "vite build --config vite-demo.config.ts",
38
40
  "serve": "vite preview",
39
41
  "deploy": "gh-pages -d deploy",
package/src/api/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { CommentData, ListData, UserData, PageData, SiteData, NotifyData } from '~/types/artalk-data'
2
2
  import ArtalkConfig from '~/types/artalk-config'
3
- import Context from '../context'
3
+ import Context from '~/types/context'
4
4
  import { Fetch, ToFormData, POST, GET } from './request'
5
5
  import * as Utils from '../lib/utils'
6
+ import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
6
7
 
7
8
  export default class Api {
8
9
  private ctx: Context
@@ -95,8 +96,6 @@ export default class Api {
95
96
  /** 用户 · 获取 */
96
97
  public userGet(name: string, email: string) {
97
98
  const ctrl = new AbortController()
98
- const { signal } = ctrl
99
-
100
99
  const params: any = {
101
100
  name, email, site_name: (this.ctx.conf.site || '')
102
101
  }
@@ -104,7 +103,7 @@ export default class Api {
104
103
  const req = Fetch(this.ctx, `${this.baseURL}/user-get`, {
105
104
  method: 'POST',
106
105
  body: ToFormData(params),
107
- signal,
106
+ signal: ctrl.signal,
108
107
  }).then((json) => ({
109
108
  user: json.data.user as UserData|null,
110
109
  is_login: json.data.is_login as boolean,
@@ -275,6 +274,25 @@ export default class Api {
275
274
  return p.pv as number
276
275
  }
277
276
 
277
+ /** 统计 */
278
+ public async stat(
279
+ type: 'latest_comments'|'latest_pages'|'pv_most_pages'|'comment_most_pages'|
280
+ 'page_pv'|'site_pv'|'page_comment'|'site_comment',
281
+ pageKeys?: string|string[],
282
+ limit?: number
283
+ ) {
284
+ const params: any = {
285
+ type,
286
+ site_name: this.ctx.conf.site || '',
287
+ }
288
+
289
+ if (pageKeys) params.page_keys = Array.isArray(pageKeys) ? pageKeys.join(',') : pageKeys
290
+ if (limit) params.limit = limit
291
+
292
+ const data = await POST<PageData[]|CommentData[]|object|number>(this.ctx, `${this.baseURL}/stat`, params)
293
+ return data
294
+ }
295
+
278
296
  /** 图片上传 */
279
297
  public async imgUpload(file: File) {
280
298
  const params: any = {
@@ -307,6 +325,8 @@ export default class Api {
307
325
  conf.emoticons = conf.emoticons.trim()
308
326
  if (conf.emoticons.startsWith("[") || conf.emoticons.startsWith("{")) {
309
327
  conf.emoticons = JSON.parse(conf.emoticons) // pase json
328
+ } else if (conf.emoticons === "false") {
329
+ conf.emoticons = false
310
330
  }
311
331
  }
312
332
 
@@ -1,84 +1,58 @@
1
- import Context from '../context'
1
+ import Context from '~/types/context'
2
2
 
3
3
  /** 公共请求函数 */
4
4
  export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit, timeout?: number): Promise<any> {
5
5
  // JWT
6
6
  if (ctx.user.data.token) {
7
- const requestHeaders: HeadersInit = new Headers()
8
- requestHeaders.set('Authorization', `Bearer ${ctx.user.data.token}`)
9
-
10
- init.headers = requestHeaders
7
+ const headers = new Headers(init.headers) // 保留原有 headers
8
+ headers.set('Authorization', `Bearer ${ctx.user.data.token}`)
9
+ init.headers = headers
11
10
  }
12
11
 
13
12
  // 请求操作
14
- try {
13
+ const resp = await timeoutFetch(ctx, input, timeout || ctx.conf.reqTimeout || 15000, init)
15
14
 
16
- let resp: Response
17
- if ((typeof timeout !== 'number' && ctx.conf.reqTimeout === 0) || timeout === 0) {
18
- resp = await fetch(input, init)
19
- } else {
20
- // 请求超时检测
21
- resp = await timeoutPromise(timeout || ctx.conf.reqTimeout || 15000, fetch(input, init))
22
- }
15
+ const respHttpCode = resp.status
16
+ const noAccessCodes = [401, 400]
17
+ const isNoAccess = noAccessCodes.includes(respHttpCode)
23
18
 
24
- const noAccessCodes = [401, 400]
19
+ if (!resp.ok && !isNoAccess)
20
+ throw new Error(`${ctx.$t('reqGot')} ${respHttpCode}`)
25
21
 
26
- if (!resp.ok && !noAccessCodes.includes(resp.status)) throw new Error(`请求响应 ${resp.status}`)
22
+ // 解析获取响应的 json
23
+ let json: any = await resp.json()
27
24
 
28
- // 解析获取响应的 json
29
- let json: any = await resp.json()
25
+ // 重新发起请求
26
+ const recall = (resolve, reject) => {
27
+ Fetch(ctx, input, init)
28
+ .then(d => { resolve(d) })
29
+ .catch(e => { reject(e) })
30
+ }
30
31
 
31
- // 重新发起请求
32
- const recall = (resolve, reject) => {
33
- Fetch(ctx, input, init).then(d => {
34
- resolve(d)
35
- }).catch(err => {
36
- reject(err)
32
+ // 请求弹出层验证
33
+ if (json.data?.need_captcha) {
34
+ // 请求需要验证码
35
+ json = await (new Promise<any>((resolve, reject) => {
36
+ ctx.checkCaptcha({
37
+ imgData: json.data.img_data,
38
+ iframe: json.data.iframe,
39
+ onSuccess: () => { recall(resolve, reject) },
40
+ onCancel: () => { reject(json) }
37
41
  })
38
- }
39
-
40
- // 请求弹出层验证
41
- if (json.data && json.data.need_captcha) {
42
- // 请求需要验证码
43
- json = await (new Promise<any>((resolve, reject) => {
44
- ctx.trigger('checker-captcha', {
45
- imgData: json.data.img_data,
46
- iframe: json.data.iframe,
47
- onSuccess: () => {
48
- recall(resolve, reject)
49
- },
50
- onCancel: () => {
51
- reject(json)
52
- }
53
- })
54
- }))
55
- } else if ((json.data && json.data.need_login) || noAccessCodes.includes(resp.status)) {
56
- // 请求需要管理员权限
57
- json = await (new Promise<any>((resolve, reject) => {
58
- ctx.trigger('checker-admin', {
59
- onSuccess: () => {
60
- recall(resolve, reject)
61
- },
62
- onCancel: () => {
63
- reject(json)
64
- }
65
- })
66
- }))
67
- }
68
-
69
- if (!json.success) throw json // throw 相当于 reject(json)
70
-
71
- return json
72
-
73
- } catch (err) {
74
- // 错误处理
75
- console.error(err)
42
+ }))
43
+ } else if (json.data?.need_login || isNoAccess) {
44
+ // 请求需要管理员权限
45
+ json = await (new Promise<any>((resolve, reject) => {
46
+ ctx.checkAdmin({
47
+ onSuccess: () => { recall(resolve, reject) },
48
+ onCancel: () => { reject(json) }
49
+ })
50
+ }))
51
+ }
76
52
 
77
- if (err instanceof TypeError)
78
- throw new Error(`网络错误`)
53
+ if (!json.success) throw json // throw 相当于 reject(json)
79
54
 
80
- throw err
81
- }
55
+ return json
82
56
  }
83
57
 
84
58
  /** 公共 POST 请求 */
@@ -108,21 +82,16 @@ export function ToFormData(object: {[key: string]: any}): FormData {
108
82
  }
109
83
 
110
84
  /** 我靠,fetch 一个 timeout,都要丑陋的实现 */
111
- function timeoutPromise<T>(ms: number, promise: Promise<T>): Promise<T> {
112
- return new Promise((resolve, reject) => {
113
- const timeoutId = setTimeout(() => {
114
- reject(new Error("请求超时"))
115
- }, ms)
116
-
117
- promise.then(
118
- (res) => {
119
- clearTimeout(timeoutId)
120
- resolve(res)
121
- },
122
- (err) => {
123
- clearTimeout(timeoutId)
124
- reject(err)
125
- }
126
- )
85
+ function timeoutFetch(ctx: Context, url: RequestInfo, ms: number, opts: RequestInit) {
86
+ const controller = new AbortController()
87
+ opts.signal?.addEventListener('abort', () => controller.abort()) // 保留原有 signal 功能
88
+ let promise = fetch(url, { ...opts, signal: controller.signal })
89
+ if (ms > 0) {
90
+ const timer = setTimeout(() => controller.abort(), ms)
91
+ promise.finally(() => { clearTimeout(timer) })
92
+ }
93
+ promise = promise.catch((err) => {
94
+ throw ((err || {}).name === 'AbortError') ? new Error(ctx.$t('reqAborted')) : err
127
95
  })
96
+ return promise
128
97
  }
package/src/artalk.ts CHANGED
@@ -2,28 +2,32 @@ import './style/main.less'
2
2
 
3
3
  import ArtalkConfig from '~/types/artalk-config'
4
4
  import { EventPayloadMap, Handler } from '~/types/event'
5
- import Context from './context'
5
+ import ArtalkPlug from '~/types/plug'
6
+ import Context from '~/types/context'
7
+ import ConcreteContext from './context'
6
8
  import defaults from './defaults'
9
+ import { internal as internalLocales } from './i18n'
7
10
 
8
11
  import CheckerLauncher from './lib/checker'
9
- import Editor from './components/editor'
10
- import List from './components/list'
11
- import SidebarLayer from './components/sidebar-layer'
12
+ import Editor from './editor'
13
+ import List from './list'
14
+ import SidebarLayer from './layer/sidebar-layer'
12
15
 
13
- import Layer, { GetLayerWrap } from './components/layer'
14
- import Api from './api'
16
+ import Layer, { GetLayerWrap } from './layer'
15
17
  import * as Utils from './lib/utils'
16
18
  import * as Ui from './lib/ui'
19
+ import * as Stat from './lib/stat'
17
20
 
18
21
  /**
19
22
  * Artalk
20
- * @link https://artalk.js.org
23
+ *
24
+ * @website https://artalk.js.org
21
25
  */
22
26
  export default class Artalk {
23
27
  public static readonly defaults: ArtalkConfig = defaults
24
28
 
25
- public ctx!: Context
26
29
  public conf!: ArtalkConfig
30
+ public ctx!: Context
27
31
  public $root!: HTMLElement
28
32
 
29
33
  public checkerLauncher!: CheckerLauncher
@@ -31,79 +35,49 @@ export default class Artalk {
31
35
  public list!: List
32
36
  public sidebarLayer!: SidebarLayer
33
37
 
34
- constructor (customConf: ArtalkConfig) {
35
- // 配置
36
- this.conf = Utils.mergeDeep(Artalk.defaults, customConf)
37
- this.conf.server = this.conf.server.replace(/\/$/, '').replace(/\/api\/?$/, '')
38
-
39
- // 默认 pageKey
40
- if (!this.conf.pageKey) {
41
- // @link http://bl.ocks.org/abernier/3070589
42
- this.conf.pageKey = `${window.location.pathname}`
43
- }
44
-
45
- // 默认 pageTitle
46
- if (!this.conf.pageTitle) {
47
- this.conf.pageTitle = `${document.title}`
48
- }
38
+ protected static Plugins: ArtalkPlug[] = [
39
+ Stat.PvCountWidget
40
+ ]
49
41
 
50
- // 列表显示模式
51
- if (this.conf.nestMax && this.conf.nestMax <= 1) {
52
- this.conf.flatMode = true
53
- }
42
+ constructor(customConf: Partial<ArtalkConfig>) {
43
+ /* 初始化基本配置 */
44
+ this.conf = Artalk.HandelBaseConf(customConf)
45
+ if (this.conf.el instanceof HTMLElement) this.$root = this.conf.el
54
46
 
55
- // 装载元素
56
- if (!!this.conf.el && this.conf.el instanceof HTMLElement) {
57
- this.$root = this.conf.el
58
- } else {
59
- try {
60
- const $root = document.querySelector<HTMLElement>(this.conf.el)
61
- if (!$root) throw Error(`Sorry, target element "${this.conf.el}" was not found.`)
62
- this.$root = $root
63
- } catch (e) {
64
- console.error(e)
65
- throw new Error('Please check your Artalk `el` config.')
66
- }
67
- }
47
+ /* 初始化 Context */
48
+ this.ctx = new ConcreteContext(this.conf, this.$root)
68
49
 
69
- // Context 初始化
70
- this.ctx = new Context(this.$root, this.conf)
71
-
72
- // 界面初始化
73
- this.$root.classList.add('artalk')
74
- this.$root.innerHTML = ''
75
-
76
- // 初始化组件
77
- if (this.conf.useBackendConf) {
78
- // 复用后端的配置
79
- // @ts-ignore
80
- return (async () => {
81
- await this.loadConfRemote()
82
- this.initCore()
83
- return this // Promise<Artalk>
84
- })()
85
- }
50
+ // @ts-ignore 远程加载引用后端的配置
51
+ if (this.conf.useBackendConf) return this.loadConfRemoteAndInitComponents()
86
52
 
87
- this.initCore()
53
+ /* 初始化组件 */
54
+ this.initComponents()
88
55
  }
89
56
 
90
- private initCore() {
91
- // 组件初始化
57
+ /** 组件初始化 */
58
+ private initComponents() {
59
+ this.initLocale()
92
60
  this.initLayer()
93
61
  this.initDarkMode()
62
+ Utils.initMarked(this.ctx)
63
+
64
+ // CheckerLauncher
94
65
  this.checkerLauncher = new CheckerLauncher(this.ctx)
95
- Utils.initMarked(this.ctx) // 初始化 marked
66
+ this.ctx.setCheckerLauncher(this.checkerLauncher)
96
67
 
97
68
  // 编辑器
98
69
  this.editor = new Editor(this.ctx)
70
+ this.ctx.setEditor(this.editor)
99
71
  this.$root.appendChild(this.editor.$el)
100
72
 
101
73
  // 评论列表
102
74
  this.list = new List(this.ctx)
75
+ this.ctx.setList(this.list)
103
76
  this.$root.appendChild(this.list.$el)
104
77
 
105
78
  // 侧边栏
106
79
  this.sidebarLayer = new SidebarLayer(this.ctx)
80
+ this.ctx.setSidebarLayer(this.sidebarLayer)
107
81
  this.$root.appendChild(this.sidebarLayer.$el)
108
82
 
109
83
  // 评论获取
@@ -112,29 +86,66 @@ export default class Artalk {
112
86
  // 事件绑定初始化
113
87
  this.initEventBind()
114
88
 
115
- // 其他
116
- this.initPV()
117
-
118
89
  // 插件初始化
119
90
  Artalk.Plugins.forEach(plugin => {
120
- if (typeof plugin === 'function') {
91
+ if (typeof plugin === 'function')
121
92
  plugin(this.ctx)
122
- }
123
93
  })
124
94
  }
125
95
 
126
- /** 获取远程配置 */
96
+ /** 基本配置初始化 */
97
+ private static HandelBaseConf(customConf: Partial<ArtalkConfig>): ArtalkConfig {
98
+ // 合并默认配置
99
+ const conf: ArtalkConfig = Utils.mergeDeep(Artalk.defaults, customConf)
100
+
101
+ // 绑定元素
102
+ if (typeof conf.el === 'string' && !!conf.el) {
103
+ try {
104
+ const findEl = document.querySelector<HTMLElement>(conf.el)
105
+ if (!findEl) throw Error(`Target element "${conf.el}" was not found.`)
106
+ conf.el = findEl
107
+ } catch (e) {
108
+ console.error(e)
109
+ throw new Error('Please check your Artalk `el` config.')
110
+ }
111
+ }
112
+
113
+ // 服务器配置
114
+ conf.server = conf.server.replace(/\/$/, '').replace(/\/api\/?$/, '')
115
+
116
+ // 默认 pageKey
117
+ if (!conf.pageKey) {
118
+ // @link http://bl.ocks.org/abernier/3070589
119
+ conf.pageKey = `${window.location.pathname}`
120
+ }
121
+
122
+ // 默认 pageTitle
123
+ if (!conf.pageTitle) {
124
+ conf.pageTitle = `${document.title}`
125
+ }
126
+
127
+ return conf
128
+ }
129
+
130
+ /** 初始化组件根据远程加载的配置 */
131
+ private async loadConfRemoteAndInitComponents() {
132
+ await this.loadConfRemote()
133
+ this.initComponents()
134
+ return this
135
+ }
136
+
137
+ /** 远程加载配置 */
127
138
  private async loadConfRemote() {
128
139
  Ui.showLoading(this.$root)
129
140
  let backendConf = {}
130
141
  try {
131
- backendConf = await (new Api(this.ctx)).conf()
132
- } catch (err) { console.error("配置远程获取失败", err) }
142
+ backendConf = await this.ctx.getApi().conf()
143
+ } catch (err) { console.error("Load config from remote err", err) }
133
144
  this.ctx.conf = Utils.mergeDeep(this.ctx.conf, backendConf)
134
145
  Ui.hideLoading(this.$root)
135
146
  }
136
147
 
137
- /** 事件绑定 · 初始化 */
148
+ /** 事件绑定初始化 */
138
149
  private initEventBind() {
139
150
  // 锚点快速跳转评论
140
151
  window.addEventListener('hashchange', () => {
@@ -142,52 +153,58 @@ export default class Artalk {
142
153
  this.list.checkGoToCommentByUrlHash()
143
154
  })
144
155
 
145
- // 仅管理员显示控制
146
- this.ctx.on('check-admin-show-el', () => {
147
- const items: HTMLElement[] = []
148
-
149
- this.$root.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
150
-
151
- // for layer
152
- const { $wrap: $layerWrap } = GetLayerWrap(this.ctx)
153
- if ($layerWrap) $layerWrap.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
154
-
155
- // for sidebar
156
- // TODO: 这个其实应该写在 packages/artalk-sidebar 里面的
157
- const $sidebarEl = document.querySelector<HTMLElement>('.atk-sidebar')
158
- if ($sidebarEl) $sidebarEl.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
159
-
160
- items.forEach(($item: HTMLElement) => {
161
- if (this.ctx.user.data.isAdmin)
162
- $item.classList.remove('atk-hide')
163
- else
164
- $item.classList.add('atk-hide')
165
- })
166
- })
167
-
168
156
  // 本地用户数据变更
169
157
  this.ctx.on('user-changed', () => {
170
- this.ctx.trigger('check-admin-show-el')
171
- this.ctx.trigger('list-refresh-ui')
158
+ this.ctx.checkAdminShowEl()
159
+ this.ctx.listRefreshUI()
172
160
  })
173
161
  }
174
162
 
175
- /** 重新加载 */
176
- public reload() {
177
- this.list.fetchComments(0)
163
+ /** 语言初始化 */
164
+ private initLocale() {
165
+ if (typeof this.conf.locale === 'string') {
166
+ if (this.conf.locale === 'auto') { // 自动切换
167
+ this.conf.locale = ((navigator.languages) ? navigator.languages[0] : navigator.language)
168
+ }
169
+
170
+ this.conf.locale = this.conf.locale.replace(
171
+ /^([a-zA-Z]+)(-[a-zA-Z]+)?$/,
172
+ (_, p1: string, p2: string) => (p1.toLowerCase() + (p2 || '').toUpperCase())
173
+ )
174
+
175
+ if (!internalLocales[this.conf.locale]) { // 语言不存在时,使用 en
176
+ console.log(`Locale "${this.conf.locale}" not found.`)
177
+ this.conf.locale = 'en'
178
+ }
179
+ }
178
180
  }
179
181
 
180
182
  /** Layer 初始化 */
181
- initLayer() {
183
+ private initLayer() {
182
184
  // 记录页面原始 Styles
183
185
  Layer.BodyOrgOverflow = document.body.style.overflow
184
186
  Layer.BodyOrgPaddingRight = document.body.style.paddingRight
185
187
  }
186
188
 
187
- /** 暗黑模式 · 初始化 */
188
- public initDarkMode() {
189
+ /** 暗黑模式初始化 */
190
+ private initDarkMode() {
191
+ if (this.conf.darkMode === 'auto') {
192
+ // 自动切换暗黑模式,事件监听
193
+ const darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)')
194
+ darkModeMedia.addEventListener('change', (e) => { this.setDarkMode(e.matches) })
195
+ this.setDarkMode(darkModeMedia.matches)
196
+ } else {
197
+ this.setDarkMode(this.conf.darkMode || false)
198
+ }
199
+ }
200
+
201
+ /** 设置暗黑模式 */
202
+ public setDarkMode(darkMode: boolean) {
189
203
  const darkModeClassName = 'atk-dark-mode'
190
204
 
205
+ this.ctx.conf.darkMode = darkMode
206
+ this.ctx.trigger('conf-updated')
207
+
191
208
  if (this.conf.darkMode) {
192
209
  this.$root.classList.add(darkModeClassName)
193
210
  } else {
@@ -205,28 +222,6 @@ export default class Artalk {
205
222
  }
206
223
  }
207
224
 
208
- /** 暗黑模式 · 设定 */
209
- public setDarkMode(darkMode: boolean) {
210
- this.ctx.conf.darkMode = darkMode
211
- this.ctx.trigger('conf-updated')
212
- this.initDarkMode()
213
- }
214
-
215
- /** PV */
216
- public async initPV() {
217
- const pvNum = await new Api(this.ctx).pv()
218
- if (Number.isNaN(Number(pvNum)))
219
- return
220
-
221
- if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
222
- return
223
-
224
- const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
225
- // $pv.innerText = '-' // 默认占位符
226
-
227
- $pv.innerText = String(pvNum)
228
- }
229
-
230
225
  /** 监听事件 */
231
226
  public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>) {
232
227
  this.ctx.on(name, handler, 'external')
@@ -242,11 +237,20 @@ export default class Artalk {
242
237
  this.ctx.trigger(name, payload, 'external')
243
238
  }
244
239
 
245
- /** Plugins */
246
- protected static Plugins: ((ctx: Context) => void)[] = []
240
+ /** 重新加载 */
241
+ public reload() {
242
+ this.ctx.listReload()
243
+ }
247
244
 
248
- /** Enable Plugin */
249
- public static Use(plugin: ((ctx: Context) => void)) {
245
+ /** Use Plugin */
246
+ public static Use(plugin: ArtalkPlug) {
250
247
  this.Plugins.push(plugin)
251
248
  }
249
+
250
+ /** 装载数量统计元素 */
251
+ public static LoadCountWidget(customConf: Partial<ArtalkConfig>) {
252
+ const conf = this.HandelBaseConf(customConf)
253
+ const ctx = new ConcreteContext(conf)
254
+ Stat.initCountWidget({ ctx, pvAdd: false })
255
+ }
252
256
  }