From: Danilo M. Date: Sun, 3 May 2026 11:13:46 +0000 (+0200) Subject: Chore: updated TODO and HANDOFF. X-Git-Tag: v1.2~5 X-Git-Url: https://git.danix.xyz/?a=commitdiff_plain;h=e28ebf9f0d2aaa9a6819f5a09cac05953080c1e9;p=publisher.git Chore: updated TODO and HANDOFF. --- diff --git a/CLAUDE.md b/CLAUDE.md index 952aac6..ef0f145 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,9 +65,24 @@ Workers emit `output(str)`, `error(str)`, `finished(bool)` signals. UI connects **Config path override in tests:** Pass `path` explicitly to `Config.load(path)` / `Config.save(path)` rather than monkeypatching the module-level constant. +## System Theme (Kvantum + qt6ct) + +App uses system Qt theme via two symlinks into PyQt6's plugin bundle. These are required for Kvantum-dark + qt6ct to work and are **not** tracked in git. + +```bash +PYQT6=$(python3 -c "import os,PyQt6; print(os.path.join(os.path.dirname(PyQt6.__file__),'Qt6','plugins'))") +mkdir -p "$PYQT6/styles" +ln -sf /usr/lib64/qt6/plugins/styles/libkvantum.so "$PYQT6/styles/libkvantum.so" +ln -sf /usr/lib64/qt6/plugins/platformthemes/libqt6ct.so "$PYQT6/platformthemes/libqt6ct.so" +``` + +**Recreate after every `pip install --upgrade PyQt6`** — pip wipes the bundle, symlinks are lost. + +PyQt6 is pinned to `>=6.10.0,<6.11.0` in `requirements.txt` to match system Qt6 (6.10.3). Kvantum uses Qt private API — upgrading PyQt6 beyond the system Qt minor version causes segfault. + ## Spec and Plan Full design spec: `docs/superpowers/specs/2026-05-01-my-publisher-design.md` Implementation plan (20 tasks): `docs/superpowers/plans/2026-05-01-my-publisher.md` -Implementation is in progress — tasks 1–4 complete (scaffold, config, models, frontmatter parser). Tasks 5–20 pending. +All 20 tasks complete. v1.0 shipped with full functionality: scaffold, config, models, frontmatter parser, article scanner, taxonomy, git worker, hugo worker, translation worker, and all UI components. diff --git a/HANDOFF.md b/HANDOFF.md index 5d9341c..1009af9 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,15 +1,17 @@ Who this is for: - Danilo is a developer and blogger running a bilingual (IT/EN) Hugo site at danix.xyz. He built a PyQt6 desktop GUI called my-publisher to centralize his blog publishing workflow and is now maintaining and extending it post-launch. + Danilo is a developer and blogger running a bilingual (IT/EN) Hugo site at danix.xyz. He maintains a PyQt6 desktop app called my-publisher that centralizes his blog publishing workflow, and is currently extending it post-v1.0 launch. What we covered: - We resumed the project in a post-implementation maintenance session. We generated a TODO.md file tracking three known gaps from the initial build (TaxonomyView Categories tab, MissingTranslationView Traduci button, multi-token tag autocomplete) and committed it to git. We added app-wide font size support by adding a font_size field (default 10) to the Config dataclass, saving/loading it via config.toml, and applying it at startup via app.setFont() in main.py. We added a TODO item for a font size spinner in the UI. We created an assets/ directory with the - blogilo.svg icon (copied from the Breeze KDE icon theme) and a my-publisher.desktop file with Exec and Icon paths pointing to /opt/my-publisher for system-wide installation. The user also manually added two more TODO items: draft article visual hints and transart backend availability validation. We pushed all commits and tagged the release as v1.0. + Two features were implemented this session. First, draft article visual hints: added a draft property to the Article dataclass, amber [DRAFT] prefix in article list items, and a DRAFT badge in the article detail header. Second, article list metadata display: added four properties to Article (meta_type, meta_tags, meta_categories, meta_date), then updated the article list to show a two-line row per article with slug+badge on line 1 and type/date/tags/categories on line 2. The two-line rendering required three iterations to fix: setWordWrap failed, setTextElideMode failed, and the final working solution was a custom QStyledItemDelegate that draws both lines directly onto the canvas bypassing Qt elision entirely. The app was tagged v1.1 and pushed to origin. What was confirmed: - All 22 tests pass. The font_size field defaults to 10 and is read from config.toml if present. The desktop file uses /opt/my-publisher paths — the repo at /home/danix/Programming/GIT/my-publisher is the development copy, and the installed copy will live at /opt/my-publisher. Tag v1.0 is pushed to origin (danix_git:publisher). TODO.md now tracks five items total: TaxonomyView Categories tab, MissingTranslationView Traduci button, multi-token tag autocomplete, font size spinner in UI, draft article visual hints, and transart backend availability validation. + All 30 tests pass. The delegate approach (ArticleItemDelegate in ui/articles_view.py) is the correct solution for multi-line QListWidget items in PyQt6. Line 2 metadata is stored in Qt.ItemDataRole.UserRole+1 and drawn in dimmed #888 color. Draft items show amber #f59e0b. + Missing-translation items show red #ff6b6b. The blog's TOML frontmatter uses lowercase keys: type, date, tags, categories. Tag v1.1 is pushed to danix_git:publisher. Still in progress: - None of the TODO items have been implemented yet. The font size is functional but only editable via config.toml — no UI spinner exists yet. The app has been physically copied to /opt/my-publisher. + Nothing was left open in this session. The remaining TODO items from TODO.md are: TaxonomyView Categories tab, MissingTranslationView Traduci button, font size spinner in SetupDialog, multi-token tag autocomplete in FrontmatterEditor, transart backend availability validation, + taxonomy tag rename propagation to articles, and Ctrl+Q keyboard shortcut. Next steps: - Pick any TODO item to implement next. Suggested order by impact: (1) draft article visual hints — visible quality-of-life improvement for daily use; (2) font size spinner in SetupDialog or a Settings dialog; (3) transart backend availability check using OLLAMA_HOST; (4) TaxonomyView Categories tab; (5) MissingTranslationView Traduci button; (6) multi-token tag autocomplete. \ No newline at end of file + Pick the next TODO item to implement. Suggested order by impact: (1) Ctrl+Q keyboard shortcut — trivial, one line in main_window.py; (2) font size spinner in SetupDialog (ui/setup_dialog.py) — moderate, saves via Config.save() and re-applies with app.setFont(); (3) transart backend + availability check using OLLAMA_HOST; (4) MissingTranslationView Traduci button — wire translate_requested signal per row; (5) TaxonomyView Categories tab; (6) taxonomy tag rename propagation; (7) multi-token tag autocomplete in ui/frontmatter_editor.py. \ No newline at end of file diff --git a/TODO.md b/TODO.md index cc33e7e..c9ce125 100644 --- a/TODO.md +++ b/TODO.md @@ -18,8 +18,16 @@ Known gaps from initial implementation. Functional for daily use — these are e ## Drafts -- [ ] Add visible hints to quickly identify articles with `draft = true` in frontmatter. The hints can be added to the list view as well as the single view as a badge, color difference, etc. +- [✅] Add visible hints to quickly identify articles with `draft = true` in frontmatter. The hints can be added to the list view as well as the single view as a badge, color difference, etc. ## Transart backend availability -- [ ] Using the `OLLAMA_HOST` variable in transart.py, validate if the backend is available or disable the "translate" button. Maybe add a popup message reminding to activate the runpod for the translation engine to be available. \ No newline at end of file +- [ ] Using the `OLLAMA_HOST` variable in transart.py, validate if the backend is available or disable the "translate" button. Maybe add a popup message reminding to activate the runpod for the translation engine to be available. + +## Interface + +- [✅] The main articles list should display the type, tags, categories and date fields for each article in both languages. +- [ ] When modifying tags in the taxonomy view, they should be updated in the relevant articles. EG modified the english "regionale" tag to "regional", after saving it, the program should find all english articles with the old "regionale" tag and modify them to the new "regional". + +## keyboard shortcuts +- [ ] The app should follow some standard keyboard shortcuts like Ctrl+q should quit the app. \ No newline at end of file diff --git a/docs/superpowers/plans/2026-05-03-article-list-metadata.md b/docs/superpowers/plans/2026-05-03-article-list-metadata.md new file mode 100644 index 0000000..a9b97aa --- /dev/null +++ b/docs/superpowers/plans/2026-05-03-article-list-metadata.md @@ -0,0 +1,200 @@ +# Article List Metadata Display Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Show type, tags, categories, and date for each article row in the articles list (both IT and EN tabs). + +**Architecture:** Extend `ArticleItem` to render a two-line item: line 1 = slug + translation badge (existing), line 2 = metadata fields extracted from `article.frontmatter`. Use `\n` in item text and `setSizeHint` for row height. No new files needed. + +**Tech Stack:** PyQt6 QListWidgetItem, existing Article.frontmatter dict. + +--- + +### Task 1: Add metadata helpers to Article model + +**Files:** +- Modify: `core/models.py` +- Test: `tests/test_models.py` + +- [ ] **Step 1: Write failing tests** + +Add to `tests/test_models.py`: + +```python +def test_article_draft_property(): + fm = {"draft": True, "type": "Tech", "tags": ["linux"], "categories": ["DIY"], "date": "2024-01-15T10:00:00+00:00"} + a = Article(slug="test", lang="it", path=Path("/tmp/test"), frontmatter=fm, has_translation=False, translation_path=None) + assert a.draft is True + +def test_article_meta_type(): + fm = {"type": "Tech"} + a = Article(slug="test", lang="it", path=Path("/tmp/test"), frontmatter=fm, has_translation=False, translation_path=None) + assert a.meta_type == "Tech" + +def test_article_meta_type_missing(): + a = Article(slug="test", lang="it", path=Path("/tmp/test"), frontmatter={}, has_translation=False, translation_path=None) + assert a.meta_type == "" + +def test_article_meta_tags(): + fm = {"tags": ["linux", "python"]} + a = Article(slug="test", lang="it", path=Path("/tmp/test"), frontmatter=fm, has_translation=False, translation_path=None) + assert a.meta_tags == "linux, python" + +def test_article_meta_tags_empty(): + a = Article(slug="test", lang="it", path=Path("/tmp/test"), frontmatter={}, has_translation=False, translation_path=None) + assert a.meta_tags == "" + +def test_article_meta_categories(): + fm = {"categories": ["DIY", "Tech"]} + a = Article(slug="test", lang="it", path=Path("/tmp/test"), frontmatter=fm, has_translation=False, translation_path=None) + assert a.meta_categories == "DIY, Tech" + +def test_article_meta_date(): + fm = {"date": "2024-01-15T10:00:00+00:00"} + a = Article(slug="test", lang="it", path=Path("/tmp/test"), frontmatter=fm, has_translation=False, translation_path=None) + assert a.meta_date == "2024-01-15" + +def test_article_meta_date_missing(): + a = Article(slug="test", lang="it", path=Path("/tmp/test"), frontmatter={}, has_translation=False, translation_path=None) + assert a.meta_date == "" +``` + +- [ ] **Step 2: Run tests to confirm they fail** + +```bash +pytest tests/test_models.py -v +``` + +Expected: FAIL — `Article` has no `meta_type`, `meta_tags`, `meta_categories`, `meta_date` attributes. + +- [ ] **Step 3: Implement properties in Article** + +In `core/models.py`, add four properties after the existing `draft` property: + +```python +@property +def meta_type(self) -> str: + return str(self.frontmatter.get("type", "")) + +@property +def meta_tags(self) -> str: + tags = self.frontmatter.get("tags", []) + if isinstance(tags, list): + return ", ".join(str(t) for t in tags) + return str(tags) if tags else "" + +@property +def meta_categories(self) -> str: + cats = self.frontmatter.get("categories", []) + if isinstance(cats, list): + return ", ".join(str(c) for c in cats) + return str(cats) if cats else "" + +@property +def meta_date(self) -> str: + raw = self.frontmatter.get("date", "") + if not raw: + return "" + return str(raw)[:10] # ISO date → "YYYY-MM-DD" +``` + +- [ ] **Step 4: Run tests to confirm they pass** + +```bash +pytest tests/test_models.py -v +``` + +Expected: all pass. + +- [ ] **Step 5: Commit** + +```bash +git add core/models.py tests/test_models.py +git commit -m "feat: add meta_type, meta_tags, meta_categories, meta_date properties to Article" +``` + +--- + +### Task 2: Render metadata in ArticleItem + +**Files:** +- Modify: `ui/articles_view.py` + +No automated tests for UI — verify manually. + +- [ ] **Step 1: Update ArticleItem to render two-line text** + +In `ui/articles_view.py`, replace the `ArticleItem.__init__` method. The first line keeps slug + translation badge (and `[DRAFT]` prefix). The second line shows metadata fields, omitting empty ones. + +Replace the existing `ArticleItem` class (lines 10-26) with: + +```python +class ArticleItem(QListWidgetItem): + def __init__(self, article: Article): + super().__init__() + self.article = article + if article.has_translation: + badge = f"🇬🇧 ✓" if article.lang == "it" else "🇮🇹 ✓" + else: + badge = f"🇬🇧 ✗" if article.lang == "it" else "🇮🇹 ✗" + line1 = f"{article.slug} [{badge}]" + if article.draft: + line1 = f"[DRAFT] {line1}" + + parts = [] + if article.meta_type: + parts.append(article.meta_type) + if article.meta_date: + parts.append(article.meta_date) + if article.meta_tags: + parts.append(f"#{article.meta_tags.replace(', ', ' #')}") + if article.meta_categories: + parts.append(f"[{article.meta_categories}]") + line2 = " ".join(parts) if parts else "" + + text = f"{line1}\n{line2}" if line2 else line1 + self.setText(text) + self.setSizeHint(QSize(0, 42) if line2 else QSize(0, 26)) + + if article.draft: + self.setForeground(QColor("#f59e0b")) + elif not article.has_translation: + self.setForeground(QColor("#ff6b6b")) +``` + +Also add `QSize` to the imports at the top of the file: + +```python +from PyQt6.QtCore import Qt, pyqtSignal, QSize +``` + +- [ ] **Step 2: Run the app and verify visually** + +```bash +python main.py +``` + +Check: +- Each article row shows two lines: slug + badge on line 1, metadata on line 2 +- Articles with no metadata (missing type/date/tags/categories) show single line +- Draft articles: amber color on both lines +- Missing-translation articles: red color +- Tags rendered as `#tag1 #tag2` +- Categories rendered as `[Cat1, Cat2]` +- Date rendered as `YYYY-MM-DD` +- Row height accommodates two lines without clipping + +- [ ] **Step 3: Run full test suite** + +```bash +pytest tests/ -v +``` + +Expected: all 22+ tests pass (UI change has no tests, but existing tests must not break). + +- [ ] **Step 4: Commit** + +```bash +git add ui/articles_view.py +git commit -m "feat: show type, date, tags, categories in article list rows" +``` diff --git a/requirements.txt b/requirements.txt index 06a9897..112f3ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -PyQt6>=6.6.0 +PyQt6>=6.10.0,<6.11.0 tomlkit>=0.12.0 mistune>=3.0.0 diff --git a/ui/article_detail.py b/ui/article_detail.py index 1fe1643..a0464ae 100644 --- a/ui/article_detail.py +++ b/ui/article_detail.py @@ -38,9 +38,13 @@ class ArticleDetailView(QWidget): 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;") + self._draft_badge = QLabel("● DRAFT") + self._draft_badge.setStyleSheet("color:#f59e0b; font-size:10px; font-weight:bold;") + self._draft_badge.hide() info = QVBoxLayout() info.addWidget(self._title_label) info.addWidget(self._path_label) + info.addWidget(self._draft_badge) h_layout.addLayout(info, stretch=1) for (icon, signal_name, style) in [ @@ -106,6 +110,7 @@ class ArticleDetailView(QWidget): self._title_label.setText(article.slug) rel = "/".join(article.path.parts[-5:]) self._path_label.setText(rel) + self._draft_badge.setVisible(article.draft) # Populate frontmatter while self._fm_content.count():