- New TUI using Textual framework with responsive interface - Search, results, download, and queue screens for YouTube video management - Download queue system with sequential background downloads - Status bar with queue count and download progress indicators - Comprehensive test suite (97 tests) with 100% pass rate - Modern Python packaging with pyproject.toml and uv support - Added requirements-tui.txt for TUI-specific dependencies - Updated setup.py and install.sh for TUI integration - Enhanced README.md with TUI usage documentation
157 lines
4.2 KiB
Python
157 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Search Screen for YouTube TUI
|
|
"""
|
|
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Container
|
|
from textual.screen import Screen
|
|
from textual.widgets import (
|
|
Button,
|
|
Footer,
|
|
Header,
|
|
Input,
|
|
Static,
|
|
)
|
|
|
|
from youtube_tui.services.youtube import YouTubeService
|
|
|
|
|
|
class SearchScreen(Screen):
|
|
"""Screen for searching YouTube videos"""
|
|
|
|
CSS = """
|
|
SearchScreen {
|
|
align: center middle;
|
|
}
|
|
|
|
#search-container {
|
|
width: 80%;
|
|
height: auto;
|
|
border: double #555555;
|
|
padding: 1 2;
|
|
margin: 2 0;
|
|
}
|
|
|
|
#search-input {
|
|
width: 100%;
|
|
margin: 1 0;
|
|
}
|
|
|
|
#buttons {
|
|
width: 100%;
|
|
height: auto;
|
|
dock: bottom;
|
|
}
|
|
|
|
Button {
|
|
width: 20;
|
|
margin: 1 1;
|
|
}
|
|
|
|
#status-bar {
|
|
dock: bottom;
|
|
height: 1;
|
|
background: #333333;
|
|
color: #aaaaaa;
|
|
padding: 0 1;
|
|
}
|
|
|
|
#loading-indicator {
|
|
height: 3;
|
|
margin: 1 0;
|
|
}
|
|
"""
|
|
|
|
BINDINGS = [
|
|
("escape", "go_back", "Back"),
|
|
("enter", "search", "Search"),
|
|
("ctrl+f", "search_from_anywhere", "Search"),
|
|
]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.youtube_service = YouTubeService()
|
|
self.search_term = ""
|
|
self.is_searching = False
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Compose the search screen"""
|
|
yield Header()
|
|
yield Container(
|
|
Static("YouTube Search", id="search-title", classes="title"),
|
|
Static("Enter a search term to find YouTube videos", id="search-hint"),
|
|
Input(placeholder="Search for videos...", id="search-input"),
|
|
Button("Search", id="search-button"),
|
|
Button("Cancel", id="cancel-button"),
|
|
id="search-container",
|
|
)
|
|
yield Static(id="status-bar")
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
"""Called when screen is mounted"""
|
|
self.query_one(Input).focus()
|
|
self.update_status(
|
|
"Ready to search - Press Enter to search, Ctrl+F to search from anywhere"
|
|
)
|
|
|
|
def action_search_from_anywhere(self) -> None:
|
|
"""Open search from anywhere"""
|
|
# This is a fallback for the key binding
|
|
self.action_search()
|
|
|
|
def update_status(self, message: str) -> None:
|
|
"""Update the status bar message"""
|
|
status_bar = self.query_one("#status-bar", Static)
|
|
status_bar.update(f"[bold white]{message}[/bold white]")
|
|
|
|
def action_search(self) -> None:
|
|
"""Perform the search operation"""
|
|
input_widget = self.query_one(Input)
|
|
search_term = input_widget.value.strip()
|
|
|
|
if not search_term:
|
|
self.update_status("[red]Please enter a search term[/red]")
|
|
return
|
|
|
|
self.search_term = search_term
|
|
self.update_status(f"[blue]Searching for: {search_term}[/blue]")
|
|
|
|
# Trigger search - get the app and push results screen
|
|
app = self.app
|
|
if hasattr(app, "push_results_screen"):
|
|
app.push_results_screen(search_term)
|
|
else:
|
|
# Fallback if app methods aren't available
|
|
from youtube_tui.screens.results import ResultsScreen
|
|
|
|
self.app.push_screen(ResultsScreen(search_term))
|
|
|
|
def action_cancel(self) -> None:
|
|
"""Handle cancel action"""
|
|
self.action_go_back()
|
|
|
|
def action_go_back(self) -> None:
|
|
"""Go back to previous screen"""
|
|
self.app.pop_screen()
|
|
|
|
def action_quit(self) -> None:
|
|
"""Quit the application"""
|
|
# Only quit if we're at the root level
|
|
if len(self.app.screen_stack) <= 2: # Header + Footer + Screen
|
|
self.app.exit()
|
|
else:
|
|
self.action_go_back()
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
"""Handle button presses"""
|
|
if event.button.id == "search-button":
|
|
self.action_search()
|
|
elif event.button.id == "cancel-button":
|
|
self.action_quit()
|
|
|
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
"""Handle enter key in search input"""
|
|
self.action_search()
|