fix: escape inline file action arguments
This commit is contained in:
+17
-17
@@ -47,7 +47,7 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou
|
||||
<script>
|
||||
const $=id=>document.getElementById(id);let current="",me=null,currentMask="",navigationRoot="";
|
||||
async function api(url,opt={}){const r=await fetch(url,{credentials:"same-origin",cache:"no-store",...opt});if(!r.ok){let t="请求失败";try{t=(await r.json()).error||t}catch{};const e=Error(t);e.status=r.status;throw e}return r.status===204?null:r.json()}
|
||||
function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]))}
|
||||
function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]))}function attrJSON(v){return esc(JSON.stringify(v))}
|
||||
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}
|
||||
@@ -66,8 +66,8 @@ function sortBy(key){if(sortKey===key)sortDir*=-1;else{sortKey=key;sortDir=1}if(
|
||||
function renderSortState(){["name","size","modified"].forEach(k=>{const el=$("sort-"+k);if(el)el.textContent=sortKey===k?(sortDir===1?"↑":"↓"):""})}
|
||||
function emptyState(title,text,action=""){const writable=currentMask.includes("W");let buttons="";if(action==="folder"&&writable)buttons='<div class="row"><button onclick="newFolder()">新建文件夹</button><button class="ghost" onclick="newText()">新建文本</button></div>';if(action==="upload"&&writable)buttons='<div class="row"><button onclick="$(\"upload\").click()">上传文件</button><button class="ghost" onclick="newFolder()">新建文件夹</button></div>';return '<div class="empty-state"><div class="empty-icon">'+svgIcon("folder")+'</div><h3>'+esc(title)+'</h3><p>'+esc(text)+'</p>'+buttons+'</div>'}
|
||||
function render(items){lastItems=sortItems(items);selected=new Set([...selected].filter(p=>lastItems.some(f=>(current?current+"/":"")+f.name===p)));renderList(lastItems);renderGrid(lastItems);updateSelection();renderSortState()}
|
||||
function renderList(items){const body=$("files");body.innerHTML=items.length?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),drag=' draggable="true"';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':'')+'"'+drag+' data-file-path="'+esc(p)+'" onclick="handleItemClick(event,'+JSON.stringify(p)+')" ondblclick="activateItem('+JSON.stringify(p)+')" ondragstart="fileDragStart(event,'+JSON.stringify(p)+')" ondragend="fileDragEnd(event)" ondragover="folderDragOver(event,'+JSON.stringify(p)+')" ondragleave="folderDragLeave(event)" ondrop="folderDrop(event,'+JSON.stringify(p)+')"><td><input class="selectbox" type="checkbox" '+(selectedNow?"checked":"")+' onclick="event.stopPropagation()" onchange="selectItem('+JSON.stringify(p)+',this.checked,event)"></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="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></td></tr>'}).join(""):'<tr><td colspan="6">'+emptyState("这个文件夹还是空的","可以上传文件,或者创建一个新的文件夹。","upload")+'</td></tr>'}
|
||||
function renderGrid(items){$("grid").innerHTML=items.length?items.map(f=>{const p=current?current+"/"+f.name:f.name,canRead=(f.mask||"").includes("R"),type=fileType(f),selectedNow=selected.has(p),drag=' draggable="true"';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':'')+'"'+drag+' data-file-path="'+esc(p)+'" onclick="handleItemClick(event,'+JSON.stringify(p)+')" ondblclick="activateItem('+JSON.stringify(p)+')" oncontextmenu="itemMenu(event,'+JSON.stringify(p)+');return false" ondragstart="fileDragStart(event,'+JSON.stringify(p)+')" ondragend="fileDragEnd(event)" ondragover="folderDragOver(event,'+JSON.stringify(p)+')" ondragleave="folderDragLeave(event)" ondrop="folderDrop(event,'+JSON.stringify(p)+')"><input class="selectbox tile-check" type="checkbox" '+(selectedNow?'checked':'')+' onclick="event.stopPropagation();selectItem('+JSON.stringify(p)+',this.checked,event)"><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(''):emptyState("这个文件夹还是空的","上传文件或创建内容后,它们会显示在这里。","upload")}
|
||||
function renderList(items){const body=$("files");body.innerHTML=items.length?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),drag=' draggable="true"';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':'')+'"'+drag+' data-file-path="'+esc(p)+'" onclick="handleItemClick(event,'+attrJSON(p)+')" ondblclick="activateItem('+attrJSON(p)+')" ondragstart="fileDragStart(event,'+attrJSON(p)+')" ondragend="fileDragEnd(event)" ondragover="folderDragOver(event,'+attrJSON(p)+')" ondragleave="folderDragLeave(event)" ondrop="folderDrop(event,'+attrJSON(p)+')"><td><input class="selectbox" type="checkbox" '+(selectedNow?"checked":"")+' onclick="event.stopPropagation()" onchange="selectItem('+attrJSON(p)+',this.checked,event)"></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="event.stopPropagation();itemMenu(event,'+attrJSON(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">'+emptyState("这个文件夹还是空的","可以上传文件,或者创建一个新的文件夹。","upload")+'</td></tr>'}
|
||||
function renderGrid(items){$("grid").innerHTML=items.length?items.map(f=>{const p=current?current+"/"+f.name:f.name,canRead=(f.mask||"").includes("R"),type=fileType(f),selectedNow=selected.has(p),drag=' draggable="true"';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':'')+'"'+drag+' data-file-path="'+esc(p)+'" onclick="handleItemClick(event,'+attrJSON(p)+')" ondblclick="activateItem('+attrJSON(p)+')" oncontextmenu="itemMenu(event,'+attrJSON(p)+');return false" ondragstart="fileDragStart(event,'+attrJSON(p)+')" ondragend="fileDragEnd(event)" ondragover="folderDragOver(event,'+attrJSON(p)+')" ondragleave="folderDragLeave(event)" ondrop="folderDrop(event,'+attrJSON(p)+')"><input class="selectbox tile-check" type="checkbox" '+(selectedNow?'checked':'')+' onclick="event.stopPropagation();selectItem('+attrJSON(p)+',this.checked,event)"><button class="iconbtn tile-action" onclick="event.stopPropagation();itemMenu(event,'+attrJSON(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(''):emptyState("这个文件夹还是空的","上传文件或创建内容后,它们会显示在这里。","upload")}
|
||||
function fileDragStart(e,p){const paths=selected.has(p)?[...selected]:[p];e.dataTransfer.effectAllowed="move";e.dataTransfer.setData("text/chfs-paths",JSON.stringify(paths));e.currentTarget.classList.add("dragging")}
|
||||
function fileDragEnd(e){e.currentTarget.classList.remove("dragging");document.querySelectorAll('.drop-target').forEach(el=>el.classList.remove('drop-target'))}
|
||||
function folderDragOver(e,target){if(!e.dataTransfer?.types.includes('text/chfs-paths'))return;const f=lastItems.find(x=>(current?current+'/':'')+x.name===target);if(!f?.dir||!(f.mask||'').includes('W'))return;e.preventDefault();if(target===current||target===""||[...selected].some(p=>p===target||target.startsWith(p+'/')))return;e.dataTransfer.dropEffect="move";e.currentTarget.classList.add('drop-target')}
|
||||
@@ -76,7 +76,7 @@ async function folderDrop(e,target){if(!e.dataTransfer?.types.includes('text/chf
|
||||
function handleItemClick(e,p){e.preventDefault();if(!me.username){guestItemActions(p);return}activateItem(p)}
|
||||
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 sharedURL(p){return new URL("/chfs/shared/"+p.split("/").map(encodeURIComponent).join("/"),location.href).href}
|
||||
function guestItemActions(p){const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f||!(f.mask||"").includes("R")){toast("当前项目没有访问权限",true);return}if(f.dir){openDir(p);return}const url=sharedURL(p);let html='<h2>'+esc(f.name)+'</h2><div class="notice">访客可以直接下载或复制分享链接。</div><div class="row" style="justify-content:flex-start"><button onclick="location.href='+JSON.stringify(sharedURL(p))+'">下载</button><button class="ghost" onclick="guestCopyShare('+JSON.stringify(url)+')">复制分享链接</button></div>';show(html)}
|
||||
function guestItemActions(p){const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f||!(f.mask||"").includes("R")){toast("当前项目没有访问权限",true);return}if(f.dir){openDir(p);return}const url=sharedURL(p);let html='<h2>'+esc(f.name)+'</h2><div class="notice">访客可以直接下载或复制分享链接。</div><div class="row" style="justify-content:flex-start"><button onclick="location.href='+attrJSON(sharedURL(p))+'">下载</button><button class="ghost" onclick="guestCopyShare('+attrJSON(url)+')">复制分享链接</button></div>';show(html)}
|
||||
async function guestCopyShare(url){try{await navigator.clipboard.writeText(url);toast("分享链接已复制");closeModal()}catch{show('<h2>分享链接</h2><div class="field"><input value="'+esc(url)+'" readonly></div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>')}}
|
||||
function toggleTile(p){selectItem(p,!selected.has(p))}
|
||||
function openSelected(){if(selected.size!==1)return;const p=[...selected][0];if(searchMode){const f=searchItems.find(x=>x.path===p);if(f)openSearchResult(f);return}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)}
|
||||
@@ -84,13 +84,13 @@ function toggleAll(v){const source=searchMode?searchItems:lastItems;source.forEa
|
||||
function clearSelection(){selected.clear();updateSelection();if(searchMode){renderSearch(searchItems);return}renderGrid(lastItems);renderList(lastItems)}
|
||||
function updateSelection(){const n=selected.size;$("bulk").classList.toggle("show",n>0);$("selectedCount").textContent=n?("已选择 "+n+" 项"):"";const source=searchMode?searchItems:lastItems;const header=$("files").closest("table")?.querySelector("thead .selectbox");if(header){const total=source.length;header.checked=total>0&&n===total;header.indeterminate=n>0&&n<total}document.querySelectorAll("#files .file-row").forEach(row=>{const p=row.dataset.searchPath||row.dataset.filePath;row.classList.toggle("selected",!!p&&selected.has(p));const cb=row.querySelector(".selectbox");if(cb)cb.checked=!!p&&selected.has(p)});$("grid").querySelectorAll(".tile").forEach((el,i)=>{const f=source[i];if(f){const p=searchMode?f.path:(current?current+"/":"")+f.name,yes=selected.has(p);el.classList.toggle("selected",yes);const cb=el.querySelector(".selectbox");if(cb)cb.checked=yes}})}
|
||||
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();e.stopPropagation();if(!selected.has(p)){selected.clear();selected.add(p);updateSelection()}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 itemMenu(e,p){e.preventDefault();e.stopPropagation();if(!selected.has(p)){selected.clear();selected.add(p);updateSelection()}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('+attrJSON(p)+');closeContext()">下载</button>';if(f?.dir&&canRead)html+='<button onclick="downloadDir('+attrJSON(p)+');closeContext()">下载 ZIP</button>';if(canWrite)html+='<button onclick="renameItem('+attrJSON(p)+');closeContext()">重命名</button><button onclick="transferItem('+attrJSON(p)+',false);closeContext()">移动到…</button><button onclick="transferItem('+attrJSON(p)+',true);closeContext()">复制到…</button>';if(canDelete)html+='<button onclick="removeItem('+attrJSON(p)+');closeContext()">删除</button>';if(f?.guestMask?.includes('R'))html+='<button onclick="shareItem('+attrJSON(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('.tile-action')&&!e.target.closest('.actions'))closeContext()});
|
||||
function bulkDownload(){if(!selected.size)return;const q=[...selected].map(p=>'path='+encodeURIComponent(p)).join('&');location.href='/chfs/downloadzip?'+q}
|
||||
let destinationResolve=null,destinationPath="";
|
||||
async function chooseDestination(title){destinationPath=current;return await new Promise(async resolve=>{destinationResolve=resolve;try{const d=await api("/chfs/roots"),roots=d.roots||[];const writableCurrent=currentMask.includes("W");if(!writableCurrent){const first=roots.find(r=>String(r.mask||"").includes("W"));if(first)destinationPath=first.path||""}await renderDestinationPicker(title)}catch(e){destinationResolve=null;fail("无法打开目标目录:"+e.message);resolve(null)}})}
|
||||
async function renderDestinationPicker(title){const data=await api("/chfs/files?filepath="+encodeURIComponent(destinationPath));const dirs=(data.files||[]).filter(f=>f.dir&&(f.mask||"").includes("W"));const parent=destinationPath?destinationPath.split("/").slice(0,-1).join("/"):null;let html='<h2>'+esc(title)+'</h2><div class="notice">当前目标:<b>'+esc(destinationPath||"全部文件")+'</b></div><div class="field" style="max-height:42vh;overflow:auto">';if(parent!==null)html+='<button class="ghost" style="display:block;width:100%;text-align:left;margin:5px 0" onclick="destinationPath='+JSON.stringify(parent)+';renderDestinationPicker('+JSON.stringify(title)+')">上一级</button>';for(const d of dirs){const p=destinationPath?(destinationPath+"/"+d.name):d.name;html+='<button class="ghost" style="display:block;width:100%;text-align:left;margin:5px 0" onclick="destinationPath='+JSON.stringify(p)+';renderDestinationPicker('+JSON.stringify(title)+')">'+svgIcon("folder")+' '+esc(d.name)+'</button>'}if(!dirs.length)html+='<div class="meta" style="padding:10px">这里没有可写入的子目录。</div>';html+='</div><div class="row"><button class="ghost" onclick="resolveDestination(null)">取消</button><button onclick="resolveDestination('+JSON.stringify(destinationPath)+')">选择此目录</button></div>';show(html)}
|
||||
async function renderDestinationPicker(title){const data=await api("/chfs/files?filepath="+encodeURIComponent(destinationPath));const dirs=(data.files||[]).filter(f=>f.dir&&(f.mask||"").includes("W"));const parent=destinationPath?destinationPath.split("/").slice(0,-1).join("/"):null;let html='<h2>'+esc(title)+'</h2><div class="notice">当前目标:<b>'+esc(destinationPath||"全部文件")+'</b></div><div class="field" style="max-height:42vh;overflow:auto">';if(parent!==null)html+='<button class="ghost" style="display:block;width:100%;text-align:left;margin:5px 0" onclick="destinationPath='+attrJSON(parent)+';renderDestinationPicker('+attrJSON(title)+')">上一级</button>';for(const d of dirs){const p=destinationPath?(destinationPath+"/"+d.name):d.name;html+='<button class="ghost" style="display:block;width:100%;text-align:left;margin:5px 0" onclick="destinationPath='+attrJSON(p)+';renderDestinationPicker('+attrJSON(title)+')">'+svgIcon("folder")+' '+esc(d.name)+'</button>'}if(!dirs.length)html+='<div class="meta" style="padding:10px">这里没有可写入的子目录。</div>';html+='</div><div class="row"><button class="ghost" onclick="resolveDestination(null)">取消</button><button onclick="resolveDestination('+attrJSON(destinationPath)+')">选择此目录</button></div>';show(html)}
|
||||
function resolveDestination(path){const resolve=destinationResolve;destinationResolve=null;closeModal();if(resolve)resolve(path)}
|
||||
let operationBusy=false;
|
||||
async function runOperation(label,fn){if(operationBusy)return null;operationBusy=true;toast(label+"…");try{return await fn()}finally{operationBusy=false}}
|
||||
@@ -103,14 +103,14 @@ async function bulkTransfer(copyOnly){if(!selected.size)return;const dstDir=awai
|
||||
async function bulkDelete(){if(!selected.size)return;const paths=[...selected];if(!await confirmBox("确认删除",paths.length===1?"确定删除 "+paths[0]+"?":"确定删除这 "+paths.length+" 项?删除文件夹时会同时删除其中内容。"))return;await runOperation("删除中",async()=>{try{const body=new URLSearchParams();paths.forEach(p=>body.append("filepath",p));await api("/chfs/rmfiles",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body});selected.clear();await refresh();toast(paths.length===1?"已删除":"已删除 "+paths.length+" 项")}catch(e){fail("删除失败:"+e.message)}})}
|
||||
|
||||
|
||||
async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>{const writable=String(r.mask||"").includes("W");return '<button class="side-item '+(r.path===current?'active':'')+'" data-root-path="'+esc(r.path)+'" onclick="jumpRoot('+JSON.stringify(r.path)+')" ondragover="sideDragOver(event,'+JSON.stringify(r.path)+','+writable+')" ondragleave="sideDragLeave(event)" ondrop="sideDrop(event,'+JSON.stringify(r.path)+','+writable+')">'+svgIcon('folder')+' '+esc(r.name)+'</button>'}).join('')||'<div class="meta" style="padding:8px">当前账号没有其他位置</div>'}catch(e){$("sideRoots").innerHTML='<div class="meta" style="padding:8px">位置加载失败</div>'}}
|
||||
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)}}
|
||||
async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>{const writable=String(r.mask||"").includes("W");return '<button class="side-item '+(r.path===current?'active':'')+'" data-root-path="'+esc(r.path)+'" onclick="jumpRoot('+attrJSON(r.path)+')" ondragover="sideDragOver(event,'+attrJSON(r.path)+','+writable+')" ondragleave="sideDragLeave(event)" ondrop="sideDrop(event,'+attrJSON(r.path)+','+writable+')">'+svgIcon('folder')+' '+esc(r.name)+'</button>'}).join('')||'<div class="meta" style="padding:8px">当前账号没有其他位置</div>'}catch(e){$("sideRoots").innerHTML='<div class="meta" style="padding:8px">位置加载失败</div>'}}
|
||||
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('+attrJSON(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 sideDragOver(e,target,writable){if(!e.dataTransfer?.types.includes('text/chfs-paths')||!writable)return;e.preventDefault();e.dataTransfer.dropEffect='move';e.currentTarget.classList.add('drop-target')}
|
||||
function sideDragLeave(e){e.currentTarget.classList.remove('drop-target')}
|
||||
async function sideDrop(e,target,writable){if(!e.dataTransfer?.types.includes('text/chfs-paths')||!writable)return;e.preventDefault();e.stopPropagation();e.currentTarget.classList.remove('drop-target');let paths=[];try{paths=JSON.parse(e.dataTransfer.getData('text/chfs-paths')||'[]')}catch{};if(!paths.length)return;await runOperation('正在移动 '+paths.length+' 项',async()=>{const failed=[],renamed=[];let done=0;for(const source of paths){const name=source.split('/').pop(),destination=target?target+'/'+name:name;try{const actual=await transferWithConflict(source,destination,false);done++;if(actual!==destination)renamed.push(name+' → '+actual.split('/').pop())}catch(err){failed.push(name+': '+err.message)}}selected.clear();await refresh();if(failed.length||renamed.length)show('<h2>移动完成</h2><div class="notice">'+esc('成功 '+done+' 项,失败 '+failed.length+' 项'+(renamed.length?'。\n\n自动重命名:\n'+renamed.join('\n'):'')+(failed.length?'。\n\n失败:\n'+failed.join('\n'):''))+'</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>');else toast('已移动 '+done+' 项')})}
|
||||
|
||||
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 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('+attrJSON(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('+attrJSON(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=sharedURL(p)}
|
||||
@@ -140,11 +140,11 @@ function searchName(f){const p=searchPath(f);return f.name||p.split("/").pop()}
|
||||
function searchParent(f){const p=searchPath(f);return p.includes("/")?p.slice(0,p.lastIndexOf("/")):"全部文件"}
|
||||
function searchCan(f,mask){return String(f.mask||"").includes(mask)}
|
||||
function renderSearch(items){searchMode=true;searchItems=sortItems(items);selected=new Set([...selected].filter(p=>searchItems.some(f=>searchPath(f)===p)));$("crumb").textContent="搜索结果";$("files").parentElement.hidden=localStorage.getItem("chfs-view")==="grid";$("grid").hidden=localStorage.getItem("chfs-view")!=="grid";renderSearchList(searchItems);renderSearchGrid(searchItems);updateSelection();renderSortState()}
|
||||
function renderSearchList(items){const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=searchPath(f),name=searchName(f),canRead=searchCan(f,"R"),canWrite=searchCan(f,"W"),canDelete=searchCan(f,"D"),selectedNow=selected.has(p),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':'')+'" data-search-path="'+esc(p)+'" onclick="selectSearchItem('+JSON.stringify(p)+',true,event)" ondblclick="openSearchResult('+JSON.stringify(f)+')" oncontextmenu="searchMenu(event,'+JSON.stringify(f)+');return false"><td><input class="selectbox" type="checkbox" '+(selectedNow?'checked':'')+' onclick="event.stopPropagation()" onchange="selectSearchItem('+JSON.stringify(p)+',this.checked,event)"></td><td><div class="name"><span class="file-kind">'+iconFor(f)+'</span><span>'+esc(name)+'</span></div></td><td class="size-cell">'+(f.dir?"—":fmt(f.size))+'</td><td class="time-cell"><span>'+esc(f.modified||"")+'</span><div class="meta">'+esc(searchParent(f))+'</div></td><td class="perm-cell"><span class="perm-dot '+(canRead?'read':'')+'"></span><span class="perm-dot '+(canWrite?'write':'')+'"></span><span class="perm-dot '+(canDelete?'delete':'')+'"></span></td><td><div class="actions"><button class="iconbtn" onclick="event.stopPropagation();searchMenu(event,'+JSON.stringify(f)+')" 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">'+emptyState("没有找到匹配文件","换一个关键词试试,或者清空搜索框返回当前位置。")+'</td></tr>'}
|
||||
function renderSearchGrid(items){$("grid").innerHTML=items.length?items.map(f=>{const p=searchPath(f),name=searchName(f),canRead=searchCan(f,"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':'');return '<div class="tile '+tileClass+(selectedNow?' selected':'')+'" data-file-path="'+esc(p)+'" onclick="selectSearchItem('+JSON.stringify(p)+',true,event)" ondblclick="openSearchResult('+JSON.stringify(f)+')" oncontextmenu="searchMenu(event,'+JSON.stringify(f)+');return false"><input class="selectbox tile-check" type="checkbox" '+(selectedNow?'checked':'')+' onclick="event.stopPropagation();selectSearchItem('+JSON.stringify(p)+',this.checked,event)"><button class="iconbtn tile-action" onclick="event.stopPropagation();searchMenu(event,'+JSON.stringify(f)+')" 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(name)+'</div></div><div class="tile-meta"><span>'+esc(searchParent(f)||"全部文件")+'</span>'+(f.modified?'<span>'+esc(f.modified)+'</span>':'')+'</div></div>'}).join(""):'<div class="empty-state search-empty"><div class="empty-icon">'+svgIcon("file")+'</div><h3>没有找到匹配文件</h3><p>换一个关键词试试,或者清空搜索框返回当前位置。</p></div>'}
|
||||
function renderSearchList(items){const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=searchPath(f),name=searchName(f),canRead=searchCan(f,"R"),canWrite=searchCan(f,"W"),canDelete=searchCan(f,"D"),selectedNow=selected.has(p),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':'')+'" data-search-path="'+esc(p)+'" onclick="selectSearchItem('+attrJSON(p)+',true,event)" ondblclick="openSearchResult('+attrJSON(f)+')" oncontextmenu="searchMenu(event,'+attrJSON(f)+');return false"><td><input class="selectbox" type="checkbox" '+(selectedNow?'checked':'')+' onclick="event.stopPropagation()" onchange="selectSearchItem('+attrJSON(p)+',this.checked,event)"></td><td><div class="name"><span class="file-kind">'+iconFor(f)+'</span><span>'+esc(name)+'</span></div></td><td class="size-cell">'+(f.dir?"—":fmt(f.size))+'</td><td class="time-cell"><span>'+esc(f.modified||"")+'</span><div class="meta">'+esc(searchParent(f))+'</div></td><td class="perm-cell"><span class="perm-dot '+(canRead?'read':'')+'"></span><span class="perm-dot '+(canWrite?'write':'')+'"></span><span class="perm-dot '+(canDelete?'delete':'')+'"></span></td><td><div class="actions"><button class="iconbtn" onclick="event.stopPropagation();searchMenu(event,'+attrJSON(f)+')" 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">'+emptyState("没有找到匹配文件","换一个关键词试试,或者清空搜索框返回当前位置。")+'</td></tr>'}
|
||||
function renderSearchGrid(items){$("grid").innerHTML=items.length?items.map(f=>{const p=searchPath(f),name=searchName(f),canRead=searchCan(f,"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':'');return '<div class="tile '+tileClass+(selectedNow?' selected':'')+'" data-file-path="'+esc(p)+'" onclick="selectSearchItem('+attrJSON(p)+',true,event)" ondblclick="openSearchResult('+attrJSON(f)+')" oncontextmenu="searchMenu(event,'+attrJSON(f)+');return false"><input class="selectbox tile-check" type="checkbox" '+(selectedNow?'checked':'')+' onclick="event.stopPropagation();selectSearchItem('+attrJSON(p)+',this.checked,event)"><button class="iconbtn tile-action" onclick="event.stopPropagation();searchMenu(event,'+attrJSON(f)+')" 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(name)+'</div></div><div class="tile-meta"><span>'+esc(searchParent(f)||"全部文件")+'</span>'+(f.modified?'<span>'+esc(f.modified)+'</span>':'')+'</div></div>'}).join(""):'<div class="empty-state search-empty"><div class="empty-icon">'+svgIcon("file")+'</div><h3>没有找到匹配文件</h3><p>换一个关键词试试,或者清空搜索框返回当前位置。</p></div>'}
|
||||
function selectSearchItem(p,checked,e){if(checked){if(e?.shiftKey){const paths=searchItems.map(searchPath),anchor=selectionAnchor&&paths.includes(selectionAnchor)?selectionAnchor:[...selected][0],a=paths.indexOf(anchor),b=paths.indexOf(p);if(a>=0&&b>=0){const lo=Math.min(a,b),hi=Math.max(a,b);if(!(e.ctrlKey||e.metaKey))selected.clear();for(let i=lo;i<=hi;i++)selected.add(paths[i])}else selected.add(p)}else if(e?.ctrlKey||e?.metaKey)selected.add(p);else{selected.clear();selected.add(p)}selectionAnchor=p}else{selected.delete(p);selectionAnchor=p}updateSelection();renderSearch(searchItems)}
|
||||
function toggleSearchItem(p,checked,e){selectSearchItem(p,checked,e)}
|
||||
function searchMenu(e,f){e.preventDefault();e.stopPropagation();const p=searchPath(f);if(!selected.has(p)){selected.clear();selected.add(p)}updateSelection();let html='';const canRead=searchCan(f,"R"),canWrite=searchCan(f,"W"),canDelete=searchCan(f,"D");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 searchMenu(e,f){e.preventDefault();e.stopPropagation();const p=searchPath(f);if(!selected.has(p)){selected.clear();selected.add(p)}updateSelection();let html='';const canRead=searchCan(f,"R"),canWrite=searchCan(f,"W"),canDelete=searchCan(f,"D");if(canRead)html+='<button onclick="downloadFile('+attrJSON(p)+');closeContext()">下载</button>';if(f.dir&&canRead)html+='<button onclick="downloadDir('+attrJSON(p)+');closeContext()">下载 ZIP</button>';if(canWrite)html+='<button onclick="renameItem('+attrJSON(p)+');closeContext()">重命名</button><button onclick="transferItem('+attrJSON(p)+',false);closeContext()">移动到…</button><button onclick="transferItem('+attrJSON(p)+',true);closeContext()">复制到…</button>';if(canDelete)html+='<button onclick="removeItem('+attrJSON(p)+');closeContext()">删除</button>';if(f.guestMask?.includes("R"))html+='<button onclick="shareItem('+attrJSON(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 openSearchResult(f){if(f.dir){current=f.path;navigationRoot=me.home&&f.path.startsWith(me.home)?me.home:f.path.split("/")[0]||"";searchMode=false;$("search").value="";selected.clear();refresh()}else previewFile(f.path)}
|
||||
let galleryFiles=[],galleryIndex=0,activeMedia=null;
|
||||
async function openGallery(p){const dir=dirOf(p);let items=dir===current?lastItems:null;if(!items){try{const d=await api("/chfs/files?filepath="+encodeURIComponent(dir));items=d.files||[]}catch(e){return fail("无法加载图片目录:"+e.message)}}galleryFiles=items.filter(f=>!f.dir&&fileType(f)==="image"&&(f.mask||"").includes("R")).map(f=>({...f,path:f.path||(dir?(dir+"/"+f.name):f.name)}));galleryIndex=Math.max(0,galleryFiles.findIndex(f=>f.path===p));renderGallery()}
|
||||
@@ -153,18 +153,18 @@ function galleryOpen(p){return openGallery(p)}
|
||||
function dirOf(p){return String(p||'').split('/').slice(0,-1).join('/')}
|
||||
function galleryMove(delta){if(!galleryFiles.length)return;galleryIndex=(galleryIndex+delta+galleryFiles.length)%galleryFiles.length;const f=galleryFiles[galleryIndex],path=f.path||(current?current+'/'+f.name:f.name);renderGallery(path)}
|
||||
function preloadGallery(){for(const offset of [-1,1]){const f=galleryFiles[(galleryIndex+offset+galleryFiles.length)%galleryFiles.length];if(f){const img=new Image();img.src="/chfs/preview?file="+encodeURIComponent(f.path)}}}
|
||||
function renderGallery(path){const f=galleryFiles[galleryIndex];if(!f)return;const p=path||((f.path)||((current?current+"/":"")+f.name)),n=f.name,src="/chfs/preview?file="+encodeURIComponent(p);preloadGallery();show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span><span class="meta">'+(galleryIndex+1)+' / '+galleryFiles.length+'</span><button class="iconbtn" onclick="location.href='+JSON.stringify(sharedURL(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"><img src="'+src+'" alt="'+esc(n)+'"><button class="gallery-nav gallery-prev" onclick="galleryMove(-1)" title="上一张"><svg class="ui-icon" viewBox="0 0 24 24"><path d="m14 6-6 6 6 6"/></svg></button><button class="gallery-nav gallery-next" onclick="galleryMove(1)" title="下一张"><svg class="ui-icon" viewBox="0 0 24 24"><path d="m10 6 6 6-6 6"/></svg></button><div class="gallery-count">'+(galleryIndex+1)+' / '+galleryFiles.length+'</div></div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true)}
|
||||
function renderGallery(path){const f=galleryFiles[galleryIndex];if(!f)return;const p=path||((f.path)||((current?current+"/":"")+f.name)),n=f.name,src="/chfs/preview?file="+encodeURIComponent(p);preloadGallery();show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span><span class="meta">'+(galleryIndex+1)+' / '+galleryFiles.length+'</span><button class="iconbtn" onclick="location.href='+attrJSON(sharedURL(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"><img src="'+src+'" alt="'+esc(n)+'"><button class="gallery-nav gallery-prev" onclick="galleryMove(-1)" title="上一张"><svg class="ui-icon" viewBox="0 0 24 24"><path d="m14 6-6 6 6 6"/></svg></button><button class="gallery-nav gallery-next" onclick="galleryMove(1)" title="下一张"><svg class="ui-icon" viewBox="0 0 24 24"><path d="m10 6 6 6-6 6"/></svg></button><div class="gallery-count">'+(galleryIndex+1)+' / '+galleryFiles.length+'</div></div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true)}
|
||||
function mediaKeydown(e){const mediaActive=!!activeMedia&&($('modal').classList.contains('show')||$('miniPlayer'));if(mediaActive){if(e.key===' '){e.preventDefault();activeMedia.paused?activeMedia.play():activeMedia.pause()}else if(e.key==='ArrowLeft'||e.key==='ArrowRight'){e.preventDefault();activeMedia.currentTime=Math.max(0,activeMedia.currentTime+(e.key==='ArrowLeft'?-5:5))}else if((e.key==='['||e.key===']')&&activeMedia.playbackRate){e.preventDefault();activeMedia.playbackRate=Math.max(.25,Math.min(2,activeMedia.playbackRate+(e.key==='['?-.25:.25)));toast("播放速度 "+activeMedia.playbackRate+"x")}}if($('modal').classList.contains('show')&&galleryFiles.length&&(e.key==='ArrowLeft'||e.key==='ArrowRight')){e.preventDefault();galleryMove(e.key==='ArrowLeft'?-1:1)}}
|
||||
function setPlaybackRate(v){if(activeMedia){activeMedia.playbackRate=Number(v);toast("播放速度 "+v+"x")}}
|
||||
addEventListener('keydown',mediaKeydown);
|
||||
function editorLines(text){return Array.from({length:Math.max(1,text.split("\n").length)},(_,i)=>i+1).join("\n")}
|
||||
function markEditorDirty(){const el=$("editorDirty");if(el)el.textContent="未保存";window._editorDirty=true}
|
||||
function syncEditorLines(){const input=$("editText"),lines=$("editorLines");if(!input||!lines)return;lines.textContent=editorLines(input.value);lines.scrollTop=input.scrollTop}
|
||||
function editText(p,content){window._editorDirty=false;show('<div class="preview-head"><span class="preview-title">编辑 · '+esc(p.split('/').pop())+'</span><span id="editorDirty" class="meta">已保存</span><button class="iconbtn" onclick="saveTextEdit('+JSON.stringify(p)+')" title="保存">保存</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" style="display:block"><div class="editor-wrap"><pre id="editorLines" class="editor-lines">1</pre><textarea id="editText" class="editor-input" spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off">'+esc(content)+'</textarea></div></div><div class="preview-foot"><div class="editor-status"><span class="meta">Tab 缩进 · Ctrl/Cmd + S 保存</span></div><button class="ghost" onclick="closeModal()">取消</button><button onclick="saveTextEdit('+JSON.stringify(p)+')">保存</button></div>',true);const input=$("editText");input.focus();syncEditorLines();input.addEventListener("input",()=>{markEditorDirty();syncEditorLines()});input.addEventListener("scroll",syncEditorLines);input.addEventListener("keydown",e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="s"){e.preventDefault();saveTextEdit(p);return}if(e.key==="Tab"){e.preventDefault();const a=input.selectionStart,b=input.selectionEnd,inputValue=input.value;if(e.shiftKey){const lineStart=inputValue.lastIndexOf("\n",a-1)+1;if(inputValue.slice(lineStart,lineStart+2)===" "){input.setSelectionRange(lineStart,lineStart+2);document.execCommand("delete");syncEditorLines()}}else{input.setRangeText(" ",a,b,"end");markEditorDirty();syncEditorLines()}}});}
|
||||
function editText(p,content){window._editorDirty=false;show('<div class="preview-head"><span class="preview-title">编辑 · '+esc(p.split('/').pop())+'</span><span id="editorDirty" class="meta">已保存</span><button class="iconbtn" onclick="saveTextEdit('+attrJSON(p)+')" title="保存">保存</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" style="display:block"><div class="editor-wrap"><pre id="editorLines" class="editor-lines">1</pre><textarea id="editText" class="editor-input" spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off">'+esc(content)+'</textarea></div></div><div class="preview-foot"><div class="editor-status"><span class="meta">Tab 缩进 · Ctrl/Cmd + S 保存</span></div><button class="ghost" onclick="closeModal()">取消</button><button onclick="saveTextEdit('+attrJSON(p)+')">保存</button></div>',true);const input=$("editText");input.focus();syncEditorLines();input.addEventListener("input",()=>{markEditorDirty();syncEditorLines()});input.addEventListener("scroll",syncEditorLines);input.addEventListener("keydown",e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="s"){e.preventDefault();saveTextEdit(p);return}if(e.key==="Tab"){e.preventDefault();const a=input.selectionStart,b=input.selectionEnd,inputValue=input.value;if(e.shiftKey){const lineStart=inputValue.lastIndexOf("\n",a-1)+1;if(inputValue.slice(lineStart,lineStart+2)===" "){input.setSelectionRange(lineStart,lineStart+2);document.execCommand("delete");syncEditorLines()}}else{input.setRangeText(" ",a,b,"end");markEditorDirty();syncEditorLines()}}});}
|
||||
async function saveTextEdit(p){const input=$("editText");const text=input?.value;if(text===undefined)return;const parts=p.split("/"),filename=parts.pop()||"",filepath=parts.join("/");const status=$("editorDirty");if(status)status.textContent="保存中…";try{await api("/chfs/updatetext",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath,filename,text})});window._editorDirty=false;if(status)status.textContent="已保存";window._previewText=text;toast("已保存");}catch(e){if(status)status.textContent="保存失败";fail(e.message)}}
|
||||
async function togglePiP(){if(!activeMedia||!('pictureInPictureEnabled' in document)||!document.pictureInPictureEnabled)return fail('当前浏览器不支持画中画');try{if(document.pictureInPictureElement)await document.exitPictureInPicture();else if(activeMedia.requestPictureInPicture)await activeMedia.requestPictureInPicture()}catch(e){fail('无法进入画中画')} }
|
||||
function startMiniPlayer(url,name){let host=$('miniPlayer');if(!host){host=document.createElement('div');host.id='miniPlayer';host.style='position:fixed;right:18px;bottom:18px;z-index:30;width:min(380px,calc(100vw - 36px));padding:12px 14px;border:1px solid var(--border);border-radius:14px;background:var(--surface);box-shadow:var(--shadow);display:grid;gap:8px';document.body.appendChild(host)}host.innerHTML='<div style="display:flex;align-items:center;gap:8px"><span class="preview-title">'+esc(name)+'</span><button class="iconbtn" style="width:30px;height:30px" onclick="document.getElementById(\'miniPlayer\')?.remove();activeMedia=null" title="关闭"><svg class="ui-icon" viewBox="0 0 24 24"><path d="m6 6 12 12M18 6 6 18"/></svg></button></div><audio controls autoplay style="width:100%"><source src="'+url+'"></audio>';activeMedia=host.querySelector('audio');closeModal();activeMedia?.focus()}
|
||||
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);if(image){return openGallery(p)}let content='';if(media){show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span><div class="media-toolbar"><label>速度</label><select onchange="setPlaybackRate(this.value)"><option value="0.5">0.5x</option><option value="0.75">0.75x</option><option value="1" selected>1x</option><option value="1.25">1.25x</option><option value="1.5">1.5x</option><option value="2">2x</option></select></div><button class="ghost" onclick="togglePiP()" title="画中画">画中画</button><button class="iconbtn" onclick="location.href='+JSON.stringify(sharedURL(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 media-stage"><video controls autoplay tabindex="0"><source src="'+url+'"></video><span class="media-hint">空格暂停 · ← / → 快进 5 秒</span></div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true);activeMedia=$('.preview-stage video');activeMedia?.focus();return}if(audio){startMiniPlayer(url,n);return}if(pdf){show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span><button class="iconbtn" onclick="location.href='+JSON.stringify(sharedURL(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"><iframe src="'+url+'"></iframe></div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true);return}if(text){try{const r=await fetch(url);if(!r.ok)throw Error();content=await r.text();const targetFile=searchMode?searchItems.find(f=>searchPath(f)===p):lastItems.find(f=>(current?current+'/':'')+f.name===p),canEdit=String(targetFile?.mask||currentMask).includes('W');show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span>'+(canEdit?'<button class="ghost" onclick="editText('+JSON.stringify(p)+',window._previewText)" title="编辑">编辑</button>':'')+'<button class="iconbtn" onclick="location.href='+JSON.stringify(sharedURL(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"><pre class="preview-text">'+esc(content)+'</pre></div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true);window._previewText=content}catch{show('<div class="preview-empty">无法读取文本内容</div>')}}else show('<div class="preview-empty">暂不支持该文件类型的在线预览</div>')}
|
||||
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);if(image){return openGallery(p)}let content='';if(media){show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span><div class="media-toolbar"><label>速度</label><select onchange="setPlaybackRate(this.value)"><option value="0.5">0.5x</option><option value="0.75">0.75x</option><option value="1" selected>1x</option><option value="1.25">1.25x</option><option value="1.5">1.5x</option><option value="2">2x</option></select></div><button class="ghost" onclick="togglePiP()" title="画中画">画中画</button><button class="iconbtn" onclick="location.href='+attrJSON(sharedURL(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 media-stage"><video controls autoplay tabindex="0"><source src="'+url+'"></video><span class="media-hint">空格暂停 · ← / → 快进 5 秒</span></div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true);activeMedia=$('.preview-stage video');activeMedia?.focus();return}if(audio){startMiniPlayer(url,n);return}if(pdf){show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span><button class="iconbtn" onclick="location.href='+attrJSON(sharedURL(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"><iframe src="'+url+'"></iframe></div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true);return}if(text){try{const r=await fetch(url);if(!r.ok)throw Error();content=await r.text();const targetFile=searchMode?searchItems.find(f=>searchPath(f)===p):lastItems.find(f=>(current?current+'/':'')+f.name===p),canEdit=String(targetFile?.mask||currentMask).includes('W');show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span>'+(canEdit?'<button class="ghost" onclick="editText('+attrJSON(p)+',window._previewText)" title="编辑">编辑</button>':'')+'<button class="iconbtn" onclick="location.href='+attrJSON(sharedURL(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"><pre class="preview-text">'+esc(content)+'</pre></div><div class="preview-foot"><button class="ghost" onclick="closeModal()">关闭</button></div>',true);window._previewText=content}catch{show('<div class="preview-empty">无法读取文本内容</div>')}}else show('<div class="preview-empty">暂不支持该文件类型的在线预览</div>')}
|
||||
|
||||
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)}}
|
||||
@@ -179,10 +179,10 @@ function show(html,preview=false){$("dialog").innerHTML=html;$("dialog").classLi
|
||||
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 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('+attrJSON(u.username)+')">权限/状态</button> <button class="ghost" onclick="resetUser('+attrJSON(u.username)+')">重置密码</button> <button class="danger" onclick="deleteUser('+attrJSON(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()}
|
||||
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('+attrJSON(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()}
|
||||
|
||||
Reference in New Issue
Block a user