1use crate::store::library_view::Row as LibRow;
2use ratatui::{
3 layout::{Constraint, Rect},
4 style::{Color, Modifier, Style},
5 widgets::{Block, Borders, Row, Table, TableState},
6 Frame,
7};
8
9pub struct LibraryView<'a> {
10 pub rows: &'a [LibRow],
11 pub selected: usize,
12 pub sort_label: &'a str,
13 pub filter_label: &'a str,
14}
15
16impl<'a> LibraryView<'a> {
17 pub fn render(&self, f: &mut Frame, area: Rect) {
18 let header = Row::new(vec!["Title", "Author", "Pages", "Progress", "Left", "Last"])
19 .style(Style::default().add_modifier(Modifier::BOLD));
20 let body: Vec<Row> = self
21 .rows
22 .iter()
23 .map(|r| {
24 let pct = r.progress_pct.unwrap_or(0.0);
25 let bar = render_bar(pct, 6);
26 Row::new(vec![
27 r.title.clone(),
28 r.author.clone().unwrap_or_default(),
29 r.pages.map(|p| p.to_string()).unwrap_or_default(),
30 format!("{bar} {pct:>3.0}%"),
31 format_time_left(r.time_left_s),
32 r.last_read_at.clone().unwrap_or_else(|| "—".into()),
33 ])
34 })
35 .collect();
36
37 let widths = [
38 Constraint::Min(20),
39 Constraint::Length(16),
40 Constraint::Length(6),
41 Constraint::Length(13),
42 Constraint::Length(6),
43 Constraint::Length(10),
44 ];
45 let title = format!(
46 " verso · Library · {} books · {} ",
47 self.rows.len(),
48 reading_count(self.rows)
49 );
50 let block = Block::default().title(title).borders(Borders::ALL);
51 let mut state = TableState::default();
52 state.select(Some(self.selected));
53 let table = Table::new(body, widths)
54 .header(header)
55 .block(block)
56 .highlight_style(Style::default().bg(Color::DarkGray));
57 f.render_stateful_widget(table, area, &mut state);
58 }
59}
60
61fn reading_count(rows: &[LibRow]) -> usize {
62 rows.iter()
63 .filter(|r| r.finished_at.is_none() && r.progress_pct.unwrap_or(0.0) > 0.0)
64 .count()
65}
66
67fn render_bar(pct: f32, width: u16) -> String {
68 let filled = (pct / 100.0 * width as f32).round() as usize;
69 let empty = (width as usize).saturating_sub(filled);
70 "█".repeat(filled) + &"░".repeat(empty)
71}
72
73fn format_time_left(s: Option<u64>) -> String {
74 match s {
75 None => "—".into(),
76 Some(secs) if secs < 3600 => format!("{}m", secs / 60),
77 Some(secs) => format!("{}h", secs / 3600),
78 }
79}