{eid}
" + f'{eid_display}
' "" ) diff --git a/src/imwald/ui/main_window.py b/src/imwald/ui/main_window.py index 7329bc2..f7ff758 100644 --- a/src/imwald/ui/main_window.py +++ b/src/imwald/ui/main_window.py @@ -7,10 +7,12 @@ from typing import Any, cast from PySide6.QtCore import QObject, QRunnable, QSize, QThreadPool, Qt, QTimer, Signal from PySide6.QtGui import QAction, QCloseEvent, QColor, QIcon, QPainter, QPen, QPixmap from PySide6.QtWidgets import ( + QApplication, QComboBox, QDialog, QDialogButtonBox, QFormLayout, + QHBoxLayout, QInputDialog, QLabel, QLineEdit, @@ -18,6 +20,7 @@ from PySide6.QtWidgets import ( QListWidgetItem, QMainWindow, QMessageBox, + QProgressBar, QSpinBox, QSplitter, QStackedWidget, @@ -139,6 +142,30 @@ class MainWindow(QMainWindow): split.setSizes([920, 280]) self.setCentralWidget(split) + self._busy_depth = 0 + self._awaiting_relay_cookie: int | None = None + self._busy_wrap = QWidget() + self._busy_wrap.setObjectName("BusyWrap") + bh = QHBoxLayout(self._busy_wrap) + bh.setContentsMargins(0, 0, 8, 0) + bh.setSpacing(10) + self._busy_label = QLabel("") + self._busy_label.setStyleSheet(f"color: {TEXT_MUTED}; font-size: 13px;") + self._busy_bar = QProgressBar() + self._busy_bar.setObjectName("BusyBar") + self._busy_bar.setRange(0, 0) + self._busy_bar.setTextVisible(False) + self._busy_bar.setFixedSize(132, 10) + self._busy_bar.setMaximumHeight(12) + bh.addWidget(self._busy_label) + bh.addWidget(self._busy_bar) + self._busy_wrap.hide() + self.statusBar().addPermanentWidget(self._busy_wrap) + self._relay_connect_timer = QTimer(self) + self._relay_connect_timer.setSingleShot(True) + self._relay_connect_timer.setInterval(45_000) + self._relay_connect_timer.timeout.connect(self._on_relay_connect_timeout) + self._acct_combo = QComboBox() self._acct_combo.setMinimumWidth(220) self._acct_combo.setIconSize(QSize(22, 22)) @@ -172,8 +199,12 @@ class MainWindow(QMainWindow): self._on_account_changed() if not self._db.get_setting("onboarding_done") and not self._accounts: - if run_onboarding_wizard(self, db=self._db, engine=self._engine, existing_accounts=self._accounts): - self._db.set_setting("onboarding_done", "1") + self.push_busy("Opening wizard…") + try: + if run_onboarding_wizard(self, db=self._db, engine=self._engine, existing_accounts=self._accounts): + self._db.set_setting("onboarding_done", "1") + finally: + self.pop_busy() self._accounts = load_accounts() self._reload_account_combo() self._notif.set_accounts(self._accounts) @@ -283,15 +314,52 @@ class MainWindow(QMainWindow): self._feed.reload_queue() self._restart_relays() + def push_busy(self, message: str = "Working…") -> None: + """Show the status-bar spinner (supports nested push/pop).""" + self._busy_depth += 1 + if self._busy_depth == 1: + self._busy_label.setText(message) + self._busy_wrap.show() + self.setCursor(Qt.CursorShape.WaitCursor) + QApplication.processEvents() + + def pop_busy(self) -> None: + if self._busy_depth <= 0: + return + self._busy_depth -= 1 + if self._busy_depth == 0: + self._busy_wrap.hide() + self.unsetCursor() + + def _on_relay_connect_ready(self, cookie: int) -> None: + if self._awaiting_relay_cookie != cookie: + return + self._awaiting_relay_cookie = None + self._relay_connect_timer.stop() + self.pop_busy() + + def _on_relay_connect_timeout(self) -> None: + if self._awaiting_relay_cookie is None: + return + self._awaiting_relay_cookie = None + self.pop_busy() + def _restart_relays(self) -> None: + self.push_busy("Connecting to relays…") pk = self._current_pubkey() resolved = resolve_for_account(self._db, pk) reads = augment_feed_with_trending(resolved.read_urls) - self._engine.start_relays( - read_urls=reads, - user_write_urls=resolved.write_urls, - list30000_owner=self.list_owner_pubkey_for_relays(), - ) + + def _go() -> None: + ck = self._engine.start_relays( + read_urls=reads, + user_write_urls=resolved.write_urls, + list30000_owner=self.list_owner_pubkey_for_relays(), + ) + self._awaiting_relay_cookie = ck + self._relay_connect_timer.start() + + QTimer.singleShot(0, _go) self._author_bootstrap_timer.start() def _bootstrap_author_metadata_queue(self) -> None: @@ -395,6 +463,7 @@ class MainWindow(QMainWindow): self._engine.event_ingested.connect(self._on_event_ingested) self._engine.relay_status.connect(self._relay_status_message) self._engine.relay_status.connect(self._relay_panel.log_line.emit) + self._engine.relay_connect_ready.connect(self._on_relay_connect_ready) def _relay_status_message(self, s: str) -> None: self.statusBar().showMessage(s, 8000) @@ -515,7 +584,11 @@ class MainWindow(QMainWindow): return if QMessageBox.question(self, "NIP-09", f"Publish deletion for {event_id[:16]}…?") != QMessageBox.StandardButton.Yes: return - self._engine.publish_nip09_deletion(acc, pw, event_id) + self.push_busy("Publishing deletion…") + try: + self._engine.publish_nip09_deletion(acc, pw, event_id) + finally: + self.pop_busy() QMessageBox.information(self, "NIP-09", "Deletion request published to your write relays.") def _edit_current(self) -> None: @@ -581,7 +654,11 @@ class MainWindow(QMainWindow): def _onboarding_again(self) -> None: self._accounts = load_accounts() - run_onboarding_wizard(self, db=self._db, engine=self._engine, existing_accounts=self._accounts) + self.push_busy("Opening wizard…") + try: + run_onboarding_wizard(self, db=self._db, engine=self._engine, existing_accounts=self._accounts) + finally: + self.pop_busy() self._accounts = load_accounts() self._reload_account_combo() self._notif.set_accounts(self._accounts) @@ -604,6 +681,10 @@ class MainWindow(QMainWindow): return acc, self._password_for(pk) def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 + self._relay_connect_timer.stop() + self._awaiting_relay_cookie = None + while self._busy_depth > 0: + self.pop_busy() self._relay_panel.shutdown_logging() self._engine.stop_relays() super().closeEvent(event) diff --git a/src/imwald/ui/theme.py b/src/imwald/ui/theme.py index b5378a1..a835532 100644 --- a/src/imwald/ui/theme.py +++ b/src/imwald/ui/theme.py @@ -2,6 +2,8 @@ from __future__ import annotations +import base64 + from PySide6.QtWidgets import QApplication from imwald.core.display_constants import IMAGE_DISPLAY_MAX_WIDTH_PX @@ -19,6 +21,14 @@ BG_CARD = "#151f1a" BORDER = "#2a3d34" BG_CODE = "#0a100d" +# Fusion’s default tab-close pixmap is nearly invisible on our dark tabs; use an explicit “×”. +_TAB_CLOSE_ICON_B64 = base64.standard_b64encode( + b'" +).decode("ascii") +_TAB_CLOSE_ICON_URL = f'url("data:image/svg+xml;base64,{_TAB_CLOSE_ICON_B64}")' + _W = IMAGE_DISPLAY_MAX_WIDTH_PX FEED_DOC_CSS = f"""