1use ratatui::{
2 layout::{Constraint, Layout},
3 style::{Color, Modifier, Style},
4 text::{Line, Span},
5 widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
6 Frame,
7};
8
9use super::app::{App, Screen};
10
11pub fn render(frame: &mut Frame, app: &mut App) {
12 match app.screen {
13 Screen::ArticleList => render_article_list(frame, app),
14 Screen::ArticleView => render_article_view(frame, app),
15 }
16}
17
18fn render_article_list(frame: &mut Frame, app: &mut App) {
19 let area = frame.area();
20 let chunks = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(area);
21
22 let items: Vec<ListItem> = app
23 .filtered_indices
24 .iter()
25 .filter_map(|&idx| app.store.get(idx))
26 .map(|entry| {
27 let date = entry
28 .published
29 .map(|dt| dt.format("%Y-%m-%d").to_string())
30 .unwrap_or_else(|| " ".to_string());
31
32 if entry.read {
33 let line = Line::from(vec![
34 Span::styled(date, Style::default().fg(Color::DarkGray)),
35 Span::styled(" ", Style::default().fg(Color::DarkGray)),
36 Span::styled(entry.title.clone(), Style::default().fg(Color::DarkGray)),
37 ]);
38 ListItem::new(line)
39 } else {
40 let line = Line::from(vec![
41 Span::styled(date, Style::default().fg(Color::DarkGray)),
42 Span::raw(" "),
43 Span::raw(&entry.title),
44 ]);
45 ListItem::new(line)
46 }
47 })
48 .collect();
49
50 let title = if app.is_showing_read() {
51 " Feed Reader (all) "
52 } else {
53 " Feed Reader (unread) "
54 };
55
56 let list = List::new(items)
57 .block(Block::default().borders(Borders::ALL).title(title))
58 .highlight_style(
59 Style::default()
60 .fg(Color::Yellow)
61 .add_modifier(Modifier::BOLD),
62 )
63 .highlight_symbol("> ");
64
65 app.list_state.select(Some(app.selected));
66 app.layout_areas.main_area = chunks[0];
67 app.layout_areas.status_bar = chunks[1];
68 frame.render_stateful_widget(list, chunks[0], &mut app.list_state);
69
70 let status = if let Some(ref msg) = app.status_message {
71 msg.clone()
72 } else if app.loading {
73 " Loading...".to_string()
74 } else {
75 " Enter: open m: read/unread o: browser a: +read/-read r: refresh Esc: quit"
76 .to_string()
77 };
78 let status_line = Paragraph::new(status).style(Style::default().fg(Color::DarkGray));
79 frame.render_widget(status_line, chunks[1]);
80}
81
82fn render_article_view(frame: &mut Frame, app: &mut App) {
83 let area = frame.area();
84 let chunks = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(area);
85
86 app.layout_areas.main_area = chunks[0];
87 app.layout_areas.status_bar = chunks[1];
88
89 let title = app.article_title.as_deref().unwrap_or("Article");
90 let url = app.article_url.as_deref().unwrap_or("");
91 let content = app.article_content.as_deref().unwrap_or("");
92
93 let mut lines: Vec<Line> = Vec::new();
94 lines.push(Line::from(vec![Span::styled(
95 title,
96 Style::default().add_modifier(Modifier::BOLD),
97 )]));
98 lines.push(Line::from(vec![Span::styled(
99 url,
100 Style::default().fg(Color::DarkGray),
101 )]));
102 lines.push(Line::from(""));
103 for line in content.lines() {
104 lines.push(Line::from(line));
105 }
106
107 let paragraph = Paragraph::new(lines)
108 .block(Block::default().borders(Borders::ALL))
109 .wrap(Wrap { trim: false })
110 .scroll((u16::try_from(app.scroll_offset).unwrap_or(u16::MAX), 0));
111
112 frame.render_widget(paragraph, chunks[0]);
113
114 let read_label = if app.current_article().is_some_and(|a| a.read) {
115 "unread"
116 } else {
117 "read"
118 };
119 let status = format!(" m: {} o: browser Esc: back", read_label);
120 let status_line = Paragraph::new(status).style(Style::default().fg(Color::DarkGray));
121 frame.render_widget(status_line, chunks[1]);
122}