@xcanwin/manyoyo 7.0.26 → 7.1.2

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.
@@ -1,800 +0,0 @@
1
- (function () {
2
- const FILE_EDIT_MAX_BYTES = 2 * 1024 * 1024;
3
- function escapeHtml(value) {
4
- return String(value == null ? '' : value)
5
- .replace(/&/g, '&')
6
- .replace(/</g, '&lt;')
7
- .replace(/>/g, '&gt;')
8
- .replace(/"/g, '&quot;')
9
- .replace(/'/g, '&#39;');
10
- }
11
-
12
- // 双向控制符(RTLO 等)/ 零宽字符可以让文件名视觉上和真实字节完全不一致
13
- // (例如 "cod" + U+202E + "exe.txt" 会被浏览器渲染成 "codtxt.exe"),
14
- // 展示前统一替换成可见的 \uXXXX 记法,破坏其排版效果并让人一眼看出异常。
15
- // 范围:U+200B-U+200F(零宽字符/LRM/RLM)、U+202A-U+202E(嵌入/覆盖方向控制符)、
16
- // U+2060-U+2069(零宽连接符/方向隔离符)、U+FEFF(BOM/零宽不换行空格)、U+061C(阿拉伯字母标记)。
17
- const SUSPICIOUS_UNICODE_PATTERN = /[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u061C]/g;
18
- function sanitizeDisplayText(value) {
19
- return String(value == null ? '' : value).replace(SUSPICIOUS_UNICODE_PATTERN, function (ch) {
20
- return '\\u' + ch.codePointAt(0).toString(16).toUpperCase().padStart(4, '0');
21
- });
22
- }
23
-
24
- function formatBytes(size) {
25
- const value = Number(size || 0);
26
- if (!Number.isFinite(value) || value <= 0) {
27
- return '0 B';
28
- }
29
- if (value < 1024) {
30
- return `${value} B`;
31
- }
32
- if (value < 1024 * 1024) {
33
- return `${(value / 1024).toFixed(1)} KB`;
34
- }
35
- if (value < 1024 * 1024 * 1024) {
36
- return `${(value / (1024 * 1024)).toFixed(1)} MB`;
37
- }
38
- return `${(value / (1024 * 1024 * 1024)).toFixed(1)} GB`;
39
- }
40
-
41
- function formatDateTime(value) {
42
- if (!value) {
43
- return '未知时间';
44
- }
45
- const date = new Date(value);
46
- if (Number.isNaN(date.getTime())) {
47
- return '未知时间';
48
- }
49
- return date.toLocaleString('zh-CN', {
50
- month: '2-digit',
51
- day: '2-digit',
52
- hour: '2-digit',
53
- minute: '2-digit'
54
- });
55
- }
56
-
57
- function buildEntryMeta(entry) {
58
- const parts = [];
59
- if (entry && entry.kind === 'directory') {
60
- parts.push('目录');
61
- } else if (entry && entry.kind === 'symlink') {
62
- parts.push('符号链接');
63
- } else {
64
- parts.push(formatBytes(entry && entry.size));
65
- }
66
- if (entry && entry.mtimeMs) {
67
- parts.push(formatDateTime(entry.mtimeMs));
68
- }
69
- return parts.join(' · ');
70
- }
71
-
72
- function buildEntryTitle(entry) {
73
- const basePath = sanitizeDisplayText(entry && (entry.path || entry.name) || '');
74
- if (entry && entry.kind === 'symlink') {
75
- return entry.symlinkTarget
76
- ? `${basePath} → ${sanitizeDisplayText(entry.symlinkTarget)}`
77
- : `${basePath}(符号链接目标无法解析,可能已损坏)`;
78
- }
79
- return basePath;
80
- }
81
-
82
- function inferLanguageFromPath(filePath) {
83
- const text = String(filePath || '').toLowerCase();
84
- if (text.endsWith('.md') || text.endsWith('.markdown')) return 'markdown';
85
- if (text.endsWith('.json')) return 'json';
86
- if (text.endsWith('.py')) return 'python';
87
- if (text.endsWith('.yaml') || text.endsWith('.yml')) return 'yaml';
88
- if (text.endsWith('.html') || text.endsWith('.htm')) return 'html';
89
- if (text.endsWith('.css')) return 'css';
90
- if (text.endsWith('.js') || text.endsWith('.jsx') || text.endsWith('.mjs') || text.endsWith('.cjs') || text.endsWith('.ts') || text.endsWith('.tsx')) {
91
- return 'javascript';
92
- }
93
- return 'text';
94
- }
95
-
96
- function create(options) {
97
- const root = options && options.root;
98
- const api = options && options.api;
99
- const onError = options && typeof options.onError === 'function'
100
- ? options.onError
101
- : function (message) { window.alert(message); };
102
- // confirmFn/promptFn 可由调用方注入非阻塞的自定义弹窗,缺省退回原生 confirm/prompt
103
- // (和上面 onError 的兜底写法保持一致)。
104
- const confirmFn = options && typeof options.confirmFn === 'function'
105
- ? options.confirmFn
106
- : function (message) { return Promise.resolve(window.confirm(message)); };
107
- const promptFn = options && typeof options.promptFn === 'function'
108
- ? options.promptFn
109
- : function (message, defaultValue) { return Promise.resolve(window.prompt(message, defaultValue)); };
110
- if (!root || typeof api !== 'function') {
111
- return {
112
- sync: function () {}
113
- };
114
- }
115
-
116
- root.innerHTML = `
117
- <section class="files-browser">
118
- <header class="files-toolbar">
119
- <div class="files-toolbar-path-group">
120
- <input type="text" class="files-toolbar-path-input" data-role="path" value="/" spellcheck="false" />
121
- <button type="button" class="secondary" data-action="visit">访问</button>
122
- </div>
123
- <div class="files-toolbar-meta">
124
- <div class="files-toolbar-status" data-role="status">未加载</div>
125
- <button type="button" class="secondary" data-action="mkdir">新建目录</button>
126
- <button type="button" class="secondary" data-action="new-file">新建文件</button>
127
- </div>
128
- </header>
129
- <div class="files-layout">
130
- <aside class="files-sidebar">
131
- <div class="files-list" data-role="list"></div>
132
- </aside>
133
- <section class="files-preview">
134
- <header class="files-preview-head">
135
- <button type="button" class="secondary files-back-btn" data-action="back-to-list">← 返回列表</button>
136
- <div class="files-preview-head-main">
137
- <div class="files-preview-title" data-role="preview-title">未选择文件</div>
138
- <div class="files-preview-meta" data-role="preview-meta">请选择左侧文件或目录</div>
139
- </div>
140
- <div class="files-preview-actions">
141
- <button type="button" class="secondary" data-action="toggle-markdown-view" hidden>查看源码</button>
142
- <button type="button" class="secondary" data-action="save" disabled>保存</button>
143
- </div>
144
- </header>
145
- <div class="files-preview-body" data-role="preview-body"></div>
146
- </section>
147
- </div>
148
- </section>
149
- `;
150
-
151
- const pathNode = root.querySelector('[data-role="path"]');
152
- const statusNode = root.querySelector('[data-role="status"]');
153
- const listNode = root.querySelector('[data-role="list"]');
154
- const previewTitleNode = root.querySelector('[data-role="preview-title"]');
155
- const previewMetaNode = root.querySelector('[data-role="preview-meta"]');
156
- const previewBodyNode = root.querySelector('[data-role="preview-body"]');
157
- const filesBrowserNode = root.querySelector('.files-browser');
158
- const visitBtn = root.querySelector('[data-action="visit"]');
159
- const mkdirBtn = root.querySelector('[data-action="mkdir"]');
160
- const newFileBtn = root.querySelector('[data-action="new-file"]');
161
- const saveBtn = root.querySelector('[data-action="save"]');
162
- const toggleMarkdownViewBtn = root.querySelector('[data-action="toggle-markdown-view"]');
163
- const backToListBtn = root.querySelector('[data-action="back-to-list"]');
164
-
165
- const state = {
166
- visible: false,
167
- sessionName: '',
168
- containerName: '',
169
- containerPath: '',
170
- historyOnly: false,
171
- currentPath: '',
172
- pathDraft: '',
173
- parentPath: '',
174
- entries: [],
175
- selectedPath: '',
176
- selectedFile: null,
177
- selectedEntry: null,
178
- loadingList: false,
179
- loadingFile: false,
180
- savingFile: false,
181
- listRequestId: 0,
182
- readRequestId: 0,
183
- editor: null,
184
- editorHost: null,
185
- previewReadOnly: true,
186
- previewDirty: false,
187
- markdownViewMode: 'rendered',
188
- mobileActivePane: 'list'
189
- };
190
-
191
- function setMobilePane(pane) {
192
- state.mobileActivePane = pane === 'preview' ? 'preview' : 'list';
193
- if (filesBrowserNode) {
194
- filesBrowserNode.setAttribute('data-mobile-pane', state.mobileActivePane);
195
- }
196
- }
197
-
198
- function setStatus(text) {
199
- if (statusNode) {
200
- statusNode.textContent = String(text || '').trim() || '就绪';
201
- }
202
- }
203
-
204
- function destroyEditor() {
205
- if (state.editor && typeof state.editor.destroy === 'function') {
206
- state.editor.destroy();
207
- }
208
- state.editor = null;
209
- state.editorHost = null;
210
- }
211
-
212
- function getPreviewLanguage() {
213
- return state.selectedFile
214
- ? (state.selectedFile.language || inferLanguageFromPath(state.selectedFile.path))
215
- : '';
216
- }
217
-
218
- function isEditablePreview() {
219
- return Boolean(
220
- state.selectedFile
221
- && state.selectedFile.kind === 'text'
222
- && state.previewReadOnly === false
223
- && state.historyOnly !== true
224
- && state.savingFile !== true
225
- && (getPreviewLanguage() !== 'markdown' || state.markdownViewMode === 'source')
226
- );
227
- }
228
-
229
- function syncSaveButton() {
230
- if (!saveBtn) {
231
- return;
232
- }
233
- saveBtn.disabled = !isEditablePreview();
234
- saveBtn.textContent = state.savingFile ? '保存中...' : '保存';
235
- }
236
-
237
- function syncMarkdownToggleButton() {
238
- if (!toggleMarkdownViewBtn) {
239
- return;
240
- }
241
- const isMarkdown = Boolean(state.selectedFile)
242
- && state.selectedFile.kind === 'text'
243
- && getPreviewLanguage() === 'markdown';
244
- toggleMarkdownViewBtn.hidden = !isMarkdown;
245
- toggleMarkdownViewBtn.textContent = state.markdownViewMode === 'source' ? '查看渲染' : '查看源码';
246
- }
247
-
248
- function resolveMarkdownImageUrl(basePath, relativeHref) {
249
- try {
250
- const lastSlash = String(basePath || '').lastIndexOf('/');
251
- const baseDir = lastSlash >= 0 ? basePath.slice(0, lastSlash + 1) : '/';
252
- const resolvedPath = new URL(relativeHref, 'http://manyoyo-internal' + baseDir).pathname;
253
- return '/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/raw?path=' + encodeURIComponent(resolvedPath);
254
- } catch (e) {
255
- return '';
256
- }
257
- }
258
-
259
- function renderPreviewEmpty(title, description) {
260
- state.selectedFile = null;
261
- state.previewReadOnly = true;
262
- state.previewDirty = false;
263
- if (previewTitleNode) {
264
- previewTitleNode.textContent = sanitizeDisplayText(title);
265
- }
266
- if (previewMetaNode) {
267
- previewMetaNode.textContent = description;
268
- }
269
- if (previewBodyNode) {
270
- previewBodyNode.innerHTML = `<div class="files-empty">${escapeHtml(description)}</div>`;
271
- }
272
- destroyEditor();
273
- syncMarkdownToggleButton();
274
- syncSaveButton();
275
- }
276
-
277
- function ensureEditorHost() {
278
- if (!previewBodyNode) {
279
- return null;
280
- }
281
- previewBodyNode.innerHTML = '';
282
- const host = document.createElement('div');
283
- host.className = 'files-editor-host';
284
- previewBodyNode.appendChild(host);
285
- state.editorHost = host;
286
- return host;
287
- }
288
-
289
- function updatePreviewMeta() {
290
- const payload = state.selectedFile;
291
- if (!previewMetaNode || !payload) {
292
- return;
293
- }
294
- const modeLabel = payload.kind === 'text'
295
- ? (payload.editable === true ? '可编辑' : '只读预览')
296
- : '只读预览';
297
- previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)} · ${modeLabel}${payload.truncated ? ' · 已截断预览' : ''}`;
298
- }
299
-
300
- function renderPreviewPayload(payload) {
301
- state.selectedFile = payload || null;
302
- state.previewReadOnly = !(payload && payload.editable === true);
303
- state.previewDirty = false;
304
- if (!payload) {
305
- renderPreviewEmpty('未选择文件', '请选择左侧文件进行预览。');
306
- return;
307
- }
308
- if (previewTitleNode) {
309
- previewTitleNode.textContent = sanitizeDisplayText(payload.path) || '未命名文件';
310
- }
311
- updatePreviewMeta();
312
- if (!previewBodyNode) {
313
- syncMarkdownToggleButton();
314
- syncSaveButton();
315
- return;
316
- }
317
-
318
- if (payload.kind === 'text') {
319
- const language = payload.language || inferLanguageFromPath(payload.path);
320
- const showMarkdownRendered = language === 'markdown'
321
- && state.markdownViewMode !== 'source'
322
- && window.ManyoyoMarkdown
323
- && typeof window.ManyoyoMarkdown.render === 'function';
324
-
325
- if (showMarkdownRendered) {
326
- destroyEditor();
327
- const markdownNode = document.createElement('div');
328
- markdownNode.className = 'md-content files-markdown-preview';
329
- markdownNode.innerHTML = window.ManyoyoMarkdown.render(String(payload.content || ''), {
330
- imageUrlResolver: function (relativeHref) {
331
- return resolveMarkdownImageUrl(payload.path, relativeHref);
332
- }
333
- });
334
- previewBodyNode.innerHTML = '';
335
- previewBodyNode.appendChild(markdownNode);
336
- syncMarkdownToggleButton();
337
- syncSaveButton();
338
- return;
339
- }
340
-
341
- if (window.ManyoyoCodeEditor && typeof window.ManyoyoCodeEditor.create === 'function') {
342
- if (!state.editor || !state.editorHost || !previewBodyNode.contains(state.editorHost)) {
343
- destroyEditor();
344
- const host = ensureEditorHost();
345
- if (host) {
346
- state.editor = window.ManyoyoCodeEditor.create(host, {
347
- doc: String(payload.content || ''),
348
- language,
349
- readOnly: state.previewReadOnly,
350
- onChange: function (nextValue) {
351
- state.previewDirty = true;
352
- if (state.selectedFile) {
353
- state.selectedFile.size = new TextEncoder().encode(nextValue).length;
354
- }
355
- updatePreviewMeta();
356
- syncSaveButton();
357
- }
358
- });
359
- }
360
- } else {
361
- state.editor.setValue(String(payload.content || ''));
362
- state.editor.setLanguage(language);
363
- state.editor.setReadOnly(state.previewReadOnly);
364
- }
365
- syncMarkdownToggleButton();
366
- syncSaveButton();
367
- return;
368
- }
369
-
370
- destroyEditor();
371
- previewBodyNode.innerHTML = `<pre class="files-pre">${escapeHtml(String(payload.content || ''))}</pre>`;
372
- syncMarkdownToggleButton();
373
- syncSaveButton();
374
- return;
375
- }
376
-
377
- if (payload.kind === 'image') {
378
- destroyEditor();
379
- const rawUrl = '/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/raw?path='
380
- + encodeURIComponent(payload.path);
381
- previewBodyNode.innerHTML = `<div class="files-image-preview"><img src="${escapeHtml(rawUrl)}" alt="${escapeHtml(payload.path || '')}" /></div>`;
382
- syncMarkdownToggleButton();
383
- syncSaveButton();
384
- return;
385
- }
386
-
387
- destroyEditor();
388
- previewBodyNode.innerHTML = `<div class="files-note">当前文件暂不支持在线预览。文件类型:${escapeHtml(payload.kind || 'unknown')}</div>`;
389
- syncMarkdownToggleButton();
390
- syncSaveButton();
391
- }
392
-
393
- function openSymlinkEntry(entry) {
394
- const safeName = sanitizeDisplayText(entry.name);
395
- const message = entry.symlinkTarget
396
- ? `"${safeName}" 是符号链接,实际指向:\n${sanitizeDisplayText(entry.symlinkTarget)}\n\n是否继续访问?`
397
- : `"${safeName}" 是一个无法解析的符号链接(可能已损坏),是否仍要尝试访问?`;
398
- confirmFn(message).then(function (proceed) {
399
- if (!proceed) {
400
- return;
401
- }
402
- if (entry.symlinkTargetKind === 'directory') {
403
- loadDirectory(entry.path);
404
- return;
405
- }
406
- loadFile(entry.path, entry);
407
- });
408
- }
409
-
410
- function renderList() {
411
- if (pathNode) {
412
- pathNode.value = state.pathDraft || state.currentPath || state.containerPath || '/';
413
- }
414
- if (visitBtn) {
415
- visitBtn.disabled = state.loadingList || state.loadingFile || !(state.pathDraft || '').trim();
416
- }
417
- if (mkdirBtn) {
418
- mkdirBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
419
- }
420
- if (newFileBtn) {
421
- newFileBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
422
- }
423
- if (!listNode) {
424
- return;
425
- }
426
- listNode.innerHTML = '';
427
-
428
- if (!state.sessionName) {
429
- setMobilePane('list');
430
- listNode.innerHTML = '<div class="files-empty">请选择左侧会话后再浏览容器文件。</div>';
431
- renderPreviewEmpty('未选择会话', '请选择左侧会话后再浏览容器文件。');
432
- setStatus('未选择会话');
433
- return;
434
- }
435
- if (state.historyOnly) {
436
- setMobilePane('list');
437
- listNode.innerHTML = '<div class="files-empty">当前会话只有历史记录,没有可访问的运行中容器。</div>';
438
- renderPreviewEmpty('容器不可用', '当前会话只有历史记录,没有可访问的运行中容器。');
439
- setStatus('容器不可用');
440
- return;
441
- }
442
- if (state.loadingList) {
443
- listNode.innerHTML = '<div class="files-empty">正在读取目录...</div>';
444
- setStatus('读取目录中');
445
- return;
446
- }
447
-
448
- if (state.parentPath) {
449
- const parentButton = document.createElement('button');
450
- parentButton.type = 'button';
451
- parentButton.className = 'files-entry files-entry-parent';
452
- parentButton.title = sanitizeDisplayText(state.parentPath);
453
- parentButton.addEventListener('click', function () {
454
- loadDirectory(state.parentPath);
455
- });
456
- parentButton.innerHTML = `
457
- <span class="files-entry-name">
458
- <span class="files-entry-title">..</span>
459
- </span>
460
- <span class="files-entry-meta">上一级</span>
461
- `;
462
- listNode.appendChild(parentButton);
463
- }
464
-
465
- if (!state.entries.length) {
466
- setStatus('目录为空');
467
- return;
468
- }
469
-
470
- state.entries.forEach(function (entry) {
471
- const button = document.createElement('button');
472
- button.type = 'button';
473
- button.className = 'files-entry' + (state.selectedPath === entry.path ? ' is-active' : '');
474
- button.title = buildEntryTitle(entry);
475
- button.addEventListener('click', function () {
476
- if (entry.kind === 'symlink') {
477
- openSymlinkEntry(entry);
478
- return;
479
- }
480
- if (entry.kind === 'directory') {
481
- loadDirectory(entry.path);
482
- return;
483
- }
484
- loadFile(entry.path, entry);
485
- });
486
- button.innerHTML = `
487
- <span class="files-entry-name">
488
- <span class="files-entry-title">${escapeHtml(sanitizeDisplayText(entry.name || entry.path || '未命名'))}</span>
489
- </span>
490
- <span class="files-entry-meta">${escapeHtml(buildEntryMeta(entry))}</span>
491
- `;
492
- listNode.appendChild(button);
493
- });
494
- setStatus(state.loadingFile ? '读取文件中' : `共 ${state.entries.length} 项`);
495
- }
496
-
497
- async function loadDirectory(targetPath) {
498
- const pathText = String(targetPath || state.currentPath || state.containerPath || '/').trim() || '/';
499
- const requestId = state.listRequestId + 1;
500
- state.listRequestId = requestId;
501
- state.loadingList = true;
502
- state.pathDraft = pathText;
503
- state.selectedPath = '';
504
- state.selectedEntry = null;
505
- setMobilePane('list');
506
- renderList();
507
- try {
508
- const payload = await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/list?path=' + encodeURIComponent(pathText));
509
- if (requestId !== state.listRequestId) {
510
- return;
511
- }
512
- state.currentPath = payload && payload.path ? payload.path : pathText;
513
- state.pathDraft = state.currentPath;
514
- state.parentPath = payload && payload.parentPath ? payload.parentPath : '';
515
- state.entries = Array.isArray(payload && payload.entries) ? payload.entries : [];
516
- renderPreviewEmpty('未选择文件', '请选择左侧文件进行预览。');
517
- } catch (e) {
518
- if (requestId !== state.listRequestId) {
519
- return;
520
- }
521
- onError(e && e.message ? e.message : '读取目录失败');
522
- } finally {
523
- if (requestId !== state.listRequestId) {
524
- return;
525
- }
526
- state.loadingList = false;
527
- renderList();
528
- }
529
- }
530
-
531
- async function loadFile(targetPath, entry) {
532
- const pathText = String(targetPath || '').trim();
533
- if (!pathText) {
534
- return;
535
- }
536
- const selectedEntry = entry && typeof entry === 'object' ? entry : null;
537
- const fileSize = Number(selectedEntry && selectedEntry.size);
538
- const requiresReadonlyConfirm = Number.isFinite(fileSize) && fileSize >= FILE_EDIT_MAX_BYTES;
539
- if (requiresReadonlyConfirm) {
540
- const yes = await confirmFn(`文件较大(${formatBytes(fileSize)}),继续后将以只读方式全量预览,无法保存。是否继续?`);
541
- if (!yes) {
542
- return;
543
- }
544
- }
545
- const requestId = state.readRequestId + 1;
546
- state.readRequestId = requestId;
547
- state.loadingFile = true;
548
- state.selectedPath = pathText;
549
- state.selectedEntry = selectedEntry;
550
- state.markdownViewMode = 'rendered';
551
- setMobilePane('preview');
552
- renderList();
553
- renderPreviewEmpty(pathText, '正在读取文件内容...');
554
- try {
555
- const payload = await api(
556
- '/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/read?path='
557
- + encodeURIComponent(pathText)
558
- + '&full=1'
559
- );
560
- if (requestId !== state.readRequestId) {
561
- return;
562
- }
563
- if (requiresReadonlyConfirm && payload && payload.kind === 'text') {
564
- payload.editable = false;
565
- }
566
- renderPreviewPayload(payload);
567
- } catch (e) {
568
- if (requestId !== state.readRequestId) {
569
- return;
570
- }
571
- renderPreviewEmpty(pathText, e && e.message ? e.message : '读取文件失败');
572
- onError(e && e.message ? e.message : '读取文件失败');
573
- } finally {
574
- if (requestId !== state.readRequestId) {
575
- return;
576
- }
577
- state.loadingFile = false;
578
- renderList();
579
- }
580
- }
581
-
582
- async function saveCurrentFile() {
583
- if (!isEditablePreview() || !state.editor || typeof state.editor.getValue !== 'function' || !state.selectedFile || !state.selectedFile.path) {
584
- return;
585
- }
586
- state.savingFile = true;
587
- syncSaveButton();
588
- setStatus('保存中');
589
- try {
590
- const nextContent = state.editor.getValue();
591
- const payload = await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/write', {
592
- method: 'PUT',
593
- body: JSON.stringify({
594
- path: state.selectedFile.path,
595
- content: nextContent
596
- })
597
- });
598
- state.previewDirty = false;
599
- if (state.selectedFile) {
600
- state.selectedFile.content = nextContent;
601
- state.selectedFile.size = payload && typeof payload.size === 'number'
602
- ? payload.size
603
- : new TextEncoder().encode(nextContent).length;
604
- }
605
- const matchedEntry = state.entries.find(function (item) {
606
- return item && state.selectedFile && item.path === state.selectedFile.path;
607
- });
608
- if (matchedEntry && state.selectedFile) {
609
- matchedEntry.size = state.selectedFile.size;
610
- }
611
- renderPreviewPayload(state.selectedFile);
612
- renderList();
613
- setStatus('已保存');
614
- } catch (e) {
615
- setStatus('保存失败');
616
- onError(e && e.message ? e.message : '保存文件失败');
617
- } finally {
618
- state.savingFile = false;
619
- syncSaveButton();
620
- }
621
- }
622
-
623
- function joinDirectoryPath(basePath, childName) {
624
- const base = String(basePath || '/').trim() || '/';
625
- const child = String(childName || '').trim();
626
- if (!child) {
627
- return base;
628
- }
629
- if (base === '/') {
630
- return '/' + child.replace(/^\/+/, '');
631
- }
632
- return base.replace(/\/+$/, '') + '/' + child.replace(/^\/+/, '');
633
- }
634
-
635
- async function createDirectory() {
636
- if (!state.sessionName || state.historyOnly === true || state.loadingList || state.loadingFile) {
637
- return;
638
- }
639
- const input = await promptFn('请输入新目录名称');
640
- const name = String(input || '').trim();
641
- if (!name) {
642
- return;
643
- }
644
- const targetPath = joinDirectoryPath(state.currentPath || state.containerPath || '/', name);
645
- setStatus('创建目录中');
646
- try {
647
- await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/mkdir', {
648
- method: 'POST',
649
- body: JSON.stringify({ path: targetPath })
650
- });
651
- await loadDirectory(state.currentPath || state.containerPath || '/');
652
- setStatus('已创建目录');
653
- } catch (e) {
654
- setStatus('创建目录失败');
655
- onError(e && e.message ? e.message : '创建目录失败');
656
- }
657
- }
658
-
659
- async function createFile() {
660
- if (!state.sessionName || state.historyOnly === true || state.loadingList || state.loadingFile) {
661
- return;
662
- }
663
- const input = await promptFn('请输入新文件名称');
664
- const name = String(input || '').trim();
665
- if (!name) {
666
- return;
667
- }
668
- const targetPath = joinDirectoryPath(state.currentPath || state.containerPath || '/', name);
669
- setStatus('创建文件中');
670
- try {
671
- await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/create', {
672
- method: 'POST',
673
- body: JSON.stringify({ path: targetPath })
674
- });
675
- await loadDirectory(state.currentPath || state.containerPath || '/');
676
- setStatus('已创建文件');
677
- } catch (e) {
678
- setStatus('创建文件失败');
679
- onError(e && e.message ? e.message : '创建文件失败');
680
- }
681
- }
682
-
683
- function sync(context) {
684
- const session = context && context.session;
685
- const detail = context && context.detail;
686
- const nextSessionName = String(session && session.name ? session.name : '').trim();
687
- const nextContainerName = String(session && session.containerName ? session.containerName : '').trim();
688
- const nextContainerPath = String(
689
- (detail && detail.containerPath)
690
- || (session && session.containerPath)
691
- || '/'
692
- ).trim() || '/';
693
- const nextVisible = Boolean(context && context.visible);
694
- const nextHistoryOnly = context && context.historyOnly === true;
695
- const sessionChanged = nextSessionName !== state.sessionName;
696
- const containerPathChanged = nextContainerPath !== state.containerPath;
697
-
698
- state.visible = nextVisible;
699
- state.historyOnly = nextHistoryOnly;
700
-
701
- if (sessionChanged) {
702
- state.sessionName = nextSessionName;
703
- state.containerName = nextContainerName;
704
- state.containerPath = nextContainerPath;
705
- state.currentPath = '';
706
- state.pathDraft = nextContainerPath;
707
- state.parentPath = '';
708
- state.entries = [];
709
- state.selectedPath = '';
710
- state.selectedEntry = null;
711
- setMobilePane('list');
712
- renderPreviewEmpty('未选择文件', '请选择左侧文件进行预览。');
713
- } else if (containerPathChanged) {
714
- state.containerPath = nextContainerPath;
715
- if (!state.currentPath) {
716
- state.pathDraft = nextContainerPath;
717
- }
718
- }
719
-
720
- renderList();
721
- if (!nextVisible || !state.sessionName || state.historyOnly) {
722
- return;
723
- }
724
- if (sessionChanged || containerPathChanged || !state.currentPath) {
725
- loadDirectory(state.containerPath || '/');
726
- }
727
- }
728
-
729
- if (pathNode) {
730
- pathNode.addEventListener('input', function () {
731
- state.pathDraft = pathNode.value;
732
- renderList();
733
- });
734
- pathNode.addEventListener('keydown', function (event) {
735
- if (event.key === 'Enter') {
736
- event.preventDefault();
737
- loadDirectory(pathNode.value);
738
- }
739
- });
740
- }
741
-
742
- if (visitBtn) {
743
- visitBtn.addEventListener('click', function () {
744
- loadDirectory(state.pathDraft || state.currentPath || state.containerPath || '/');
745
- });
746
- }
747
-
748
- if (mkdirBtn) {
749
- mkdirBtn.addEventListener('click', function () {
750
- createDirectory().catch(function (e) {
751
- onError(e && e.message ? e.message : '创建目录失败');
752
- });
753
- });
754
- }
755
-
756
- if (newFileBtn) {
757
- newFileBtn.addEventListener('click', function () {
758
- createFile().catch(function (e) {
759
- onError(e && e.message ? e.message : '创建文件失败');
760
- });
761
- });
762
- }
763
-
764
- if (saveBtn) {
765
- saveBtn.addEventListener('click', function () {
766
- saveCurrentFile().catch(function (e) {
767
- onError(e && e.message ? e.message : '保存文件失败');
768
- });
769
- });
770
- }
771
-
772
- if (toggleMarkdownViewBtn) {
773
- toggleMarkdownViewBtn.addEventListener('click', function () {
774
- if (state.editor && typeof state.editor.getValue === 'function' && state.selectedFile) {
775
- state.selectedFile.content = state.editor.getValue();
776
- }
777
- state.markdownViewMode = state.markdownViewMode === 'source' ? 'rendered' : 'source';
778
- renderPreviewPayload(state.selectedFile);
779
- });
780
- }
781
-
782
- if (backToListBtn) {
783
- backToListBtn.addEventListener('click', function () {
784
- setMobilePane('list');
785
- });
786
- }
787
-
788
- setMobilePane('list');
789
- renderList();
790
-
791
- return {
792
- sync
793
- };
794
- }
795
-
796
- window.ManyoyoFileBrowser = {
797
- create,
798
- sanitizeDisplayText
799
- };
800
- }());