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,91 @@
|
||||
"""
|
||||
Search tab - in-app YouTube search via yt-dlp (ytsearchN:)
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from .ytsage_gui_cards import VideoCardGrid
|
||||
from ..core.ytsage_client import YtdlpWorker
|
||||
from ..utils.ytsage_localization import _
|
||||
from ..utils.ytsage_logger import logger
|
||||
|
||||
PAGE_SIZE = 24
|
||||
|
||||
|
||||
class SearchPage(QWidget):
|
||||
def __init__(self, router, parent: Optional[QWidget] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._router = router
|
||||
self._worker: Optional[YtdlpWorker] = None
|
||||
self._query = ""
|
||||
self._offset = 0
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
bar = QHBoxLayout()
|
||||
self.query_input = QLineEdit()
|
||||
self.query_input.setPlaceholderText(_("search.placeholder"))
|
||||
self.query_input.returnPressed.connect(self.start_search)
|
||||
self.query_input.setMinimumHeight(38)
|
||||
bar.addWidget(self.query_input, stretch=1)
|
||||
|
||||
self.search_btn = QPushButton(_("search.button"))
|
||||
self.search_btn.clicked.connect(self.start_search)
|
||||
self.search_btn.setMinimumHeight(38)
|
||||
bar.addWidget(self.search_btn)
|
||||
layout.addLayout(bar)
|
||||
|
||||
self.status_label = QLabel("")
|
||||
self.status_label.setStyleSheet("color: #9aa0a6; padding: 2px;")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
self.grid = VideoCardGrid(router, self)
|
||||
self.grid.loadMoreRequested.connect(self.load_more)
|
||||
layout.addWidget(self.grid, stretch=1)
|
||||
|
||||
# ---------------------------------------------------------------- search
|
||||
|
||||
def start_search(self) -> None:
|
||||
query = self.query_input.text().strip()
|
||||
if not query or self._worker is not None:
|
||||
return
|
||||
self._query = query
|
||||
self._offset = 0
|
||||
self.grid.clear()
|
||||
self._fetch(append=False)
|
||||
|
||||
def load_more(self) -> None:
|
||||
if self._worker is None and self._query:
|
||||
self._offset += PAGE_SIZE
|
||||
self._fetch(append=True)
|
||||
|
||||
def _fetch(self, append: bool) -> None:
|
||||
self.status_label.setText(_("search.searching"))
|
||||
self.search_btn.setEnabled(False)
|
||||
query, offset = self._query, self._offset
|
||||
self._worker = YtdlpWorker(lambda c: c.search(query, n=PAGE_SIZE, offset=offset))
|
||||
self._worker.result.connect(lambda entries: self._on_results(entries, append))
|
||||
self._worker.error.connect(self._on_error)
|
||||
self._worker.finished.connect(self._on_finished)
|
||||
self._worker.start()
|
||||
|
||||
def _on_results(self, entries: List[Dict[str, Any]], append: bool) -> None:
|
||||
show_more = len(entries) >= PAGE_SIZE
|
||||
if append:
|
||||
self.grid.append_entries(entries, show_load_more=show_more)
|
||||
else:
|
||||
self.grid.set_entries(entries, show_load_more=show_more)
|
||||
self.status_label.setText(_("search.results_count", count=self.grid.card_count()))
|
||||
|
||||
def _on_error(self, message: str) -> None:
|
||||
logger.error(f"Search failed: {message}")
|
||||
self.status_label.setText(_("search.failed", error=message[:200]))
|
||||
|
||||
def _on_finished(self) -> None:
|
||||
self.search_btn.setEnabled(True)
|
||||
if self._worker is not None:
|
||||
self._worker.deleteLater()
|
||||
self._worker = None
|
||||
Reference in New Issue
Block a user