Restructure main window into a watch-first tab shell

The single-page downloader layout becomes the Downloads tab of a
SmoothTabWidget with Watch / Search / Feed / Browse / Downloads pages.
The init_ui edit is deliberately small (the old central widget is now
self.download_page); all new behavior lives in new modules:

- ytsage_gui_router.py: AppRouter signal hub (playVideo, queueVideo,
  downloadVideo, openChannel, openPlaylist). A card's Download button
  deep-links into the Downloads tab with the URL prefilled and analysis
  started automatically.
- ytsage_gui_cards.py: VideoCard (thumbnail with disk cache under
  APP_THUMBNAILS_DIR, title/channel/duration, Play/Queue/Download
  actions, double-click to play) and VideoCardGrid (responsive grid,
  Load more pagination).
- ytsage_gui_watch.py: WatchPage hosting the mpv PlayerPanel and a
  drag-reorderable play queue with auto-advance on end of file.
- ytsage_gui_search.py: SearchPage running YtdlpClient.search off the
  GUI thread with Load more pagination.
- Browse and Feed pages are placeholders, implemented next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-25 01:48:39 +02:00
parent f886125857
commit 16be4b4afa
8 changed files with 641 additions and 2 deletions
+59 -2
View File
@@ -53,6 +53,7 @@ from .ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.
from .ytsage_gui_format_table import FormatTableMixin
from .ytsage_gui_video_info import VideoInfoMixin
from .ytsage_gui_analysis import AnalysisMixin
from .ytsage_smooth_tab_widget import SmoothTabWidget
from ..utils.ytsage_constants import (
ICON_PATH,
SOUND_PATH,
@@ -381,9 +382,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
self.setWindowTitle(f"{_('app.title')} {_('app.version', version=self.version)}")
self.setMinimumSize(900, 750)
# Main widget and layout
# Downloads page keeps the original single-page layout; the central
# widget becomes a tab shell built at the end of init_ui
main_widget = QWidget()
self.setCentralWidget(main_widget)
self.download_page = main_widget
layout = QVBoxLayout(main_widget)
layout.setSpacing(8)
layout.setContentsMargins(20, 20, 20, 20)
@@ -626,6 +628,61 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
# Disable analysis-dependent controls until video is analyzed
self.toggle_analysis_dependent_controls(enabled=False)
self._setup_main_tabs()
def _setup_main_tabs(self) -> None:
"""Wrap the pages in the watch-first tab shell (SageTube)."""
from .ytsage_gui_browse import BrowsePage
from .ytsage_gui_feed import FeedPage
from .ytsage_gui_player import PlayerPanel, create_player_panel
from .ytsage_gui_router import AppRouter
from .ytsage_gui_search import SearchPage
from .ytsage_gui_watch import WatchPage
self.router = AppRouter(self)
self.watch_page = WatchPage(self.router, self)
self.search_page = SearchPage(self.router, self)
self.feed_page = FeedPage(self.router, self)
self.browse_page = BrowsePage(self.router, self)
self.main_tabs = SmoothTabWidget(self)
self.main_tabs.addTab(self.watch_page, _("main_tabs.watch"))
self.main_tabs.addTab(self.search_page, _("main_tabs.search"))
self.main_tabs.addTab(self.feed_page, _("main_tabs.feed"))
self.main_tabs.addTab(self.browse_page, _("main_tabs.browse"))
self.main_tabs.addTab(self.download_page, _("main_tabs.downloads"))
self.setCentralWidget(self.main_tabs)
self.router.playVideo.connect(self._route_play_video)
self.router.queueVideo.connect(self.watch_page.enqueue)
self.router.downloadVideo.connect(self._route_download_video)
self.router.openChannel.connect(self._route_open_channel)
self.router.openPlaylist.connect(self._route_open_playlist)
def _tab_index_of(self, page) -> int:
for i in range(self.main_tabs.stack.count()):
if self.main_tabs.stack.widget(i) is page:
return i
return 0
def _route_play_video(self, entry: dict) -> None:
self.main_tabs.set_current_index(self._tab_index_of(self.watch_page))
self.watch_page.play_entry(entry)
def _route_download_video(self, url: str) -> None:
self.main_tabs.set_current_index(self._tab_index_of(self.download_page))
self.url_input.setText(url)
self.analyze_url()
def _route_open_channel(self, url: str) -> None:
self.main_tabs.set_current_index(self._tab_index_of(self.browse_page))
self.browse_page.open_url(url)
def _route_open_playlist(self, url: str) -> None:
self.main_tabs.set_current_index(self._tab_index_of(self.browse_page))
self.browse_page.open_url(url)
def _on_url_text_changed(self, text: str) -> None:
"""Enable or disable the Analyze button based on URL input content."""
self.analyze_button.setEnabled(bool(text.strip()))