Files
CHFS-NG/chfs-ng/internal/api/web/index.html
T

135 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>__CHFS_TITLE__</title>
<style>
:root{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033;background:#f5f7fb}
*{box-sizing:border-box}body{margin:0}.top{height:64px;background:#fff;border-bottom:1px solid #e5e9f2;display:flex;align-items:center;padding:0 22px;gap:18px;position:sticky;top:0;z-index:2}.brand{font-weight:750;font-size:20px}.path{color:#718096;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.spacer{flex:1}
button,input{font:inherit}button{border:0;border-radius:9px;padding:9px 13px;cursor:pointer;background:#2563eb;color:#fff}button.ghost{background:#eef2f7;color:#334155}button.danger{background:#dc2626}
main{max-width:1180px;margin:26px auto;padding:0 18px}.toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:14px}.toolgroup{display:flex;align-items:center;gap:6px}.toolbar .searchbox{min-width:220px;flex:1;display:flex;align-items:center;gap:8px;padding:0 12px;border:1px solid var(--border);border-radius:10px;background:var(--surface);transition:border-color .15s,box-shadow .15s}.toolbar .searchbox:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 12%,transparent)}.toolbar .searchbox .ui-icon{width:16px;height:16px;color:var(--muted)}.toolbar .searchbox input{min-width:0;flex:1;padding:10px 0;border:0;outline:0;background:transparent;color:var(--text)}.toolbar .divider{width:1px;height:26px;background:var(--border);margin:0 2px}.toolbar .primary-action{box-shadow:0 2px 6px rgba(37,99,235,.18)}.toolbar .iconbtn{flex:0 0 auto}
.card{background:#fff;border:1px solid #e4e8f0;border-radius:14px;overflow:hidden;box-shadow:0 4px 18px rgba(20,30,50,.04)}
table{width:100%;border-collapse:separate;border-spacing:0}th,td{text-align:left;padding:12px 15px;border-bottom:1px solid #edf0f5}th{font-size:12px;color:#7b8798;background:#fafbfc;font-weight:650}tr:last-child td{border-bottom:0}.name{display:flex;align-items:center;gap:10px;min-width:0}.name a{color:#172033;text-decoration:none;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meta{color:#8792a5;font-size:13px}.actions{display:flex;gap:7px;justify-content:flex-end;opacity:.55;transition:opacity .15s}.file-row:hover .actions,.file-row.selected .actions{opacity:1}.file-row{transition:background .12s}.file-row:hover{background:var(--surface2)}.file-row.selected{background:color-mix(in srgb,var(--primary) 7%,var(--surface))}.file-row:focus-within{box-shadow:inset 2px 0 0 var(--primary)}.tile:focus-visible,.file-row:focus-within{outline:2px solid color-mix(in srgb,var(--primary) 65%,transparent);outline-offset:-2px}.file-row .file-kind{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;background:var(--surface2);color:var(--muted);flex:0 0 auto}.file-row .file-kind .ui-icon{width:18px;height:18px}.file-row .size-cell,.file-row .time-cell{white-space:nowrap;color:var(--muted);font-size:12px}.file-row .perm-cell{white-space:nowrap}.perm-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--border);margin-right:5px}.perm-dot.read{background:var(--muted)}.perm-dot.write{background:var(--primary)}.perm-dot.delete{background:var(--danger)}.empty{padding:45px;text-align:center;color:#8a94a6}
.modal{position:fixed;inset:0;background:rgba(15,23,42,.5);display:none;align-items:center;justify-content:center;padding:18px;z-index:4;backdrop-filter:blur(4px)}.modal.show{display:flex}.dialog{width:min(480px,100%);max-height:calc(100vh - 36px);overflow:auto;background:var(--surface);border:1px solid var(--border);border-radius:18px;padding:22px;box-shadow:0 24px 80px rgba(0,0,0,.18)}.dialog.preview-dialog{width:min(1000px,100%);padding:0;overflow:hidden}.dialog h2{margin-top:0}.field{display:grid;gap:7px;margin:13px 0}.field input{padding:10px 11px;border:1px solid #d7ddea;border-radius:9px}.row{display:flex;gap:9px;justify-content:flex-end;margin-top:18px}.preview-head{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid var(--border)}.preview-title{font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:auto}.preview-stage{min-height:180px;max-height:calc(100vh - 150px);overflow:auto;background:#0b0f14;padding:24px;display:flex;align-items:center;justify-content:center}.preview-stage img{max-width:100%;max-height:70vh;object-fit:contain}.preview-stage video{width:min(920px,100%);max-height:70vh;border-radius:10px}.preview-stage audio{width:min(720px,100%)}.preview-stage iframe{width:100%;height:70vh;border:0;background:#fff}.preview-text{width:min(920px,100%);max-height:70vh;overflow:auto;text-align:left;white-space:pre-wrap;background:#111821;color:#dce5f2;padding:18px;border-radius:10px;font:13px/1.6 ui-monospace,SFMono-Regular,monospace}.preview-empty{color:#aab5c5;text-align:center}.preview-foot{padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px}
.login{max-width:390px;margin:10vh auto}.notice{padding:12px 14px;background:#eff6ff;border-radius:10px;color:#315b9b;margin-bottom:14px}
@media(max-width:700px){.size-col,.time-col,.perm-col,.size-cell,.time-cell,.perm-cell{display:none}.top{padding:0 13px}.path{max-width:40vw}.actions button{padding:7px 9px}.toolbar{gap:6px;margin-bottom:10px}.toolbar .toolgroup{flex-wrap:wrap}.toolbar .divider{display:none}.toolbar .searchbox{order:3;flex-basis:100%;min-width:0}.toolbar .toolgroup:nth-of-type(2){flex:1}.toolbar .toolgroup:last-child{margin-left:auto}.toolbar button{padding:8px 10px}.file-row td{padding:10px 9px}.bulk{position:sticky;bottom:10px;z-index:3;padding:8px 10px;border:1px solid var(--border);border-radius:11px;background:color-mix(in srgb,var(--surface) 94%,transparent);box-shadow:var(--shadow);backdrop-filter:blur(10px);flex-wrap:wrap}.bulk .count{flex-basis:100%;margin-right:0}}
:root{--bg:#f5f7fb;--surface:#fff;--surface2:#f8fafc;--text:#172033;--muted:#7b8798;--border:#e5e9f2;--primary:#2563eb;--danger:#dc2626;--shadow:0 8px 30px rgba(20,30,50,.08)}
html.dark{--bg:#0f141c;--surface:#171e28;--surface2:#202936;--text:#e7edf7;--muted:#9aa7b8;--border:#2c3746;--primary:#60a5fa;--danger:#f87171;--shadow:0 12px 35px rgba(0,0,0,.3)}
body{background:var(--bg);color:var(--text);transition:background .2s,color .2s}.top,.card,.dialog{background:var(--surface);border-color:var(--border)}
button.ghost{background:var(--surface2);color:var(--text)}button.danger{background:var(--danger)}.meta,.path{color:var(--muted)}
.card{box-shadow:var(--shadow)}th{background:var(--surface2);color:var(--muted)}th,td{border-color:var(--border)}.field input,.toolbar input,.field select{background:var(--surface);color:var(--text);border-color:var(--border)}
.iconbtn{width:38px;height:38px;padding:0;background:var(--surface2);color:var(--text);display:inline-flex;align-items:center;justify-content:center}.ui-icon{width:18px;height:18px;display:block;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.ui-icon.fill{fill:currentColor;stroke:none}.viewbtn.active{background:var(--primary);color:#fff}.selectbox{width:18px;height:18px;accent-color:var(--primary)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:14px;padding:16px}.tile{position:relative;border:1px solid var(--border);border-radius:16px;padding:12px;background:var(--surface);min-height:192px;cursor:pointer;transition:border-color .16s,box-shadow .16s,transform .16s}.tile:hover{border-color:var(--primary);box-shadow:0 10px 28px rgba(20,30,50,.08);transform:translateY(-1px)}.tile.selected{border-color:var(--primary);box-shadow:0 0 0 2px rgba(37,99,235,.14)}.tile .thumb{height:112px;display:flex;align-items:center;justify-content:center;border-radius:11px;background:var(--surface2);overflow:hidden;margin-bottom:11px;color:var(--muted);transition:background .16s}.tile:hover .thumb{background:var(--surface2)}.tile .thumb .ui-icon{width:44px;height:44px;stroke-width:1.35}.tile .thumb img{width:100%;height:100%;object-fit:cover;display:block}.tile .tile-title{display:flex;align-items:center;gap:7px;min-width:0;padding:0 2px}.tile .tile-title .ui-icon{width:16px;height:16px;flex:0 0 auto}.tile .file-name{font-size:14px;font-weight:560;line-height:1.35}.tile .tile-meta{display:flex;gap:8px;padding:6px 2px 0;color:var(--muted);font-size:12px}.tile .tile-check{position:absolute;left:10px;top:10px;opacity:0;transition:opacity .14s}.tile:hover .tile-check,.tile.selected .tile-check{opacity:1}.tile .tile-action{position:absolute;right:9px;top:9px;width:30px;height:30px;padding:0;border:1px solid var(--border);background:var(--surface);color:var(--text);opacity:0;transition:opacity .14s}.tile:hover .tile-action,.tile.selected .tile-action{opacity:1}.tile .tile-action .ui-icon{width:15px;height:15px}.tile.folder-tile .thumb{background:var(--surface2)}.tile.media-tile .thumb{background:#0d1218;color:#d7e0ec}.tile.media-tile .thumb .ui-icon{stroke-width:1.45}.progress{height:8px;background:var(--surface2);border-radius:99px;overflow:hidden}.progress i{display:block;height:100%;width:0;background:var(--primary);transition:width .15s}.upload-row{display:grid;gap:7px;padding:10px 0;border-bottom:1px solid var(--border)}.drop-active{border-color:var(--primary)!important;background:var(--surface2)!important}.qr{width:min(320px,80vw);aspect-ratio:1;margin:auto;display:block;border-radius:12px;background:#fff}.drop-hint{color:var(--muted);font-size:13px;margin-top:7px}.tile .tilecheck{position:absolute;top:9px;left:9px}.tile .tilemenu{position:absolute;right:7px;top:7px}.file-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bulk{display:none;align-items:center;gap:8px;margin-bottom:12px}.bulk.show{display:flex}.bulk .count{font-weight:650;margin-right:auto}.layout{display:grid;grid-template-columns:210px minmax(0,1fr);gap:16px}.sidebar{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:12px;height:max-content;position:sticky;top:82px}.side-title{font-size:12px;font-weight:700;color:var(--muted);padding:6px 9px}.side-item{width:100%;text-align:left;background:transparent;color:var(--text);margin:2px 0}.side-item.active{background:var(--surface2);color:var(--primary)}@media(max-width:900px){.layout{display:block}.sidebar{position:static;margin-bottom:14px;display:flex;gap:6px;overflow:auto}.side-title{display:none}.side-item{white-space:nowrap;width:auto}}.toast-host{position:fixed;right:18px;bottom:18px;z-index:20;display:grid;gap:8px}.toast{min-width:240px;max-width:min(420px,calc(100vw - 36px));padding:11px 14px;border:1px solid var(--border);border-radius:11px;background:var(--surface);box-shadow:var(--shadow);color:var(--text);animation:toastIn .18s ease}@keyframes toastIn{from{transform:translateY(8px);opacity:0}to{transform:none;opacity:1}}.toast.error{border-color:var(--danger)}.context{position:fixed;z-index:10;display:none;min-width:170px;background:var(--surface);border:1px solid var(--border);box-shadow:var(--shadow);border-radius:10px;padding:5px}.context button{display:block;width:100%;text-align:left;background:transparent;color:var(--text);border-radius:7px}.context button:hover{background:var(--surface2)}
</style>
</head>
<body>
<header class="top">
<div class="brand" id="brand">CHFS</div><div class="path crumbbar" id="crumb"></div><div class="spacer"></div>
<span id="who" class="meta"></span><button class="ghost" id="loginBtn" onclick="openLogin()">登录</button>
<button class="ghost" id="logoutBtn" onclick="logout()" hidden>退出</button>
</header>
<main>
<div class="layout"><aside class="sidebar"><div class="side-title">位置</div><div id="sideRoots"><div class="meta" style="padding:8px">加载中…</div></div></aside><section>
<div id="dropZone" class="card" style="display:none;padding:24px;text-align:center;margin-bottom:14px;border:2px dashed var(--border)">将文件或文件夹拖到这里上传</div>
<div id="serverNotice">__CHFS_NOTICE__</div><div id="notice"></div>
<div class="toolbar">
<div class="toolgroup"><button class="ghost iconbtn" onclick="goUp()" title="上一级"><svg class="ui-icon" viewBox="0 0 24 24"><path d="m14 6-6 6 6 6M8 12h12"/></svg></button><button class="ghost iconbtn" onclick="refresh()" title="刷新"><svg class="ui-icon" viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 0 1 4M20 5v6h-6"/></svg></button><button class="ghost" onclick="showRoots()">位置</button></div>
<span class="divider"></span>
<div class="toolgroup"><button id="newFolderBtn" onclick="newFolder()">新建文件夹</button><button id="newTextBtn" class="ghost" onclick="newText()">新建文本</button><label><button id="uploadBtn" class="primary-action">上传</button><input id="upload" type="file" multiple hidden></label><button class="ghost" onclick="showMobileTransfer()">手机传输</button></div>
<div class="searchbox"><svg class="ui-icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="6"/><path d="m16 16 5 5"/></svg><input id="search" placeholder="搜索文件名,回车执行"></div>
<div class="toolgroup"><button id="usersBtn" class="ghost" onclick="showUsers()" hidden>用户管理</button><button class="ghost" id="pwdBtn" onclick="changePassword()" hidden>修改密码</button><button class="iconbtn" id="themeBtn" onclick="toggleTheme()" title="主题"><span id="themeIcon"></span></button><button class="iconbtn viewbtn active" id="listBtn" onclick="setView('list')" title="列表"><span id="listIcon"></span></button><button class="iconbtn viewbtn" id="gridBtn" onclick="setView('grid')" title="网格"><span id="gridIcon"></span></button></div>
</div>
<div id="bulk" class="bulk"><span id="selectedCount" class="count"></span><button class="ghost" onclick="bulkMove()">移动</button><button class="ghost" onclick="bulkCopy()">复制</button><button class="ghost" onclick="bulkDownload()">下载 ZIP</button><button class="danger" onclick="bulkDelete()">删除</button><button class="ghost" onclick="clearSelection()">取消选择</button></div><div class="card" id="fileCard"><table><thead><tr><th class="check-col"><input class="selectbox" type="checkbox" onchange="toggleAll(this.checked)"></th><th>名称</th><th class="size-col">大小</th><th class="time-col">修改时间</th><th class="perm-col">权限</th><th class="action-col"></th></tr></thead><tbody id="files"></tbody></table><div id="grid" class="grid" hidden></div></div></section></div>
</main><div id="context" class="context"></div><div id="toastHost" class="toast-host"></div>
<div class="modal" id="modal"><div class="dialog" id="dialog"></div></div>
<script>
const $=id=>document.getElementById(id);let current="",me=null,currentMask="",navigationRoot="";
async function api(url,opt={}){const r=await fetch(url,opt);if(!r.ok){let t="请求失败";try{t=(await r.json()).error||t}catch{};throw Error(t)}return r.status===204?null:r.json()}
function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]))}
function fmt(n){if(n<1024)return n+" B";if(n<1048576)return (n/1024).toFixed(1)+" KB";if(n<1073741824)return (n/1048576).toFixed(1)+" MB";return (n/1073741824).toFixed(1)+" GB"}
async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"",admin:false}};updateUser();current=me.home||"";navigationRoot=me.home||"";const h=new URLSearchParams(location.hash.slice(1));const shared=h.get("folder");if(shared!==null)current=shared;await refresh()}
function updateUser(){$("who").textContent=me.username?me.username: "访客";$("loginBtn").hidden=!!me.username;$("logoutBtn").hidden=!me.username;$("usersBtn").hidden=!me.admin;$("pwdBtn").hidden=!me.username||!me.managed}
async function refresh(){try{const q=encodeURIComponent(current);const data=await api("/chfs/files?filepath="+q);currentMask=data.currentMask||"";const writable=currentMask.includes("W");$("newFolderBtn").disabled=!writable;$("newTextBtn").disabled=!writable;$("uploadBtn").disabled=!writable;$("search").disabled=!data.currentMask?.includes("R");render(data.files||[]);loadSideRoots();renderBreadcrumb();$("notice").innerHTML=""}catch(e){$("notice").innerHTML='<div class="notice">'+esc(e.message)+"</div>"}}
function fileType(f){const n=f.name.toLowerCase();if(f.dir)return "folder";if(/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(n))return "image";if(/\.(mp4|webm|mov|mkv)$/.test(n))return "video";if(/\.(mp3|wav|flac|m4a|ogg)$/.test(n))return "audio";if(/\.(pdf)$/.test(n))return "pdf";return "file"}
function svgIcon(type){const p={folder:'<path d="M3 6.5h6l2 2H21v9.5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',image:'<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8" cy="9" r="1.4"/><path d="m4 17 5-5 4 4 2-2 5 5"/>',video:'<rect x="3" y="5" width="13" height="14" rx="2"/><path d="m16 10 5-3v10l-5-3z"/>',audio:'<path d="M9 18V6l9-2v12"/><circle cx="6" cy="18" r="3"/><circle cx="15" cy="16" r="3"/>',pdf:'<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v5h5M8 16h8M8 12h6"/>',archive:'<path d="M7 3h10v18H7z"/><path d="M10 3v3h4V3M10 8h4v3h-4v3h4v3h-4"/>',document:'<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v5h5M9 12h6M9 16h6"/>',code:'<path d="m9 7-5 5 5 5M15 7l5 5-5 5M13 4l-2 16"/>',spreadsheet:'<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M4 8h16M10 8v13M15 8v13M4 13h16M4 17h16"/>',presentation:'<rect x="3" y="4" width="18" height="14" rx="2"/><path d="M12 18v3M8 21h8M7 14l3-4 3 2 4-5"/>',file:'<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v5h5"/>'};return '<svg class="ui-icon" viewBox="0 0 24 24" aria-hidden="true">'+(p[type]||p.file)+'</svg>'}function iconFor(f){const n=f.name.toLowerCase();const t=fileType(f);if(t!=="file")return svgIcon(t);if(/\.(zip|7z|rar|tar|gz|bz2|xz)$/.test(n))return svgIcon("archive");if(/\.(xls|xlsx|ods)$/.test(n))return svgIcon("spreadsheet");if(/\.(ppt|pptx|odp)$/.test(n))return svgIcon("presentation");if(/\.(doc|docx|odt|rtf)$/.test(n))return svgIcon("document");if(/\.(js|ts|jsx|tsx|css|scss|html|htm|go|rs|py|java|c|cpp|h|hpp|sh|sql|json|yaml|yml|xml)$/.test(n))return svgIcon("code");return svgIcon("file")}
function selectItem(p,checked){if(checked)selected.add(p);else selected.delete(p);updateSelection();renderList(lastItems)}
let selected=new Set(),lastItems=[];
function render(items){lastItems=items;selected=new Set([...selected].filter(p=>items.some(f=>(current?current+"/":"")+f.name===p)));renderList(items);renderGrid(items);updateSelection()}
function renderList(items){const body=$("files");body.innerHTML=items.map(f=>{const p=current?current+"/"+f.name:f.name,canRead=(f.mask||"").includes("R"),canWrite=(f.mask||"").includes("W"),canDelete=(f.mask||"").includes("D"),selectedNow=selected.has(p);const perms='<span class="perm-dot '+(canRead?'read':'')+'"></span><span class="perm-dot '+(canWrite?'write':'')+'"></span><span class="perm-dot '+(canDelete?'delete':'')+'"></span>';return '<tr class="file-row '+(selectedNow?'selected':'')+'" onclick="selectItem('+JSON.stringify(p)+',true)" ondblclick="activateItem('+JSON.stringify(p)+')"><td><input class="selectbox" type="checkbox" '+(selectedNow?"checked":"")+' onclick="event.stopPropagation()" onchange="selectItem('+JSON.stringify(p)+',this.checked)"></td><td><div class="name"><span class="file-kind">'+iconFor(f)+'</span><span>'+esc(f.name)+'</span></div></td><td class="size-cell">'+(f.dir?"—":fmt(f.size))+'</td><td class="time-cell">'+esc(f.modified||"")+'</td><td class="perm-cell">'+perms+'</td><td><div class="actions"><button class="iconbtn" onclick="itemMenu(event,'+JSON.stringify(p)+')" title="更多操作"><svg class="ui-icon" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/></svg></button></div></td></tr>'}).join("")||'<tr><td colspan="6" class="empty">这里还没有文件</td></tr>'}
function renderGrid(items){$("grid").innerHTML=items.map(f=>{const p=current?current+"/"+f.name:f.name,canRead=(f.mask||"").includes("R"),type=fileType(f),selectedNow=selected.has(p);let thumb=iconFor(f);if(type==="image"&&canRead)thumb='<img src="/chfs/preview?file='+encodeURIComponent(p)+'" loading="lazy" alt="">';const tileClass=(f.dir?'folder-tile ':'')+(['video','audio'].includes(type)?'media-tile':'');const meta=f.dir?'文件夹':fmt(f.size);return '<div class="tile '+tileClass+(selectedNow?' selected':'')+'" onclick="selectItem('+JSON.stringify(p)+',true)" ondblclick="activateItem('+JSON.stringify(p)+')" oncontextmenu="itemMenu(event,'+JSON.stringify(p)+');return false"><input class="selectbox tile-check" type="checkbox" '+(selectedNow?'checked':'')+' onclick="event.stopPropagation();selectItem('+JSON.stringify(p)+',this.checked)"><button class="iconbtn tile-action" onclick="event.stopPropagation();itemMenu(event,'+JSON.stringify(p)+')" title="更多操作"><svg class="ui-icon" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/></svg></button><div class="thumb">'+thumb+'</div><div class="tile-title">'+iconFor(f)+'<div class="file-name">'+esc(f.name)+'</div></div><div class="tile-meta"><span>'+meta+'</span>'+(f.modified?'<span>'+esc(f.modified)+'</span>':'')+'</div></div>'}).join('')||'<div class="empty">这里还没有文件</div>'}
function activateItem(p){const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f)return;if(f.dir&&(f.mask||"").includes("R"))openDir(p);else if((f.mask||"").includes("R"))previewFile(p)}
function toggleTile(p){selectItem(p,!selected.has(p))}
function openSelected(){if(selected.size!==1)return;const p=[...selected][0],f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f)return;if(f.dir&&(f.mask||"").includes("R"))openDir(p);else if((f.mask||"").includes("R"))previewFile(p)}
function toggleAll(v){lastItems.forEach(f=>{const p=current?current+"/"+f.name:f.name;if(v)selected.add(p);else selected.delete(p)});render(lastItems)}
function clearSelection(){selected.clear();updateSelection();renderGrid(lastItems);renderList(lastItems)}
function updateSelection(){const n=selected.size;$("bulk").classList.toggle("show",n>0);$("selectedCount").textContent=n?`已选择 ${n} 项`:"";$("grid").querySelectorAll('.tile').forEach((el,i)=>{const f=lastItems[i];if(f)el.classList.toggle('selected',selected.has(current?current+'/'+f.name:f.name))})}
function setView(v){localStorage.setItem('chfs-view',v);$("files").parentElement.hidden=v==='grid';$("grid").hidden=v!=='grid';document.querySelectorAll('.viewbtn').forEach(b=>b.classList.remove('active'));$(v+'Btn').classList.add('active')}
function itemMenu(e,p){e.preventDefault();if(!selected.has(p)){selected.clear();selected.add(p);updateSelection();renderList(lastItems);renderGrid(lastItems)}const f=lastItems.find(x=>(current?current+'/':'')+x.name===p);const canRead=(f?.mask||'').includes('R'),canWrite=(f?.mask||'').includes('W'),canDelete=(f?.mask||'').includes('D');let html='';if(canRead)html+='<button onclick="downloadFile('+JSON.stringify(p)+');closeContext()">下载</button>';if(f?.dir&&canRead)html+='<button onclick="downloadDir('+JSON.stringify(p)+');closeContext()">下载 ZIP</button>';if(canWrite)html+='<button onclick="renameItem('+JSON.stringify(p)+');closeContext()">重命名</button><button onclick="transferItem('+JSON.stringify(p)+',false);closeContext()">移动到…</button><button onclick="transferItem('+JSON.stringify(p)+',true);closeContext()">复制到…</button>';if(canDelete)html+='<button onclick="removeItem('+JSON.stringify(p)+');closeContext()">删除</button>';if(f?.guestMask?.includes('R'))html+='<button onclick="shareItem('+JSON.stringify(p)+');closeContext()">分享</button>';if(!html)html='<div class="meta" style="padding:8px">没有可用操作</div>';$("context").innerHTML=html;$("context").style.left=Math.min(e.clientX,innerWidth-190)+'px';$("context").style.top=Math.min(e.clientY,innerHeight-220)+'px';$("context").style.display='block'}
function closeContext(){$("context").style.display='none'}
addEventListener('click',e=>{if(!e.target.closest('#context')&&!e.target.closest('.tilemenu'))closeContext()});
function bulkDownload(){if(!selected.size)return;const q=[...selected].map(p=>'path='+encodeURIComponent(p)).join('&');location.href='/chfs/downloadzip?'+q}
async function chooseDestination(title){return await ask(title,"目标目录(留空表示当前目录)",current)}
async function transferItem(source,copyOnly){const dstDir=await chooseDestination(copyOnly?"复制到":"移动到");if(dstDir===null)return;const name=source.split("/").pop();const base=String(dstDir).replace(/^\/+|\/+$/g,"");const destination=base?(base+"/"+name):name;try{await api(copyOnly?"/chfs/copy":"/chfs/move",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({source,destination})});refresh()}catch(e){show('<h2>操作失败</h2><div class="notice">'+esc(e.message)+'</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>')}}
async function bulkMove(){await bulkTransfer(false)}
async function bulkCopy(){await bulkTransfer(true)}
async function bulkTransfer(copyOnly){if(!selected.size)return;const dstDir=await chooseDestination(copyOnly?"复制选中项目到":"移动选中项目到");if(dstDir===null)return;const base=String(dstDir).replace(/^\/+|\/+$/g,"");const failed=[];for(const source of [...selected]){const name=source.split("/").pop(),destination=base?(base+"/"+name):name;try{await api(copyOnly?"/chfs/copy":"/chfs/move",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({source,destination})})}catch(e){failed.push(name+": "+e.message)}}selected.clear();await refresh();if(failed.length)show('<h2>部分操作失败</h2><div class="notice">'+esc(failed.join("\n"))+'</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>')}
async function bulkDelete(){if(!selected.size)return;if(!await confirmBox("批量删除","确定删除已选择的 "+selected.size+" 项?"))return;for(const p of [...selected]){try{await api('/chfs/rmfiles',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({filepath:p})})}catch(e){fail(p+': '+e.message)}}selected.clear();refresh()}
async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>'<button class="side-item '+(r.path===current?'active':'')+'" onclick="jumpRoot('+JSON.stringify(r.path)+')">'+svgIcon(r.home?'folder':'folder')+' '+esc(r.name)+'</button>').join('')||'<div class="meta" style="padding:8px">没有可进入的位置</div>'}catch{}}
async function showRoots(){try{const d=await api("/chfs/roots");const roots=d.roots||[];let html='<h2>选择位置</h2><div>';for(const r of roots)html+='<button class="ghost" style="display:block;width:100%;text-align:left;margin:8px 0" onclick="jumpRoot('+JSON.stringify(r.path)+')"><b>'+esc(r.name)+'</b><span class="meta"> · '+esc(r.path||"/")+' · '+esc(r.mask)+'</span></button>';if(!roots.length)html+='<div class="empty">没有可进入的共享位置</div>';html+='</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>';show(html)}catch(e){fail(e.message)}}
function jumpRoot(p){current=p;navigationRoot=p;closeModal();refresh()}
function renderBreadcrumb(){const el=$("crumb"),parts=current?current.split("/").filter(Boolean):[];const rootName=navigationRoot?(navigationRoot===me?.home?'我的空间':'当前位置'): '全部文件';let html='<button class="crumb-seg '+(!current||current===navigationRoot?'current':'')+'" onclick="navigateTo('+JSON.stringify(navigationRoot||'')+')">'+esc(rootName)+'</button>';let acc='';for(let i=0;i<parts.length;i++){acc=acc?acc+'/'+parts[i]:parts[i];html+='<span class="crumb-sep">/</span><button class="crumb-seg '+(i===parts.length-1?'current':'')+'" onclick="navigateTo('+JSON.stringify(acc)+')">'+esc(parts[i])+'</button>'}el.innerHTML=html}
function navigateTo(p){if(navigationRoot){if(p!==navigationRoot&&!p.startsWith(navigationRoot+'/'))return}else if(p!==''&&!me?.admin)return;current=p;refresh()}
function openDir(p){current=p;refresh()}function goUp(){if(!current||current===navigationRoot)return;let p=current.split("/");p.pop();let n=p.join("/");if(navigationRoot&&!(n===navigationRoot||n.startsWith(navigationRoot+"/")))n=navigationRoot;current=n;refresh()}
function downloadFile(p){location.href="/chfs/shared/"+encodeURIComponent(p).replace(/%2F/g,"/")}
function downloadDir(p){location.href="/chfs/downloaddir/"+encodeURI(p)}
async function shareItem(p){const url=new URL("/chfs/shared/"+encodeURI(p),location.href).href;try{await navigator.clipboard.writeText(url);toast("分享链接已复制")}catch{show("<h2>分享链接</h2><div class=\"field\"><input id=\"shareUrl\" value=\""+esc(url)+"\" readonly></div><div class=\"row\"><button class=\"ghost\" onclick=\"closeModal()\">关闭</button><button onclick=\"navigator.clipboard.writeText($(\"shareUrl\").value);closeModal()\">复制</button></div>")}}
async function renameItem(p){const name=p.split("/").pop();const n=await ask("重命名","新名称",name);if(!n||n===name)return;const dest=(p.split("/").slice(0,-1).concat(n)).join("/");try{await api("/chfs/rename",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({old:p,new:dest})});refresh()}catch(e){fail(e.message)}}
async function removeItem(p){if(!await confirmBox("确认删除","确定删除 "+p+"?"))return;try{await api("/chfs/rmfiles",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:p})});refresh()}catch(e){fail(e.message)}}
async function newFolder(){const n=await ask("新建文件夹","文件夹名称");if(!n)return;const p=current?current+"/"+n:n;try{await api("/chfs/newdir",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:p})});refresh()}catch(e){fail(e.message)}}
async function newText(){const n=await ask("新建文本","文件名","new.txt");if(!n)return;const text=await ask("新建文本","文本内容");if(text===null)return;try{await api("/chfs/newtext",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:current,title:n,text:text||""})});refresh()}catch(e){fail(e.message)}}
$("upload").addEventListener("change",async e=>{await uploadFiles(e.target.files);e.target.value=""})
async function uploadFiles(files){const list=[...files].map(file=>({file,relativePath:file.webkitRelativePath||file.name}));if(!list.length)return;await uploadEntries(list)}
async function uploadEntries(entries){show('<h2>上传文件</h2><div class="notice">正在上传 '+entries.length+' 个文件;目录结构会按原路径保留。</div><div id="uploadList"></div><div class="row"><button class="ghost" onclick="closeModal()">后台继续</button></div>');const box=$("uploadList");for(const item of entries){const row=document.createElement("div");row.className="upload-row";row.innerHTML='<b>'+esc(item.relativePath)+'</b><span class="meta">'+fmt(item.file.size)+' · 准备上传</span><div class="progress"><i></i></div>';box.appendChild(row);await uploadOne(item.file,row,item.relativePath)}refresh()}
async function collectDroppedEntries(items){const out=[];async function walk(entry,prefix=""){if(entry.isFile){await new Promise(resolve=>entry.file(file=>{out.push({file,relativePath:prefix+file.name});resolve()},()=>resolve()));return}if(entry.isDirectory){const reader=entry.createReader();const all=[];while(true){const batch=await new Promise(resolve=>reader.readEntries(resolve,()=>resolve([])));if(!batch.length)break;all.push(...batch)}for(const child of all)await walk(child,prefix+entry.name+'/')}}for(const item of items){const entry=item.webkitGetAsEntry?.();if(entry)await walk(entry);else{const file=item.getAsFile?.();if(file)out.push({file,relativePath:file.name})}}return out}
function showMobileTransfer(){const u=new URL(location.href);u.hash='folder='+encodeURIComponent(current);show('<h2>手机传输</h2><div class="notice">用手机扫描二维码打开当前文件夹。登录后即可按你的权限上传、下载和管理文件。</div><img class="qr" src="/chfs/qrcode?url='+encodeURIComponent(u.href)+'" alt="QR Code"><div class="field"><input value="'+esc(u.href)+'" readonly></div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>')}
function setupDropZone(){const z=$("dropZone"),main=$("fileCard");let depth=0;[document,main].forEach(el=>{el.addEventListener('dragenter',e=>{if(!e.dataTransfer?.types.includes('Files'))return;e.preventDefault();depth++;z.style.display='block';z.classList.add('drop-active')});el.addEventListener('dragover',e=>{if(e.dataTransfer?.types.includes('Files'))e.preventDefault()});el.addEventListener('dragleave',e=>{if(!e.dataTransfer?.types.includes('Files'))return;depth--;if(depth<=0){depth=0;z.classList.remove('drop-active')}});el.addEventListener('drop',async e=>{if(!e.dataTransfer?.types.includes('Files'))return;e.preventDefault();depth=0;z.classList.remove('drop-active');z.style.display='none';const entries=await collectDroppedEntries(e.dataTransfer.items||[]);if(entries.length)await uploadEntries(entries);else if(e.dataTransfer.files?.length)await uploadFiles(e.dataTransfer.files)})})}
function uploadOne(file,row,relativePath=file.name){return new Promise(resolve=>{const xhr=new XMLHttpRequest(),fd=new FormData();const clean=String(relativePath||file.name).replace(/\\/g,'/').replace(/^\/+|\/+$/g,'');const parts=clean.split('/');const name=parts.pop()||file.name;const folder=parts.length?[current,...parts].filter(Boolean).join('/') : current;fd.append("folder",folder);fd.append("file",file,name);const meta=row.querySelector('.meta'),bar=row.querySelector('i');xhr.open('POST','/chfs/upload');xhr.upload.onprogress=e=>{if(e.lengthComputable){const pct=Math.round(e.loaded/e.total*100);bar.style.width=pct+'%';meta.textContent=fmt(file.size)+' · '+pct+'%'}};xhr.onload=()=>{if(xhr.status>=200&&xhr.status<300){bar.style.width='100%';meta.textContent=fmt(file.size)+' · 完成'}else{meta.textContent='上传失败';bar.style.width='0'}};xhr.onerror=()=>{meta.textContent='网络错误';bar.style.width='0'};xhr.onloadend=resolve;xhr.send(fd)})}
$("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const s=e.target.value.trim();if(!s){refresh();return}try{const d=await api("/chfs/search?str="+encodeURIComponent(s));renderSearch(d.files||[])}catch(x){fail(x.message)}})
function renderSearch(items){$("crumb").textContent="搜索结果";$("files").parentElement.hidden=false;$("grid").hidden=true;const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=f.path,canRead=true;return '<tr><td><input class="selectbox" type="checkbox" disabled></td><td><div class="name">'+iconFor(f)+' <a href="#" onclick="openSearchResult('+JSON.stringify(f)+');return false">'+esc(f.path)+'</a></div></td><td class="meta">'+(f.dir?"—":fmt(f.size))+'</td><td class="meta">'+esc(f.modified||"")+'</td><td></td></tr>'}).join(""): '<tr><td colspan="5" class="empty">没有找到匹配文件</td></tr>'}
function openSearchResult(f){if(f.dir){current=f.path;navigationRoot=me.home||"";refresh()}else previewFile(f.path)}
async function previewFile(p){const n=p.split('/').pop(),ext=n.toLowerCase().split('.').pop(),url='/chfs/preview?file='+encodeURIComponent(p),media=['mp4','webm','mov','mkv'].includes(ext),audio=['mp3','wav','flac','m4a','ogg'].includes(ext),image=['png','jpg','jpeg','gif','webp','bmp','svg'].includes(ext),pdf=ext==='pdf',text=['txt','md','markdown','json','yaml','yml','xml','csv','log','js','ts','jsx','tsx','css','scss','html','htm','go','rs','py','java','c','cpp','h','hpp','sh','sql','toml','ini','conf'].includes(ext);let body='';if(image)body='<img src="'+url+'" alt="'+esc(n)+'">';else if(media)body='<video controls autoplay><source src="'+url+'"></video>';else if(audio)body='<audio controls autoplay><source src="'+url+'"></audio>';else if(pdf)body='<iframe src="'+url+'"></iframe>';else if(text){try{const r=await fetch(url);if(!r.ok)throw Error();body='<pre class="preview-text">'+esc(await r.text())+'</pre>'}catch{body='<div class="preview-empty">无法读取文本内容</div>'}}else body='<div class="preview-empty">暂不支持该文件类型的在线预览</div>';show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span><button class="iconbtn" onclick="location.href='+JSON.stringify('/chfs/shared/'+encodeURI(p))+'" title="下载">'+svgIcon('file')+'</button><button class="iconbtn" onclick="closeModal()" title="关闭"><svg class="ui-icon" viewBox="0 0 24 24"><path d="m6 6 12 12M18 6 6 18"/></svg></button></div><div class="preview-stage">'+body+'</div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true)}
function changePassword(){show('<h2>修改密码</h2><div class="field"><input id="cp" type="password" placeholder="当前密码"><input id="npw" type="password" placeholder="新密码"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="savePassword()">保存</button></div>')}
async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});toast("密码已更新");closeModal()}catch(e){fail(e.message)}}
function openLogin(){show('<h2>登录</h2><div class="field"><label>用户名</label><input id="u" autocomplete="username"></div><div class="field"><label>密码</label><input id="p" type="password" autocomplete="current-password"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="login()">登录</button></div>')}
async function login(){try{await fetch("/chfs/session",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({user:$("u").value,pwd:$("p").value})}).then(async r=>{if(!r.ok)throw Error((await r.json()).error||"登录失败")});closeModal();await init()}catch(e){fail(e.message)}}
async function logout(){await fetch("/chfs/session",{method:"DELETE"});location.reload()}
function toast(message,error=false){const host=$("toastHost"),el=document.createElement("div");el.className='toast'+(error?' error':'');el.textContent=message;host.appendChild(el);setTimeout(()=>el.remove(),2800)}
function fail(message){toast(message,true)}
function keyboardNavigation(e){const tag=(e.target?.tagName||"").toLowerCase();if(tag==="input"||tag==="textarea"||tag==="select"||tag==="button"||tag==="a"||e.target?.isContentEditable)return;if(e.key==="Escape"){if($("modal").classList.contains("show"))closeModal();else closeContext();return}if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="a"){e.preventDefault();toggleAll(true);return}if(e.key==="Enter"){e.preventDefault();openSelected();return}if(e.key==="Delete"&&selected.size){e.preventDefault();bulkDelete();return}if(e.key==="Backspace"&&current){e.preventDefault();goUp()}}
addEventListener("keydown",keyboardNavigation)
function show(html,preview=false){$("dialog").innerHTML=html;$("dialog").classList.toggle("preview-dialog",preview);$("modal").classList.add("show")}function closeModal(){$("modal").classList.remove("show");$("dialog").classList.remove("preview-dialog")}
function ask(title,label,value="",type="text"){return new Promise(resolve=>{show('<h2>'+esc(title)+'</h2><div class="field"><label>'+esc(label)+'</label><input id="askInput" type="'+type+'" value="'+esc(value)+'"></div><div class="row"><button class="ghost" onclick="closeModal();window._askResolve(null)">取消</button><button onclick="window._askResolve($("askInput").value)">确定</button></div>');window._askResolve=v=>{closeModal();resolve(v)}})}
function confirmBox(title,message){return new Promise(resolve=>{show('<h2>'+esc(title)+'</h2><div class="notice">'+esc(message)+'</div><div class="row"><button class="ghost" onclick="closeModal();window._confirmResolve(false)">取消</button><button class="danger" onclick="closeModal();window._confirmResolve(true)">确定</button></div>');window._confirmResolve=v=>resolve(v)})}
$("modal").addEventListener("click",e=>{if(e.target.id==="modal")closeModal()})
async function showUsers(){try{const d=await api("/chfs/users");let html='<h2>用户管理</h2><div class="field"><input id="nu" placeholder="用户名"><input id="np" type="password" placeholder="初始密码"><button onclick="createUser()">创建用户</button></div><div>';for(const u of d.users)html+='<div style="padding:10px 0;border-bottom:1px solid #edf0f5"><b>'+esc(u.username)+'</b> · '+(u.enabled?"启用":"已禁用")+' · '+esc(u.home||"")+(u.managed&&!u.admin?' <button class="ghost" onclick="editUser('+JSON.stringify(u.username)+')">权限/状态</button> <button class="ghost" onclick="resetUser('+JSON.stringify(u.username)+')">重置密码</button> <button class="danger" onclick="deleteUser('+JSON.stringify(u.username)+')">删除</button>':"")+"</div>";html+='</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>';show(html)}catch(e){fail(e.message)}}
async function createUser(){try{await api("/chfs/users",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:$("nu").value,password:$("np").value})});showUsers()}catch(e){fail(e.message)}}
async function resetUser(u){const p=await ask("重置密码","为 "+u+" 设置新密码","","password");if(!p)return;try{await api("/chfs/users",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:p})});toast("密码已更新")}catch(e){fail(e.message)}}
async function editUser(name){const d=await api("/chfs/users");const u=d.users.find(x=>x.username===name);if(!u)return;window._editUser=u;window._editRules=[...(u.rules||[])];show('<h2>'+esc(u.username)+'</h2><div class="notice">主目录:'+esc(u.home)+'<br>主目录始终保留 RWD;下面的“共享目录”用于给用户开放其他位置,也可以单独设置拒绝规则。</div><label><input id="ue" type="checkbox" '+(u.enabled?"checked":"")+'> 账号启用</label><h3>共享目录</h3><div id="sharedRules"></div><div class="field"><input id="rp" placeholder="目录,例如 public 或 shared/team"><div class="row" style="justify-content:flex-start;margin-top:0"><button class="ghost" onclick="setRulePreset(1)">只读</button><button class="ghost" onclick="setRulePreset(2)">读写</button><button class="ghost" onclick="setRulePreset(3)">完全控制</button><button class="ghost" onclick="setRulePreset(0)">禁止访问</button></div><label><input id="rc" type="checkbox"> 仅作用于目录内部,不改变目录本身</label><button onclick="addRule()">添加共享目录</button></div><details style="margin-top:14px"><summary>高级规则</summary><div class="meta" style="margin:8px 0">可对已授权目录增加更细的拒绝规则,例如禁止访问 public/private。</div><div id="rules"></div></details><div class="row"><button class="ghost" onclick="showUsers()">取消</button><button onclick="saveUser('+JSON.stringify(u.username)+')">保存</button></div>');renderEditRules()}
function isHomeRule(r){return String(r.dir||"").replace(/^\/+|\/+$/g,"")===String(window._editUser.home||"").replace(/^\/+|\/+$/g,"")}
function setRulePreset(level){const mask=["","r","rw","rwd"][level];window._rulePreset={mask,deny:level===0};$("rp").focus();$("rp").dataset.mask=mask;$("rp").dataset.deny=level===0?"1":"0"}
function addRule(){const dir=$("rp").value.trim().replace(/^\/+|\/+$/g,"");if(!dir)return;const preset=$("rp").dataset.mask||"r";const rule={dir,mask:preset};if($("rp").dataset.deny==="1")rule.deny=true;if($("rc").checked)rule.childrenOnly=true;window._editRules=window._editRules.filter(r=>!(String(r.dir||"").replace(/^\/+|\/+$/g,"")===dir&&!isHomeRule(r)));window._editRules.push(rule);$("rp").value="";delete $("rp").dataset.mask;delete $("rp").dataset.deny;$("rc").checked=false;renderEditRules()}
function removeRule(i){if(i<0||isHomeRule(window._editRules[i]))return;window._editRules.splice(i,1);renderEditRules()}
function renderEditRules(){const shared=window._editRules.filter(r=>!isHomeRule(r));$("sharedRules").innerHTML=shared.length?shared.map(r=>{const i=window._editRules.indexOf(r);const flags=(r.deny?"禁止":"允许")+" · "+(r.mask||"无权限")+(r.childrenOnly?" · 仅子项":"");return '<div class="row" style="justify-content:space-between;border-top:1px solid #edf0f5;padding:9px 0;margin:0"><span><b>'+esc(r.dir)+'</b><br><span class="meta">'+esc(flags)+'</span></span><button class="danger" onclick="removeRule('+i+')">移除</button></div>'}).join(""):'<div class="empty" style="padding:18px">还没有共享目录权限</div>';$("rules").innerHTML=window._editRules.map((r,i)=>isHomeRule(r)?'':'<div class="row" style="justify-content:space-between;border-top:1px solid #edf0f5;padding:8px 0"><span><b>'+esc(r.dir)+'</b> · '+esc((r.deny?"拒绝":"允许")+" · "+(r.mask||"禁止")+(r.childrenOnly?" · 仅子项":""))+'</span><button class="danger" onclick="removeRule('+i+')">移除</button></div>').join("")}
async function saveUser(u){try{await api("/chfs/users",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,enabled:$("ue").checked,rules:window._editRules})});showUsers()}catch(e){fail(e.message)}}
async function deleteUser(u){if(!await confirmBox("删除用户","删除用户 "+u+"?用户目录不会自动删除。"))return;try{await api("/chfs/users?username="+encodeURIComponent(u),{method:"DELETE"});showUsers()}catch(e){fail(e.message)}}
function toggleTheme(){const dark=document.documentElement.classList.toggle("dark");localStorage.setItem("chfs-theme",dark?"dark":"light");renderChromeIcons()}function renderChromeIcons(){const dark=document.documentElement.classList.contains("dark");$("themeIcon").innerHTML='<svg class="ui-icon" viewBox="0 0 24 24">'+(dark?'<path d="M20 15.5A8.5 8.5 0 0 1 8.5 4 8.5 8.5 0 1 0 20 15.5z"/>':'<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>')+'</svg>';$('listIcon').innerHTML='<svg class="ui-icon" viewBox="0 0 24 24"><path d="M4 6h2M9 6h11M4 12h2M9 12h11M4 18h2M9 18h11"/></svg>';$('gridIcon').innerHTML='<svg class="ui-icon" viewBox="0 0 24 24"><rect x="4" y="4" width="6" height="6" rx="1"/><rect x="14" y="4" width="6" height="6" rx="1"/><rect x="4" y="14" width="6" height="6" rx="1"/><rect x="14" y="14" width="6" height="6" rx="1"/></svg>'}
if(localStorage.getItem("chfs-theme")==="dark"||(localStorage.getItem("chfs-theme")===null&&matchMedia("(prefers-color-scheme: dark)").matches))document.documentElement.classList.add("dark");
setView(localStorage.getItem("chfs-view")||"list");renderChromeIcons();setupDropZone();init();
</script>
</body></html>