youtube-tui/screens/modal.py

254 lines
7.7 KiB
Python

#!/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()