Move InfoPanel from app.py to info_panel.py (no behavior change)
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.
This commit is contained in:
parent
9d99ecfcb5
commit
eded7790af
@ -51,177 +51,6 @@ from .settings import SettingsDialog
|
|||||||
log = logging.getLogger("booru")
|
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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -- Main App --
|
# -- Main App --
|
||||||
|
|
||||||
class BooruApp(QMainWindow):
|
class BooruApp(QMainWindow):
|
||||||
@ -3554,3 +3383,4 @@ def run() -> None:
|
|||||||
from .search_state import SearchState # re-export for refactor compat
|
from .search_state import SearchState # re-export for refactor compat
|
||||||
from .log_handler import LogHandler # re-export for refactor compat
|
from .log_handler import LogHandler # re-export for refactor compat
|
||||||
from .async_signals import AsyncSignals # re-export for refactor compat
|
from .async_signals import AsyncSignals # re-export for refactor compat
|
||||||
|
from .info_panel import InfoPanel # re-export for refactor compat
|
||||||
|
|||||||
186
booru_viewer/gui/info_panel.py
Normal file
186
booru_viewer/gui/info_panel.py
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
"""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()
|
||||||
Loading…
x
Reference in New Issue
Block a user