"""Popout (fullscreen preview) lifecycle, state sync, and geometry persistence.""" from __future__ import annotations import logging from typing import TYPE_CHECKING if TYPE_CHECKING: from .main_window import BooruApp log = logging.getLogger("booru") # -- Pure functions (tested in tests/gui/test_popout_controller.py) -- def build_video_sync_dict( volume: int, mute: bool, autoplay: bool, loop_state: int, position_ms: int, ) -> dict: """Build the video-state transfer dict used on popout open/close.""" return { "volume": volume, "mute": mute, "autoplay": autoplay, "loop_state": loop_state, "position_ms": position_ms, } # -- Controller -- class PopoutController: """Owns popout lifecycle, state sync, and geometry persistence.""" def __init__(self, app: BooruApp) -> None: self._app = app self._fullscreen_window = None self._popout_active = False self._info_was_visible = False self._right_splitter_sizes: list[int] = [] @property def window(self): return self._fullscreen_window @property def is_active(self) -> bool: return self._popout_active # -- Open -- def open(self) -> None: path = self._app._preview._current_path if not path: return info = self._app._preview._info_label.text() video_pos = 0 if self._app._preview._stack.currentIndex() == 1: video_pos = self._app._preview._video_player.get_position_ms() self._popout_active = True self._info_was_visible = self._app._info_panel.isVisible() self._right_splitter_sizes = self._app._right_splitter.sizes() self._app._preview.clear() self._app._preview.hide() self._app._info_panel.show() self._app._right_splitter.setSizes([0, 0, 1000]) self._app._preview._current_path = path idx = self._app._grid.selected_index if 0 <= idx < len(self._app._posts): self._app._info_panel.set_post(self._app._posts[idx]) from .popout.window import FullscreenPreview saved_geo = self._app._db.get_setting("slideshow_geometry") saved_fs = self._app._db.get_setting_bool("slideshow_fullscreen") if saved_geo: parts = saved_geo.split(",") if len(parts) == 4: from PySide6.QtCore import QRect FullscreenPreview._saved_geometry = QRect(*[int(p) for p in parts]) FullscreenPreview._saved_fullscreen = saved_fs else: FullscreenPreview._saved_geometry = None FullscreenPreview._saved_fullscreen = True else: FullscreenPreview._saved_fullscreen = True cols = self._app._grid._flow.columns show_actions = self._app._stack.currentIndex() != 2 monitor = self._app._db.get_setting("slideshow_monitor") anchor = self._app._db.get_setting("popout_anchor") or "center" self._fullscreen_window = FullscreenPreview(grid_cols=cols, show_actions=show_actions, monitor=monitor, anchor=anchor, parent=self._app) self._fullscreen_window.navigate.connect(self.navigate) self._fullscreen_window.play_next_requested.connect(self._app._on_video_end_next) from ..core.config import library_folders self._fullscreen_window.set_folders_callback(library_folders) self._fullscreen_window.save_to_folder.connect(self._app._post_actions.save_from_preview) self._fullscreen_window.unsave_requested.connect(self._app._post_actions.unsave_from_preview) self._fullscreen_window.toggle_save_requested.connect(self._app._post_actions.toggle_save_from_preview) if show_actions: self._fullscreen_window.bookmark_requested.connect(self._app._post_actions.bookmark_from_preview) self._fullscreen_window.set_bookmark_folders_callback(self._app._db.get_folders) self._fullscreen_window.bookmark_to_folder.connect(self._app._post_actions.bookmark_to_folder_from_preview) self._fullscreen_window.blacklist_tag_requested.connect(self._app._post_actions.blacklist_tag_from_popout) self._fullscreen_window.blacklist_post_requested.connect(self._app._post_actions.blacklist_post_from_popout) self._fullscreen_window.open_in_default.connect(self._app._open_preview_in_default) self._fullscreen_window.open_in_browser.connect(self._app._open_preview_in_browser) self._fullscreen_window.closed.connect(self.on_closed) self._fullscreen_window.privacy_requested.connect(self._app._privacy.toggle) post = self._app._preview._current_post if post: self._fullscreen_window.set_post_tags(post.tag_categories, post.tag_list) pv = self._app._preview._video_player self._fullscreen_window.sync_video_state( volume=pv.volume, mute=pv.is_muted, autoplay=pv.autoplay, loop_state=pv.loop_state, ) if video_pos > 0: self._fullscreen_window.connect_media_ready_once( lambda: self._fullscreen_window.seek_video_to(video_pos) ) pre_w = post.width if post else 0 pre_h = post.height if post else 0 self._fullscreen_window.set_media(path, info, width=pre_w, height=pre_h) self.update_state() # -- Close -- def on_closed(self) -> None: if self._fullscreen_window: from .popout.window import FullscreenPreview fs = FullscreenPreview._saved_fullscreen geo = FullscreenPreview._saved_geometry self._app._db.set_setting("slideshow_fullscreen", "1" if fs else "0") if geo: self._app._db.set_setting("slideshow_geometry", f"{geo.x()},{geo.y()},{geo.width()},{geo.height()}") self._app._preview.show() if not self._info_was_visible: self._app._info_panel.hide() if self._right_splitter_sizes: self._app._right_splitter.setSizes(self._right_splitter_sizes) self._popout_active = False video_pos = 0 if self._fullscreen_window: vstate = self._fullscreen_window.get_video_state() pv = self._app._preview._video_player pv.volume = vstate["volume"] pv.is_muted = vstate["mute"] pv.autoplay = vstate["autoplay"] pv.loop_state = vstate["loop_state"] video_pos = vstate["position_ms"] path = self._app._preview._current_path info = self._app._preview._info_label.text() self._fullscreen_window = None if path: if video_pos > 0: def _seek_preview(): self._app._preview._video_player.seek_to_ms(video_pos) try: self._app._preview._video_player.media_ready.disconnect(_seek_preview) except RuntimeError: pass self._app._preview._video_player.media_ready.connect(_seek_preview) self._app._preview.set_media(path, info) # -- Navigation -- def navigate(self, direction: int) -> None: self._app._navigate_preview(direction) # -- State sync -- def update_media(self, path: str, info: str) -> None: """Sync the popout with new media from browse/bookmark/library.""" if self._fullscreen_window and self._fullscreen_window.isVisible(): self._app._preview._video_player.stop() cp = self._app._preview._current_post w = cp.width if cp else 0 h = cp.height if cp else 0 self._fullscreen_window.set_media(path, info, width=w, height=h) show_full = self._app._stack.currentIndex() != 2 self._fullscreen_window.set_toolbar_visibility( bookmark=show_full, save=True, bl_tag=show_full, bl_post=show_full, ) self.update_state() def update_state(self) -> None: """Update popout button states by mirroring the embedded preview.""" if not self._fullscreen_window: return self._fullscreen_window.update_state( self._app._preview._is_bookmarked, self._app._preview._is_saved, ) post = self._app._preview._current_post if post is not None: self._fullscreen_window.set_post_tags( post.tag_categories or {}, post.tag_list )