feat: show i.O. photos in review, lightbox on thumbnail click

This commit is contained in:
Ferdinand
2026-04-08 10:40:07 +02:00
parent 8a80021983
commit 35dccd4f1b
2 changed files with 98 additions and 9 deletions

View File

@@ -50,6 +50,17 @@
.stat { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #222; font-size: 0.95rem; }
.stat-value { font-weight: 700; color: #e94560; }
.hint { margin-top: 1rem; color: #888; font-size: 0.85rem; }
.section-divider { font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #555; margin: 1.2rem 0 0.5rem; }
.badge-reject { font-size: 0.75rem; font-weight: 600; color: #e94560; background: rgba(233,69,96,0.12); border-radius: 4px; padding: 0.15rem 0.45rem; margin-left: 0.4rem; }
.badge-ok { font-size: 0.75rem; font-weight: 600; color: #4caf7d; background: rgba(76,175,125,0.12); border-radius: 4px; padding: 0.15rem 0.45rem; margin-left: 0.4rem; }
.photo-item img { cursor: zoom-in; }
/* Lightbox */
.lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.88); z-index: 1000; align-items: center; justify-content: center; }
.lightbox.open { display: flex; }
.lightbox img { max-width: 90vw; max-height: 90vh; border-radius: 8px; object-fit: contain; box-shadow: 0 8px 40px rgba(0,0,0,0.6); }
.lightbox-close { position: absolute; top: 1.2rem; right: 1.5rem; font-size: 2rem; color: #fff; cursor: pointer; line-height: 1; opacity: 0.7; }
.lightbox-close:hover { opacity: 1; }
.lightbox-name { position: absolute; bottom: 1.5rem; color: #ccc; font-size: 0.9rem; }
</style>
</head>
<body>
@@ -135,11 +146,28 @@
<button class="primary" id="restart-btn">Neuen Ordner analysieren</button>
</div>
<!-- Lightbox -->
<div class="lightbox" id="lightbox">
<span class="lightbox-close" id="lightbox-close">&times;</span>
<img id="lightbox-img" src="" alt="">
<span class="lightbox-name" id="lightbox-name"></span>
</div>
<script>
// --- State ---
let analysisResults = [];
let okPaths = [];
let currentFolder = "";
// --- Lightbox ---
function openLightbox(src, name) {
el("lightbox-img").src = src;
el("lightbox-name").textContent = name;
el("lightbox").classList.add("open");
}
el("lightbox-close").addEventListener("click", () => el("lightbox").classList.remove("open"));
el("lightbox").addEventListener("click", e => { if (e.target === el("lightbox")) el("lightbox").classList.remove("open"); });
// --- Helpers ---
function showView(id) {
document.querySelectorAll(".view").forEach(v => v.classList.remove("active"));
@@ -207,6 +235,7 @@
}
analysisResults = data.results;
okPaths = data.ok_paths || [];
if (analysisResults.length === 0) {
renderResult(0);
@@ -222,10 +251,27 @@
});
// --- Review ---
function makeThumb(path, name) {
const img = document.createElement("img");
img.src = "/preview?path=" + encodeURIComponent(path);
img.alt = name;
img.onerror = function() { this.style.display = "none"; };
img.addEventListener("click", () => openLightbox(img.src, name));
return img;
}
function renderReview() {
const list = el("review-list");
list.textContent = "";
// --- Aussortierte Fotos ---
if (analysisResults.length > 0) {
const div = document.createElement("div");
div.className = "section-divider";
div.textContent = "Aussortieren (" + analysisResults.length + ")";
list.appendChild(div);
}
analysisResults.forEach((item, idx) => {
const name = item.path.split("/").pop();
@@ -233,11 +279,6 @@
row.className = "photo-item";
row.id = "item-" + idx;
const img = document.createElement("img");
img.src = "/preview?path=" + encodeURIComponent(item.path);
img.alt = name;
img.onerror = function() { this.style.display = "none"; };
const info = document.createElement("div");
info.className = "photo-info";
@@ -245,6 +286,11 @@
nameEl.className = "photo-name";
nameEl.textContent = name;
const badge = document.createElement("span");
badge.className = "badge-reject";
badge.textContent = "aussortieren";
nameEl.appendChild(badge);
const reasonsEl = document.createElement("div");
reasonsEl.className = "photo-reasons";
reasonsEl.textContent = item.reasons.join(", ");
@@ -257,14 +303,48 @@
btn.textContent = "Behalten";
btn.addEventListener("click", () => {
row.classList.toggle("kept");
btn.textContent = row.classList.contains("kept") ? "Aussortieren" : "Behalten";
const isKept = row.classList.contains("kept");
btn.textContent = isKept ? "Aussortieren" : "Behalten";
badge.textContent = isKept ? "i.O." : "aussortieren";
badge.className = isKept ? "badge-ok" : "badge-reject";
});
row.appendChild(img);
row.appendChild(makeThumb(item.path, name));
row.appendChild(info);
row.appendChild(btn);
list.appendChild(row);
});
// --- i.O. Fotos ---
if (okPaths.length > 0) {
const div = document.createElement("div");
div.className = "section-divider";
div.textContent = "Behalten i.O. (" + okPaths.length + ")";
list.appendChild(div);
okPaths.forEach(path => {
const name = path.split("/").pop();
const row = document.createElement("div");
row.className = "photo-item";
const info = document.createElement("div");
info.className = "photo-info";
const nameEl = document.createElement("div");
nameEl.className = "photo-name";
nameEl.textContent = name;
const badge = document.createElement("span");
badge.className = "badge-ok";
badge.textContent = "i.O.";
nameEl.appendChild(badge);
info.appendChild(nameEl);
row.appendChild(makeThumb(path, name));
row.appendChild(info);
list.appendChild(row);
});
}
}
// --- Confirm & Move ---
@@ -323,7 +403,8 @@
stats.appendChild(row);
}
addStat("Analysierte Fotos", analysisResults.length);
addStat("Analysierte Fotos", analysisResults.length + okPaths.length);
addStat("Behalten (i.O.)", okPaths.length);
addStat("Aussortiert", movedCount);
Object.entries(byReason).forEach(([reason, count]) => {
addStat(" davon: " + reason, count);

View File

@@ -86,7 +86,15 @@ def analyze(req: AnalyzeRequest):
use_ai=req.use_ai,
api_key=api_key,
)
return {"results": results}
from analyzer import SUPPORTED_EXTENSIONS
all_paths = {
os.path.join(req.folder, f)
for f in os.listdir(req.folder)
if os.path.splitext(f)[1].lower() in SUPPORTED_EXTENSIONS
}
flagged_paths = {item["path"] for item in results}
ok_paths = sorted(all_paths - flagged_paths)
return {"results": results, "ok_paths": ok_paths}
@app.post("/move")