dsh-my-computer 0.0.0-stage → 0.1.0

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/lib/client.js ADDED
@@ -0,0 +1,1423 @@
1
+ // dsh-my-computer —— 浏览器半侧(DSH client bundle)
2
+ //
3
+ // 格式照 DSH 的 __ModuleLoader__ 约定:整文件是一个 load({ id, factory }),
4
+ // factory 里拿到宿主提供的 require,能取到 react;返回值就是这个插件模块的
5
+ // exports({ apply, inject }),apply(ctx) 里往 slots 注册两个东西:
6
+ // · sidebar.panellist → 右栏图标入口「我的电脑」
7
+ // · main → 点开后渲染的面板本体(key 必须与图标 id 一致)
8
+ //
9
+ // 注意:全部用 React.createElement 手写,不用 JSX、不用构建步骤。
10
+ // 所有请求走同源相对路径 /api/my-computer/*(DSH 的鉴权就在 /api 通道上)。
11
+ try {
12
+ window.__ModuleLoader__.load({
13
+ id: 'dsh-my-computer',
14
+ factory: function (require) {
15
+ var module = { exports: {} }
16
+ var exports = module.exports
17
+
18
+ var React = null
19
+ try {
20
+ React = require('react')
21
+ } catch (error) {
22
+ React = null
23
+ }
24
+ if (!React) React = globalThis.React
25
+ if (!React) throw new Error('dsh-my-computer: 拿不到 React 运行时')
26
+
27
+ var h = React.createElement
28
+ var useState = React.useState
29
+ var useEffect = React.useEffect
30
+ var useRef = React.useRef
31
+ var useCallback = React.useCallback
32
+ var useMemo = React.useMemo
33
+
34
+ // 面板 id 必须与侧栏图标 id 一致,点图标才会切到这个面板
35
+ var PANEL_ID = 'my-computer'
36
+ // DSH 的精确 fetch 路由挂在 /api 共享通道下(鉴权在那一层做),所以真实前缀是 /api/my-computer
37
+ var BASE = '/api/my-computer'
38
+ var LS_KEY = 'dsh-my-computer:last-path'
39
+
40
+ /* ------------------------------------------------------------ *
41
+ * 小工具
42
+ * ------------------------------------------------------------ */
43
+
44
+ function enc(value) {
45
+ return encodeURIComponent(String(value == null ? '' : value))
46
+ }
47
+
48
+ function fmtSize(bytes) {
49
+ if (bytes == null || !isFinite(bytes)) return ''
50
+ if (bytes < 1024) return bytes + ' B'
51
+ var units = ['KB', 'MB', 'GB', 'TB', 'PB']
52
+ var value = bytes
53
+ var index = -1
54
+ while (value >= 1024 && index < units.length - 1) {
55
+ value = value / 1024
56
+ index += 1
57
+ }
58
+ return (value < 10 ? value.toFixed(1) : String(Math.round(value))) + ' ' + units[index]
59
+ }
60
+
61
+ function pad2(n) {
62
+ return n < 10 ? '0' + n : String(n)
63
+ }
64
+
65
+ function fmtTime(ms) {
66
+ if (!ms) return ''
67
+ var d = new Date(ms)
68
+ var now = new Date()
69
+ var sameYear = d.getFullYear() === now.getFullYear()
70
+ var head = sameYear ? '' : d.getFullYear() + '-'
71
+ return head + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes())
72
+ }
73
+
74
+ function isRoot(p) {
75
+ return /^[A-Za-z]:\\$/.test(String(p || ''))
76
+ }
77
+
78
+ function parentOf(p) {
79
+ var s = String(p || '').replace(/\\+$/, '')
80
+ if (!s || /^[A-Za-z]:$/.test(s)) return null
81
+ var cut = s.lastIndexOf('\\')
82
+ if (cut < 0) return null
83
+ if (cut === 2) return s.slice(0, 3) + '\\'
84
+ return s.slice(0, cut)
85
+ }
86
+
87
+ function join(dir, name) {
88
+ var d = String(dir || '')
89
+ return d.endsWith('\\') ? d + name : d + '\\' + name
90
+ }
91
+
92
+ /** D:\a\b → [{label:'D:',path:'D:\'},{label:'a',path:'D:\a'},{label:'b',path:'D:\a\b'}] */
93
+ function crumbsOf(p) {
94
+ var s = String(p || '')
95
+ var out = []
96
+ if (!/^[A-Za-z]:[\\/]/.test(s)) return out
97
+ var root = s.slice(0, 2).toUpperCase() + '\\'
98
+ out.push({ label: s.slice(0, 2).toUpperCase(), path: root })
99
+ var rest = s.slice(3).split('\\').filter(function (x) { return x.length > 0 })
100
+ var acc = root
101
+ for (var i = 0; i < rest.length; i += 1) {
102
+ acc = join(acc, rest[i])
103
+ out.push({ label: rest[i], path: acc })
104
+ }
105
+ return out
106
+ }
107
+
108
+ var IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.avif', '.svg']
109
+ var TEXT_EXT = [
110
+ '.txt', '.md', '.markdown', '.log', '.json', '.jsonl', '.yml', '.yaml', '.toml', '.ini',
111
+ '.cfg', '.conf', '.csv', '.tsv', '.html', '.htm', '.css', '.js', '.mjs', '.cjs', '.ts',
112
+ '.tsx', '.jsx', '.py', '.sh', '.bat', '.cmd', '.ps1', '.xml', '.env', '.gitignore', '.vue',
113
+ '.java', '.c', '.h', '.cpp', '.hpp', '.rs', '.go', '.rb', '.php', '.sql', '.lua', '.kt',
114
+ ]
115
+
116
+ function extOf(name) {
117
+ var lower = String(name || '').toLowerCase()
118
+ var dot = lower.lastIndexOf('.')
119
+ return dot <= 0 ? '' : lower.slice(dot)
120
+ }
121
+
122
+ function kindOfExt(name) {
123
+ var ext = extOf(name)
124
+ if (IMAGE_EXT.indexOf(ext) >= 0) return 'image'
125
+ if (TEXT_EXT.indexOf(ext) >= 0) return 'text'
126
+ return 'other'
127
+ }
128
+
129
+ function iconFor(entry) {
130
+ var effective = entry.kind === 'link' ? entry.targetKind : entry.kind
131
+ if (effective === 'dir') return '📁'
132
+ if (entry.kind === 'link') return '🔗'
133
+ var group = kindOfExt(entry.name)
134
+ if (group === 'image') return '🖼️'
135
+ if (group === 'text') return '📄'
136
+ return '📦'
137
+ }
138
+
139
+ /* ------------------------------------------------------------ *
140
+ * HTTP
141
+ * ------------------------------------------------------------ */
142
+
143
+ function parseResponse(res) {
144
+ return res.text().then(function (raw) {
145
+ var data = null
146
+ try {
147
+ data = raw ? JSON.parse(raw) : null
148
+ } catch (error) {
149
+ throw new Error('服务端返回的不是 JSON(HTTP ' + res.status + '):' + raw.slice(0, 160))
150
+ }
151
+ if (data && Array.isArray(data.failed) && data.failed.length > 0) {
152
+ var detail = data.failed
153
+ .map(function (item) {
154
+ return item.path + '(' + (item.message || item.code) + ')'
155
+ })
156
+ .join(';')
157
+ throw new Error('部分操作失败:' + detail)
158
+ }
159
+ if (!res.ok || (data && data.ok === false)) {
160
+ var problem = data && data.error
161
+ var code = problem && problem.code ? problem.code + ': ' : ''
162
+ throw new Error(code + ((problem && problem.message) || 'HTTP ' + res.status))
163
+ }
164
+ return data
165
+ })
166
+ }
167
+
168
+ function apiGet(pathWithQuery) {
169
+ return fetch(BASE + pathWithQuery, { credentials: 'same-origin' }).then(parseResponse)
170
+ }
171
+
172
+ function apiPost(pathName, body) {
173
+ return fetch(BASE + pathName, {
174
+ method: 'POST',
175
+ credentials: 'same-origin',
176
+ headers: { 'content-type': 'application/json' },
177
+ body: JSON.stringify(body || {}),
178
+ }).then(parseResponse)
179
+ }
180
+
181
+ function apiUpload(dir, file, overwrite) {
182
+ var query =
183
+ '?path=' + enc(dir) + '&name=' + enc(file.name) + (overwrite ? '&overwrite=1' : '')
184
+ return fetch(BASE + '/upload' + query, {
185
+ method: 'PUT',
186
+ credentials: 'same-origin',
187
+ headers: { 'content-type': 'application/octet-stream' },
188
+ body: file,
189
+ }).then(parseResponse)
190
+ }
191
+
192
+ function downloadUrl(p) {
193
+ return BASE + '/download?path=' + enc(p)
194
+ }
195
+
196
+ function triggerDownload(p) {
197
+ var anchor = document.createElement('a')
198
+ anchor.href = downloadUrl(p)
199
+ anchor.setAttribute('download', '')
200
+ anchor.rel = 'noopener'
201
+ document.body.appendChild(anchor)
202
+ anchor.click()
203
+ setTimeout(function () {
204
+ anchor.remove()
205
+ }, 0)
206
+ }
207
+
208
+ /* ------------------------------------------------------------ *
209
+ * 样式:跟随宿主主题变量(--dsw-alias-*),拿不到时退到明/暗两套兜底
210
+ * ------------------------------------------------------------ */
211
+
212
+ var CSS = [
213
+ '.mc-root{display:flex;flex-direction:column;width:100%;box-sizing:border-box;height:100%;',
214
+ ' min-height:calc(100dvh - 96px);color:var(--mc-fg);background:var(--mc-bg);font-size:13px;',
215
+ ' --mc-bg:var(--dsw-alias-bg-layer-1,#fff);--mc-bg2:var(--dsw-alias-bg-layer-2,#f6f7f9);',
216
+ ' --mc-fg:var(--dsw-alias-label-primary,#1a1a1a);--mc-fg2:var(--dsw-alias-label-secondary,#6b6b6b);',
217
+ ' --mc-line:var(--dsw-alias-border-l1,rgba(0,0,0,.12));--mc-hover:var(--dsw-alias-bg-hover,rgba(0,0,0,.05));',
218
+ ' --mc-accent:var(--dsw-alias-brand-primary,#2f6fed);--mc-danger:#e5484d;}',
219
+ '@media (prefers-color-scheme:dark){.mc-root{',
220
+ ' --mc-bg:var(--dsw-alias-bg-layer-1,#161616);--mc-bg2:var(--dsw-alias-bg-layer-2,#202020);',
221
+ ' --mc-fg:var(--dsw-alias-label-primary,#e9e9e9);--mc-fg2:var(--dsw-alias-label-secondary,#9b9b9b);',
222
+ ' --mc-line:var(--dsw-alias-border-l1,rgba(255,255,255,.14));--mc-hover:var(--dsw-alias-bg-hover,rgba(255,255,255,.08));',
223
+ ' --mc-accent:var(--dsw-alias-brand-primary,#5b8cff);}}',
224
+ '.mc-head{flex:none;display:flex;flex-direction:column;gap:6px;padding:8px 10px 6px;}',
225
+ '.mc-line{display:flex;align-items:center;gap:6px;min-width:0;}',
226
+ '.mc-drives{display:flex;gap:6px;overflow-x:auto;flex:1 1 auto;padding-bottom:2px;',
227
+ ' scrollbar-width:none;-ms-overflow-style:none;}',
228
+ '.mc-drives::-webkit-scrollbar{display:none;}',
229
+ '.mc-chip{flex:none;border:1px solid var(--mc-line);background:transparent;color:var(--mc-fg);',
230
+ ' border-radius:999px;padding:3px 11px;font-size:12px;line-height:1.6;cursor:pointer;white-space:nowrap;}',
231
+ '.mc-chip:hover{background:var(--mc-hover);}',
232
+ '.mc-chip.on{background:var(--mc-accent);border-color:transparent;color:#fff;}',
233
+ '.mc-chip small{opacity:.7;font-size:10.5px;margin-left:4px;}',
234
+ '.mc-crumbs{display:flex;align-items:center;gap:2px;overflow-x:auto;flex:1 1 auto;min-width:0;',
235
+ ' white-space:nowrap;scrollbar-width:none;font-size:12.5px;}',
236
+ '.mc-crumbs::-webkit-scrollbar{display:none;}',
237
+ '.mc-crumb{appearance:none;border:0;background:transparent;color:var(--mc-fg2);cursor:pointer;',
238
+ ' padding:2px 4px;border-radius:5px;font-size:12.5px;max-width:42vw;overflow:hidden;',
239
+ ' text-overflow:ellipsis;white-space:nowrap;}',
240
+ '.mc-crumb:hover{background:var(--mc-hover);color:var(--mc-fg);}',
241
+ '.mc-crumb.last{color:var(--mc-fg);font-weight:600;}',
242
+ '.mc-sep{color:var(--mc-fg2);opacity:.55;font-size:11px;}',
243
+ '.mc-input{width:100%;box-sizing:border-box;background:var(--mc-bg2);color:var(--mc-fg);',
244
+ ' border:1px solid var(--mc-line);border-radius:8px;padding:7px 9px;font-size:12.5px;font-family:inherit;}',
245
+ '.mc-input:focus{outline:none;border-color:var(--mc-accent);}',
246
+ '.mc-tools{flex:none;display:flex;align-items:center;gap:6px;padding:0 10px 8px;flex-wrap:wrap;}',
247
+ '.mc-btn{appearance:none;border:1px solid var(--mc-line);background:transparent;color:var(--mc-fg);',
248
+ ' border-radius:8px;padding:4px 10px;font-size:12px;line-height:1.6;cursor:pointer;white-space:nowrap;}',
249
+ '.mc-btn:hover{background:var(--mc-hover);}',
250
+ '.mc-btn:disabled{opacity:.45;cursor:default;}',
251
+ '.mc-btn.primary{background:var(--mc-accent);border-color:transparent;color:#fff;}',
252
+ '.mc-btn.danger{color:var(--mc-danger);border-color:rgba(229,72,77,.45);}',
253
+ '.mc-btn.danger.solid{background:var(--mc-danger);color:#fff;border-color:transparent;}',
254
+ '.mc-flex{flex:1 1 auto;}',
255
+ '.mc-search{flex:1 1 130px;min-width:110px;}',
256
+ '.mc-list{flex:1 1 auto;min-height:240px;overflow:auto;border-top:1px solid var(--mc-line);',
257
+ ' -webkit-overflow-scrolling:touch;}',
258
+ '.mc-row{display:flex;align-items:center;gap:8px;padding:7px 10px;cursor:pointer;',
259
+ ' border-bottom:1px solid var(--mc-line);}',
260
+ '.mc-row:hover{background:var(--mc-hover);}',
261
+ '.mc-row.on{background:rgba(47,111,237,.13);}',
262
+ '.mc-check{flex:none;width:16px;height:16px;margin:0;accent-color:var(--mc-accent);}',
263
+ '.mc-ico{flex:none;width:20px;text-align:center;font-size:15px;line-height:1;}',
264
+ '.mc-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;}',
265
+ '.mc-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
266
+ '.mc-sub{font-size:11px;color:var(--mc-fg2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
267
+ '.mc-acts{flex:none;display:flex;gap:1px;}',
268
+ '.mc-ibtn{appearance:none;border:0;background:transparent;color:var(--mc-fg2);cursor:pointer;',
269
+ ' font-size:14px;line-height:1;padding:5px;border-radius:6px;}',
270
+ '.mc-ibtn:hover{background:var(--mc-hover);color:var(--mc-fg);}',
271
+ '.mc-ibtn.danger:hover{color:var(--mc-danger);}',
272
+ '.mc-foot{flex:none;display:flex;align-items:center;gap:10px;padding:6px 10px;font-size:11.5px;',
273
+ ' color:var(--mc-fg2);border-top:1px solid var(--mc-line);}',
274
+ '.mc-err{flex:none;margin:0 10px 6px;padding:7px 9px;border-radius:8px;font-size:12px;',
275
+ ' color:#c9343b;background:rgba(229,72,77,.12);border:1px solid rgba(229,72,77,.35);',
276
+ ' overflow-wrap:anywhere;}',
277
+ '.mc-ok{flex:none;margin:0 10px 6px;padding:7px 9px;border-radius:8px;font-size:12px;',
278
+ ' color:#1a7f4b;background:rgba(26,127,75,.12);border:1px solid rgba(26,127,75,.3);',
279
+ ' overflow-wrap:anywhere;}',
280
+ '@media (prefers-color-scheme:dark){.mc-err{color:#ff8f94;}.mc-ok{color:#6ddba0;}}',
281
+ '.mc-empty{padding:26px 12px;text-align:center;color:var(--mc-fg2);font-size:12.5px;}',
282
+ '.mc-modal-bg{position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.45);display:flex;',
283
+ ' align-items:center;justify-content:center;padding:14px;}',
284
+ '.mc-modal{width:min(520px,100%);max-height:86dvh;overflow:auto;box-sizing:border-box;',
285
+ ' background:var(--mc-bg);color:var(--mc-fg);border:1px solid var(--mc-line);border-radius:12px;',
286
+ ' padding:14px;display:flex;flex-direction:column;gap:10px;box-shadow:0 14px 44px rgba(0,0,0,.4);}',
287
+ '.mc-modal.wide{width:min(780px,100%);}',
288
+ '.mc-modal h3{margin:0;font-size:14px;font-weight:600;}',
289
+ '.mc-modal p{margin:0;font-size:12.5px;color:var(--mc-fg2);overflow-wrap:anywhere;}',
290
+ '.mc-modal-acts{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;}',
291
+ '.mc-editor{width:100%;box-sizing:border-box;min-height:260px;max-height:62dvh;resize:vertical;',
292
+ ' background:var(--mc-bg2);color:var(--mc-fg);border:1px solid var(--mc-line);border-radius:8px;',
293
+ ' padding:9px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;',
294
+ ' line-height:1.55;white-space:pre;overflow:auto;tab-size:2;}',
295
+ '.mc-img{max-width:100%;max-height:62dvh;object-fit:contain;border-radius:8px;',
296
+ ' background:var(--mc-bg2);display:block;margin:0 auto;}',
297
+ '.mc-hint{font-size:11.5px;color:var(--mc-fg2);}',
298
+ '@media (max-width:420px){',
299
+ ' .mc-root{min-height:calc(100dvh - 72px);font-size:12.5px;}',
300
+ ' .mc-head{padding:6px 8px 4px;}',
301
+ ' .mc-tools{padding:0 8px 6px;}',
302
+ ' .mc-btn{padding:4px 8px;font-size:11.5px;}',
303
+ ' .mc-row{padding:8px;}',
304
+ ' .mc-foot{padding:5px 8px;}}',
305
+ ].join('\n')
306
+
307
+ function ensureStyle() {
308
+ if (typeof document === 'undefined') return function () {}
309
+ var existing = document.querySelector('style[data-dsh-my-computer]')
310
+ if (existing) return function () {}
311
+ var styleEl = document.createElement('style')
312
+ styleEl.setAttribute('data-dsh-my-computer', '')
313
+ styleEl.textContent = CSS
314
+ document.head.appendChild(styleEl)
315
+ return function () {
316
+ styleEl.remove()
317
+ }
318
+ }
319
+
320
+ /* ------------------------------------------------------------ *
321
+ * 面板
322
+ * ------------------------------------------------------------ */
323
+
324
+ function MyComputerPanel() {
325
+ var _cwd = useState('')
326
+ var cwd = _cwd[0]
327
+ var setCwd = _cwd[1]
328
+
329
+ var _entries = useState([])
330
+ var entries = _entries[0]
331
+ var setEntries = _entries[1]
332
+
333
+ var _drives = useState([])
334
+ var drives = _drives[0]
335
+ var setDrives = _drives[1]
336
+
337
+ var _cap = useState({ total: null, free: null })
338
+ var capacity = _cap[0]
339
+ var setCapacity = _cap[1]
340
+
341
+ var _loading = useState(false)
342
+ var loading = _loading[0]
343
+ var setLoading = _loading[1]
344
+
345
+ var _error = useState('')
346
+ var errorText = _error[0]
347
+ var setErrorText = _error[1]
348
+
349
+ var _notice = useState('')
350
+ var notice = _notice[0]
351
+ var setNotice = _notice[1]
352
+
353
+ var _sel = useState([])
354
+ var selected = _sel[0]
355
+ var setSelected = _sel[1]
356
+
357
+ var _query = useState('')
358
+ var query = _query[0]
359
+ var setQuery = _query[1]
360
+
361
+ var _pathInput = useState('')
362
+ var pathInput = _pathInput[0]
363
+ var setPathInput = _pathInput[1]
364
+
365
+ var _dialog = useState(null)
366
+ var dialog = _dialog[0]
367
+ var setDialog = _dialog[1]
368
+
369
+ var _busy = useState('')
370
+ var busy = _busy[0]
371
+ var setBusy = _busy[1]
372
+
373
+ var fileInputRef = useRef(null)
374
+ var mountedRef = useRef(true)
375
+
376
+ useEffect(function () {
377
+ mountedRef.current = true
378
+ return function () {
379
+ mountedRef.current = false
380
+ }
381
+ }, [])
382
+
383
+ function reportError(error) {
384
+ setErrorText(String((error && error.message) || error || '未知错误'))
385
+ }
386
+
387
+ function clearMessages() {
388
+ setErrorText('')
389
+ setNotice('')
390
+ }
391
+
392
+ /* ---- 加载 ---- */
393
+
394
+ var navigate = useCallback(function (target) {
395
+ if (!target) return Promise.resolve()
396
+ clearMessages()
397
+ setLoading(true)
398
+ return apiGet('/list?path=' + enc(target))
399
+ .then(function (data) {
400
+ if (!mountedRef.current) return
401
+ setCwd(data.path)
402
+ setEntries(data.entries || [])
403
+ setCapacity({ total: data.total, free: data.free })
404
+ setSelected([])
405
+ setQuery('')
406
+ setPathInput(data.path)
407
+ try {
408
+ window.localStorage.setItem(LS_KEY, data.path)
409
+ } catch (error) {
410
+ /* 隐私模式下 localStorage 可能不可用 */
411
+ }
412
+ })
413
+ .catch(function (error) {
414
+ if (mountedRef.current) reportError(error)
415
+ })
416
+ .then(function () {
417
+ if (mountedRef.current) setLoading(false)
418
+ })
419
+ }, [])
420
+
421
+ var loadDrives = useCallback(function () {
422
+ return apiGet('/drives')
423
+ .then(function (data) {
424
+ if (!mountedRef.current) return []
425
+ var list = data.drives || []
426
+ setDrives(list)
427
+ return list
428
+ })
429
+ .catch(function (error) {
430
+ if (mountedRef.current) reportError(error)
431
+ return []
432
+ })
433
+ }, [])
434
+
435
+ // 首屏:先探测盘符,再跳到最后一次待过的目录(没有就 D:\,再没有就用第一个盘)
436
+ useEffect(function () {
437
+ var startPath = ''
438
+ try {
439
+ startPath = window.localStorage.getItem(LS_KEY) || ''
440
+ } catch (error) {
441
+ startPath = ''
442
+ }
443
+ loadDrives().then(function (list) {
444
+ if (!mountedRef.current) return
445
+ var preferred = ['D:', 'C:']
446
+ var chosen = ''
447
+ if (startPath) chosen = startPath
448
+ if (!chosen) {
449
+ for (var i = 0; i < preferred.length; i += 1) {
450
+ for (var j = 0; j < list.length; j += 1) {
451
+ if (list[j].letter.toUpperCase() === preferred[i]) chosen = list[j].letter + '\\'
452
+ }
453
+ if (chosen) break
454
+ }
455
+ }
456
+ if (!chosen && list.length > 0) chosen = list[0].letter + '\\'
457
+ if (!chosen) {
458
+ setErrorText('没有探测到任何可用的磁盘')
459
+ return
460
+ }
461
+ navigate(chosen)
462
+ })
463
+ }, [loadDrives, navigate])
464
+
465
+ /* ---- 导航动作 ---- */
466
+
467
+ function goUp() {
468
+ var up = parentOf(cwd)
469
+ if (up) navigate(up)
470
+ }
471
+
472
+ function switchDrive(letter) {
473
+ navigate(letter + '\\')
474
+ }
475
+
476
+ function submitPath(event) {
477
+ event.preventDefault()
478
+ var target = String(pathInput || '').trim()
479
+ if (!target) return
480
+ navigate(target)
481
+ }
482
+
483
+ function refresh() {
484
+ if (cwd) navigate(cwd)
485
+ loadDrives()
486
+ }
487
+
488
+ /* ---- 选择 ---- */
489
+
490
+ function isSelected(p) {
491
+ return selected.indexOf(p) >= 0
492
+ }
493
+
494
+ function toggleSelect(p) {
495
+ setSelected(function (prev) {
496
+ var at = prev.indexOf(p)
497
+ if (at >= 0) {
498
+ var next = prev.slice()
499
+ next.splice(at, 1)
500
+ return next
501
+ }
502
+ return prev.concat([p])
503
+ })
504
+ }
505
+
506
+ function toggleSelectAll() {
507
+ var visible = visibleEntries.map(function (entry) {
508
+ return entry.path
509
+ })
510
+ var allSelected =
511
+ visible.length > 0 &&
512
+ visible.every(function (p) {
513
+ return isSelected(p)
514
+ })
515
+ setSelected(allSelected ? [] : visible)
516
+ }
517
+
518
+ /* ---- 行内动作 ---- */
519
+
520
+ function openEntry(entry) {
521
+ var effective = entry.kind === 'link' ? entry.targetKind : entry.kind
522
+ if (effective === 'dir') {
523
+ navigate(entry.path)
524
+ return
525
+ }
526
+ if (entry.kind === 'link' && entry.targetKind === 'unknown') {
527
+ setErrorText('这是一个断链(目标不存在):' + entry.path)
528
+ return
529
+ }
530
+ setDialog({
531
+ kind: 'preview',
532
+ path: entry.path,
533
+ name: entry.name,
534
+ size: entry.size,
535
+ mtime: entry.mtime,
536
+ group: kindOfExt(entry.name),
537
+ text: null,
538
+ original: '',
539
+ truncated: false,
540
+ encoding: '',
541
+ loaded: false,
542
+ loadError: '',
543
+ })
544
+ }
545
+
546
+ function removePaths(paths) {
547
+ if (!paths || paths.length === 0) return
548
+ setDialog({ kind: 'delete', paths: paths.slice() })
549
+ }
550
+
551
+ /* ---- 上传 ---- */
552
+
553
+ function pickFiles() {
554
+ if (fileInputRef.current) fileInputRef.current.click()
555
+ }
556
+
557
+ function uploadFiles(files, overwrite) {
558
+ if (!files || files.length === 0) return Promise.resolve()
559
+ setBusy('上传中 0/' + files.length)
560
+ clearMessages()
561
+ var conflicts = []
562
+ var done = 0
563
+ var failed = []
564
+ var chain = Promise.resolve()
565
+ files.forEach(function (file) {
566
+ chain = chain.then(function () {
567
+ return apiUpload(cwd, file, Boolean(overwrite))
568
+ .then(function () {
569
+ done += 1
570
+ if (mountedRef.current) setBusy('上传中 ' + done + '/' + files.length)
571
+ })
572
+ .catch(function (error) {
573
+ var message = String((error && error.message) || error)
574
+ if (!overwrite && message.indexOf('ALREADY_EXISTS') === 0) conflicts.push(file)
575
+ else failed.push(file.name + ':' + message)
576
+ })
577
+ })
578
+ })
579
+ return chain.then(function () {
580
+ if (mountedRef.current) setBusy('')
581
+ if (failed.length > 0) {
582
+ setErrorText('上传失败 ' + failed.length + ' 个:' + failed.join(';'))
583
+ } else if (done > 0) {
584
+ setNotice('已上传 ' + done + ' 个文件')
585
+ }
586
+ if (conflicts.length > 0) {
587
+ setDialog({ kind: 'overwrite', files: conflicts })
588
+ }
589
+ return navigate(cwd)
590
+ })
591
+ }
592
+
593
+ function onFilesPicked(event) {
594
+ var files = event.target.files ? Array.prototype.slice.call(event.target.files) : []
595
+ event.target.value = ''
596
+ uploadFiles(files, false)
597
+ }
598
+
599
+ /* ---- 计算可见列表 ---- */
600
+
601
+ var visibleEntries = useMemo(function () {
602
+ var keyword = query.trim().toLowerCase()
603
+ if (!keyword) return entries
604
+ return entries.filter(function (entry) {
605
+ return String(entry.name || '').toLowerCase().indexOf(keyword) >= 0
606
+ })
607
+ }, [entries, query])
608
+
609
+ var crumbList = useMemo(function () {
610
+ return crumbsOf(cwd)
611
+ }, [cwd])
612
+
613
+ var currentDrive = cwd ? cwd.slice(0, 2).toUpperCase() : ''
614
+ var allVisibleSelected =
615
+ visibleEntries.length > 0 &&
616
+ visibleEntries.every(function (entry) {
617
+ return isSelected(entry.path)
618
+ })
619
+
620
+ /* ---- 渲染 ---- */
621
+
622
+ var header = h(
623
+ 'div',
624
+ { className: 'mc-head' },
625
+ h(
626
+ 'div',
627
+ { className: 'mc-line' },
628
+ h(
629
+ 'div',
630
+ { className: 'mc-drives' },
631
+ drives.length === 0
632
+ ? h('span', { className: 'mc-hint' }, '正在探测磁盘…')
633
+ : drives.map(function (drive) {
634
+ var active = drive.letter.toUpperCase() === currentDrive
635
+ return h(
636
+ 'button',
637
+ {
638
+ key: drive.letter,
639
+ type: 'button',
640
+ className: 'mc-chip' + (active ? ' on' : ''),
641
+ onClick: function () {
642
+ switchDrive(drive.letter)
643
+ },
644
+ title: (drive.label || '') + (drive.total ? ' · 可用 ' + fmtSize(drive.free) : ''),
645
+ },
646
+ drive.letter,
647
+ drive.total
648
+ ? h('small', null, fmtSize(drive.free) + ' 可用')
649
+ : null,
650
+ )
651
+ }),
652
+ ),
653
+ h(
654
+ 'button',
655
+ {
656
+ type: 'button',
657
+ className: 'mc-btn',
658
+ onClick: refresh,
659
+ disabled: loading,
660
+ title: '刷新当前目录',
661
+ },
662
+ loading ? '…' : '刷新',
663
+ ),
664
+ ),
665
+ h(
666
+ 'div',
667
+ { className: 'mc-line' },
668
+ h(
669
+ 'button',
670
+ {
671
+ type: 'button',
672
+ className: 'mc-btn',
673
+ onClick: goUp,
674
+ disabled: !parentOf(cwd),
675
+ title: '回到上级目录',
676
+ },
677
+ '↑ 上级',
678
+ ),
679
+ h(
680
+ 'div',
681
+ { className: 'mc-crumbs' },
682
+ crumbList.length === 0
683
+ ? h('span', { className: 'mc-hint' }, cwd || '未选择位置')
684
+ : crumbList.map(function (crumb, index) {
685
+ var nodes = []
686
+ if (index > 0) nodes.push(h('span', { key: 'sep' + index, className: 'mc-sep' }, '›'))
687
+ nodes.push(
688
+ h(
689
+ 'button',
690
+ {
691
+ key: 'crumb' + index,
692
+ type: 'button',
693
+ className: 'mc-crumb' + (index === crumbList.length - 1 ? ' last' : ''),
694
+ onClick: function () {
695
+ navigate(crumb.path)
696
+ },
697
+ title: crumb.path,
698
+ },
699
+ crumb.label,
700
+ ),
701
+ )
702
+ return nodes
703
+ }),
704
+ ),
705
+ ),
706
+ h(
707
+ 'form',
708
+ { className: 'mc-line', onSubmit: submitPath },
709
+ h('input', {
710
+ className: 'mc-input',
711
+ value: pathInput,
712
+ placeholder: '粘贴绝对路径后回车,例如 D:\\dsh-workspace\\vault',
713
+ spellCheck: false,
714
+ autoCapitalize: 'off',
715
+ autoCorrect: 'off',
716
+ onChange: function (event) {
717
+ setPathInput(event.target.value)
718
+ },
719
+ }),
720
+ ),
721
+ )
722
+
723
+ var tools = h(
724
+ 'div',
725
+ { className: 'mc-tools' },
726
+ h(
727
+ 'button',
728
+ { type: 'button', className: 'mc-btn', onClick: function () { setDialog({ kind: 'mkdir', value: '' }) } },
729
+ '+ 新建文件夹',
730
+ ),
731
+ h('button', { type: 'button', className: 'mc-btn', onClick: pickFiles }, '⬆ 上传'),
732
+ h(
733
+ 'button',
734
+ { type: 'button', className: 'mc-btn', onClick: toggleSelectAll, disabled: visibleEntries.length === 0 },
735
+ allVisibleSelected ? '取消全选' : '全选',
736
+ ),
737
+ selected.length > 0
738
+ ? h(
739
+ 'button',
740
+ {
741
+ type: 'button',
742
+ className: 'mc-btn danger solid',
743
+ onClick: function () {
744
+ removePaths(selected)
745
+ },
746
+ },
747
+ '删除 ' + selected.length + ' 项',
748
+ )
749
+ : null,
750
+ h('input', {
751
+ className: 'mc-input mc-search',
752
+ value: query,
753
+ placeholder: '筛选当前目录',
754
+ spellCheck: false,
755
+ onChange: function (event) {
756
+ setQuery(event.target.value)
757
+ },
758
+ }),
759
+ h('input', {
760
+ type: 'file',
761
+ multiple: true,
762
+ ref: fileInputRef,
763
+ style: { display: 'none' },
764
+ onChange: onFilesPicked,
765
+ }),
766
+ )
767
+
768
+ var rows = visibleEntries.map(function (entry) {
769
+ var effective = entry.kind === 'link' ? entry.targetKind : entry.kind
770
+ var metaParts = []
771
+ if (effective === 'dir') metaParts.push('文件夹')
772
+ else if (entry.size != null) metaParts.push(fmtSize(entry.size))
773
+ else metaParts.push('—')
774
+ if (entry.kind === 'link') metaParts.push('快捷方式')
775
+ if (entry.statError) metaParts.push('无法读取:' + entry.statError)
776
+ else if (entry.mtime) metaParts.push(fmtTime(entry.mtime))
777
+
778
+ return h(
779
+ 'div',
780
+ {
781
+ key: entry.path,
782
+ className: 'mc-row' + (isSelected(entry.path) ? ' on' : ''),
783
+ onClick: function () {
784
+ openEntry(entry)
785
+ },
786
+ title: entry.path,
787
+ },
788
+ h('input', {
789
+ type: 'checkbox',
790
+ className: 'mc-check',
791
+ checked: isSelected(entry.path),
792
+ onClick: function (event) {
793
+ event.stopPropagation()
794
+ },
795
+ onChange: function () {
796
+ toggleSelect(entry.path)
797
+ },
798
+ }),
799
+ h('span', { className: 'mc-ico' }, iconFor(entry)),
800
+ h(
801
+ 'div',
802
+ { className: 'mc-main' },
803
+ h('div', { className: 'mc-name' }, entry.name),
804
+ h('div', { className: 'mc-sub' }, metaParts.join(' · ')),
805
+ ),
806
+ h(
807
+ 'div',
808
+ {
809
+ className: 'mc-acts',
810
+ onClick: function (event) {
811
+ event.stopPropagation()
812
+ },
813
+ },
814
+ effective === 'file'
815
+ ? h(
816
+ 'button',
817
+ {
818
+ type: 'button',
819
+ className: 'mc-ibtn',
820
+ title: '下载',
821
+ onClick: function () {
822
+ triggerDownload(entry.path)
823
+ },
824
+ },
825
+ '⤓',
826
+ )
827
+ : null,
828
+ h(
829
+ 'button',
830
+ {
831
+ type: 'button',
832
+ className: 'mc-ibtn',
833
+ title: '重命名',
834
+ onClick: function () {
835
+ setDialog({ kind: 'rename', path: entry.path, value: entry.name })
836
+ },
837
+ },
838
+ '✎',
839
+ ),
840
+ h(
841
+ 'button',
842
+ {
843
+ type: 'button',
844
+ className: 'mc-ibtn danger',
845
+ title: '删除',
846
+ onClick: function () {
847
+ removePaths([entry.path])
848
+ },
849
+ },
850
+ '🗑',
851
+ ),
852
+ ),
853
+ )
854
+ })
855
+
856
+ var listBody =
857
+ loading && entries.length === 0
858
+ ? h('div', { className: 'mc-empty' }, '加载中…')
859
+ : rows.length === 0
860
+ ? h(
861
+ 'div',
862
+ { className: 'mc-empty' },
863
+ query.trim() ? '没有匹配「' + query.trim() + '」的条目' : '这个目录是空的',
864
+ )
865
+ : rows
866
+
867
+ var footer = h(
868
+ 'div',
869
+ { className: 'mc-foot' },
870
+ h('span', null, '共 ' + visibleEntries.length + ' 项' + (query.trim() ? '(已筛选)' : '')),
871
+ capacity.total != null
872
+ ? h(
873
+ 'span',
874
+ null,
875
+ '可用 ' + fmtSize(capacity.free) + ' / ' + fmtSize(capacity.total),
876
+ )
877
+ : null,
878
+ selected.length > 0 ? h('span', null, '已选 ' + selected.length) : null,
879
+ busy ? h('span', { className: 'mc-hint' }, busy) : null,
880
+ )
881
+
882
+ return h(
883
+ 'div',
884
+ { className: 'mc-root' },
885
+ header,
886
+ tools,
887
+ errorText ? h('div', { className: 'mc-err' }, errorText) : null,
888
+ notice ? h('div', { className: 'mc-ok' }, notice) : null,
889
+ h('div', { className: 'mc-list' }, listBody),
890
+ footer,
891
+ dialog ? renderDialog(dialog, setDialog, navigate, cwd, uploadFiles, reportError, setNotice, setBusy) : null,
892
+ )
893
+ }
894
+
895
+ /* ------------------------------------------------------------ *
896
+ * 弹窗(新建 / 改名 / 删除确认 / 覆盖确认 / 预览)
897
+ * ------------------------------------------------------------ */
898
+
899
+ function ModalShell(props) {
900
+ return h(
901
+ 'div',
902
+ {
903
+ className: 'mc-modal-bg',
904
+ onClick: function (event) {
905
+ if (event.target === event.currentTarget && props.onCancel) props.onCancel()
906
+ },
907
+ },
908
+ h(
909
+ 'div',
910
+ { className: 'mc-modal' + (props.wide ? ' wide' : '') },
911
+ h('h3', null, props.title),
912
+ props.children,
913
+ ),
914
+ )
915
+ }
916
+
917
+ function NameDialog(props) {
918
+ var _value = useState(props.initial || '')
919
+ var value = _value[0]
920
+ var setValue = _value[1]
921
+ var _busy = useState(false)
922
+ var busy = _busy[0]
923
+ var setBusy = _busy[1]
924
+ var inputRef = useRef(null)
925
+
926
+ useEffect(function () {
927
+ if (inputRef.current) inputRef.current.focus()
928
+ }, [])
929
+
930
+ function submit(event) {
931
+ if (event) event.preventDefault()
932
+ var name = String(value || '').trim()
933
+ if (!name || busy) return
934
+ setBusy(true)
935
+ props
936
+ .onSubmit(name)
937
+ .catch(function (error) {
938
+ props.onError(error)
939
+ setBusy(false)
940
+ })
941
+ }
942
+
943
+ return h(
944
+ ModalShell,
945
+ { title: props.title, onCancel: busy ? null : props.onCancel },
946
+ h('p', null, props.hint || ''),
947
+ h('form', { onSubmit: submit },
948
+ h('input', {
949
+ className: 'mc-input',
950
+ ref: inputRef,
951
+ value: value,
952
+ placeholder: '名称',
953
+ spellCheck: false,
954
+ onChange: function (event) {
955
+ setValue(event.target.value)
956
+ },
957
+ }),
958
+ ),
959
+ h(
960
+ 'div',
961
+ { className: 'mc-modal-acts' },
962
+ h('button', { type: 'button', className: 'mc-btn', onClick: props.onCancel, disabled: busy }, '取消'),
963
+ h('button', { type: 'button', className: 'mc-btn primary', onClick: submit, disabled: busy }, busy ? '处理中…' : '确定'),
964
+ ),
965
+ )
966
+ }
967
+
968
+ function PreviewDialog(props) {
969
+ var _state = useState({
970
+ text: props.text || '',
971
+ original: props.text || '',
972
+ loaded: props.loaded,
973
+ loadError: props.loadError || '',
974
+ truncated: props.truncated,
975
+ encoding: props.encoding,
976
+ saving: false,
977
+ })
978
+ var state = _state[0]
979
+ var setState = _state[1]
980
+
981
+ useEffect(function () {
982
+ if (state.loaded || state.loadError) return
983
+ apiGet('/read?path=' + enc(props.path) + '&maxBytes=' + String(512 * 1024))
984
+ .then(function (data) {
985
+ setState(function (prev) {
986
+ return Object.assign({}, prev, {
987
+ text: data.text,
988
+ original: data.text,
989
+ loaded: true,
990
+ truncated: data.truncated,
991
+ encoding: data.encoding,
992
+ })
993
+ })
994
+ })
995
+ .catch(function (error) {
996
+ setState(function (prev) {
997
+ return Object.assign({}, prev, {
998
+ loaded: true,
999
+ loadError: String((error && error.message) || error),
1000
+ })
1001
+ })
1002
+ })
1003
+ }, [])
1004
+
1005
+ var dirty = state.loaded && state.text !== state.original
1006
+
1007
+ function save() {
1008
+ setState(function (prev) {
1009
+ return Object.assign({}, prev, { saving: true })
1010
+ })
1011
+ apiPost('/write', { path: props.path, text: state.text })
1012
+ .then(function () {
1013
+ setState(function (prev) {
1014
+ return Object.assign({}, prev, { saving: false, original: prev.text })
1015
+ })
1016
+ props.onSaved('已保存 ' + props.name)
1017
+ })
1018
+ .catch(function (error) {
1019
+ setState(function (prev) {
1020
+ return Object.assign({}, prev, { saving: false })
1021
+ })
1022
+ props.onError(error)
1023
+ })
1024
+ }
1025
+
1026
+ var body
1027
+ if (props.group === 'image') {
1028
+ body = h('img', { className: 'mc-img', src: downloadUrl(props.path), alt: props.name })
1029
+ } else if (props.group === 'text') {
1030
+ if (state.loadError) {
1031
+ body = h(
1032
+ 'div',
1033
+ null,
1034
+ h('p', null, state.loadError),
1035
+ h(
1036
+ 'button',
1037
+ {
1038
+ type: 'button',
1039
+ className: 'mc-btn primary',
1040
+ onClick: function () {
1041
+ triggerDownload(props.path)
1042
+ },
1043
+ },
1044
+ '下载文件',
1045
+ ),
1046
+ )
1047
+ } else if (!state.loaded) {
1048
+ body = h('p', null, '读取中…')
1049
+ } else {
1050
+ body = h(
1051
+ 'div',
1052
+ { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
1053
+ h('textarea', {
1054
+ className: 'mc-editor',
1055
+ value: state.text,
1056
+ spellCheck: false,
1057
+ onChange: function (event) {
1058
+ var next = event.target.value
1059
+ setState(function (prev) {
1060
+ return Object.assign({}, prev, { text: next })
1061
+ })
1062
+ },
1063
+ }),
1064
+ h(
1065
+ 'div',
1066
+ { className: 'mc-modal-acts' },
1067
+ h(
1068
+ 'span',
1069
+ { className: 'mc-hint', style: { marginRight: 'auto' } },
1070
+ (state.encoding || 'utf-8') +
1071
+ (state.truncated ? ' · 已截断(大于 512KB,只能看前缀,保存会覆盖全文件)' : '') +
1072
+ (dirty ? ' · 已修改' : ''),
1073
+ ),
1074
+ h(
1075
+ 'button',
1076
+ {
1077
+ type: 'button',
1078
+ className: 'mc-btn',
1079
+ onClick: function () {
1080
+ triggerDownload(props.path)
1081
+ },
1082
+ },
1083
+ '下载',
1084
+ ),
1085
+ h(
1086
+ 'button',
1087
+ {
1088
+ type: 'button',
1089
+ className: 'mc-btn primary',
1090
+ disabled: !dirty || state.saving,
1091
+ onClick: save,
1092
+ },
1093
+ state.saving ? '保存中…' : '保存',
1094
+ ),
1095
+ ),
1096
+ )
1097
+ }
1098
+ } else {
1099
+ body = h(
1100
+ 'div',
1101
+ { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
1102
+ h('p', null, '这种文件不支持在线预览,可以直接下载到手机/电脑。'),
1103
+ h(
1104
+ 'button',
1105
+ {
1106
+ type: 'button',
1107
+ className: 'mc-btn primary',
1108
+ onClick: function () {
1109
+ triggerDownload(props.path)
1110
+ },
1111
+ },
1112
+ '下载 ' + props.name,
1113
+ ),
1114
+ )
1115
+ }
1116
+
1117
+ return h(
1118
+ ModalShell,
1119
+ { title: props.name, wide: true, onCancel: props.onCancel },
1120
+ h(
1121
+ 'p',
1122
+ { className: 'mc-hint' },
1123
+ props.path + (props.size != null ? ' · ' + fmtSize(props.size) : ''),
1124
+ ),
1125
+ body,
1126
+ props.group === 'image' || props.group === 'other'
1127
+ ? h(
1128
+ 'div',
1129
+ { className: 'mc-modal-acts' },
1130
+ h('button', { type: 'button', className: 'mc-btn', onClick: props.onCancel }, '关闭'),
1131
+ )
1132
+ : h(
1133
+ 'div',
1134
+ { className: 'mc-modal-acts' },
1135
+ h('button', { type: 'button', className: 'mc-btn', onClick: props.onCancel }, '关闭'),
1136
+ ),
1137
+ )
1138
+ }
1139
+
1140
+ function renderDialog(dialog, setDialog, navigate, cwd, uploadFiles, reportError, setNotice, setBusy) {
1141
+ function close() {
1142
+ setDialog(null)
1143
+ }
1144
+
1145
+ if (dialog.kind === 'mkdir') {
1146
+ return h(NameDialog, {
1147
+ title: '新建文件夹',
1148
+ hint: '将在 ' + cwd + ' 下创建',
1149
+ initial: '',
1150
+ onCancel: close,
1151
+ onError: reportError,
1152
+ onSubmit: function (name) {
1153
+ return apiPost('/mkdir', { path: cwd, name: name }).then(function () {
1154
+ setDialog(null)
1155
+ setNotice('已创建文件夹 ' + name)
1156
+ return navigate(cwd)
1157
+ })
1158
+ },
1159
+ })
1160
+ }
1161
+
1162
+ if (dialog.kind === 'rename') {
1163
+ return h(NameDialog, {
1164
+ title: '重命名',
1165
+ hint: dialog.path,
1166
+ initial: dialog.value,
1167
+ onCancel: close,
1168
+ onError: reportError,
1169
+ onSubmit: function (name) {
1170
+ return apiPost('/rename', { path: dialog.path, name: name }).then(function () {
1171
+ setDialog(null)
1172
+ setNotice('已重命名为 ' + name)
1173
+ return navigate(cwd)
1174
+ })
1175
+ },
1176
+ })
1177
+ }
1178
+
1179
+ if (dialog.kind === 'delete') {
1180
+ var preview = dialog.paths
1181
+ .slice(0, 4)
1182
+ .map(function (p) {
1183
+ return p.split('\\').pop()
1184
+ })
1185
+ .join('、')
1186
+ var more = dialog.paths.length > 4 ? ' 等 ' + dialog.paths.length + ' 项' : ''
1187
+ return h(
1188
+ ModalShell,
1189
+ { title: '确认删除', onCancel: close },
1190
+ h('p', null, '将永久删除:' + preview + more + '。'),
1191
+ h('p', null, '删除是递归且不可恢复的(不进回收站),请确认路径没有写错。'),
1192
+ h(
1193
+ 'div',
1194
+ { className: 'mc-modal-acts' },
1195
+ h('button', { type: 'button', className: 'mc-btn', onClick: close }, '取消'),
1196
+ h(
1197
+ 'button',
1198
+ {
1199
+ type: 'button',
1200
+ className: 'mc-btn danger solid',
1201
+ onClick: function () {
1202
+ setBusy('正在删除…')
1203
+ apiPost('/delete', { paths: dialog.paths, confirm: true })
1204
+ .then(function () {
1205
+ setBusy('')
1206
+ setDialog(null)
1207
+ setNotice('已删除 ' + dialog.paths.length + ' 项')
1208
+ return navigate(cwd)
1209
+ })
1210
+ .catch(function (error) {
1211
+ setBusy('')
1212
+ setDialog(null)
1213
+ reportError(error)
1214
+ return navigate(cwd)
1215
+ })
1216
+ },
1217
+ },
1218
+ '永久删除',
1219
+ ),
1220
+ ),
1221
+ )
1222
+ }
1223
+
1224
+ if (dialog.kind === 'overwrite') {
1225
+ return h(
1226
+ ModalShell,
1227
+ { title: '同名文件已存在', onCancel: close },
1228
+ h(
1229
+ 'p',
1230
+ null,
1231
+ dialog.files.length + ' 个文件在目标目录里已有同名文件:' +
1232
+ dialog.files
1233
+ .slice(0, 4)
1234
+ .map(function (file) {
1235
+ return file.name
1236
+ })
1237
+ .join('、'),
1238
+ ),
1239
+ h('p', null, '覆盖会直接替换原文件,不可恢复。'),
1240
+ h(
1241
+ 'div',
1242
+ { className: 'mc-modal-acts' },
1243
+ h('button', { type: 'button', className: 'mc-btn', onClick: close }, '跳过'),
1244
+ h(
1245
+ 'button',
1246
+ {
1247
+ type: 'button',
1248
+ className: 'mc-btn danger solid',
1249
+ onClick: function () {
1250
+ var files = dialog.files
1251
+ setDialog(null)
1252
+ uploadFiles(files, true)
1253
+ },
1254
+ },
1255
+ '覆盖上传',
1256
+ ),
1257
+ ),
1258
+ )
1259
+ }
1260
+
1261
+ if (dialog.kind === 'preview') {
1262
+ return h(PreviewDialog, {
1263
+ path: dialog.path,
1264
+ name: dialog.name,
1265
+ size: dialog.size,
1266
+ group: dialog.group,
1267
+ text: dialog.text,
1268
+ loaded: dialog.loaded,
1269
+ loadError: dialog.loadError,
1270
+ truncated: dialog.truncated,
1271
+ encoding: dialog.encoding,
1272
+ onCancel: close,
1273
+ onError: reportError,
1274
+ onSaved: function (message) {
1275
+ setNotice(message)
1276
+ close()
1277
+ navigate(cwd)
1278
+ },
1279
+ })
1280
+ }
1281
+
1282
+ return null
1283
+ }
1284
+
1285
+ /* ------------------------------------------------------------ *
1286
+ * 侧栏图标
1287
+ * ------------------------------------------------------------ */
1288
+
1289
+ function MyComputerIcon() {
1290
+ return h(
1291
+ 'svg',
1292
+ {
1293
+ xmlns: 'http://www.w3.org/2000/svg',
1294
+ width: 20,
1295
+ height: 20,
1296
+ viewBox: '0 0 24 24',
1297
+ fill: 'none',
1298
+ stroke: 'currentColor',
1299
+ strokeWidth: 1.7,
1300
+ strokeLinecap: 'round',
1301
+ strokeLinejoin: 'round',
1302
+ 'aria-hidden': 'true',
1303
+ },
1304
+ h('rect', { key: 'screen', x: 2.5, y: 4, width: 19, height: 12.5, rx: 2 }),
1305
+ h('path', { key: 'stand', d: 'M9 20.5h6' }),
1306
+ h('path', { key: 'neck', d: 'M12 16.5v4' }),
1307
+ h('path', { key: 'drive', d: 'M5.5 8h4' }),
1308
+ )
1309
+ }
1310
+
1311
+ /* ------------------------------------------------------------ *
1312
+ * 插件入口
1313
+ * ------------------------------------------------------------ */
1314
+
1315
+ function PanelTitle() {
1316
+ return h(
1317
+ 'span',
1318
+ { style: { display: 'inline-flex', alignItems: 'center', gap: '6px' } },
1319
+ h(MyComputerIcon, { size: 14 }),
1320
+ h('span', null, '我的电脑'),
1321
+ )
1322
+ }
1323
+
1324
+ function apply(ctx) {
1325
+ var disposeStyle = ensureStyle()
1326
+ ctx.effect(function () {
1327
+ return disposeStyle
1328
+ }, 'dsh-my-computer: styles')
1329
+
1330
+ /* ---- 1) 右侧栏 tab 类型(这才是用户看到的「+」菜单里的那一项) ----
1331
+ * 契约见 @deepseek-ai/dsh-client-ui-sidebar-right:
1332
+ * ctx.sidebarRightTabs.register({ id, kind, title, guide })
1333
+ * 正文 ctx.slots.register({ name:'sidebar.right.pane.tab', key:id })
1334
+ * chip 标题 ctx.slots.register({ name:'sidebar.right.pane.tab.title', key:id })
1335
+ * guide 数组是「添加 tab」菜单里的入口框,不给就不出现在菜单里。
1336
+ */
1337
+ ctx.inject(['sidebarRightTabs'], function (c) {
1338
+ var disposers = []
1339
+ function keep(d) {
1340
+ if (typeof d === 'function') disposers.push(d)
1341
+ }
1342
+ function release() {
1343
+ while (disposers.length) {
1344
+ try {
1345
+ disposers.pop()()
1346
+ } catch (e) {}
1347
+ }
1348
+ }
1349
+ try {
1350
+ if (!c || !c.sidebarRightTabs || typeof c.sidebarRightTabs.register !== 'function') {
1351
+ return release
1352
+ }
1353
+ keep(
1354
+ c.sidebarRightTabs.register({
1355
+ id: PANEL_ID,
1356
+ kind: PANEL_ID,
1357
+ title: function () {
1358
+ return '我的电脑'
1359
+ },
1360
+ guide: [
1361
+ {
1362
+ id: PANEL_ID,
1363
+ order: 5,
1364
+ title: function () {
1365
+ return '我的电脑'
1366
+ },
1367
+ description: function () {
1368
+ return '浏览本机全部磁盘(C: / D: …),可上传、下载、改名、删除、编辑'
1369
+ },
1370
+ icon: MyComputerIcon,
1371
+ },
1372
+ ],
1373
+ }),
1374
+ )
1375
+ keep(
1376
+ c.slots.inject('sidebar.right.pane.tab', function () {
1377
+ return c.slots.register({ name: 'sidebar.right.pane.tab', key: PANEL_ID }, MyComputerPanel)
1378
+ }),
1379
+ )
1380
+ keep(
1381
+ c.slots.inject('sidebar.right.pane.tab.title', function () {
1382
+ return c.slots.register({ name: 'sidebar.right.pane.tab.title', key: PANEL_ID }, PanelTitle)
1383
+ }),
1384
+ )
1385
+ } catch (error) {
1386
+ try {
1387
+ window.__MYCOMP_ERR = String((error && error.message) || error)
1388
+ } catch (e) {}
1389
+ release()
1390
+ return
1391
+ }
1392
+ return release
1393
+ })
1394
+
1395
+ /* ---- 2) 左侧栏面板入口(手机上是整屏打开,桌面在主栏) ---- */
1396
+ ctx.slots.inject('sidebar.panellist', function () {
1397
+ return ctx.slots.register(
1398
+ {
1399
+ name: 'sidebar.panellist',
1400
+ id: PANEL_ID,
1401
+ order: -5,
1402
+ label: function () {
1403
+ return '我的电脑'
1404
+ },
1405
+ },
1406
+ MyComputerIcon,
1407
+ )
1408
+ })
1409
+ ctx.slots.inject('main', function () {
1410
+ return ctx.slots.register({ name: 'main', key: PANEL_ID }, MyComputerPanel)
1411
+ })
1412
+ }
1413
+
1414
+ exports.apply = apply
1415
+ exports.inject = ['slots']
1416
+ exports.PANEL_ID = PANEL_ID
1417
+ return module.exports
1418
+ },
1419
+ })
1420
+ } catch (error) {
1421
+ var text = String((error && error.message) || error)
1422
+ if (!/duplicate factory registration/.test(text)) throw error
1423
+ }