From 72e4d5c5a236a7c97316def0abf39113c21e929a Mon Sep 17 00:00:00 2001 From: pax Date: Sun, 5 Apr 2026 01:38:41 -0500 Subject: [PATCH] =?UTF-8?q?v0.1.4=20=E2=80=94=20Library=20rewrite:=20Brows?= =?UTF-8?q?e=20|=20Bookmarks=20|=20Library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major restructure of the favorites/library system: - Rename "Favorites" to "Bookmarks" throughout (DB API, GUI, signals) - Add Library tab for browsing saved files on disk with sorting - Decouple bookmark from save — independent operations now - Two indicators on thumbnails: star (bookmarked), green dot (saved) - Both indicators QSS-controllable (qproperty-bookmarkedColor/savedColor) - Unbookmarking no longer deletes saved files - Saving no longer auto-bookmarks - Library tab: folder sidebar, sort by date/name/size, async thumbnails - DB table kept as "favorites" internally for migration safety --- booru_viewer/core/db.py | 75 ++++-- booru_viewer/gui/app.py | 246 +++++++++--------- .../gui/{favorites.py => bookmarks.py} | 86 +++--- booru_viewer/gui/grid.py | 33 ++- booru_viewer/gui/library.py | 224 ++++++++++++++++ booru_viewer/gui/preview.py | 22 +- booru_viewer/gui/settings.py | 40 +-- booru_viewer/gui/sites.py | 2 +- pyproject.toml | 2 +- 9 files changed, 491 insertions(+), 239 deletions(-) rename booru_viewer/gui/{favorites.py => bookmarks.py} (81%) create mode 100644 booru_viewer/gui/library.py diff --git a/booru_viewer/core/db.py b/booru_viewer/core/db.py index 7cf4722..861dbee 100644 --- a/booru_viewer/core/db.py +++ b/booru_viewer/core/db.py @@ -1,4 +1,4 @@ -"""SQLite database for favorites, sites, and cache metadata.""" +"""SQLite database for bookmarks, sites, and cache metadata.""" from __future__ import annotations @@ -105,7 +105,7 @@ class Site: @dataclass -class Favorite: +class Bookmark: id: int site_id: int post_id: int @@ -117,7 +117,11 @@ class Favorite: source: str | None cached_path: str | None folder: str | None - favorited_at: str + bookmarked_at: str + + +# Back-compat alias — will be removed in a future version. +Favorite = Bookmark class Database: @@ -217,9 +221,9 @@ class Database: ) self.conn.commit() - # -- Favorites -- + # -- Bookmarks -- - def add_favorite( + def add_bookmark( self, site_id: int, post_id: int, @@ -231,7 +235,7 @@ class Database: source: str | None = None, cached_path: str | None = None, folder: str | None = None, - ) -> Favorite: + ) -> Bookmark: now = datetime.now(timezone.utc).isoformat() cur = self.conn.execute( "INSERT OR IGNORE INTO favorites " @@ -240,7 +244,7 @@ class Database: (site_id, post_id, file_url, preview_url, tags, rating, score, source, cached_path, folder, now), ) self.conn.commit() - return Favorite( + return Bookmark( id=cur.lastrowid, # type: ignore[arg-type] site_id=site_id, post_id=post_id, @@ -252,12 +256,15 @@ class Database: source=source, cached_path=cached_path, folder=folder, - favorited_at=now, + bookmarked_at=now, ) - def add_favorites_batch(self, favorites: list[dict]) -> None: - """Add multiple favorites in a single transaction.""" - for fav in favorites: + # Back-compat shim + add_favorite = add_bookmark + + def add_bookmarks_batch(self, bookmarks: list[dict]) -> None: + """Add multiple bookmarks in a single transaction.""" + for fav in bookmarks: self.conn.execute( "INSERT OR IGNORE INTO favorites " "(site_id, post_id, file_url, preview_url, tags, rating, score, source, cached_path, folder, favorited_at) " @@ -268,28 +275,37 @@ class Database: ) self.conn.commit() - def remove_favorite(self, site_id: int, post_id: int) -> None: + # Back-compat shim + add_favorites_batch = add_bookmarks_batch + + def remove_bookmark(self, site_id: int, post_id: int) -> None: self.conn.execute( "DELETE FROM favorites WHERE site_id = ? AND post_id = ?", (site_id, post_id), ) self.conn.commit() - def is_favorited(self, site_id: int, post_id: int) -> bool: + # Back-compat shim + remove_favorite = remove_bookmark + + def is_bookmarked(self, site_id: int, post_id: int) -> bool: row = self.conn.execute( "SELECT 1 FROM favorites WHERE site_id = ? AND post_id = ?", (site_id, post_id), ).fetchone() return row is not None - def get_favorites( + # Back-compat shim + is_favorited = is_bookmarked + + def get_bookmarks( self, search: str | None = None, site_id: int | None = None, folder: str | None = None, limit: int = 100, offset: int = 0, - ) -> list[Favorite]: + ) -> list[Bookmark]: q = "SELECT * FROM favorites WHERE 1=1" params: list = [] if site_id is not None: @@ -305,11 +321,14 @@ class Database: q += " ORDER BY favorited_at DESC LIMIT ? OFFSET ?" params.extend([limit, offset]) rows = self.conn.execute(q, params).fetchall() - return [self._row_to_favorite(r) for r in rows] + return [self._row_to_bookmark(r) for r in rows] + + # Back-compat shim + get_favorites = get_bookmarks @staticmethod - def _row_to_favorite(r) -> Favorite: - return Favorite( + def _row_to_bookmark(r) -> Bookmark: + return Bookmark( id=r["id"], site_id=r["site_id"], post_id=r["post_id"], @@ -321,20 +340,29 @@ class Database: source=r["source"], cached_path=r["cached_path"], folder=r["folder"] if "folder" in r.keys() else None, - favorited_at=r["favorited_at"], + bookmarked_at=r["favorited_at"], ) - def update_favorite_cache_path(self, fav_id: int, cached_path: str) -> None: + # Back-compat shim + _row_to_favorite = _row_to_bookmark + + def update_bookmark_cache_path(self, fav_id: int, cached_path: str) -> None: self.conn.execute( "UPDATE favorites SET cached_path = ? WHERE id = ?", (cached_path, fav_id), ) self.conn.commit() - def favorite_count(self) -> int: + # Back-compat shim + update_favorite_cache_path = update_bookmark_cache_path + + def bookmark_count(self) -> int: row = self.conn.execute("SELECT COUNT(*) FROM favorites").fetchone() return row[0] + # Back-compat shim + favorite_count = bookmark_count + # -- Folders -- def get_folders(self) -> list[str]: @@ -363,12 +391,15 @@ class Database: ) self.conn.commit() - def move_favorite_to_folder(self, fav_id: int, folder: str | None) -> None: + def move_bookmark_to_folder(self, fav_id: int, folder: str | None) -> None: self.conn.execute( "UPDATE favorites SET folder = ? WHERE id = ?", (folder, fav_id) ) self.conn.commit() + # Back-compat shim + move_favorite_to_folder = move_bookmark_to_folder + # -- Blacklist -- def add_blacklisted_tag(self, tag: str) -> None: diff --git a/booru_viewer/gui/app.py b/booru_viewer/gui/app.py index c454c84..3d5d037 100644 --- a/booru_viewer/gui/app.py +++ b/booru_viewer/gui/app.py @@ -43,7 +43,8 @@ from .grid import ThumbnailGrid from .preview import ImagePreview from .search import SearchBar from .sites import SiteManagerDialog -from .favorites import FavoritesView +from .bookmarks import BookmarksView +from .library import LibraryView from .settings import SettingsDialog log = logging.getLogger("booru") @@ -78,8 +79,8 @@ class AsyncSignals(QObject): thumb_done = Signal(int, str) image_done = Signal(str, str) image_error = Signal(str) - fav_done = Signal(int, str) - fav_error = Signal(str) + bookmark_done = Signal(int, str) + bookmark_error = Signal(str) autocomplete_done = Signal(list) batch_progress = Signal(int, int) # current, total batch_done = Signal(str) @@ -231,8 +232,8 @@ class BooruApp(QMainWindow): s.thumb_done.connect(self._on_thumb_done, Q) s.image_done.connect(self._on_image_done, Q) s.image_error.connect(self._on_image_error, Q) - s.fav_done.connect(self._on_fav_done, Q) - s.fav_error.connect(self._on_fav_error, Q) + s.bookmark_done.connect(self._on_bookmark_done, Q) + s.bookmark_error.connect(self._on_bookmark_error, Q) s.autocomplete_done.connect(self._on_autocomplete_done, Q) s.batch_progress.connect(self._on_batch_progress, Q) s.batch_done.connect(lambda m: self._status.showMessage(m), Q) @@ -254,7 +255,7 @@ class BooruApp(QMainWindow): self._dl_progress.hide() self._status.showMessage(f"Error: {e}") - def _on_fav_error(self, e: str) -> None: + def _on_bookmark_error(self, e: str) -> None: self._status.showMessage(f"Error: {e}") def _run_async(self, coro_func, *args): @@ -314,10 +315,16 @@ class BooruApp(QMainWindow): self._browse_btn.clicked.connect(lambda: self._switch_view(0)) nav.addWidget(self._browse_btn) - self._fav_btn = QPushButton("Favorites") - self._fav_btn.setCheckable(True) - self._fav_btn.clicked.connect(lambda: self._switch_view(1)) - nav.addWidget(self._fav_btn) + self._bookmark_btn = QPushButton("Bookmarks") + self._bookmark_btn.setCheckable(True) + self._bookmark_btn.clicked.connect(lambda: self._switch_view(1)) + nav.addWidget(self._bookmark_btn) + + self._library_btn = QPushButton("Library") + self._library_btn.setCheckable(True) + self._library_btn.setFixedWidth(60) + self._library_btn.clicked.connect(lambda: self._switch_view(2)) + nav.addWidget(self._library_btn) layout.addLayout(nav) @@ -338,10 +345,15 @@ class BooruApp(QMainWindow): self._grid.page_back.connect(self._prev_page) self._stack.addWidget(self._grid) - self._favorites_view = FavoritesView(self._db) - self._favorites_view.favorite_selected.connect(self._on_favorite_selected) - self._favorites_view.favorite_activated.connect(self._on_favorite_activated) - self._stack.addWidget(self._favorites_view) + self._bookmarks_view = BookmarksView(self._db) + self._bookmarks_view.bookmark_selected.connect(self._on_bookmark_selected) + self._bookmarks_view.bookmark_activated.connect(self._on_bookmark_activated) + self._stack.addWidget(self._bookmarks_view) + + self._library_view = LibraryView(self._db) + self._library_view.file_selected.connect(self._on_library_selected) + self._library_view.file_activated.connect(self._on_library_activated) + self._stack.addWidget(self._library_view) self._splitter.addWidget(self._stack) @@ -352,7 +364,7 @@ class BooruApp(QMainWindow): self._preview.close_requested.connect(self._close_preview) self._preview.open_in_default.connect(self._open_preview_in_default) self._preview.open_in_browser.connect(self._open_preview_in_browser) - self._preview.favorite_requested.connect(self._favorite_from_preview) + self._preview.bookmark_requested.connect(self._bookmark_from_preview) self._preview.save_to_folder.connect(self._save_from_preview) self._preview.unsave_requested.connect(self._unsave_from_preview) self._preview.navigate.connect(self._navigate_preview) @@ -500,10 +512,13 @@ class BooruApp(QMainWindow): def _switch_view(self, index: int) -> None: self._stack.setCurrentIndex(index) self._browse_btn.setChecked(index == 0) - self._fav_btn.setChecked(index == 1) + self._bookmark_btn.setChecked(index == 1) + self._library_btn.setChecked(index == 2) if index == 1: - self._favorites_view.refresh() - self._favorites_view._grid.setFocus() + self._bookmarks_view.refresh() + self._bookmarks_view._grid.setFocus() + elif index == 2: + self._library_view.refresh() else: self._grid.setFocus() @@ -672,21 +687,21 @@ class BooruApp(QMainWindow): if d.exists(): _folder_saved[folder] = {int(f.stem) for f in d.iterdir() if f.is_file() and f.stem.isdigit()} - # Pre-fetch favorites for the site once (used for folder checks) - _favs = self._db.get_favorites(site_id=site_id) if site_id else [] + # Pre-fetch bookmarks for the site once (used for folder checks) + _favs = self._db.get_bookmarks(site_id=site_id) if site_id else [] for i, (post, thumb) in enumerate(zip(posts, thumbs)): - if site_id and self._db.is_favorited(site_id, post.id): - thumb.set_favorited(True) - # Check if saved to library (not just cached) - saved = post.id in _saved_ids - if not saved: - # Check folders - for f in _favs: - if f.post_id == post.id and f.folder and f.folder in _folder_saved: - saved = post.id in _folder_saved[f.folder] - break - thumb.set_saved_locally(saved) + # Bookmark status (DB) + if site_id and self._db.is_bookmarked(site_id, post.id): + thumb.set_bookmarked(True) + # Saved status (filesystem) — independent of bookmark + saved = post.id in _saved_ids + if not saved: + for folder_name, folder_ids in _folder_saved.items(): + if post.id in folder_ids: + saved = True + break + thumb.set_saved_locally(saved) # Set drag path from cache from ..core.cache import cached_path_for cached = cached_path_for(post.file_url) @@ -834,9 +849,9 @@ class BooruApp(QMainWindow): site_id = self._site_combo.currentData() if self._stack.currentIndex() == 1: - # Favorites view - grid = self._favorites_view._grid - favs = self._favorites_view._favorites + # Bookmarks view + grid = self._bookmarks_view._grid + favs = self._bookmarks_view._bookmarks idx = grid.selected_index if 0 <= idx < len(favs): fav = favs[idx] @@ -858,7 +873,7 @@ class BooruApp(QMainWindow): idx = self._grid.selected_index if 0 <= idx < len(self._posts) and site_id: post = self._posts[idx] - favorited = self._db.is_favorited(site_id, post.id) + bookmarked = self._db.is_bookmarked(site_id, post.id) saved = any( (saved_dir() / f"{post.id}{ext}").exists() for ext in MEDIA_EXTENSIONS @@ -871,7 +886,7 @@ class BooruApp(QMainWindow): ) if saved: break - self._fullscreen_window.update_state(favorited, saved) + self._fullscreen_window.update_state(bookmarked, saved) else: self._fullscreen_window.update_state(False, False) @@ -897,19 +912,27 @@ class BooruApp(QMainWindow): current = cache_size_bytes(include_thumbnails=False) if current > max_bytes: protected = set() - for fav in self._db.get_favorites(limit=999999): + for fav in self._db.get_bookmarks(limit=999999): if fav.cached_path: protected.add(fav.cached_path) evicted = evict_oldest(max_bytes, protected) if evicted: log.info(f"Auto-evicted {evicted} cached files") - def _on_favorite_selected(self, fav) -> None: - self._status.showMessage(f"Favorite #{fav.post_id}") - self._on_favorite_activated(fav) + def _on_library_selected(self, path: str) -> None: + self._preview.set_media(path, Path(path).name) + self._update_fullscreen(path, Path(path).name) - def _on_favorite_activated(self, fav) -> None: - info = f"Favorite #{fav.post_id}" + def _on_library_activated(self, path: str) -> None: + self._preview.set_media(path, Path(path).name) + self._update_fullscreen(path, Path(path).name) + + def _on_bookmark_selected(self, fav) -> None: + self._status.showMessage(f"Bookmark #{fav.post_id}") + self._on_bookmark_activated(fav) + + def _on_bookmark_activated(self, fav) -> None: + info = f"Bookmark #{fav.post_id}" # Try local cache first if fav.cached_path and Path(fav.cached_path).exists(): @@ -937,8 +960,8 @@ class BooruApp(QMainWindow): try: path = await download_image(fav.file_url) # Update cached_path in DB - self._db.update_favorite_cache_path(fav.id, str(path)) - info = f"Favorite #{fav.post_id}" + self._db.update_bookmark_cache_path(fav.id, str(path)) + info = f"Bookmark #{fav.post_id}" self._signals.image_done.emit(str(path), info) except Exception as e: self._signals.image_error.emit(str(e)) @@ -970,13 +993,13 @@ class BooruApp(QMainWindow): def _navigate_preview(self, direction: int) -> None: """Navigate to prev/next post in the preview. direction: -1 or +1.""" if self._stack.currentIndex() == 1: - # Favorites view - grid = self._favorites_view._grid - favs = self._favorites_view._favorites + # Bookmarks view + grid = self._bookmarks_view._grid + favs = self._bookmarks_view._bookmarks idx = grid.selected_index + direction if 0 <= idx < len(favs): grid._select(idx) - self._on_favorite_activated(favs[idx]) + self._on_bookmark_activated(favs[idx]) else: idx = self._grid.selected_index + direction log.info(f"Navigate: direction={direction} current={self._grid.selected_index} next={idx} total={len(self._posts)}") @@ -990,10 +1013,10 @@ class BooruApp(QMainWindow): self._nav_page_turn = "last" self._prev_page() - def _favorite_from_preview(self) -> None: + def _bookmark_from_preview(self) -> None: idx = self._grid.selected_index if 0 <= idx < len(self._posts): - self._toggle_favorite(idx) + self._toggle_bookmark(idx) self._update_fullscreen_state() def _save_from_preview(self, folder: str) -> None: @@ -1013,7 +1036,7 @@ class BooruApp(QMainWindow): site_id = self._site_combo.currentData() folder = None if site_id: - favs = self._db.get_favorites(site_id=site_id) + favs = self._db.get_bookmarks(site_id=site_id) for f in favs: if f.post_id == post.id and f.folder: folder = f.folder @@ -1042,7 +1065,7 @@ class BooruApp(QMainWindow): cols = self._grid._flow.columns self._fullscreen_window = FullscreenPreview(grid_cols=cols, parent=self) self._fullscreen_window.navigate.connect(self._navigate_fullscreen) - self._fullscreen_window.favorite_requested.connect(self._favorite_from_preview) + self._fullscreen_window.bookmark_requested.connect(self._bookmark_from_preview) self._fullscreen_window.save_toggle_requested.connect(self._save_toggle_from_slideshow) self._fullscreen_window.destroyed.connect(self._on_fullscreen_closed) self._fullscreen_window.set_media(path, self._preview._info_label.text()) @@ -1053,7 +1076,7 @@ class BooruApp(QMainWindow): def _navigate_fullscreen(self, direction: int) -> None: self._navigate_preview(direction) - # For synchronous loads (cached/favorites), update immediately + # For synchronous loads (cached/bookmarks), update immediately if self._preview._current_path: self._update_fullscreen( self._preview._current_path, @@ -1091,7 +1114,7 @@ class BooruApp(QMainWindow): copy_url = menu.addAction("Copy Image URL") copy_tags = menu.addAction("Copy Tags") menu.addSeparator() - fav_action = menu.addAction("Unfavorite" if self._is_current_favorited(index) else "Favorite") + fav_action = menu.addAction("Remove Bookmark" if self._is_current_bookmarked(index) else "Bookmark") menu.addSeparator() bl_menu = menu.addMenu("Blacklist Tag") if post.tag_categories: @@ -1129,7 +1152,7 @@ class BooruApp(QMainWindow): site_id = self._site_combo.currentData() folder = None if site_id: - favs = self._db.get_favorites(site_id=site_id) + favs = self._db.get_bookmarks(site_id=site_id) for f in favs: if f.post_id == post.id and f.folder: folder = f.folder @@ -1147,7 +1170,7 @@ class BooruApp(QMainWindow): QApplication.clipboard().setText(post.tags) self._status.showMessage("Tags copied") elif action == fav_action: - self._toggle_favorite(index) + self._toggle_bookmark(index) elif self._is_child_of_menu(action, bl_menu): tag = action.text() self._db.add_blacklisted_tag(tag) @@ -1176,7 +1199,7 @@ class BooruApp(QMainWindow): count = len(posts) menu = QMenu(self) - fav_all = menu.addAction(f"Favorite All ({count})") + fav_all = menu.addAction(f"Bookmark All ({count})") save_menu = menu.addMenu(f"Save All to Library ({count})") save_unsorted = save_menu.addAction("Unsorted") @@ -1188,7 +1211,7 @@ class BooruApp(QMainWindow): save_new = save_menu.addAction("+ New Folder...") menu.addSeparator() - unfav_all = menu.addAction(f"Unfavorite All ({count})") + unfav_all = menu.addAction(f"Remove All Bookmarks ({count})") menu.addSeparator() batch_dl = menu.addAction(f"Download All ({count})...") copy_urls = menu.addAction("Copy All URLs") @@ -1198,7 +1221,7 @@ class BooruApp(QMainWindow): return if action == fav_all: - self._bulk_favorite(indices, posts) + self._bulk_bookmark(indices, posts) elif action == save_unsorted: self._bulk_save(indices, posts, None) elif action == save_new: @@ -1225,19 +1248,19 @@ class BooruApp(QMainWindow): # Delete from all folders for folder in self._db.get_folders(): delete_from_library(post.id, folder) - self._db.remove_favorite(site_id, 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_favorited(False) + self._grid._thumbs[idx].set_bookmarked(False) self._grid._thumbs[idx].set_saved_locally(False) self._grid._clear_multi() - self._status.showMessage(f"Unfavorited {count} posts") + self._status.showMessage(f"Removed {count} bookmarks") elif action == copy_urls: urls = "\n".join(p.file_url for p in posts) QApplication.clipboard().setText(urls) self._status.showMessage(f"Copied {count} URLs") - def _bulk_favorite(self, indices: list[int], posts: list[Post]) -> None: + def _bulk_bookmark(self, indices: list[int], posts: list[Post]) -> None: site_id = self._site_combo.currentData() if not site_id: return @@ -1245,20 +1268,20 @@ class BooruApp(QMainWindow): async def _do(): for i, (idx, post) in enumerate(zip(indices, posts)): - if self._db.is_favorited(site_id, post.id): + if self._db.is_bookmarked(site_id, post.id): continue try: path = await download_image(post.file_url) - self._db.add_favorite( + self._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), ) - self._signals.fav_done.emit(idx, f"Favorited {i+1}/{len(posts)}") + self._signals.bookmark_done.emit(idx, f"Bookmarked {i+1}/{len(posts)}") except Exception: pass - self._signals.batch_done.emit(f"Favorited {len(posts)} posts") + self._signals.batch_done.emit(f"Bookmarked {len(posts)} posts") self._run_async(_do) @@ -1278,30 +1301,30 @@ class BooruApp(QMainWindow): dest = dest_dir / f"{post.id}{ext}" if not dest.exists(): shutil.copy2(path, dest) - if site_id and not self._db.is_favorited(site_id, post.id): - self._db.add_favorite( + if site_id and not self._db.is_bookmarked(site_id, post.id): + self._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, ) - self._signals.fav_done.emit(idx, f"Saved {i+1}/{len(posts)} to {where}") + self._signals.bookmark_done.emit(idx, f"Saved {i+1}/{len(posts)} to {where}") except Exception: pass self._signals.batch_done.emit(f"Saved {len(posts)} to {where}") self._run_async(_do) - def _toggle_favorite_if_not(self, post: Post) -> None: - """Favorite a post if not already favorited.""" + def _ensure_bookmarked(self, post: Post) -> None: + """Bookmark a post if not already bookmarked.""" site_id = self._site_combo.currentData() - if not site_id or self._db.is_favorited(site_id, post.id): + if not site_id or self._db.is_bookmarked(site_id, post.id): return async def _fav(): try: path = await download_image(post.file_url) - self._db.add_favorite( + self._db.add_bookmark( site_id=site_id, post_id=post.id, file_url=post.file_url, @@ -1334,11 +1357,11 @@ class BooruApp(QMainWindow): self._run_async(_batch) - def _is_current_favorited(self, index: int) -> bool: + def _is_current_bookmarked(self, index: int) -> bool: site_id = self._site_combo.currentData() if not site_id or index < 0 or index >= len(self._posts): return False - return self._db.is_favorited(site_id, self._posts[index].id) + return self._db.is_bookmarked(site_id, self._posts[index].id) def _open_in_browser(self, post: Post) -> None: if self._current_site: @@ -1381,36 +1404,13 @@ class BooruApp(QMainWindow): import shutil shutil.copy2(path, dest) - # Also favorite it with the folder - site_id = self._site_combo.currentData() - if site_id and not self._db.is_favorited(site_id, post.id): - self._db.add_favorite( - 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, - ) - elif site_id and folder: - # Already favorited, just update the folder - favs = self._db.get_favorites(site_id=site_id) - for f in favs: - if f.post_id == post.id: - self._db.move_favorite_to_folder(f.id, folder) - break - where = folder or "Unsorted" - self._signals.fav_done.emit( + self._signals.bookmark_done.emit( self._grid.selected_index, f"Saved #{post.id} to {where}" ) except Exception as e: - self._signals.fav_error.emit(str(e)) + self._signals.bookmark_error.emit(str(e)) self._run_async(_save) @@ -1479,12 +1479,12 @@ class BooruApp(QMainWindow): def _open_settings(self) -> None: dlg = SettingsDialog(self._db, self) dlg.settings_changed.connect(self._apply_settings) - self._favorites_imported = False - dlg.favorites_imported.connect(lambda: setattr(self, '_favorites_imported', True)) + self._bookmarks_imported = False + dlg.bookmarks_imported.connect(lambda: setattr(self, '_favorites_imported', True)) dlg.exec() - if self._favorites_imported: + if self._bookmarks_imported: self._switch_view(1) - self._favorites_view.refresh() + self._bookmarks_view.refresh() def _apply_settings(self) -> None: """Re-read settings from DB and apply to UI.""" @@ -1493,7 +1493,7 @@ class BooruApp(QMainWindow): if idx >= 0: self._rating_combo.setCurrentIndex(idx) self._score_spin.setValue(self._db.get_setting_int("default_score")) - self._favorites_view.refresh() + self._bookmarks_view.refresh() self._status.showMessage("Settings applied") # -- Fullscreen & Privacy -- @@ -1541,7 +1541,7 @@ class BooruApp(QMainWindow): if key == Qt.Key.Key_F and self._posts: idx = self._grid.selected_index if 0 <= idx < len(self._posts): - self._toggle_favorite(idx) + self._toggle_bookmark(idx) return elif key == Qt.Key.Key_I: self._toggle_info() @@ -1553,35 +1553,27 @@ class BooruApp(QMainWindow): return super().keyPressEvent(event) - # -- Favorites -- + # -- Bookmarks -- - def _toggle_favorite(self, index: int) -> None: + def _toggle_bookmark(self, index: int) -> None: post = self._posts[index] site_id = self._site_combo.currentData() if not site_id: return - if self._db.is_favorited(site_id, post.id): - # Delete from library if saved - favs = self._db.get_favorites(site_id=site_id) - for f in favs: - if f.post_id == post.id: - from ..core.cache import delete_from_library - delete_from_library(post.id, f.folder) - break - self._db.remove_favorite(site_id, post.id) - self._status.showMessage(f"Unfavorited #{post.id}") + if self._db.is_bookmarked(site_id, post.id): + self._db.remove_bookmark(site_id, post.id) + self._status.showMessage(f"Unbookmarked #{post.id}") thumbs = self._grid._thumbs if 0 <= index < len(thumbs): - thumbs[index].set_favorited(False) - thumbs[index].set_saved_locally(False) + thumbs[index].set_bookmarked(False) else: - self._status.showMessage(f"Favoriting #{post.id}...") + self._status.showMessage(f"Bookmarking #{post.id}...") async def _fav(): try: path = await download_image(post.file_url) - self._db.add_favorite( + self._db.add_bookmark( site_id=site_id, post_id=post.id, file_url=post.file_url, @@ -1592,17 +1584,17 @@ class BooruApp(QMainWindow): source=post.source, cached_path=str(path), ) - self._signals.fav_done.emit(index, f"Favorited #{post.id}") + self._signals.bookmark_done.emit(index, f"Bookmarked #{post.id}") except Exception as e: - self._signals.fav_error.emit(str(e)) + self._signals.bookmark_error.emit(str(e)) self._run_async(_fav) - def _on_fav_done(self, index: int, msg: str) -> None: + def _on_bookmark_done(self, index: int, msg: str) -> None: self._status.showMessage(msg) thumbs = self._grid._thumbs if 0 <= index < len(thumbs): - thumbs[index].set_favorited(True) + thumbs[index].set_bookmarked(True) # Only green if actually saved to library, not just cached if "Saved" in msg: thumbs[index].set_saved_locally(True) diff --git a/booru_viewer/gui/favorites.py b/booru_viewer/gui/bookmarks.py similarity index 81% rename from booru_viewer/gui/favorites.py rename to booru_viewer/gui/bookmarks.py index 0e2a7ae..e129797 100644 --- a/booru_viewer/gui/favorites.py +++ b/booru_viewer/gui/bookmarks.py @@ -1,4 +1,4 @@ -"""Favorites browser widget with folder support.""" +"""Bookmarks browser widget with folder support.""" from __future__ import annotations @@ -23,7 +23,7 @@ from PySide6.QtWidgets import ( QMessageBox, ) -from ..core.db import Database, Favorite +from ..core.db import Database, Bookmark from ..core.cache import download_thumbnail from .grid import ThumbnailGrid @@ -34,16 +34,16 @@ class FavThumbSignals(QObject): thumb_ready = Signal(int, str) -class FavoritesView(QWidget): - """Browse and search local favorites with folder support.""" +class BookmarksView(QWidget): + """Browse and search local bookmarks with folder support.""" - favorite_selected = Signal(object) - favorite_activated = Signal(object) + bookmark_selected = Signal(object) + bookmark_activated = Signal(object) def __init__(self, db: Database, parent: QWidget | None = None) -> None: super().__init__(parent) self._db = db - self._favorites: list[Favorite] = [] + self._bookmarks: list[Bookmark] = [] self._signals = FavThumbSignals() self._signals.thumb_ready.connect(self._on_thumb_ready, Qt.ConnectionType.QueuedConnection) @@ -65,7 +65,7 @@ class FavoritesView(QWidget): top.addWidget(manage_btn) self._search_input = QLineEdit() - self._search_input.setPlaceholderText("Search favorites by tag...") + self._search_input.setPlaceholderText("Search bookmarks by tag...") self._search_input.returnPressed.connect(self._do_search) top.addWidget(self._search_input, stretch=1) @@ -93,7 +93,7 @@ class FavoritesView(QWidget): current = self._folder_combo.currentText() self._folder_combo.blockSignals(True) self._folder_combo.clear() - self._folder_combo.addItem("All Favorites") + self._folder_combo.addItem("All Bookmarks") self._folder_combo.addItem("Unfiled") for folder in self._db.get_folders(): self._folder_combo.addItem(folder) @@ -110,26 +110,26 @@ class FavoritesView(QWidget): folder_filter = None if folder_text == "Unfiled": folder_filter = "" # sentinel for NULL folder - elif folder_text not in ("All Favorites", ""): + elif folder_text not in ("All Bookmarks", ""): folder_filter = folder_text if folder_filter == "": # Get unfiled: folder IS NULL - self._favorites = [ - f for f in self._db.get_favorites(search=search, limit=500) + self._bookmarks = [ + f for f in self._db.get_bookmarks(search=search, limit=500) if f.folder is None ] elif folder_filter: - self._favorites = self._db.get_favorites(search=search, folder=folder_filter, limit=500) + self._bookmarks = self._db.get_bookmarks(search=search, folder=folder_filter, limit=500) else: - self._favorites = self._db.get_favorites(search=search, limit=500) + self._bookmarks = self._db.get_bookmarks(search=search, limit=500) - self._count_label.setText(f"{len(self._favorites)} favorites") - thumbs = self._grid.set_posts(len(self._favorites)) + self._count_label.setText(f"{len(self._bookmarks)} bookmarks") + thumbs = self._grid.set_posts(len(self._bookmarks)) from ..core.config import saved_dir, saved_folder_dir, MEDIA_EXTENSIONS - for i, (fav, thumb) in enumerate(zip(self._favorites, thumbs)): - thumb.set_favorited(True) + for i, (fav, thumb) in enumerate(zip(self._bookmarks, thumbs)): + thumb.set_bookmarked(True) # Check if saved to library saved = False if fav.folder: @@ -171,15 +171,15 @@ class FavoritesView(QWidget): self.refresh(search=text if text else None) def _on_selected(self, index: int) -> None: - if 0 <= index < len(self._favorites): - self.favorite_selected.emit(self._favorites[index]) + if 0 <= index < len(self._bookmarks): + self.bookmark_selected.emit(self._bookmarks[index]) def _on_activated(self, index: int) -> None: - if 0 <= index < len(self._favorites): - self.favorite_activated.emit(self._favorites[index]) + if 0 <= index < len(self._bookmarks): + self.bookmark_activated.emit(self._bookmarks[index]) - def _copy_to_library_unsorted(self, fav: Favorite) -> None: - """Copy a favorited image to the unsorted library folder.""" + def _copy_to_library_unsorted(self, fav: Bookmark) -> None: + """Copy a bookmarked image to the unsorted library folder.""" from ..core.config import saved_dir if fav.cached_path and Path(fav.cached_path).exists(): import shutil @@ -188,8 +188,8 @@ class FavoritesView(QWidget): if not dest.exists(): shutil.copy2(src, dest) - def _copy_to_library(self, fav: Favorite, folder: str) -> None: - """Copy a favorited image to the library folder on disk.""" + def _copy_to_library(self, fav: Bookmark, folder: str) -> None: + """Copy a bookmarked image to the library folder on disk.""" from ..core.config import saved_folder_dir if fav.cached_path and Path(fav.cached_path).exists(): import shutil @@ -205,9 +205,9 @@ class FavoritesView(QWidget): self._refresh_folders() def _on_context_menu(self, index: int, pos) -> None: - if index < 0 or index >= len(self._favorites): + if index < 0 or index >= len(self._bookmarks): return - fav = self._favorites[index] + fav = self._bookmarks[index] from PySide6.QtGui import QDesktopServices from PySide6.QtCore import QUrl @@ -246,7 +246,7 @@ class FavoritesView(QWidget): move_new = move_menu.addAction("+ New Folder...") menu.addSeparator() - unfav = menu.addAction("Unfavorite") + remove_bookmark = menu.addAction("Remove Bookmark") action = menu.exec(pos) if not action: @@ -260,7 +260,7 @@ class FavoritesView(QWidget): if ok and name.strip(): self._db.add_folder(name.strip()) self._copy_to_library(fav, name.strip()) - self._db.move_favorite_to_folder(fav.id, name.strip()) + self._db.move_bookmark_to_folder(fav.id, name.strip()) self.refresh() elif id(action) in save_lib_folders: folder_name = save_lib_folders[id(action)] @@ -281,28 +281,26 @@ class FavoritesView(QWidget): elif action == copy_tags: QApplication.clipboard().setText(fav.tags) elif action == move_none: - self._db.move_favorite_to_folder(fav.id, None) + self._db.move_bookmark_to_folder(fav.id, None) self.refresh() elif action == move_new: name, ok = QInputDialog.getText(self, "New Folder", "Folder name:") if ok and name.strip(): self._db.add_folder(name.strip()) - self._db.move_favorite_to_folder(fav.id, name.strip()) + self._db.move_bookmark_to_folder(fav.id, name.strip()) self._copy_to_library(fav, name.strip()) self.refresh() elif id(action) in folder_actions: folder_name = folder_actions[id(action)] - self._db.move_favorite_to_folder(fav.id, folder_name) + self._db.move_bookmark_to_folder(fav.id, folder_name) self._copy_to_library(fav, folder_name) self.refresh() - elif action == unfav: - from ..core.cache import delete_from_library - delete_from_library(fav.post_id, fav.folder) - self._db.remove_favorite(fav.site_id, fav.post_id) + elif action == remove_bookmark: + self._db.remove_bookmark(fav.site_id, fav.post_id) self.refresh() def _on_multi_context_menu(self, indices: list, pos) -> None: - favs = [self._favorites[i] for i in indices if 0 <= i < len(self._favorites)] + favs = [self._bookmarks[i] for i in indices if 0 <= i < len(self._bookmarks)] if not favs: return @@ -320,7 +318,7 @@ class FavoritesView(QWidget): folder_actions[id(a)] = folder menu.addSeparator() - unfav_all = menu.addAction(f"Unfavorite All ({len(favs)})") + remove_all = menu.addAction(f"Remove All Bookmarks ({len(favs)})") action = menu.exec(pos) if not action: @@ -340,17 +338,15 @@ class FavoritesView(QWidget): self.refresh() elif action == move_none: for fav in favs: - self._db.move_favorite_to_folder(fav.id, None) + self._db.move_bookmark_to_folder(fav.id, None) self.refresh() elif id(action) in folder_actions: folder_name = folder_actions[id(action)] for fav in favs: - self._db.move_favorite_to_folder(fav.id, folder_name) + self._db.move_bookmark_to_folder(fav.id, folder_name) self._copy_to_library(fav, folder_name) self.refresh() - elif action == unfav_all: - from ..core.cache import delete_from_library + elif action == remove_all: for fav in favs: - delete_from_library(fav.post_id, fav.folder) - self._db.remove_favorite(fav.site_id, fav.post_id) + self._db.remove_bookmark(fav.site_id, fav.post_id) self.refresh() diff --git a/booru_viewer/gui/grid.py b/booru_viewer/gui/grid.py index 2a1760d..f92df24 100644 --- a/booru_viewer/gui/grid.py +++ b/booru_viewer/gui/grid.py @@ -27,17 +27,17 @@ class ThumbnailWidget(QWidget): double_clicked = Signal(int) right_clicked = Signal(int, object) # index, QPoint - # QSS-controllable dot colors: qproperty-savedColor / qproperty-favoritedColor + # QSS-controllable dot colors: qproperty-savedColor / qproperty-bookmarkedColor _saved_color = QColor("#22cc22") - _favorited_color = QColor("#ff4444") + _bookmarked_color = QColor("#ff4444") def _get_saved_color(self): return self._saved_color def _set_saved_color(self, c): self._saved_color = QColor(c) if isinstance(c, str) else c savedColor = Property(QColor, _get_saved_color, _set_saved_color) - def _get_favorited_color(self): return self._favorited_color - def _set_favorited_color(self, c): self._favorited_color = QColor(c) if isinstance(c, str) else c - favoritedColor = Property(QColor, _get_favorited_color, _set_favorited_color) + def _get_bookmarked_color(self): return self._bookmarked_color + def _set_bookmarked_color(self, c): self._bookmarked_color = QColor(c) if isinstance(c, str) else c + bookmarkedColor = Property(QColor, _get_bookmarked_color, _set_bookmarked_color) def __init__(self, index: int, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -45,7 +45,7 @@ class ThumbnailWidget(QWidget): self._pixmap: QPixmap | None = None self._selected = False self._multi_selected = False - self._favorited = False + self._bookmarked = False self._saved_locally = False self._hover = False self._drag_start: QPoint | None = None @@ -71,8 +71,8 @@ class ThumbnailWidget(QWidget): self._multi_selected = selected self.update() - def set_favorited(self, favorited: bool) -> None: - self._favorited = favorited + def set_bookmarked(self, bookmarked: bool) -> None: + self._bookmarked = bookmarked self.update() def set_saved_locally(self, saved: bool) -> None: @@ -120,11 +120,20 @@ class ThumbnailWidget(QWidget): y = (self.height() - self._pixmap.height()) // 2 p.drawPixmap(x, y, self._pixmap) - # Favorite/saved indicator - if self._favorited: + # Bookmark/saved indicators (independent dots) + dot_x = self.width() - 14 + if self._saved_locally: p.setPen(Qt.PenStyle.NoPen) - p.setBrush(self._saved_color if self._saved_locally else self._favorited_color) - p.drawEllipse(self.width() - 14, 4, 10, 10) + p.setBrush(self._saved_color) + p.drawEllipse(dot_x, 4, 10, 10) + dot_x -= 14 + if self._bookmarked: + from PySide6.QtGui import QFont + p.setPen(Qt.PenStyle.NoPen) + p.setBrush(self._bookmarked_color) + p.setFont(QFont(p.font().family(), 10)) + p.setPen(self._bookmarked_color) + p.drawText(dot_x - 2, 14, "\u2605") # Multi-select checkmark if self._multi_selected: diff --git a/booru_viewer/gui/library.py b/booru_viewer/gui/library.py new file mode 100644 index 0000000..fe95fff --- /dev/null +++ b/booru_viewer/gui/library.py @@ -0,0 +1,224 @@ +"""Library browser widget — browse saved files on disk.""" + +from __future__ import annotations + +import logging +import os +import threading +from pathlib import Path + +from PySide6.QtCore import Qt, Signal, QObject +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, + QPushButton, + QLabel, + QComboBox, +) + +from ..core.config import saved_dir, saved_folder_dir, MEDIA_EXTENSIONS, thumbnails_dir +from .grid import ThumbnailGrid + +log = logging.getLogger("booru") + +LIBRARY_THUMB_SIZE = 180 + + +class _LibThumbSignals(QObject): + thumb_ready = Signal(int, str) + + +class LibraryView(QWidget): + """Browse files saved to the library on disk.""" + + file_selected = Signal(str) + file_activated = Signal(str) + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self._files: list[Path] = [] + self._signals = _LibThumbSignals() + self._signals.thumb_ready.connect( + self._on_thumb_ready, Qt.ConnectionType.QueuedConnection + ) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + # --- Top bar --- + top = QHBoxLayout() + top.setContentsMargins(0, 0, 0, 0) + + self._folder_combo = QComboBox() + self._folder_combo.setMinimumWidth(140) + self._folder_combo.currentTextChanged.connect(lambda _: self.refresh()) + top.addWidget(self._folder_combo) + + self._sort_combo = QComboBox() + self._sort_combo.addItems(["Date", "Name", "Size"]) + self._sort_combo.setFixedWidth(80) + self._sort_combo.currentTextChanged.connect(lambda _: self.refresh()) + top.addWidget(self._sort_combo) + + refresh_btn = QPushButton("Refresh") + refresh_btn.setFixedWidth(65) + refresh_btn.clicked.connect(self.refresh) + top.addWidget(refresh_btn) + + top.addStretch(1) + layout.addLayout(top) + + # --- Count label --- + self._count_label = QLabel() + layout.addWidget(self._count_label) + + # --- Grid --- + self._grid = ThumbnailGrid() + self._grid.post_selected.connect(self._on_selected) + self._grid.post_activated.connect(self._on_activated) + layout.addWidget(self._grid) + + # ------------------------------------------------------------------ + # Public + # ------------------------------------------------------------------ + + def refresh(self) -> None: + """Scan the selected folder, sort, display thumbnails.""" + self._refresh_folders() + self._files = self._scan_files() + self._sort_files() + + self._count_label.setText(f"{len(self._files)} files") + thumbs = self._grid.set_posts(len(self._files)) + + lib_thumb_dir = thumbnails_dir() / "library" + lib_thumb_dir.mkdir(parents=True, exist_ok=True) + + for i, (filepath, thumb) in enumerate(zip(self._files, thumbs)): + thumb._cached_path = str(filepath) + cached_thumb = lib_thumb_dir / f"{filepath.stem}.jpg" + if cached_thumb.exists(): + pix = QPixmap(str(cached_thumb)) + if not pix.isNull(): + thumb.set_pixmap(pix) + else: + self._generate_thumb_async(i, filepath, cached_thumb) + + # ------------------------------------------------------------------ + # Folder list + # ------------------------------------------------------------------ + + def _refresh_folders(self) -> None: + current = self._folder_combo.currentText() + self._folder_combo.blockSignals(True) + self._folder_combo.clear() + self._folder_combo.addItem("All Files") + self._folder_combo.addItem("Unsorted") + + root = saved_dir() + if root.is_dir(): + for entry in sorted(root.iterdir()): + if entry.is_dir(): + self._folder_combo.addItem(entry.name) + + idx = self._folder_combo.findText(current) + if idx >= 0: + self._folder_combo.setCurrentIndex(idx) + self._folder_combo.blockSignals(False) + + # ------------------------------------------------------------------ + # File scanning + # ------------------------------------------------------------------ + + def _scan_files(self) -> list[Path]: + root = saved_dir() + folder_text = self._folder_combo.currentText() + + if folder_text == "All Files": + return self._collect_recursive(root) + elif folder_text == "Unsorted": + return self._collect_top_level(root) + else: + sub = root / folder_text + if sub.is_dir(): + return self._collect_top_level(sub) + return [] + + @staticmethod + def _collect_recursive(directory: Path) -> list[Path]: + files: list[Path] = [] + for dirpath, _dirnames, filenames in os.walk(directory): + for name in filenames: + p = Path(dirpath) / name + if p.suffix.lower() in MEDIA_EXTENSIONS: + files.append(p) + return files + + @staticmethod + def _collect_top_level(directory: Path) -> list[Path]: + if not directory.is_dir(): + return [] + return [ + p + for p in directory.iterdir() + if p.is_file() and p.suffix.lower() in MEDIA_EXTENSIONS + ] + + # ------------------------------------------------------------------ + # Sorting + # ------------------------------------------------------------------ + + def _sort_files(self) -> None: + mode = self._sort_combo.currentText() + if mode == "Name": + self._files.sort(key=lambda p: p.name.lower()) + elif mode == "Size": + self._files.sort(key=lambda p: p.stat().st_size, reverse=True) + else: + # Date — newest first + self._files.sort(key=lambda p: p.stat().st_mtime, reverse=True) + + # ------------------------------------------------------------------ + # Async thumbnail generation + # ------------------------------------------------------------------ + + def _generate_thumb_async( + self, index: int, source: Path, dest: Path + ) -> None: + def _work() -> None: + try: + from PIL import Image + + with Image.open(source) as img: + img.thumbnail( + (LIBRARY_THUMB_SIZE, LIBRARY_THUMB_SIZE), Image.LANCZOS + ) + if img.mode in ("RGBA", "P"): + img = img.convert("RGB") + img.save(str(dest), "JPEG", quality=85) + self._signals.thumb_ready.emit(index, str(dest)) + except Exception as e: + log.warning("Library thumb %d (%s) failed: %s", index, source.name, e) + + threading.Thread(target=_work, daemon=True).start() + + def _on_thumb_ready(self, index: int, path: str) -> None: + thumbs = self._grid._thumbs + if 0 <= index < len(thumbs): + pix = QPixmap(path) + if not pix.isNull(): + thumbs[index].set_pixmap(pix) + + # ------------------------------------------------------------------ + # Selection signals + # ------------------------------------------------------------------ + + def _on_selected(self, index: int) -> None: + if 0 <= index < len(self._files): + self.file_selected.emit(str(self._files[index])) + + def _on_activated(self, index: int) -> None: + if 0 <= index < len(self._files): + self.file_activated.emit(str(self._files[index])) diff --git a/booru_viewer/gui/preview.py b/booru_viewer/gui/preview.py index 77bfedd..b4c114e 100644 --- a/booru_viewer/gui/preview.py +++ b/booru_viewer/gui/preview.py @@ -26,7 +26,7 @@ class FullscreenPreview(QMainWindow): """Fullscreen media viewer with navigation — images, GIFs, and video.""" navigate = Signal(int) # direction: -1/+1 for left/right, -cols/+cols for up/down - favorite_requested = Signal() + bookmark_requested = Signal() save_toggle_requested = Signal() # save or unsave depending on state def __init__(self, grid_cols: int = 3, parent=None) -> None: @@ -44,10 +44,10 @@ class FullscreenPreview(QMainWindow): toolbar = QHBoxLayout(self._toolbar) toolbar.setContentsMargins(8, 4, 8, 4) - self._fav_btn = QPushButton("Favorite") - self._fav_btn.setFixedWidth(80) - self._fav_btn.clicked.connect(self.favorite_requested) - toolbar.addWidget(self._fav_btn) + self._bookmark_btn = QPushButton("Bookmark") + self._bookmark_btn.setFixedWidth(80) + self._bookmark_btn.clicked.connect(self.bookmark_requested) + toolbar.addWidget(self._bookmark_btn) self._save_btn = QPushButton("Save") self._save_btn.setFixedWidth(70) @@ -80,9 +80,9 @@ class FullscreenPreview(QMainWindow): QApplication.instance().installEventFilter(self) self.showFullScreen() - def update_state(self, favorited: bool, saved: bool) -> None: - self._fav_btn.setText("Unfavorite" if favorited else "Favorite") - self._fav_btn.setFixedWidth(90 if favorited else 80) + def update_state(self, bookmarked: bool, saved: bool) -> None: + self._bookmark_btn.setText("Unbookmark" if bookmarked else "Bookmark") + self._bookmark_btn.setFixedWidth(90 if bookmarked else 80) self._is_saved = saved self._save_btn.setText("Unsave" if saved else "Save") @@ -500,7 +500,7 @@ class ImagePreview(QWidget): open_in_browser = Signal() save_to_folder = Signal(str) unsave_requested = Signal() - favorite_requested = Signal() + bookmark_requested = Signal() navigate = Signal(int) # -1 = prev, +1 = next fullscreen_requested = Signal() @@ -587,7 +587,7 @@ class ImagePreview(QWidget): def _on_context_menu(self, pos) -> None: menu = QMenu(self) - fav_action = menu.addAction("Favorite") + fav_action = menu.addAction("Bookmark") save_menu = menu.addMenu("Save to Library") save_unsorted = save_menu.addAction("Unsorted") @@ -624,7 +624,7 @@ class ImagePreview(QWidget): if not action: return if action == fav_action: - self.favorite_requested.emit() + self.bookmark_requested.emit() elif action == save_unsorted: self.save_to_folder.emit("") elif action == save_new: diff --git a/booru_viewer/gui/settings.py b/booru_viewer/gui/settings.py index d0ea1a9..aad208b 100644 --- a/booru_viewer/gui/settings.py +++ b/booru_viewer/gui/settings.py @@ -35,7 +35,7 @@ class SettingsDialog(QDialog): """Full settings panel with tabs.""" settings_changed = Signal() - favorites_imported = Signal() + bookmarks_imported = Signal() def __init__(self, db: Database, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -153,8 +153,8 @@ class SettingsDialog(QDialog): self._cache_size_label = QLabel(f"{total_mb:.1f} MB") stats_layout.addRow("Total size:", self._cache_size_label) - self._fav_count_label = QLabel(f"{self._db.favorite_count()}") - stats_layout.addRow("Favorites:", self._fav_count_label) + self._fav_count_label = QLabel(f"{self._db.bookmark_count()}") + stats_layout.addRow("Bookmarks:", self._fav_count_label) layout.addWidget(stats_group) @@ -317,12 +317,12 @@ class SettingsDialog(QDialog): exp_group = QGroupBox("Backup") exp_layout = QHBoxLayout(exp_group) - export_btn = QPushButton("Export Favorites") - export_btn.clicked.connect(self._export_favorites) + export_btn = QPushButton("Export Bookmarks") + export_btn.clicked.connect(self._export_bookmarks) exp_layout.addWidget(export_btn) - import_btn = QPushButton("Import Favorites") - import_btn.clicked.connect(self._import_favorites) + import_btn = QPushButton("Import Bookmarks") + import_btn.clicked.connect(self._import_bookmarks) exp_layout.addWidget(import_btn) layout.addWidget(exp_group) @@ -499,7 +499,7 @@ class SettingsDialog(QDialog): def _clear_image_cache(self) -> None: reply = QMessageBox.question( self, "Confirm", - "Delete all cached images? (Favorites stay in the database but cached files are removed.)", + "Delete all cached images? (Bookmarks stay in the database but cached files are removed.)", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: @@ -520,9 +520,9 @@ class SettingsDialog(QDialog): def _evict_now(self) -> None: max_bytes = self._max_cache.value() * 1024 * 1024 - # Protect favorited file paths + # Protect bookmarked file paths protected = set() - for fav in self._db.get_favorites(limit=999999): + for fav in self._db.get_bookmarks(limit=999999): if fav.cached_path: protected.add(fav.cached_path) count = evict_oldest(max_bytes, protected) @@ -559,13 +559,13 @@ class SettingsDialog(QDialog): from PySide6.QtCore import QUrl QDesktopServices.openUrl(QUrl.fromLocalFile(str(data_dir()))) - def _export_favorites(self) -> None: + def _export_bookmarks(self) -> None: from .dialogs import save_file import json - path = save_file(self, "Export Favorites", "favorites.json", "JSON (*.json)") + path = save_file(self, "Export Bookmarks", "bookmarks.json", "JSON (*.json)") if not path: return - favs = self._db.get_favorites(limit=999999) + favs = self._db.get_bookmarks(limit=999999) data = [ { "post_id": f.post_id, @@ -577,18 +577,18 @@ class SettingsDialog(QDialog): "score": f.score, "source": f.source, "folder": f.folder, - "favorited_at": f.favorited_at, + "bookmarked_at": f.bookmarked_at, } for f in favs ] with open(path, "w") as fp: json.dump(data, fp, indent=2) - QMessageBox.information(self, "Done", f"Exported {len(data)} favorites.") + QMessageBox.information(self, "Done", f"Exported {len(data)} bookmarks.") - def _import_favorites(self) -> None: + def _import_bookmarks(self) -> None: from .dialogs import open_file import json - path = open_file(self, "Import Favorites", "JSON (*.json)") + path = open_file(self, "Import Bookmarks", "JSON (*.json)") if not path: return try: @@ -598,7 +598,7 @@ class SettingsDialog(QDialog): for item in data: try: folder = item.get("folder") - self._db.add_favorite( + self._db.add_bookmark( site_id=item["site_id"], post_id=item["post_id"], file_url=item["file_url"], @@ -614,8 +614,8 @@ class SettingsDialog(QDialog): count += 1 except Exception: pass - QMessageBox.information(self, "Done", f"Imported {count} favorites.") - self.favorites_imported.emit() + QMessageBox.information(self, "Done", f"Imported {count} bookmarks.") + self.bookmarks_imported.emit() except Exception as e: QMessageBox.warning(self, "Error", str(e)) diff --git a/booru_viewer/gui/sites.py b/booru_viewer/gui/sites.py index 24d6035..b8361da 100644 --- a/booru_viewer/gui/sites.py +++ b/booru_viewer/gui/sites.py @@ -288,7 +288,7 @@ class SiteManagerDialog(QDialog): return site_id = item.data(Qt.ItemDataRole.UserRole) reply = QMessageBox.question( - self, "Confirm", "Remove this site and all its favorites?", + self, "Confirm", "Remove this site and all its bookmarks?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: diff --git a/pyproject.toml b/pyproject.toml index 0e1eff4..26345c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "booru-viewer" -version = "0.1.3" +version = "0.1.4" description = "Local booru image browser with Qt6 GUI" requires-python = ">=3.11" dependencies = [