#!/usr/bin/env python3 """ Category Selection Modal for YouTube TUI """ import logging from typing import Optional from textual.app import ComposeResult from textual.containers import Container, Vertical from textual.screen import ModalScreen from textual.widgets import ( Button, Footer, Header, Input, ListItem, ListView, Static, ) from youtube_tui.services.youtube import YouTubeService logger = logging.getLogger(__name__) class CategorySelectionModal(ModalScreen): """Modal for selecting a download category""" CSS = """ CategorySelectionModal { align: center middle; } #modal-container { width: 60%; height: auto; border: solid #555555; background: $surface; padding: 1; } #modal-title { width: 100%; height: 3; dock: top; background: $primary; content-align: center middle; color: $text; } #categories-container { width: 100%; height: 20; margin: 1 0; } #custom-input { width: 100%; margin: 1 0; } #modal-actions { width: 100%; height: auto; dock: bottom; margin-top: 1; } Button { width: 15; margin: 1 1; } ListItem { height: 3; padding: 0 1; } ListItem:hover { background: $primary-darken-2; } ListItem.--highlight { background: $primary; } """ BINDINGS = [ ("escape", "close_modal", "Cancel"), ("enter", "select_category", "Select"), ("up", "cursor_up", "Cursor Up"), ("down", "cursor_down", "Cursor Down"), ] def __init__(self) -> None: super().__init__() self.youtube_service = YouTubeService() self.selected_category: Optional[str] = None self.selected_index = 0 def compose(self) -> ComposeResult: """Compose the modal""" yield Header() yield Container( Static("Select Download Category", id="modal-title"), Vertical( Static("Available Categories:", id="categories-label"), ListView(id="categories-list"), Static("Or type custom folder name:", id="custom-label"), Input(placeholder="Enter custom folder name...", id="custom-input"), id="categories-container", ), Container( Button("Select", id="select-btn"), Button("Cancel", id="cancel-btn"), id="modal-actions", ), id="modal-container", ) yield Footer() def on_mount(self) -> None: """Called when modal is mounted""" self.load_categories() self.update_status("Use arrow keys to select, Enter to confirm") def load_categories(self) -> None: """Load available categories into the list""" list_view = self.query_one("#categories-list", ListView) list_view.clear() try: # Note: This is called from on_mount which is sync # In a real async context, this should be awaited categories: list = self.youtube_service.cli.get_categories( self.youtube_service.cli.config ) logger.debug(f"load_categories: categories={categories}") for category in categories: # Extract folder name for display from pathlib import Path folder_name = Path(category).name if Path(category).name else "Root" category_id = Path(category).name.replace(" ", "-").replace("/", "-") logger.debug( f"load_categories: category={category}, folder_name={folder_name}, category_id={category_id}" ) # Create a custom widget for the item item = ListItem( Static(f" {folder_name}"), id=f"category-{category_id}" ) list_view.append(item) # Highlight first item if list_view.children: list_view.children[0].add_class("--highlight") self.selected_index = 0 logger.debug( f"load_categories: first item highlighted, selected_index={self.selected_index}" ) except Exception as e: list_view.append( ListItem(Static(f"[red]Error loading categories: {e}[/red]")) ) def update_status(self, message: str) -> None: """Update the modal status""" # We could add a status line if needed pass def action_select_category(self) -> None: """Select the current category""" list_view = self.query_one("#categories-list", ListView) # Debug logging logger.debug( f"action_select_category: selected_index={self.selected_index}, children_count={len(list_view.children)}" ) if list_view.children and 0 <= self.selected_index < len(list_view.children): # Get the selected item item = list_view.children[self.selected_index] category_id = item.id logger.debug(f"action_select_category: item.id={category_id}") if category_id and category_id.startswith("category-"): self.selected_category = category_id.replace("category-", "") logger.debug( f"action_select_category: selected_category={self.selected_category}" ) self.dismiss(self.selected_category) return # Check custom input custom_input = self.query_one("#custom-input", Input) custom_name = custom_input.value.strip() if custom_name: self.selected_category = custom_name self.dismiss(self.selected_category) return # No valid selection self.update_status("[red]Please select a category or enter a custom name[/red]") def action_cursor_up(self) -> None: """Move cursor up""" list_view = self.query_one("#categories-list", ListView) if list_view.children: # Remove highlight from current item if 0 <= self.selected_index < len(list_view.children): list_view.children[self.selected_index].remove_class("--highlight") # Move up self.selected_index = max(0, self.selected_index - 1) # Highlight new item list_view.children[self.selected_index].add_class("--highlight") def action_cursor_down(self) -> None: """Move cursor down""" list_view = self.query_one("#categories-list", ListView) if list_view.children: # Remove highlight from current item if 0 <= self.selected_index < len(list_view.children): list_view.children[self.selected_index].remove_class("--highlight") # Move down self.selected_index = min( len(list_view.children) - 1, self.selected_index + 1 ) # Highlight new item list_view.children[self.selected_index].add_class("--highlight") def action_close_modal(self) -> None: """Close modal without selecting""" self.selected_category = None self.dismiss(None) def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses""" if event.button.id == "select-btn": self.action_select_category() elif event.button.id == "cancel-btn": self.action_close_modal() def on_input_submitted(self, event: Input.Submitted) -> None: """Handle enter key in custom input""" self.action_select_category()