From 9455ff0f03043bb8d889fe4f6433079a0cd6c077 Mon Sep 17 00:00:00 2001 From: pax Date: Wed, 8 Apr 2026 16:10:26 -0500 Subject: [PATCH] Batch download: incremental saved-dot updates + browse-only gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes for the File → Batch Download Page (Ctrl+D) flow. 1. Saved-dot refresh Pre-fix: when the user picked a destination inside the library, the batch wrote files to disk but the browse grid's saved-dots stayed dark until the next refresh. The grid was lying about local state. Fix: stash the chosen destination as self._batch_dest at the dispatch site, then in _on_batch_progress (which already fires per-file via the existing batch_progress signal) check whether dest is inside saved_dir(); if so, find the just-finished post in self._posts by id and light its grid thumb's saved-locally dot. Dots appear incrementally as each file lands, not all at once at the end. The batch_progress signal grew a third int param (post_id of the just-finished item). It's a single-consumer signal — only _on_batch_progress connects to it — so the shape change is local. Both batch download paths (the file menu's _batch_download and the multi-select menu's _batch_download_posts) pass post.id through. When the destination is OUTSIDE the library, dots stay dark — the saved-dot means "in library", not "downloaded somewhere". The check uses Path.is_relative_to (Python 3.11+). self._batch_dest is cleared in _on_batch_done after the batch finishes so a subsequent non-batch save doesn't accidentally see a stale dest. 2. Tab gating Pre-fix: File → Batch Download Page... was enabled on Bookmarks and Library tabs, where it makes no sense (those tabs already show local files). Ctrl+D fired regardless of active tab. Fix: store the QAction as self._batch_action instead of a local var in _setup_menu, then toggle setEnabled(index == 0) from _switch_view. Disabling the QAction also disables its keyboard shortcut, so Ctrl+D becomes a no-op on non-browse tabs without a separate guard. Verified manually: - Browse tab → menu enabled, Ctrl+D works - Bookmarks/Library tabs → menu grayed out, Ctrl+D no-op - Batch dl into ~/.local/share/booru-viewer/saved → dots light up one-by-one as files land - Batch dl into /tmp → files written, dots stay dark --- booru_viewer/gui/async_signals.py | 2 +- booru_viewer/gui/main_window.py | 42 +++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/booru_viewer/gui/async_signals.py b/booru_viewer/gui/async_signals.py index 9ae7920..0c81daf 100644 --- a/booru_viewer/gui/async_signals.py +++ b/booru_viewer/gui/async_signals.py @@ -23,7 +23,7 @@ class AsyncSignals(QObject): bookmark_done = Signal(int, str) bookmark_error = Signal(str) autocomplete_done = Signal(list) - batch_progress = Signal(int, int) # current, total + batch_progress = Signal(int, int, int) # current, total, post_id (of the just-finished item) batch_done = Signal(str) download_progress = Signal(int, int) # bytes_downloaded, total_bytes prefetch_progress = Signal(int, float) # index, progress (0-1 or -1 to hide) diff --git a/booru_viewer/gui/main_window.py b/booru_viewer/gui/main_window.py index e88912b..9c1e14c 100644 --- a/booru_viewer/gui/main_window.py +++ b/booru_viewer/gui/main_window.py @@ -465,10 +465,10 @@ class BooruApp(QMainWindow): file_menu.addSeparator() - batch_action = QAction("Batch &Download Page...", self) - batch_action.setShortcut(QKeySequence("Ctrl+D")) - batch_action.triggered.connect(self._batch_download) - file_menu.addAction(batch_action) + self._batch_action = QAction("Batch &Download Page...", self) + self._batch_action.setShortcut(QKeySequence("Ctrl+D")) + self._batch_action.triggered.connect(self._batch_download) + file_menu.addAction(self._batch_action) file_menu.addSeparator() @@ -538,6 +538,11 @@ class BooruApp(QMainWindow): self._browse_btn.setChecked(index == 0) self._bookmark_btn.setChecked(index == 1) self._library_btn.setChecked(index == 2) + # Batch Download (Ctrl+D / File menu) only makes sense on browse — + # bookmarks and library tabs already show local files, downloading + # them again is meaningless. Disabling the QAction also disables + # its keyboard shortcut. + self._batch_action.setEnabled(index == 0) # Clear grid selections and current post to prevent cross-tab action conflicts # Preview media stays visible but actions are disabled until a new post is selected self._grid.clear_selection() @@ -2622,6 +2627,9 @@ class BooruApp(QMainWindow): self._run_async(_fav) def _batch_download_posts(self, posts: list, dest: str) -> None: + # Same _batch_dest stash as _batch_download — _on_batch_progress + # incrementally lights saved dots when dest is inside the library. + self._batch_dest = Path(dest) async def _batch(): for i, post in enumerate(posts): try: @@ -2631,7 +2639,7 @@ class BooruApp(QMainWindow): if not target.exists(): import shutil shutil.copy2(path, target) - self._signals.batch_progress.emit(i + 1, len(posts)) + self._signals.batch_progress.emit(i + 1, len(posts), post.id) except Exception as e: log.warning(f"Batch #{post.id} failed: {e}") self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest}") @@ -2814,6 +2822,9 @@ class BooruApp(QMainWindow): if not dest: return + # Stash dest so _on_batch_done can decide whether the destination + # is inside the library and the saved-dots need refreshing. + self._batch_dest = Path(dest) posts = list(self._posts) self._status.showMessage(f"Downloading {len(posts)} images...") @@ -2826,15 +2837,29 @@ class BooruApp(QMainWindow): if not target.exists(): import shutil shutil.copy2(path, target) - self._signals.batch_progress.emit(i + 1, len(posts)) + self._signals.batch_progress.emit(i + 1, len(posts), post.id) except Exception as e: log.warning(f"Batch #{post.id} failed: {e}") self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest}") self._run_async(_batch) - def _on_batch_progress(self, current: int, total: int) -> None: + def _on_batch_progress(self, current: int, total: int, post_id: int) -> None: self._status.showMessage(f"Downloading {current}/{total}...") + # Light the browse saved-dot for the just-finished post if the + # batch destination is inside the library. Runs per-post on the + # main thread (this is a Qt slot), so the dot appears as the + # files land instead of all at once when the batch completes. + dest = getattr(self, "_batch_dest", None) + if dest is None: + return + from ..core.config import saved_dir + if not dest.is_relative_to(saved_dir()): + return + for i, p in enumerate(self._posts): + if p.id == post_id and i < len(self._grid._thumbs): + self._grid._thumbs[i].set_saved_locally(True) + break # -- Toggles -- @@ -3101,6 +3126,9 @@ class BooruApp(QMainWindow): self._bookmarks_view.refresh() if self._stack.currentIndex() == 2: self._library_view.refresh() + # Saved-dot updates happen incrementally in _on_batch_progress as + # each file lands; this slot just clears the destination stash. + self._batch_dest = None def closeEvent(self, event) -> None: # Flush any pending splitter / window-state saves (debounce timers