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:
parent
afa08ff007
commit
4675c0a691
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user