Files
momentry_core/src/player/selector.rs
accusys 383201cacd feat: Initial v0.9 release with API Key authentication
## v0.9.20260325_144654

### Features
- API Key Authentication System
- Job Worker System
- V2 Backup Versioning

### Bug Fixes
- get_processor_results_by_job column mapping

Co-authored-by: OpenCode
2026-03-25 14:53:41 +08:00

164 lines
5.0 KiB
Rust

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<PathBuf>,
}
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<VideoEntry>,
selected_index: usize,
}
impl VideoSelector {
pub fn new(videos: Vec<VideoEntry>) -> Self {
Self {
videos,
selected_index: 0,
}
}
pub fn run(&mut self) -> Result<Option<VideoEntry>> {
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<ListItem> = 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]);
}
}