## 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
164 lines
5.0 KiB
Rust
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]);
|
|
}
|
|
}
|