From db774fc33e3df68213a8ac03ebf87d08ec0e1871 Mon Sep 17 00:00:00 2001 From: pax Date: Wed, 8 Apr 2026 15:59:46 -0500 Subject: [PATCH] Browse multi-select: split library + bookmark actions, conditional visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The browse grid's multi-select right-click menu collapsed library and bookmark actions into a single "Remove All Bookmarks" entry that did *both* — it called delete_from_library and remove_bookmark per post, and was unconditionally visible regardless of selection state. Two problems: 1. There was no way to bulk-unsave files from the library without also stripping the bookmarks. Saved-but-not-bookmarked posts had no bulk-unsave path at all. 2. The single misleadingly-named action didn't match the single-post right-click menu's clean separation of "Save to Library / Unsave from Library" vs. "Bookmark as / Remove Bookmark". Reshape: split into four distinct actions, each with symmetric conditional visibility: - Save All to Library → shown only if any post is unsaved - Unsave All from Library → shown only if any post is saved (NEW) - Bookmark All → shown only if any post is unbookmarked - Remove All Bookmarks → shown only if any post is bookmarked Mixed selections show whichever subset of the four is relevant. The new Unsave All from Library calls a new _bulk_unsave method that mirrors the _bulk_save shape but synchronously (delete_from_library is a filesystem op, no httpx round-trip). Remove All Bookmarks now *only* removes bookmarks — it no longer touches the library, matching the single-post Remove Bookmark action's scope. Always-shown actions (Download All, Copy All URLs) stay below a separator at the bottom. Verified: - Multi-select unbookmarked+unsaved posts → only Save All / Bookmark All - Multi-select saved-not-bookmarked → only Unsave All / Bookmark All - Multi-select bookmarked+saved → only Unsave All / Remove All Bookmarks - Mixed selection → all four appear - Unsave All from Library removes files, leaves bookmarks - Remove All Bookmarks removes bookmarks, leaves files --- booru_viewer/gui/main_window.py | 105 +++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 23 deletions(-) diff --git a/booru_viewer/gui/main_window.py b/booru_viewer/gui/main_window.py index a545a85..9484dd4 100644 --- a/booru_viewer/gui/main_window.py +++ b/booru_viewer/gui/main_window.py @@ -2411,28 +2411,65 @@ class BooruApp(QMainWindow): return False def _on_multi_context_menu(self, indices: list, pos) -> None: - """Context menu for multi-selected posts.""" + """Context menu for multi-selected posts. + + Library and bookmark actions are split into independent + save/unsave and bookmark/remove-bookmark pairs (mirroring the + single-post menu's separation), with symmetric conditional + visibility: each action only appears when the selection actually + contains posts the action would affect. Save All to Library + appears only when at least one post is unsaved; Unsave All from + Library only when at least one is saved; Bookmark All only when + at least one is unbookmarked; Remove All Bookmarks only when at + least one is bookmarked. + """ posts = [self._posts[i] for i in indices if 0 <= i < len(self._posts)] if not posts: return count = len(posts) + site_id = self._site_combo.currentData() + any_bookmarked = bool(site_id) and any(self._db.is_bookmarked(site_id, p.id) for p in posts) + any_unbookmarked = bool(site_id) and any(not self._db.is_bookmarked(site_id, p.id) for p in posts) + any_saved = any(self._is_post_saved(p.id) for p in posts) + any_unsaved = any(not self._is_post_saved(p.id) for p in posts) + menu = QMenu(self) - fav_all = menu.addAction(f"Bookmark All ({count})") - save_menu = menu.addMenu(f"Save All to Library ({count})") - save_unsorted = save_menu.addAction("Unfiled") - save_folder_actions = {} - from ..core.config import library_folders - for folder in library_folders(): - a = save_menu.addAction(folder) - save_folder_actions[id(a)] = folder - save_menu.addSeparator() - save_new = save_menu.addAction("+ New Folder...") + # Library section + save_menu = None + save_unsorted = None + save_new = None + save_folder_actions: dict[int, str] = {} + if any_unsaved: + from ..core.config import library_folders + save_menu = menu.addMenu(f"Save All to Library ({count})") + save_unsorted = save_menu.addAction("Unfiled") + for folder in library_folders(): + a = save_menu.addAction(folder) + save_folder_actions[id(a)] = folder + save_menu.addSeparator() + save_new = save_menu.addAction("+ New Folder...") - menu.addSeparator() - unfav_all = menu.addAction(f"Remove All Bookmarks ({count})") - menu.addSeparator() + unsave_lib_all = None + if any_saved: + unsave_lib_all = menu.addAction(f"Unsave All from Library ({count})") + + # Bookmark section + if (any_unsaved or any_saved) and (any_unbookmarked or any_bookmarked): + menu.addSeparator() + + fav_all = None + if any_unbookmarked: + fav_all = menu.addAction(f"Bookmark All ({count})") + + unfav_all = None + if any_bookmarked: + unfav_all = menu.addAction(f"Remove All Bookmarks ({count})") + + # Always-shown actions + if any_unsaved or any_saved or any_unbookmarked or any_bookmarked: + menu.addSeparator() batch_dl = menu.addAction(f"Download All ({count})...") copy_urls = menu.addAction("Copy All URLs") @@ -2440,11 +2477,11 @@ class BooruApp(QMainWindow): if not action: return - if action == fav_all: + if fav_all is not None and action == fav_all: self._bulk_bookmark(indices, posts) - elif action == save_unsorted: + elif save_unsorted is not None and action == save_unsorted: self._bulk_save(indices, posts, None) - elif action == save_new: + elif save_new is not None and action == save_new: from PySide6.QtWidgets import QInputDialog, QMessageBox name, ok = QInputDialog.getText(self, "New Folder", "Folder name:") if ok and name.strip(): @@ -2457,25 +2494,24 @@ class BooruApp(QMainWindow): self._bulk_save(indices, posts, name.strip()) elif id(action) in save_folder_actions: self._bulk_save(indices, posts, save_folder_actions[id(action)]) + elif unsave_lib_all is not None and action == unsave_lib_all: + self._bulk_unsave(indices, posts) elif action == batch_dl: from .dialogs import select_directory dest = select_directory(self, "Download to folder") if dest: self._batch_download_posts(posts, dest) - elif action == unfav_all: - site_id = self._site_combo.currentData() + elif unfav_all is not None and action == unfav_all: if site_id: - from ..core.cache import delete_from_library for post in posts: - # Single call now walks every library folder by post id. - delete_from_library(post.id) self._db.remove_bookmark(site_id, post.id) for idx in indices: if 0 <= idx < len(self._grid._thumbs): self._grid._thumbs[idx].set_bookmarked(False) - self._grid._thumbs[idx].set_saved_locally(False) self._grid._clear_multi() self._status.showMessage(f"Removed {count} bookmarks") + if self._stack.currentIndex() == 1: + self._bookmarks_view.refresh() elif action == copy_urls: urls = "\n".join(p.file_url for p in posts) QApplication.clipboard().setText(urls) @@ -2537,6 +2573,29 @@ class BooruApp(QMainWindow): self._run_async(_do) + def _bulk_unsave(self, indices: list[int], posts: list[Post]) -> None: + """Bulk-remove selected posts from the library. + + Mirrors `_bulk_save` shape but synchronously — `delete_from_library` + is a filesystem op, no httpx round-trip needed. Touches only the + library (filesystem); bookmarks are a separate DB-backed concept + and stay untouched. The grid's saved-locally dot clears for every + selection slot regardless of whether the file was actually present + — the user's intent is "make these not-saved", and a missing file + is already not-saved. + """ + from ..core.cache import delete_from_library + for post in posts: + delete_from_library(post.id) + for idx in indices: + if 0 <= idx < len(self._grid._thumbs): + self._grid._thumbs[idx].set_saved_locally(False) + self._grid._clear_multi() + self._status.showMessage(f"Removed {len(posts)} from library") + if self._stack.currentIndex() == 2: + self._library_view.refresh() + self._update_fullscreen_state() + def _ensure_bookmarked(self, post: Post) -> None: """Bookmark a post if not already bookmarked.""" site_id = self._site_combo.currentData()