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:
parent
243a889fc1
commit
72e4d5c5a2
@ -1,4 +1,4 @@
|
|||||||
"""SQLite database for favorites, sites, and cache metadata."""
|
"""SQLite database for bookmarks, sites, and cache metadata."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ class Site:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Favorite:
|
class Bookmark:
|
||||||
id: int
|
id: int
|
||||||
site_id: int
|
site_id: int
|
||||||
post_id: int
|
post_id: int
|
||||||
@ -117,7 +117,11 @@ class Favorite:
|
|||||||
source: str | None
|
source: str | None
|
||||||
cached_path: str | None
|
cached_path: str | None
|
||||||
folder: 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:
|
class Database:
|
||||||
@ -217,9 +221,9 @@ class Database:
|
|||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
# -- Favorites --
|
# -- Bookmarks --
|
||||||
|
|
||||||
def add_favorite(
|
def add_bookmark(
|
||||||
self,
|
self,
|
||||||
site_id: int,
|
site_id: int,
|
||||||
post_id: int,
|
post_id: int,
|
||||||
@ -231,7 +235,7 @@ class Database:
|
|||||||
source: str | None = None,
|
source: str | None = None,
|
||||||
cached_path: str | None = None,
|
cached_path: str | None = None,
|
||||||
folder: str | None = None,
|
folder: str | None = None,
|
||||||
) -> Favorite:
|
) -> Bookmark:
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
cur = self.conn.execute(
|
cur = self.conn.execute(
|
||||||
"INSERT OR IGNORE INTO favorites "
|
"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),
|
(site_id, post_id, file_url, preview_url, tags, rating, score, source, cached_path, folder, now),
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
return Favorite(
|
return Bookmark(
|
||||||
id=cur.lastrowid, # type: ignore[arg-type]
|
id=cur.lastrowid, # type: ignore[arg-type]
|
||||||
site_id=site_id,
|
site_id=site_id,
|
||||||
post_id=post_id,
|
post_id=post_id,
|
||||||
@ -252,12 +256,15 @@ class Database:
|
|||||||
source=source,
|
source=source,
|
||||||
cached_path=cached_path,
|
cached_path=cached_path,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
favorited_at=now,
|
bookmarked_at=now,
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_favorites_batch(self, favorites: list[dict]) -> None:
|
# Back-compat shim
|
||||||
"""Add multiple favorites in a single transaction."""
|
add_favorite = add_bookmark
|
||||||
for fav in favorites:
|
|
||||||
|
def add_bookmarks_batch(self, bookmarks: list[dict]) -> None:
|
||||||
|
"""Add multiple bookmarks in a single transaction."""
|
||||||
|
for fav in bookmarks:
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT OR IGNORE INTO favorites "
|
"INSERT OR IGNORE INTO favorites "
|
||||||
"(site_id, post_id, file_url, preview_url, tags, rating, score, source, cached_path, folder, favorited_at) "
|
"(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()
|
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(
|
self.conn.execute(
|
||||||
"DELETE FROM favorites WHERE site_id = ? AND post_id = ?",
|
"DELETE FROM favorites WHERE site_id = ? AND post_id = ?",
|
||||||
(site_id, post_id),
|
(site_id, post_id),
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
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(
|
row = self.conn.execute(
|
||||||
"SELECT 1 FROM favorites WHERE site_id = ? AND post_id = ?",
|
"SELECT 1 FROM favorites WHERE site_id = ? AND post_id = ?",
|
||||||
(site_id, post_id),
|
(site_id, post_id),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return row is not None
|
return row is not None
|
||||||
|
|
||||||
def get_favorites(
|
# Back-compat shim
|
||||||
|
is_favorited = is_bookmarked
|
||||||
|
|
||||||
|
def get_bookmarks(
|
||||||
self,
|
self,
|
||||||
search: str | None = None,
|
search: str | None = None,
|
||||||
site_id: int | None = None,
|
site_id: int | None = None,
|
||||||
folder: str | None = None,
|
folder: str | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[Favorite]:
|
) -> list[Bookmark]:
|
||||||
q = "SELECT * FROM favorites WHERE 1=1"
|
q = "SELECT * FROM favorites WHERE 1=1"
|
||||||
params: list = []
|
params: list = []
|
||||||
if site_id is not None:
|
if site_id is not None:
|
||||||
@ -305,11 +321,14 @@ class Database:
|
|||||||
q += " ORDER BY favorited_at DESC LIMIT ? OFFSET ?"
|
q += " ORDER BY favorited_at DESC LIMIT ? OFFSET ?"
|
||||||
params.extend([limit, offset])
|
params.extend([limit, offset])
|
||||||
rows = self.conn.execute(q, params).fetchall()
|
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
|
@staticmethod
|
||||||
def _row_to_favorite(r) -> Favorite:
|
def _row_to_bookmark(r) -> Bookmark:
|
||||||
return Favorite(
|
return Bookmark(
|
||||||
id=r["id"],
|
id=r["id"],
|
||||||
site_id=r["site_id"],
|
site_id=r["site_id"],
|
||||||
post_id=r["post_id"],
|
post_id=r["post_id"],
|
||||||
@ -321,20 +340,29 @@ class Database:
|
|||||||
source=r["source"],
|
source=r["source"],
|
||||||
cached_path=r["cached_path"],
|
cached_path=r["cached_path"],
|
||||||
folder=r["folder"] if "folder" in r.keys() else None,
|
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(
|
self.conn.execute(
|
||||||
"UPDATE favorites SET cached_path = ? WHERE id = ?",
|
"UPDATE favorites SET cached_path = ? WHERE id = ?",
|
||||||
(cached_path, fav_id),
|
(cached_path, fav_id),
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
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()
|
row = self.conn.execute("SELECT COUNT(*) FROM favorites").fetchone()
|
||||||
return row[0]
|
return row[0]
|
||||||
|
|
||||||
|
# Back-compat shim
|
||||||
|
favorite_count = bookmark_count
|
||||||
|
|
||||||
# -- Folders --
|
# -- Folders --
|
||||||
|
|
||||||
def get_folders(self) -> list[str]:
|
def get_folders(self) -> list[str]:
|
||||||
@ -363,12 +391,15 @@ class Database:
|
|||||||
)
|
)
|
||||||
self.conn.commit()
|
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(
|
self.conn.execute(
|
||||||
"UPDATE favorites SET folder = ? WHERE id = ?", (folder, fav_id)
|
"UPDATE favorites SET folder = ? WHERE id = ?", (folder, fav_id)
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
# Back-compat shim
|
||||||
|
move_favorite_to_folder = move_bookmark_to_folder
|
||||||
|
|
||||||
# -- Blacklist --
|
# -- Blacklist --
|
||||||
|
|
||||||
def add_blacklisted_tag(self, tag: str) -> None:
|
def add_blacklisted_tag(self, tag: str) -> None:
|
||||||
|
|||||||
@ -43,7 +43,8 @@ from .grid import ThumbnailGrid
|
|||||||
from .preview import ImagePreview
|
from .preview import ImagePreview
|
||||||
from .search import SearchBar
|
from .search import SearchBar
|
||||||
from .sites import SiteManagerDialog
|
from .sites import SiteManagerDialog
|
||||||
from .favorites import FavoritesView
|
from .bookmarks import BookmarksView
|
||||||
|
from .library import LibraryView
|
||||||
from .settings import SettingsDialog
|
from .settings import SettingsDialog
|
||||||
|
|
||||||
log = logging.getLogger("booru")
|
log = logging.getLogger("booru")
|
||||||
@ -78,8 +79,8 @@ class AsyncSignals(QObject):
|
|||||||
thumb_done = Signal(int, str)
|
thumb_done = Signal(int, str)
|
||||||
image_done = Signal(str, str)
|
image_done = Signal(str, str)
|
||||||
image_error = Signal(str)
|
image_error = Signal(str)
|
||||||
fav_done = Signal(int, str)
|
bookmark_done = Signal(int, str)
|
||||||
fav_error = Signal(str)
|
bookmark_error = Signal(str)
|
||||||
autocomplete_done = Signal(list)
|
autocomplete_done = Signal(list)
|
||||||
batch_progress = Signal(int, int) # current, total
|
batch_progress = Signal(int, int) # current, total
|
||||||
batch_done = Signal(str)
|
batch_done = Signal(str)
|
||||||
@ -231,8 +232,8 @@ class BooruApp(QMainWindow):
|
|||||||
s.thumb_done.connect(self._on_thumb_done, Q)
|
s.thumb_done.connect(self._on_thumb_done, Q)
|
||||||
s.image_done.connect(self._on_image_done, Q)
|
s.image_done.connect(self._on_image_done, Q)
|
||||||
s.image_error.connect(self._on_image_error, Q)
|
s.image_error.connect(self._on_image_error, Q)
|
||||||
s.fav_done.connect(self._on_fav_done, Q)
|
s.bookmark_done.connect(self._on_bookmark_done, Q)
|
||||||
s.fav_error.connect(self._on_fav_error, Q)
|
s.bookmark_error.connect(self._on_bookmark_error, Q)
|
||||||
s.autocomplete_done.connect(self._on_autocomplete_done, Q)
|
s.autocomplete_done.connect(self._on_autocomplete_done, Q)
|
||||||
s.batch_progress.connect(self._on_batch_progress, Q)
|
s.batch_progress.connect(self._on_batch_progress, Q)
|
||||||
s.batch_done.connect(lambda m: self._status.showMessage(m), Q)
|
s.batch_done.connect(lambda m: self._status.showMessage(m), Q)
|
||||||
@ -254,7 +255,7 @@ class BooruApp(QMainWindow):
|
|||||||
self._dl_progress.hide()
|
self._dl_progress.hide()
|
||||||
self._status.showMessage(f"Error: {e}")
|
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}")
|
self._status.showMessage(f"Error: {e}")
|
||||||
|
|
||||||
def _run_async(self, coro_func, *args):
|
def _run_async(self, coro_func, *args):
|
||||||
@ -314,10 +315,16 @@ class BooruApp(QMainWindow):
|
|||||||
self._browse_btn.clicked.connect(lambda: self._switch_view(0))
|
self._browse_btn.clicked.connect(lambda: self._switch_view(0))
|
||||||
nav.addWidget(self._browse_btn)
|
nav.addWidget(self._browse_btn)
|
||||||
|
|
||||||
self._fav_btn = QPushButton("Favorites")
|
self._bookmark_btn = QPushButton("Bookmarks")
|
||||||
self._fav_btn.setCheckable(True)
|
self._bookmark_btn.setCheckable(True)
|
||||||
self._fav_btn.clicked.connect(lambda: self._switch_view(1))
|
self._bookmark_btn.clicked.connect(lambda: self._switch_view(1))
|
||||||
nav.addWidget(self._fav_btn)
|
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)
|
layout.addLayout(nav)
|
||||||
|
|
||||||
@ -338,10 +345,15 @@ class BooruApp(QMainWindow):
|
|||||||
self._grid.page_back.connect(self._prev_page)
|
self._grid.page_back.connect(self._prev_page)
|
||||||
self._stack.addWidget(self._grid)
|
self._stack.addWidget(self._grid)
|
||||||
|
|
||||||
self._favorites_view = FavoritesView(self._db)
|
self._bookmarks_view = BookmarksView(self._db)
|
||||||
self._favorites_view.favorite_selected.connect(self._on_favorite_selected)
|
self._bookmarks_view.bookmark_selected.connect(self._on_bookmark_selected)
|
||||||
self._favorites_view.favorite_activated.connect(self._on_favorite_activated)
|
self._bookmarks_view.bookmark_activated.connect(self._on_bookmark_activated)
|
||||||
self._stack.addWidget(self._favorites_view)
|
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)
|
self._splitter.addWidget(self._stack)
|
||||||
|
|
||||||
@ -352,7 +364,7 @@ class BooruApp(QMainWindow):
|
|||||||
self._preview.close_requested.connect(self._close_preview)
|
self._preview.close_requested.connect(self._close_preview)
|
||||||
self._preview.open_in_default.connect(self._open_preview_in_default)
|
self._preview.open_in_default.connect(self._open_preview_in_default)
|
||||||
self._preview.open_in_browser.connect(self._open_preview_in_browser)
|
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.save_to_folder.connect(self._save_from_preview)
|
||||||
self._preview.unsave_requested.connect(self._unsave_from_preview)
|
self._preview.unsave_requested.connect(self._unsave_from_preview)
|
||||||
self._preview.navigate.connect(self._navigate_preview)
|
self._preview.navigate.connect(self._navigate_preview)
|
||||||
@ -500,10 +512,13 @@ class BooruApp(QMainWindow):
|
|||||||
def _switch_view(self, index: int) -> None:
|
def _switch_view(self, index: int) -> None:
|
||||||
self._stack.setCurrentIndex(index)
|
self._stack.setCurrentIndex(index)
|
||||||
self._browse_btn.setChecked(index == 0)
|
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:
|
if index == 1:
|
||||||
self._favorites_view.refresh()
|
self._bookmarks_view.refresh()
|
||||||
self._favorites_view._grid.setFocus()
|
self._bookmarks_view._grid.setFocus()
|
||||||
|
elif index == 2:
|
||||||
|
self._library_view.refresh()
|
||||||
else:
|
else:
|
||||||
self._grid.setFocus()
|
self._grid.setFocus()
|
||||||
|
|
||||||
@ -672,21 +687,21 @@ class BooruApp(QMainWindow):
|
|||||||
if d.exists():
|
if d.exists():
|
||||||
_folder_saved[folder] = {int(f.stem) for f in d.iterdir() if f.is_file() and f.stem.isdigit()}
|
_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)
|
# Pre-fetch bookmarks for the site once (used for folder checks)
|
||||||
_favs = self._db.get_favorites(site_id=site_id) if site_id else []
|
_favs = self._db.get_bookmarks(site_id=site_id) if site_id else []
|
||||||
|
|
||||||
for i, (post, thumb) in enumerate(zip(posts, thumbs)):
|
for i, (post, thumb) in enumerate(zip(posts, thumbs)):
|
||||||
if site_id and self._db.is_favorited(site_id, post.id):
|
# Bookmark status (DB)
|
||||||
thumb.set_favorited(True)
|
if site_id and self._db.is_bookmarked(site_id, post.id):
|
||||||
# Check if saved to library (not just cached)
|
thumb.set_bookmarked(True)
|
||||||
saved = post.id in _saved_ids
|
# Saved status (filesystem) — independent of bookmark
|
||||||
if not saved:
|
saved = post.id in _saved_ids
|
||||||
# Check folders
|
if not saved:
|
||||||
for f in _favs:
|
for folder_name, folder_ids in _folder_saved.items():
|
||||||
if f.post_id == post.id and f.folder and f.folder in _folder_saved:
|
if post.id in folder_ids:
|
||||||
saved = post.id in _folder_saved[f.folder]
|
saved = True
|
||||||
break
|
break
|
||||||
thumb.set_saved_locally(saved)
|
thumb.set_saved_locally(saved)
|
||||||
# Set drag path from cache
|
# Set drag path from cache
|
||||||
from ..core.cache import cached_path_for
|
from ..core.cache import cached_path_for
|
||||||
cached = cached_path_for(post.file_url)
|
cached = cached_path_for(post.file_url)
|
||||||
@ -834,9 +849,9 @@ class BooruApp(QMainWindow):
|
|||||||
site_id = self._site_combo.currentData()
|
site_id = self._site_combo.currentData()
|
||||||
|
|
||||||
if self._stack.currentIndex() == 1:
|
if self._stack.currentIndex() == 1:
|
||||||
# Favorites view
|
# Bookmarks view
|
||||||
grid = self._favorites_view._grid
|
grid = self._bookmarks_view._grid
|
||||||
favs = self._favorites_view._favorites
|
favs = self._bookmarks_view._bookmarks
|
||||||
idx = grid.selected_index
|
idx = grid.selected_index
|
||||||
if 0 <= idx < len(favs):
|
if 0 <= idx < len(favs):
|
||||||
fav = favs[idx]
|
fav = favs[idx]
|
||||||
@ -858,7 +873,7 @@ class BooruApp(QMainWindow):
|
|||||||
idx = self._grid.selected_index
|
idx = self._grid.selected_index
|
||||||
if 0 <= idx < len(self._posts) and site_id:
|
if 0 <= idx < len(self._posts) and site_id:
|
||||||
post = self._posts[idx]
|
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 = any(
|
||||||
(saved_dir() / f"{post.id}{ext}").exists()
|
(saved_dir() / f"{post.id}{ext}").exists()
|
||||||
for ext in MEDIA_EXTENSIONS
|
for ext in MEDIA_EXTENSIONS
|
||||||
@ -871,7 +886,7 @@ class BooruApp(QMainWindow):
|
|||||||
)
|
)
|
||||||
if saved:
|
if saved:
|
||||||
break
|
break
|
||||||
self._fullscreen_window.update_state(favorited, saved)
|
self._fullscreen_window.update_state(bookmarked, saved)
|
||||||
else:
|
else:
|
||||||
self._fullscreen_window.update_state(False, False)
|
self._fullscreen_window.update_state(False, False)
|
||||||
|
|
||||||
@ -897,19 +912,27 @@ class BooruApp(QMainWindow):
|
|||||||
current = cache_size_bytes(include_thumbnails=False)
|
current = cache_size_bytes(include_thumbnails=False)
|
||||||
if current > max_bytes:
|
if current > max_bytes:
|
||||||
protected = set()
|
protected = set()
|
||||||
for fav in self._db.get_favorites(limit=999999):
|
for fav in self._db.get_bookmarks(limit=999999):
|
||||||
if fav.cached_path:
|
if fav.cached_path:
|
||||||
protected.add(fav.cached_path)
|
protected.add(fav.cached_path)
|
||||||
evicted = evict_oldest(max_bytes, protected)
|
evicted = evict_oldest(max_bytes, protected)
|
||||||
if evicted:
|
if evicted:
|
||||||
log.info(f"Auto-evicted {evicted} cached files")
|
log.info(f"Auto-evicted {evicted} cached files")
|
||||||
|
|
||||||
def _on_favorite_selected(self, fav) -> None:
|
def _on_library_selected(self, path: str) -> None:
|
||||||
self._status.showMessage(f"Favorite #{fav.post_id}")
|
self._preview.set_media(path, Path(path).name)
|
||||||
self._on_favorite_activated(fav)
|
self._update_fullscreen(path, Path(path).name)
|
||||||
|
|
||||||
def _on_favorite_activated(self, fav) -> None:
|
def _on_library_activated(self, path: str) -> None:
|
||||||
info = f"Favorite #{fav.post_id}"
|
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
|
# Try local cache first
|
||||||
if fav.cached_path and Path(fav.cached_path).exists():
|
if fav.cached_path and Path(fav.cached_path).exists():
|
||||||
@ -937,8 +960,8 @@ class BooruApp(QMainWindow):
|
|||||||
try:
|
try:
|
||||||
path = await download_image(fav.file_url)
|
path = await download_image(fav.file_url)
|
||||||
# Update cached_path in DB
|
# Update cached_path in DB
|
||||||
self._db.update_favorite_cache_path(fav.id, str(path))
|
self._db.update_bookmark_cache_path(fav.id, str(path))
|
||||||
info = f"Favorite #{fav.post_id}"
|
info = f"Bookmark #{fav.post_id}"
|
||||||
self._signals.image_done.emit(str(path), info)
|
self._signals.image_done.emit(str(path), info)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._signals.image_error.emit(str(e))
|
self._signals.image_error.emit(str(e))
|
||||||
@ -970,13 +993,13 @@ class BooruApp(QMainWindow):
|
|||||||
def _navigate_preview(self, direction: int) -> None:
|
def _navigate_preview(self, direction: int) -> None:
|
||||||
"""Navigate to prev/next post in the preview. direction: -1 or +1."""
|
"""Navigate to prev/next post in the preview. direction: -1 or +1."""
|
||||||
if self._stack.currentIndex() == 1:
|
if self._stack.currentIndex() == 1:
|
||||||
# Favorites view
|
# Bookmarks view
|
||||||
grid = self._favorites_view._grid
|
grid = self._bookmarks_view._grid
|
||||||
favs = self._favorites_view._favorites
|
favs = self._bookmarks_view._bookmarks
|
||||||
idx = grid.selected_index + direction
|
idx = grid.selected_index + direction
|
||||||
if 0 <= idx < len(favs):
|
if 0 <= idx < len(favs):
|
||||||
grid._select(idx)
|
grid._select(idx)
|
||||||
self._on_favorite_activated(favs[idx])
|
self._on_bookmark_activated(favs[idx])
|
||||||
else:
|
else:
|
||||||
idx = self._grid.selected_index + direction
|
idx = self._grid.selected_index + direction
|
||||||
log.info(f"Navigate: direction={direction} current={self._grid.selected_index} next={idx} total={len(self._posts)}")
|
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._nav_page_turn = "last"
|
||||||
self._prev_page()
|
self._prev_page()
|
||||||
|
|
||||||
def _favorite_from_preview(self) -> None:
|
def _bookmark_from_preview(self) -> None:
|
||||||
idx = self._grid.selected_index
|
idx = self._grid.selected_index
|
||||||
if 0 <= idx < len(self._posts):
|
if 0 <= idx < len(self._posts):
|
||||||
self._toggle_favorite(idx)
|
self._toggle_bookmark(idx)
|
||||||
self._update_fullscreen_state()
|
self._update_fullscreen_state()
|
||||||
|
|
||||||
def _save_from_preview(self, folder: str) -> None:
|
def _save_from_preview(self, folder: str) -> None:
|
||||||
@ -1013,7 +1036,7 @@ class BooruApp(QMainWindow):
|
|||||||
site_id = self._site_combo.currentData()
|
site_id = self._site_combo.currentData()
|
||||||
folder = None
|
folder = None
|
||||||
if site_id:
|
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:
|
for f in favs:
|
||||||
if f.post_id == post.id and f.folder:
|
if f.post_id == post.id and f.folder:
|
||||||
folder = f.folder
|
folder = f.folder
|
||||||
@ -1042,7 +1065,7 @@ class BooruApp(QMainWindow):
|
|||||||
cols = self._grid._flow.columns
|
cols = self._grid._flow.columns
|
||||||
self._fullscreen_window = FullscreenPreview(grid_cols=cols, parent=self)
|
self._fullscreen_window = FullscreenPreview(grid_cols=cols, parent=self)
|
||||||
self._fullscreen_window.navigate.connect(self._navigate_fullscreen)
|
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.save_toggle_requested.connect(self._save_toggle_from_slideshow)
|
||||||
self._fullscreen_window.destroyed.connect(self._on_fullscreen_closed)
|
self._fullscreen_window.destroyed.connect(self._on_fullscreen_closed)
|
||||||
self._fullscreen_window.set_media(path, self._preview._info_label.text())
|
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:
|
def _navigate_fullscreen(self, direction: int) -> None:
|
||||||
self._navigate_preview(direction)
|
self._navigate_preview(direction)
|
||||||
# For synchronous loads (cached/favorites), update immediately
|
# For synchronous loads (cached/bookmarks), update immediately
|
||||||
if self._preview._current_path:
|
if self._preview._current_path:
|
||||||
self._update_fullscreen(
|
self._update_fullscreen(
|
||||||
self._preview._current_path,
|
self._preview._current_path,
|
||||||
@ -1091,7 +1114,7 @@ class BooruApp(QMainWindow):
|
|||||||
copy_url = menu.addAction("Copy Image URL")
|
copy_url = menu.addAction("Copy Image URL")
|
||||||
copy_tags = menu.addAction("Copy Tags")
|
copy_tags = menu.addAction("Copy Tags")
|
||||||
menu.addSeparator()
|
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()
|
menu.addSeparator()
|
||||||
bl_menu = menu.addMenu("Blacklist Tag")
|
bl_menu = menu.addMenu("Blacklist Tag")
|
||||||
if post.tag_categories:
|
if post.tag_categories:
|
||||||
@ -1129,7 +1152,7 @@ class BooruApp(QMainWindow):
|
|||||||
site_id = self._site_combo.currentData()
|
site_id = self._site_combo.currentData()
|
||||||
folder = None
|
folder = None
|
||||||
if site_id:
|
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:
|
for f in favs:
|
||||||
if f.post_id == post.id and f.folder:
|
if f.post_id == post.id and f.folder:
|
||||||
folder = f.folder
|
folder = f.folder
|
||||||
@ -1147,7 +1170,7 @@ class BooruApp(QMainWindow):
|
|||||||
QApplication.clipboard().setText(post.tags)
|
QApplication.clipboard().setText(post.tags)
|
||||||
self._status.showMessage("Tags copied")
|
self._status.showMessage("Tags copied")
|
||||||
elif action == fav_action:
|
elif action == fav_action:
|
||||||
self._toggle_favorite(index)
|
self._toggle_bookmark(index)
|
||||||
elif self._is_child_of_menu(action, bl_menu):
|
elif self._is_child_of_menu(action, bl_menu):
|
||||||
tag = action.text()
|
tag = action.text()
|
||||||
self._db.add_blacklisted_tag(tag)
|
self._db.add_blacklisted_tag(tag)
|
||||||
@ -1176,7 +1199,7 @@ class BooruApp(QMainWindow):
|
|||||||
count = len(posts)
|
count = len(posts)
|
||||||
|
|
||||||
menu = QMenu(self)
|
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_menu = menu.addMenu(f"Save All to Library ({count})")
|
||||||
save_unsorted = save_menu.addAction("Unsorted")
|
save_unsorted = save_menu.addAction("Unsorted")
|
||||||
@ -1188,7 +1211,7 @@ class BooruApp(QMainWindow):
|
|||||||
save_new = save_menu.addAction("+ New Folder...")
|
save_new = save_menu.addAction("+ New Folder...")
|
||||||
|
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
unfav_all = menu.addAction(f"Unfavorite All ({count})")
|
unfav_all = menu.addAction(f"Remove All Bookmarks ({count})")
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
batch_dl = menu.addAction(f"Download All ({count})...")
|
batch_dl = menu.addAction(f"Download All ({count})...")
|
||||||
copy_urls = menu.addAction("Copy All URLs")
|
copy_urls = menu.addAction("Copy All URLs")
|
||||||
@ -1198,7 +1221,7 @@ class BooruApp(QMainWindow):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if action == fav_all:
|
if action == fav_all:
|
||||||
self._bulk_favorite(indices, posts)
|
self._bulk_bookmark(indices, posts)
|
||||||
elif action == save_unsorted:
|
elif action == save_unsorted:
|
||||||
self._bulk_save(indices, posts, None)
|
self._bulk_save(indices, posts, None)
|
||||||
elif action == save_new:
|
elif action == save_new:
|
||||||
@ -1225,19 +1248,19 @@ class BooruApp(QMainWindow):
|
|||||||
# Delete from all folders
|
# Delete from all folders
|
||||||
for folder in self._db.get_folders():
|
for folder in self._db.get_folders():
|
||||||
delete_from_library(post.id, folder)
|
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:
|
for idx in indices:
|
||||||
if 0 <= idx < len(self._grid._thumbs):
|
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._thumbs[idx].set_saved_locally(False)
|
||||||
self._grid._clear_multi()
|
self._grid._clear_multi()
|
||||||
self._status.showMessage(f"Unfavorited {count} posts")
|
self._status.showMessage(f"Removed {count} bookmarks")
|
||||||
elif action == copy_urls:
|
elif action == copy_urls:
|
||||||
urls = "\n".join(p.file_url for p in posts)
|
urls = "\n".join(p.file_url for p in posts)
|
||||||
QApplication.clipboard().setText(urls)
|
QApplication.clipboard().setText(urls)
|
||||||
self._status.showMessage(f"Copied {count} 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()
|
site_id = self._site_combo.currentData()
|
||||||
if not site_id:
|
if not site_id:
|
||||||
return
|
return
|
||||||
@ -1245,20 +1268,20 @@ class BooruApp(QMainWindow):
|
|||||||
|
|
||||||
async def _do():
|
async def _do():
|
||||||
for i, (idx, post) in enumerate(zip(indices, posts)):
|
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
|
continue
|
||||||
try:
|
try:
|
||||||
path = await download_image(post.file_url)
|
path = await download_image(post.file_url)
|
||||||
self._db.add_favorite(
|
self._db.add_bookmark(
|
||||||
site_id=site_id, post_id=post.id,
|
site_id=site_id, post_id=post.id,
|
||||||
file_url=post.file_url, preview_url=post.preview_url,
|
file_url=post.file_url, preview_url=post.preview_url,
|
||||||
tags=post.tags, rating=post.rating, score=post.score,
|
tags=post.tags, rating=post.rating, score=post.score,
|
||||||
source=post.source, cached_path=str(path),
|
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:
|
except Exception:
|
||||||
pass
|
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)
|
self._run_async(_do)
|
||||||
|
|
||||||
@ -1278,30 +1301,30 @@ class BooruApp(QMainWindow):
|
|||||||
dest = dest_dir / f"{post.id}{ext}"
|
dest = dest_dir / f"{post.id}{ext}"
|
||||||
if not dest.exists():
|
if not dest.exists():
|
||||||
shutil.copy2(path, dest)
|
shutil.copy2(path, dest)
|
||||||
if site_id and not self._db.is_favorited(site_id, post.id):
|
if site_id and not self._db.is_bookmarked(site_id, post.id):
|
||||||
self._db.add_favorite(
|
self._db.add_bookmark(
|
||||||
site_id=site_id, post_id=post.id,
|
site_id=site_id, post_id=post.id,
|
||||||
file_url=post.file_url, preview_url=post.preview_url,
|
file_url=post.file_url, preview_url=post.preview_url,
|
||||||
tags=post.tags, rating=post.rating, score=post.score,
|
tags=post.tags, rating=post.rating, score=post.score,
|
||||||
source=post.source, cached_path=str(path), folder=folder,
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._signals.batch_done.emit(f"Saved {len(posts)} to {where}")
|
self._signals.batch_done.emit(f"Saved {len(posts)} to {where}")
|
||||||
|
|
||||||
self._run_async(_do)
|
self._run_async(_do)
|
||||||
|
|
||||||
def _toggle_favorite_if_not(self, post: Post) -> None:
|
def _ensure_bookmarked(self, post: Post) -> None:
|
||||||
"""Favorite a post if not already favorited."""
|
"""Bookmark a post if not already bookmarked."""
|
||||||
site_id = self._site_combo.currentData()
|
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
|
return
|
||||||
|
|
||||||
async def _fav():
|
async def _fav():
|
||||||
try:
|
try:
|
||||||
path = await download_image(post.file_url)
|
path = await download_image(post.file_url)
|
||||||
self._db.add_favorite(
|
self._db.add_bookmark(
|
||||||
site_id=site_id,
|
site_id=site_id,
|
||||||
post_id=post.id,
|
post_id=post.id,
|
||||||
file_url=post.file_url,
|
file_url=post.file_url,
|
||||||
@ -1334,11 +1357,11 @@ class BooruApp(QMainWindow):
|
|||||||
|
|
||||||
self._run_async(_batch)
|
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()
|
site_id = self._site_combo.currentData()
|
||||||
if not site_id or index < 0 or index >= len(self._posts):
|
if not site_id or index < 0 or index >= len(self._posts):
|
||||||
return False
|
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:
|
def _open_in_browser(self, post: Post) -> None:
|
||||||
if self._current_site:
|
if self._current_site:
|
||||||
@ -1381,36 +1404,13 @@ class BooruApp(QMainWindow):
|
|||||||
import shutil
|
import shutil
|
||||||
shutil.copy2(path, dest)
|
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"
|
where = folder or "Unsorted"
|
||||||
self._signals.fav_done.emit(
|
self._signals.bookmark_done.emit(
|
||||||
self._grid.selected_index,
|
self._grid.selected_index,
|
||||||
f"Saved #{post.id} to {where}"
|
f"Saved #{post.id} to {where}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._signals.fav_error.emit(str(e))
|
self._signals.bookmark_error.emit(str(e))
|
||||||
|
|
||||||
self._run_async(_save)
|
self._run_async(_save)
|
||||||
|
|
||||||
@ -1479,12 +1479,12 @@ class BooruApp(QMainWindow):
|
|||||||
def _open_settings(self) -> None:
|
def _open_settings(self) -> None:
|
||||||
dlg = SettingsDialog(self._db, self)
|
dlg = SettingsDialog(self._db, self)
|
||||||
dlg.settings_changed.connect(self._apply_settings)
|
dlg.settings_changed.connect(self._apply_settings)
|
||||||
self._favorites_imported = False
|
self._bookmarks_imported = False
|
||||||
dlg.favorites_imported.connect(lambda: setattr(self, '_favorites_imported', True))
|
dlg.bookmarks_imported.connect(lambda: setattr(self, '_favorites_imported', True))
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
if self._favorites_imported:
|
if self._bookmarks_imported:
|
||||||
self._switch_view(1)
|
self._switch_view(1)
|
||||||
self._favorites_view.refresh()
|
self._bookmarks_view.refresh()
|
||||||
|
|
||||||
def _apply_settings(self) -> None:
|
def _apply_settings(self) -> None:
|
||||||
"""Re-read settings from DB and apply to UI."""
|
"""Re-read settings from DB and apply to UI."""
|
||||||
@ -1493,7 +1493,7 @@ class BooruApp(QMainWindow):
|
|||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
self._rating_combo.setCurrentIndex(idx)
|
self._rating_combo.setCurrentIndex(idx)
|
||||||
self._score_spin.setValue(self._db.get_setting_int("default_score"))
|
self._score_spin.setValue(self._db.get_setting_int("default_score"))
|
||||||
self._favorites_view.refresh()
|
self._bookmarks_view.refresh()
|
||||||
self._status.showMessage("Settings applied")
|
self._status.showMessage("Settings applied")
|
||||||
|
|
||||||
# -- Fullscreen & Privacy --
|
# -- Fullscreen & Privacy --
|
||||||
@ -1541,7 +1541,7 @@ class BooruApp(QMainWindow):
|
|||||||
if key == Qt.Key.Key_F and self._posts:
|
if key == Qt.Key.Key_F and self._posts:
|
||||||
idx = self._grid.selected_index
|
idx = self._grid.selected_index
|
||||||
if 0 <= idx < len(self._posts):
|
if 0 <= idx < len(self._posts):
|
||||||
self._toggle_favorite(idx)
|
self._toggle_bookmark(idx)
|
||||||
return
|
return
|
||||||
elif key == Qt.Key.Key_I:
|
elif key == Qt.Key.Key_I:
|
||||||
self._toggle_info()
|
self._toggle_info()
|
||||||
@ -1553,35 +1553,27 @@ class BooruApp(QMainWindow):
|
|||||||
return
|
return
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
# -- Favorites --
|
# -- Bookmarks --
|
||||||
|
|
||||||
def _toggle_favorite(self, index: int) -> None:
|
def _toggle_bookmark(self, index: int) -> None:
|
||||||
post = self._posts[index]
|
post = self._posts[index]
|
||||||
site_id = self._site_combo.currentData()
|
site_id = self._site_combo.currentData()
|
||||||
if not site_id:
|
if not site_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._db.is_favorited(site_id, post.id):
|
if self._db.is_bookmarked(site_id, post.id):
|
||||||
# Delete from library if saved
|
self._db.remove_bookmark(site_id, post.id)
|
||||||
favs = self._db.get_favorites(site_id=site_id)
|
self._status.showMessage(f"Unbookmarked #{post.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}")
|
|
||||||
thumbs = self._grid._thumbs
|
thumbs = self._grid._thumbs
|
||||||
if 0 <= index < len(thumbs):
|
if 0 <= index < len(thumbs):
|
||||||
thumbs[index].set_favorited(False)
|
thumbs[index].set_bookmarked(False)
|
||||||
thumbs[index].set_saved_locally(False)
|
|
||||||
else:
|
else:
|
||||||
self._status.showMessage(f"Favoriting #{post.id}...")
|
self._status.showMessage(f"Bookmarking #{post.id}...")
|
||||||
|
|
||||||
async def _fav():
|
async def _fav():
|
||||||
try:
|
try:
|
||||||
path = await download_image(post.file_url)
|
path = await download_image(post.file_url)
|
||||||
self._db.add_favorite(
|
self._db.add_bookmark(
|
||||||
site_id=site_id,
|
site_id=site_id,
|
||||||
post_id=post.id,
|
post_id=post.id,
|
||||||
file_url=post.file_url,
|
file_url=post.file_url,
|
||||||
@ -1592,17 +1584,17 @@ class BooruApp(QMainWindow):
|
|||||||
source=post.source,
|
source=post.source,
|
||||||
cached_path=str(path),
|
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:
|
except Exception as e:
|
||||||
self._signals.fav_error.emit(str(e))
|
self._signals.bookmark_error.emit(str(e))
|
||||||
|
|
||||||
self._run_async(_fav)
|
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)
|
self._status.showMessage(msg)
|
||||||
thumbs = self._grid._thumbs
|
thumbs = self._grid._thumbs
|
||||||
if 0 <= index < len(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
|
# Only green if actually saved to library, not just cached
|
||||||
if "Saved" in msg:
|
if "Saved" in msg:
|
||||||
thumbs[index].set_saved_locally(True)
|
thumbs[index].set_saved_locally(True)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""Favorites browser widget with folder support."""
|
"""Bookmarks browser widget with folder support."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ from PySide6.QtWidgets import (
|
|||||||
QMessageBox,
|
QMessageBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..core.db import Database, Favorite
|
from ..core.db import Database, Bookmark
|
||||||
from ..core.cache import download_thumbnail
|
from ..core.cache import download_thumbnail
|
||||||
from .grid import ThumbnailGrid
|
from .grid import ThumbnailGrid
|
||||||
|
|
||||||
@ -34,16 +34,16 @@ class FavThumbSignals(QObject):
|
|||||||
thumb_ready = Signal(int, str)
|
thumb_ready = Signal(int, str)
|
||||||
|
|
||||||
|
|
||||||
class FavoritesView(QWidget):
|
class BookmarksView(QWidget):
|
||||||
"""Browse and search local favorites with folder support."""
|
"""Browse and search local bookmarks with folder support."""
|
||||||
|
|
||||||
favorite_selected = Signal(object)
|
bookmark_selected = Signal(object)
|
||||||
favorite_activated = Signal(object)
|
bookmark_activated = Signal(object)
|
||||||
|
|
||||||
def __init__(self, db: Database, parent: QWidget | None = None) -> None:
|
def __init__(self, db: Database, parent: QWidget | None = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._db = db
|
self._db = db
|
||||||
self._favorites: list[Favorite] = []
|
self._bookmarks: list[Bookmark] = []
|
||||||
self._signals = FavThumbSignals()
|
self._signals = FavThumbSignals()
|
||||||
self._signals.thumb_ready.connect(self._on_thumb_ready, Qt.ConnectionType.QueuedConnection)
|
self._signals.thumb_ready.connect(self._on_thumb_ready, Qt.ConnectionType.QueuedConnection)
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ class FavoritesView(QWidget):
|
|||||||
top.addWidget(manage_btn)
|
top.addWidget(manage_btn)
|
||||||
|
|
||||||
self._search_input = QLineEdit()
|
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)
|
self._search_input.returnPressed.connect(self._do_search)
|
||||||
top.addWidget(self._search_input, stretch=1)
|
top.addWidget(self._search_input, stretch=1)
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ class FavoritesView(QWidget):
|
|||||||
current = self._folder_combo.currentText()
|
current = self._folder_combo.currentText()
|
||||||
self._folder_combo.blockSignals(True)
|
self._folder_combo.blockSignals(True)
|
||||||
self._folder_combo.clear()
|
self._folder_combo.clear()
|
||||||
self._folder_combo.addItem("All Favorites")
|
self._folder_combo.addItem("All Bookmarks")
|
||||||
self._folder_combo.addItem("Unfiled")
|
self._folder_combo.addItem("Unfiled")
|
||||||
for folder in self._db.get_folders():
|
for folder in self._db.get_folders():
|
||||||
self._folder_combo.addItem(folder)
|
self._folder_combo.addItem(folder)
|
||||||
@ -110,26 +110,26 @@ class FavoritesView(QWidget):
|
|||||||
folder_filter = None
|
folder_filter = None
|
||||||
if folder_text == "Unfiled":
|
if folder_text == "Unfiled":
|
||||||
folder_filter = "" # sentinel for NULL folder
|
folder_filter = "" # sentinel for NULL folder
|
||||||
elif folder_text not in ("All Favorites", ""):
|
elif folder_text not in ("All Bookmarks", ""):
|
||||||
folder_filter = folder_text
|
folder_filter = folder_text
|
||||||
|
|
||||||
if folder_filter == "":
|
if folder_filter == "":
|
||||||
# Get unfiled: folder IS NULL
|
# Get unfiled: folder IS NULL
|
||||||
self._favorites = [
|
self._bookmarks = [
|
||||||
f for f in self._db.get_favorites(search=search, limit=500)
|
f for f in self._db.get_bookmarks(search=search, limit=500)
|
||||||
if f.folder is None
|
if f.folder is None
|
||||||
]
|
]
|
||||||
elif folder_filter:
|
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:
|
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")
|
self._count_label.setText(f"{len(self._bookmarks)} bookmarks")
|
||||||
thumbs = self._grid.set_posts(len(self._favorites))
|
thumbs = self._grid.set_posts(len(self._bookmarks))
|
||||||
|
|
||||||
from ..core.config import saved_dir, saved_folder_dir, MEDIA_EXTENSIONS
|
from ..core.config import saved_dir, saved_folder_dir, MEDIA_EXTENSIONS
|
||||||
for i, (fav, thumb) in enumerate(zip(self._favorites, thumbs)):
|
for i, (fav, thumb) in enumerate(zip(self._bookmarks, thumbs)):
|
||||||
thumb.set_favorited(True)
|
thumb.set_bookmarked(True)
|
||||||
# Check if saved to library
|
# Check if saved to library
|
||||||
saved = False
|
saved = False
|
||||||
if fav.folder:
|
if fav.folder:
|
||||||
@ -171,15 +171,15 @@ class FavoritesView(QWidget):
|
|||||||
self.refresh(search=text if text else None)
|
self.refresh(search=text if text else None)
|
||||||
|
|
||||||
def _on_selected(self, index: int) -> None:
|
def _on_selected(self, index: int) -> None:
|
||||||
if 0 <= index < len(self._favorites):
|
if 0 <= index < len(self._bookmarks):
|
||||||
self.favorite_selected.emit(self._favorites[index])
|
self.bookmark_selected.emit(self._bookmarks[index])
|
||||||
|
|
||||||
def _on_activated(self, index: int) -> None:
|
def _on_activated(self, index: int) -> None:
|
||||||
if 0 <= index < len(self._favorites):
|
if 0 <= index < len(self._bookmarks):
|
||||||
self.favorite_activated.emit(self._favorites[index])
|
self.bookmark_activated.emit(self._bookmarks[index])
|
||||||
|
|
||||||
def _copy_to_library_unsorted(self, fav: Favorite) -> None:
|
def _copy_to_library_unsorted(self, fav: Bookmark) -> None:
|
||||||
"""Copy a favorited image to the unsorted library folder."""
|
"""Copy a bookmarked image to the unsorted library folder."""
|
||||||
from ..core.config import saved_dir
|
from ..core.config import saved_dir
|
||||||
if fav.cached_path and Path(fav.cached_path).exists():
|
if fav.cached_path and Path(fav.cached_path).exists():
|
||||||
import shutil
|
import shutil
|
||||||
@ -188,8 +188,8 @@ class FavoritesView(QWidget):
|
|||||||
if not dest.exists():
|
if not dest.exists():
|
||||||
shutil.copy2(src, dest)
|
shutil.copy2(src, dest)
|
||||||
|
|
||||||
def _copy_to_library(self, fav: Favorite, folder: str) -> None:
|
def _copy_to_library(self, fav: Bookmark, folder: str) -> None:
|
||||||
"""Copy a favorited image to the library folder on disk."""
|
"""Copy a bookmarked image to the library folder on disk."""
|
||||||
from ..core.config import saved_folder_dir
|
from ..core.config import saved_folder_dir
|
||||||
if fav.cached_path and Path(fav.cached_path).exists():
|
if fav.cached_path and Path(fav.cached_path).exists():
|
||||||
import shutil
|
import shutil
|
||||||
@ -205,9 +205,9 @@ class FavoritesView(QWidget):
|
|||||||
self._refresh_folders()
|
self._refresh_folders()
|
||||||
|
|
||||||
def _on_context_menu(self, index: int, pos) -> None:
|
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
|
return
|
||||||
fav = self._favorites[index]
|
fav = self._bookmarks[index]
|
||||||
|
|
||||||
from PySide6.QtGui import QDesktopServices
|
from PySide6.QtGui import QDesktopServices
|
||||||
from PySide6.QtCore import QUrl
|
from PySide6.QtCore import QUrl
|
||||||
@ -246,7 +246,7 @@ class FavoritesView(QWidget):
|
|||||||
move_new = move_menu.addAction("+ New Folder...")
|
move_new = move_menu.addAction("+ New Folder...")
|
||||||
|
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
unfav = menu.addAction("Unfavorite")
|
remove_bookmark = menu.addAction("Remove Bookmark")
|
||||||
|
|
||||||
action = menu.exec(pos)
|
action = menu.exec(pos)
|
||||||
if not action:
|
if not action:
|
||||||
@ -260,7 +260,7 @@ class FavoritesView(QWidget):
|
|||||||
if ok and name.strip():
|
if ok and name.strip():
|
||||||
self._db.add_folder(name.strip())
|
self._db.add_folder(name.strip())
|
||||||
self._copy_to_library(fav, 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()
|
self.refresh()
|
||||||
elif id(action) in save_lib_folders:
|
elif id(action) in save_lib_folders:
|
||||||
folder_name = save_lib_folders[id(action)]
|
folder_name = save_lib_folders[id(action)]
|
||||||
@ -281,28 +281,26 @@ class FavoritesView(QWidget):
|
|||||||
elif action == copy_tags:
|
elif action == copy_tags:
|
||||||
QApplication.clipboard().setText(fav.tags)
|
QApplication.clipboard().setText(fav.tags)
|
||||||
elif action == move_none:
|
elif action == move_none:
|
||||||
self._db.move_favorite_to_folder(fav.id, None)
|
self._db.move_bookmark_to_folder(fav.id, None)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
elif action == move_new:
|
elif action == move_new:
|
||||||
name, ok = QInputDialog.getText(self, "New Folder", "Folder name:")
|
name, ok = QInputDialog.getText(self, "New Folder", "Folder name:")
|
||||||
if ok and name.strip():
|
if ok and name.strip():
|
||||||
self._db.add_folder(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._copy_to_library(fav, name.strip())
|
||||||
self.refresh()
|
self.refresh()
|
||||||
elif id(action) in folder_actions:
|
elif id(action) in folder_actions:
|
||||||
folder_name = folder_actions[id(action)]
|
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._copy_to_library(fav, folder_name)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
elif action == unfav:
|
elif action == remove_bookmark:
|
||||||
from ..core.cache import delete_from_library
|
self._db.remove_bookmark(fav.site_id, fav.post_id)
|
||||||
delete_from_library(fav.post_id, fav.folder)
|
|
||||||
self._db.remove_favorite(fav.site_id, fav.post_id)
|
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def _on_multi_context_menu(self, indices: list, pos) -> None:
|
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:
|
if not favs:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -320,7 +318,7 @@ class FavoritesView(QWidget):
|
|||||||
folder_actions[id(a)] = folder
|
folder_actions[id(a)] = folder
|
||||||
|
|
||||||
menu.addSeparator()
|
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)
|
action = menu.exec(pos)
|
||||||
if not action:
|
if not action:
|
||||||
@ -340,17 +338,15 @@ class FavoritesView(QWidget):
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
elif action == move_none:
|
elif action == move_none:
|
||||||
for fav in favs:
|
for fav in favs:
|
||||||
self._db.move_favorite_to_folder(fav.id, None)
|
self._db.move_bookmark_to_folder(fav.id, None)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
elif id(action) in folder_actions:
|
elif id(action) in folder_actions:
|
||||||
folder_name = folder_actions[id(action)]
|
folder_name = folder_actions[id(action)]
|
||||||
for fav in favs:
|
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._copy_to_library(fav, folder_name)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
elif action == unfav_all:
|
elif action == remove_all:
|
||||||
from ..core.cache import delete_from_library
|
|
||||||
for fav in favs:
|
for fav in favs:
|
||||||
delete_from_library(fav.post_id, fav.folder)
|
self._db.remove_bookmark(fav.site_id, fav.post_id)
|
||||||
self._db.remove_favorite(fav.site_id, fav.post_id)
|
|
||||||
self.refresh()
|
self.refresh()
|
||||||
@ -27,17 +27,17 @@ class ThumbnailWidget(QWidget):
|
|||||||
double_clicked = Signal(int)
|
double_clicked = Signal(int)
|
||||||
right_clicked = Signal(int, object) # index, QPoint
|
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")
|
_saved_color = QColor("#22cc22")
|
||||||
_favorited_color = QColor("#ff4444")
|
_bookmarked_color = QColor("#ff4444")
|
||||||
|
|
||||||
def _get_saved_color(self): return self._saved_color
|
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
|
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)
|
savedColor = Property(QColor, _get_saved_color, _set_saved_color)
|
||||||
|
|
||||||
def _get_favorited_color(self): return self._favorited_color
|
def _get_bookmarked_color(self): return self._bookmarked_color
|
||||||
def _set_favorited_color(self, c): self._favorited_color = QColor(c) if isinstance(c, str) else c
|
def _set_bookmarked_color(self, c): self._bookmarked_color = QColor(c) if isinstance(c, str) else c
|
||||||
favoritedColor = Property(QColor, _get_favorited_color, _set_favorited_color)
|
bookmarkedColor = Property(QColor, _get_bookmarked_color, _set_bookmarked_color)
|
||||||
|
|
||||||
def __init__(self, index: int, parent: QWidget | None = None) -> None:
|
def __init__(self, index: int, parent: QWidget | None = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -45,7 +45,7 @@ class ThumbnailWidget(QWidget):
|
|||||||
self._pixmap: QPixmap | None = None
|
self._pixmap: QPixmap | None = None
|
||||||
self._selected = False
|
self._selected = False
|
||||||
self._multi_selected = False
|
self._multi_selected = False
|
||||||
self._favorited = False
|
self._bookmarked = False
|
||||||
self._saved_locally = False
|
self._saved_locally = False
|
||||||
self._hover = False
|
self._hover = False
|
||||||
self._drag_start: QPoint | None = None
|
self._drag_start: QPoint | None = None
|
||||||
@ -71,8 +71,8 @@ class ThumbnailWidget(QWidget):
|
|||||||
self._multi_selected = selected
|
self._multi_selected = selected
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def set_favorited(self, favorited: bool) -> None:
|
def set_bookmarked(self, bookmarked: bool) -> None:
|
||||||
self._favorited = favorited
|
self._bookmarked = bookmarked
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def set_saved_locally(self, saved: bool) -> None:
|
def set_saved_locally(self, saved: bool) -> None:
|
||||||
@ -120,11 +120,20 @@ class ThumbnailWidget(QWidget):
|
|||||||
y = (self.height() - self._pixmap.height()) // 2
|
y = (self.height() - self._pixmap.height()) // 2
|
||||||
p.drawPixmap(x, y, self._pixmap)
|
p.drawPixmap(x, y, self._pixmap)
|
||||||
|
|
||||||
# Favorite/saved indicator
|
# Bookmark/saved indicators (independent dots)
|
||||||
if self._favorited:
|
dot_x = self.width() - 14
|
||||||
|
if self._saved_locally:
|
||||||
p.setPen(Qt.PenStyle.NoPen)
|
p.setPen(Qt.PenStyle.NoPen)
|
||||||
p.setBrush(self._saved_color if self._saved_locally else self._favorited_color)
|
p.setBrush(self._saved_color)
|
||||||
p.drawEllipse(self.width() - 14, 4, 10, 10)
|
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
|
# Multi-select checkmark
|
||||||
if self._multi_selected:
|
if self._multi_selected:
|
||||||
|
|||||||
224
booru_viewer/gui/library.py
Normal file
224
booru_viewer/gui/library.py
Normal 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]))
|
||||||
@ -26,7 +26,7 @@ class FullscreenPreview(QMainWindow):
|
|||||||
"""Fullscreen media viewer with navigation — images, GIFs, and video."""
|
"""Fullscreen media viewer with navigation — images, GIFs, and video."""
|
||||||
|
|
||||||
navigate = Signal(int) # direction: -1/+1 for left/right, -cols/+cols for up/down
|
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
|
save_toggle_requested = Signal() # save or unsave depending on state
|
||||||
|
|
||||||
def __init__(self, grid_cols: int = 3, parent=None) -> None:
|
def __init__(self, grid_cols: int = 3, parent=None) -> None:
|
||||||
@ -44,10 +44,10 @@ class FullscreenPreview(QMainWindow):
|
|||||||
toolbar = QHBoxLayout(self._toolbar)
|
toolbar = QHBoxLayout(self._toolbar)
|
||||||
toolbar.setContentsMargins(8, 4, 8, 4)
|
toolbar.setContentsMargins(8, 4, 8, 4)
|
||||||
|
|
||||||
self._fav_btn = QPushButton("Favorite")
|
self._bookmark_btn = QPushButton("Bookmark")
|
||||||
self._fav_btn.setFixedWidth(80)
|
self._bookmark_btn.setFixedWidth(80)
|
||||||
self._fav_btn.clicked.connect(self.favorite_requested)
|
self._bookmark_btn.clicked.connect(self.bookmark_requested)
|
||||||
toolbar.addWidget(self._fav_btn)
|
toolbar.addWidget(self._bookmark_btn)
|
||||||
|
|
||||||
self._save_btn = QPushButton("Save")
|
self._save_btn = QPushButton("Save")
|
||||||
self._save_btn.setFixedWidth(70)
|
self._save_btn.setFixedWidth(70)
|
||||||
@ -80,9 +80,9 @@ class FullscreenPreview(QMainWindow):
|
|||||||
QApplication.instance().installEventFilter(self)
|
QApplication.instance().installEventFilter(self)
|
||||||
self.showFullScreen()
|
self.showFullScreen()
|
||||||
|
|
||||||
def update_state(self, favorited: bool, saved: bool) -> None:
|
def update_state(self, bookmarked: bool, saved: bool) -> None:
|
||||||
self._fav_btn.setText("Unfavorite" if favorited else "Favorite")
|
self._bookmark_btn.setText("Unbookmark" if bookmarked else "Bookmark")
|
||||||
self._fav_btn.setFixedWidth(90 if favorited else 80)
|
self._bookmark_btn.setFixedWidth(90 if bookmarked else 80)
|
||||||
self._is_saved = saved
|
self._is_saved = saved
|
||||||
self._save_btn.setText("Unsave" if saved else "Save")
|
self._save_btn.setText("Unsave" if saved else "Save")
|
||||||
|
|
||||||
@ -500,7 +500,7 @@ class ImagePreview(QWidget):
|
|||||||
open_in_browser = Signal()
|
open_in_browser = Signal()
|
||||||
save_to_folder = Signal(str)
|
save_to_folder = Signal(str)
|
||||||
unsave_requested = Signal()
|
unsave_requested = Signal()
|
||||||
favorite_requested = Signal()
|
bookmark_requested = Signal()
|
||||||
navigate = Signal(int) # -1 = prev, +1 = next
|
navigate = Signal(int) # -1 = prev, +1 = next
|
||||||
fullscreen_requested = Signal()
|
fullscreen_requested = Signal()
|
||||||
|
|
||||||
@ -587,7 +587,7 @@ class ImagePreview(QWidget):
|
|||||||
|
|
||||||
def _on_context_menu(self, pos) -> None:
|
def _on_context_menu(self, pos) -> None:
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
fav_action = menu.addAction("Favorite")
|
fav_action = menu.addAction("Bookmark")
|
||||||
|
|
||||||
save_menu = menu.addMenu("Save to Library")
|
save_menu = menu.addMenu("Save to Library")
|
||||||
save_unsorted = save_menu.addAction("Unsorted")
|
save_unsorted = save_menu.addAction("Unsorted")
|
||||||
@ -624,7 +624,7 @@ class ImagePreview(QWidget):
|
|||||||
if not action:
|
if not action:
|
||||||
return
|
return
|
||||||
if action == fav_action:
|
if action == fav_action:
|
||||||
self.favorite_requested.emit()
|
self.bookmark_requested.emit()
|
||||||
elif action == save_unsorted:
|
elif action == save_unsorted:
|
||||||
self.save_to_folder.emit("")
|
self.save_to_folder.emit("")
|
||||||
elif action == save_new:
|
elif action == save_new:
|
||||||
|
|||||||
@ -35,7 +35,7 @@ class SettingsDialog(QDialog):
|
|||||||
"""Full settings panel with tabs."""
|
"""Full settings panel with tabs."""
|
||||||
|
|
||||||
settings_changed = Signal()
|
settings_changed = Signal()
|
||||||
favorites_imported = Signal()
|
bookmarks_imported = Signal()
|
||||||
|
|
||||||
def __init__(self, db: Database, parent: QWidget | None = None) -> None:
|
def __init__(self, db: Database, parent: QWidget | None = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -153,8 +153,8 @@ class SettingsDialog(QDialog):
|
|||||||
self._cache_size_label = QLabel(f"{total_mb:.1f} MB")
|
self._cache_size_label = QLabel(f"{total_mb:.1f} MB")
|
||||||
stats_layout.addRow("Total size:", self._cache_size_label)
|
stats_layout.addRow("Total size:", self._cache_size_label)
|
||||||
|
|
||||||
self._fav_count_label = QLabel(f"{self._db.favorite_count()}")
|
self._fav_count_label = QLabel(f"{self._db.bookmark_count()}")
|
||||||
stats_layout.addRow("Favorites:", self._fav_count_label)
|
stats_layout.addRow("Bookmarks:", self._fav_count_label)
|
||||||
|
|
||||||
layout.addWidget(stats_group)
|
layout.addWidget(stats_group)
|
||||||
|
|
||||||
@ -317,12 +317,12 @@ class SettingsDialog(QDialog):
|
|||||||
exp_group = QGroupBox("Backup")
|
exp_group = QGroupBox("Backup")
|
||||||
exp_layout = QHBoxLayout(exp_group)
|
exp_layout = QHBoxLayout(exp_group)
|
||||||
|
|
||||||
export_btn = QPushButton("Export Favorites")
|
export_btn = QPushButton("Export Bookmarks")
|
||||||
export_btn.clicked.connect(self._export_favorites)
|
export_btn.clicked.connect(self._export_bookmarks)
|
||||||
exp_layout.addWidget(export_btn)
|
exp_layout.addWidget(export_btn)
|
||||||
|
|
||||||
import_btn = QPushButton("Import Favorites")
|
import_btn = QPushButton("Import Bookmarks")
|
||||||
import_btn.clicked.connect(self._import_favorites)
|
import_btn.clicked.connect(self._import_bookmarks)
|
||||||
exp_layout.addWidget(import_btn)
|
exp_layout.addWidget(import_btn)
|
||||||
|
|
||||||
layout.addWidget(exp_group)
|
layout.addWidget(exp_group)
|
||||||
@ -499,7 +499,7 @@ class SettingsDialog(QDialog):
|
|||||||
def _clear_image_cache(self) -> None:
|
def _clear_image_cache(self) -> None:
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self, "Confirm",
|
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,
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
@ -520,9 +520,9 @@ class SettingsDialog(QDialog):
|
|||||||
|
|
||||||
def _evict_now(self) -> None:
|
def _evict_now(self) -> None:
|
||||||
max_bytes = self._max_cache.value() * 1024 * 1024
|
max_bytes = self._max_cache.value() * 1024 * 1024
|
||||||
# Protect favorited file paths
|
# Protect bookmarked file paths
|
||||||
protected = set()
|
protected = set()
|
||||||
for fav in self._db.get_favorites(limit=999999):
|
for fav in self._db.get_bookmarks(limit=999999):
|
||||||
if fav.cached_path:
|
if fav.cached_path:
|
||||||
protected.add(fav.cached_path)
|
protected.add(fav.cached_path)
|
||||||
count = evict_oldest(max_bytes, protected)
|
count = evict_oldest(max_bytes, protected)
|
||||||
@ -559,13 +559,13 @@ class SettingsDialog(QDialog):
|
|||||||
from PySide6.QtCore import QUrl
|
from PySide6.QtCore import QUrl
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(str(data_dir())))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(str(data_dir())))
|
||||||
|
|
||||||
def _export_favorites(self) -> None:
|
def _export_bookmarks(self) -> None:
|
||||||
from .dialogs import save_file
|
from .dialogs import save_file
|
||||||
import json
|
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:
|
if not path:
|
||||||
return
|
return
|
||||||
favs = self._db.get_favorites(limit=999999)
|
favs = self._db.get_bookmarks(limit=999999)
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
"post_id": f.post_id,
|
"post_id": f.post_id,
|
||||||
@ -577,18 +577,18 @@ class SettingsDialog(QDialog):
|
|||||||
"score": f.score,
|
"score": f.score,
|
||||||
"source": f.source,
|
"source": f.source,
|
||||||
"folder": f.folder,
|
"folder": f.folder,
|
||||||
"favorited_at": f.favorited_at,
|
"bookmarked_at": f.bookmarked_at,
|
||||||
}
|
}
|
||||||
for f in favs
|
for f in favs
|
||||||
]
|
]
|
||||||
with open(path, "w") as fp:
|
with open(path, "w") as fp:
|
||||||
json.dump(data, fp, indent=2)
|
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
|
from .dialogs import open_file
|
||||||
import json
|
import json
|
||||||
path = open_file(self, "Import Favorites", "JSON (*.json)")
|
path = open_file(self, "Import Bookmarks", "JSON (*.json)")
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@ -598,7 +598,7 @@ class SettingsDialog(QDialog):
|
|||||||
for item in data:
|
for item in data:
|
||||||
try:
|
try:
|
||||||
folder = item.get("folder")
|
folder = item.get("folder")
|
||||||
self._db.add_favorite(
|
self._db.add_bookmark(
|
||||||
site_id=item["site_id"],
|
site_id=item["site_id"],
|
||||||
post_id=item["post_id"],
|
post_id=item["post_id"],
|
||||||
file_url=item["file_url"],
|
file_url=item["file_url"],
|
||||||
@ -614,8 +614,8 @@ class SettingsDialog(QDialog):
|
|||||||
count += 1
|
count += 1
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
QMessageBox.information(self, "Done", f"Imported {count} favorites.")
|
QMessageBox.information(self, "Done", f"Imported {count} bookmarks.")
|
||||||
self.favorites_imported.emit()
|
self.bookmarks_imported.emit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, "Error", str(e))
|
QMessageBox.warning(self, "Error", str(e))
|
||||||
|
|
||||||
|
|||||||
@ -288,7 +288,7 @@ class SiteManagerDialog(QDialog):
|
|||||||
return
|
return
|
||||||
site_id = item.data(Qt.ItemDataRole.UserRole)
|
site_id = item.data(Qt.ItemDataRole.UserRole)
|
||||||
reply = QMessageBox.question(
|
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,
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
|||||||
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "booru-viewer"
|
name = "booru-viewer"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
description = "Local booru image browser with Qt6 GUI"
|
description = "Local booru image browser with Qt6 GUI"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user