Refactor directory structure; Fix warnings
This commit is contained in:
parent
3806865ae4
commit
b496edf404
328
src/app.rs
328
src/app.rs
@ -1,24 +1,22 @@
|
|||||||
use std::time::Duration;
|
pub mod common;
|
||||||
|
pub mod seeding;
|
||||||
|
pub mod snowballing;
|
||||||
|
|
||||||
use ratatui::{crossterm::event::KeyCode, widgets::ListState};
|
use log::{error, info, warn};
|
||||||
|
use ratatui::crossterm::event::KeyCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::mpsc::{self, error::SendError},
|
sync::mpsc::{self, error::SendError},
|
||||||
time::sleep,
|
time::sleep,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::snowballing::{
|
use crate::literature::{
|
||||||
Publication, SnowballingHistory, SnowballingStep, get_publication_by_id,
|
Publication, SnowballingHistory, get_publication_by_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::{error, info, warn};
|
use seeding::{SeedingAction, SeedingComponent};
|
||||||
|
use snowballing::{SnowballingAction, SnowballingComponent};
|
||||||
#[derive(Serialize, Deserialize, Default, PartialEq)]
|
|
||||||
pub enum ActivePane {
|
|
||||||
IncludedPublications,
|
|
||||||
#[default]
|
|
||||||
PendingPublications,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum StatusMessage {
|
pub enum StatusMessage {
|
||||||
@ -33,42 +31,6 @@ impl Default for StatusMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct SerializableListState {
|
|
||||||
pub offset: usize,
|
|
||||||
pub selected: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod liststate_serde {
|
|
||||||
use serde::{Deserializer, Serializer};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub fn serialize<S>(
|
|
||||||
state: &ListState,
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let surrogate = SerializableListState {
|
|
||||||
offset: state.offset(),
|
|
||||||
selected: state.selected(),
|
|
||||||
};
|
|
||||||
surrogate.serialize(serializer)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<ListState, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = SerializableListState::deserialize(deserializer)?;
|
|
||||||
Ok(ListState::default()
|
|
||||||
.with_offset(s.offset)
|
|
||||||
.with_selected(s.selected))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, PartialEq, Copy, Clone)]
|
#[derive(Serialize, Deserialize, Default, PartialEq, Copy, Clone)]
|
||||||
pub enum Tab {
|
pub enum Tab {
|
||||||
#[default]
|
#[default]
|
||||||
@ -76,27 +38,8 @@ pub enum Tab {
|
|||||||
Snowballing,
|
Snowballing,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum SeedingAction {
|
|
||||||
EnterChar(char),
|
|
||||||
EnterBackspace,
|
|
||||||
ClearInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum SnowballingAction {
|
|
||||||
SelectLeftPane,
|
|
||||||
SelectRightPane,
|
|
||||||
SearchForSelected,
|
|
||||||
NextItem,
|
|
||||||
PrevItem,
|
|
||||||
ShowIncludedPublication(Publication),
|
|
||||||
ShowPendingPublication(Publication),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum GlobalAction {
|
pub enum GlobalAction {
|
||||||
TriggerNextSnowballingStep,
|
|
||||||
ShowStatusMessage(StatusMessage),
|
ShowStatusMessage(StatusMessage),
|
||||||
ClearStatusMessage,
|
ClearStatusMessage,
|
||||||
AddIncludedPublication(Publication),
|
AddIncludedPublication(Publication),
|
||||||
@ -112,6 +55,14 @@ pub enum Action {
|
|||||||
Global(GlobalAction),
|
Global(GlobalAction),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait Component<T> {
|
||||||
|
fn handle_action(
|
||||||
|
&mut self,
|
||||||
|
action: T,
|
||||||
|
tx: &mpsc::UnboundedSender<Action>,
|
||||||
|
) -> Result<(), SendError<Action>>;
|
||||||
|
}
|
||||||
|
|
||||||
impl From<GlobalAction> for Action {
|
impl From<GlobalAction> for Action {
|
||||||
fn from(action: GlobalAction) -> Self {
|
fn from(action: GlobalAction) -> Self {
|
||||||
Action::Global(action)
|
Action::Global(action)
|
||||||
@ -130,175 +81,6 @@ impl From<SeedingAction> for Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait Component<T> {
|
|
||||||
fn handle_action(
|
|
||||||
&mut self,
|
|
||||||
action: T,
|
|
||||||
tx: &mpsc::UnboundedSender<Action>,
|
|
||||||
) -> Result<(), SendError<Action>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
|
||||||
pub struct SeedingComponent {
|
|
||||||
pub input: String,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub included_publications: Vec<Publication>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component<SeedingAction> for SeedingComponent {
|
|
||||||
fn handle_action(
|
|
||||||
&mut self,
|
|
||||||
action: SeedingAction,
|
|
||||||
action_tx: &mpsc::UnboundedSender<Action>,
|
|
||||||
) -> Result<(), SendError<Action>> {
|
|
||||||
match action {
|
|
||||||
SeedingAction::ClearInput => {
|
|
||||||
self.input.clear();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SeedingAction::EnterChar(c) => {
|
|
||||||
self.input.push(c);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SeedingAction::EnterBackspace => {
|
|
||||||
if self.input.len() > 0 {
|
|
||||||
self.input.truncate(self.input.len() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
|
||||||
pub struct SnowballingComponent {
|
|
||||||
#[serde(with = "liststate_serde")]
|
|
||||||
pub included_list_state: ListState,
|
|
||||||
#[serde(with = "liststate_serde")]
|
|
||||||
pub pending_list_state: ListState,
|
|
||||||
pub active_pane: ActivePane,
|
|
||||||
/// Local component copy of the included publications list
|
|
||||||
#[serde(skip)]
|
|
||||||
pub included_publications: Vec<Publication>,
|
|
||||||
/// Local component copy of the pending publications list
|
|
||||||
#[serde(skip)]
|
|
||||||
pub pending_publications: Vec<Publication>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SnowballingComponent {
|
|
||||||
fn next_list_item(
|
|
||||||
list_state: &mut ListState,
|
|
||||||
publications: &Vec<Publication>,
|
|
||||||
) {
|
|
||||||
let i = match list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i >= publications.len().wrapping_sub(1) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prev_list_item(
|
|
||||||
list_state: &mut ListState,
|
|
||||||
publications: &Vec<Publication>,
|
|
||||||
) {
|
|
||||||
let i = match list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i == 0 {
|
|
||||||
publications.len().wrapping_sub(1)
|
|
||||||
} else {
|
|
||||||
i - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component<SnowballingAction> for SnowballingComponent {
|
|
||||||
fn handle_action(
|
|
||||||
&mut self,
|
|
||||||
action: SnowballingAction,
|
|
||||||
action_tx: &mpsc::UnboundedSender<Action>,
|
|
||||||
) -> Result<(), SendError<Action>> {
|
|
||||||
match action {
|
|
||||||
SnowballingAction::SelectLeftPane => {
|
|
||||||
self.active_pane = ActivePane::IncludedPublications;
|
|
||||||
|
|
||||||
if let None = self.included_list_state.selected() {
|
|
||||||
self.included_list_state.select(Some(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SnowballingAction::SelectRightPane => {
|
|
||||||
self.active_pane = ActivePane::IncludedPublications;
|
|
||||||
|
|
||||||
if let None = self.included_list_state.selected() {
|
|
||||||
self.included_list_state.select(Some(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SnowballingAction::SearchForSelected => match self.active_pane {
|
|
||||||
ActivePane::IncludedPublications => {
|
|
||||||
if let Some(idx) = self.included_list_state.selected() {
|
|
||||||
open::that(&self.included_publications[idx].id)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
ActivePane::PendingPublications => {
|
|
||||||
if let Some(idx) = self.pending_list_state.selected() {
|
|
||||||
open::that(&self.pending_publications[idx].id).unwrap();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SnowballingAction::NextItem => match self.active_pane {
|
|
||||||
ActivePane::IncludedPublications => {
|
|
||||||
Self::next_list_item(
|
|
||||||
&mut self.included_list_state,
|
|
||||||
&self.included_publications,
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
ActivePane::PendingPublications => {
|
|
||||||
Self::next_list_item(
|
|
||||||
&mut self.pending_list_state,
|
|
||||||
&self.pending_publications,
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SnowballingAction::PrevItem => match self.active_pane {
|
|
||||||
ActivePane::IncludedPublications => {
|
|
||||||
Self::prev_list_item(
|
|
||||||
&mut self.included_list_state,
|
|
||||||
&self.included_publications,
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
ActivePane::PendingPublications => {
|
|
||||||
Self::prev_list_item(
|
|
||||||
&mut self.pending_list_state,
|
|
||||||
&self.pending_publications,
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
// UI state
|
// UI state
|
||||||
@ -327,6 +109,7 @@ pub struct App {
|
|||||||
pub action_tx: &'static mpsc::UnboundedSender<Action>,
|
pub action_tx: &'static mpsc::UnboundedSender<Action>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused_macros)]
|
||||||
macro_rules! status_info {
|
macro_rules! status_info {
|
||||||
($action_tx:expr, $text:expr, $($args:expr)*) => {
|
($action_tx:expr, $text:expr, $($args:expr)*) => {
|
||||||
$action_tx.send(
|
$action_tx.send(
|
||||||
@ -335,6 +118,7 @@ macro_rules! status_info {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#[allow(unused_macros)]
|
||||||
macro_rules! status_warn {
|
macro_rules! status_warn {
|
||||||
($action_tx:expr, $text:expr, $($args:expr)*) => {
|
($action_tx:expr, $text:expr, $($args:expr)*) => {
|
||||||
$action_tx.send(
|
$action_tx.send(
|
||||||
@ -343,6 +127,7 @@ macro_rules! status_warn {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#[allow(unused_macros)]
|
||||||
macro_rules! status_error {
|
macro_rules! status_error {
|
||||||
($action_tx:expr, $text:expr $(, $args:expr)*) => {
|
($action_tx:expr, $text:expr $(, $args:expr)*) => {
|
||||||
$action_tx.send(
|
$action_tx.send(
|
||||||
@ -403,76 +188,6 @@ impl App {
|
|||||||
self.state.status_message = StatusMessage::Info("".to_string());
|
self.state.status_message = StatusMessage::Info("".to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
GlobalAction::TriggerNextSnowballingStep => {
|
|
||||||
// if self.pending_publications.len() > 0 {
|
|
||||||
// warn!(
|
|
||||||
// "The next snowballing step can only be initiated \
|
|
||||||
// after screening all pending publications"
|
|
||||||
// );
|
|
||||||
// self.set_status_message(StatusMessage::Warning(
|
|
||||||
// "The next snowballing step can only be initiated \
|
|
||||||
// after screening all pending publications"
|
|
||||||
// .to_string(),
|
|
||||||
// ));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// match self.snowballing_step {
|
|
||||||
// SnowballingStep::Forward => {
|
|
||||||
// // TODO: Implement
|
|
||||||
// }
|
|
||||||
// SnowballingStep::Backward => {
|
|
||||||
// self.set_status_message(StatusMessage::Info(
|
|
||||||
// "Fetching references...".to_string(),
|
|
||||||
// ));
|
|
||||||
//
|
|
||||||
// // TODO: Find a way to not clone the publications
|
|
||||||
// for publication in self.included_publications.clone() {
|
|
||||||
// // TODO: In addition to the referenced_works do
|
|
||||||
// // an API call for citations
|
|
||||||
// for reference in &publication.referenced_works {
|
|
||||||
// let api_link = format!(
|
|
||||||
// "https://api.openalex.org/{}",
|
|
||||||
// &reference[21..]
|
|
||||||
// );
|
|
||||||
// let publ = get_publication_by_id(
|
|
||||||
// &api_link,
|
|
||||||
// "an.tsouchlos@gmail.com",
|
|
||||||
// )
|
|
||||||
// .await;
|
|
||||||
//
|
|
||||||
// match publ {
|
|
||||||
// Ok(publ) => {
|
|
||||||
// self.pending_publications.push(publ)
|
|
||||||
// }
|
|
||||||
// Err(err) => {
|
|
||||||
// warn!(
|
|
||||||
// "Failed to get publication\
|
|
||||||
// metadata using OpenAlex API: \
|
|
||||||
// {}",
|
|
||||||
// err
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// self.set_status_message(
|
|
||||||
// StatusMessage::Error(format!(
|
|
||||||
// "Failed to get publication\
|
|
||||||
// metadata using OpenAlex API: \
|
|
||||||
// {}",
|
|
||||||
// err
|
|
||||||
// )),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// self.set_status_message(StatusMessage::Info(
|
|
||||||
// "Done".to_string(),
|
|
||||||
// ));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
GlobalAction::SubmitSeedLink => {
|
GlobalAction::SubmitSeedLink => {
|
||||||
if !self
|
if !self
|
||||||
.state
|
.state
|
||||||
@ -543,7 +258,6 @@ impl App {
|
|||||||
.push(publ.clone());
|
.push(publ.clone());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Ok(()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
src/app/common.rs
Normal file
0
src/app/common.rs
Normal file
47
src/app/seeding.rs
Normal file
47
src/app/seeding.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::mpsc::{self, error::SendError};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{Action, Component},
|
||||||
|
literature::Publication,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SeedingAction {
|
||||||
|
EnterChar(char),
|
||||||
|
EnterBackspace,
|
||||||
|
ClearInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub struct SeedingComponent {
|
||||||
|
pub input: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub included_publications: Vec<Publication>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<SeedingAction> for SeedingComponent {
|
||||||
|
fn handle_action(
|
||||||
|
&mut self,
|
||||||
|
action: SeedingAction,
|
||||||
|
_: &mpsc::UnboundedSender<Action>,
|
||||||
|
) -> Result<(), SendError<Action>> {
|
||||||
|
match action {
|
||||||
|
SeedingAction::ClearInput => {
|
||||||
|
self.input.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SeedingAction::EnterChar(c) => {
|
||||||
|
self.input.push(c);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SeedingAction::EnterBackspace => {
|
||||||
|
if self.input.len() > 0 {
|
||||||
|
self.input.truncate(self.input.len() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
189
src/app/snowballing.rs
Normal file
189
src/app/snowballing.rs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
use ratatui::widgets::ListState;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::mpsc::error::SendError;
|
||||||
|
|
||||||
|
use crate::app::{Action, Component};
|
||||||
|
use crate::literature::Publication;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, PartialEq)]
|
||||||
|
pub enum ActivePane {
|
||||||
|
IncludedPublications,
|
||||||
|
#[default]
|
||||||
|
PendingPublications,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SnowballingAction {
|
||||||
|
SelectLeftPane,
|
||||||
|
SelectRightPane,
|
||||||
|
SearchForSelected,
|
||||||
|
NextItem,
|
||||||
|
PrevItem,
|
||||||
|
ShowIncludedPublication(Publication),
|
||||||
|
ShowPendingPublication(Publication),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct SerializableListState {
|
||||||
|
pub offset: usize,
|
||||||
|
pub selected: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod liststate_serde {
|
||||||
|
use serde::{Deserializer, Serializer};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn serialize<S>(
|
||||||
|
state: &ListState,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let surrogate = SerializableListState {
|
||||||
|
offset: state.offset(),
|
||||||
|
selected: state.selected(),
|
||||||
|
};
|
||||||
|
surrogate.serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<ListState, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = SerializableListState::deserialize(deserializer)?;
|
||||||
|
Ok(ListState::default()
|
||||||
|
.with_offset(s.offset)
|
||||||
|
.with_selected(s.selected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub struct SnowballingComponent {
|
||||||
|
#[serde(with = "liststate_serde")]
|
||||||
|
pub included_list_state: ListState,
|
||||||
|
#[serde(with = "liststate_serde")]
|
||||||
|
pub pending_list_state: ListState,
|
||||||
|
pub active_pane: ActivePane,
|
||||||
|
/// Local component copy of the included publications list
|
||||||
|
#[serde(skip)]
|
||||||
|
pub included_publications: Vec<Publication>,
|
||||||
|
/// Local component copy of the pending publications list
|
||||||
|
#[serde(skip)]
|
||||||
|
pub pending_publications: Vec<Publication>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SnowballingComponent {
|
||||||
|
fn next_list_item(
|
||||||
|
list_state: &mut ListState,
|
||||||
|
publications: &Vec<Publication>,
|
||||||
|
) {
|
||||||
|
let i = match list_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i >= publications.len().wrapping_sub(1) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_list_item(
|
||||||
|
list_state: &mut ListState,
|
||||||
|
publications: &Vec<Publication>,
|
||||||
|
) {
|
||||||
|
let i = match list_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
publications.len().wrapping_sub(1)
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component<SnowballingAction> for SnowballingComponent {
|
||||||
|
fn handle_action(
|
||||||
|
&mut self,
|
||||||
|
action: SnowballingAction,
|
||||||
|
_: &mpsc::UnboundedSender<Action>,
|
||||||
|
) -> Result<(), SendError<Action>> {
|
||||||
|
match action {
|
||||||
|
SnowballingAction::SelectLeftPane => {
|
||||||
|
self.active_pane = ActivePane::IncludedPublications;
|
||||||
|
|
||||||
|
if let None = self.included_list_state.selected() {
|
||||||
|
self.included_list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SnowballingAction::SelectRightPane => {
|
||||||
|
self.active_pane = ActivePane::IncludedPublications;
|
||||||
|
|
||||||
|
if let None = self.included_list_state.selected() {
|
||||||
|
self.included_list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SnowballingAction::SearchForSelected => match self.active_pane {
|
||||||
|
ActivePane::IncludedPublications => {
|
||||||
|
if let Some(idx) = self.included_list_state.selected() {
|
||||||
|
open::that(&self.included_publications[idx].id)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ActivePane::PendingPublications => {
|
||||||
|
if let Some(idx) = self.pending_list_state.selected() {
|
||||||
|
open::that(&self.pending_publications[idx].id).unwrap();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SnowballingAction::NextItem => match self.active_pane {
|
||||||
|
ActivePane::IncludedPublications => {
|
||||||
|
Self::next_list_item(
|
||||||
|
&mut self.included_list_state,
|
||||||
|
&self.included_publications,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ActivePane::PendingPublications => {
|
||||||
|
Self::next_list_item(
|
||||||
|
&mut self.pending_list_state,
|
||||||
|
&self.pending_publications,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SnowballingAction::PrevItem => match self.active_pane {
|
||||||
|
ActivePane::IncludedPublications => {
|
||||||
|
Self::prev_list_item(
|
||||||
|
&mut self.included_list_state,
|
||||||
|
&self.included_publications,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ActivePane::PendingPublications => {
|
||||||
|
Self::prev_list_item(
|
||||||
|
&mut self.pending_list_state,
|
||||||
|
&self.pending_publications,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
use std::{error::Error, io, time::Duration};
|
use std::{error::Error, io, time::Duration};
|
||||||
|
|
||||||
use crossterm::event::KeyCode;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Terminal,
|
Terminal,
|
||||||
|
|||||||
@ -136,10 +136,10 @@ impl Publication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
// #[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct OpenAlexResponse {
|
// pub struct OpenAlexResponse {
|
||||||
pub results: Vec<Publication>,
|
// pub results: Vec<Publication>,
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub async fn get_publication_by_id(
|
pub async fn get_publication_by_id(
|
||||||
api_link: &str,
|
api_link: &str,
|
||||||
@ -159,24 +159,24 @@ pub async fn get_publication_by_id(
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get all papers, not just the first page
|
// // TODO: Get all papers, not just the first page
|
||||||
pub async fn get_citing_papers(
|
// pub async fn get_citing_papers(
|
||||||
target_id: &str,
|
// target_id: &str,
|
||||||
email: &str,
|
// email: &str,
|
||||||
) -> Result<Vec<Publication>, Error> {
|
// ) -> Result<Vec<Publication>, Error> {
|
||||||
let url = format!(
|
// let url = format!(
|
||||||
"https://api.openalex.org/works?filter=cites:{}&mailto={}",
|
// "https://api.openalex.org/works?filter=cites:{}&mailto={}",
|
||||||
target_id, email
|
// target_id, email
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
let client = reqwest::Client::new();
|
// let client = reqwest::Client::new();
|
||||||
let response = client
|
// let response = client
|
||||||
.get(url)
|
// .get(url)
|
||||||
.header("User-Agent", "Rust-OpenAlex-Client/1.0")
|
// .header("User-Agent", "Rust-OpenAlex-Client/1.0")
|
||||||
.send()
|
// .send()
|
||||||
.await?
|
// .await?
|
||||||
.json::<OpenAlexResponse>()
|
// .json::<OpenAlexResponse>()
|
||||||
.await?;
|
// .await?;
|
||||||
|
//
|
||||||
Ok(response.results)
|
// Ok(response.results)
|
||||||
}
|
// }
|
||||||
10
src/main.rs
10
src/main.rs
@ -1,11 +1,11 @@
|
|||||||
|
mod app;
|
||||||
|
mod literature;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
mod app;
|
use crate::app::AppState;
|
||||||
mod ui;
|
|
||||||
use crate::app::{App, AppState, StatusMessage};
|
|
||||||
mod snowballing;
|
|
||||||
|
|
||||||
// use crate::snowballing::get_citing_papers;
|
// use crate::snowballing::get_citing_papers;
|
||||||
// #[tokio::main]
|
// #[tokio::main]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user