92 lines
4.6 KiB
Python
92 lines
4.6 KiB
Python
"""Render the review page.
|
|
|
|
The HTML/CSS/JS lives in review.html (loaded once at import). This module fills
|
|
in the %%TOKENS%%: the folder-tree sidebar, the image cards, totals. The server
|
|
contract is preserved: each .nsfw checkbox's value is the image's absolute path,
|
|
thumbnails load via GET /img?path=, actions POST to /apply /untag /tagged.
|
|
"""
|
|
import html
|
|
import os
|
|
from pathlib import Path
|
|
from urllib.parse import quote
|
|
|
|
# Inline Lucide-style SVG icons (no emoji, theme-able via currentColor).
|
|
_SVG_EXPAND = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3M8 21H5a2 2 0 0 1-2-2v-3m18 0v3a2 2 0 0 1-2 2h-3"/></svg>'
|
|
_SVG_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
|
|
|
|
_PAGE = (Path(__file__).parent / "review.html").read_text(encoding="utf-8")
|
|
|
|
|
|
def folder_tree_html(cands):
|
|
"""Indented folder list for the sidebar file browser, built from the candidate
|
|
paths. Each row filters the grid to that folder + its subfolders (prefix match);
|
|
'All folders' resets. Counts are images directly in that folder."""
|
|
folders = {}
|
|
for p, _ in cands:
|
|
folders[str(p.parent)] = folders.get(str(p.parent), 0) + 1
|
|
if not folders:
|
|
return ""
|
|
paths = sorted(folders)
|
|
root = os.path.commonpath(paths) if len(paths) > 1 else paths[0]
|
|
rows = [
|
|
f'<button type="button" class="fitem on" data-folder="" onclick="pickFolder(this)">'
|
|
f'<span class="fname">All folders</span><span class="fcount">{sum(folders.values())}</span></button>'
|
|
]
|
|
for folder in paths:
|
|
rel = os.path.relpath(folder, root)
|
|
depth = 0 if rel == "." else rel.count(os.sep) + 1
|
|
name = os.path.basename(folder) or folder
|
|
rows.append(
|
|
f'<button type="button" class="fitem" data-folder="{html.escape(folder.lower(), quote=True)}" '
|
|
f'style="padding-left:{depth * 14 + 12}px" title="{html.escape(folder, quote=True)}" '
|
|
f'onclick="pickFolder(this)"><span class="fname">{html.escape(name)}</span>'
|
|
f'<span class="fcount">{folders[folder]}</span></button>'
|
|
)
|
|
return "".join(rows)
|
|
|
|
|
|
def render_page(cands, threshold):
|
|
"""Build the review page. cands: list of (Path, score) sorted desc."""
|
|
cards, seen = [], set()
|
|
for p, score in cands:
|
|
sp = str(p)
|
|
if sp in seen: # never list the same path twice
|
|
continue
|
|
seen.add(sp)
|
|
# No pre-selection at render. The per-folder scan decides state so persisted
|
|
# marks win: nsfw -> pink, sfw -> green, undecided high-score -> red.
|
|
# (A dismissed/sfw image must never come back pre-selected red on reload.)
|
|
checked = ""
|
|
src = f"/img?path={quote(sp)}"
|
|
esc = html.escape(sp, quote=True)
|
|
name = html.escape(p.name)
|
|
folder = html.escape(str(p.parent))
|
|
folder_attr = html.escape(str(p.parent), quote=True)
|
|
name_low = html.escape(p.name.lower(), quote=True)
|
|
folder_low = html.escape(str(p.parent).lower(), quote=True)
|
|
cards.append(
|
|
f'<div class="card" data-score="{score:.4f}" data-name="{name_low}" '
|
|
f'data-folder="{folder_low}" data-hay="{name_low} {folder_low}" '
|
|
f'data-tagged="?" data-sfw="?">'
|
|
f'<div class="thumb">'
|
|
f'<img loading="lazy" src="{src}" alt="">'
|
|
f'<span class="score" data-s="{score:.4f}">{score:.2f}</span>'
|
|
f'<button type="button" class="icon zoom" aria-label="View full size" '
|
|
f'data-src="{src}" data-name="{name}" data-score="{score:.2f}" '
|
|
f'onclick="openLb(this)">{_SVG_EXPAND}</button>'
|
|
f'<span class="tagged" hidden>{_SVG_CHECK}<span>tagged</span></span>'
|
|
f'<span class="safe" hidden>{_SVG_CHECK}<span>tagged</span></span>'
|
|
f'<button type="button" class="tick" aria-label="Mark {name} NSFW or SFW" '
|
|
f'onclick="toggleCard(this)">{_SVG_CHECK}</button>'
|
|
f'<input type="checkbox" class="nsfw" value="{esc}" {checked} tabindex="-1" aria-hidden="true">'
|
|
f'</div>'
|
|
f'<div class="info"><div class="name" title="{esc}">{name}</div>'
|
|
f'<div class="path" title="{folder_attr}">{folder}</div></div>'
|
|
f'</div>'
|
|
)
|
|
return (_PAGE
|
|
.replace("%%TREE%%", folder_tree_html(cands))
|
|
.replace("%%CARDS%%", "".join(cards))
|
|
.replace("%%TOTAL%%", str(len(cards)))
|
|
.replace("%%THRESHOLD%%", f"{threshold:.2f}"))
|