Step 11 of the gui/app.py + gui/preview.py structural refactor. Pure
copy: the toggleable info panel widget with category-coloured tag
list moves to its own module. The new module gets its own
`log = logging.getLogger("booru")` at module level — same logger
instance the rest of the app uses (logging.getLogger is idempotent
by name), matching the existing per-module convention used by
grid.py / bookmarks.py / library.py. All six tag-color Qt Properties
preserved verbatim. app.py grows another shim line. Shim removed
in commit 14.
187 lines
7.9 KiB
Python
187 lines
7.9 KiB
Python
"""Toggleable info panel showing post details with category-coloured tags."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from PySide6.QtCore import Qt, Property, Signal
|
|
from PySide6.QtGui import QColor
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QLabel, QScrollArea, QPushButton,
|
|
)
|
|
|
|
from ..core.api.base import Post
|
|
|
|
log = logging.getLogger("booru")
|
|
|
|
|
|
# -- Info Panel --
|
|
|
|
class InfoPanel(QWidget):
|
|
"""Toggleable panel showing post details."""
|
|
|
|
tag_clicked = Signal(str)
|
|
|
|
# Tag category colors. Defaults follow the booru convention (Danbooru,
|
|
# Gelbooru, etc.) so the panel reads naturally to anyone coming from a
|
|
# booru site. Each is exposed as a Qt Property so a custom.qss can
|
|
# override it via `qproperty-tag<Category>Color` selectors on
|
|
# `InfoPanel`. An empty string means "use the default text color"
|
|
# (the General category) and is preserved as a sentinel.
|
|
_tag_artist_color = QColor("#f2ac08")
|
|
_tag_character_color = QColor("#0a0")
|
|
_tag_copyright_color = QColor("#c0f")
|
|
_tag_species_color = QColor("#e44")
|
|
_tag_meta_color = QColor("#888")
|
|
_tag_lore_color = QColor("#888")
|
|
|
|
def _get_artist(self): return self._tag_artist_color
|
|
def _set_artist(self, c): self._tag_artist_color = QColor(c) if isinstance(c, str) else c
|
|
tagArtistColor = Property(QColor, _get_artist, _set_artist)
|
|
|
|
def _get_character(self): return self._tag_character_color
|
|
def _set_character(self, c): self._tag_character_color = QColor(c) if isinstance(c, str) else c
|
|
tagCharacterColor = Property(QColor, _get_character, _set_character)
|
|
|
|
def _get_copyright(self): return self._tag_copyright_color
|
|
def _set_copyright(self, c): self._tag_copyright_color = QColor(c) if isinstance(c, str) else c
|
|
tagCopyrightColor = Property(QColor, _get_copyright, _set_copyright)
|
|
|
|
def _get_species(self): return self._tag_species_color
|
|
def _set_species(self, c): self._tag_species_color = QColor(c) if isinstance(c, str) else c
|
|
tagSpeciesColor = Property(QColor, _get_species, _set_species)
|
|
|
|
def _get_meta(self): return self._tag_meta_color
|
|
def _set_meta(self, c): self._tag_meta_color = QColor(c) if isinstance(c, str) else c
|
|
tagMetaColor = Property(QColor, _get_meta, _set_meta)
|
|
|
|
def _get_lore(self): return self._tag_lore_color
|
|
def _set_lore(self, c): self._tag_lore_color = QColor(c) if isinstance(c, str) else c
|
|
tagLoreColor = Property(QColor, _get_lore, _set_lore)
|
|
|
|
def _category_color(self, category: str) -> str:
|
|
"""Resolve a category name to a hex color string for inline QSS use.
|
|
Returns "" for the General category (no override → use default text
|
|
color) and unrecognized categories (so callers can render them with
|
|
no color attribute set)."""
|
|
cat = (category or "").lower()
|
|
m = {
|
|
"artist": self._tag_artist_color,
|
|
"character": self._tag_character_color,
|
|
"copyright": self._tag_copyright_color,
|
|
"species": self._tag_species_color,
|
|
"meta": self._tag_meta_color,
|
|
"lore": self._tag_lore_color,
|
|
}
|
|
c = m.get(cat)
|
|
return c.name() if c is not None else ""
|
|
|
|
def __init__(self, parent: QWidget | None = None) -> None:
|
|
super().__init__(parent)
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(6, 6, 6, 6)
|
|
|
|
self._title = QLabel("No post selected")
|
|
self._title.setStyleSheet("font-weight: bold;")
|
|
layout.addWidget(self._title)
|
|
|
|
self._details = QLabel()
|
|
self._details.setWordWrap(True)
|
|
self._details.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextBrowserInteraction)
|
|
self._details.setMaximumHeight(120)
|
|
layout.addWidget(self._details)
|
|
|
|
self._tags_label = QLabel("Tags:")
|
|
self._tags_label.setStyleSheet("font-weight: bold; margin-top: 8px;")
|
|
layout.addWidget(self._tags_label)
|
|
|
|
self._tags_scroll = QScrollArea()
|
|
self._tags_scroll.setWidgetResizable(True)
|
|
self._tags_scroll.setStyleSheet("QScrollArea { border: none; }")
|
|
self._tags_widget = QWidget()
|
|
self._tags_flow = QVBoxLayout(self._tags_widget)
|
|
self._tags_flow.setContentsMargins(0, 0, 0, 0)
|
|
self._tags_flow.setSpacing(2)
|
|
self._tags_scroll.setWidget(self._tags_widget)
|
|
layout.addWidget(self._tags_scroll, stretch=1)
|
|
|
|
def set_post(self, post: Post) -> None:
|
|
log.debug(f"InfoPanel: tag_categories={list(post.tag_categories.keys()) if post.tag_categories else 'empty'}")
|
|
self._title.setText(f"Post #{post.id}")
|
|
filetype = Path(post.file_url.split("?")[0]).suffix.lstrip(".").upper() if post.file_url else "unknown"
|
|
source = post.source or "none"
|
|
# Truncate display text but keep full URL for the link
|
|
source_full = source
|
|
if len(source) > 60:
|
|
source_display = source[:57] + "..."
|
|
else:
|
|
source_display = source
|
|
if source_full.startswith(("http://", "https://")):
|
|
source_html = f'<a href="{source_full}" style="color: #4fc3f7;">{source_display}</a>'
|
|
else:
|
|
source_html = source_display
|
|
from html import escape
|
|
self._details.setText(
|
|
f"Score: {post.score}\n"
|
|
f"Rating: {post.rating or 'unknown'}\n"
|
|
f"Filetype: {filetype}"
|
|
)
|
|
self._details.setTextFormat(Qt.TextFormat.RichText)
|
|
self._details.setText(
|
|
f"Score: {post.score}<br>"
|
|
f"Rating: {escape(post.rating or 'unknown')}<br>"
|
|
f"Filetype: {filetype}<br>"
|
|
f"Source: {source_html}"
|
|
)
|
|
self._details.setOpenExternalLinks(True)
|
|
# Clear old tags
|
|
while self._tags_flow.count():
|
|
item = self._tags_flow.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
if post.tag_categories:
|
|
# Display tags grouped by category. Colors come from the
|
|
# tag*Color Qt Properties so a custom.qss can override any of
|
|
# them via `InfoPanel { qproperty-tagCharacterColor: ...; }`.
|
|
for category, tags in post.tag_categories.items():
|
|
color = self._category_color(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:
|
|
self._title.setText("No post selected")
|
|
self._details.setText("")
|
|
while self._tags_flow.count():
|
|
item = self._tags_flow.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|