feat(model-manager): add "Delete all" gallery button + clearer 401 errors
- Gallery: DELETE /api/gallery/all removes every image under output/; "Delete all" button with in-app confirm and a deleted/failed count. - Downloads: surface a clear, actionable message when CivitAI/HuggingFace returns 401/403 (model requires login/early-access, or the key/token lacks access) instead of a bare error, both at resolve time and during the download stream. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -236,7 +236,8 @@ SparkyUI includes a **StabilityMatrix-style Model Manager** - a lightweight Fast
|
|||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- **Gallery** - browse generated photos from ComfyUI's `output/` in a large desktop grid,
|
- **Gallery** - browse generated photos from ComfyUI's `output/` in a large desktop grid,
|
||||||
click for a full-size lightbox view, and **permanently delete** photos (with confirm).
|
click for a full-size lightbox view, and **permanently delete** photos one at a time or
|
||||||
|
all at once (with confirm).
|
||||||
- **Browse CivitAI** - search the CivitAI catalog in a thumbnail grid (filter by type,
|
- **Browse CivitAI** - search the CivitAI catalog in a thumbnail grid (filter by type,
|
||||||
sort, period, NSFW toggle) and **click a model to download it** - no URL pasting needed.
|
sort, period, NSFW toggle) and **click a model to download it** - no URL pasting needed.
|
||||||
Multi-version models get a version picker on the card.
|
Multi-version models get a version picker on the card.
|
||||||
|
|||||||
@@ -111,8 +111,11 @@ async def run_download(download_id: int, url: str, headers: dict[str, str],
|
|||||||
db.update_download(download_id, status="canceled", error="Canceled by user")
|
db.update_download(download_id, status="canceled", error="Canceled by user")
|
||||||
_cleanup(part_path)
|
_cleanup(part_path)
|
||||||
except httpx.HTTPStatusError as exc:
|
except httpx.HTTPStatusError as exc:
|
||||||
db.update_download(download_id, status="failed",
|
code = exc.response.status_code
|
||||||
error=f"HTTP {exc.response.status_code}")
|
msg = f"HTTP {code}"
|
||||||
|
if code in (401, 403):
|
||||||
|
msg += " — requires login/early-access or invalid API key"
|
||||||
|
db.update_download(download_id, status="failed", error=msg)
|
||||||
_cleanup(part_path)
|
_cleanup(part_path)
|
||||||
except Exception as exc: # noqa: BLE001 - surface any failure to the UI
|
except Exception as exc: # noqa: BLE001 - surface any failure to the UI
|
||||||
db.update_download(download_id, status="failed", error=str(exc))
|
db.update_download(download_id, status="failed", error=str(exc))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
@@ -141,6 +142,15 @@ async def start_download(body: DownloadIn) -> dict:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
resolved = await registries.resolve(url)
|
resolved = await registries.resolve(url)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
code = exc.response.status_code
|
||||||
|
if code in (401, 403):
|
||||||
|
raise HTTPException(
|
||||||
|
400,
|
||||||
|
f"The source returned {code} for this model. It likely requires "
|
||||||
|
"being logged in / early access on that site, or your API key/token "
|
||||||
|
"doesn't have access to it. (Public models download fine.)")
|
||||||
|
raise HTTPException(400, f"Could not resolve URL (HTTP {code})")
|
||||||
except Exception as exc: # noqa: BLE001 - report resolution failure to the user
|
except Exception as exc: # noqa: BLE001 - report resolution failure to the user
|
||||||
raise HTTPException(400, f"Could not resolve URL: {exc}")
|
raise HTTPException(400, f"Could not resolve URL: {exc}")
|
||||||
|
|
||||||
@@ -246,6 +256,22 @@ def list_gallery(limit: int = 60, offset: int = 0) -> dict:
|
|||||||
"offset": offset, "returned": len(page)}
|
"offset": offset, "returned": len(page)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/gallery/all")
|
||||||
|
def delete_all_photos() -> dict:
|
||||||
|
"""Permanently delete every image under OUTPUT_DIR."""
|
||||||
|
deleted = 0
|
||||||
|
failed = 0
|
||||||
|
if OUTPUT_DIR.is_dir():
|
||||||
|
for path in OUTPUT_DIR.rglob("*"):
|
||||||
|
if path.is_file() and path.suffix.lower() in IMAGE_EXTS:
|
||||||
|
try:
|
||||||
|
path.unlink()
|
||||||
|
deleted += 1
|
||||||
|
except OSError:
|
||||||
|
failed += 1
|
||||||
|
return {"deleted": deleted, "failed": failed}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/gallery/file")
|
@app.get("/gallery/file")
|
||||||
def gallery_file(path: str):
|
def gallery_file(path: str):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -376,7 +376,28 @@ document.addEventListener("keydown", (e) => {
|
|||||||
$("#lbDelete").addEventListener("click", () => {
|
$("#lbDelete").addEventListener("click", () => {
|
||||||
if (lbCurrent) deletePhoto(lbCurrent.p, lbCurrent.card);
|
if (lbCurrent) deletePhoto(lbCurrent.p, lbCurrent.card);
|
||||||
});
|
});
|
||||||
|
async function deleteAllPhotos() {
|
||||||
|
if (!galleryState.total) {
|
||||||
|
toast($("#galleryToast"), "No photos to delete", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = await confirmDialog(
|
||||||
|
`Permanently delete ALL ${galleryState.total} photo(s)? This cannot be undone.`,
|
||||||
|
"Delete all");
|
||||||
|
if (!ok) return;
|
||||||
|
try {
|
||||||
|
const res = await api("/api/gallery/all", { method: "DELETE" });
|
||||||
|
let msg = `Deleted ${res.deleted} photo(s)`;
|
||||||
|
if (res.failed) msg += ` (${res.failed} could not be removed — permissions)`;
|
||||||
|
toast($("#galleryToast"), msg, res.failed > 0);
|
||||||
|
loadGallery(true);
|
||||||
|
} catch (err) {
|
||||||
|
toast($("#galleryToast"), err.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$("#refreshGallery").addEventListener("click", () => loadGallery(true));
|
$("#refreshGallery").addEventListener("click", () => loadGallery(true));
|
||||||
|
$("#deleteAllGallery").addEventListener("click", deleteAllPhotos);
|
||||||
$("#galleryMore").addEventListener("click", () => loadGallery(false));
|
$("#galleryMore").addEventListener("click", () => loadGallery(false));
|
||||||
|
|
||||||
// ---- browse civitai -------------------------------------------------------
|
// ---- browse civitai -------------------------------------------------------
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
<span class="form-msg" id="galleryToast"></span>
|
<span class="form-msg" id="galleryToast"></span>
|
||||||
<button class="btn" id="refreshGallery">Refresh</button>
|
<button class="btn" id="refreshGallery">Refresh</button>
|
||||||
|
<button class="btn danger" id="deleteAllGallery">Delete all</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div id="galleryGrid" class="gallery-grid">
|
<div id="galleryGrid" class="gallery-grid">
|
||||||
|
|||||||
Reference in New Issue
Block a user