feat(model-manager): generated-photo Gallery + device-routing landing
- Gallery view: grid of generated photos from ComfyUI's output/, full-size
lightbox, and permanent delete (with confirm). Paginated ("Load more").
- Backend: GET /api/gallery, GET /gallery/file (path-guarded image serve),
DELETE /api/gallery (path-guarded; clear error on permission denial).
- Mount ./output read-write into model-manager so the gallery can delete.
- Device-routing landing at /start: phones -> ComfyUIMini, desktops ->
the Gallery; ?force=mobile|desktop overrides. Ports come from the new
/api/ui-config (COMFYUI_PORT / COMFYUIMINI_PORT env).
- Responsive tweaks so the gallery is usable if opened directly on a phone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,16 @@ from pydantic import BaseModel
|
||||
|
||||
from . import db, downloader, registries
|
||||
from .config import (
|
||||
COMFYUI_PORT,
|
||||
COMFYUIMINI_PORT,
|
||||
IMAGE_EXTS,
|
||||
KEY_BY_FOLDER,
|
||||
MODEL_TYPES,
|
||||
MODELS_DIR,
|
||||
OUTPUT_DIR,
|
||||
TYPE_BY_KEY,
|
||||
ensure_dirs,
|
||||
safe_output_path,
|
||||
)
|
||||
|
||||
STATIC_DIR = Path(__file__).parent / "static"
|
||||
@@ -214,6 +219,78 @@ def delete_download(download_id: int) -> dict:
|
||||
return {"removed": download_id}
|
||||
|
||||
|
||||
# ---- gallery (generated photos) -------------------------------------------
|
||||
|
||||
@app.get("/api/gallery")
|
||||
def list_gallery(limit: int = 60, offset: int = 0) -> dict:
|
||||
"""List generated images under OUTPUT_DIR, newest first, paginated."""
|
||||
files: list[dict] = []
|
||||
if OUTPUT_DIR.is_dir():
|
||||
for path in OUTPUT_DIR.rglob("*"):
|
||||
if not path.is_file() or path.suffix.lower() not in IMAGE_EXTS:
|
||||
continue
|
||||
rel = path.relative_to(OUTPUT_DIR).as_posix()
|
||||
stat = path.stat()
|
||||
files.append({
|
||||
"path": rel,
|
||||
"name": path.name,
|
||||
"subfolder": path.parent.relative_to(OUTPUT_DIR).as_posix()
|
||||
if path.parent != OUTPUT_DIR else "",
|
||||
"size": stat.st_size,
|
||||
"mtime": stat.st_mtime,
|
||||
})
|
||||
files.sort(key=lambda f: f["mtime"], reverse=True)
|
||||
total = len(files)
|
||||
page = files[offset:offset + max(1, min(limit, 500))]
|
||||
return {"items": page, "total": total,
|
||||
"offset": offset, "returned": len(page)}
|
||||
|
||||
|
||||
@app.get("/gallery/file")
|
||||
def gallery_file(path: str):
|
||||
try:
|
||||
target = safe_output_path(path)
|
||||
except ValueError:
|
||||
raise HTTPException(400, "Invalid path")
|
||||
if not target.is_file() or target.suffix.lower() not in IMAGE_EXTS:
|
||||
raise HTTPException(404, "Image not found")
|
||||
return FileResponse(target)
|
||||
|
||||
|
||||
class DeletePhotoIn(BaseModel):
|
||||
path: str
|
||||
|
||||
|
||||
@app.delete("/api/gallery")
|
||||
def delete_photo(body: DeletePhotoIn) -> dict:
|
||||
try:
|
||||
target = safe_output_path(body.path)
|
||||
except ValueError:
|
||||
raise HTTPException(400, "Invalid path")
|
||||
if not target.is_file():
|
||||
raise HTTPException(404, "Image not found")
|
||||
try:
|
||||
target.unlink()
|
||||
except PermissionError:
|
||||
raise HTTPException(
|
||||
403,
|
||||
"Permission denied (file is in a folder owned by another user). "
|
||||
"Run ComfyUI as the same UID, or remove it from a host shell.")
|
||||
return {"deleted": body.path}
|
||||
|
||||
|
||||
# ---- device routing / ui config -------------------------------------------
|
||||
|
||||
@app.get("/api/ui-config")
|
||||
def ui_config() -> dict:
|
||||
return {"comfyui_port": COMFYUI_PORT, "comfyuimini_port": COMFYUIMINI_PORT}
|
||||
|
||||
|
||||
@app.get("/start")
|
||||
def start() -> FileResponse:
|
||||
return FileResponse(STATIC_DIR / "start.html")
|
||||
|
||||
|
||||
# ---- static UI ------------------------------------------------------------
|
||||
|
||||
@app.get("/")
|
||||
|
||||
Reference in New Issue
Block a user