Skip to main content

verso/ui/
library_view.rs

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}