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:
34
app/app.py
34
app/app.py
@@ -20,6 +20,7 @@ import sqlite3
|
||||
import binascii
|
||||
import base64
|
||||
import mimetypes
|
||||
import zipfile
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
@@ -28,7 +29,7 @@ import math
|
||||
import logging
|
||||
import httpx
|
||||
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.staticfiles import StaticFiles
|
||||
from starlette.websockets import WebSocketState
|
||||
@@ -1385,6 +1386,37 @@ async def api_files_full(path_b64: str, request: Request):
|
||||
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) ----------
|
||||
|
||||
def ensure_invites_table() -> None:
|
||||
|
||||
@@ -548,8 +548,11 @@
|
||||
<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="text-lg font-medium">Files</div>
|
||||
<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 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 => {
|
||||
const thumbUrl = it.is_image ? `/api/files/thumb/${it.path_b64}` : '';
|
||||
@@ -571,7 +574,10 @@
|
||||
wrap.innerHTML = html;
|
||||
const dlg = wrap.firstElementChild;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user