use anyhow::Result; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, style::{Color, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, Paragraph}, Frame, Terminal, }; use std::io; use std::path::PathBuf; #[derive(Debug, Clone)] pub struct VideoEntry { pub uuid: String, pub file_name: String, pub file_path: String, pub duration: f64, pub width: u32, pub height: u32, pub thumbnail_dir: Option, } impl VideoEntry { pub fn format_duration(&self) -> String { let secs = self.duration as u64; let hours = secs / 3600; let mins = (secs % 3600) / 60; let secs = secs % 60; if hours > 0 { format!("{}:{:02}:{:02}", hours, mins, secs) } else { format!("{}:{:02}", mins, secs) } } pub fn format_resolution(&self) -> String { format!("{}x{}", self.width, self.height) } } pub struct VideoSelector { videos: Vec, selected_index: usize, } impl VideoSelector { pub fn new(videos: Vec) -> Self { Self { videos, selected_index: 0, } } pub fn run(&mut self) -> Result> { let stdout = io::stdout(); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; loop { terminal.draw(|f| self.render(f))?; match crossterm::event::read() { Ok(crossterm::event::Event::Key(key)) => match key.code { crossterm::event::KeyCode::Up => { if self.selected_index > 0 { self.selected_index -= 1; } } crossterm::event::KeyCode::Down => { if self.selected_index < self.videos.len() - 1 { self.selected_index += 1; } } crossterm::event::KeyCode::Enter => { let selected = self.videos.get(self.selected_index).cloned(); terminal.show_cursor()?; return Ok(selected); } crossterm::event::KeyCode::Char('q') | crossterm::event::KeyCode::Esc => { terminal.show_cursor()?; return Ok(None); } _ => {} }, Ok(crossterm::event::Event::Resize(_, _)) => {} _ => {} } } } fn render(&self, f: &mut Frame) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ]) .split(f.area()); // Title let title = Paragraph::new("🎬 Video Selector") .style(Style::default().fg(Color::Cyan)) .block( Block::default() .borders(Borders::ALL) .title(" Select Video "), ); f.render_widget(title, chunks[0]); // Video list let items: Vec = self .videos .iter() .enumerate() .map(|(i, video)| { let style = if i == self.selected_index { Style::default().fg(Color::Yellow).bg(Color::DarkGray) } else { Style::default() }; let duration = video.format_duration(); let resolution = video.format_resolution(); let thumb_info = if video.thumbnail_dir.is_some() { "📷" } else { "" }; let content = Line::from(vec![ Span::raw(format!( "{} ", if i == self.selected_index { "▶" } else { " " } )), Span::raw(&video.file_name), Span::raw(" "), Span::styled( format!("{} | {}", duration, resolution), Style::default().fg(Color::Blue), ), Span::raw(" "), Span::raw(thumb_info), ]); ListItem::new(content).style(style) }) .collect(); let list = List::new(items) .block(Block::default().borders(Borders::ALL).title(" Videos ")) .highlight_style(Style::default().fg(Color::Yellow)); f.render_widget(list, chunks[1]); // Help text let help = Paragraph::new(" [↑/↓] Navigate [Enter] Select [q] Quit ") .style(Style::default().fg(Color::DarkGray)) .block(Block::default().borders(Borders::ALL)); f.render_widget(help, chunks[2]); } }