From b6c6a6222a05bcb3f5de6131bbb74268a7a1e9d7 Mon Sep 17 00:00:00 2001 From: pax Date: Sat, 4 Apr 2026 21:34:01 -0500 Subject: [PATCH] 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 --- booru_viewer/core/api/base.py | 1 + booru_viewer/core/api/danbooru.py | 17 ++++++++++ booru_viewer/core/api/e621.py | 18 +++++++++++ booru_viewer/gui/app.py | 54 +++++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/booru_viewer/core/api/base.py b/booru_viewer/core/api/base.py index 036f517..62c3fd7 100644 --- a/booru_viewer/core/api/base.py +++ b/booru_viewer/core/api/base.py @@ -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]: diff --git a/booru_viewer/core/api/danbooru.py b/booru_viewer/core/api/danbooru.py index 4d4d832..02b5ba2 100644 --- a/booru_viewer/core/api/danbooru.py +++ b/booru_viewer/core/api/danbooru.py @@ -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 diff --git a/booru_viewer/core/api/e621.py b/booru_viewer/core/api/e621.py index d0142a9..b465ea1 100644 --- a/booru_viewer/core/api/e621.py +++ b/booru_viewer/core/api/e621.py @@ -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.""" diff --git a/booru_viewer/gui/app.py b/booru_viewer/gui/app.py index 4f183d8..0ee7d8f 100644 --- a/booru_viewer/gui/app.py +++ b/booru_viewer/gui/app.py @@ -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: