Skip to content

feat(viewer): thumbnails sidebar + harden store-update, fitz auth, and thumbnail epoch guard#117

Open
nelsonduarte wants to merge 3 commits into
mainfrom
feat/thumbnails-sidebar
Open

feat(viewer): thumbnails sidebar + harden store-update, fitz auth, and thumbnail epoch guard#117
nelsonduarte wants to merge 3 commits into
mainfrom
feat/thumbnails-sidebar

Conversation

@nelsonduarte

Copy link
Copy Markdown
Owner

Summary

This branch adds the page-thumbnails sidebar to the viewer and hardens several correctness/robustness issues surfaced during adversarial review. All fixes below passed adversarial review and the full test suite (443 passed, 6 skipped).

Fixes by severity

High

  • i18n store-update version handling (app/i18n.py): validate the format and per-component magnitude (each UInt16 component must be ≤ 65535) of Store versions before comparing. A new _is_valid_store_version helper gates parsing, and corrupt dismissed-version keys are now auto-scrubbed so a stale/malformed persisted value can no longer permanently suppress update notifications.
  • Fitz authentication check (app/base.py): _open_fitz now verifies the return value of authenticate() and raises a translated ValueError on a wrong password instead of silently proceeding with a locked document.

Medium

  • Thumbnail cross-document contamination (app/viewer/thumbnails.py): added an epoch counter so in-flight thumbnail results from a previously loaded document are discarded rather than painted onto the newly opened document. The worker is now disconnected before wait() to avoid stale-signal delivery.

Low / hygiene

  • Removed unused variables flagged by Ruff F841 in app/tools/_pdf_extract.py, app/viewer/panel.py, and app/window.py.
  • Added requirements-dev.txt (mypy, bandit, pip-audit, pyyaml, ruff).
  • Ignore debug repro artifacts thumb_repro*.png via .gitignore.

Tests

  • New/updated: tests/test_store_updater.py, tests/test_thumbnails.py, tests/test_i18n.py.
  • Full suite: 443 passed, 6 skipped.

🤖 Generated with Claude Code

nelsonduarte and others added 3 commits July 12, 2026 09:47
New Pages tab in the viewer sidebar showing clickable page thumbnails
rendered from the PDF via fitz. Click a thumbnail to jump to that
page; the current page is highlighted with ACCENT and stays in sync
as the user scrolls the canvas.

Thumbnails render in a background QThread (a private fitz.Document
copy so we don't share pymupdf state with the canvas) at 40 DPI, then
smooth-scale to 120x160 on paint. Cache is capped at CACHE_MAX (200)
with LRU eviction so long PDFs don't leak memory.

The sidebar container is now a QTabWidget with two tabs:
- Contents (existing TOC) — hidden when the PDF has no outline
- Pages (new thumbnails) — always available once a doc is loaded

The bookmark button on the header now toggles the whole sidebar. It
becomes visible on any PDF load (previously only when TOC existed)
because the Pages tab is always useful.

- New: app/viewer/thumbnails.py
  * ThumbnailWorker (QThread) — background render + password support
  * ThumbnailModel (QAbstractListModel) — page slots + LRU cache
  * ThumbnailDelegate (QStyledItemDelegate) — custom paint with
    current-page ACCENT highlight and hover feedback
  * ThumbnailPanel (QWidget) — sidebar container + view lifecycle

- Modified: app/viewer/panel.py
  * Wrap sidebar in QTabWidget with Contents + Pages tabs
  * Wire page_requested to canvas scroll_to_page
  * Sync current page from _update_page_label
  * Clear thumbnails on doc replace and closeEvent
  * Forward theme changes to thumbnail delegate

- New i18n keys (8 languages): viewer.sidebar.contents,
  viewer.sidebar.pages, viewer.thumbnails.loading

- Tests: 18 (source-level guards + ThumbnailModel behavioural +
  i18n parity + panel integration)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Qt requires QPixmap to be created/mutated only in the main GUI thread.
The initial ThumbnailWorker created QPixmap.fromImage() in the worker
thread, which silently failed (or would crash on some platforms) - no
thumbnails ever appeared in the sidebar.

Now:
- Worker emits QImage (thread-safe) via thumbnail_ready signal
- ThumbnailPanel._on_image_ready slot converts to QPixmap on the main
  thread, then hands it to the model
- Connection uses explicit Qt.ConnectionType.QueuedConnection to
  respect Py3.14 PySide6's stricter cross-thread routing
  (see memory/project_compress_freeze_py314.md)
- @slot decorator on the receiver for the same reason

Also adds debug logging in worker + panel for future diagnostics.
…nail epoch guard

- i18n: validate store version format and UInt16 component magnitude; scrub corrupt dismissed-version keys so stale values no longer suppress update notifications
- base: verify _open_fitz authenticate() return and raise on wrong password
- viewer/thumbnails: add epoch counter to discard stale cross-document thumbnails
- remove unused variables (F841); add requirements-dev.txt; ignore thumb_repro*.png

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread app/viewer/panel.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants