Categorized tags in info panel with color coding

- Artist (gold), Character (green), Copyright (purple), Species
  (red), General, Meta/Lore (gray)
- Danbooru and e621 provide categories from API
- Gelbooru/Moebooru fall back to flat tag list
This commit is contained in:
pax 2026-04-04 21:34:01 -05:00
parent 9f636532c0
commit b6c6a6222a
4 changed files with 80 additions and 10 deletions

View File

@ -25,6 +25,7 @@ class Post:
source: str | None
width: int = 0
height: int = 0
tag_categories: dict[str, list[str]] = field(default_factory=dict)
@property
def tag_list(self) -> list[str]:

View File

@ -51,6 +51,7 @@ class DanbooruClient(BooruClient):
source=item.get("source"),
width=item.get("image_width", 0),
height=item.get("image_height", 0),
tag_categories=self._extract_tag_categories(item),
)
)
return posts
@ -105,3 +106,19 @@ class DanbooruClient(BooruClient):
if key in item and item[key]:
parts.append(item[key])
return " ".join(parts) if parts else ""
@staticmethod
def _extract_tag_categories(item: dict) -> dict[str, list[str]]:
cats: dict[str, list[str]] = {}
mapping = {
"tag_string_artist": "Artist",
"tag_string_character": "Character",
"tag_string_copyright": "Copyright",
"tag_string_general": "General",
"tag_string_meta": "Meta",
}
for key, label in mapping.items():
val = item.get(key, "")
if val and val.strip():
cats[label] = val.split()
return cats

View File

@ -67,6 +67,7 @@ class E621Client(BooruClient):
source=self._get_source(item),
width=self._get_nested(item, "file", "width") or 0,
height=self._get_nested(item, "file", "height") or 0,
tag_categories=self._extract_tag_categories(item),
)
)
return posts
@ -156,6 +157,23 @@ class E621Client(BooruClient):
return tags_obj
return ""
@staticmethod
def _extract_tag_categories(item: dict) -> dict[str, list[str]]:
tags_obj = item.get("tags")
if not isinstance(tags_obj, dict):
return {}
cats: dict[str, list[str]] = {}
mapping = {
"artist": "Artist", "character": "Character",
"copyright": "Copyright", "species": "Species",
"general": "General", "meta": "Meta", "lore": "Lore",
}
for key, label in mapping.items():
tag_list = tags_obj.get(key, [])
if isinstance(tag_list, list) and tag_list:
cats[label] = tag_list
return cats
@staticmethod
def _get_score(item: dict) -> int:
"""e621 score is a dict with up/down/total."""

View File

@ -136,16 +136,50 @@ class InfoPanel(QWidget):
item = self._tags_flow.takeAt(0)
if item.widget():
item.widget().deleteLater()
# Add clickable tags
for tag in post.tag_list[:100]:
btn = QPushButton(tag)
btn.setFlat(True)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
btn.setStyleSheet(
"QPushButton { text-align: left; padding: 1px 4px; border: none; }"
)
btn.clicked.connect(lambda checked, t=tag: self.tag_clicked.emit(t))
self._tags_flow.addWidget(btn)
# Tag category colors
_CAT_COLORS = {
"Artist": "#f2ac08",
"Character": "#0a0",
"Copyright": "#c0f",
"Species": "#e44",
"General": "",
"Meta": "#888",
"Lore": "#888",
}
if post.tag_categories:
# Display tags grouped by category
for category, tags in post.tag_categories.items():
color = _CAT_COLORS.get(category, "")
header = QLabel(f"{category}:")
header.setStyleSheet(
f"font-weight: bold; margin-top: 6px; margin-bottom: 2px;"
+ (f" color: {color};" if color else "")
)
self._tags_flow.addWidget(header)
for tag in tags[:50]:
btn = QPushButton(tag)
btn.setFlat(True)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
style = "QPushButton { text-align: left; padding: 1px 4px; border: none;"
if color:
style += f" color: {color};"
style += " }"
btn.setStyleSheet(style)
btn.clicked.connect(lambda checked, t=tag: self.tag_clicked.emit(t))
self._tags_flow.addWidget(btn)
else:
# Fallback: flat tag list (Gelbooru, Moebooru)
for tag in post.tag_list[:100]:
btn = QPushButton(tag)
btn.setFlat(True)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
btn.setStyleSheet(
"QPushButton { text-align: left; padding: 1px 4px; border: none; }"
)
btn.clicked.connect(lambda checked, t=tag: self.tag_clicked.emit(t))
self._tags_flow.addWidget(btn)
self._tags_flow.addStretch()
def clear(self) -> None: