dsh-remote-plugin 0.5.2 → 0.5.4

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/public/admin.js CHANGED
@@ -2,6 +2,9 @@
2
2
  'use strict'
3
3
 
4
4
  const $ = (id) => document.getElementById(id)
5
+ const I18N = window.I18N
6
+ const t = (k, v) => I18N.t(k, v)
7
+ I18N.init(window.ADMIN_STR)
5
8
  // 插件内嵌(/remote/ 或 ?embedded=1)直接进管理面板, 不需要任何令牌门禁;
6
9
  // 独立网关模式(/admin/)仍保留令牌输入。路径判断兼容无尾斜杠 /remote。
7
10
  const pluginMode = location.pathname === '/remote'
@@ -31,10 +34,10 @@ function toast(text, kind = '') {
31
34
  }
32
35
 
33
36
  function fmtUptime(sec) {
34
- if (sec < 60) return sec + ''
35
- if (sec < 3600) return Math.floor(sec / 60) + ' 分钟'
36
- if (sec < 86400) return Math.floor(sec / 3600) + ' 小时 ' + Math.floor(sec % 3600 / 60) + ''
37
- return Math.floor(sec / 86400) + '' + Math.floor(sec % 86400 / 3600) + ' 小时'
37
+ if (sec < 60) return sec + t('unit.sec')
38
+ if (sec < 3600) return Math.floor(sec / 60) + t('unit.min')
39
+ if (sec < 86400) return Math.floor(sec / 3600) + t('unit.hour') + Math.floor(sec % 3600 / 60) + t('unit.minShort')
40
+ return Math.floor(sec / 86400) + t('unit.day') + Math.floor(sec % 86400 / 3600) + t('unit.hour')
38
41
  }
39
42
 
40
43
  function fmtTime(ts) {
@@ -54,10 +57,10 @@ async function loadState() {
54
57
  render(st)
55
58
  } catch (e) {
56
59
  if (e.message === 'AUTH') {
57
- toast('令牌无效', 'err')
60
+ toast(t('toast.tokenInvalid'), 'err')
58
61
  logout()
59
62
  } else {
60
- $('conn-badge').textContent = '连接失败'
63
+ $('conn-badge').textContent = t('toast.connFailed')
61
64
  $('conn-badge').className = 'conn-badge off'
62
65
  }
63
66
  }
@@ -68,10 +71,10 @@ function render(st) {
68
71
  const isPlugin = st.mode === 'plugin'
69
72
  const isGateway = st.mode === 'gateway'
70
73
  shownToken = st.token || token
71
- $('conn-badge').textContent = isPlugin ? '内嵌' : isGateway ? '网关' : '已连接'
74
+ $('conn-badge').textContent = t(isPlugin ? 'badge.embedded' : isGateway ? 'badge.gateway' : 'badge.connected')
72
75
  $('conn-badge').className = 'conn-badge ' + (isPlugin || isGateway ? 'on' : 'off')
73
- $('conn-badge').title = isGateway ? '新标签页打开网关管理面板' : '网关未运行'
74
- $('token-full').textContent = shownToken || (isPlugin ? '插件模式 · 未接网关, 无需令牌' : '未获取到令牌')
76
+ $('conn-badge').title = t(isGateway ? 'badge.gateway.title' : 'badge.gatewayDown')
77
+ $('token-full').textContent = shownToken || t(isPlugin ? 'token.pluginNoGateway' : 'token.unavailable')
75
78
  // 主机端插件模式: 显示真实令牌(复制可用), 只隐藏退出按钮; 令牌门禁本身不存在
76
79
  $('btn-copy').classList.toggle('hidden', !shownToken)
77
80
  $('btn-logout').classList.toggle('hidden', pluginMode)
@@ -83,65 +86,70 @@ function render(st) {
83
86
  gatewayRunning = isGateway
84
87
  $('btn-gateway').classList.toggle('hidden', !pluginMode)
85
88
  $('btn-gateway').textContent = gatewayBusy
86
- ? (gatewayRunning ? '停止中…' : '启动中…')
87
- : (gatewayRunning ? '停止网关' : '启动网关')
89
+ ? t(gatewayRunning ? 'stopping' : 'starting')
90
+ : t(gatewayRunning ? 'stopGateway' : 'startGateway')
88
91
  $('btn-gateway').disabled = gatewayBusy
89
92
  const upOk = st.upstream.reachable
90
- const hostIPs = (st.lanIPs || []).join('') || '127.0.0.1'
93
+ const hostIPs = (st.lanIPs || []).join(t('stat.ipSep')) || '127.0.0.1'
91
94
  const latestHtml = st.latest?.newer
92
- ? `<div class="v">v${st.latest.version} 可用</div><div class="k">当前 v${st.version} · <a href="${st.latest.url || '#'}" target="_blank" rel="noopener" style="color:var(--orange)">去下载</a></div>`
93
- : `<div class="v">v${st.version}</div><div class="k">${isPlugin ? 'DSH 内嵌 · 免网关' : st.latest?.error ? '更新检查: ' + st.latest.error : st.latest?.version ? '已是最新(来源检查)' : '未检查更新'}</div>`
95
+ ? `<div class="v">${t('stat.updateAvailable', { version: st.latest.version })}</div><div class="k">${t('stat.currentV', { version: st.version })} · <a href="${st.latest.url || '#'}" target="_blank" rel="noopener" style="color:var(--dsr-accent-strong)">${t('stat.download')}</a></div>`
96
+ : `<div class="v">v${st.version}</div><div class="k">${isPlugin ? t('stat.embedded') : st.latest?.error ? t('stat.updateCheck', { error: st.latest.error }) : st.latest?.version ? t('stat.latest') : t('stat.notChecked')}</div>`
94
97
  $('stats').innerHTML = `
95
- <div class="stat-card"><div class="v">v${st.version}</div><div class="k">${isPlugin ? '插件版本' : '网关版本'}</div></div>
98
+ <div class="stat-card"><div class="v">v${st.version}</div><div class="k">${t(isPlugin ? 'stat.pluginVersion' : 'stat.gatewayVersion')}</div></div>
96
99
  <div class="stat-card ${st.latest?.newer ? 'warn' : 'ok'}">${latestHtml}</div>
97
- <div class="stat-card ok"><div class="v" style="font-size:15px">${hostIPs}</div><div class="k">主机 IP · ${st.hostname}${isPlugin ? ' (手机连 8787 网关)' : ' (手机连这个地址)'}</div></div>
98
- <div class="stat-card ${upOk ? 'ok' : 'warn'}"><div class="v">${upOk ? '可达' : '不可达'}</div><div class="k">DSH 上游 ${st.upstream.url}</div></div>
99
- <div class="stat-card"><div class="v">${st.onlineCount}/${st.deviceCount}</div><div class="k">设备在线 / 累计</div></div>
100
- <div class="stat-card"><div class="v">${st.totalRequests}</div><div class="k">总请求数</div></div>
101
- <div class="stat-card"><div class="v">${st.authFailures}</div><div class="k">认证失败</div></div>
102
- <div class="stat-card"><div class="v">${fmtUptime(st.uptimeSec)}</div><div class="k">运行时长 · ${st.host}:${st.port}</div></div>`
100
+ <div class="stat-card ok"><div class="v" style="font-size:13px">${hostIPs}</div><div class="k">${t('stat.hostIP', { hostname: st.hostname })}${isPlugin ? t('stat.phoneGateway') : t('stat.phoneThis')}</div></div>
101
+ <div class="stat-card ${upOk ? 'ok' : 'warn'}"><div class="v">${t(upOk ? 'stat.reachable' : 'stat.unreachable')}</div><div class="k">${t('stat.dshUpstream', { url: st.upstream.url })}</div></div>
102
+ <div class="stat-card"><div class="v">${st.onlineCount}/${st.deviceCount}</div><div class="k">${t('stat.devicesOnline')}</div></div>
103
+ <div class="stat-card"><div class="v">${st.totalRequests}</div><div class="k">${t('stat.totalRequests')}</div></div>
104
+ <div class="stat-card"><div class="v">${st.authFailures}</div><div class="k">${t('stat.authFailures')}</div></div>
105
+ <div class="stat-card"><div class="v">${fmtUptime(st.uptimeSec)}</div><div class="k">${t('stat.uptime', { host: st.host, port: st.port })}</div></div>`
103
106
 
104
107
  $('device-summary').textContent = isPlugin
105
- ? (st.gatewayInstalled ? '网关已安装 · 当前未运行' : '未检测到网关程序')
106
- : `${st.devices.length} 个 IP · 每 5 秒刷新`
108
+ ? t(st.gatewayInstalled ? 'device.installedNotRunning' : 'device.noGatewayBinary')
109
+ : t('device.ipRefresh', { n: st.devices.length })
107
110
  if (isPlugin && !st.devices.length) {
108
111
  $('device-rows').innerHTML = ''
109
112
  const rel = 'https://github.com/Blank-not-black/dsh-Remote/releases/latest/download/'
110
- const apkBtn = `<a class="mini-btn" href="${rel}dsh-remote.apk" target="_blank" rel="noopener">下载手机 App</a>`
113
+ const apkBtn = `<a class="mini-btn" href="${rel}dsh-remote.apk" target="_blank" rel="noopener">${t('device.downloadApp')}</a>`
111
114
  if (!st.gatewayInstalled) {
112
115
  // 只有插件包真的没有内置网关程序时, 才引导下载网关
113
116
  const isWin = /windows|win32/i.test(navigator.userAgent)
114
117
  const gwAsset = isWin ? 'dsh-remote-win-x64.exe' : 'dsh-remote-linux-x64'
115
118
  $('device-empty').innerHTML = `
116
- <div>本插件包未包含网关程序:下载对应系统的网关并运行</div>
119
+ <div>${t('device.noBinaryGuide')}</div>
117
120
  <div class="empty-actions">
118
- <a class="mini-btn" href="${rel}${gwAsset}" target="_blank" rel="noopener">下载网关 (${isWin ? 'Windows x64' : 'Linux x64'})</a>
121
+ <a class="mini-btn" href="${rel}${gwAsset}" target="_blank" rel="noopener">${t('device.downloadGateway', { os: isWin ? 'Windows x64' : 'Linux x64' })}</a>
119
122
  ${apkBtn}
120
123
  </div>
121
- <div class="muted" style="margin-top:10px">运行网关后回到本页刷新,即可看到设备监控与完整令牌</div>`
124
+ <div class="muted" style="margin-top:10px">${t('device.afterRunGuide')}</div>`
122
125
  } else {
123
126
  $('device-empty').innerHTML = `
124
- <div>网关已随插件安装,当前未运行 — 点击上方「启动网关」开启</div>
127
+ <div>${t('device.installedGuide')}</div>
125
128
  <div class="empty-actions">${apkBtn}</div>
126
- <div class="muted" style="margin-top:10px">启动后本页会自动刷新为网关模式(完整设备监控 + 令牌)</div>`
129
+ <div class="muted" style="margin-top:10px">${t('device.afterStartGuide')}</div>`
127
130
  }
128
131
  $('device-empty').classList.remove('hidden')
129
132
  } else {
130
133
  // 网关模式: 清掉可能残留的引导文案, 设备为空时只显示中性提示
131
- $('device-empty').textContent = '暂无设备记录'
134
+ $('device-empty').textContent = t('noDevices')
132
135
  $('device-empty').classList.toggle('hidden', st.devices.length > 0)
133
- $('device-rows').innerHTML = st.devices.map(d => `
136
+ $('device-rows').innerHTML = st.devices.map(d => {
137
+ const kindText = t(d.kind === 'app' ? 'device.kind.app' : d.kind === 'admin' ? 'device.kind.admin' : d.kind === 'web' ? 'device.kind.web' : 'device.kind.unknown')
138
+ const noteHtml = d.note ? `<b>${d.note.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]))}</b>` : '<span class="muted">—</span>'
139
+ const ch = `${d.channels.mux ? 'mux' : ''}${d.channels.mux && d.channels.host ? ' · ' : ''}${d.channels.host ? 'host' : ''}${!d.channels.mux && !d.channels.host ? '—' : ''}`
140
+ return `
134
141
  <tr>
135
- <td><span class="dot ${d.online ? 'on' : 'off'}"></span>${d.online ? '在线' : '离线'}</td>
136
- <td>${d.note ? `<b>${d.note.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]))}</b>` : '<span class="muted">—</span>'}<button class="mini-btn" data-note-ip="${d.ip}" data-note="${d.note.replace(/"/g, '&quot;')}" style="margin-left:6px;padding:1px 7px">备注</button></td>
137
- <td><span class="badge ${d.kind}">${d.kind === 'app' ? '手机App' : d.kind === 'admin' ? '管理页' : d.kind === 'web' ? '浏览器' : '未知'}</span></td>
138
- <td class="mono">${d.ip}</td>
139
- <td class="mono">${d.channels.mux ? 'mux' : ''}${d.channels.mux && d.channels.host ? ' · ' : ''}${d.channels.host ? 'host' : ''}${!d.channels.mux && !d.channels.host ? '—' : ''}</td>
140
- <td>${d.requests}</td>
141
- <td>${fmtTime(d.lastSeen)}</td>
142
- <td class="ua" title="${d.ua.replace(/"/g, '&quot;')}">${d.ua || '—'}</td>
143
- <td>${d.online && d.kind !== 'admin' ? `<button class="mini-btn" data-kick="${d.ip}">断开</button>` : ''}</td>
144
- </tr>`).join('')
142
+ <td data-label="${t('th.status')}"><span class="dot ${d.online ? 'on' : 'off'}"></span>${t(d.online ? 'device.online' : 'device.offline')}</td>
143
+ <td data-label="${t('th.name')}">${noteHtml}<button class="mini-btn" data-note-ip="${d.ip}" data-note="${d.note.replace(/"/g, '&quot;')}" style="margin-left:6px;padding:1px 7px">${t('device.note')}</button></td>
144
+ <td data-label="${t('th.type')}"><span class="badge ${d.kind}">${kindText}</span></td>
145
+ <td data-label="${t('th.ip')}" class="mono nowrap">${d.ip}</td>
146
+ <td data-label="${t('th.channels')}" class="mono nowrap">${ch}</td>
147
+ <td data-label="${t('th.requests')}">${d.requests}</td>
148
+ <td data-label="${t('th.lastSeen')}" class="nowrap">${fmtTime(d.lastSeen)}</td>
149
+ <td data-label="${t('th.ua')}" class="ua" title="${d.ua.replace(/"/g, '&quot;')}">${d.ua || '—'}</td>
150
+ <td class="act">${d.online && d.kind !== 'admin' ? `<button class="mini-btn" data-kick="${d.ip}">${t('device.kick')}</button>` : ''}</td>
151
+ </tr>`
152
+ }).join('')
145
153
  }
146
154
  document.querySelectorAll('[data-kick]').forEach(btn =>
147
155
  btn.addEventListener('click', () => kick(btn.dataset.kick)))
@@ -167,21 +175,21 @@ function renderQr(st) {
167
175
  return
168
176
  }
169
177
  try {
170
- const t = pairTarget(st)
178
+ const pt = pairTarget(st)
171
179
  const qr = window.qrcode(0, 'M')
172
- qr.addData(t.url)
180
+ qr.addData(pt.url)
173
181
  qr.make()
174
182
  $('pair-qr').innerHTML = qr.createSvgTag({ cellSize: 4, margin: 2, scalable: true })
175
- $('pair-hint').textContent = `${t.base} · App「设置 扫码连接」,或系统相机扫一扫自动打开 App`
183
+ $('pair-hint').textContent = t('pair.hint', { base: pt.base })
176
184
  box.classList.remove('hidden')
177
185
  } catch (e) {
178
- $('pair-qr').textContent = '二维码生成失败'
186
+ $('pair-qr').textContent = t('pair.failed')
179
187
  box.classList.remove('hidden')
180
188
  }
181
189
  }
182
190
 
183
191
  async function setNote(ip, current) {
184
- const name = prompt('' + ip + ' 设置备注(留空清除):', current || '')
192
+ const name = prompt(t('prompt.note', { ip }), current || '')
185
193
  if (name === null) return
186
194
  const res = await fetch(`${API}/note`, {
187
195
  method: 'POST',
@@ -189,33 +197,33 @@ async function setNote(ip, current) {
189
197
  body: JSON.stringify({ ip, name })
190
198
  })
191
199
  if (res.ok) {
192
- toast('备注已保存', 'ok')
200
+ toast(t('toast.noteSaved'), 'ok')
193
201
  setTimeout(loadState, 300)
194
202
  } else {
195
- toast('保存失败', 'err')
203
+ toast(t('toast.noteFailed'), 'err')
196
204
  }
197
205
  }
198
206
 
199
207
  async function kick(ip) {
200
- if (!confirm('断开该设备的连接?')) return
208
+ if (!confirm(t('confirm.kick'))) return
201
209
  const res = await fetch(`${API}/kick`, {
202
210
  method: 'POST',
203
211
  headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'admin' },
204
212
  body: JSON.stringify({ ip })
205
213
  })
206
214
  if (res.ok) {
207
- toast('已断开 ' + ip, 'ok')
215
+ toast(t('toast.kicked', { ip }), 'ok')
208
216
  setTimeout(loadState, 400)
209
217
  } else {
210
- toast('操作失败', 'err')
218
+ toast(t('toast.opFailed'), 'err')
211
219
  }
212
220
  }
213
221
 
214
222
  function enter() {
215
- const t = $('token-input').value.trim()
216
- if (!t) return
217
- token = t
218
- store.set('dshAdminToken', t)
223
+ const val = $('token-input').value.trim()
224
+ if (!val) return
225
+ token = val
226
+ store.set('dshAdminToken', val)
219
227
  history.replaceState(null, '', location.pathname)
220
228
  showMain()
221
229
  loadState()
@@ -233,7 +241,7 @@ function logout() {
233
241
  clearInterval(timer)
234
242
  $('main-view').classList.add('hidden')
235
243
  $('login-view').classList.remove('hidden')
236
- $('conn-badge').textContent = '未认证'
244
+ $('conn-badge').textContent = t('unauth')
237
245
  $('conn-badge').className = 'conn-badge off'
238
246
  $('token-input').value = ''
239
247
  }
@@ -248,9 +256,9 @@ $('btn-close-drawer').addEventListener('click', () => {
248
256
  $('btn-copy').addEventListener('click', async () => {
249
257
  try {
250
258
  await navigator.clipboard.writeText(shownToken || token)
251
- toast('令牌已复制', 'ok')
259
+ toast(t('toast.tokenCopied'), 'ok')
252
260
  } catch {
253
- toast('复制失败,请手动选择', 'err')
261
+ toast(t('toast.copyFailed'), 'err')
254
262
  }
255
263
  })
256
264
 
@@ -262,19 +270,19 @@ $('btn-qr').addEventListener('click', () => {
262
270
  /* 右上角「网关」徽章: 新标签页打开独立网关管理面板(带 token 免登录) */
263
271
  $('conn-badge').addEventListener('click', () => {
264
272
  const st = lastState
265
- if (!st || st.mode !== 'gateway') { toast('网关未运行', 'err'); return }
273
+ if (!st || st.mode !== 'gateway') { toast(t('toast.gatewayDown'), 'err'); return }
266
274
  const host = location.hostname || '127.0.0.1'
267
275
  const port = st.port || 8787
268
276
  const url = `http://${host}:${port}/admin?token=${encodeURIComponent(shownToken || token)}`
269
277
  try {
270
278
  window.open(url, '_blank', 'noopener')
271
279
  } catch {
272
- toast('浏览器阻止了新窗口,请允许弹窗', 'err')
280
+ toast(t('toast.popupBlocked'), 'err')
273
281
  }
274
282
  })
275
283
 
276
284
  $('btn-rotate').addEventListener('click', async () => {
277
- if (!confirm('轮换后旧令牌立即失效,手机与浏览器都需要重新扫码/输入。继续?')) return
285
+ if (!confirm(t('confirm.rotate'))) return
278
286
  try {
279
287
  const res = await fetch(`${API}/token/rotate`, {
280
288
  method: 'POST',
@@ -284,13 +292,13 @@ $('btn-rotate').addEventListener('click', async () => {
284
292
  if (out.ok && out.token) {
285
293
  token = out.token
286
294
  store.set('dshAdminToken', out.token)
287
- toast('令牌已轮换,请重新给设备配对', 'ok')
295
+ toast(t('toast.rotated'), 'ok')
288
296
  setTimeout(loadState, 300)
289
297
  } else {
290
- toast(out.detail || out.error || '轮换失败', 'err')
298
+ toast(out.detail || out.error || t('toast.rotateFailed'), 'err')
291
299
  }
292
300
  } catch (e) {
293
- toast('轮换失败:' + (e.message || e), 'err')
301
+ toast(t('toast.rotateFailedMsg', { msg: e.message || e }), 'err')
294
302
  }
295
303
  })
296
304
 
@@ -299,7 +307,7 @@ $('btn-gateway').addEventListener('click', async () => {
299
307
  gatewayBusy = true
300
308
  const btn = $('btn-gateway')
301
309
  btn.disabled = true
302
- btn.textContent = gatewayRunning ? '停止中…' : '启动中…'
310
+ btn.textContent = t(gatewayRunning ? 'stopping' : 'starting')
303
311
  try {
304
312
  const res = await fetch(`${API}/gateway`, {
305
313
  method: 'POST',
@@ -308,17 +316,76 @@ $('btn-gateway').addEventListener('click', async () => {
308
316
  })
309
317
  const out = await res.json().catch(() => ({}))
310
318
  if (out.ok) {
311
- toast(out.started ? '网关已启动' : out.running ? '网关已在运行' : gatewayRunning ? '网关已停止' : (out.pending ? '网关启动中,稍后刷新' : '已执行'), 'ok')
319
+ toast(out.started ? t('toast.gatewayStarted') : out.running ? t('toast.gatewayAlready') : gatewayRunning ? t('toast.gatewayStopped') : (out.pending ? t('toast.gatewayPending') : t('toast.done')), 'ok')
312
320
  } else {
313
- toast(out.error || '操作失败', 'err')
321
+ toast(out.error || t('toast.opFailed'), 'err')
314
322
  }
315
323
  } catch (e) {
316
- toast('操作失败:' + (e.message || e), 'err')
324
+ toast(t('toast.opFailedMsg', { msg: e.message || e }), 'err')
317
325
  }
318
326
  gatewayBusy = false
319
327
  setTimeout(loadState, 700)
320
328
  })
321
329
 
330
+ function renderLangBtn() {
331
+ const btn = $('btn-lang')
332
+ if (btn) btn.textContent = I18N.lang === 'zh' ? 'EN' : '中文'
333
+ document.title = t('login.title')
334
+ }
335
+
336
+ const THEME_META = [
337
+ { id: 'default', sw: ['#0B0E1A', '#151B33', '#5B8CFF'] },
338
+ { id: 'dark', sw: ['#05348B', '#0D438F', '#F9A647'] },
339
+ { id: 'light', sw: ['#EFEEEC', '#FAF8F5', '#E6BC7B'] },
340
+ { id: 'neutral', sw: ['#DDD4B8', '#585818', '#832D15'] }
341
+ ]
342
+
343
+ function renderThemeBtn() {
344
+ const cur = window.DSHTheme.get()
345
+ const meta = THEME_META.find(m => m.id === cur)
346
+ const label = $('theme-label')
347
+ const swatch = $('theme-swatch')
348
+ if (label) label.textContent = t('theme.' + cur)
349
+ if (swatch && meta) swatch.style.background = meta.sw[0]
350
+ const btn = $('btn-theme')
351
+ if (btn) btn.title = t('theme.' + cur)
352
+ }
353
+
354
+ function renderThemeOptions() {
355
+ const box = $('theme-options')
356
+ if (!box) return
357
+ const cur = window.DSHTheme.get()
358
+ box.innerHTML = THEME_META.map(m => `
359
+ <button class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" title="${t('theme.' + m.id)}">
360
+ <span class="theme-swatches">${m.sw.map(c => `<i style="background:${c}"></i>`).join('')}</span>
361
+ <span class="theme-name">${t('theme.' + m.id)}</span>
362
+ <span class="theme-check">${m.id === cur ? '✓' : ''}</span>
363
+ </button>`).join('')
364
+ box.querySelectorAll('.theme-option').forEach(btn =>
365
+ btn.addEventListener('click', () => {
366
+ window.DSHTheme.set(btn.dataset.theme)
367
+ renderThemeBtn()
368
+ renderThemeOptions()
369
+ $('modal-theme').classList.add('hidden')
370
+ }))
371
+ }
372
+
373
+ function openThemePanel() {
374
+ renderThemeOptions()
375
+ $('modal-theme').classList.remove('hidden')
376
+ }
377
+
378
+ $('btn-lang').addEventListener('click', () => {
379
+ I18N.setLang(I18N.lang === 'zh' ? 'en' : 'zh')
380
+ renderLangBtn()
381
+ renderThemeBtn()
382
+ if (lastState) render(lastState)
383
+ else if (!token && !pluginMode) $('conn-badge').textContent = t('unauth')
384
+ })
385
+
386
+ $('btn-theme').addEventListener('click', openThemePanel)
387
+ $('theme-close').addEventListener('click', () => $('modal-theme').classList.add('hidden'))
388
+
322
389
  function start(showLogin) {
323
390
  if (!showLogin) {
324
391
  $('login-view').classList.add('hidden')
@@ -342,3 +409,5 @@ if (pluginMode) {
342
409
  $('main-view').classList.add('hidden')
343
410
  $('login-view').classList.remove('hidden')
344
411
  }
412
+ renderLangBtn()
413
+ renderThemeBtn()