From: Danilo M. Date: Sun, 3 May 2026 08:20:48 +0000 (+0200) Subject: feat: main window with sidebar navigation and file watcher X-Git-Tag: v1.0~19 X-Git-Url: https://git.danix.xyz/?a=commitdiff_plain;h=3ade340c587da09c777f4b3fbb383f550c457c9b;p=publisher.git feat: main window with sidebar navigation and file watcher Co-Authored-By: Claude Sonnet 4.6 --- diff --git a/main.py b/main.py index 98a3fd4..4c270dd 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,23 @@ import sys from PyQt6.QtWidgets import QApplication +from core.config import Config +from ui.main_window import MainWindow +from ui.setup_dialog import SetupDialog def main(): app = QApplication(sys.argv) app.setApplicationName("my-publisher") app.setOrganizationName("danix") + + config = Config.load() + if not config.is_complete(): + dlg = SetupDialog(config) + if dlg.exec() == 0: + sys.exit(0) + config = Config.load() + + window = MainWindow(config) + window.show() sys.exit(app.exec()) if __name__ == "__main__": diff --git a/ui/main_window.py b/ui/main_window.py new file mode 100644 index 0000000..9f0594b --- /dev/null +++ b/ui/main_window.py @@ -0,0 +1,144 @@ +from __future__ import annotations +from PyQt6.QtWidgets import ( + QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, + QLabel, QPushButton, QStackedWidget, QFrame, QSizePolicy, +) +from PyQt6.QtCore import Qt, QFileSystemWatcher +from PyQt6.QtGui import QFont +from pathlib import Path +from core.config import Config +from core.article_scanner import scan_articles +from core.models import Article + +class SidebarButton(QPushButton): + def __init__(self, text: str, parent=None): + super().__init__(text, parent) + self.setFlat(True) + self.setCheckable(True) + self.setStyleSheet(""" + QPushButton { text-align: left; padding: 5px 10px; border-radius: 4px; color: #888; } + QPushButton:checked { background: #2a2a4e; color: #a855f7; } + QPushButton:hover:!checked { background: #1a1a2e; color: #ccc; } + """) + +class MainWindow(QMainWindow): + def __init__(self, config: Config, parent=None): + super().__init__(parent) + self.config = config + self._articles: list[Article] = [] + self._watcher = QFileSystemWatcher(self) + self._watcher.directoryChanged.connect(self._on_fs_change) + self.setWindowTitle("my-publisher") + self.setMinimumSize(1100, 700) + self._build_ui() + self._refresh_articles() + self._setup_watcher() + + def _build_ui(self): + central = QWidget() + self.setCentralWidget(central) + root = QHBoxLayout(central) + root.setContentsMargins(0, 0, 0, 0) + root.setSpacing(0) + + # Sidebar + sidebar = self._build_sidebar() + root.addWidget(sidebar) + + # Divider + line = QFrame() + line.setFrameShape(QFrame.Shape.VLine) + line.setStyleSheet("color: #2a2a4e;") + root.addWidget(line) + + # Content stack + self._stack = QStackedWidget() + root.addWidget(self._stack, stretch=1) + + # Placeholder pages (replaced in later tasks) + for name in ["articles", "no_translation", "new_article", "taxonomy", + "media", "translations", "git", "hugo"]: + w = QLabel(f"[{name}]") + w.setAlignment(Qt.AlignmentFlag.AlignCenter) + self._stack.addWidget(w) + setattr(self, f"_page_{name}", self._stack.count() - 1) + + def _build_sidebar(self) -> QWidget: + w = QWidget() + w.setFixedWidth(210) + w.setStyleSheet("background: #1a1a2e;") + layout = QVBoxLayout(w) + layout.setContentsMargins(8, 12, 8, 12) + layout.setSpacing(2) + + header = QHBoxLayout() + title = QLabel("📰 my-publisher") + title.setStyleSheet("color: #fff; font-weight: bold; font-size: 13px; padding: 4px 8px;") + header.addWidget(title) + self._refresh_btn = QPushButton("🔄") + self._refresh_btn.setFlat(True) + self._refresh_btn.setFixedSize(28, 28) + self._refresh_btn.setStyleSheet("color: #555; border: none;") + self._refresh_btn.clicked.connect(self._refresh_articles) + header.addWidget(self._refresh_btn) + layout.addLayout(header) + + def section(text): + lbl = QLabel(text) + lbl.setStyleSheet("color: #a855f7; font-size: 9px; text-transform: uppercase; letter-spacing: 1px; padding: 8px 8px 2px;") + layout.addWidget(lbl) + + self._btn_group: list[SidebarButton] = [] + + def btn(icon_text: str, page_attr: str) -> SidebarButton: + b = SidebarButton(icon_text) + b.clicked.connect(lambda: self._show_page(page_attr, b)) + layout.addWidget(b) + self._btn_group.append(b) + return b + + section("CONTENUTO") + btn("📋 Articoli", "_page_articles") + self._btn_no_trans = btn("⚠️ Senza Traduzione", "_page_no_translation") + self._badge_no_trans = QLabel("0") + self._badge_no_trans.setStyleSheet("background:#3a1a1a;color:#ff6b6b;border-radius:8px;padding:1px 6px;font-size:10px;") + btn("➕ Nuovo articolo", "_page_new_article") + btn("🏷 Tassonomia", "_page_taxonomy") + btn("🖼 Media", "_page_media") + + section("WORKFLOW") + btn("🌍 Traduzioni", "_page_translations") + btn("🔧 Git ops", "_page_git") + btn("🚀 Hugo server", "_page_hugo") + + section("DEPLOY") + btn("🧪 Test (master)", "_page_git") + btn("🚢 Production", "_page_git") + + layout.addStretch() + return w + + def _show_page(self, page_attr: str, active_btn: SidebarButton): + for b in self._btn_group: + b.setChecked(False) + active_btn.setChecked(True) + self._stack.setCurrentIndex(getattr(self, page_attr)) + + def _refresh_articles(self): + if not self.config.blog_repo: + return + self._articles = scan_articles(Path(self.config.blog_repo)) + missing = [a for a in self._articles if not a.has_translation] + self._badge_no_trans.setText(str(len(missing))) + + def _setup_watcher(self): + if not self.config.blog_repo: + return + root = Path(self.config.blog_repo) + for lang in ("it", "en"): + d = root / "content" / lang / "articles" + if d.exists(): + self._watcher.addPath(str(d)) + + def _on_fs_change(self, _path: str): + self._refresh_articles() diff --git a/ui/setup_dialog.py b/ui/setup_dialog.py new file mode 100644 index 0000000..4a8c6b7 --- /dev/null +++ b/ui/setup_dialog.py @@ -0,0 +1,55 @@ +from __future__ import annotations +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QFileDialog, QFormLayout, +) +from core.config import Config + +class SetupDialog(QDialog): + def __init__(self, config: Config, parent=None): + super().__init__(parent) + self.setWindowTitle("my-publisher — Setup") + self.setMinimumWidth(500) + self._config = config + self._build_ui() + + def _build_ui(self): + layout = QVBoxLayout(self) + layout.addWidget(QLabel("Configura my-publisher prima di iniziare.")) + + form = QFormLayout() + + self._blog_repo = QLineEdit(self._config.blog_repo) + browse_btn = QPushButton("Sfoglia...") + browse_btn.clicked.connect(self._browse_repo) + row = QHBoxLayout() + row.addWidget(self._blog_repo) + row.addWidget(browse_btn) + form.addRow("Blog repo:", row) + + self._transart = QLineEdit(self._config.transart_script) + form.addRow("transart.py:", self._transart) + + self._typora = QLineEdit(self._config.typora_bin) + form.addRow("Typora bin:", self._typora) + + layout.addLayout(form) + + btns = QHBoxLayout() + save_btn = QPushButton("Salva") + save_btn.clicked.connect(self._save) + btns.addStretch() + btns.addWidget(save_btn) + layout.addLayout(btns) + + def _browse_repo(self): + path = QFileDialog.getExistingDirectory(self, "Seleziona blog repo") + if path: + self._blog_repo.setText(path) + + def _save(self): + self._config.blog_repo = self._blog_repo.text().strip() + self._config.transart_script = self._transart.text().strip() + self._config.typora_bin = self._typora.text().strip() + self._config.save() + self.accept()