feat: Add "Download All" button and outside-click close to gallery

Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
2026-05-18 16:01:03 +00:00
parent d00e1dceeb
commit 2523410c84
2 changed files with 41 additions and 3 deletions

View File

@@ -20,6 +20,7 @@ import sqlite3
import binascii import binascii
import base64 import base64
import mimetypes import mimetypes
import zipfile
import pytz import pytz
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
@@ -28,7 +29,7 @@ import math
import logging import logging
import httpx import httpx
from fastapi import FastAPI, UploadFile, WebSocket, WebSocketDisconnect, Request, Form from fastapi import FastAPI, UploadFile, WebSocket, WebSocketDisconnect, Request, Form
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse, Response from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse, Response, StreamingResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.websockets import WebSocketState from starlette.websockets import WebSocketState
@@ -1385,6 +1386,37 @@ async def api_files_full(path_b64: str, request: Request):
return FileResponse(file_path) return FileResponse(file_path)
@app.get("/api/files/zip/{path_b64}")
async def api_files_zip(path_b64: str, request: Request):
if not request.session.get("accessToken"):
return Response(status_code=401)
try:
rel_path = base64.urlsafe_b64decode(path_b64).decode()
dir_path = os.path.join(UPLOAD_ROOT, rel_path)
except Exception:
return Response(status_code=400)
if not _is_safe_path(UPLOAD_ROOT, dir_path) or not os.path.isdir(dir_path):
return Response(status_code=404)
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
for filename in sorted(os.listdir(dir_path)):
file_path = os.path.join(dir_path, filename)
if os.path.isfile(file_path):
zip_file.write(file_path, filename)
zip_buffer.seek(0)
zip_filename = f"{os.path.basename(rel_path) or 'download'}.zip"
return StreamingResponse(
iter([zip_buffer.getvalue()]),
media_type="application/zip",
headers={"Content-Disposition": f"attachment; filename=\"{zip_filename}\""}
)
# ---------- Invites (one-time/expiring links) ---------- # ---------- Invites (one-time/expiring links) ----------
def ensure_invites_table() -> None: def ensure_invites_table() -> None:

View File

@@ -548,7 +548,10 @@
<div class="max-w-6xl w-full h-[90vh] rounded-2xl bg-white dark:bg-gray-900 border dark:border-gray-700 p-4 flex flex-col"> <div class="max-w-6xl w-full h-[90vh] rounded-2xl bg-white dark:bg-gray-900 border dark:border-gray-700 p-4 flex flex-col">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<div class="text-lg font-medium">Files</div> <div class="text-lg font-medium">Files</div>
<button class="dlgClose rounded-lg border px-2 py-1 text-xs dark:border-gray-600">Close</button> <div class="flex items-center gap-2">
<a href="/api/files/zip/${path_b64}" class="rounded-lg border px-2 py-1 text-xs dark:border-gray-600">Download All</a>
<button class="dlgClose rounded-lg border px-2 py-1 text-xs dark:border-gray-600">Close</button>
</div>
</div> </div>
<div class="flex-1 overflow-auto"> <div class="flex-1 overflow-auto">
${files.length ? `<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-4">` + files.map(it => { ${files.length ? `<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-4">` + files.map(it => {
@@ -571,7 +574,10 @@
wrap.innerHTML = html; wrap.innerHTML = html;
const dlg = wrap.firstElementChild; const dlg = wrap.firstElementChild;
document.body.appendChild(dlg); document.body.appendChild(dlg);
dlg.querySelectorAll('.dlgClose').forEach(b => b.onclick = () => { try { dlg.remove(); } catch {} });
const close = () => { try { dlg.remove(); } catch {} };
dlg.onclick = (e) => { if (e.target === e.currentTarget) close(); };
dlg.querySelectorAll('.dlgClose').forEach(b => b.onclick = close);
} }
btnFilesRefresh.onclick = loadDirs; btnFilesRefresh.onclick = loadDirs;