feat: modernize search workspace interactions
This commit is contained in:
@@ -62,7 +62,7 @@ function selectablePaths(){return lastItems.map(itemPath)}
|
||||
function selectItem(p,checked,e){if(checked){const paths=selectablePaths();if(e?.shiftKey&&selectionAnchor){const a=paths.indexOf(selectionAnchor),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()}
|
||||
let selected=new Set(),lastItems=[],sortKey="name",sortDir=1;
|
||||
function sortItems(items){return [...items].sort((a,b)=>{if(a.dir!==b.dir)return a.dir?-1:1;let av=sortKey==="size"?Number(a.size||0):sortKey==="modified"?String(a.modified||""):String(a.name||"").toLocaleLowerCase();let bv=sortKey==="size"?Number(b.size||0):sortKey==="modified"?String(b.modified||""):String(b.name||"").toLocaleLowerCase();return av<bv?-sortDir:av>bv?sortDir:0})}
|
||||
function sortBy(key){if(sortKey===key)sortDir*=-1;else{sortKey=key;sortDir=1}render(lastItems)}
|
||||
function sortBy(key){if(sortKey===key)sortDir*=-1;else{sortKey=key;sortDir=1}if(searchMode){searchItems=sortItems(searchItems);renderSearch(searchItems);return}render(lastItems)}
|
||||
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()}
|
||||
@@ -75,10 +75,10 @@ function folderDragLeave(e){e.currentTarget.classList.remove('drop-target')}
|
||||
async function folderDrop(e,target){if(!e.dataTransfer?.types.includes('text/chfs-paths'))return;e.preventDefault();e.stopPropagation();e.currentTarget.classList.remove('drop-target');let paths=[];try{paths=JSON.parse(e.dataTransfer.getData('text/chfs-paths')||'[]')}catch{};const f=lastItems.find(x=>(current?current+'/':'')+x.name===target);if(!f?.dir||!paths.length)return;if(paths.some(p=>p===target||target.startsWith(p+'/')))return fail('不能移动到自身或子目录');const base=target.replace(/^\/+|\/+$/g,'');await runOperation('移动 '+paths.length+' 项',async()=>{let done=0,failed=[],renamed=[];for(const source of paths){const name=source.split('/').pop(),destination=base?base+'/'+name:name;try{const actual=await transferWithConflict(source,destination,false);if(actual!==destination)renamed.push(name+' → '+actual.split('/').pop());done++}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 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+" 项"):"";const header=$("files").closest("table")?.querySelector("thead .selectbox");if(header){const total=lastItems.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=lastItems[i];if(f){const p=current?current+"/"+f.name:f.name,yes=selected.has(p);el.classList.toggle("selected",yes);const cb=el.querySelector(".selectbox");if(cb)cb.checked=yes}})}
|
||||
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)}
|
||||
function toggleAll(v){const source=searchMode?searchItems:lastItems;source.forEach(f=>{const p=searchMode?f.path:(current?current+"/":"")+f.name;if(v)selected.add(p);else selected.delete(p)});searchMode?updateSelection():render(lastItems)}
|
||||
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();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'}
|
||||
@@ -130,15 +130,22 @@ function showMobileTransfer(){const u=new URL(location.href);u.hash='folder='+en
|
||||
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,task=null){return new Promise(resolve=>{const xhr=new XMLHttpRequest(),fd=new FormData();if(task)task.xhr=xhr;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=()=>{const ok=xhr.status>=200&&xhr.status<300;if(ok){bar.style.width='100%';meta.textContent=fmt(file.size)+' · 完成'}else{meta.textContent='上传失败'+(xhr.status?' · '+xhr.status:'');bar.style.width='0'};if(task)task.xhr=null;resolve(ok)};xhr.onerror=()=>{if(task&&task.state==='cancelled'){meta.textContent=fmt(file.size)+' · 已取消'}else{meta.textContent='网络错误';bar.style.width='0'};if(task)task.xhr=null;resolve(false)};xhr.onabort=()=>{if(task)task.xhr=null;resolve(false)};xhr.send(fd)})}
|
||||
let searchMode=false,searchItems=[];
|
||||
$("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const s=e.target.value.trim();if(!s){searchMode=false;searchItems=[];refresh();return}try{const d=await api("/chfs/search?str="+encodeURIComponent(s));renderSearch(d.files||[])}catch(x){fail(x.message)}})
|
||||
function renderSearch(items){searchMode=true;searchItems=items;selected.clear();$("crumb").textContent="搜索结果";$("files").parentElement.hidden=false;$("grid").hidden=true;const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=f.path,name=f.name||p.split("/").pop(),parent=p.includes("/")?p.slice(0,p.lastIndexOf("/")):"当前位置";return '<tr class="file-row" data-search-path="'+esc(p)+'" onclick="selectSearchItem('+JSON.stringify(p)+')" ondblclick="openSearchResult('+JSON.stringify(f)+')" oncontextmenu="searchMenu(event,'+JSON.stringify(f)+');return false"><td><input class="selectbox" type="checkbox" onclick="event.stopPropagation()" onchange="toggleSearchItem('+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(parent)+'</div></td><td class="perm-cell"><span class="meta">搜索结果</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>';updateSelection()}
|
||||
function selectSearchItem(p){selected.clear();if(p)selected.add(p);updateSelection()}
|
||||
function toggleSearchItem(p,checked){if(checked)selected.add(p);else selected.delete(p);updateSelection()}
|
||||
function searchMenu(e,f){e.preventDefault();selectSearchItem(f.path);let html='';html+='<button onclick="downloadFile('+JSON.stringify(f.path)+');closeContext()">下载</button>';if(f.dir)html+='<button onclick="downloadDir('+JSON.stringify(f.path)+');closeContext()">下载 ZIP</button>';html+='<button onclick="openSearchResult('+JSON.stringify(f)+');closeContext()">打开</button>';$("context").innerHTML=html;$("context").style.left=Math.min(e.clientX,innerWidth-190)+'px';$("context").style.top=Math.min(e.clientY,innerHeight-160)+'px';$("context").style.display='block'}
|
||||
function openSearchResult(f){if(f.dir){current=f.path;navigationRoot=me.home||"";searchMode=false;$("search").value="";refresh()}else previewFile(f.path)}
|
||||
$("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const s=e.target.value.trim();if(!s){searchMode=false;searchItems=[];selected.clear();refresh();return}try{const d=await api("/chfs/search?str="+encodeURIComponent(s));renderSearch(d.files||[])}catch(x){fail(x.message)}})
|
||||
function searchPath(f){return f.path||f.name||""}
|
||||
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 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();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 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;
|
||||
function previewFilesFor(p){const dir=p.includes('/')?p.split('/').slice(0,-1).join('/'):'',items=dir===current?lastItems:searchItems;return items.filter(f=>!f.dir&&fileType(f)==='image'&&(f.mask||'').includes('R'))}
|
||||
function galleryOpen(p){galleryFiles=previewFilesFor(p);galleryIndex=Math.max(0,galleryFiles.findIndex(f=>(dirOf(f.path||f.name)===current?f.name:f.path)===p));renderGallery()}
|
||||
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()}
|
||||
function previewFilesFor(p){const dir=dirOf(p),items=dir===current?lastItems:searchItems;return items.filter(f=>!f.dir&&fileType(f)==="image"&&(f.mask||"").includes("R")).map(f=>({...f,path:f.path||(dir?(dir+"/"+f.name):f.name)}))}
|
||||
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 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);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('/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"><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)}
|
||||
@@ -148,7 +155,7 @@ function editText(p,content){show('<div class="preview-head"><span class="previe
|
||||
async function saveTextEdit(p){const text=$('editText')?.value;if(text===undefined)return;const parts=p.split('/'),filename=parts.pop()||'';const filepath=parts.join('/');try{await api('/chfs/updatetext',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({filepath,filename,text})});toast('已保存');closeModal();refresh()}catch(e){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){galleryFiles=previewFilesFor(p);galleryIndex=Math.max(0,galleryFiles.findIndex(f=>(f.path||((current?current+'/':'')+f.name))===p));return renderGallery(p)}let content='';if(media){show('<div class="preview-head"><span class="preview-title">'+esc(n)+'</span><button class="ghost" onclick="togglePiP()" title="画中画">画中画</button><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 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('/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"><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 canEdit=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('/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"><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><button class="ghost" onclick="togglePiP()" title="画中画">画中画</button><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 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('/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"><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('/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"><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)}}
|
||||
@@ -157,7 +164,7 @@ async function login(){try{await fetch("/chfs/session",{method:"POST",headers:{"
|
||||
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"&¤t){e.preventDefault();goUp();return}if((e.key==="ArrowDown"||e.key==="ArrowUp")&&lastItems.length){e.preventDefault();const paths=selectablePaths(),anchor=selectionAnchor&&paths.includes(selectionAnchor)?selectionAnchor:[...selected][0],idx=Math.max(0,paths.indexOf(anchor)),next=Math.max(0,Math.min(paths.length-1,idx+(e.key==="ArrowDown"?1:-1)));selectItem(paths[next],true,e);if(e.shiftKey)selectionAnchor=anchor;return}if(e.key==="Home"&&lastItems.length){e.preventDefault();selectItem(selectablePaths()[0],true,e);return}if(e.key==="End"&&lastItems.length){e.preventDefault();selectItem(selectablePaths().at(-1),true,e)}}
|
||||
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"&¤t&&!searchMode){e.preventDefault();goUp();return}if((e.key==="ArrowDown"||e.key==="ArrowUp")){const source=searchMode?searchItems:lastItems;if(!source.length)return;e.preventDefault();const paths=searchMode?source.map(searchPath):selectablePaths(),anchor=selectionAnchor&&paths.includes(selectionAnchor)?selectionAnchor:[...selected][0],idx=Math.max(0,paths.indexOf(anchor)),next=Math.max(0,Math.min(paths.length-1,idx+(e.key==="ArrowDown"?1:-1)));if(searchMode)selectSearchItem(paths[next],true,e);else selectItem(paths[next],true,e);return}if(e.key==="Home"||e.key==="End"){const source=searchMode?searchItems:lastItems;if(!source.length)return;e.preventDefault();const paths=searchMode?source.map(searchPath):selectablePaths(),p=e.key==="Home"?paths[0]:paths.at(-1);if(searchMode)selectSearchItem(p,true,e);else selectItem(p,true,e)}}
|
||||
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)}})}
|
||||
|
||||
Reference in New Issue
Block a user