diff --git a/internal/api/server.go b/internal/api/server.go
index f8b3676..49e5e51 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -71,6 +71,7 @@ type fileInfo struct {
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", s.health)
+ mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })
mux.HandleFunc("/webdav/", s.webdav)
mux.HandleFunc("/webdav", s.webdav)
mux.HandleFunc("/chfs/files", s.files)
diff --git a/internal/api/web/index.html b/internal/api/web/index.html
index 3058b3c..6f65c51 100644
--- a/internal/api/web/index.html
+++ b/internal/api/web/index.html
@@ -4,14 +4,15 @@
__CHFS_TITLE__
+
@@ -157,8 +158,9 @@ 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/thumbnail?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(''+esc(n)+''+(galleryIndex+1)+' / '+galleryFiles.length+' 
'+(galleryIndex+1)+' / '+galleryFiles.length+'
',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)}}
+let galleryTouchX=0;function galleryTouchStart(e){galleryTouchX=e.changedTouches[0]?.clientX||0}function galleryTouchEnd(e){const x=e.changedTouches[0]?.clientX||0,d=x-galleryTouchX;if(Math.abs(d)>45)galleryMove(d<0?1:-1)}
+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(''+esc(n)+''+(galleryIndex+1)+' / '+galleryFiles.length+' 
'+(galleryIndex+1)+' / '+galleryFiles.length+'
',true);const stage=$("galleryStage");stage?.addEventListener("touchstart",galleryTouchStart,{passive:true});stage?.addEventListener("touchend",galleryTouchEnd,{passive: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")}}}
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")}
@@ -177,7 +179,7 @@ async function login(){try{await fetch("/chfs/session",{method:"POST",credential
async function logout(){try{await api("/chfs/session",{method:"DELETE"})}finally{me={username:"",home:"",admin:false,managed:false};current="";navigationRoot="";selected.clear();updateUser();await refresh()}}
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&&!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)}}
+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($("modal").classList.contains("show")&&$("galleryStage")&&galleryFiles.length){if(e.key==="ArrowLeft"){e.preventDefault();galleryMove(-1);return}if(e.key==="ArrowRight"){e.preventDefault();galleryMove(1);return}if(e.key==="Home"){e.preventDefault();galleryIndex=0;renderGallery();return}if(e.key==="End"){e.preventDefault();galleryIndex=galleryFiles.length-1;renderGallery();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")}async function closeModal(){if(window._editorDirty&&$("editText")){const ok=await confirmBox("放弃未保存修改?","当前文本还有未保存的修改,关闭后这些修改将丢失。");if(!ok)return false}window._editorDirty=false;$("modal").classList.remove("show");$("dialog").classList.remove("preview-dialog");return true}
function ask(title,label,value="",type="text"){return new Promise(resolve=>{show(''+esc(title)+'
');window._askResolve=v=>{closeModal();resolve(v)}})}