diff --git a/booru_viewer/gui/app.py b/booru_viewer/gui/app.py index 510caa7..773420d 100644 --- a/booru_viewer/gui/app.py +++ b/booru_viewer/gui/app.py @@ -217,7 +217,15 @@ class BooruApp(QMainWindow): self._status.showMessage(f"Error: {e}") 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: central = QWidget() @@ -303,6 +311,7 @@ class BooruApp(QMainWindow): self._preview.open_in_browser.connect(self._open_preview_in_browser) self._preview.favorite_requested.connect(self._favorite_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.fullscreen_requested.connect(self._open_fullscreen_preview) self._preview.set_folders_callback(self._db.get_folders) @@ -841,6 +850,26 @@ class BooruApp(QMainWindow): self._db.add_folder(folder) 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: path = self._preview._current_path if not path: @@ -851,6 +880,9 @@ 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.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.set_media(path, self._preview._info_label.text()) @@ -893,6 +925,7 @@ class BooruApp(QMainWindow): save_lib_menu.addSeparator() save_lib_new = save_lib_menu.addAction("+ New Folder...") + unsave_lib = menu.addAction("Unsave from Library") copy_url = menu.addAction("Copy Image URL") copy_tags = menu.addAction("Copy Tags") menu.addSeparator() @@ -922,6 +955,22 @@ class BooruApp(QMainWindow): self._save_to_library(post, name.strip()) elif id(action) in save_lib_folders: 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: QApplication.clipboard().setText(post.file_url) self._status.showMessage("URL copied") @@ -1376,6 +1425,7 @@ class BooruApp(QMainWindow): def closeEvent(self, event) -> None: 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"): from ..core.cache import clear_cache clear_cache(clear_images=True, clear_thumbnails=True) diff --git a/booru_viewer/gui/preview.py b/booru_viewer/gui/preview.py index e38f0ab..7287acf 100644 --- a/booru_viewer/gui/preview.py +++ b/booru_viewer/gui/preview.py @@ -25,15 +25,50 @@ def _is_video(path: str) -> bool: class FullscreenPreview(QMainWindow): """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: super().__init__(parent, Qt.WindowType.Window) self.setWindowTitle("booru-viewer — Fullscreen") 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.setCentralWidget(self._stack) + main_layout.addWidget(self._stack, stretch=1) self._viewer = ImageViewer() self._viewer.close_requested.connect(self.close) @@ -42,11 +77,14 @@ class FullscreenPreview(QMainWindow): self._video = VideoPlayer() self._stack.addWidget(self._video) + self.setCentralWidget(central) + from PySide6.QtWidgets import QApplication QApplication.instance().installEventFilter(self) self.showFullScreen() def set_media(self, path: str, info: str = "") -> None: + self._info_label.setText(info) ext = Path(path).suffix.lower() if _is_video(path): self._viewer.clear() @@ -402,6 +440,7 @@ class ImagePreview(QWidget): open_in_default = Signal() open_in_browser = Signal() save_to_folder = Signal(str) + unsave_requested = Signal() favorite_requested = Signal() navigate = Signal(int) # -1 = prev, +1 = next fullscreen_requested = Signal() @@ -511,6 +550,9 @@ class ImagePreview(QWidget): if self._stack.currentIndex() == 0: reset_action = menu.addAction("Reset View") + menu.addSeparator() + unsave_action = menu.addAction("Unsave from Library") + slideshow_action = None if self._current_path: slideshow_action = menu.addAction("Slideshow Mode") @@ -539,6 +581,8 @@ class ImagePreview(QWidget): elif action == reset_action: self._image_viewer._fit_to_view() self._image_viewer.update() + elif action == unsave_action: + self.unsave_requested.emit() elif action == slideshow_action: self.fullscreen_requested.emit() elif action == clear_action: