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/apk/dsh-remote.apk +0 -0
- package/package.json +1 -1
- package/public/admin.html +245 -45
- package/public/admin.js +137 -68
- package/public/app.js +294 -201
- package/public/i18n.js +52 -0
- package/public/index.html +328 -67
- package/public/styles.css +542 -173
- package/public/theme.js +57 -0
- package/public/update.json +3 -3
- package/public/version.json +1 -1
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) + '
|
|
37
|
-
return Math.floor(sec / 86400) + '
|
|
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('
|
|
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 ? '
|
|
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('
|
|
93
|
+
const hostIPs = (st.lanIPs || []).join(t('stat.ipSep')) || '127.0.0.1'
|
|
91
94
|
const latestHtml = st.latest?.newer
|
|
92
|
-
? `<div class="v"
|
|
93
|
-
: `<div class="v">v${st.version}</div><div class="k">${isPlugin ? '
|
|
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 ? '
|
|
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:
|
|
98
|
-
<div class="stat-card ${upOk ? 'ok' : 'warn'}"><div class="v">${upOk ? '
|
|
99
|
-
<div class="stat-card"><div class="v">${st.onlineCount}/${st.deviceCount}</div><div class="k"
|
|
100
|
-
<div class="stat-card"><div class="v">${st.totalRequests}</div><div class="k"
|
|
101
|
-
<div class="stat-card"><div class="v">${st.authFailures}</div><div class="k"
|
|
102
|
-
<div class="stat-card"><div class="v">${fmtUptime(st.uptimeSec)}</div><div class="k"
|
|
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
|
-
:
|
|
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"
|
|
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
|
|
119
|
+
<div>${t('device.noBinaryGuide')}</div>
|
|
117
120
|
<div class="empty-actions">
|
|
118
|
-
<a class="mini-btn" href="${rel}${gwAsset}" target="_blank" rel="noopener"
|
|
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"
|
|
124
|
+
<div class="muted" style="margin-top:10px">${t('device.afterRunGuide')}</div>`
|
|
122
125
|
} else {
|
|
123
126
|
$('device-empty').innerHTML = `
|
|
124
|
-
<div
|
|
127
|
+
<div>${t('device.installedGuide')}</div>
|
|
125
128
|
<div class="empty-actions">${apkBtn}</div>
|
|
126
|
-
<div class="muted" style="margin-top:10px"
|
|
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 => ({'<':'<','>':'>','&':'&','"':'"'}[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 ? '
|
|
136
|
-
<td
|
|
137
|
-
<td><span class="badge ${d.kind}">${
|
|
138
|
-
<td class="mono">${d.ip}</td>
|
|
139
|
-
<td
|
|
140
|
-
<td>${d.requests}</td>
|
|
141
|
-
<td>${fmtTime(d.lastSeen)}</td>
|
|
142
|
-
<td class="ua" title="${d.ua.replace(/"/g, '"')}">${d.ua || '—'}</td>
|
|
143
|
-
<td>${d.online && d.kind !== 'admin' ? `<button class="mini-btn" data-kick="${d.ip}"
|
|
144
|
-
</tr>`
|
|
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, '"')}" 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, '"')}">${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
|
|
178
|
+
const pt = pairTarget(st)
|
|
171
179
|
const qr = window.qrcode(0, 'M')
|
|
172
|
-
qr.addData(
|
|
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 =
|
|
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('
|
|
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('
|
|
200
|
+
toast(t('toast.noteSaved'), 'ok')
|
|
193
201
|
setTimeout(loadState, 300)
|
|
194
202
|
} else {
|
|
195
|
-
toast('
|
|
203
|
+
toast(t('toast.noteFailed'), 'err')
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
async function kick(ip) {
|
|
200
|
-
if (!confirm('
|
|
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('
|
|
215
|
+
toast(t('toast.kicked', { ip }), 'ok')
|
|
208
216
|
setTimeout(loadState, 400)
|
|
209
217
|
} else {
|
|
210
|
-
toast('
|
|
218
|
+
toast(t('toast.opFailed'), 'err')
|
|
211
219
|
}
|
|
212
220
|
}
|
|
213
221
|
|
|
214
222
|
function enter() {
|
|
215
|
-
const
|
|
216
|
-
if (!
|
|
217
|
-
token =
|
|
218
|
-
store.set('dshAdminToken',
|
|
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('
|
|
259
|
+
toast(t('toast.tokenCopied'), 'ok')
|
|
252
260
|
} catch {
|
|
253
|
-
toast('
|
|
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('
|
|
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('
|
|
280
|
+
toast(t('toast.popupBlocked'), 'err')
|
|
273
281
|
}
|
|
274
282
|
})
|
|
275
283
|
|
|
276
284
|
$('btn-rotate').addEventListener('click', async () => {
|
|
277
|
-
if (!confirm('
|
|
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('
|
|
295
|
+
toast(t('toast.rotated'), 'ok')
|
|
288
296
|
setTimeout(loadState, 300)
|
|
289
297
|
} else {
|
|
290
|
-
toast(out.detail || out.error || '
|
|
298
|
+
toast(out.detail || out.error || t('toast.rotateFailed'), 'err')
|
|
291
299
|
}
|
|
292
300
|
} catch (e) {
|
|
293
|
-
toast('
|
|
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 ? '
|
|
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 || '
|
|
321
|
+
toast(out.error || t('toast.opFailed'), 'err')
|
|
314
322
|
}
|
|
315
323
|
} catch (e) {
|
|
316
|
-
toast('
|
|
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()
|