use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, }; use super::{format_date, format_duration, format_size, media_type_color}; use crate::app::AppState; pub fn render(f: &mut Frame, state: &AppState, area: Rect) { let Some(item) = &state.selected_media else { let msg = Paragraph::new("No item selected") .block(Block::default().borders(Borders::ALL).title(" Detail ")); f.render_widget(msg, area); return; }; let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(0)]) .split(area); let label_style = Style::default() .fg(Color::Yellow) .add_modifier(Modifier::BOLD); let value_style = Style::default().fg(Color::White); let dim_style = Style::default().fg(Color::DarkGray); let pad = " "; let label_width = 14; let make_label = |name: &str| -> String { format!("{name: = Vec::new(); // Section: File Info lines.push(Line::from(Span::styled( "--- File Info ---", Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ))); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Name"), label_style), Span::styled(&item.file_name, value_style), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Path"), label_style), Span::styled(&item.path, dim_style), ])); let type_color = media_type_color(&item.media_type); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Type"), label_style), Span::styled(&item.media_type, Style::default().fg(type_color)), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Size"), label_style), Span::styled(format_size(item.file_size), value_style), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Hash"), label_style), Span::styled(&item.content_hash, dim_style), ])); if item.has_thumbnail { lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Thumbnail"), label_style), Span::styled("Yes", Style::default().fg(Color::Green)), ])); } lines.push(Line::default()); // blank line // Section: Metadata lines.push(Line::from(Span::styled( "--- Metadata ---", Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ))); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Title"), label_style), Span::styled(item.title.as_deref().unwrap_or("-"), value_style), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Artist"), label_style), Span::styled(item.artist.as_deref().unwrap_or("-"), value_style), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Album"), label_style), Span::styled(item.album.as_deref().unwrap_or("-"), value_style), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Genre"), label_style), Span::styled(item.genre.as_deref().unwrap_or("-"), value_style), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Year"), label_style), Span::styled( item.year.map_or_else(|| "-".to_string(), |y| y.to_string()), value_style, ), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Duration"), label_style), Span::styled( item .duration_secs .map_or_else(|| "-".to_string(), format_duration), value_style, ), ])); // Description if let Some(ref desc) = item.description && !desc.is_empty() { lines.push(Line::default()); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Description"), label_style), Span::styled(desc.as_str(), value_style), ])); } // Custom fields if !item.custom_fields.is_empty() { lines.push(Line::default()); lines.push(Line::from(Span::styled( "--- Custom Fields ---", Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ))); let mut fields: Vec<_> = item.custom_fields.iter().collect(); fields.sort_by_key(|(k, _)| k.as_str()); for (key, field) in fields { lines.push(Line::from(vec![ Span::raw(pad), Span::styled(format!("{key: = state.tags.iter().map(|t| t.name.as_str()).collect(); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(tag_names.join(", "), Style::default().fg(Color::Green)), ])); } // Book metadata section if let Some(ref book) = state.book_metadata { lines.push(Line::default()); lines.push(Line::from(Span::styled( "--- Book Metadata ---", Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ))); if let Some(ref subtitle) = book.subtitle { lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Subtitle"), label_style), Span::styled(subtitle.as_str(), value_style), ])); } if !book.authors.is_empty() { let authors: Vec<&str> = book.authors.iter().map(|a| a.name.as_str()).collect(); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Authors"), label_style), Span::styled(authors.join(", "), value_style), ])); } if let Some(ref publisher) = book.publisher { lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Publisher"), label_style), Span::styled(publisher.as_str(), value_style), ])); } if let Some(isbn) = book.isbn13.as_ref().or(book.isbn.as_ref()) { lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("ISBN"), label_style), Span::styled(isbn.as_str(), value_style), ])); } if let Some(ref language) = book.language { lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Language"), label_style), Span::styled(language.as_str(), value_style), ])); } if let Some(pages) = book.page_count { lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Pages"), label_style), Span::styled(pages.to_string(), value_style), ])); } if let Some(ref series) = book.series { let series_display = book .series_index .map_or_else(|| series.clone(), |idx| format!("{series} #{idx}")); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Series"), label_style), Span::styled(series_display, value_style), ])); } } // Reading progress section if let Some(ref progress) = state.reading_progress { lines.push(Line::default()); lines.push(Line::from(Span::styled( "--- Reading Progress ---", Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ))); let page_display = progress.total_pages.map_or_else( || format!("Page {}", progress.current_page), |total| format!("Page {} / {total}", progress.current_page), ); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Progress"), label_style), Span::styled(page_display, value_style), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Status"), label_style), Span::styled(&progress.status, value_style), ])); } lines.push(Line::default()); // Section: Timestamps lines.push(Line::from(Span::styled( "--- Timestamps ---", Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ))); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Created"), label_style), Span::styled(format_date(&item.created_at), dim_style), ])); lines.push(Line::from(vec![ Span::raw(pad), Span::styled(make_label("Updated"), label_style), Span::styled(format_date(&item.updated_at), dim_style), ])); let title = item.title.as_ref().map_or_else( || format!(" Detail: {} ", item.file_name), |title_str| format!(" Detail: {title_str} "), ); let detail = Paragraph::new(lines) .block(Block::default().borders(Borders::ALL).title(title)); f.render_widget(detail, chunks[0]); }