/* ── File Manager Module ── */ window.FilesModule = { currentPath: '', contextTarget: null, toDelete: null, pickerMode: false, pickerCallback: null, init: function() { var self = this; var params = new URLSearchParams(window.location.search); if (params.get('picker') === '1') { self.pickerMode = true; self.pickerMode = params.get('mode') || 'dir'; self.pickerCallback = params.get('cb') || null; } window.addEventListener('message', function(e) { try { var d = JSON.parse(e.data); if (d.action === 'fm-picker-open') { self.pickerMode = true; self.pickerMode = d.mode || 'dir'; self.pickerCallback = d.callback_id || null; self.refresh(); } } catch(ex) {} }); // Path input: Enter to jump var input = document.getElementById('fm-path-input'); input.onkeydown = function(e) { if (e.key === 'Enter') { var p = input.value.trim(); if (p) self._jumpTo(p); } }; self.refresh(); }, /* ── API helpers ── */ _apiBase: '', _apiUrl: function(path) { // Use explicit base if set (standalone/iframe mode), otherwise relative if (this._apiBase) return this._apiBase + '/api/files' + path; return './api/files' + path; }, _api: function(url, opts) { opts = opts || {}; opts.credentials = 'include'; return fetch(url, opts).then(function(r) { if (!r.ok) throw new Error(r.status + ' ' + r.statusText); return r.json(); }); }, _get: function(url) { return this._api(this._apiUrl(url)); }, _post: function(url, body) { return this._api(this._apiUrl(url), {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)}); }, /* ── Jump to path (with existence check) ── */ _jumpTo: function(path) { var self = this; // Probe: try to list the target path this._get('/info?path=' + encodeURIComponent(path)) .then(function(info) { if (info.error) { self._pathError(info.error); return; } if (info.is_dir) { self.currentPath = info.path; self.refresh(); } else { self._pathError('路径是文件,不是目录'); } }).catch(function(e) { self._pathError('目录不存在或无权限访问: ' + path); }); }, _pathError: function(msg) { var list = document.getElementById('fm-list'); list.innerHTML = '
' + '⚠ ' + this._esc(msg) + '
'; // Still update the input and breadcrumb to show what was attempted document.getElementById('fm-breadcrumb').innerHTML = '' + this._esc(msg) + ''; }, navigate: function(path) { this.currentPath = path || ''; this.refresh(); }, refresh: function() { var self = this; var showHidden = document.getElementById('fm-hidden')?.checked ? '1' : '0'; self._get('/list?path=' + encodeURIComponent(self.currentPath) + '&show_hidden=' + showHidden) .then(function(d) { self._render(d); }).catch(function(e) { document.getElementById('fm-list').innerHTML = '
加载失败: ' + self._esc(e.message) + '
'; }); }, /* ── Render ── */ _render: function(d) { var self = this; // Update editable path input var input = document.getElementById('fm-path-input'); input.value = d.current || ''; // Breadcrumbs (logical path) var bc = document.getElementById('fm-breadcrumb'); var html = self._buildBreadcrumbHTML(d.breadcrumbs); // Resolved breadcrumbs — show as second row when symlink redirect if (d.resolved_breadcrumbs) { html += '
' + '' + self._buildBreadcrumbHTML(d.resolved_breadcrumbs) + '
'; } bc.innerHTML = html; bc.querySelectorAll('.fm-crumb').forEach(function(el) { el.onclick = function() { self.navigate(this.dataset.path); }; }); // File list var list = document.getElementById('fm-list'); var rows = ''; // ".." row for parent (unless at filesystem root) if (d.parent && d.parent !== d.current) { rows += '
' + '📂' + '..' + '' + '' + '
'; } if (d.items && d.items.length) { d.items.forEach(function(item) { if (item.broken) { // Broken symlink or unreadable entry — show as disabled rows += '
' + '' + '' + self._esc(item.name) + '' + '' + '
'; return; } var icon = item.is_dir ? '📁' : FilesModule._fileIcon(item.ext); var sizeStr = item.is_dir ? '—' : FilesModule._fmtSize(item.size); rows += '
' + '' + icon + '' + '' + self._esc(item.name) + '' + '' + sizeStr + '' + '' + (item.mtime_str || '') + '' + '
'; }); } if (!rows) { list.innerHTML = '
空目录
'; } else { list.innerHTML = rows; } // Row click handlers list.querySelectorAll('.fm-row').forEach(function(row) { row.onclick = function(e) { var p = this.dataset.path; var isDir = this.dataset.isDir === '1'; if (isDir) { self.navigate(p); } else if (self.pickerMode) { self._pickResult(p); } }; row.oncontextmenu = function(e) { e.preventDefault(); self._showContext(e, { path: this.dataset.path, isDir: this.dataset.isDir === '1', name: this.querySelector('.fm-name').textContent }); }; }); // Close context on outside click document.onclick = function() { document.getElementById('fm-context').style.display = 'none'; }; // Picker banner — at bottom if (self.pickerMode) { var banner = document.getElementById('fm-picker-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'fm-picker-banner'; banner.innerHTML = '
' + '📂 选择' + (self.pickerMode === 'file' ? '文件' : '目录') + '模式' + '' + '' + '
'; list.parentNode.appendChild(banner); list.style.flex = ''; banner.style.marginTop = 'auto'; banner.style.paddingBottom = '0'; } else { banner.style.display = 'block'; } } else { var oldBanner = document.getElementById('fm-picker-banner'); if (oldBanner) oldBanner.style.display = 'none'; } }, /* ── Picker ── */ _pickResult: function(path) { // In iframe (picker mode): post to parent, parent closes overlay if (window.parent !== window) { window.parent.postMessage(JSON.stringify({action:'fm-picked', path: path, callback_id: this.pickerCallback}), '*'); return; } // Standalone clipboard fallback navigator.clipboard?.writeText(path); alert('已选择: ' + path); this.pickerMode = false; this.refresh(); }, _cancelPicker: function() { if (window.parent !== window) { window.parent.postMessage(JSON.stringify({action:'fm-picked', path: null, callback_id: this.pickerCallback}), '*'); return; } this.pickerMode = false; this.refresh(); }, /* ── Create ── */ createFile: function() { var self = this; var name = prompt('新建文件名:'); if (!name) return; this._post('/touch', {path: this.currentPath, name: name}) .then(function() { self.refresh(); }) .catch(function(e) { alert('创建失败: ' + e.message); }); }, createDir: function() { var self = this; var name = prompt('新建文件夹名:'); if (!name) return; this._post('/mkdir', {path: this.currentPath, name: name}) .then(function() { self.refresh(); }) .catch(function(e) { alert('创建失败: ' + e.message); }); }, upload: function(files) { var self = this; if (!files || !files.length) return; var form = new FormData(); for (var i = 0; i < files.length; i++) form.append('file', files[i]); fetch(self._apiUrl('/upload?path=') + encodeURIComponent(self.currentPath), {method:'POST', credentials:'include', body:form}) .then(function(r) { return r.json(); }) .then(function(d) { if (d.ok) self.refresh(); else alert('上传失败: ' + (d.error || 'unknown')); }).catch(function(e) { alert('上传失败: ' + e.message); }); }, /* ── Editor ── */ openEditor: function(path, name) { var self = this; this._get('/read?path=' + encodeURIComponent(path)) .then(function(d) { document.getElementById('fm-editor-title').textContent = '📝 ' + (name || d.name); document.getElementById('fm-editor-textarea').value = d.content; document.getElementById('fm-editor-textarea').dataset.path = path; document.getElementById('fm-editor').style.display = 'flex'; }).catch(function(e) { alert('无法读取: ' + e.message); }); }, closeEditor: function() { document.getElementById('fm-editor').style.display = 'none'; }, saveFile: function() { var self = this; var ta = document.getElementById('fm-editor-textarea'); this._post('/write', {path: ta.dataset.path, content: ta.value}) .then(function() { self.closeEditor(); self.refresh(); }) .catch(function(e) { alert('保存失败: ' + e.message); }); }, /* ── Context menu ── */ _showContext: function(e, item) { this.contextTarget = item; var ctx = document.getElementById('fm-context'); ctx.style.display = 'block'; ctx.style.left = e.pageX + 'px'; ctx.style.top = e.pageY + 'px'; var items = ctx.querySelectorAll('.fm-context-item'); items[2].style.display = item.isDir ? 'none' : 'block'; }, ctxDownload: function() { var t = this.contextTarget; if (t) window.open(FilesModule._apiUrl('/download?path=') + encodeURIComponent(t.path), '_blank'); document.getElementById('fm-context').style.display = 'none'; }, ctxRename: function() { var self = this; var t = this.contextTarget; if (!t) return; var nn = prompt('新名称:', t.name); if (!nn || nn === t.name) return; this._post('/rename', {path: t.path, new_name: nn}) .then(function() { self.refresh(); }) .catch(function(e) { alert('重命名失败: ' + e.message); }); document.getElementById('fm-context').style.display = 'none'; }, ctxEdit: function() { var t = this.contextTarget; if (t && !t.isDir) this.openEditor(t.path, t.name); document.getElementById('fm-context').style.display = 'none'; }, ctxDelete: function() { var t = this.contextTarget; if (!t) return; this.toDelete = t; document.getElementById('fm-dialog-msg').textContent = '确认删除 "' + t.name + '"?此操作不可撤销。'; document.getElementById('fm-dialog').style.display = 'flex'; document.getElementById('fm-context').style.display = 'none'; }, confirmDelete: function() { var self = this; if (!this.toDelete) return; this._post('/delete', {path: this.toDelete.path}) .then(function() { self.toDelete = null; self.closeDialog(); self.refresh(); }) .catch(function(e) { alert('删除失败: ' + e.message); }); }, closeDialog: function() { document.getElementById('fm-dialog').style.display = 'none'; this.toDelete = null; }, /* ── Helpers ── */ _buildBreadcrumbHTML: function(crumbs) { if (!crumbs || !crumbs.length) return ''; var h = ''; for (var i = 0; i < crumbs.length; i++) { var c = crumbs[i]; h += '' + this._esc(c.label) + ''; if (i < crumbs.length - 1) h += ''; } return h; }, _esc: function(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); }, _escAttr: function(s) { return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>'); }, _fmtSize: function(bytes) { if (bytes === null || bytes === undefined) return '—'; if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB'; return (bytes / 1073741824).toFixed(2) + ' GB'; }, _fileIcon: function(ext) { var map = { '.py':'🐍','.js':'📜','.ts':'📘','.html':'🌐','.css':'🎨', '.json':'📋','.yaml':'⚙','.yml':'⚙','.md':'📝','.txt':'📄', '.log':'📊','.sh':'💻','.bat':'💻','.xml':'📰','.toml':'⚙', '.cfg':'⚙','.ini':'⚙','.conf':'⚙','.sql':'🗄','.csv':'📊', '.zip':'📦','.tar':'📦','.gz':'📦','.7z':'📦', '.png':'🖼','.jpg':'🖼','.jpeg':'🖼','.gif':'🖼','.svg':'🖼','.ico':'🖼', '.mp3':'🎵','.wav':'🎵','.ogg':'🎵','.mp4':'🎬','.avi':'🎬', '.pdf':'📕','.doc':'📃','.docx':'📃','.xls':'📊','.xlsx':'📊', '.c':'⚡','.h':'⚡','.cpp':'⚡','.java':'☕','.rs':'🦀','.go':'🔵', }; return map[ext] || '📄'; }, destroy: function() { document.getElementById('fm-context').style.display = 'none'; document.getElementById('fm-dialog').style.display = 'none'; document.getElementById('fm-editor').style.display = 'none'; } };