fix: 移除遗留fmfuncs目录创建 + 清理init_service无用导入 + 文件管理器SVG图标 + 示例插件MD3改造

- init_service: 删除_load_fmfuncs方法和fmfuncs目录创建(已迁移到sdk/)
- init_service: 清理未使用的importlib.util和sys导入
- 文件管理器: 全部emoji替换为MD3 SVG矢量图标(文件夹/文件类型/右键菜单)
- 示例插件: dashboard.html改为MD3风格 CSS变量自动适配日夜主题
- 面包屑: 加底色+模糊+左右外边距
- CSS: .btn::after加pointer-events:none
- 插件开发指南: 更新2.6节MD3主题同步和完善的CSS变量/组件类参考表

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-12 19:00:19 +08:00
parent 9b997d8bda
commit 9de094ea54
16 changed files with 283 additions and 308 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ commands:
permissions: permissions:
- framework.command.test - framework.command.test
source: internal source: internal
last_updated: 297792.734060618 last_updated: 316707.28385033
plugin_commands: plugin_commands:
example_plugin: example_plugin:
echo: *id001 echo: *id001
+1 -1
View File
@@ -1,5 +1,5 @@
http_port: 4200 http_port: 4200
last_updated: 297792.741311191 last_updated: 316707.287225746
plugin_routes: plugin_routes:
example_plugin: example_plugin:
- methods: - methods:
+55 -24
View File
@@ -3432,41 +3432,72 @@ class MyPlugin(PluginWebMixin):
- 提取 `<script>` 标签,通过动态 script 元素执行 - 提取 `<script>` 标签,通过动态 script 元素执行
- 页面 content 区 padding 归零,插件内容边到边铺满 - 页面 content 区 padding 归零,插件内容边到边铺满
#### 2.6.3 HTML 编写建议 #### 2.6.3 MD3 主题同步与 HTML 编写建议
插件页面会自动注入到 SenSu 主面板的 `<div class=\"plugin-page-root\">` 容器中。**CSS 中的所有 `body` 选择器会被自动替换为 `.plugin-page-root`**,不会污染主页面。
**主题同步无需任何代码** — CSS 中使用 `var(--xxx)` 引用 MD3 token,日夜切换自动生效。
```html ```html
<!-- 推荐:使用内联样式 + 容器类,不依赖 body 选择器 --> <!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>My Plugin</title>
<style> <style>
/* body 选择器不会生效(body 标签已被剥离) */ /* ✅ 正确: 使用 MD3 CSS 变量,自动适配日夜主题 */
/* 改用类选择器或直接用容器 div */ .plugin-page-root{font-family:system-ui,sans-serif;padding:20px;min-height:100%}
.plugin-root { h2{color:var(--primary);font-weight:500}
background: var(--bg); /* 继承 SenSu 主题背景 */ /* ✅ 使用 MD3 组件类 */
color: var(--text); /* 继承 SenSu 主题文字 */ </style></head><body>
font-family: system-ui, sans-serif; <h2>插件面板</h2>
padding: 16px; <!-- ✅ 复用 MD3 卡片样式 -->
min-height: 100%; <div class="card" style="margin-bottom:16px">
} <h3>数据面板</h3>
.plugin-root h2 { color: var(--primary); } <div id="value">0</div>
</style> <button class="btn btn-sm btn-filled" onclick="doAction()">操作</button>
<div class="plugin-root">
<h2>插件面板标题</h2>
<button class="btn btn-sm btn-tonal" onclick="...">操作</button>
</div> </div>
<script> <script>
// 脚本会被动态 script 元素执行,声明的函数可被 onclick 调用 function doAction(){ /* ... */ }
function handleClick() { ... } </script></body></html>
</script>
``` ```
**常用 MD3 CSS 变量 (自动跟随日夜主题):**
| 变量 | 用途 |
|------|------|
| `--bg` | 页面背景色 |
| `--bg-card` | 卡片/容器背景 |
| `--text` | 主文字色 |
| `--text-dim` | 次要文字色 |
| `--primary` | 主题色 |
| `--primary-container` | 主题色容器背景 |
| `--outline` | 边框色 |
| `--error` | 错误/危险色 |
| `--shape-xs` / `--shape-sm` / `--shape-md` | 圆角 (8/12/16px) |
| `--md-sys-elevation-1` ~ `--md-sys-elevation-5` | 阴影 |
**常用 MD3 组件类:**
| 类 | 用途 |
|------|------|
| `.card` | MD3 卡片容器 |
| `.card.outlined` | 带边框的卡片 |
| `.btn` + `.btn-filled` | 实心按钮 |
| `.btn` + `.btn-tonal` | 半透明按钮 |
| `.btn` + `.btn-outlined` | 轮廓按钮 |
| `.btn-sm` / `.btn-lg` | 按钮尺寸 (需配合 .btn) |
| `.input` | MD3 输入框 |
| `.input-group` | 输入框容器 (含 label) |
| `.badge` / `.badge-run` / `.badge-stop` | 状态徽章 |
| `.chip` / `.chip.active` | 标签/筛选 |
#### 2.6.4 注意事项 #### 2.6.4 注意事项
| 事项 | 说明 | | 事项 | 说明 |
|------|------| |------|------|
| `body` 选择器 | CSS 中 `body { ... }` 不会生效,改用容器类 | | `body` 选择器 | v0.6.0 起自动替换为 `.plugin-page-root`,可放心使用 |
| 硬编码背景色 | 避免 `background: #000`,用 `var(--bg)` 自适应主题 | | 硬编码色 | `background:#000` / `color:#fff` → ✅ `var(--bg)` / `var(--text)` |
| `onclick` 函数 | 函数需在 `<script>` 中声明,浏览器 innerHTML 不执行 script | | 主题同步 | 无需额外代码,CSS 变量自动跟随 `data-theme` 属性切换 |
| `<html>/<head>` | 可省略,直接写 body 内容 | | `onclick` 函数 | 函数需在 `<script>` 中声明,框架通过动态 script 元素执行 |
| 文件管理器调用 | 可通过 postMessage API 唤出文件选择器 (见第八章) | | `<html>/<head>` | 可完整书写,框架自动剥离包装标签 |
| 文件管理器调用 | 通过 `window.pickPath('dir', callback)` 唤出选择器 |
## 三、插件生命周期管理 ## 三、插件生命周期管理
+31 -9
View File
@@ -1,12 +1,34 @@
<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>Example</title> <!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>Example</title>
<style>body{font-family:monospace;background:#0e1416;color:#e0e3e4;padding:16px} <style>
h2{color:#00bcd4}.card{background:#1a1f21;border-radius:8px;padding:12px;margin:8px 0} .plugin-page-root{font-family:system-ui,sans-serif;padding:20px;min-height:100%}
button{background:#00bcd4;color:#000;padding:8px 16px;border:none;border-radius:4px;cursor:pointer} h2{color:var(--primary);font-weight:500;margin-bottom:16px}
.counter{font-size:48px;color:#4caf50;text-align:center;padding:20px} .counter{font-size:48px;color:var(--primary);text-align:center;padding:20px}
.log{background:#000;color:#0f0;padding:8px;border-radius:4px;max-height:200px;overflow-y:auto;font-size:12px} .log{background:var(--md-sys-color-surface-container-lowest);color:#4caf50;padding:8px 12px;border-radius:var(--shape-xs);max-height:200px;overflow-y:auto;font-size:12px;font-family:monospace;line-height:1.6}
</style></head><body> </style></head><body>
<h2>Example Plugin Panel</h2> <h2>Example Plugin Panel</h2>
<div class="card"><h3>Counter</h3><div class="counter" id="count">0</div> <div class="card" style="margin-bottom:16px">
<button onclick="inc()">+1</button> <button onclick="reset()">Reset</button></div> <h3 style="margin-bottom:8px">Counter</h3>
<div class="card"><h3>Events (SSE)</h3><div class="log" id="log">Waiting...</div></div> <div class="counter" id="count">0</div>
<script>let n=0;function inc(){n++;document.getElementById("count").textContent=n;fetch("/SenSu/plugin/example_plugin/event",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"counter",value:n})})}function reset(){n=0;document.getElementById("count").textContent=0}const es=new EventSource("/SenSu/plugin/example_plugin/sse");es.addEventListener("counter",e=>{const d=JSON.parse(e.data);document.getElementById("log").innerHTML+=new Date().toLocaleTimeString()+" counter="+d.value+"<br>"})</script></body></html> <div style="display:flex;gap:8px;justify-content:center">
<button class="btn btn-sm btn-filled" onclick="inc()">+1</button>
<button class="btn btn-sm btn-outlined" onclick="reset()">Reset</button>
</div>
</div>
<div class="card">
<h3 style="margin-bottom:8px">Events (SSE)</h3>
<div class="log" id="log">Waiting...</div>
</div>
<script>
var n=0;
function inc(){
n++;
document.getElementById("count").textContent=n;
fetch("/SenSu/plugin/example_plugin/event",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"counter",value:n})});
}
function reset(){n=0;document.getElementById("count").textContent=0;}
var es=new EventSource("/SenSu/plugin/example_plugin/sse");
es.addEventListener("counter",function(e){
var d=JSON.parse(e.data);
document.getElementById("log").innerHTML+=new Date().toLocaleTimeString()+" counter="+d.value+"<br>";
});
</script></body></html>
+2 -44
View File
@@ -6,8 +6,6 @@ import asyncio
from pathlib import Path from pathlib import Path
from typing import Dict, Any from typing import Dict, Any
import yaml import yaml
import importlib.util
import sys
import os import os
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -18,7 +16,6 @@ class InitService:
def __init__(self, config_path: str = "config/framework"): def __init__(self, config_path: str = "config/framework"):
self.config_path = Path(config_path) self.config_path = Path(config_path)
self.configs: Dict[str, Any] = {} self.configs: Dict[str, Any] = {}
self.fmfuncs_loaded = False
logger.debug("InitService初始化开始") logger.debug("InitService初始化开始")
async def initialize_framework(self): async def initialize_framework(self):
@@ -32,10 +29,7 @@ class InitService:
# 2. 创建必要目录 # 2. 创建必要目录
await self._create_directories() await self._create_directories()
# 3. 加载框架功能集 # 3. 验证初始化状态
await self._load_fmfuncs()
# 4. 验证初始化状态
await self._validate_init() await self._validate_init()
logger.info("框架初始化完成") logger.info("框架初始化完成")
@@ -94,8 +88,7 @@ class InitService:
"logs/runtime", "logs/runtime",
"logs/debug", "logs/debug",
"plugins", "plugins",
"utils", "utils"
"fmfuncs"
] ]
for dir_path in directories: for dir_path in directories:
@@ -109,41 +102,6 @@ class InitService:
logger.error(f"创建目录时出错: {str(e)}", exc_info=True) logger.error(f"创建目录时出错: {str(e)}", exc_info=True)
raise raise
async def _load_fmfuncs(self):
"""加载框架功能集"""
try:
logger.debug("开始加载框架功能集")
fmfuncs_path = Path(os.getenv("SENSU_CODE_DIR", ".")) / "fmfuncs"
if not fmfuncs_path.exists():
logger.warning("fmfuncs目录不存在,跳过加载")
return
# 动态加载所有Python文件
for py_file in fmfuncs_path.glob("*.py"):
if py_file.name == "__init__.py":
continue
try:
module_name = f"fmfuncs.{py_file.stem}"
spec = importlib.util.spec_from_file_location(module_name, py_file)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
logger.debug(f"加载框架功能: {module_name}")
except Exception as e:
logger.error(f"加载框架功能 {py_file} 时出错: {str(e)}", exc_info=True)
continue
self.fmfuncs_loaded = True
logger.debug("框架功能集加载完成")
except Exception as e:
logger.error(f"加载框架功能集时出错: {str(e)}", exc_info=True)
raise
async def _validate_init(self): async def _validate_init(self):
"""验证初始化状态""" """验证初始化状态"""
try: try:
+6 -3
View File
@@ -275,6 +275,9 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
.tab.active{background:var(--primary-container);color:var(--md-sys-color-on-primary-container)} .tab.active{background:var(--primary-container);color:var(--md-sys-color-on-primary-container)}
.tab:hover:not(.active){color:var(--text)} .tab:hover:not(.active){color:var(--text)}
/* ── Plugin page container ── */
.plugin-page-root{font-family:inherit;color:inherit}
/* ── Responsive ── */ /* ── Responsive ── */
@media(max-width:1100px){.dash-layout{flex-direction:column;height:auto;overflow:visible}.dash-sidebar{width:100%}} @media(max-width:1100px){.dash-layout{flex-direction:column;height:auto;overflow:visible}.dash-sidebar{width:100%}}
@media(max-width:768px){.app-frame{grid-template-columns:var(--sidebar-collapsed) 1fr}.nav-label{display:none}.toggle-sidebar{display:none}} @media(max-width:768px){.app-frame{grid-template-columns:var(--sidebar-collapsed) 1fr}.nav-label{display:none}.toggle-sidebar{display:none}}
@@ -353,6 +356,7 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
.btn::after{ .btn::after{
content:"";position:absolute;inset:0;background:radial-gradient(circle at center,currentColor 10%,transparent 10%); content:"";position:absolute;inset:0;background:radial-gradient(circle at center,currentColor 10%,transparent 10%);
background-size:0 0;background-repeat:no-repeat;opacity:0;transition:background-size .4s,opacity .3s; background-size:0 0;background-repeat:no-repeat;opacity:0;transition:background-size .4s,opacity .3s;
pointer-events:none;
} }
.btn:active::after{background-size:300% 300%;opacity:.12;transition:0s} .btn:active::after{background-size:300% 300%;opacity:.12;transition:0s}
@@ -443,10 +447,9 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
═══════════════════════════════════════════ */ ═══════════════════════════════════════════ */
.fm-container{display:flex;flex-direction:column;height:calc(100vh - var(--topbar-h) - 48px);gap:12px} .fm-container{display:flex;flex-direction:column;height:calc(100vh - var(--topbar-h) - 48px);gap:12px}
.fm-toolbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px} .fm-toolbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px}
.fm-path-bar{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px} .fm-path-input{flex:1;min-width:140px;padding:8px 12px;background:var(--md-sys-color-surface-container-lowest);border:1px solid var(--outline);border-radius:var(--shape-xs);color:var(--text);font-family:"JetBrains Mono",monospace;font-size:12px;outline:none;transition:border-color .2s}
.fm-path-input{width:100%;padding:8px 12px;background:var(--md-sys-color-surface-container-lowest);border:1px solid var(--outline);border-radius:var(--shape-xs);color:var(--text);font-family:"JetBrains Mono",monospace;font-size:12px;outline:none;transition:border-color .2s}
.fm-path-input:focus{border-color:var(--primary);box-shadow:0 0 0 2px rgba(208,188,255,.15)} .fm-path-input:focus{border-color:var(--primary);box-shadow:0 0 0 2px rgba(208,188,255,.15)}
.fm-breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:4px;overflow-x:auto;scrollbar-width:none;flex:1;min-width:0} .fm-breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:4px;padding:6px 10px;margin:4px 8px 0 8px;overflow-x:auto;scrollbar-width:none;flex-shrink:0;background:var(--md-sys-color-surface-container);border-radius:var(--shape-xs);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.fm-breadcrumb::-webkit-scrollbar{display:none} .fm-breadcrumb::-webkit-scrollbar{display:none}
.fm-crumb{white-space:nowrap;padding:4px 10px;border-radius:var(--shape-full);cursor:pointer;font-size:.82rem;color:var(--text-dim);transition:.15s} .fm-crumb{white-space:nowrap;padding:4px 10px;border-radius:var(--shape-full);cursor:pointer;font-size:.82rem;color:var(--text-dim);transition:.15s}
.fm-crumb:hover{background:rgba(208,188,255,.1);color:var(--primary)} .fm-crumb:hover{background:rgba(208,188,255,.1);color:var(--primary)}
+1 -1
View File
@@ -72,6 +72,6 @@
<script src="./static/js/chart.js?v=0603"></script> <script src="./static/js/chart.js?v=0603"></script>
<!-- 🟢 2. 再加载主逻辑 --> <!-- 🟢 2. 再加载主逻辑 -->
<script src="./static/js/app.js?v=0702"></script> <script src="./static/js/app.js?v=0704"></script>
</body> </body>
</html> </html>
+73 -21
View File
@@ -154,8 +154,12 @@ async function loadPluginPage(pluginName) {
var scripts = []; var scripts = [];
clean = clean.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; }); clean = clean.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
// Scope plugin styles: replace body selector with .plugin-page-root to avoid
// styling the main page's <body> element
styles = styles.replace(/body\s*\{/gi, '.plugin-page-root{').replace(/body\b/gi, '.plugin-page-root');
content.style.padding = '0'; content.style.padding = '0';
content.innerHTML = '<style>'+styles+'</style>'+clean; content.innerHTML = '<div class="plugin-page-root"><style>'+styles+'</style>'+clean+'</div>';
scripts.forEach(function(code){ scripts.forEach(function(code){
try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); } try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); }
@@ -172,50 +176,98 @@ async function loadPluginPage(pluginName) {
} }
// ═══ Global file/dir picker (in-page iframe modal) ═══ // ═══ Global file/dir picker (in-page overlay, no iframe) ═══
window.pickPath = function(mode, callback) { window.pickPath = function(mode, callback) {
var id = 'picker_' + Date.now(); var id = 'picker_' + Date.now();
var url = './static/pages/files.html?picker=1&mode=' + (mode || 'dir') + '&cb=' + id;
// Create modal overlay // Create modal overlay
var overlay = document.createElement('div'); var overlay = document.createElement('div');
overlay.id = 'fm-picker-overlay'; overlay.id = 'fm-picker-overlay';
overlay.style.cssText = 'position:fixed;inset:0;z-index:500;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center'; overlay.style.cssText = 'position:fixed;inset:0;z-index:500;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center';
overlay.onclick = function(e) { if (e.target === overlay) closePicker(); };
var box = document.createElement('div'); var box = document.createElement('div');
box.style.cssText = 'background:var(--md-sys-color-surface-container-high);border-radius:var(--shape-lg);width:720px;height:520px;max-width:95vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:var(--md-sys-elevation-5)'; box.style.cssText = 'background:var(--md-sys-color-surface-container-high);border-radius:var(--shape-lg);width:720px;height:520px;max-width:95vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:var(--md-sys-elevation-5)';
// Header // Header with close button
var header = document.createElement('div'); var header = document.createElement('div');
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--outline);flex-shrink:0'; header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--outline);flex-shrink:0';
header.innerHTML = '<span style="font-weight:500;color:var(--primary)">📂 选择' + (mode==='file'?'文件':'目录') + '</span>' + var headerTitle = document.createElement('span');
'<button class="btn btn-sm btn-outlined" onclick="closePicker()" style="font-size:16px;line-height:1">✕</button>'; headerTitle.style.cssText = 'font-weight:500;color:var(--primary)';
headerTitle.textContent = '📂 选择' + (mode==='file'?'文件':'目录');
header.appendChild(headerTitle);
var closeBtn = document.createElement('span');
closeBtn.style.cssText = 'cursor:pointer;font-size:20px;line-height:1;padding:4px 8px;border-radius:4px;color:var(--text-dim)';
closeBtn.textContent = '✕';
closeBtn.onclick = function(){ closePicker(); };
header.appendChild(closeBtn);
box.appendChild(header); box.appendChild(header);
// Iframe // Content area — load file manager via fetch
var iframe = document.createElement('iframe'); var contentArea = document.createElement('div');
iframe.src = url; contentArea.style.cssText = 'flex:1;overflow:hidden;display:flex;flex-direction:column';
iframe.style.cssText = 'flex:1;border:none;width:100%'; box.appendChild(contentArea);
box.appendChild(iframe);
overlay.appendChild(box); overlay.appendChild(box);
document.body.appendChild(overlay); document.body.appendChild(overlay);
// Close on backdrop click
overlay.addEventListener('click', function(e){ if(e.target===overlay) closePicker(); });
function closePicker() { function closePicker() {
window.removeEventListener('message', handler);
if(overlay.parentNode) overlay.parentNode.removeChild(overlay); if(overlay.parentNode) overlay.parentNode.removeChild(overlay);
} }
window._closePicker = closePicker;
// Listen for pick result from iframe // Load file manager HTML into content area
window.addEventListener('message', function handler(e) { fetch('./static/pages/files.html?t=' + Date.now())
try { .then(function(r){ return r.text(); })
var d = JSON.parse(e.data); .then(function(html){
if (d.action === 'fm-picked' && d.callback_id === id) { // Strip meta and CSS link (already available in main page)
html = html.replace(/<meta[^>]*>/gi, '').replace(/<link[^>]*>/gi, '');
// Extract inline scripts
var scripts = [];
var clean = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
contentArea.innerHTML = clean;
// Execute scripts
scripts.forEach(function(code){
try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); }
catch(ex){}
});
// Load files.js if not already loaded
// Fix container height for overlay
var fmContainer = contentArea.querySelector('.fm-container');
if(fmContainer) fmContainer.style.height = '100%';
function doInit(){
window.FilesModule._apiBase = '';
window.FilesModule.currentPath = '';
window.FilesModule.pickerMode = mode || 'dir';
window.FilesModule.pickerCallback = id;
window.FilesModule._scope = contentArea;
window.FilesModule._getEl = function(sel){ return contentArea.querySelector(sel); };
// Override _pickResult to close overlay instead of alert
window.FilesModule._pickResult = function(path){
closePicker(); closePicker();
if (d.path && callback) callback(d.path); if(callback) callback(path || window.FilesModule.currentPath || '/');
};
if(window.FilesModule.destroy) window.FilesModule.destroy();
if(window.FilesModule.init) window.FilesModule.init();
} }
} catch(ex) {} // Override picker actions to close overlay and call callback
window._fmPick = function(){
var p = window.FilesModule && window.FilesModule.currentPath || '/';
closePicker();
if(callback) callback(p);
};
window._fmCancel = function(){ closePicker(); };
if(window.FilesModule){ doInit(); }
else {
var s=document.createElement('script');
s.src='./static/pages/files.js?v=0603';
s.onload=doInit;
document.head.appendChild(s);
}
})
.catch(function(e){
contentArea.innerHTML = '<div style="color:var(--error);padding:40px;text-align:center">加载失败: '+e.message+'</div>';
}); });
}; };
@@ -1,21 +0,0 @@
framework:
debug: true
name: SenSu
version: Alpha_0.2.0
logging:
debug_level_file: true
level: INFO
max_file_size: 10MB
max_log_files: 20
plugins:
auto_load: true
hot_reload: true
max_retry_count: 3
services:
internet:
api_port: 8000
enable_reverse_proxy: false
ws_port: 8765
tui:
layout:
grid-rows: 4fr 5fr 1fr
@@ -1,12 +0,0 @@
admin_permissions:
- framework.*
- plugin.*
- service.*
default_permissions:
- framework.status.read
- plugin.self.info.read
permission_levels:
- read
- write
- execute
- admin
@@ -1 +0,0 @@
{}
@@ -1 +0,0 @@
{}
@@ -1,89 +0,0 @@
commands:
autoscroll:
description: '滚动控制: 切换自动滚动'
permissions:
- framework.tui.control
source: internal
create-plugin:
description: 创建新插件脚手架
permissions:
- framework.scaffold.plugin
source: internal
help:
description: 显示帮助信息
permissions:
- framework.command.help.read
source: internal
history:
description: 显示命令历史
permissions:
- framework.command.history.read
source: internal
netdiag:
description: 网络服务诊断
permissions:
- framework.network.diagnose
source: internal
permissions:
description: '权限管理: 显示权限状态'
permissions:
- framework.permission.read
source: internal
pm_plugin_status:
description: '权限管理: 查看插件权限状态'
permissions:
- framework.permission.read
source: internal
pmallow:
description: '权限管理: 同意权限请求'
permissions:
- framework.permission.read
source: internal
pmdeny:
description: '权限管理: 拒绝权限请求'
permissions:
- framework.permission.read
source: internal
pmhelp:
description: '权限管理: 显示权限命令帮助'
permissions:
- framework.permission.read
source: internal
pmignore:
description: '权限管理: 暂时忽略权限请求'
permissions:
- framework.permission.read
source: internal
pmpending:
description: '权限管理: 查看待授权请求列表'
permissions:
- framework.permission.read
source: internal
pmrequests:
description: '权限管理: 查看待授权请求列表(别名)'
permissions:
- framework.permission.read
source: internal
pmtest:
description: '权限管理: 测试权限配置文件'
permissions:
- framework.permission.read
source: internal
scroll:
description: '滚动控制: 手动滚动到底部'
permissions:
- framework.tui.control
source: internal
status:
description: 显示框架状态
permissions:
- framework.status.read
source: internal
testlog:
description: 生成测试日志
permissions:
- framework.command.test
source: internal
last_updated: 283025.170846772
plugin_commands: {}
total_commands: 17
+27 -23
View File
@@ -4,10 +4,7 @@
<div class="fm-container"> <div class="fm-container">
<!-- Top bar: editable path + actions --> <!-- Top bar: editable path + actions -->
<div class="fm-toolbar"> <div class="fm-toolbar">
<div class="fm-path-bar">
<input class="fm-path-input" id="fm-path-input" type="text" spellcheck="false" title="输入路径后回车跳转"> <input class="fm-path-input" id="fm-path-input" type="text" spellcheck="false" title="输入路径后回车跳转">
<div class="fm-breadcrumb" id="fm-breadcrumb"></div>
</div>
<div class="fm-actions"> <div class="fm-actions">
<label class="fm-toggle"> <label class="fm-toggle">
<input type="checkbox" id="fm-hidden" onchange="FilesModule.refresh()"> <input type="checkbox" id="fm-hidden" onchange="FilesModule.refresh()">
@@ -29,6 +26,9 @@
</div> </div>
</div> </div>
<!-- Breadcrumb row (own line) -->
<div class="fm-breadcrumb" id="fm-breadcrumb"></div>
<!-- File list --> <!-- File list -->
<div class="fm-list" id="fm-list"> <div class="fm-list" id="fm-list">
<div style="text-align:center;color:var(--text-dim);padding:40px">加载中...</div> <div style="text-align:center;color:var(--text-dim);padding:40px">加载中...</div>
@@ -59,36 +59,40 @@
<!-- Context menu --> <!-- Context menu -->
<div class="fm-context" id="fm-context" style="display:none"> <div class="fm-context" id="fm-context" style="display:none">
<div class="fm-context-item" onclick="FilesModule.ctxDownload()"> 下载</div> <div class="fm-context-item" onclick="FilesModule.ctxDownload()"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg> 下载</div>
<div class="fm-context-item" onclick="FilesModule.ctxRename()"> 重命名</div> <div class="fm-context-item" onclick="FilesModule.ctxRename()"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg> 重命名</div>
<div class="fm-context-item" onclick="FilesModule.ctxEdit()">📝 编辑</div> <div class="fm-context-item" onclick="FilesModule.ctxEdit()"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM8 14h8v2H8v-2zm0-4h8v2H8v-2z"/></svg> 编辑</div>
<div class="fm-context-sep"></div> <div class="fm-context-sep"></div>
<div class="fm-context-item danger" onclick="FilesModule.ctxDelete()">🗑 删除</div> <div class="fm-context-item danger" onclick="FilesModule.ctxDelete()"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg> 删除</div>
</div> </div>
</div> </div>
<script> <script>
// Load files.js for standalone mode (iframe picker) // Load files.js for standalone mode (iframe picker)
(function(){ if(window.parent !== window && !window.FilesModule){
// Sync theme from parent window
try {
var parentTheme = window.parent.document.documentElement.getAttribute('data-theme');
if(parentTheme) document.documentElement.setAttribute('data-theme', parentTheme);
} catch(e){}
// Reset body/container for iframe — no topbar, no sidebar // Reset body/container for iframe — no topbar, no sidebar
if(window.parent !== window){ var fmStyle=document.createElement('style');
var s=document.createElement('style'); fmStyle.textContent='html,body{height:100%;margin:0;overflow:hidden}.fm-container{height:100%;gap:8px}.fm-list{padding-bottom:0}';
s.textContent='html,body{height:100%;margin:0;overflow:hidden}.fm-container{height:100%;gap:8px}.fm-list{padding-bottom:0}'; document.head.appendChild(fmStyle);
document.head.appendChild(s);
} // Derive paths from our own URL: /SenSu/static/pages/files.html → /SenSu
var cssHref = document.querySelector('link[href*=\"style.css\"]')?.href || ''; var apiBase = location.pathname.replace(/\/static\/pages\/files\.html.*$/, '');
// e.g. http://host:4200/SenSu/static/css/style.css → staticBase = .../SenSu/static
var staticBase = cssHref.replace(/\/css\/style\.css.*$/, '');
// apiBase = .../SenSu (strip /static)
var apiBase = cssHref.replace(/\/static\/css\/style\.css.*$/, '');
var script = document.createElement('script'); var script = document.createElement('script');
script.src = staticBase + '/pages/files.js?v=0602'; script.src = apiBase + '/static/pages/files.js?v=0603';
script.onload = function(){ script.onload = function(){
if(window.FilesModule){ if(!window.FilesModule) return;
window.FilesModule._apiBase = apiBase; window.FilesModule._apiBase = apiBase;
if(window.FilesModule.init) window.FilesModule.init(); window.FilesModule.init();
} };
script.onerror = function(){
document.body.innerHTML = '<div style=\"color:red;padding:20px\">Failed to load files.js</div>';
}; };
document.head.appendChild(script); document.head.appendChild(script);
})(); }
</script> </script>
+79 -49
View File
@@ -27,7 +27,7 @@ window.FilesModule = {
}); });
// Path input: Enter to jump // Path input: Enter to jump
var input = document.getElementById('fm-path-input'); var input = FilesModule._$('fm-path-input');
input.onkeydown = function(e) { input.onkeydown = function(e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
var p = input.value.trim(); var p = input.value.trim();
@@ -40,6 +40,8 @@ window.FilesModule = {
/* ── API helpers ── */ /* ── API helpers ── */
_apiBase: '', _apiBase: '',
_scope: null,
_$: function(id){ return this._scope ? this._scope.querySelector('#'+id) : document.getElementById(id); },
_apiUrl: function(path) { _apiUrl: function(path) {
// Use explicit base if set (standalone/iframe mode), otherwise relative // Use explicit base if set (standalone/iframe mode), otherwise relative
if (this._apiBase) return this._apiBase + '/api/files' + path; if (this._apiBase) return this._apiBase + '/api/files' + path;
@@ -77,11 +79,11 @@ window.FilesModule = {
}, },
_pathError: function(msg) { _pathError: function(msg) {
var list = document.getElementById('fm-list'); var list = FilesModule._$('fm-list');
list.innerHTML = '<div style="text-align:center;color:var(--error);padding:40px">' + list.innerHTML = '<div style="text-align:center;color:var(--error);padding:40px">' +
'⚠ ' + this._esc(msg) + '</div>'; '⚠ ' + this._esc(msg) + '</div>';
// Still update the input and breadcrumb to show what was attempted // Still update the input and breadcrumb to show what was attempted
document.getElementById('fm-breadcrumb').innerHTML = FilesModule._$('fm-breadcrumb').innerHTML =
'<span class="fm-crumb" style="color:var(--error)">' + this._esc(msg) + '</span>'; '<span class="fm-crumb" style="color:var(--error)">' + this._esc(msg) + '</span>';
}, },
@@ -92,12 +94,12 @@ window.FilesModule = {
refresh: function() { refresh: function() {
var self = this; var self = this;
var showHidden = document.getElementById('fm-hidden')?.checked ? '1' : '0'; var showHidden = FilesModule._$('fm-hidden')?.checked ? '1' : '0';
self._get('/list?path=' + encodeURIComponent(self.currentPath) + '&show_hidden=' + showHidden) self._get('/list?path=' + encodeURIComponent(self.currentPath) + '&show_hidden=' + showHidden)
.then(function(d) { .then(function(d) {
self._render(d); self._render(d);
}).catch(function(e) { }).catch(function(e) {
document.getElementById('fm-list').innerHTML = FilesModule._$('fm-list').innerHTML =
'<div style="text-align:center;color:var(--error);padding:40px">加载失败: ' + self._esc(e.message) + '</div>'; '<div style="text-align:center;color:var(--error);padding:40px">加载失败: ' + self._esc(e.message) + '</div>';
}); });
}, },
@@ -105,13 +107,14 @@ window.FilesModule = {
/* ── Render ── */ /* ── Render ── */
_render: function(d) { _render: function(d) {
var self = this; var self = this;
self.currentPath = d.current || '';
// Update editable path input // Update editable path input
var input = document.getElementById('fm-path-input'); var input = FilesModule._$('fm-path-input');
input.value = d.current || ''; input.value = self.currentPath;
// Breadcrumbs (logical path) // Breadcrumbs (logical path)
var bc = document.getElementById('fm-breadcrumb'); var bc = FilesModule._$('fm-breadcrumb');
var html = self._buildBreadcrumbHTML(d.breadcrumbs); var html = self._buildBreadcrumbHTML(d.breadcrumbs);
// Resolved breadcrumbs — show as second row when symlink redirect // Resolved breadcrumbs — show as second row when symlink redirect
if (d.resolved_breadcrumbs) { if (d.resolved_breadcrumbs) {
@@ -125,13 +128,13 @@ window.FilesModule = {
}); });
// File list // File list
var list = document.getElementById('fm-list'); var list = FilesModule._$('fm-list');
var rows = ''; var rows = '';
// ".." row for parent (unless at filesystem root) // ".." row for parent (unless at filesystem root)
if (d.parent && d.parent !== d.current) { if (d.parent && d.parent !== d.current) {
rows += '<div class="fm-row fm-dir fm-parent" data-path="' + self._escAttr(d.parent) + '" data-is-dir="1">' + rows += '<div class="fm-row fm-dir fm-parent" data-path="' + self._escAttr(d.parent) + '" data-is-dir="1">' +
'<span class="fm-icon">📂</span>' + '<span class="fm-icon"><svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-1 4H5V8h14v2z"/></svg></span>' +
'<span class="fm-name" style="font-weight:600">..</span>' + '<span class="fm-name" style="font-weight:600">..</span>' +
'<span class="fm-size"></span>' + '<span class="fm-size"></span>' +
'<span class="fm-date"></span>' + '<span class="fm-date"></span>' +
@@ -143,13 +146,13 @@ window.FilesModule = {
if (item.broken) { if (item.broken) {
// Broken symlink or unreadable entry — show as disabled // Broken symlink or unreadable entry — show as disabled
rows += '<div class="fm-row fm-broken" data-path="">' + rows += '<div class="fm-row fm-broken" data-path="">' +
'<span class="fm-icon"></span>' + '<span class="fm-icon"><svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--error)" d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg></span>' +
'<span class="fm-name" style="color:var(--error)" title="损坏的链接或无法访问">' + self._esc(item.name) + '</span>' + '<span class="fm-name" style="color:var(--error)" title="损坏的链接或无法访问">' + self._esc(item.name) + '</span>' +
'<span class="fm-size">—</span>' + '<span class="fm-size">—</span>' +
'<span class="fm-date">—</span></div>'; '<span class="fm-date">—</span></div>';
return; return;
} }
var icon = item.is_dir ? '📁' : FilesModule._fileIcon(item.ext); var icon = item.is_dir ? FilesModule._dirIcon() : FilesModule._fileIcon(item.ext);
var sizeStr = item.is_dir ? '—' : FilesModule._fmtSize(item.size); var sizeStr = item.is_dir ? '—' : FilesModule._fmtSize(item.size);
rows += '<div class="fm-row' + (item.is_dir ? ' fm-dir' : '') + rows += '<div class="fm-row' + (item.is_dir ? ' fm-dir' : '') +
'" data-path="' + self._escAttr(item.path) + '" data-path="' + self._escAttr(item.path) +
@@ -190,19 +193,19 @@ window.FilesModule = {
// Close context on outside click // Close context on outside click
document.onclick = function() { document.onclick = function() {
document.getElementById('fm-context').style.display = 'none'; FilesModule._$('fm-context').style.display = 'none';
}; };
// Picker banner — at bottom // Picker banner — at bottom
if (self.pickerMode) { if (self.pickerMode) {
var banner = document.getElementById('fm-picker-banner'); var banner = FilesModule._$('fm-picker-banner');
if (!banner) { if (!banner) {
banner = document.createElement('div'); banner = document.createElement('div');
banner.id = 'fm-picker-banner'; banner.id = 'fm-picker-banner';
banner.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--primary-container);color:var(--md-sys-color-on-primary-container);border-radius:var(--shape-xs);font-size:.82rem">' + banner.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--primary-container);color:var(--md-sys-color-on-primary-container);border-radius:var(--shape-xs);font-size:.82rem">' +
'<span>📂 选择' + (self.pickerMode === 'file' ? '文件' : '目录') + '模式</span>' + '<span><svg width="16" height="16" viewBox="0 0 24 24" style="vertical-align:-3px"><path fill="currentColor" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z"/></svg> 选择' + (self.pickerMode === 'file' ? '文件' : '目录') + '模式</span>' +
'<button class="btn btn-sm btn-filled" onclick="FilesModule._pickResult(FilesModule.currentPath)" style="margin-left:auto">选择当前目录</button>' + '<button type="button" style="margin-left:auto;padding:6px 18px;border:none;border-radius:20px;background:var(--primary);color:var(--on-primary);font-weight:500;cursor:pointer;font-size:.82rem" onclick="_fmPick()">选择当前目录</button>' +
'<button class="btn btn-sm btn-outlined" onclick="FilesModule._cancelPicker()">取消</button>' + '<button type="button" style="padding:6px 18px;border:1px solid;border-radius:20px;background:transparent;color:var(--md-sys-color-on-primary-container);font-weight:500;cursor:pointer;font-size:.82rem" onclick="_fmCancel()">取消</button>' +
'</div>'; '</div>';
list.parentNode.appendChild(banner); list.parentNode.appendChild(banner);
list.style.flex = ''; list.style.flex = '';
@@ -212,7 +215,7 @@ window.FilesModule = {
banner.style.display = 'block'; banner.style.display = 'block';
} }
} else { } else {
var oldBanner = document.getElementById('fm-picker-banner'); var oldBanner = FilesModule._$('fm-picker-banner');
if (oldBanner) oldBanner.style.display = 'none'; if (oldBanner) oldBanner.style.display = 'none';
} }
}, },
@@ -221,7 +224,9 @@ window.FilesModule = {
_pickResult: function(path) { _pickResult: function(path) {
// In iframe (picker mode): post to parent, parent closes overlay // In iframe (picker mode): post to parent, parent closes overlay
if (window.parent !== window) { if (window.parent !== window) {
window.parent.postMessage(JSON.stringify({action:'fm-picked', path: path, callback_id: this.pickerCallback}), '*'); // Ensure path is never empty — use current path as fallback
var p = path || this.currentPath || '/';
window.parent.postMessage(JSON.stringify({action:'fm-picked', path: p, callback_id: this.pickerCallback}), '*');
return; return;
} }
// Standalone clipboard fallback // Standalone clipboard fallback
@@ -274,18 +279,18 @@ window.FilesModule = {
var self = this; var self = this;
this._get('/read?path=' + encodeURIComponent(path)) this._get('/read?path=' + encodeURIComponent(path))
.then(function(d) { .then(function(d) {
document.getElementById('fm-editor-title').textContent = '📝 ' + (name || d.name); FilesModule._$('fm-editor-title').innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" style="vertical-align:-3px;margin-right:4px"><path fill="var(--primary)" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM8 14h8v2H8v-2zm0-4h8v2H8v-2z"/></svg>' + (name || d.name);
document.getElementById('fm-editor-textarea').value = d.content; FilesModule._$('fm-editor-textarea').value = d.content;
document.getElementById('fm-editor-textarea').dataset.path = path; FilesModule._$('fm-editor-textarea').dataset.path = path;
document.getElementById('fm-editor').style.display = 'flex'; FilesModule._$('fm-editor').style.display = 'flex';
}).catch(function(e) { alert('无法读取: ' + e.message); }); }).catch(function(e) { alert('无法读取: ' + e.message); });
}, },
closeEditor: function() { closeEditor: function() {
document.getElementById('fm-editor').style.display = 'none'; FilesModule._$('fm-editor').style.display = 'none';
}, },
saveFile: function() { saveFile: function() {
var self = this; var self = this;
var ta = document.getElementById('fm-editor-textarea'); var ta = FilesModule._$('fm-editor-textarea');
this._post('/write', {path: ta.dataset.path, content: ta.value}) this._post('/write', {path: ta.dataset.path, content: ta.value})
.then(function() { self.closeEditor(); self.refresh(); }) .then(function() { self.closeEditor(); self.refresh(); })
.catch(function(e) { alert('保存失败: ' + e.message); }); .catch(function(e) { alert('保存失败: ' + e.message); });
@@ -294,7 +299,7 @@ window.FilesModule = {
/* ── Context menu ── */ /* ── Context menu ── */
_showContext: function(e, item) { _showContext: function(e, item) {
this.contextTarget = item; this.contextTarget = item;
var ctx = document.getElementById('fm-context'); var ctx = FilesModule._$('fm-context');
ctx.style.display = 'block'; ctx.style.display = 'block';
ctx.style.left = e.pageX + 'px'; ctx.style.left = e.pageX + 'px';
ctx.style.top = e.pageY + 'px'; ctx.style.top = e.pageY + 'px';
@@ -304,7 +309,7 @@ window.FilesModule = {
ctxDownload: function() { ctxDownload: function() {
var t = this.contextTarget; var t = this.contextTarget;
if (t) window.open(FilesModule._apiUrl('/download?path=') + encodeURIComponent(t.path), '_blank'); if (t) window.open(FilesModule._apiUrl('/download?path=') + encodeURIComponent(t.path), '_blank');
document.getElementById('fm-context').style.display = 'none'; FilesModule._$('fm-context').style.display = 'none';
}, },
ctxRename: function() { ctxRename: function() {
var self = this; var self = this;
@@ -315,20 +320,20 @@ window.FilesModule = {
this._post('/rename', {path: t.path, new_name: nn}) this._post('/rename', {path: t.path, new_name: nn})
.then(function() { self.refresh(); }) .then(function() { self.refresh(); })
.catch(function(e) { alert('重命名失败: ' + e.message); }); .catch(function(e) { alert('重命名失败: ' + e.message); });
document.getElementById('fm-context').style.display = 'none'; FilesModule._$('fm-context').style.display = 'none';
}, },
ctxEdit: function() { ctxEdit: function() {
var t = this.contextTarget; var t = this.contextTarget;
if (t && !t.isDir) this.openEditor(t.path, t.name); if (t && !t.isDir) this.openEditor(t.path, t.name);
document.getElementById('fm-context').style.display = 'none'; FilesModule._$('fm-context').style.display = 'none';
}, },
ctxDelete: function() { ctxDelete: function() {
var t = this.contextTarget; var t = this.contextTarget;
if (!t) return; if (!t) return;
this.toDelete = t; this.toDelete = t;
document.getElementById('fm-dialog-msg').textContent = '确认删除 "' + t.name + '"?此操作不可撤销。'; FilesModule._$('fm-dialog-msg').textContent = '确认删除 "' + t.name + '"?此操作不可撤销。';
document.getElementById('fm-dialog').style.display = 'flex'; FilesModule._$('fm-dialog').style.display = 'flex';
document.getElementById('fm-context').style.display = 'none'; FilesModule._$('fm-context').style.display = 'none';
}, },
confirmDelete: function() { confirmDelete: function() {
var self = this; var self = this;
@@ -338,7 +343,7 @@ window.FilesModule = {
.catch(function(e) { alert('删除失败: ' + e.message); }); .catch(function(e) { alert('删除失败: ' + e.message); });
}, },
closeDialog: function() { closeDialog: function() {
document.getElementById('fm-dialog').style.display = 'none'; FilesModule._$('fm-dialog').style.display = 'none';
this.toDelete = null; this.toDelete = null;
}, },
@@ -366,24 +371,49 @@ window.FilesModule = {
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB'; if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
return (bytes / 1073741824).toFixed(2) + ' GB'; return (bytes / 1073741824).toFixed(2) + ' GB';
}, },
_dirIcon: function() {
return '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--primary)" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>';
},
_fileIcon: function(ext) { _fileIcon: function(ext) {
var map = { // Group by category → single SVG per category
'.py':'🐍','.js':'📜','.ts':'📘','.html':'🌐','.css':'🎨', var code = '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM8 14h4v-2H8v2zm0 3h2v-2H8v2z"/></svg>';
'.json':'📋','.yaml':'⚙','.yml':'⚙','.md':'📝','.txt':'📄', var img = '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>';
'.log':'📊','.sh':'💻','.bat':'💻','.xml':'📰','.toml':'⚙', var arch = '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-2 10H6v-2h12v2zm0-4H6V8h12v4z"/></svg>';
'.cfg':'⚙','.ini':'⚙','.conf':'⚙','.sql':'🗄','.csv':'📊', var media = '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="var(--text-dim)" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
'.zip':'📦','.tar':'📦','.gz':'📦','.7z':'📦',
'.png':'🖼','.jpg':'🖼','.jpeg':'🖼','.gif':'🖼','.svg':'🖼','.ico':'🖼', var codeExts = ['.py','.js','.ts','.jsx','.tsx','.html','.css','.scss','.less','.json','.yaml','.yml','.xml','.toml','.ini','.cfg','.conf','.sql','.sh','.bat','.c','.h','.cpp','.hpp','.java','.kt','.rs','.go','.rb','.php','.swift','.r','.lua','.pl','.scala','.dart','.Makefile','.gitignore','.dockerfile','.env'];
'.mp3':'🎵','.wav':'🎵','.ogg':'🎵','.mp4':'🎬','.avi':'🎬', var imgExts = ['.png','.jpg','.jpeg','.gif','.svg','.ico','.bmp','.webp','.tiff'];
'.pdf':'📕','.doc':'📃','.docx':'📃','.xls':'📊','.xlsx':'📊', var mediaExts = ['.mp3','.wav','.ogg','.flac','.aac','.mp4','.avi','.mkv','.mov','.webm'];
'.c':'⚡','.h':'⚡','.cpp':'⚡','.java':'☕','.rs':'🦀','.go':'🔵', var archExts = ['.zip','.tar','.gz','.7z','.rar','.bz2','.xz'];
};
return map[ext] || '📄'; if (codeExts.indexOf(ext) >= 0) return code;
if (imgExts.indexOf(ext) >= 0) return img;
if (mediaExts.indexOf(ext) >= 0) return media;
if (archExts.indexOf(ext) >= 0) return arch;
return code; // default: document icon
}, },
destroy: function() { destroy: function() {
document.getElementById('fm-context').style.display = 'none'; FilesModule._$('fm-context').style.display = 'none';
document.getElementById('fm-dialog').style.display = 'none'; FilesModule._$('fm-dialog').style.display = 'none';
document.getElementById('fm-editor').style.display = 'none'; FilesModule._$('fm-editor').style.display = 'none';
}
};
/* ── Global picker functions (used by onclick in banner HTML) ── */
window._fmPick = function() {
var p = window.FilesModule.currentPath || '/';
// Visual feedback to confirm execution
var banner = FilesModule._$('fm-picker-banner');
if(banner) banner.style.background = '#4caf50';
if (window.parent !== window) {
window.parent.postMessage(JSON.stringify({action:'fm-picked', path:p, callback_id:window.FilesModule.pickerCallback}), '*');
}
};
window._fmCancel = function() {
var banner = FilesModule._$('fm-picker-banner');
if(banner) banner.style.background = '#f44336';
if (window.parent !== window) {
window.parent.postMessage(JSON.stringify({action:'fm-picked', path:null, callback_id:window.FilesModule.pickerCallback}), '*');
} }
}; };