344 lines
10 KiB
Rust
344 lines
10 KiB
Rust
use crate::{
|
|
app::{ActivePane, ActiveTab, App, StatusMessage},
|
|
snowballing::Publication,
|
|
};
|
|
use ratatui::{
|
|
Frame,
|
|
layout::{Constraint, Direction, Layout, Rect},
|
|
style::{Color, Modifier, Style, Stylize},
|
|
text::{Line, Span},
|
|
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
|
|
};
|
|
|
|
pub fn draw(f: &mut Frame, app: &mut App) {
|
|
match app.active_tab {
|
|
ActiveTab::Seeding => draw_seeding_tab(f, app),
|
|
ActiveTab::Snowballing => draw_snowballing_tab(f, app),
|
|
}
|
|
}
|
|
|
|
fn draw_seeding_tab(f: &mut Frame, app: &mut App) {
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([
|
|
Constraint::Min(3),
|
|
Constraint::Length(3),
|
|
Constraint::Length(1),
|
|
])
|
|
.split(f.area());
|
|
|
|
// Included publication list
|
|
|
|
let items = create_publication_item_list(
|
|
&app.included_publications,
|
|
None,
|
|
chunks[0].width.saturating_sub(4) as usize,
|
|
false,
|
|
);
|
|
|
|
let list = List::new(items).block(
|
|
Block::default()
|
|
.title_top(Line::from("Included Publications").centered())
|
|
.title_top(
|
|
Line::from(Span::styled(
|
|
format!("{} entries", app.included_publications.len()),
|
|
Style::default().fg(Color::Yellow),
|
|
))
|
|
.right_aligned(),
|
|
)
|
|
.borders(Borders::ALL),
|
|
);
|
|
|
|
f.render_stateful_widget(
|
|
list,
|
|
chunks[0],
|
|
&mut ListState::default().with_selected(
|
|
match app.included_publications.len() {
|
|
0 => None,
|
|
_ => Some(app.included_publications.len() - 1),
|
|
},
|
|
),
|
|
);
|
|
|
|
// Text entry
|
|
|
|
let input = Paragraph::new(app.seeding_input.as_str())
|
|
.block(Block::bordered().title("Input"));
|
|
|
|
f.render_widget(input, chunks[1]);
|
|
|
|
// Status line
|
|
|
|
draw_status_line(f, app, chunks[2]);
|
|
}
|
|
|
|
fn draw_snowballing_tab(f: &mut Frame, app: &mut App) {
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([Constraint::Min(2), Constraint::Length(1)])
|
|
.split(f.area());
|
|
|
|
let content_chunks = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([Constraint::Percentage(25), Constraint::Percentage(75)])
|
|
.split(chunks[0]);
|
|
|
|
draw_left_pane(f, app, content_chunks[0]);
|
|
draw_right_pane(f, app, content_chunks[1]);
|
|
draw_status_line(f, app, chunks[1]);
|
|
}
|
|
|
|
fn format_title<'a>(
|
|
idx: usize,
|
|
title: &Option<String>,
|
|
year: &Option<u32>,
|
|
available_width: usize,
|
|
is_selected: bool,
|
|
) -> Vec<Line<'a>> {
|
|
let idx_text = format!("[{}]", idx + 1);
|
|
let title_text =
|
|
title.clone().unwrap_or("[No title available]".to_string());
|
|
let year_text = year
|
|
.map(|val| format!("({})", val))
|
|
.unwrap_or("[unknown]".to_string());
|
|
|
|
let wrapped = textwrap::fill(
|
|
&format!("{} {} {}", idx_text, title_text, year_text),
|
|
available_width,
|
|
);
|
|
|
|
let lines: Vec<&str> = wrapped.lines().collect();
|
|
|
|
let mut result = Vec::<Line>::new();
|
|
|
|
let (border_char, border_style) = if is_selected {
|
|
(
|
|
"┃ ",
|
|
Style::default()
|
|
.fg(Color::Gray)
|
|
.add_modifier(Modifier::BOLD),
|
|
)
|
|
} else {
|
|
(" ", Style::default())
|
|
};
|
|
|
|
if lines.len() == 1 {
|
|
let line = lines[0];
|
|
|
|
result.push(Line::from(vec![
|
|
Span::styled(border_char, border_style),
|
|
Span::styled(
|
|
line[0..idx_text.len()].to_string(),
|
|
Style::default().fg(Color::DarkGray),
|
|
),
|
|
Span::raw(
|
|
line[idx_text.len()..line.len() - year_text.len()].to_string(),
|
|
),
|
|
Span::styled(
|
|
line[line.len() - year_text.len()..].to_string(),
|
|
Style::default().fg(Color::Yellow),
|
|
),
|
|
]));
|
|
} else {
|
|
let first_line = lines[0];
|
|
let middle_lines = &lines[1..lines.len() - 1];
|
|
let last_line = lines[lines.len() - 1];
|
|
|
|
result.push(Line::from(vec![
|
|
Span::styled(border_char, border_style),
|
|
Span::styled(
|
|
first_line[0..idx_text.len()].to_string(),
|
|
Style::default().fg(Color::DarkGray),
|
|
),
|
|
Span::raw(first_line[idx_text.len()..].to_string()),
|
|
]));
|
|
for line in middle_lines {
|
|
result.push(Line::from(vec![
|
|
Span::styled(border_char, border_style),
|
|
Span::raw(line.to_string()),
|
|
]));
|
|
}
|
|
result.push(Line::from(vec![
|
|
Span::styled(border_char, border_style),
|
|
Span::raw(
|
|
last_line[..last_line.len() - year_text.len()].to_string(),
|
|
),
|
|
Span::styled(
|
|
last_line[last_line.len() - year_text.len()..].to_string(),
|
|
Style::default().fg(Color::Yellow),
|
|
),
|
|
]));
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
fn create_publication_item_list(
|
|
publications: &Vec<Publication>,
|
|
selected_idx: Option<usize>,
|
|
available_width: usize,
|
|
show_abstract: bool,
|
|
) -> Vec<ListItem<'_>> {
|
|
publications
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, publ)| {
|
|
let is_selected = Some(idx) == selected_idx;
|
|
|
|
let (border_char, border_style) = if is_selected {
|
|
(
|
|
"┃ ",
|
|
Style::default()
|
|
.fg(Color::Gray)
|
|
.add_modifier(Modifier::BOLD),
|
|
)
|
|
} else {
|
|
(" ", Style::default())
|
|
};
|
|
|
|
let mut lines = Vec::<Line>::new();
|
|
|
|
lines.append(&mut format_title(
|
|
idx,
|
|
&publ.get_title(),
|
|
&publ.get_year(),
|
|
available_width,
|
|
is_selected,
|
|
));
|
|
|
|
lines.push(Line::from(vec![
|
|
Span::styled(border_char, border_style),
|
|
Span::styled(
|
|
publ.get_author_text(),
|
|
Style::default().fg(Color::Blue),
|
|
),
|
|
]));
|
|
|
|
if show_abstract {
|
|
let wrapped = textwrap::fill(
|
|
&publ
|
|
.get_abstract()
|
|
.unwrap_or("[No abstract available]".to_string()),
|
|
available_width.saturating_sub(2),
|
|
);
|
|
for wrapped_line in wrapped.lines() {
|
|
lines.push(Line::from(vec![
|
|
Span::styled(border_char, border_style),
|
|
Span::raw(format!(" {}", wrapped_line)),
|
|
]));
|
|
}
|
|
}
|
|
|
|
if is_selected {
|
|
lines = lines
|
|
.iter()
|
|
.map(|line| line.clone().add_modifier(Modifier::BOLD))
|
|
.collect();
|
|
}
|
|
|
|
ListItem::new(lines)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn draw_left_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([Constraint::Min(10), Constraint::Length(2)])
|
|
.split(area);
|
|
|
|
// Included Publication list
|
|
|
|
let items = create_publication_item_list(
|
|
&app.included_publications,
|
|
if app.active_pane == ActivePane::IncludedPublications {
|
|
app.included_list_state.selected()
|
|
} else {
|
|
None
|
|
},
|
|
chunks[0].width.saturating_sub(4) as usize,
|
|
false,
|
|
);
|
|
|
|
let list = List::new(items).block(
|
|
Block::default()
|
|
.title_top(Line::from("Included Publications").centered())
|
|
.title_top(
|
|
Line::from(Span::styled(
|
|
format!("{} entries", app.included_publications.len()),
|
|
Style::default().fg(Color::Yellow),
|
|
))
|
|
.right_aligned(),
|
|
)
|
|
.borders(Borders::ALL),
|
|
);
|
|
|
|
frame.render_stateful_widget(list, chunks[0], &mut app.included_list_state);
|
|
|
|
// Snowballing progress
|
|
|
|
let progress = vec![
|
|
Line::from(vec![
|
|
Span::raw("Iteration: "),
|
|
Span::styled(
|
|
app.snowballing_iteration.to_string(),
|
|
Style::default().fg(Color::Cyan),
|
|
),
|
|
]),
|
|
Line::from(vec![
|
|
Span::raw("Step: "),
|
|
Span::styled(
|
|
app.snowballing_step.to_string(),
|
|
Style::default().fg(Color::Cyan),
|
|
),
|
|
]),
|
|
];
|
|
|
|
let progress_widget =
|
|
ratatui::widgets::Paragraph::new(progress).style(Style::default());
|
|
frame.render_widget(progress_widget, chunks[1]);
|
|
}
|
|
|
|
fn draw_right_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|
let items = create_publication_item_list(
|
|
&app.pending_publications,
|
|
if app.active_pane == ActivePane::PendingPublications {
|
|
app.pending_list_state.selected()
|
|
} else {
|
|
None
|
|
},
|
|
area.width.saturating_sub(4) as usize,
|
|
true,
|
|
);
|
|
|
|
let list = List::new(items).block(
|
|
Block::default()
|
|
.title_top(Line::from("Publications Pending Screening").centered())
|
|
.title_top(
|
|
Line::from(Span::styled(
|
|
format!("{} entries", app.pending_publications.len()),
|
|
Style::default().fg(Color::Yellow),
|
|
))
|
|
.right_aligned(),
|
|
)
|
|
.borders(Borders::ALL),
|
|
);
|
|
|
|
frame.render_stateful_widget(list, area, &mut app.pending_list_state);
|
|
}
|
|
|
|
fn draw_status_line(frame: &mut Frame, app: &App, area: Rect) {
|
|
let line = Paragraph::new(Line::from(match app.status_message.clone() {
|
|
StatusMessage::Info(s) => Span::raw(s),
|
|
StatusMessage::Warning(s) => {
|
|
Span::styled(s, Style::default().fg(Color::Yellow))
|
|
}
|
|
StatusMessage::Error(s) => {
|
|
Span::styled(s, Style::default().fg(Color::Red))
|
|
}
|
|
}))
|
|
.style(Style::default().bg(Color::Rgb(60, 56, 54)));
|
|
|
|
frame.render_widget(line, area);
|
|
}
|