From: Danilo M. Date: Sun, 3 May 2026 08:32:57 +0000 (+0200) Subject: feat: article detail panel with frontmatter display and markdown preview X-Git-Tag: v1.0~16 X-Git-Url: https://git.danix.xyz/?a=commitdiff_plain;h=65bcddd895b4b61f41e96167658bfa491598387c;p=publisher.git feat: article detail panel with frontmatter display and markdown preview --- diff --git a/ui/article_detail.py b/ui/article_detail.py new file mode 100644 index 0000000..77dadb7 --- /dev/null +++ b/ui/article_detail.py @@ -0,0 +1,132 @@ +from __future__ import annotations +import subprocess +import re +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QPushButton, QTextBrowser, QScrollArea, QSizePolicy, QFrame, +) +from PyQt6.QtCore import pyqtSignal, Qt +from core.models import Article +from core.frontmatter import parse_frontmatter + +def _strip_shortcodes(text: str) -> str: + return re.sub(r'\{\{[^}]+\}\}', '[shortcode]', text) + +class ArticleDetailView(QWidget): + open_typora = pyqtSignal(object) # Article + open_frontmatter = pyqtSignal(object) # Article + translate = pyqtSignal(object) # Article + push_master = pyqtSignal(object) # Article + publish = pyqtSignal(object) # Article + delete_article = pyqtSignal(object) # Article + + def __init__(self, parent=None): + super().__init__(parent) + self._article: Article | None = None + self._build_ui() + + def _build_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + # Header + header = QWidget() + header.setStyleSheet("background: #0f0f1a; border-bottom: 1px solid #2a2a4e;") + h_layout = QHBoxLayout(header) + h_layout.setContentsMargins(12, 8, 12, 8) + + self._title_label = QLabel("") + self._title_label.setStyleSheet("color: #fff; font-weight: bold; font-size: 13px;") + self._path_label = QLabel("") + self._path_label.setStyleSheet("color: #555; font-size: 10px;") + info = QVBoxLayout() + info.addWidget(self._title_label) + info.addWidget(self._path_label) + h_layout.addLayout(info, stretch=1) + + for (icon, signal_name, style) in [ + ("✏️ Typora", "open_typora", "color:#a855f7;border:1px solid #a855f7;"), + ("🔧 Frontmatter", "open_frontmatter", "color:#888;border:1px solid #444;"), + ("🌍 Traduci", "translate", "color:#f59e0b;border:1px solid #f59e0b;"), + ("🧪 Push master", "push_master", "color:#60a5fa;border:1px solid #60a5fa;"), + ("🚢 Pubblica", "publish", "color:#00ff88;border:1px solid #00ff88;"), + ("🗑 Elimina", "delete_article", "color:#ff6b6b;border:1px solid #ff6b6b;"), + ]: + btn = QPushButton(icon) + btn.setStyleSheet(f"background:#1a1a2e;{style}padding:4px 10px;border-radius:4px;font-size:10px;") + btn.clicked.connect(lambda _, s=signal_name: getattr(self, s).emit(self._article)) + h_layout.addWidget(btn) + + layout.addWidget(header) + + # Body: split columns + body = QHBoxLayout() + body.setContentsMargins(0, 0, 0, 0) + body.setSpacing(0) + + # Frontmatter column + self._fm_widget = QWidget() + self._fm_widget.setFixedWidth(230) + self._fm_widget.setStyleSheet("background:#0a0a12;") + self._fm_layout = QVBoxLayout(self._fm_widget) + self._fm_layout.setContentsMargins(10, 10, 10, 10) + fm_title = QLabel("Frontmatter") + fm_title.setStyleSheet("color:#a855f7;font-size:9px;text-transform:uppercase;letter-spacing:1px;") + self._fm_layout.addWidget(fm_title) + self._fm_content = QVBoxLayout() + self._fm_layout.addLayout(self._fm_content) + self._fm_layout.addStretch() + body.addWidget(self._fm_widget) + + divider = QFrame() + divider.setFrameShape(QFrame.Shape.VLine) + divider.setStyleSheet("color:#1a1a2e;") + body.addWidget(divider) + + # Preview column + preview_container = QWidget() + preview_layout = QVBoxLayout(preview_container) + preview_layout.setContentsMargins(12, 10, 12, 10) + preview_title = QLabel("Preview markdown") + preview_title.setStyleSheet("color:#a855f7;font-size:9px;text-transform:uppercase;letter-spacing:1px;") + preview_layout.addWidget(preview_title) + self._preview = QTextBrowser() + self._preview.setStyleSheet("background:#0d0d1a;color:#ccc;border:none;") + self._preview.setOpenExternalLinks(True) + preview_layout.addWidget(self._preview) + body.addWidget(preview_container, stretch=1) + + layout.addLayout(body, stretch=1) + + def set_article(self, article: Article): + self._article = article + self._title_label.setText(article.slug) + rel = str(article.path).replace(str(article.path.parent.parent.parent.parent), "") + self._path_label.setText(rel) + + # Populate frontmatter + while self._fm_content.count(): + item = self._fm_content.takeAt(0) + if item.widget(): + item.widget().deleteLater() + + for key, val in article.frontmatter.items(): + row = QVBoxLayout() + key_lbl = QLabel(key) + key_lbl.setStyleSheet("color:#555;font-size:9px;") + val_lbl = QLabel(str(val) if not isinstance(val, list) else ", ".join(str(v) for v in val)) + val_lbl.setStyleSheet("color:#e2e8f0;font-size:11px;") + val_lbl.setWordWrap(True) + row.addWidget(key_lbl) + row.addWidget(val_lbl) + container = QWidget() + container.setLayout(row) + self._fm_content.addWidget(container) + + # Load and render markdown preview + try: + _, body = parse_frontmatter(article.path) + clean = _strip_shortcodes(body) + self._preview.setMarkdown(clean) + except Exception as e: + self._preview.setPlainText(f"[Errore caricamento: {e}]")