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:
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Watch tab - player plus play queue
|
||||
==================================
|
||||
|
||||
Hosts the embedded mpv PlayerPanel (or the libmpv-missing hint) and a simple
|
||||
play queue. Entries arrive via AppRouter.playVideo / queueVideo.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .ytsage_gui_player import PlayerPanel, create_player_panel
|
||||
from ..utils.ytsage_localization import _
|
||||
from ..utils.ytsage_logger import logger
|
||||
|
||||
|
||||
class WatchPage(QWidget):
|
||||
def __init__(self, router, parent: Optional[QWidget] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._router = router
|
||||
self._queue: List[Dict[str, Any]] = []
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal, self)
|
||||
layout.addWidget(splitter)
|
||||
|
||||
self.player = create_player_panel(self)
|
||||
splitter.addWidget(self.player)
|
||||
|
||||
queue_panel = QWidget(self)
|
||||
queue_layout = QVBoxLayout(queue_panel)
|
||||
queue_layout.setContentsMargins(4, 0, 0, 0)
|
||||
|
||||
queue_header = QHBoxLayout()
|
||||
queue_label = QLabel(_("player.queue"))
|
||||
queue_label.setStyleSheet("font-weight: bold;")
|
||||
queue_header.addWidget(queue_label)
|
||||
queue_header.addStretch()
|
||||
self.clear_queue_btn = QPushButton("✕")
|
||||
self.clear_queue_btn.setFixedWidth(28)
|
||||
self.clear_queue_btn.setToolTip(_("watch.clear_queue"))
|
||||
self.clear_queue_btn.clicked.connect(self.clear_queue)
|
||||
queue_header.addWidget(self.clear_queue_btn)
|
||||
queue_layout.addLayout(queue_header)
|
||||
|
||||
self.queue_list = QListWidget()
|
||||
self.queue_list.setDragDropMode(QListWidget.DragDropMode.InternalMove)
|
||||
self.queue_list.itemDoubleClicked.connect(self._on_queue_item_activated)
|
||||
self.queue_list.model().rowsMoved.connect(self._on_rows_moved)
|
||||
queue_layout.addWidget(self.queue_list)
|
||||
|
||||
splitter.addWidget(queue_panel)
|
||||
splitter.setStretchFactor(0, 4)
|
||||
splitter.setStretchFactor(1, 1)
|
||||
splitter.setSizes([900, 240])
|
||||
|
||||
if isinstance(self.player, PlayerPanel):
|
||||
self.player.playbackEnded.connect(self._on_playback_ended)
|
||||
|
||||
# ------------------------------------------------------------ public API
|
||||
|
||||
def play_entry(self, entry: Dict[str, Any], resume_pos: float = 0.0) -> None:
|
||||
if isinstance(self.player, PlayerPanel):
|
||||
self.player.play(entry, resume_pos=resume_pos)
|
||||
else:
|
||||
logger.warning("Play requested but libmpv is unavailable")
|
||||
|
||||
def enqueue(self, entry: Dict[str, Any]) -> None:
|
||||
self._queue.append(dict(entry))
|
||||
item = QListWidgetItem(entry.get("title") or entry.get("url") or "?")
|
||||
item.setData(Qt.ItemDataRole.UserRole, dict(entry))
|
||||
self.queue_list.addItem(item)
|
||||
# Start playing right away when nothing is on and this is the first item
|
||||
if isinstance(self.player, PlayerPanel) and not self.player.current_entry() and len(self._queue) == 1:
|
||||
self._play_next_from_queue()
|
||||
|
||||
def clear_queue(self) -> None:
|
||||
self._queue.clear()
|
||||
self.queue_list.clear()
|
||||
|
||||
def queue_entries(self) -> List[Dict[str, Any]]:
|
||||
return [self.queue_list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.queue_list.count())]
|
||||
|
||||
# --------------------------------------------------------------- internal
|
||||
|
||||
def _play_next_from_queue(self) -> None:
|
||||
if self.queue_list.count() == 0:
|
||||
return
|
||||
item = self.queue_list.takeItem(0)
|
||||
entry = item.data(Qt.ItemDataRole.UserRole)
|
||||
if entry in self._queue:
|
||||
self._queue.remove(entry)
|
||||
self.play_entry(entry)
|
||||
|
||||
def _on_playback_ended(self, reason: str) -> None:
|
||||
if reason in ("eof", "") and self.queue_list.count() > 0:
|
||||
self._play_next_from_queue()
|
||||
|
||||
def _on_queue_item_activated(self, item: QListWidgetItem) -> None:
|
||||
entry = item.data(Qt.ItemDataRole.UserRole)
|
||||
row = self.queue_list.row(item)
|
||||
self.queue_list.takeItem(row)
|
||||
if entry in self._queue:
|
||||
self._queue.remove(entry)
|
||||
self.play_entry(entry)
|
||||
|
||||
def _on_rows_moved(self, *args) -> None:
|
||||
self._queue = self.queue_entries()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if isinstance(self.player, PlayerPanel):
|
||||
self.player.shutdown()
|
||||
Reference in New Issue
Block a user