v0.1.4 — Library rewrite: Browse | Bookmarks | Library

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
This commit is contained in:
pax 2026-04-05 01:38:41 -05:00
parent 243a889fc1
commit 72e4d5c5a2
9 changed files with 491 additions and 239 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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()

View File

@ -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:

224
booru_viewer/gui/library.py Normal file
View File

@ -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]))

View File

@ -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:

View File

@ -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))

View File

@ -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:

View File

@ -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 = [