from __future__ import annotations from pathlib import Path from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QDialog, QFormLayout, QLineEdit, QMessageBox, QHeaderView, ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QColor from core.taxonomy import TaxonomyModel, load_taxonomy, save_taxonomy, load_categories, save_categories from core.article_scanner import scan_articles from core.frontmatter import parse_frontmatter, write_frontmatter class AddTermDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Aggiungi termine") layout = QVBoxLayout(self) form = QFormLayout() self._it = QLineEdit() self._en = QLineEdit() form.addRow("IT:", self._it) form.addRow("EN:", self._en) layout.addLayout(form) btns = QHBoxLayout() ok = QPushButton("Aggiungi") ok.clicked.connect(self.accept) cancel = QPushButton("Annulla") cancel.clicked.connect(self.reject) btns.addStretch() btns.addWidget(cancel) btns.addWidget(ok) layout.addLayout(btns) @property def it_term(self) -> str: return self._it.text().strip() @property def en_term(self) -> str: return self._en.text().strip() class TaxonomyView(QWidget): def __init__(self, blog_root: Path, parent=None): super().__init__(parent) self._blog_root = blog_root self._tags_it = blog_root / "docs" / "tags-it.txt" self._tags_en = blog_root / "docs" / "tags-en.txt" self._categories_path = blog_root / "docs" / "categories.txt" self._model: TaxonomyModel | None = None self._categories: list[str] = [] self._build_ui() self.load() def _build_ui(self): layout = QVBoxLayout(self) tabs = QTabWidget() tags_tab = QWidget() tl = QVBoxLayout(tags_tab) self._tags_table = QTableWidget(0, 2) self._tags_table.setHorizontalHeaderLabels(["๐Ÿ‡ฎ๐Ÿ‡น Italiano", "๐Ÿ‡ฌ๐Ÿ‡ง English"]) self._tags_table.horizontalHeader().setStretchLastSection(True) tl.addWidget(self._tags_table) tag_btns = QHBoxLayout() add_tag_btn = QPushButton("โž• Aggiungi termine") add_tag_btn.clicked.connect(self._add_term) save_tags_btn = QPushButton("๐Ÿ’พ Salva") save_tags_btn.clicked.connect(self._save) tag_btns.addWidget(add_tag_btn) tag_btns.addStretch() tag_btns.addWidget(save_tags_btn) tl.addLayout(tag_btns) tabs.addTab(tags_tab, "๐Ÿท Tags") cats_tab = QWidget() cl = QVBoxLayout(cats_tab) self._cats_table = QTableWidget(0, 2) self._cats_table.setHorizontalHeaderLabels(["Categoria", "Articoli"]) self._cats_table.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection) self._cats_table.horizontalHeader().setStretchLastSection(False) self._cats_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) self._cats_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) cl.addWidget(self._cats_table) cat_btns = QHBoxLayout() add_cat_btn = QPushButton("โž• Aggiungi") add_cat_btn.clicked.connect(self._add_category) remove_cat_btn = QPushButton("๐Ÿ—‘ Rimuovi") remove_cat_btn.clicked.connect(self._remove_category) save_cats_btn = QPushButton("๐Ÿ’พ Salva") save_cats_btn.clicked.connect(self._save_categories) cat_btns.addWidget(add_cat_btn) cat_btns.addWidget(remove_cat_btn) cat_btns.addStretch() cat_btns.addWidget(save_cats_btn) cl.addLayout(cat_btns) tabs.addTab(cats_tab, "๐Ÿ“‚ Categorie") layout.addWidget(tabs) self._status = QLabel("") self._status.setStyleSheet("color:#888;font-size:10px;padding:4px;") layout.addWidget(self._status) def load(self): if not self._tags_it.exists(): self._status.setText("tags-it.txt non trovato.") else: self._model = load_taxonomy(self._tags_it, self._tags_en) self._populate_table() self._categories = load_categories(self._categories_path) self._populate_cats_table() def _populate_table(self): if not self._model: return self._tags_table.setRowCount(0) for it_term, en_term in sorted(self._model.it_to_en.items()): row = self._tags_table.rowCount() self._tags_table.insertRow(row) self._tags_table.setItem(row, 0, QTableWidgetItem(it_term)) self._tags_table.setItem(row, 1, QTableWidgetItem(en_term)) for orphan in self._model.orphans_it: row = self._tags_table.rowCount() self._tags_table.insertRow(row) it_item = QTableWidgetItem(orphan) it_item.setForeground(QColor("#ff6b6b")) en_item = QTableWidgetItem("โš  mancante") en_item.setForeground(QColor("#ff6b6b")) self._tags_table.setItem(row, 0, it_item) self._tags_table.setItem(row, 1, en_item) self._status.setText( f"{len(self._model.it_to_en)} coppie ยท {len(self._model.orphans_it)} IT orfani ยท {len(self._model.orphans_en)} EN orfani" ) def _add_term(self): dlg = AddTermDialog(self) if dlg.exec() and dlg.it_term and dlg.en_term: if self._model: self._model.it_to_en[dlg.it_term] = dlg.en_term self._populate_table() def _populate_cats_table(self): articles = scan_articles(self._blog_root) counts: dict[str, int] = {} for a in articles: for c in a.frontmatter.get("categories", []): key = str(c).strip() counts[key] = counts.get(key, 0) + 1 self._cats_table.setRowCount(0) for cat in sorted(self._categories): row = self._cats_table.rowCount() self._cats_table.insertRow(row) self._cats_table.setItem(row, 0, QTableWidgetItem(cat)) count_item = QTableWidgetItem(str(counts.get(cat, 0))) count_item.setFlags(count_item.flags() & ~Qt.ItemFlag.ItemIsEditable) count_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self._cats_table.setItem(row, 1, count_item) def _add_category(self): dlg = QDialog(self) dlg.setWindowTitle("Aggiungi categoria") layout = QVBoxLayout(dlg) form = QFormLayout() name_edit = QLineEdit() form.addRow("Nome:", name_edit) layout.addLayout(form) btns = QHBoxLayout() ok = QPushButton("Aggiungi") ok.clicked.connect(dlg.accept) cancel = QPushButton("Annulla") cancel.clicked.connect(dlg.reject) btns.addStretch() btns.addWidget(cancel) btns.addWidget(ok) layout.addLayout(btns) if dlg.exec() and name_edit.text().strip(): name = name_edit.text().strip() if name not in self._categories: self._categories.append(name) self._populate_cats_table() def _remove_category(self): rows = {idx.row() for idx in self._cats_table.selectedIndexes()} if not rows: return names = [self._cats_table.item(r, 0).text() for r in rows if self._cats_table.item(r, 0)] if not names: return reply = QMessageBox.question( self, "Rimuovi categorie", f"Rimuovere {len(names)} categorie?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: to_remove = set(names) self._categories = [c for c in self._categories if c not in to_remove] self._populate_cats_table() def _save_categories(self): cats: list[str] = [] for row in range(self._cats_table.rowCount()): item = self._cats_table.item(row, 0) if item and item.text().strip(): cats.append(item.text().strip()) self._categories = sorted(set(cats)) save_categories(self._categories_path, self._categories) self._status.setText("Categorie salvate.") @staticmethod def _detect_renames( old: dict[str, str], new: dict[str, str] ) -> tuple[dict[str, str], dict[str, str]]: it_renames: dict[str, str] = {} en_renames: dict[str, str] = {} old_en_to_it = {v: k for k, v in old.items()} for new_it, new_en in new.items(): if new_it not in old: if new_en in old_en_to_it: it_renames[old_en_to_it[new_en]] = new_it else: if old[new_it] != new_en: en_renames[old[new_it]] = new_en return it_renames, en_renames def _propagate_renames(self, it_renames: dict[str, str], en_renames: dict[str, str]) -> int: if not it_renames and not en_renames: return 0 articles = scan_articles(self._blog_root) count = 0 for article in articles: renames = it_renames if article.lang == "it" else en_renames if not renames: continue current_tags = article.frontmatter.get("tags", []) if not isinstance(current_tags, list): continue new_tags = [renames.get(str(t), str(t)) for t in current_tags] if new_tags == [str(t) for t in current_tags]: continue try: fm, body = parse_frontmatter(article.path) fm["tags"] = new_tags write_frontmatter(article.path, fm, body) count += 1 except Exception: pass return count def _save(self): if not self._model: return old_it_to_en = dict(self._model.it_to_en) updated: dict[str, str] = {} for row in range(self._tags_table.rowCount()): it_item = self._tags_table.item(row, 0) en_item = self._tags_table.item(row, 1) if it_item and en_item: it_val = it_item.text().strip() en_val = en_item.text().strip() if it_val and en_val and en_val != "โš  mancante": updated[it_val] = en_val self._model.it_to_en = updated try: save_taxonomy(self._model, self._tags_it, self._tags_en) except OSError as e: QMessageBox.critical(self, "Errore", f"Impossibile salvare: {e}") return it_renames, en_renames = self._detect_renames(old_it_to_en, updated) count = self._propagate_renames(it_renames, en_renames) if count: self._status.setText(f"Salvato ยท {count} articol{'o' if count == 1 else 'i'} aggiornati.") else: self._status.setText("Salvato.")