dsh-remote-plugin 0.6.10 → 0.6.11
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.en.md +20 -1
- package/README.md +20 -1
- package/apk/dsh-remote.apk +0 -0
- package/client.js +56 -4
- package/gateway-stats.cjs +40 -11
- package/gateway.cjs +100 -4
- package/package.json +1 -1
- package/public/app.js +524 -43
- package/public/index.html +84 -6
- package/public/plugin-icon.svg +5 -0
- package/public/styles.css +86 -0
- package/public/theme.js +9 -2
- package/public/update.json +8 -4
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -58,6 +58,7 @@ const state = {
|
|
|
58
58
|
selectingServer: false, // 防重入: 测速/切换中
|
|
59
59
|
sessions: [],
|
|
60
60
|
sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
|
|
61
|
+
workspaceFilter: LS.get('workspaceFilterV1', ''),
|
|
61
62
|
byId: new Map(),
|
|
62
63
|
current: null, // 当前打开的 sessionId
|
|
63
64
|
hostInfo: null,
|
|
@@ -77,7 +78,7 @@ const state = {
|
|
|
77
78
|
streamMode: 'ws', // 'ws' | 'poll'
|
|
78
79
|
pollSeq: { mux: 0, host: 0 },
|
|
79
80
|
refreshTimer: null,
|
|
80
|
-
fs: { path: null, initial: null, loaded: false, upload: null },
|
|
81
|
+
fs: { path: null, initial: null, loaded: false, upload: null, workspaceId: LS.get('fsWorkspaceIdV1', ''), preview: null },
|
|
81
82
|
composerImages: [], // 当前草稿中的图片附件:只在发送成功后释放
|
|
82
83
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
83
84
|
wb: null,
|
|
@@ -101,6 +102,135 @@ function toast(text, kind = '') {
|
|
|
101
102
|
toast._t = setTimeout(() => el.classList.add('hidden'), 3200)
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/* ---------------- 应用内选择抽屉 ---------------- */
|
|
106
|
+
const CUSTOM_SELECT_TITLES = {
|
|
107
|
+
'session-workspace-filter': 'workspace.select',
|
|
108
|
+
'session-sort': 'sessions.sortLabel',
|
|
109
|
+
'fs-workspace': 'workspace.select',
|
|
110
|
+
'mobile-enter-action': 'settings.mobileEnterTitle',
|
|
111
|
+
'bg-interval': 'settings.bgIntervalTitle',
|
|
112
|
+
'new-session-workspace': 'workspace.select'
|
|
113
|
+
}
|
|
114
|
+
let customSelectCurrent = null
|
|
115
|
+
|
|
116
|
+
function syncCustomSelect(select) {
|
|
117
|
+
const trigger = select?._customSelectTrigger
|
|
118
|
+
if (!trigger) return
|
|
119
|
+
const option = select.selectedOptions?.[0] || select.options?.[select.selectedIndex]
|
|
120
|
+
const label = trigger.querySelector('.custom-select-trigger-label')
|
|
121
|
+
if (label) label.textContent = option?.textContent?.trim() || t('select.choose')
|
|
122
|
+
trigger.disabled = !!select.disabled
|
|
123
|
+
trigger.setAttribute('aria-expanded', customSelectCurrent?.select === select ? 'true' : 'false')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function closeCustomSelect({ restoreFocus = true } = {}) {
|
|
127
|
+
if (!$('custom-select-sheet')) return
|
|
128
|
+
const previous = customSelectCurrent
|
|
129
|
+
customSelectCurrent = null
|
|
130
|
+
$('custom-select-backdrop').classList.add('hidden')
|
|
131
|
+
$('custom-select-sheet').classList.add('hidden')
|
|
132
|
+
if (previous?.select) syncCustomSelect(previous.select)
|
|
133
|
+
if (restoreFocus) previous?.trigger?.focus?.({ preventScroll: true })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function customSelectOptionCopy(option) {
|
|
137
|
+
const copy = document.createElement('span')
|
|
138
|
+
copy.className = 'custom-select-option-copy'
|
|
139
|
+
const raw = option.textContent?.trim() || ''
|
|
140
|
+
const separator = raw.indexOf(' — ')
|
|
141
|
+
const name = document.createElement('span')
|
|
142
|
+
name.className = 'custom-select-option-name'
|
|
143
|
+
name.textContent = separator >= 0 ? raw.slice(0, separator) : raw
|
|
144
|
+
copy.appendChild(name)
|
|
145
|
+
if (separator >= 0) {
|
|
146
|
+
const path = document.createElement('span')
|
|
147
|
+
path.className = 'custom-select-option-path'
|
|
148
|
+
path.textContent = raw.slice(separator + 3)
|
|
149
|
+
copy.appendChild(path)
|
|
150
|
+
}
|
|
151
|
+
return copy
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function openCustomSelect(select) {
|
|
155
|
+
const trigger = select?._customSelectTrigger
|
|
156
|
+
if (!select || !trigger || select.disabled) return
|
|
157
|
+
closeFeedbackSheet()
|
|
158
|
+
syncCustomSelect(select)
|
|
159
|
+
customSelectCurrent = { select, trigger }
|
|
160
|
+
const titleKey = CUSTOM_SELECT_TITLES[select.id]
|
|
161
|
+
$('custom-select-title').textContent = t(titleKey || 'select.choose')
|
|
162
|
+
const options = $('custom-select-options')
|
|
163
|
+
options.replaceChildren()
|
|
164
|
+
let selectedButton = null
|
|
165
|
+
Array.from(select.options).forEach(option => {
|
|
166
|
+
const button = document.createElement('button')
|
|
167
|
+
button.type = 'button'
|
|
168
|
+
button.className = 'custom-select-option' + (option.selected ? ' current' : '')
|
|
169
|
+
button.setAttribute('role', 'option')
|
|
170
|
+
button.setAttribute('aria-selected', option.selected ? 'true' : 'false')
|
|
171
|
+
button.disabled = option.disabled
|
|
172
|
+
button.appendChild(customSelectOptionCopy(option))
|
|
173
|
+
const check = document.createElement('span')
|
|
174
|
+
check.className = 'custom-select-option-check'
|
|
175
|
+
check.setAttribute('aria-hidden', 'true')
|
|
176
|
+
check.textContent = option.selected ? '✓' : ''
|
|
177
|
+
button.appendChild(check)
|
|
178
|
+
button.addEventListener('click', () => {
|
|
179
|
+
if (option.disabled) return
|
|
180
|
+
select.value = option.value
|
|
181
|
+
select.dispatchEvent(new Event('input', { bubbles: true }))
|
|
182
|
+
select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
183
|
+
syncCustomSelect(select)
|
|
184
|
+
closeCustomSelect()
|
|
185
|
+
})
|
|
186
|
+
options.appendChild(button)
|
|
187
|
+
if (option.selected) selectedButton = button
|
|
188
|
+
})
|
|
189
|
+
$('custom-select-backdrop').classList.remove('hidden')
|
|
190
|
+
$('custom-select-sheet').classList.remove('hidden')
|
|
191
|
+
syncCustomSelect(select)
|
|
192
|
+
setTimeout(() => (selectedButton || options.querySelector('button:not(:disabled)'))?.focus(), 30)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function enhanceCustomSelect(select) {
|
|
196
|
+
if (!select || select._customSelectTrigger || select.multiple || Number(select.size) > 1) return
|
|
197
|
+
select.classList.add('custom-select-native')
|
|
198
|
+
select.tabIndex = -1
|
|
199
|
+
select.setAttribute('aria-hidden', 'true')
|
|
200
|
+
const trigger = document.createElement('button')
|
|
201
|
+
trigger.type = 'button'
|
|
202
|
+
trigger.id = `${select.id}-trigger`
|
|
203
|
+
trigger.className = 'custom-select-trigger'
|
|
204
|
+
if (select.classList.contains('workspace-select')) trigger.classList.add('workspace-select-trigger')
|
|
205
|
+
if (select.classList.contains('full')) trigger.classList.add('full')
|
|
206
|
+
if (select.classList.contains('session-sort')) trigger.classList.add('session-sort-trigger')
|
|
207
|
+
if (select.classList.contains('setting-select')) trigger.classList.add('setting-select-trigger')
|
|
208
|
+
trigger.setAttribute('aria-haspopup', 'listbox')
|
|
209
|
+
trigger.setAttribute('aria-expanded', 'false')
|
|
210
|
+
const label = document.createElement('span')
|
|
211
|
+
label.className = 'custom-select-trigger-label'
|
|
212
|
+
trigger.appendChild(label)
|
|
213
|
+
trigger.addEventListener('click', () => openCustomSelect(select))
|
|
214
|
+
select.insertAdjacentElement('afterend', trigger)
|
|
215
|
+
select._customSelectTrigger = trigger
|
|
216
|
+
const associatedLabel = document.querySelector(`label[for="${CSS.escape(select.id)}"]`)
|
|
217
|
+
if (associatedLabel) associatedLabel.htmlFor = trigger.id
|
|
218
|
+
select.addEventListener('change', () => syncCustomSelect(select))
|
|
219
|
+
select._customSelectObserver = new MutationObserver(() => syncCustomSelect(select))
|
|
220
|
+
select._customSelectObserver.observe(select, { childList: true, subtree: true, attributes: true, attributeFilter: ['disabled', 'selected'] })
|
|
221
|
+
syncCustomSelect(select)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function initCustomSelects() {
|
|
225
|
+
document.querySelectorAll('select').forEach(enhanceCustomSelect)
|
|
226
|
+
$('custom-select-backdrop').addEventListener('click', () => closeCustomSelect())
|
|
227
|
+
$('custom-select-close').addEventListener('click', () => closeCustomSelect())
|
|
228
|
+
$('custom-select-cancel').addEventListener('click', () => closeCustomSelect())
|
|
229
|
+
document.addEventListener('keydown', (event) => {
|
|
230
|
+
if (event.key === 'Escape' && customSelectCurrent) closeCustomSelect()
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
104
234
|
/* ---------------- 反馈 ---------------- */
|
|
105
235
|
const FEEDBACK_LINKS = {
|
|
106
236
|
githubIssues: 'https://github.com/Blank-not-black/dsh-Remote/issues',
|
|
@@ -151,7 +281,8 @@ async function submitFeedback() {
|
|
|
151
281
|
const btn = $('fb-submit')
|
|
152
282
|
btn.disabled = true
|
|
153
283
|
try {
|
|
154
|
-
const base = (
|
|
284
|
+
const base = updateBase()
|
|
285
|
+
if (!base) throw new Error(t('feedback.networkError'))
|
|
155
286
|
const res = await fetch(base + '/feedback', {
|
|
156
287
|
method: 'POST',
|
|
157
288
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
|
|
@@ -1244,6 +1375,58 @@ function wbJoin(root, name) {
|
|
|
1244
1375
|
function workbenchRoot() {
|
|
1245
1376
|
return state.wb?.bound && state.wb.path ? state.wb.path : ''
|
|
1246
1377
|
}
|
|
1378
|
+
const WORKSPACE_UNGROUPED = '__ungrouped__'
|
|
1379
|
+
function workspaceItems() {
|
|
1380
|
+
return (state.wbProjects || []).filter(w => w && typeof w.workspaceId === 'string' && w.workspaceId && typeof w.path === 'string' && w.path)
|
|
1381
|
+
}
|
|
1382
|
+
function workspaceById(workspaceId) {
|
|
1383
|
+
return workspaceItems().find(w => w.workspaceId === workspaceId) || null
|
|
1384
|
+
}
|
|
1385
|
+
function workspaceForSession(session) {
|
|
1386
|
+
if (!session) return null
|
|
1387
|
+
const byMembership = workspaceItems().find(w => Array.isArray(w.sessionIds) && w.sessionIds.includes(session.sessionId))
|
|
1388
|
+
if (byMembership) return byMembership
|
|
1389
|
+
const cwdKey = wbPathKey(sessionCwd(session))
|
|
1390
|
+
return cwdKey ? workspaceItems().find(w => wbPathKey(w.path) === cwdKey) || null : null
|
|
1391
|
+
}
|
|
1392
|
+
function workspaceName(workspace) {
|
|
1393
|
+
return String(workspace?.title || wbBaseName(workspace?.path) || workspace?.path || '').trim()
|
|
1394
|
+
}
|
|
1395
|
+
function workspaceOptionLabel(workspace) {
|
|
1396
|
+
const name = workspaceName(workspace)
|
|
1397
|
+
return workspace.path && workspace.path !== name ? `${name} — ${workspace.path}` : name
|
|
1398
|
+
}
|
|
1399
|
+
function workspaceOptionsHtml({ all = false, ungrouped = false, root = false, selected = '' } = {}) {
|
|
1400
|
+
const rows = []
|
|
1401
|
+
if (all) rows.push({ id: '', label: t('workspace.all') })
|
|
1402
|
+
if (root) rows.push({ id: '', label: t('fs.root') })
|
|
1403
|
+
for (const workspace of workspaceItems()) rows.push({ id: workspace.workspaceId, label: workspaceOptionLabel(workspace) })
|
|
1404
|
+
if (ungrouped) rows.push({ id: WORKSPACE_UNGROUPED, label: t('workspace.ungrouped') })
|
|
1405
|
+
return rows.map(row => `<option value="${esc(row.id)}"${row.id === selected ? ' selected' : ''}>${esc(row.label)}</option>`).join('')
|
|
1406
|
+
}
|
|
1407
|
+
function renderWorkspaceNavigation() {
|
|
1408
|
+
const items = workspaceItems()
|
|
1409
|
+
if (state.workspaceFilter !== WORKSPACE_UNGROUPED && state.workspaceFilter && !workspaceById(state.workspaceFilter)) {
|
|
1410
|
+
state.workspaceFilter = ''
|
|
1411
|
+
LS.del('workspaceFilterV1')
|
|
1412
|
+
}
|
|
1413
|
+
const sessionSelect = $('session-workspace-filter')
|
|
1414
|
+
if (sessionSelect) sessionSelect.innerHTML = workspaceOptionsHtml({ all: true, ungrouped: true, selected: state.workspaceFilter })
|
|
1415
|
+
const selectedWorkspace = workspaceById(state.workspaceFilter)
|
|
1416
|
+
const pathBox = $('session-workspace-path')
|
|
1417
|
+
if (pathBox) pathBox.textContent = selectedWorkspace?.path || (state.workspaceFilter === WORKSPACE_UNGROUPED ? t('workspace.ungrouped') : t('workspace.allPath'))
|
|
1418
|
+
const filesButton = $('session-workspace-files')
|
|
1419
|
+
if (filesButton) filesButton.disabled = !selectedWorkspace
|
|
1420
|
+
|
|
1421
|
+
if (state.fs.workspaceId && !workspaceById(state.fs.workspaceId)) {
|
|
1422
|
+
state.fs.workspaceId = ''
|
|
1423
|
+
LS.del('fsWorkspaceIdV1')
|
|
1424
|
+
}
|
|
1425
|
+
const fsSelect = $('fs-workspace')
|
|
1426
|
+
if (fsSelect) fsSelect.innerHTML = workspaceOptionsHtml({ root: true, selected: state.fs.workspaceId })
|
|
1427
|
+
if ($('modal-new-session') && !$('modal-new-session').classList.contains('hidden')) renderNewSessionWorkspace()
|
|
1428
|
+
return items
|
|
1429
|
+
}
|
|
1247
1430
|
async function refreshWorkbench() {
|
|
1248
1431
|
if (!state.token) return
|
|
1249
1432
|
try {
|
|
@@ -1271,7 +1454,9 @@ async function refreshWorkbench() {
|
|
|
1271
1454
|
const listData = await listRes.json().catch(() => ({}))
|
|
1272
1455
|
if (Array.isArray(listData.entries)) {
|
|
1273
1456
|
const diskDirs = new Set(listData.entries.filter(e => e.type === 'dir').map(e => wbPathKey(wbJoin(state.wb.path, e.name))))
|
|
1274
|
-
|
|
1457
|
+
// 工作台只管理自己根目录下的项目;DSH 中位于其他路径的工作区必须保留,
|
|
1458
|
+
// 否则手机的工作区选择器会因绑定了工作台而丢失条目。
|
|
1459
|
+
state.wbProjects = state.wbProjects.filter(w => !wbStrictInside(w.path, state.wb.path) || diskDirs.has(wbPathKey(w.path)))
|
|
1275
1460
|
const have = new Set(state.wbProjects.map(w => wbPathKey(w.path)))
|
|
1276
1461
|
for (const entry of listData.entries) {
|
|
1277
1462
|
if (entry.type !== 'dir') continue
|
|
@@ -1286,6 +1471,7 @@ async function refreshWorkbench() {
|
|
|
1286
1471
|
}
|
|
1287
1472
|
} catch {}
|
|
1288
1473
|
}
|
|
1474
|
+
renderWorkspaceNavigation()
|
|
1289
1475
|
renderWorkbench()
|
|
1290
1476
|
renderSessions()
|
|
1291
1477
|
}
|
|
@@ -1338,8 +1524,12 @@ function renderWorkbench() {
|
|
|
1338
1524
|
|
|
1339
1525
|
function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
|
|
1340
1526
|
function sessionWorkspaceLabel(s) {
|
|
1341
|
-
const
|
|
1342
|
-
return
|
|
1527
|
+
const workspace = workspaceForSession(s)
|
|
1528
|
+
return workspace?.path || sessionCwd(s) || t('sessions.workspaceUnknown')
|
|
1529
|
+
}
|
|
1530
|
+
function sessionWorkspaceName(s) {
|
|
1531
|
+
const workspace = workspaceForSession(s)
|
|
1532
|
+
return workspace ? workspaceName(workspace) : workspaceDisplayName(sessionWorkspaceLabel(s))
|
|
1343
1533
|
}
|
|
1344
1534
|
function workspaceDisplayName(label) {
|
|
1345
1535
|
const value = String(label || '').trim()
|
|
@@ -1363,14 +1553,12 @@ function sortedSessions() {
|
|
|
1363
1553
|
function renderSessions() {
|
|
1364
1554
|
const list = $('session-list')
|
|
1365
1555
|
const allItems = sortedSessions()
|
|
1366
|
-
const wbIds = new Set()
|
|
1367
|
-
if (state.wb?.bound) for (const w of state.wbProjects) for (const id of (w.sessionIds || [])) wbIds.add(id)
|
|
1368
|
-
const root = workbenchRoot()
|
|
1369
1556
|
const archivedSet = new Set(state.wbArchived || [])
|
|
1370
1557
|
const visible = allItems.filter(s => {
|
|
1371
|
-
if (!state.
|
|
1372
|
-
|
|
1373
|
-
|
|
1558
|
+
if (!state.workspaceFilter) return true
|
|
1559
|
+
const workspace = workspaceForSession(s)
|
|
1560
|
+
if (state.workspaceFilter === WORKSPACE_UNGROUPED) return !workspace
|
|
1561
|
+
return workspace?.workspaceId === state.workspaceFilter
|
|
1374
1562
|
})
|
|
1375
1563
|
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1376
1564
|
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
@@ -1380,9 +1568,9 @@ function renderSessions() {
|
|
|
1380
1568
|
const rows = []
|
|
1381
1569
|
for (const s of items) {
|
|
1382
1570
|
const workspace = sessionWorkspaceLabel(s)
|
|
1383
|
-
const
|
|
1571
|
+
const workspaceTitle = sessionWorkspaceName(s)
|
|
1384
1572
|
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1385
|
-
rows.push(`<div class="session-group-label" title="${esc(workspace)}"><span class="session-group-icon" aria-hidden="true">⌂</span><span class="session-group-name">${esc(
|
|
1573
|
+
rows.push(`<div class="session-group-label" title="${esc(workspace)}"><span class="session-group-icon" aria-hidden="true">⌂</span><span class="session-group-name">${esc(workspaceTitle)}</span></div>`)
|
|
1386
1574
|
lastWorkspace = workspace
|
|
1387
1575
|
}
|
|
1388
1576
|
const title = titleOf(s)
|
|
@@ -1404,7 +1592,7 @@ function renderSessions() {
|
|
|
1404
1592
|
${s.running ? '<span>' + t('sessions.running') + '</span>' : ''}
|
|
1405
1593
|
${badge}${queueBadge}
|
|
1406
1594
|
</div>
|
|
1407
|
-
<div class="sc-workspace" title="${esc(workspace)}">⌂ ${esc(
|
|
1595
|
+
<div class="sc-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceTitle)}</div>
|
|
1408
1596
|
<span class="sc-arrow">›</span>
|
|
1409
1597
|
</div>
|
|
1410
1598
|
${archiveButton}
|
|
@@ -1414,11 +1602,10 @@ function renderSessions() {
|
|
|
1414
1602
|
}
|
|
1415
1603
|
const divider = archived.length ? `<button class="archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1416
1604
|
const rows = renderItems(main) + divider + (showArchived ? renderItems(archived) : '')
|
|
1417
|
-
|
|
1418
|
-
list.innerHTML = rows || `<div class="empty">${esc(hiddenByWorkbench ? t('wb.flatHidden', { n: hiddenByWorkbench }) : t('home.empty'))}</div>`
|
|
1605
|
+
list.innerHTML = rows || `<div class="empty">${esc(t('home.empty'))}</div>`
|
|
1419
1606
|
list.classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1420
1607
|
const sort = $('session-sort')
|
|
1421
|
-
if (sort) sort.value = state.sessionSort
|
|
1608
|
+
if (sort) { sort.value = state.sessionSort; syncCustomSelect(sort) }
|
|
1422
1609
|
$('home-empty').classList.toggle('hidden', visible.length > 0)
|
|
1423
1610
|
const running = state.sessions.filter(s => s.running).length
|
|
1424
1611
|
const pending = state.approvals.length + state.questions.length
|
|
@@ -1460,6 +1647,7 @@ function bindNativeBack() {
|
|
|
1460
1647
|
try {
|
|
1461
1648
|
CAP.Plugins?.App?.addListener?.('backButton', () => {
|
|
1462
1649
|
if ($('composer-wrap')?.classList.contains('fs')) { setComposerFullscreen(false); return }
|
|
1650
|
+
if (customSelectCurrent) { closeCustomSelect(); return }
|
|
1463
1651
|
const openModal = [...document.querySelectorAll('.modal')].find(m => !m.classList.contains('hidden'))
|
|
1464
1652
|
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1465
1653
|
if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
|
|
@@ -2232,19 +2420,41 @@ async function cancelSession() {
|
|
|
2232
2420
|
}
|
|
2233
2421
|
|
|
2234
2422
|
async function newSession() {
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2423
|
+
if (!state.token) { showView('view-settings'); return }
|
|
2424
|
+
if (!workspaceItems().length) await refreshWorkbench()
|
|
2425
|
+
const preferred = workspaceById(state.workspaceFilter)?.workspaceId || LS.get('lastNewSessionWorkspaceV1', '')
|
|
2426
|
+
renderNewSessionWorkspace(preferred)
|
|
2427
|
+
$('modal-new-session').classList.remove('hidden')
|
|
2428
|
+
}
|
|
2429
|
+
function renderNewSessionWorkspace(preferredId = '') {
|
|
2430
|
+
const items = workspaceItems()
|
|
2431
|
+
const select = $('new-session-workspace')
|
|
2432
|
+
if (!select) return
|
|
2433
|
+
const current = workspaceById(preferredId || select.value)?.workspaceId || items[0]?.workspaceId || ''
|
|
2434
|
+
select.innerHTML = workspaceOptionsHtml({ selected: current })
|
|
2435
|
+
select.disabled = !items.length
|
|
2436
|
+
const workspace = workspaceById(select.value)
|
|
2437
|
+
$('new-session-workspace-path').textContent = workspace?.path || ''
|
|
2438
|
+
$('new-session-empty').classList.toggle('hidden', !!items.length)
|
|
2439
|
+
$('new-session-create').disabled = !workspace
|
|
2440
|
+
}
|
|
2441
|
+
function closeNewSessionModal() {
|
|
2442
|
+
$('modal-new-session').classList.add('hidden')
|
|
2443
|
+
}
|
|
2444
|
+
async function createSessionInWorkspace() {
|
|
2445
|
+
const workspaceId = $('new-session-workspace').value
|
|
2446
|
+
if (!workspaceById(workspaceId)) return toast(t('newSession.chooseWorkspace'), 'err')
|
|
2447
|
+
const button = $('new-session-create')
|
|
2448
|
+
button.disabled = true
|
|
2449
|
+
const v = await safeRpc('session.create', { workspaceId: workspaceId }, t('home.createFailed'))
|
|
2450
|
+
button.disabled = false
|
|
2247
2451
|
if (!v?.sessionId) return
|
|
2452
|
+
state.workspaceFilter = workspaceId
|
|
2453
|
+
state.fs.workspaceId = workspaceId
|
|
2454
|
+
LS.set('workspaceFilterV1', workspaceId)
|
|
2455
|
+
LS.set('fsWorkspaceIdV1', workspaceId)
|
|
2456
|
+
LS.set('lastNewSessionWorkspaceV1', workspaceId)
|
|
2457
|
+
closeNewSessionModal()
|
|
2248
2458
|
toast(t('home.created'), 'ok')
|
|
2249
2459
|
await refreshSessions()
|
|
2250
2460
|
openSession(v.sessionId)
|
|
@@ -2554,6 +2764,32 @@ function fsParent(p) {
|
|
|
2554
2764
|
return clean.slice(0, idx)
|
|
2555
2765
|
}
|
|
2556
2766
|
|
|
2767
|
+
const FS_PREVIEW_EXTENSIONS = new Set([
|
|
2768
|
+
'.txt', '.md', '.markdown', '.log', '.json', '.jsonl', '.js', '.mjs', '.cjs', '.jsx',
|
|
2769
|
+
'.ts', '.tsx', '.py', '.css', '.html', '.htm', '.xml', '.yaml', '.yml', '.toml',
|
|
2770
|
+
'.ini', '.conf', '.env', '.sh', '.bash', '.zsh', '.fish', '.sql', '.java', '.kt',
|
|
2771
|
+
'.kts', '.go', '.rs', '.c', '.h', '.cpp', '.hpp', '.cs', '.php', '.rb', '.vue',
|
|
2772
|
+
'.svelte', '.gradle', '.properties', '.gitignore', '.dockerfile'
|
|
2773
|
+
])
|
|
2774
|
+
function fsPreviewExtension(name) {
|
|
2775
|
+
const lower = String(name || '').toLowerCase()
|
|
2776
|
+
if (lower === 'dockerfile') return '.dockerfile'
|
|
2777
|
+
const dot = lower.lastIndexOf('.')
|
|
2778
|
+
return dot >= 0 ? lower.slice(dot) : ''
|
|
2779
|
+
}
|
|
2780
|
+
function fsCanPreview(name, size) {
|
|
2781
|
+
return Number(size) <= 1024 * 1024 && FS_PREVIEW_EXTENSIONS.has(fsPreviewExtension(name))
|
|
2782
|
+
}
|
|
2783
|
+
function openWorkspaceFiles(workspaceId) {
|
|
2784
|
+
const workspace = workspaceById(workspaceId)
|
|
2785
|
+
if (!workspace) return
|
|
2786
|
+
state.fs.workspaceId = workspace.workspaceId
|
|
2787
|
+
state.fs.loaded = false
|
|
2788
|
+
LS.set('fsWorkspaceIdV1', workspace.workspaceId)
|
|
2789
|
+
renderWorkspaceNavigation()
|
|
2790
|
+
showView('view-files')
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2557
2793
|
async function openWorkspaceModal() {
|
|
2558
2794
|
if (!state.token) { toast(t('fs.noTokenToast'), 'err'); showView('view-settings'); return }
|
|
2559
2795
|
if (!state.fs.path) await loadFs(null, { silent: true })
|
|
@@ -2581,7 +2817,19 @@ async function createWorkspace() {
|
|
|
2581
2817
|
}
|
|
2582
2818
|
closeWorkspaceModal()
|
|
2583
2819
|
await loadFs(parent || null, { silent: true })
|
|
2584
|
-
|
|
2820
|
+
let workspace = null
|
|
2821
|
+
try {
|
|
2822
|
+
const created = await rpc('workspace.create', { path: data.path })
|
|
2823
|
+
workspace = created?.workspace || null
|
|
2824
|
+
} catch {}
|
|
2825
|
+
const sessionPayload = workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: data.path }
|
|
2826
|
+
const v = await safeRpc('session.create', sessionPayload, t('home.createFailed'))
|
|
2827
|
+
if (workspace?.workspaceId) {
|
|
2828
|
+
state.workspaceFilter = workspace.workspaceId
|
|
2829
|
+
state.fs.workspaceId = workspace.workspaceId
|
|
2830
|
+
LS.set('workspaceFilterV1', workspace.workspaceId)
|
|
2831
|
+
LS.set('fsWorkspaceIdV1', workspace.workspaceId)
|
|
2832
|
+
}
|
|
2585
2833
|
await refreshSessions()
|
|
2586
2834
|
if (v?.sessionId) {
|
|
2587
2835
|
toast(t('workspace.created'), 'ok')
|
|
@@ -2648,17 +2896,18 @@ function renderFs(data) {
|
|
|
2648
2896
|
}
|
|
2649
2897
|
list.innerHTML = data.entries.map(e => {
|
|
2650
2898
|
const isDir = e.type === 'dir'
|
|
2651
|
-
|
|
2899
|
+
const preview = !isDir && fsCanPreview(e.name, e.size)
|
|
2900
|
+
return `<div class="fs-row" data-name="${esc(e.name)}" data-type="${esc(e.type)}" data-size="${Number(e.size) || 0}">
|
|
2652
2901
|
<span class="fs-ico">${fsIconSvg(isDir)}</span>
|
|
2653
2902
|
<span class="fs-meta">
|
|
2654
2903
|
<span class="fs-name">${esc(e.name)}</span>
|
|
2655
2904
|
<span class="fs-sub">${isDir ? t('fs.dir') : fmtSize(e.size)} · ${fmtFullTime(e.mtimeMs)}</span>
|
|
2656
2905
|
</span>
|
|
2657
|
-
<span class="fs-arrow">${isDir ? '›' : '↓'}</span>
|
|
2906
|
+
<span class="fs-arrow">${isDir || preview ? '›' : '↓'}</span>
|
|
2658
2907
|
</div>`
|
|
2659
2908
|
}).join('')
|
|
2660
2909
|
list.querySelectorAll('.fs-row').forEach(row =>
|
|
2661
|
-
row.addEventListener('click', () => fsOpenEntry(row.dataset.name, row.dataset.type)))
|
|
2910
|
+
row.addEventListener('click', () => fsOpenEntry(row.dataset.name, row.dataset.type, Number(row.dataset.size))))
|
|
2662
2911
|
}
|
|
2663
2912
|
|
|
2664
2913
|
function fsIconSvg(isDir) {
|
|
@@ -2667,15 +2916,20 @@ function fsIconSvg(isDir) {
|
|
|
2667
2916
|
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3.5h8l4 4V20a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1Z"/><path d="M14 3.5v4h4M8 13h8M8 16h6"/></svg>'
|
|
2668
2917
|
}
|
|
2669
2918
|
|
|
2670
|
-
function fsOpenEntry(name, type) {
|
|
2919
|
+
function fsOpenEntry(name, type, size = 0) {
|
|
2671
2920
|
if (!name) return
|
|
2672
2921
|
const p = fsJoin(state.fs.path, name)
|
|
2673
2922
|
if (type === 'dir') return loadFs(p)
|
|
2923
|
+
if (fsCanPreview(name, size)) return openFsPreview(p, name)
|
|
2674
2924
|
downloadFsFile(name)
|
|
2675
2925
|
}
|
|
2676
2926
|
|
|
2677
2927
|
function downloadFsFile(name) {
|
|
2678
2928
|
const p = fsJoin(state.fs.path, name)
|
|
2929
|
+
downloadFsPath(p, name)
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
function downloadFsPath(p, name) {
|
|
2679
2933
|
const url = fsApiUrl('/file', { path: p })
|
|
2680
2934
|
if (CAP?.isNativePlatform?.()) {
|
|
2681
2935
|
if (window.NativeFile?.downloadToDownloads) {
|
|
@@ -2701,6 +2955,54 @@ function downloadFsFile(name) {
|
|
|
2701
2955
|
a.remove()
|
|
2702
2956
|
}
|
|
2703
2957
|
|
|
2958
|
+
let fsPreviewGeneration = 0
|
|
2959
|
+
async function openFsPreview(pathValue, name) {
|
|
2960
|
+
const generation = ++fsPreviewGeneration
|
|
2961
|
+
state.fs.preview = { path: pathValue, name, extension: fsPreviewExtension(name), content: '' }
|
|
2962
|
+
$('file-preview-title').textContent = name
|
|
2963
|
+
$('file-preview-path').textContent = pathValue
|
|
2964
|
+
$('file-preview-loading').textContent = t('fs.previewLoading')
|
|
2965
|
+
$('file-preview-loading').classList.remove('hidden')
|
|
2966
|
+
$('file-preview-source').classList.add('hidden')
|
|
2967
|
+
$('file-preview-rendered').classList.add('hidden')
|
|
2968
|
+
$('file-preview-tabs').classList.add('hidden')
|
|
2969
|
+
$('modal-file-preview').classList.remove('hidden')
|
|
2970
|
+
try {
|
|
2971
|
+
const res = await fetch(fsApiUrl('/preview', { path: pathValue }), { headers: fsHeaders() })
|
|
2972
|
+
if (res.status === 401) { closeFsPreview(); fsAuthError(401); return }
|
|
2973
|
+
const data = await res.json().catch(() => ({}))
|
|
2974
|
+
if (generation !== fsPreviewGeneration) return
|
|
2975
|
+
if (!res.ok) {
|
|
2976
|
+
const message = data.error === 'preview-too-large' ? t('fs.previewTooLarge')
|
|
2977
|
+
: (data.error === 'preview-unsupported' || data.error === 'preview-binary') ? t('fs.previewUnsupported')
|
|
2978
|
+
: t('fs.previewFailed', { msg: data.error || ('HTTP ' + res.status) })
|
|
2979
|
+
throw new Error(message)
|
|
2980
|
+
}
|
|
2981
|
+
state.fs.preview = data
|
|
2982
|
+
$('file-preview-loading').classList.add('hidden')
|
|
2983
|
+
$('file-preview-source').textContent = data.content || ''
|
|
2984
|
+
const markdown = data.extension === '.md' || data.extension === '.markdown'
|
|
2985
|
+
$('file-preview-tabs').classList.toggle('hidden', !markdown)
|
|
2986
|
+
if (markdown) $('file-preview-rendered').innerHTML = window.mdToHtml(data.content || '')
|
|
2987
|
+
showFsPreviewMode(markdown ? 'rendered' : 'source')
|
|
2988
|
+
} catch (e) {
|
|
2989
|
+
if (generation !== fsPreviewGeneration) return
|
|
2990
|
+
$('file-preview-loading').textContent = e.message || t('fs.previewFailed', { msg: t('fs.networkError') })
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
function showFsPreviewMode(mode) {
|
|
2994
|
+
const rendered = mode === 'rendered' && !($('file-preview-tabs').classList.contains('hidden'))
|
|
2995
|
+
$('file-preview-source').classList.toggle('hidden', rendered)
|
|
2996
|
+
$('file-preview-rendered').classList.toggle('hidden', !rendered)
|
|
2997
|
+
$('file-preview-source-tab').classList.toggle('current', !rendered)
|
|
2998
|
+
$('file-preview-rendered-tab').classList.toggle('current', rendered)
|
|
2999
|
+
}
|
|
3000
|
+
function closeFsPreview() {
|
|
3001
|
+
fsPreviewGeneration++
|
|
3002
|
+
state.fs.preview = null
|
|
3003
|
+
$('modal-file-preview').classList.add('hidden')
|
|
3004
|
+
}
|
|
3005
|
+
|
|
2704
3006
|
function showFsProgress(pct, loaded, total) {
|
|
2705
3007
|
$('fs-progress').classList.remove('hidden')
|
|
2706
3008
|
$('fs-progress-bar').style.width = Math.max(2, Math.min(100, pct)) + '%'
|
|
@@ -3094,6 +3396,7 @@ async function loadLocalVersion() {
|
|
|
3094
3396
|
*/
|
|
3095
3397
|
const ANNOUNCEMENTS_KEY = 'seenAnnouncementsV1'
|
|
3096
3398
|
const ANNOUNCEMENT_HISTORY_KEY = 'announcementHistoryV1'
|
|
3399
|
+
const ANNOUNCEMENT_VOTES_KEY = 'announcementVotesV1'
|
|
3097
3400
|
function readSeenAnnouncements() {
|
|
3098
3401
|
try {
|
|
3099
3402
|
const value = JSON.parse(LS.get(ANNOUNCEMENTS_KEY, '{}'))
|
|
@@ -3111,6 +3414,32 @@ function markAnnouncementSeen(id) {
|
|
|
3111
3414
|
}
|
|
3112
3415
|
LS.set(ANNOUNCEMENTS_KEY, JSON.stringify(seen))
|
|
3113
3416
|
}
|
|
3417
|
+
function readAnnouncementVotes() {
|
|
3418
|
+
try {
|
|
3419
|
+
const value = JSON.parse(LS.get(ANNOUNCEMENT_VOTES_KEY, '{}'))
|
|
3420
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
3421
|
+
} catch { return {} }
|
|
3422
|
+
}
|
|
3423
|
+
function announcementVoteKey(announcementId, pollId) {
|
|
3424
|
+
return `${announcementId}\u0000${pollId}`
|
|
3425
|
+
}
|
|
3426
|
+
function storeAnnouncementVote(announcementId, pollId, optionId) {
|
|
3427
|
+
const votes = readAnnouncementVotes()
|
|
3428
|
+
votes[announcementVoteKey(announcementId, pollId)] = { optionId, votedAt: Date.now() }
|
|
3429
|
+
const keys = Object.keys(votes)
|
|
3430
|
+
if (keys.length > 100) {
|
|
3431
|
+
keys.sort((a, b) => Number(votes[a]?.votedAt || 0) - Number(votes[b]?.votedAt || 0))
|
|
3432
|
+
for (const key of keys.slice(0, keys.length - 100)) delete votes[key]
|
|
3433
|
+
}
|
|
3434
|
+
LS.set(ANNOUNCEMENT_VOTES_KEY, JSON.stringify(votes))
|
|
3435
|
+
}
|
|
3436
|
+
function announcementVote(item) {
|
|
3437
|
+
if (!item?.poll?.id) return null
|
|
3438
|
+
const saved = readAnnouncementVotes()[announcementVoteKey(item.id, item.poll.id)]
|
|
3439
|
+
if (!saved?.optionId) return null
|
|
3440
|
+
const option = item.poll.options.find(entry => entry.id === saved.optionId)
|
|
3441
|
+
return option ? { ...saved, option } : null
|
|
3442
|
+
}
|
|
3114
3443
|
function readAnnouncementHistory() {
|
|
3115
3444
|
try {
|
|
3116
3445
|
const value = JSON.parse(LS.get(ANNOUNCEMENT_HISTORY_KEY, '[]'))
|
|
@@ -3135,7 +3464,11 @@ function renderAnnouncementHistory() {
|
|
|
3135
3464
|
box.innerHTML = list.map(item => {
|
|
3136
3465
|
const date = Number(item.publishedAt) > 0 ? fmtFullTime(item.publishedAt) : t('announcement.noDate')
|
|
3137
3466
|
const action = item.actionUrl ? `<a class="announcement-action" href="${esc(item.actionUrl)}" target="_blank" rel="noopener">${esc(item.actionText || t('announcement.open'))}</a>` : ''
|
|
3138
|
-
|
|
3467
|
+
const vote = announcementVote(item)
|
|
3468
|
+
const pollAction = item.poll ? (vote
|
|
3469
|
+
? `<div class="announcement-poll-status">${esc(t('announcement.voteThanks', { option: vote.option.label }))}</div>`
|
|
3470
|
+
: `<button class="mini-btn" type="button" data-announcement-poll="${esc(item.id)}">${esc(t('announcement.voteFromHistory'))}</button>`) : ''
|
|
3471
|
+
return `<details class="announcement-history-item"><summary><span>${esc(item.title)}</span><small>${esc(date)}</small></summary><div class="announcement-history-content">${esc(item.content).replace(/\r?\n/g, '<br>')}${action}${pollAction}</div></details>`
|
|
3139
3472
|
}).join('')
|
|
3140
3473
|
}
|
|
3141
3474
|
function openAnnouncementHistory() {
|
|
@@ -3153,6 +3486,22 @@ function announcementVersionMatch(item) {
|
|
|
3153
3486
|
if (max && cmpVersion(state.localVersion, max) > 0) return false
|
|
3154
3487
|
return true
|
|
3155
3488
|
}
|
|
3489
|
+
function normalizeAnnouncementPoll(value, announcementId) {
|
|
3490
|
+
if (!value || typeof value !== 'object') return null
|
|
3491
|
+
const id = String(value.id || announcementId || '').trim().slice(0, 120)
|
|
3492
|
+
const question = String(value.question || '').trim().slice(0, 300)
|
|
3493
|
+
if (!id || !question || !Array.isArray(value.options)) return null
|
|
3494
|
+
const seen = new Set()
|
|
3495
|
+
const options = []
|
|
3496
|
+
for (const raw of value.options.slice(0, 8)) {
|
|
3497
|
+
const optionId = String(raw?.id || '').trim().slice(0, 120)
|
|
3498
|
+
const label = String(raw?.label || '').trim().slice(0, 200)
|
|
3499
|
+
if (!optionId || !label || seen.has(optionId)) continue
|
|
3500
|
+
seen.add(optionId)
|
|
3501
|
+
options.push({ id: optionId, label, description: String(raw?.description || '').trim().slice(0, 500) })
|
|
3502
|
+
}
|
|
3503
|
+
return options.length >= 2 ? { id, question, options } : null
|
|
3504
|
+
}
|
|
3156
3505
|
function normalizeAnnouncement(item, base) {
|
|
3157
3506
|
if (!item || typeof item !== 'object') return null
|
|
3158
3507
|
const id = String(item.id || '').trim().slice(0, 120)
|
|
@@ -3176,13 +3525,34 @@ function normalizeAnnouncement(item, base) {
|
|
|
3176
3525
|
id, title, content, actionUrl,
|
|
3177
3526
|
actionText: String(item.actionText || '').trim().slice(0, 80),
|
|
3178
3527
|
publishedAt: Number.isFinite(startsAt) ? startsAt : 0,
|
|
3179
|
-
force: item.force === true
|
|
3180
|
-
|
|
3528
|
+
force: item.force === true,
|
|
3529
|
+
poll: normalizeAnnouncementPoll(item.poll, id)
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
function renderAnnouncementPoll(item) {
|
|
3533
|
+
const panel = $('announcement-poll')
|
|
3534
|
+
const poll = item?.poll
|
|
3535
|
+
panel.classList.toggle('hidden', !poll)
|
|
3536
|
+
if (!poll) return
|
|
3537
|
+
const vote = announcementVote(item)
|
|
3538
|
+
$('announcement-poll-question').textContent = poll.question
|
|
3539
|
+
$('announcement-poll-options').innerHTML = poll.options.map(option => `
|
|
3540
|
+
<label class="announcement-poll-option">
|
|
3541
|
+
<input type="radio" name="announcement-poll-option" value="${esc(option.id)}"${vote?.optionId === option.id ? ' checked' : ''}${vote ? ' disabled' : ''}>
|
|
3542
|
+
<span><strong>${esc(option.label)}</strong>${option.description ? `<small>${esc(option.description)}</small>` : ''}</span>
|
|
3543
|
+
</label>`).join('')
|
|
3544
|
+
const status = $('announcement-poll-status')
|
|
3545
|
+
status.textContent = vote ? t('announcement.voteThanks', { option: vote.option.label }) : ''
|
|
3546
|
+
status.classList.toggle('hidden', !vote)
|
|
3547
|
+
const submit = $('announcement-poll-submit')
|
|
3548
|
+
submit.classList.toggle('hidden', !!vote)
|
|
3549
|
+
submit.disabled = true
|
|
3181
3550
|
}
|
|
3182
3551
|
function openAnnouncementModal(item) {
|
|
3183
3552
|
state.announcement = item
|
|
3184
3553
|
$('announcement-title').textContent = item.title
|
|
3185
3554
|
$('announcement-content').innerHTML = esc(item.content).replace(/\r?\n/g, '<br>')
|
|
3555
|
+
renderAnnouncementPoll(item)
|
|
3186
3556
|
const action = $('announcement-action')
|
|
3187
3557
|
if (item.actionUrl) {
|
|
3188
3558
|
action.href = item.actionUrl
|
|
@@ -3196,6 +3566,48 @@ function openAnnouncementModal(item) {
|
|
|
3196
3566
|
$('announcement-later').classList.toggle('hidden', item.force)
|
|
3197
3567
|
$('modal-announcement').classList.remove('hidden')
|
|
3198
3568
|
}
|
|
3569
|
+
async function submitAnnouncementVote() {
|
|
3570
|
+
const item = state.announcement
|
|
3571
|
+
const poll = item?.poll
|
|
3572
|
+
const optionId = document.querySelector('input[name="announcement-poll-option"]:checked')?.value || ''
|
|
3573
|
+
const option = poll?.options.find(entry => entry.id === optionId)
|
|
3574
|
+
if (!poll || !option) return toast(t('announcement.voteChoose'), 'err')
|
|
3575
|
+
const button = $('announcement-poll-submit')
|
|
3576
|
+
button.disabled = true
|
|
3577
|
+
try {
|
|
3578
|
+
const base = updateBase()
|
|
3579
|
+
if (!base) throw new Error(t('announcement.voteNetworkError'))
|
|
3580
|
+
const res = await fetch(base + '/feedback', {
|
|
3581
|
+
method: 'POST',
|
|
3582
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
|
|
3583
|
+
body: JSON.stringify({
|
|
3584
|
+
type: 'poll',
|
|
3585
|
+
message: `Poll ${poll.id}: ${option.id}`,
|
|
3586
|
+
announcementId: item.id,
|
|
3587
|
+
pollId: poll.id,
|
|
3588
|
+
optionId: option.id,
|
|
3589
|
+
appVersion: state.localVersion
|
|
3590
|
+
})
|
|
3591
|
+
})
|
|
3592
|
+
const data = await res.json().catch(() => ({}))
|
|
3593
|
+
if (res.ok && data.ok) {
|
|
3594
|
+
storeAnnouncementVote(item.id, poll.id, option.id)
|
|
3595
|
+
markAnnouncementSeen(item.id)
|
|
3596
|
+
renderAnnouncementPoll(item)
|
|
3597
|
+
renderAnnouncementHistory()
|
|
3598
|
+
toast(t('announcement.voteThanks', { option: option.label }), 'ok')
|
|
3599
|
+
} else if (res.status === 429) {
|
|
3600
|
+
toast(t('announcement.voteAgainLater'), 'err')
|
|
3601
|
+
button.disabled = false
|
|
3602
|
+
} else {
|
|
3603
|
+
toast(t('announcement.voteFailed', { msg: data.error || res.status }), 'err')
|
|
3604
|
+
button.disabled = false
|
|
3605
|
+
}
|
|
3606
|
+
} catch (e) {
|
|
3607
|
+
toast(t('announcement.voteFailed', { msg: e.message || t('announcement.voteNetworkError') }), 'err')
|
|
3608
|
+
button.disabled = false
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3199
3611
|
function closeAnnouncement(markSeen) {
|
|
3200
3612
|
if (markSeen && state.announcement) markAnnouncementSeen(state.announcement.id)
|
|
3201
3613
|
state.announcement = null
|
|
@@ -3502,6 +3914,7 @@ function applyBgConfigFromNative() {
|
|
|
3502
3914
|
const v = String(cfg.intervalMin ?? 1)
|
|
3503
3915
|
const opts = Array.from($('bg-interval')?.options || [])
|
|
3504
3916
|
if (opts.some(o => o.value === v)) $('bg-interval').value = v
|
|
3917
|
+
syncCustomSelect($('bg-interval'))
|
|
3505
3918
|
if ($('opt-task-done')) $('opt-task-done').checked = cfg.notifyTaskDone !== false
|
|
3506
3919
|
$('bg-auth-status')?.classList.toggle('hidden', !cfg.loginExpired)
|
|
3507
3920
|
} catch {}
|
|
@@ -3594,9 +4007,24 @@ function deletePreset(id) {
|
|
|
3594
4007
|
|
|
3595
4008
|
/* ---------------- 峰谷计费提醒(前台服务进程内定时, 绕开 MIUI 后台限制) ---------------- */
|
|
3596
4009
|
function peakRemindOn() { return LS.get('peakRemind', '0') === '1' }
|
|
4010
|
+
const LEGACY_PEAK_NOTIFICATION_IDS = [8801, 8802, 8803, 8804]
|
|
3597
4011
|
|
|
3598
|
-
async function
|
|
4012
|
+
async function cancelLegacyPeakNotifications() {
|
|
3599
4013
|
if (!CAP?.isNativePlatform?.()) return false
|
|
4014
|
+
const notifications = CAP.Plugins?.LocalNotifications
|
|
4015
|
+
if (!notifications?.cancel) return true
|
|
4016
|
+
try {
|
|
4017
|
+
await notifications.cancel({ notifications: LEGACY_PEAK_NOTIFICATION_IDS.map(id => ({ id })) })
|
|
4018
|
+
return true
|
|
4019
|
+
} catch (error) {
|
|
4020
|
+
console.warn('Failed to cancel legacy peak reminders', error)
|
|
4021
|
+
return false
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
async function schedulePeakReminders({ legacyCleaned = false } = {}) {
|
|
4026
|
+
if (!CAP?.isNativePlatform?.()) return false
|
|
4027
|
+
if (!legacyCleaned && !await cancelLegacyPeakNotifications()) return false
|
|
3600
4028
|
const b = bgBridge()
|
|
3601
4029
|
if (!b?.startPeakReminder) return false
|
|
3602
4030
|
try {
|
|
@@ -3609,10 +4037,19 @@ async function cancelPeakReminders() {
|
|
|
3609
4037
|
const b = bgBridge()
|
|
3610
4038
|
if (!b?.stopPeakReminder) return false
|
|
3611
4039
|
try {
|
|
3612
|
-
|
|
4040
|
+
const stopped = b.stopPeakReminder() !== false
|
|
4041
|
+
const legacyCleaned = await cancelLegacyPeakNotifications()
|
|
4042
|
+
return stopped && legacyCleaned
|
|
3613
4043
|
} catch { return false }
|
|
3614
4044
|
}
|
|
3615
4045
|
|
|
4046
|
+
async function restorePeakReminders() {
|
|
4047
|
+
if (!CAP?.isNativePlatform?.()) return
|
|
4048
|
+
// 旧版使用 LocalNotifications 每日调度;无论当前开关状态都先清理,防止与前台服务重复提醒。
|
|
4049
|
+
const legacyCleaned = await cancelLegacyPeakNotifications()
|
|
4050
|
+
if (peakRemindOn() && legacyCleaned) await schedulePeakReminders({ legacyCleaned: true })
|
|
4051
|
+
}
|
|
4052
|
+
|
|
3616
4053
|
/* ---------------- 视图切换 ---------------- */
|
|
3617
4054
|
function showView(id) {
|
|
3618
4055
|
for (const v of ['view-home', 'view-files', 'view-session', 'view-activity', 'view-stats', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
@@ -3620,7 +4057,10 @@ function showView(id) {
|
|
|
3620
4057
|
document.body.classList.toggle('in-session', id === 'view-session')
|
|
3621
4058
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
3622
4059
|
window.scrollTo(0, 0)
|
|
3623
|
-
if (id === 'view-files' && !state.fs.loaded)
|
|
4060
|
+
if (id === 'view-files' && !state.fs.loaded) {
|
|
4061
|
+
const workspace = workspaceById(state.fs.workspaceId)
|
|
4062
|
+
loadFs(workspace?.path || null, { silent: true, resetRoot: true })
|
|
4063
|
+
}
|
|
3624
4064
|
if (id === 'view-stats') loadStats()
|
|
3625
4065
|
if (id === 'view-settings') showSettingsHome()
|
|
3626
4066
|
}
|
|
@@ -4064,6 +4504,8 @@ function bindUi() {
|
|
|
4064
4504
|
if (newButton) {
|
|
4065
4505
|
safeRpc('session.create', { workspaceId: newButton.dataset.wbNew }, t('home.createFailed')).then(async v => {
|
|
4066
4506
|
if (!v?.sessionId) return
|
|
4507
|
+
state.workspaceFilter = newButton.dataset.wbNew
|
|
4508
|
+
LS.set('workspaceFilterV1', state.workspaceFilter)
|
|
4067
4509
|
toast(t('home.created'), 'ok')
|
|
4068
4510
|
await refreshSessions()
|
|
4069
4511
|
openSession(v.sessionId)
|
|
@@ -4110,12 +4552,41 @@ function bindUi() {
|
|
|
4110
4552
|
if (e.key === 'Escape' && !$('feedback-sheet').classList.contains('hidden')) { closeFeedbackSheet(); $('btn-feedback').focus() }
|
|
4111
4553
|
})
|
|
4112
4554
|
$('btn-new-session').addEventListener('click', newSession)
|
|
4555
|
+
$('session-workspace-filter').addEventListener('change', (e) => {
|
|
4556
|
+
state.workspaceFilter = e.target.value
|
|
4557
|
+
if (state.workspaceFilter) LS.set('workspaceFilterV1', state.workspaceFilter)
|
|
4558
|
+
else LS.del('workspaceFilterV1')
|
|
4559
|
+
renderWorkspaceNavigation()
|
|
4560
|
+
renderSessions()
|
|
4561
|
+
})
|
|
4562
|
+
$('session-workspace-files').addEventListener('click', () => openWorkspaceFiles(state.workspaceFilter))
|
|
4563
|
+
$('new-session-workspace').addEventListener('change', (e) => renderNewSessionWorkspace(e.target.value))
|
|
4564
|
+
$('new-session-cancel').addEventListener('click', closeNewSessionModal)
|
|
4565
|
+
$('new-session-create').addEventListener('click', createSessionInWorkspace)
|
|
4566
|
+
$('modal-new-session').addEventListener('click', (e) => { if (e.target === $('modal-new-session')) closeNewSessionModal() })
|
|
4113
4567
|
$('btn-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
4114
4568
|
$('session-sort')?.addEventListener('change', (e) => {
|
|
4115
4569
|
state.sessionSort = e.target.value === 'workspace' ? 'workspace' : 'time'
|
|
4116
4570
|
LS.set('sessionSort', state.sessionSort)
|
|
4117
4571
|
renderSessions()
|
|
4118
4572
|
})
|
|
4573
|
+
$('fs-workspace').addEventListener('change', (e) => {
|
|
4574
|
+
state.fs.workspaceId = e.target.value
|
|
4575
|
+
if (state.fs.workspaceId) LS.set('fsWorkspaceIdV1', state.fs.workspaceId)
|
|
4576
|
+
else LS.del('fsWorkspaceIdV1')
|
|
4577
|
+
state.fs.loaded = false
|
|
4578
|
+
const workspace = workspaceById(state.fs.workspaceId)
|
|
4579
|
+
loadFs(workspace?.path || null, { resetRoot: true })
|
|
4580
|
+
})
|
|
4581
|
+
$('file-preview-close').addEventListener('click', closeFsPreview)
|
|
4582
|
+
$('file-preview-done').addEventListener('click', closeFsPreview)
|
|
4583
|
+
$('file-preview-source-tab').addEventListener('click', () => showFsPreviewMode('source'))
|
|
4584
|
+
$('file-preview-rendered-tab').addEventListener('click', () => showFsPreviewMode('rendered'))
|
|
4585
|
+
$('file-preview-download').addEventListener('click', () => {
|
|
4586
|
+
const preview = state.fs.preview
|
|
4587
|
+
if (preview?.path && preview?.name) downloadFsPath(preview.path, preview.name)
|
|
4588
|
+
})
|
|
4589
|
+
$('modal-file-preview').addEventListener('click', (e) => { if (e.target === $('modal-file-preview')) closeFsPreview() })
|
|
4119
4590
|
$('btn-cancel').addEventListener('click', cancelSession)
|
|
4120
4591
|
$('btn-send').addEventListener('click', sendMessage)
|
|
4121
4592
|
$('btn-fs-send').addEventListener('click', sendMessage)
|
|
@@ -4213,10 +4684,18 @@ function bindUi() {
|
|
|
4213
4684
|
$('modal-archive').addEventListener('click', (e) => { if (e.target === $('modal-archive')) closeArchiveConfirm() })
|
|
4214
4685
|
$('announcement-later').addEventListener('click', () => closeAnnouncement(false))
|
|
4215
4686
|
$('announcement-confirm').addEventListener('click', () => closeAnnouncement(true))
|
|
4687
|
+
$('announcement-poll-options').addEventListener('change', () => { $('announcement-poll-submit').disabled = false })
|
|
4688
|
+
$('announcement-poll-submit').addEventListener('click', submitAnnouncementVote)
|
|
4216
4689
|
$('modal-announcement').addEventListener('click', (e) => {
|
|
4217
4690
|
if (e.target === $('modal-announcement') && !state.announcement?.force) closeAnnouncement(false)
|
|
4218
4691
|
})
|
|
4219
4692
|
$('announcement-history-close').addEventListener('click', closeAnnouncementHistory)
|
|
4693
|
+
$('announcement-history-list').addEventListener('click', (e) => {
|
|
4694
|
+
const button = e.target.closest('[data-announcement-poll]')
|
|
4695
|
+
if (!button) return
|
|
4696
|
+
const item = readAnnouncementHistory().find(entry => entry.id === button.dataset.announcementPoll)
|
|
4697
|
+
if (item?.poll) { closeAnnouncementHistory(); openAnnouncementModal(item) }
|
|
4698
|
+
})
|
|
4220
4699
|
$('modal-announcement-history').addEventListener('click', (e) => {
|
|
4221
4700
|
if (e.target === $('modal-announcement-history')) closeAnnouncementHistory()
|
|
4222
4701
|
})
|
|
@@ -4256,7 +4735,7 @@ function bindUi() {
|
|
|
4256
4735
|
$('btn-update-expand').addEventListener('click', toggleUpdateExpand)
|
|
4257
4736
|
$('btn-reset').addEventListener('click', () => {
|
|
4258
4737
|
if (!confirm(t('settings.confirmReset'))) return
|
|
4259
|
-
LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction'); LS.del(ANNOUNCEMENTS_KEY); LS.del(ANNOUNCEMENT_HISTORY_KEY)
|
|
4738
|
+
LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction'); LS.del(ANNOUNCEMENTS_KEY); LS.del(ANNOUNCEMENT_HISTORY_KEY); LS.del(ANNOUNCEMENT_VOTES_KEY)
|
|
4260
4739
|
if (bgBridge()?.saveBackgroundConfig) saveBgConfig(false)
|
|
4261
4740
|
location.reload()
|
|
4262
4741
|
})
|
|
@@ -4301,8 +4780,8 @@ function bindUi() {
|
|
|
4301
4780
|
})
|
|
4302
4781
|
$('btn-test-notify').addEventListener('click', sendTestNotification)
|
|
4303
4782
|
$('btn-announcement-history').addEventListener('click', openAnnouncementHistory)
|
|
4304
|
-
//
|
|
4305
|
-
|
|
4783
|
+
// 启动时先清理旧版 LocalNotifications,再按当前开关恢复前台提醒服务。
|
|
4784
|
+
restorePeakReminders()
|
|
4306
4785
|
applyBgConfigFromNative()
|
|
4307
4786
|
$('opt-bg-poll').addEventListener('change', async (e) => {
|
|
4308
4787
|
if (e.target.checked) {
|
|
@@ -4355,6 +4834,8 @@ function bindUi() {
|
|
|
4355
4834
|
|
|
4356
4835
|
bindRail()
|
|
4357
4836
|
|
|
4837
|
+
initCustomSelects()
|
|
4838
|
+
|
|
4358
4839
|
// 向上翻历史 / 向下回最新
|
|
4359
4840
|
$('history').addEventListener('scroll', () => {
|
|
4360
4841
|
const box = $('history')
|