unsave_from_preview only refreshed the library grid when on the library tab. Now also refreshes the bookmarks grid when on the bookmarks tab so the saved dot clears immediately.
611 lines
27 KiB
Python
611 lines
27 KiB
Python
"""Bookmark, save/library, batch download, and blacklist operations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ..core.cache import download_image
|
|
|
|
if TYPE_CHECKING:
|
|
from .main_window import BooruApp
|
|
|
|
log = logging.getLogger("booru")
|
|
|
|
|
|
# Pure functions
|
|
|
|
def is_batch_message(msg: str) -> bool:
|
|
"""Detect batch progress messages like 'Saved 3/10 to Unfiled'."""
|
|
return "/" in msg and any(c.isdigit() for c in msg.split("/")[0][-2:])
|
|
|
|
def is_in_library(path: Path, saved_root: Path) -> bool:
|
|
"""Check if path is inside the library root."""
|
|
try:
|
|
return path.is_relative_to(saved_root)
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
|
|
class PostActionsController:
|
|
def __init__(self, app: BooruApp) -> None:
|
|
self._app = app
|
|
self._batch_dest: Path | None = None
|
|
|
|
def on_bookmark_error(self, e: str) -> None:
|
|
self._app._status.showMessage(f"Error: {e}")
|
|
|
|
def is_post_saved(self, post_id: int) -> bool:
|
|
return self._app._db.is_post_in_library(post_id)
|
|
|
|
def _maybe_unbookmark(self, post) -> None:
|
|
"""Remove the bookmark for *post* if the unbookmark-on-save setting is on.
|
|
|
|
Handles DB removal, grid thumbnail dot, preview state, bookmarks
|
|
tab refresh, and popout sync in one place so every save path
|
|
(single, bulk, Save As, batch download) can call it.
|
|
"""
|
|
if not self._app._db.get_setting_bool("unbookmark_on_save"):
|
|
return
|
|
site_id = (
|
|
self._app._preview._current_site_id
|
|
or self._app._site_combo.currentData()
|
|
)
|
|
if not site_id or not self._app._db.is_bookmarked(site_id, post.id):
|
|
return
|
|
self._app._db.remove_bookmark(site_id, post.id)
|
|
# Update grid thumbnail bookmark dot
|
|
for i, p in enumerate(self._app._posts):
|
|
if p.id == post.id and i < len(self._app._grid._thumbs):
|
|
self._app._grid._thumbs[i].set_bookmarked(False)
|
|
break
|
|
# Update preview and popout
|
|
if (self._app._preview._current_post
|
|
and self._app._preview._current_post.id == post.id):
|
|
self._app._preview.update_bookmark_state(False)
|
|
self._app._popout_ctrl.update_state()
|
|
# Refresh bookmarks tab if visible
|
|
if self._app._stack.currentIndex() == 1:
|
|
self._app._bookmarks_view.refresh()
|
|
|
|
def get_preview_post(self):
|
|
idx = self._app._grid.selected_index
|
|
if 0 <= idx < len(self._app._posts):
|
|
return self._app._posts[idx], idx
|
|
if self._app._preview._current_post:
|
|
return self._app._preview._current_post, -1
|
|
return None, -1
|
|
|
|
def bookmark_from_preview(self) -> None:
|
|
post, idx = self.get_preview_post()
|
|
if not post:
|
|
return
|
|
site_id = self._app._preview._current_site_id or self._app._site_combo.currentData()
|
|
if not site_id:
|
|
return
|
|
if idx >= 0:
|
|
self.toggle_bookmark(idx)
|
|
else:
|
|
if self._app._db.is_bookmarked(site_id, post.id):
|
|
self._app._db.remove_bookmark(site_id, post.id)
|
|
else:
|
|
from ..core.cache import cached_path_for
|
|
cached = cached_path_for(post.file_url)
|
|
self._app._db.add_bookmark(
|
|
site_id=site_id, post_id=post.id,
|
|
file_url=post.file_url, preview_url=post.preview_url or "",
|
|
tags=post.tags, rating=post.rating, score=post.score,
|
|
source=post.source, cached_path=str(cached) if cached.exists() else None,
|
|
tag_categories=post.tag_categories,
|
|
)
|
|
bookmarked = bool(self._app._db.is_bookmarked(site_id, post.id))
|
|
self._app._preview.update_bookmark_state(bookmarked)
|
|
self._app._popout_ctrl.update_state()
|
|
if self._app._stack.currentIndex() == 1:
|
|
self._app._bookmarks_view.refresh()
|
|
|
|
def bookmark_to_folder_from_preview(self, folder: str) -> None:
|
|
"""Bookmark the current preview post into a specific bookmark folder.
|
|
|
|
Triggered by the toolbar Bookmark-as submenu, which only shows
|
|
when the post is not yet bookmarked -- so this method only handles
|
|
the create path, never the move/remove paths. Empty string means
|
|
Unfiled. Brand-new folder names get added to the DB folder list
|
|
first so the bookmarks tab combo immediately shows them.
|
|
"""
|
|
post, idx = self.get_preview_post()
|
|
if not post:
|
|
return
|
|
site_id = self._app._preview._current_site_id or self._app._site_combo.currentData()
|
|
if not site_id:
|
|
return
|
|
target = folder if folder else None
|
|
if target and target not in self._app._db.get_folders():
|
|
try:
|
|
self._app._db.add_folder(target)
|
|
except ValueError as e:
|
|
self._app._status.showMessage(f"Invalid folder name: {e}")
|
|
return
|
|
if idx >= 0:
|
|
# In the grid -- go through toggle_bookmark so the grid
|
|
# thumbnail's bookmark badge updates via on_bookmark_done.
|
|
self.toggle_bookmark(idx, target)
|
|
else:
|
|
# Preview-only post (e.g. opened from the bookmarks tab while
|
|
# browse is empty). Inline the add -- no grid index to update.
|
|
from ..core.cache import cached_path_for
|
|
cached = cached_path_for(post.file_url)
|
|
self._app._db.add_bookmark(
|
|
site_id=site_id, post_id=post.id,
|
|
file_url=post.file_url, preview_url=post.preview_url or "",
|
|
tags=post.tags, rating=post.rating, score=post.score,
|
|
source=post.source,
|
|
cached_path=str(cached) if cached.exists() else None,
|
|
folder=target,
|
|
tag_categories=post.tag_categories,
|
|
)
|
|
where = target or "Unfiled"
|
|
self._app._status.showMessage(f"Bookmarked #{post.id} to {where}")
|
|
self._app._preview.update_bookmark_state(True)
|
|
self._app._popout_ctrl.update_state()
|
|
# Refresh bookmarks tab if visible so the new entry appears.
|
|
if self._app._stack.currentIndex() == 1:
|
|
self._app._bookmarks_view.refresh()
|
|
|
|
def save_from_preview(self, folder: str) -> None:
|
|
post, idx = self.get_preview_post()
|
|
if post:
|
|
target = folder if folder else None
|
|
self.save_to_library(post, target)
|
|
|
|
def toggle_save_from_preview(self) -> None:
|
|
"""Toggle library save: unsave if already saved, save to Unfiled otherwise."""
|
|
post, _ = self.get_preview_post()
|
|
if not post:
|
|
return
|
|
if self.is_post_saved(post.id):
|
|
self.unsave_from_preview()
|
|
else:
|
|
self.save_from_preview("")
|
|
|
|
def unsave_from_preview(self) -> None:
|
|
post, idx = self.get_preview_post()
|
|
if not post:
|
|
return
|
|
# delete_from_library walks every library folder by post id and
|
|
# deletes every match in one call -- no folder hint needed. Pass
|
|
# db so templated filenames also get unlinked AND the meta row
|
|
# gets cleaned up.
|
|
from ..core.cache import delete_from_library
|
|
deleted = delete_from_library(post.id, db=self._app._db)
|
|
if deleted:
|
|
self._app._status.showMessage(f"Removed #{post.id} from library")
|
|
self._app._preview.update_save_state(False)
|
|
# Update browse grid thumbnail saved dot
|
|
for i, p in enumerate(self._app._posts):
|
|
if p.id == post.id and i < len(self._app._grid._thumbs):
|
|
self._app._grid._thumbs[i].set_saved_locally(False)
|
|
break
|
|
# Update bookmarks grid thumbnail
|
|
bm_grid = self._app._bookmarks_view._grid
|
|
for i, fav in enumerate(self._app._bookmarks_view._bookmarks):
|
|
if fav.post_id == post.id and i < len(bm_grid._thumbs):
|
|
bm_grid._thumbs[i].set_saved_locally(False)
|
|
break
|
|
# Refresh the active tab's grid so the unsaved post disappears
|
|
# from library or loses its saved dot on bookmarks.
|
|
if self._app._stack.currentIndex() == 2:
|
|
self._app._library_view.refresh()
|
|
elif self._app._stack.currentIndex() == 1:
|
|
self._app._bookmarks_view.refresh()
|
|
else:
|
|
self._app._status.showMessage(f"#{post.id} not in library")
|
|
self._app._popout_ctrl.update_state()
|
|
|
|
def blacklist_tag_from_popout(self, tag: str) -> None:
|
|
from PySide6.QtWidgets import QMessageBox
|
|
reply = QMessageBox.question(
|
|
self._app, "Blacklist Tag",
|
|
f"Blacklist tag \"{tag}\"?\nPosts with this tag will be hidden.",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
)
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
self._app._db.add_blacklisted_tag(tag)
|
|
self._app._db.set_setting("blacklist_enabled", "1")
|
|
self._app._status.showMessage(f"Blacklisted: {tag}")
|
|
self._app._search_ctrl.remove_blacklisted_from_grid(tag=tag)
|
|
|
|
def blacklist_post_from_popout(self) -> None:
|
|
post, idx = self.get_preview_post()
|
|
if post:
|
|
from PySide6.QtWidgets import QMessageBox
|
|
reply = QMessageBox.question(
|
|
self._app, "Blacklist Post",
|
|
f"Blacklist post #{post.id}?\nThis post will be hidden from results.",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
)
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
self._app._db.add_blacklisted_post(post.file_url)
|
|
self._app._status.showMessage(f"Post #{post.id} blacklisted")
|
|
self._app._search_ctrl.remove_blacklisted_from_grid(post_url=post.file_url)
|
|
|
|
def toggle_bookmark(self, index: int, folder: str | None = None) -> None:
|
|
"""Toggle the bookmark state of post at `index`.
|
|
|
|
When `folder` is given and the post is not yet bookmarked, the
|
|
new bookmark is filed under that bookmark folder. The folder
|
|
arg is ignored when removing -- bookmark folder membership is
|
|
moot if the bookmark itself is going away.
|
|
"""
|
|
post = self._app._posts[index]
|
|
site_id = self._app._site_combo.currentData()
|
|
if not site_id:
|
|
return
|
|
|
|
if self._app._db.is_bookmarked(site_id, post.id):
|
|
self._app._db.remove_bookmark(site_id, post.id)
|
|
self._app._search_ctrl.invalidate_lookup_caches()
|
|
self._app._status.showMessage(f"Unbookmarked #{post.id}")
|
|
thumbs = self._app._grid._thumbs
|
|
if 0 <= index < len(thumbs):
|
|
thumbs[index].set_bookmarked(False)
|
|
else:
|
|
self._app._status.showMessage(f"Bookmarking #{post.id}...")
|
|
|
|
async def _fav():
|
|
try:
|
|
path = await download_image(post.file_url)
|
|
self._app._db.add_bookmark(
|
|
site_id=site_id,
|
|
post_id=post.id,
|
|
file_url=post.file_url,
|
|
preview_url=post.preview_url,
|
|
tags=post.tags,
|
|
rating=post.rating,
|
|
score=post.score,
|
|
source=post.source,
|
|
cached_path=str(path),
|
|
folder=folder,
|
|
tag_categories=post.tag_categories,
|
|
)
|
|
where = folder or "Unfiled"
|
|
self._app._signals.bookmark_done.emit(index, f"Bookmarked #{post.id} to {where}")
|
|
except Exception as e:
|
|
self._app._signals.bookmark_error.emit(str(e))
|
|
|
|
self._app._run_async(_fav)
|
|
|
|
def bulk_bookmark(self, indices: list[int], posts: list) -> None:
|
|
site_id = self._app._site_combo.currentData()
|
|
if not site_id:
|
|
return
|
|
self._app._status.showMessage(f"Bookmarking {len(posts)}...")
|
|
|
|
async def _do():
|
|
for i, (idx, post) in enumerate(zip(indices, posts)):
|
|
if self._app._db.is_bookmarked(site_id, post.id):
|
|
continue
|
|
try:
|
|
path = await download_image(post.file_url)
|
|
self._app._db.add_bookmark(
|
|
site_id=site_id, post_id=post.id,
|
|
file_url=post.file_url, preview_url=post.preview_url,
|
|
tags=post.tags, rating=post.rating, score=post.score,
|
|
source=post.source, cached_path=str(path),
|
|
tag_categories=post.tag_categories,
|
|
)
|
|
self._app._signals.bookmark_done.emit(idx, f"Bookmarked {i+1}/{len(posts)}")
|
|
except Exception as e:
|
|
log.warning(f"Operation failed: {e}")
|
|
self._app._signals.batch_done.emit(f"Bookmarked {len(posts)} posts")
|
|
|
|
self._app._run_async(_do)
|
|
|
|
def bulk_save(self, indices: list[int], posts: list, folder: str | None) -> None:
|
|
"""Bulk-save the selected posts into the library, optionally inside a subfolder.
|
|
|
|
Each iteration routes through save_post_file with a shared
|
|
in_flight set so template-collision-prone batches (e.g.
|
|
%artist% on a page that has many posts by the same artist) get
|
|
sequential _1, _2, _3 suffixes instead of clobbering each other.
|
|
"""
|
|
from ..core.config import saved_dir, saved_folder_dir
|
|
from ..core.library_save import save_post_file
|
|
|
|
where = folder or "Unfiled"
|
|
self._app._status.showMessage(f"Saving {len(posts)} to {where}...")
|
|
try:
|
|
dest_dir = saved_folder_dir(folder) if folder else saved_dir()
|
|
except ValueError as e:
|
|
self._app._status.showMessage(f"Invalid folder name: {e}")
|
|
return
|
|
|
|
in_flight: set[str] = set()
|
|
|
|
async def _do():
|
|
fetcher = self._app._get_category_fetcher()
|
|
for i, (idx, post) in enumerate(zip(indices, posts)):
|
|
try:
|
|
src = Path(await download_image(post.file_url))
|
|
await save_post_file(src, post, dest_dir, self._app._db, in_flight, category_fetcher=fetcher)
|
|
self.copy_library_thumb(post)
|
|
self._app._signals.bookmark_done.emit(idx, f"Saved {i+1}/{len(posts)} to {where}")
|
|
self._maybe_unbookmark(post)
|
|
except Exception as e:
|
|
log.warning(f"Bulk save #{post.id} failed: {e}")
|
|
self._app._signals.batch_done.emit(f"Saved {len(posts)} to {where}")
|
|
|
|
self._app._run_async(_do)
|
|
|
|
def bulk_unsave(self, indices: list[int], posts: list) -> 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, db=self._app._db)
|
|
for idx in indices:
|
|
if 0 <= idx < len(self._app._grid._thumbs):
|
|
self._app._grid._thumbs[idx].set_saved_locally(False)
|
|
self._app._grid._clear_multi()
|
|
self._app._status.showMessage(f"Removed {len(posts)} from library")
|
|
if self._app._stack.currentIndex() == 2:
|
|
self._app._library_view.refresh()
|
|
self._app._popout_ctrl.update_state()
|
|
|
|
def ensure_bookmarked(self, post) -> None:
|
|
"""Bookmark a post if not already bookmarked."""
|
|
site_id = self._app._site_combo.currentData()
|
|
if not site_id or self._app._db.is_bookmarked(site_id, post.id):
|
|
return
|
|
|
|
async def _fav():
|
|
try:
|
|
path = await download_image(post.file_url)
|
|
self._app._db.add_bookmark(
|
|
site_id=site_id,
|
|
post_id=post.id,
|
|
file_url=post.file_url,
|
|
preview_url=post.preview_url,
|
|
tags=post.tags,
|
|
rating=post.rating,
|
|
score=post.score,
|
|
source=post.source,
|
|
cached_path=str(path),
|
|
)
|
|
except Exception as e:
|
|
log.warning(f"Operation failed: {e}")
|
|
|
|
self._app._run_async(_fav)
|
|
|
|
def batch_download_posts(self, posts: list, dest: str) -> None:
|
|
"""Multi-select Download All entry point. Delegates to
|
|
batch_download_to so the in_flight set, library_meta write,
|
|
and saved-dots refresh share one implementation."""
|
|
self.batch_download_to(posts, Path(dest))
|
|
|
|
def batch_download_to(self, posts: list, dest_dir: Path) -> None:
|
|
"""Download `posts` into `dest_dir`, routing each save through
|
|
save_post_file with a shared in_flight set so collision-prone
|
|
templates produce sequential _1, _2 suffixes within the batch.
|
|
|
|
Stashes `dest_dir` on `self._batch_dest` so on_batch_progress
|
|
and on_batch_done can decide whether the destination is inside
|
|
the library and the saved-dots need refreshing. The library_meta
|
|
write happens automatically inside save_post_file when dest_dir
|
|
is inside saved_dir() -- fixes the v0.2.3 latent bug where batch
|
|
downloads into a library folder left files unregistered.
|
|
"""
|
|
from ..core.library_save import save_post_file
|
|
|
|
self._batch_dest = dest_dir
|
|
self._app._status.showMessage(f"Downloading {len(posts)} images...")
|
|
in_flight: set[str] = set()
|
|
|
|
async def _batch():
|
|
fetcher = self._app._get_category_fetcher()
|
|
for i, post in enumerate(posts):
|
|
try:
|
|
src = Path(await download_image(post.file_url))
|
|
await save_post_file(src, post, dest_dir, self._app._db, in_flight, category_fetcher=fetcher)
|
|
self._app._signals.batch_progress.emit(i + 1, len(posts), post.id)
|
|
self._maybe_unbookmark(post)
|
|
except Exception as e:
|
|
log.warning(f"Batch #{post.id} failed: {e}")
|
|
self._app._signals.batch_done.emit(f"Downloaded {len(posts)} images to {dest_dir}")
|
|
|
|
self._app._run_async(_batch)
|
|
|
|
def batch_download(self) -> None:
|
|
if not self._app._posts:
|
|
self._app._status.showMessage("No posts to download")
|
|
return
|
|
from .dialogs import select_directory
|
|
dest = select_directory(self._app, "Download to folder")
|
|
if not dest:
|
|
return
|
|
self.batch_download_to(list(self._app._posts), Path(dest))
|
|
|
|
def is_current_bookmarked(self, index: int) -> bool:
|
|
site_id = self._app._site_combo.currentData()
|
|
if not site_id or index < 0 or index >= len(self._app._posts):
|
|
return False
|
|
return self._app._db.is_bookmarked(site_id, self._app._posts[index].id)
|
|
|
|
def copy_library_thumb(self, post) -> None:
|
|
"""Copy a post's browse thumbnail into the library thumbnail
|
|
cache so the Library tab can paint it without re-downloading.
|
|
No-op if there's no preview_url or the source thumb isn't cached."""
|
|
if not post.preview_url:
|
|
return
|
|
from ..core.config import thumbnails_dir
|
|
from ..core.cache import cached_path_for
|
|
thumb_src = cached_path_for(post.preview_url, thumbnails_dir())
|
|
if not thumb_src.exists():
|
|
return
|
|
lib_thumb_dir = thumbnails_dir() / "library"
|
|
lib_thumb_dir.mkdir(parents=True, exist_ok=True)
|
|
lib_thumb = lib_thumb_dir / f"{post.id}.jpg"
|
|
if not lib_thumb.exists():
|
|
import shutil
|
|
shutil.copy2(thumb_src, lib_thumb)
|
|
|
|
def save_to_library(self, post, folder: str | None) -> None:
|
|
"""Save a post into the library, optionally inside a subfolder.
|
|
|
|
Routes through the unified save_post_file flow so the filename
|
|
template, sequential collision suffixes, same-post idempotency,
|
|
and library_meta write are all handled in one place. Re-saving
|
|
the same post into the same folder is a no-op (idempotent);
|
|
saving into a different folder produces a second copy without
|
|
touching the first.
|
|
"""
|
|
from ..core.config import saved_dir, saved_folder_dir
|
|
from ..core.library_save import save_post_file
|
|
|
|
self._app._status.showMessage(f"Saving #{post.id} to library...")
|
|
try:
|
|
dest_dir = saved_folder_dir(folder) if folder else saved_dir()
|
|
except ValueError as e:
|
|
self._app._status.showMessage(f"Invalid folder name: {e}")
|
|
return
|
|
|
|
async def _save():
|
|
try:
|
|
src = Path(await download_image(post.file_url))
|
|
await save_post_file(src, post, dest_dir, self._app._db, category_fetcher=self._app._get_category_fetcher())
|
|
self.copy_library_thumb(post)
|
|
where = folder or "Unfiled"
|
|
self._app._signals.bookmark_done.emit(
|
|
self._app._grid.selected_index,
|
|
f"Saved #{post.id} to {where}",
|
|
)
|
|
self._maybe_unbookmark(post)
|
|
except Exception as e:
|
|
self._app._signals.bookmark_error.emit(str(e))
|
|
|
|
self._app._run_async(_save)
|
|
|
|
def save_as(self, post) -> None:
|
|
"""Open a Save As dialog for a single post and write the file
|
|
through the unified save_post_file flow.
|
|
|
|
The default name in the dialog comes from rendering the user's
|
|
library_filename_template against the post; the user can edit
|
|
before confirming. If the chosen destination ends up inside
|
|
saved_dir(), save_post_file registers a library_meta row --
|
|
a behavior change from v0.2.3 (where Save As never wrote meta
|
|
regardless of destination)."""
|
|
from ..core.cache import cached_path_for
|
|
from ..core.config import render_filename_template
|
|
from ..core.library_save import save_post_file
|
|
from .dialogs import save_file
|
|
|
|
src = cached_path_for(post.file_url)
|
|
if not src.exists():
|
|
self._app._status.showMessage("Image not cached — double-click to download first")
|
|
return
|
|
ext = src.suffix
|
|
template = self._app._db.get_setting("library_filename_template")
|
|
default_name = render_filename_template(template, post, ext)
|
|
dest = save_file(self._app, "Save Image", default_name, f"Images (*{ext})")
|
|
if not dest:
|
|
return
|
|
dest_path = Path(dest)
|
|
|
|
async def _do_save():
|
|
try:
|
|
actual = await save_post_file(
|
|
src, post, dest_path.parent, self._app._db,
|
|
explicit_name=dest_path.name,
|
|
category_fetcher=self._app._get_category_fetcher(),
|
|
)
|
|
self._app._signals.bookmark_done.emit(
|
|
self._app._grid.selected_index,
|
|
f"Saved to {actual}",
|
|
)
|
|
self._maybe_unbookmark(post)
|
|
except Exception as e:
|
|
self._app._signals.bookmark_error.emit(f"Save failed: {e}")
|
|
|
|
self._app._run_async(_do_save)
|
|
|
|
def on_bookmark_done(self, index: int, msg: str) -> None:
|
|
self._app._status.showMessage(f"{len(self._app._posts)} results — {msg}")
|
|
self._app._search_ctrl.invalidate_lookup_caches()
|
|
# Detect batch operations (e.g. "Saved 3/10 to Unfiled") -- skip heavy updates
|
|
is_batch = is_batch_message(msg)
|
|
thumbs = self._app._grid._thumbs
|
|
if 0 <= index < len(thumbs):
|
|
if "Saved" in msg:
|
|
thumbs[index].set_saved_locally(True)
|
|
if "Bookmarked" in msg:
|
|
thumbs[index].set_bookmarked(True)
|
|
if not is_batch:
|
|
if "Bookmarked" in msg:
|
|
self._app._preview.update_bookmark_state(True)
|
|
if "Saved" in msg:
|
|
self._app._preview.update_save_state(True)
|
|
if self._app._stack.currentIndex() == 1:
|
|
bm_grid = self._app._bookmarks_view._grid
|
|
bm_idx = bm_grid.selected_index
|
|
if 0 <= bm_idx < len(bm_grid._thumbs):
|
|
bm_grid._thumbs[bm_idx].set_saved_locally(True)
|
|
if self._app._stack.currentIndex() == 2:
|
|
self._app._library_view.refresh()
|
|
self._app._popout_ctrl.update_state()
|
|
|
|
def on_batch_progress(self, current: int, total: int, post_id: int) -> None:
|
|
self._app._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 = self._batch_dest
|
|
if dest is None:
|
|
return
|
|
from ..core.config import saved_dir
|
|
if not is_in_library(dest, saved_dir()):
|
|
return
|
|
for i, p in enumerate(self._app._posts):
|
|
if p.id == post_id and i < len(self._app._grid._thumbs):
|
|
self._app._grid._thumbs[i].set_saved_locally(True)
|
|
break
|
|
|
|
def on_batch_done(self, msg: str) -> None:
|
|
self._app._status.showMessage(msg)
|
|
self._app._popout_ctrl.update_state()
|
|
if self._app._stack.currentIndex() == 1:
|
|
self._app._bookmarks_view.refresh()
|
|
if self._app._stack.currentIndex() == 2:
|
|
self._app._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 on_library_files_deleted(self, post_ids: list) -> None:
|
|
"""Library deleted files -- clear saved dots on browse grid."""
|
|
for i, p in enumerate(self._app._posts):
|
|
if p.id in post_ids and i < len(self._app._grid._thumbs):
|
|
self._app._grid._thumbs[i].set_saved_locally(False)
|
|
|
|
def refresh_browse_saved_dots(self) -> None:
|
|
"""Bookmarks changed -- rescan saved state for all visible browse grid posts."""
|
|
for i, p in enumerate(self._app._posts):
|
|
if i < len(self._app._grid._thumbs):
|
|
self._app._grid._thumbs[i].set_saved_locally(self.is_post_saved(p.id))
|
|
site_id = self._app._site_combo.currentData()
|
|
self._app._grid._thumbs[i].set_bookmarked(
|
|
bool(site_id and self._app._db.is_bookmarked(site_id, p.id))
|
|
)
|