Slideshow toolbar, unsave from library, fix async error handling

- Add Favorite/Save/Unsave buttons to slideshow mode toolbar
- Add "Unsave from Library" to grid and preview right-click menus
- Fix silent exception swallowing in persistent event loop
- Fix closeEvent race condition with async thread join
This commit is contained in:
pax 2026-04-04 20:31:10 -05:00
parent afa08ff007
commit 4675c0a691
2 changed files with 97 additions and 3 deletions

View File

@ -217,7 +217,15 @@ class BooruApp(QMainWindow):
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):
asyncio.run_coroutine_threadsafe(coro_func(*args), self._async_loop) future = asyncio.run_coroutine_threadsafe(coro_func(*args), self._async_loop)
future.add_done_callback(self._on_async_done)
@staticmethod
def _on_async_done(future):
try:
future.result()
except Exception as e:
log.error(f"Async worker failed: {e}")
def _setup_ui(self) -> None: def _setup_ui(self) -> None:
central = QWidget() central = QWidget()
@ -303,6 +311,7 @@ class BooruApp(QMainWindow):
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.favorite_requested.connect(self._favorite_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.navigate.connect(self._navigate_preview) self._preview.navigate.connect(self._navigate_preview)
self._preview.fullscreen_requested.connect(self._open_fullscreen_preview) self._preview.fullscreen_requested.connect(self._open_fullscreen_preview)
self._preview.set_folders_callback(self._db.get_folders) self._preview.set_folders_callback(self._db.get_folders)
@ -841,6 +850,26 @@ class BooruApp(QMainWindow):
self._db.add_folder(folder) self._db.add_folder(folder)
self._save_to_library(self._posts[idx], target) self._save_to_library(self._posts[idx], target)
def _unsave_from_preview(self) -> None:
idx = self._grid.selected_index
if 0 <= idx < len(self._posts):
post = self._posts[idx]
from ..core.cache import delete_from_library
site_id = self._site_combo.currentData()
folder = None
if site_id:
favs = self._db.get_favorites(site_id=site_id)
for f in favs:
if f.post_id == post.id and f.folder:
folder = f.folder
break
if delete_from_library(post.id, folder):
self._status.showMessage(f"Removed #{post.id} from library")
if 0 <= idx < len(self._grid._thumbs):
self._grid._thumbs[idx].set_saved_locally(False)
else:
self._status.showMessage(f"#{post.id} not in library")
def _open_fullscreen_preview(self) -> None: def _open_fullscreen_preview(self) -> None:
path = self._preview._current_path path = self._preview._current_path
if not path: if not path:
@ -851,6 +880,9 @@ 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.save_requested.connect(lambda: self._save_from_preview(""))
self._fullscreen_window.unsave_requested.connect(self._unsave_from_preview)
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())
@ -893,6 +925,7 @@ class BooruApp(QMainWindow):
save_lib_menu.addSeparator() save_lib_menu.addSeparator()
save_lib_new = save_lib_menu.addAction("+ New Folder...") save_lib_new = save_lib_menu.addAction("+ New Folder...")
unsave_lib = menu.addAction("Unsave from Library")
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()
@ -922,6 +955,22 @@ class BooruApp(QMainWindow):
self._save_to_library(post, name.strip()) self._save_to_library(post, name.strip())
elif id(action) in save_lib_folders: elif id(action) in save_lib_folders:
self._save_to_library(post, save_lib_folders[id(action)]) self._save_to_library(post, save_lib_folders[id(action)])
elif action == unsave_lib:
from ..core.cache import delete_from_library
site_id = self._site_combo.currentData()
folder = None
if site_id:
favs = self._db.get_favorites(site_id=site_id)
for f in favs:
if f.post_id == post.id and f.folder:
folder = f.folder
break
if delete_from_library(post.id, folder):
self._status.showMessage(f"Removed #{post.id} from library")
if 0 <= index < len(self._grid._thumbs):
self._grid._thumbs[index].set_saved_locally(False)
else:
self._status.showMessage(f"#{post.id} not in library")
elif action == copy_url: elif action == copy_url:
QApplication.clipboard().setText(post.file_url) QApplication.clipboard().setText(post.file_url)
self._status.showMessage("URL copied") self._status.showMessage("URL copied")
@ -1376,6 +1425,7 @@ class BooruApp(QMainWindow):
def closeEvent(self, event) -> None: def closeEvent(self, event) -> None:
self._async_loop.call_soon_threadsafe(self._async_loop.stop) self._async_loop.call_soon_threadsafe(self._async_loop.stop)
self._async_thread.join(timeout=2)
if self._db.get_setting_bool("clear_cache_on_exit"): if self._db.get_setting_bool("clear_cache_on_exit"):
from ..core.cache import clear_cache from ..core.cache import clear_cache
clear_cache(clear_images=True, clear_thumbnails=True) clear_cache(clear_images=True, clear_thumbnails=True)

View File

@ -25,15 +25,50 @@ def _is_video(path: str) -> bool:
class FullscreenPreview(QMainWindow): class FullscreenPreview(QMainWindow):
"""Fullscreen media viewer with navigation — images, GIFs, and video.""" """Fullscreen media viewer with navigation — images, GIFs, and video."""
navigate = Signal(int) # -1 = prev, +1 = next navigate = Signal(int) # direction: -1/+1 for left/right, -cols/+cols for up/down
favorite_requested = Signal()
save_requested = Signal()
unsave_requested = Signal()
def __init__(self, grid_cols: int = 3, parent=None) -> None: def __init__(self, grid_cols: int = 3, parent=None) -> None:
super().__init__(parent, Qt.WindowType.Window) super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle("booru-viewer — Fullscreen") self.setWindowTitle("booru-viewer — Fullscreen")
self._grid_cols = grid_cols self._grid_cols = grid_cols
central = QWidget()
main_layout = QVBoxLayout(central)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# Top toolbar
toolbar = QHBoxLayout()
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._save_btn = QPushButton("Save")
self._save_btn.setFixedWidth(60)
self._save_btn.clicked.connect(self.save_requested)
toolbar.addWidget(self._save_btn)
self._unsave_btn = QPushButton("Unsave")
self._unsave_btn.setFixedWidth(70)
self._unsave_btn.clicked.connect(self.unsave_requested)
toolbar.addWidget(self._unsave_btn)
toolbar.addStretch()
self._info_label = QLabel()
toolbar.addWidget(self._info_label)
main_layout.addLayout(toolbar)
# Media stack
self._stack = QStackedWidget() self._stack = QStackedWidget()
self.setCentralWidget(self._stack) main_layout.addWidget(self._stack, stretch=1)
self._viewer = ImageViewer() self._viewer = ImageViewer()
self._viewer.close_requested.connect(self.close) self._viewer.close_requested.connect(self.close)
@ -42,11 +77,14 @@ class FullscreenPreview(QMainWindow):
self._video = VideoPlayer() self._video = VideoPlayer()
self._stack.addWidget(self._video) self._stack.addWidget(self._video)
self.setCentralWidget(central)
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
QApplication.instance().installEventFilter(self) QApplication.instance().installEventFilter(self)
self.showFullScreen() self.showFullScreen()
def set_media(self, path: str, info: str = "") -> None: def set_media(self, path: str, info: str = "") -> None:
self._info_label.setText(info)
ext = Path(path).suffix.lower() ext = Path(path).suffix.lower()
if _is_video(path): if _is_video(path):
self._viewer.clear() self._viewer.clear()
@ -402,6 +440,7 @@ class ImagePreview(QWidget):
open_in_default = Signal() open_in_default = Signal()
open_in_browser = Signal() open_in_browser = Signal()
save_to_folder = Signal(str) save_to_folder = Signal(str)
unsave_requested = Signal()
favorite_requested = Signal() favorite_requested = Signal()
navigate = Signal(int) # -1 = prev, +1 = next navigate = Signal(int) # -1 = prev, +1 = next
fullscreen_requested = Signal() fullscreen_requested = Signal()
@ -511,6 +550,9 @@ class ImagePreview(QWidget):
if self._stack.currentIndex() == 0: if self._stack.currentIndex() == 0:
reset_action = menu.addAction("Reset View") reset_action = menu.addAction("Reset View")
menu.addSeparator()
unsave_action = menu.addAction("Unsave from Library")
slideshow_action = None slideshow_action = None
if self._current_path: if self._current_path:
slideshow_action = menu.addAction("Slideshow Mode") slideshow_action = menu.addAction("Slideshow Mode")
@ -539,6 +581,8 @@ class ImagePreview(QWidget):
elif action == reset_action: elif action == reset_action:
self._image_viewer._fit_to_view() self._image_viewer._fit_to_view()
self._image_viewer.update() self._image_viewer.update()
elif action == unsave_action:
self.unsave_requested.emit()
elif action == slideshow_action: elif action == slideshow_action:
self.fullscreen_requested.emit() self.fullscreen_requested.emit()
elif action == clear_action: elif action == clear_action: