Batch download: incremental saved-dot updates + browse-only gating

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
This commit is contained in:
pax 2026-04-08 16:10:26 -05:00
parent dbc530bb3c
commit 9455ff0f03
2 changed files with 36 additions and 8 deletions

View File

@ -23,7 +23,7 @@ class AsyncSignals(QObject):
bookmark_done = Signal(int, str) bookmark_done = Signal(int, str)
bookmark_error = Signal(str) bookmark_error = Signal(str)
autocomplete_done = Signal(list) 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) batch_done = Signal(str)
download_progress = Signal(int, int) # bytes_downloaded, total_bytes download_progress = Signal(int, int) # bytes_downloaded, total_bytes
prefetch_progress = Signal(int, float) # index, progress (0-1 or -1 to hide) prefetch_progress = Signal(int, float) # index, progress (0-1 or -1 to hide)

View File

@ -465,10 +465,10 @@ class BooruApp(QMainWindow):
file_menu.addSeparator() file_menu.addSeparator()
batch_action = QAction("Batch &Download Page...", self) self._batch_action = QAction("Batch &Download Page...", self)
batch_action.setShortcut(QKeySequence("Ctrl+D")) self._batch_action.setShortcut(QKeySequence("Ctrl+D"))
batch_action.triggered.connect(self._batch_download) self._batch_action.triggered.connect(self._batch_download)
file_menu.addAction(batch_action) file_menu.addAction(self._batch_action)
file_menu.addSeparator() file_menu.addSeparator()
@ -538,6 +538,11 @@ class BooruApp(QMainWindow):
self._browse_btn.setChecked(index == 0) self._browse_btn.setChecked(index == 0)
self._bookmark_btn.setChecked(index == 1) self._bookmark_btn.setChecked(index == 1)
self._library_btn.setChecked(index == 2) 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 # 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 # Preview media stays visible but actions are disabled until a new post is selected
self._grid.clear_selection() self._grid.clear_selection()
@ -2622,6 +2627,9 @@ class BooruApp(QMainWindow):
self._run_async(_fav) self._run_async(_fav)
def _batch_download_posts(self, posts: list, dest: str) -> None: 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(): async def _batch():
for i, post in enumerate(posts): for i, post in enumerate(posts):
try: try:
@ -2631,7 +2639,7 @@ class BooruApp(QMainWindow):
if not target.exists(): if not target.exists():
import shutil import shutil
shutil.copy2(path, target) 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: except Exception as e:
log.warning(f"Batch #{post.id} failed: {e}") log.warning(f"Batch #{post.id} failed: {e}")
self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest}") self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest}")
@ -2814,6 +2822,9 @@ class BooruApp(QMainWindow):
if not dest: if not dest:
return 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) posts = list(self._posts)
self._status.showMessage(f"Downloading {len(posts)} images...") self._status.showMessage(f"Downloading {len(posts)} images...")
@ -2826,15 +2837,29 @@ class BooruApp(QMainWindow):
if not target.exists(): if not target.exists():
import shutil import shutil
shutil.copy2(path, target) 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: except Exception as e:
log.warning(f"Batch #{post.id} failed: {e}") log.warning(f"Batch #{post.id} failed: {e}")
self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest}") self._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest}")
self._run_async(_batch) 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}...") 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 -- # -- Toggles --
@ -3101,6 +3126,9 @@ class BooruApp(QMainWindow):
self._bookmarks_view.refresh() self._bookmarks_view.refresh()
if self._stack.currentIndex() == 2: if self._stack.currentIndex() == 2:
self._library_view.refresh() 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: def closeEvent(self, event) -> None:
# Flush any pending splitter / window-state saves (debounce timers # Flush any pending splitter / window-state saves (debounce timers