bookmarks: fix save/unsave UX — no flash, correct dot indicators
Save to Library and Unsave from Library are now mutually exclusive in both single and multi-select context menus (previously both showed simultaneously). Replaced full grid refresh() after save/unsave with targeted dot updates — save_done signal fires per-post after async save completes and lights the saved dot on just that thumbnail. Unsave clears the dot inline. Eliminates the visible flash from grid rebuild. behavior change: context menus show either Save or Unsave, never both. Saved dots appear without grid flash.
This commit is contained in:
parent
5e8035cb1d
commit
79419794f6
@ -32,6 +32,7 @@ log = logging.getLogger("booru")
|
|||||||
|
|
||||||
class BookmarkThumbSignals(QObject):
|
class BookmarkThumbSignals(QObject):
|
||||||
thumb_ready = Signal(int, str)
|
thumb_ready = Signal(int, str)
|
||||||
|
save_done = Signal(int) # post_id
|
||||||
|
|
||||||
|
|
||||||
class BookmarksView(QWidget):
|
class BookmarksView(QWidget):
|
||||||
@ -48,6 +49,7 @@ class BookmarksView(QWidget):
|
|||||||
self._bookmarks: list[Bookmark] = []
|
self._bookmarks: list[Bookmark] = []
|
||||||
self._signals = BookmarkThumbSignals()
|
self._signals = BookmarkThumbSignals()
|
||||||
self._signals.thumb_ready.connect(self._on_thumb_ready, Qt.ConnectionType.QueuedConnection)
|
self._signals.thumb_ready.connect(self._on_thumb_ready, Qt.ConnectionType.QueuedConnection)
|
||||||
|
self._signals.save_done.connect(self._on_save_done, Qt.ConnectionType.QueuedConnection)
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
@ -236,6 +238,13 @@ class BookmarksView(QWidget):
|
|||||||
if not pix.isNull():
|
if not pix.isNull():
|
||||||
thumbs[index].set_pixmap(pix)
|
thumbs[index].set_pixmap(pix)
|
||||||
|
|
||||||
|
def _on_save_done(self, post_id: int) -> None:
|
||||||
|
"""Light the saved-locally dot on the thumbnail for post_id."""
|
||||||
|
for i, fav in enumerate(self._bookmarks):
|
||||||
|
if fav.post_id == post_id and i < len(self._grid._thumbs):
|
||||||
|
self._grid._thumbs[i].set_saved_locally(True)
|
||||||
|
break
|
||||||
|
|
||||||
def _do_search(self) -> None:
|
def _do_search(self) -> None:
|
||||||
text = self._search_input.text().strip()
|
text = self._search_input.text().strip()
|
||||||
self.refresh(search=text if text else None)
|
self.refresh(search=text if text else None)
|
||||||
@ -290,6 +299,7 @@ class BookmarksView(QWidget):
|
|||||||
async def _do():
|
async def _do():
|
||||||
try:
|
try:
|
||||||
await save_post_file(src, post, dest_dir, self._db)
|
await save_post_file(src, post, dest_dir, self._db)
|
||||||
|
self._signals.save_done.emit(fav.post_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"Bookmark→library save #{fav.post_id} failed: {e}")
|
log.warning(f"Bookmark→library save #{fav.post_id} failed: {e}")
|
||||||
|
|
||||||
@ -329,25 +339,25 @@ class BookmarksView(QWidget):
|
|||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
save_as = menu.addAction("Save As...")
|
save_as = menu.addAction("Save As...")
|
||||||
|
|
||||||
# Save to Library submenu — folders come from the library
|
# Save to Library / Unsave — mutually exclusive based on
|
||||||
# filesystem, not the bookmark folder DB.
|
# whether the post is already in the library.
|
||||||
from ..core.config import library_folders
|
from ..core.config import library_folders
|
||||||
|
save_lib_menu = None
|
||||||
|
save_lib_unsorted = None
|
||||||
|
save_lib_new = None
|
||||||
|
save_lib_folders = {}
|
||||||
|
unsave_lib = None
|
||||||
|
if self._db.is_post_in_library(fav.post_id):
|
||||||
|
unsave_lib = menu.addAction("Unsave from Library")
|
||||||
|
else:
|
||||||
save_lib_menu = menu.addMenu("Save to Library")
|
save_lib_menu = menu.addMenu("Save to Library")
|
||||||
save_lib_unsorted = save_lib_menu.addAction("Unfiled")
|
save_lib_unsorted = save_lib_menu.addAction("Unfiled")
|
||||||
save_lib_menu.addSeparator()
|
save_lib_menu.addSeparator()
|
||||||
save_lib_folders = {}
|
|
||||||
for folder in library_folders():
|
for folder in library_folders():
|
||||||
a = save_lib_menu.addAction(folder)
|
a = save_lib_menu.addAction(folder)
|
||||||
save_lib_folders[id(a)] = folder
|
save_lib_folders[id(a)] = folder
|
||||||
save_lib_menu.addSeparator()
|
save_lib_menu.addSeparator()
|
||||||
save_lib_new = save_lib_menu.addAction("+ New Folder...")
|
save_lib_new = save_lib_menu.addAction("+ New Folder...")
|
||||||
|
|
||||||
unsave_lib = None
|
|
||||||
# Only show unsave if the post is actually saved. is_post_in_library
|
|
||||||
# is the format-agnostic DB check — works for digit-stem and
|
|
||||||
# templated filenames alike.
|
|
||||||
if self._db.is_post_in_library(fav.post_id):
|
|
||||||
unsave_lib = menu.addAction("Unsave from Library")
|
|
||||||
copy_file = menu.addAction("Copy File to Clipboard")
|
copy_file = menu.addAction("Copy File to Clipboard")
|
||||||
copy_url = menu.addAction("Copy Image URL")
|
copy_url = menu.addAction("Copy Image URL")
|
||||||
copy_tags = menu.addAction("Copy Tags")
|
copy_tags = menu.addAction("Copy Tags")
|
||||||
@ -373,13 +383,9 @@ class BookmarksView(QWidget):
|
|||||||
|
|
||||||
if action == save_lib_unsorted:
|
if action == save_lib_unsorted:
|
||||||
self._copy_to_library_unsorted(fav)
|
self._copy_to_library_unsorted(fav)
|
||||||
self.refresh()
|
|
||||||
elif action == save_lib_new:
|
elif action == save_lib_new:
|
||||||
name, ok = QInputDialog.getText(self, "New Folder", "Folder name:")
|
name, ok = QInputDialog.getText(self, "New Folder", "Folder name:")
|
||||||
if ok and name.strip():
|
if ok and name.strip():
|
||||||
# Validate the name via saved_folder_dir() which mkdir's
|
|
||||||
# the library subdir and runs the path-traversal check.
|
|
||||||
# No DB folder write — bookmark folders are independent.
|
|
||||||
try:
|
try:
|
||||||
from ..core.config import saved_folder_dir
|
from ..core.config import saved_folder_dir
|
||||||
saved_folder_dir(name.strip())
|
saved_folder_dir(name.strip())
|
||||||
@ -387,11 +393,9 @@ class BookmarksView(QWidget):
|
|||||||
QMessageBox.warning(self, "Invalid Folder Name", str(e))
|
QMessageBox.warning(self, "Invalid Folder Name", str(e))
|
||||||
return
|
return
|
||||||
self._copy_to_library(fav, name.strip())
|
self._copy_to_library(fav, name.strip())
|
||||||
self.refresh()
|
|
||||||
elif id(action) in save_lib_folders:
|
elif id(action) in save_lib_folders:
|
||||||
folder_name = save_lib_folders[id(action)]
|
folder_name = save_lib_folders[id(action)]
|
||||||
self._copy_to_library(fav, folder_name)
|
self._copy_to_library(fav, folder_name)
|
||||||
self.refresh()
|
|
||||||
elif action == open_browser:
|
elif action == open_browser:
|
||||||
self.open_in_browser_requested.emit(fav.site_id, fav.post_id)
|
self.open_in_browser_requested.emit(fav.site_id, fav.post_id)
|
||||||
elif action == open_default:
|
elif action == open_default:
|
||||||
@ -421,12 +425,11 @@ class BookmarksView(QWidget):
|
|||||||
run_on_app_loop(_do_save_as())
|
run_on_app_loop(_do_save_as())
|
||||||
elif action == unsave_lib:
|
elif action == unsave_lib:
|
||||||
from ..core.cache import delete_from_library
|
from ..core.cache import delete_from_library
|
||||||
# Pass db so templated filenames are matched and the meta
|
|
||||||
# row gets cleaned up. Refresh on success OR on a meta-only
|
|
||||||
# cleanup (orphan row, no on-disk file) — either way the
|
|
||||||
# saved-dot indicator state has changed.
|
|
||||||
delete_from_library(fav.post_id, db=self._db)
|
delete_from_library(fav.post_id, db=self._db)
|
||||||
self.refresh()
|
for i, f in enumerate(self._bookmarks):
|
||||||
|
if f.post_id == fav.post_id and i < len(self._grid._thumbs):
|
||||||
|
self._grid._thumbs[i].set_saved_locally(False)
|
||||||
|
break
|
||||||
self.bookmarks_changed.emit()
|
self.bookmarks_changed.emit()
|
||||||
elif action == copy_file:
|
elif action == copy_file:
|
||||||
path = fav.cached_path
|
path = fav.cached_path
|
||||||
@ -477,20 +480,24 @@ class BookmarksView(QWidget):
|
|||||||
|
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
|
|
||||||
# Save All to Library submenu — folders are filesystem-truth.
|
any_unsaved = any(not self._db.is_post_in_library(f.post_id) for f in favs)
|
||||||
# Conversion from a flat action to a submenu so the user can
|
any_saved = any(self._db.is_post_in_library(f.post_id) for f in favs)
|
||||||
# pick a destination instead of having "save all" silently use
|
|
||||||
# each bookmark's fav.folder (which was the cross-bleed bug).
|
save_lib_menu = None
|
||||||
|
save_lib_unsorted = None
|
||||||
|
save_lib_new = None
|
||||||
|
save_lib_folder_actions: dict[int, str] = {}
|
||||||
|
unsave_all = None
|
||||||
|
if any_unsaved:
|
||||||
save_lib_menu = menu.addMenu(f"Save All ({len(favs)}) to Library")
|
save_lib_menu = menu.addMenu(f"Save All ({len(favs)}) to Library")
|
||||||
save_lib_unsorted = save_lib_menu.addAction("Unfiled")
|
save_lib_unsorted = save_lib_menu.addAction("Unfiled")
|
||||||
save_lib_menu.addSeparator()
|
save_lib_menu.addSeparator()
|
||||||
save_lib_folder_actions: dict[int, str] = {}
|
|
||||||
for folder in library_folders():
|
for folder in library_folders():
|
||||||
a = save_lib_menu.addAction(folder)
|
a = save_lib_menu.addAction(folder)
|
||||||
save_lib_folder_actions[id(a)] = folder
|
save_lib_folder_actions[id(a)] = folder
|
||||||
save_lib_menu.addSeparator()
|
save_lib_menu.addSeparator()
|
||||||
save_lib_new = save_lib_menu.addAction("+ New Folder...")
|
save_lib_new = save_lib_menu.addAction("+ New Folder...")
|
||||||
|
if any_saved:
|
||||||
unsave_all = menu.addAction(f"Unsave All ({len(favs)}) from Library")
|
unsave_all = menu.addAction(f"Unsave All ({len(favs)}) from Library")
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
@ -516,7 +523,6 @@ class BookmarksView(QWidget):
|
|||||||
self._copy_to_library(fav, folder_name)
|
self._copy_to_library(fav, folder_name)
|
||||||
else:
|
else:
|
||||||
self._copy_to_library_unsorted(fav)
|
self._copy_to_library_unsorted(fav)
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
if action == save_lib_unsorted:
|
if action == save_lib_unsorted:
|
||||||
_save_all_into(None)
|
_save_all_into(None)
|
||||||
@ -534,9 +540,13 @@ class BookmarksView(QWidget):
|
|||||||
_save_all_into(save_lib_folder_actions[id(action)])
|
_save_all_into(save_lib_folder_actions[id(action)])
|
||||||
elif action == unsave_all:
|
elif action == unsave_all:
|
||||||
from ..core.cache import delete_from_library
|
from ..core.cache import delete_from_library
|
||||||
|
unsaved_ids = set()
|
||||||
for fav in favs:
|
for fav in favs:
|
||||||
delete_from_library(fav.post_id, db=self._db)
|
delete_from_library(fav.post_id, db=self._db)
|
||||||
self.refresh()
|
unsaved_ids.add(fav.post_id)
|
||||||
|
for i, fav in enumerate(self._bookmarks):
|
||||||
|
if fav.post_id in unsaved_ids and i < len(self._grid._thumbs):
|
||||||
|
self._grid._thumbs[i].set_saved_locally(False)
|
||||||
self.bookmarks_changed.emit()
|
self.bookmarks_changed.emit()
|
||||||
elif action == move_none:
|
elif action == move_none:
|
||||||
for fav in favs:
|
for fav in favs:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user