diff --git a/src/app.rs b/src/app.rs index b162293..e88d23c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,19 +33,12 @@ impl Default for StatusMessage { } } -#[derive(Serialize, Deserialize, Default, PartialEq, Copy, Clone)] -pub enum Tab { - #[default] - Seeding, - Snowballing, -} - #[derive(Clone, Debug)] pub enum GlobalAction { - ShowStatusMessage(StatusMessage), - ClearStatusMessage, - AddIncludedPublication(Publication), - SubmitSeedLink, + ShowStatMsg(StatusMessage), + ClearStatMsg, + AddIncludedPub(Publication), + FetchPub, NextTab, Quit, } @@ -75,6 +68,13 @@ impl From for Action { } } +#[derive(Serialize, Deserialize, Default, PartialEq, Copy, Clone)] +pub enum Tab { + #[default] + Seeding, + Snowballing, +} + #[derive(Serialize, Deserialize, Default)] pub struct AppState { // UI state @@ -109,121 +109,131 @@ pub struct App { // TODO: Implement export of included papers as csv for keywording with a spreadsheet // TODO: Implement export of included papers into zotero (Use RIS format somehow) impl App { + fn quit(&mut self) { + self.should_quit = true; + } + + fn next_tab(&mut self) { + self.state.current_tab = match self.state.current_tab { + Tab::Seeding => Tab::Snowballing, + Tab::Snowballing => Tab::Seeding, + }; + } + // TODO: Have status messages always last the same amount of time + fn show_stat_msg( + &mut self, + msg: StatusMessage, + action_tx: &'static mpsc::UnboundedSender, + ) { + match &msg { + StatusMessage::Error(_) => { + error!("Status message: {:?}", msg) + } + StatusMessage::Warning(_) => { + warn!("Status message: {:?}", msg) + } + StatusMessage::Info(_) => { + info!("Status message: {:?}", msg) + } + } + + self.state.status_message = msg; + + tokio::spawn(async move { + sleep(Duration::from_millis(4000)).await; + + if let Err(err) = action_tx.send(GlobalAction::ClearStatMsg.into()) + { + error!("{}", err); + } + }); + } + + fn clear_stat_msg(&mut self) { + self.state.status_message = StatusMessage::Info("".to_string()); + } + + fn add_included_publ( + &mut self, + publ: Publication, + ) -> Result<(), SendError> { + self.state + .history + .current_iteration + .included_publications + .push(publ.clone()); + Ok(()) + } + + fn fetch_publication( + &self, + action_tx: &'static mpsc::UnboundedSender, + ) -> Result<(), SendError> { + if !self + .state + .seeding + .input + .starts_with("https://openalex.org/") + { + status_error!( + self.action_tx, + "Seed link must start with 'https://openalex.org/'" + )?; + return Ok(()); + } + + status_info!( + self.action_tx, + "Submitting seed link: {}", + &self.state.seeding.input + )?; + + let api_link = format!( + "https://api.openalex.org/{}", + self.state + .seeding + .input + .trim_start_matches("https://openalex.org/") + ); + + tokio::spawn(async move { + let publ = + get_publication_by_id(&api_link, "an.tsouchlos@gmail.com"); + + match publ.await { + Ok(publ) => { + let _ = status_info!( + action_tx, + "Seed paper obtained successfully: {}", + publ.get_title() + .unwrap_or("[title unavailable]".to_string()) + ); + + let _ = action_tx + .send(GlobalAction::AddIncludedPub(publ).into()); + } + Err(e) => { + let _ = status_error!(action_tx, "{}", e.to_string()); + } + } + }); + + self.action_tx.send(SeedingAction::ClearInput.into()) + } + fn handle_global_action( &mut self, action: GlobalAction, - action_tx: &'static mpsc::UnboundedSender, + tx: &'static mpsc::UnboundedSender, ) -> Result<(), SendError> { match action { - GlobalAction::NextTab => { - self.state.current_tab = match self.state.current_tab { - Tab::Seeding => Tab::Snowballing, - Tab::Snowballing => Tab::Seeding, - }; - Ok(()) - } - GlobalAction::ShowStatusMessage(msg) => { - match &msg { - StatusMessage::Error(_) => { - error!("Status message: {:?}", msg) - } - StatusMessage::Warning(_) => { - warn!("Status message: {:?}", msg) - } - StatusMessage::Info(_) => { - info!("Status message: {:?}", msg) - } - } - - self.state.status_message = msg; - - tokio::spawn(async move { - sleep(Duration::from_millis(1000)).await; - - if let Err(err) = - action_tx.send(GlobalAction::ClearStatusMessage.into()) - { - error!("{}", err); - } - }); - - Ok(()) - } - GlobalAction::ClearStatusMessage => { - self.state.status_message = StatusMessage::Info("".to_string()); - Ok(()) - } - GlobalAction::SubmitSeedLink => { - if !self - .state - .seeding - .input - .starts_with("https://openalex.org/") - { - status_error!( - self.action_tx, - "Seed link must start with 'https://openalex.org/'" - )?; - return Ok(()); - } - - status_info!( - self.action_tx, - "Submitting seed link: {}", - &self.state.seeding.input - )?; - - let api_link = format!( - "https://api.openalex.org/{}", - self.state - .seeding - .input - .trim_start_matches("https://openalex.org/") - ); - - tokio::spawn(async move { - let publ = get_publication_by_id( - &api_link, - "an.tsouchlos@gmail.com", - ); - - match publ.await { - Ok(publ) => { - let _ = status_info!( - action_tx, - "Seed paper obtained successfully: {}", - publ.get_title().unwrap_or( - "[title unavailable]".to_string() - ) - ); - - let _ = action_tx.send( - GlobalAction::AddIncludedPublication(publ) - .into(), - ); - } - Err(e) => { - let _ = - status_error!(action_tx, "{}", e.to_string()); - } - } - }); - - self.action_tx.send(SeedingAction::ClearInput.into()) - } - GlobalAction::Quit => { - self.should_quit = true; - Ok(()) - } - GlobalAction::AddIncludedPublication(publ) => { - self.state - .history - .current_iteration - .included_publications - .push(publ.clone()); - Ok(()) - } + GlobalAction::Quit => Ok(self.quit()), + GlobalAction::NextTab => Ok(self.next_tab()), + GlobalAction::ClearStatMsg => Ok(self.clear_stat_msg()), + GlobalAction::ShowStatMsg(msg) => Ok(self.show_stat_msg(msg, tx)), + GlobalAction::FetchPub => self.fetch_publication(tx), + GlobalAction::AddIncludedPub(publ) => self.add_included_publ(publ), } } @@ -264,7 +274,7 @@ impl App { self.action_tx.send(SeedingAction::EnterBackspace.into())?; } (Tab::Seeding, KeyCode::Enter) => { - self.action_tx.send(GlobalAction::SubmitSeedLink.into())?; + self.action_tx.send(GlobalAction::FetchPub.into())?; } _ => {} } diff --git a/src/app/common.rs b/src/app/common.rs index 0de64eb..ffd5a38 100644 --- a/src/app/common.rs +++ b/src/app/common.rs @@ -7,7 +7,7 @@ use crate::app::Action; macro_rules! status_info { ($action_tx:expr, $text:expr, $($args:expr)*) => { $action_tx.send( - GlobalAction::ShowStatusMessage(StatusMessage::Info(format!($text, $($args)*))) + GlobalAction::ShowStatMsg(StatusMessage::Info(format!($text, $($args)*))) .into(), ) }; @@ -18,7 +18,7 @@ macro_rules! status_info { macro_rules! status_warn { ($action_tx:expr, $text:expr, $($args:expr)*) => { $action_tx.send( - GlobalAction::ShowStatusMessage(StatusMessage::Warning(format!($text, $($args)*))) + GlobalAction::ShowStatMsg(StatusMessage::Warning(format!($text, $($args)*))) .into(), ) }; @@ -29,7 +29,7 @@ macro_rules! status_warn { macro_rules! status_error { ($action_tx:expr, $text:expr $(, $args:expr)*) => { $action_tx.send( - GlobalAction::ShowStatusMessage(StatusMessage::Error(format!($text, $($args)*))) + GlobalAction::ShowStatMsg(StatusMessage::Error(format!($text, $($args)*))) .into(), ) }; diff --git a/src/app/seeding.rs b/src/app/seeding.rs index e6076e0..7d994c9 100644 --- a/src/app/seeding.rs +++ b/src/app/seeding.rs @@ -17,6 +17,22 @@ pub struct SeedingComponent { pub included_publications: Vec, } +impl SeedingComponent { + pub fn clear_input(&mut self) { + self.input.clear(); + } + + pub fn enter_char(&mut self, c: char) { + self.input.push(c); + } + + pub fn enter_backspace(&mut self) { + if self.input.len() > 0 { + self.input.truncate(self.input.len() - 1); + } + } +} + impl Component for SeedingComponent { fn handle_action( &mut self, @@ -24,21 +40,9 @@ impl Component for SeedingComponent { _: &mpsc::UnboundedSender, ) -> Result<(), SendError> { 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(()) - } + SeedingAction::ClearInput => Ok(self.clear_input()), + SeedingAction::EnterChar(c) => Ok(self.enter_char(c)), + SeedingAction::EnterBackspace => Ok(self.enter_backspace()), } } } diff --git a/src/app/snowballing.rs b/src/app/snowballing.rs index d39f3ab..0b7cea7 100644 --- a/src/app/snowballing.rs +++ b/src/app/snowballing.rs @@ -17,11 +17,9 @@ pub enum ActivePane { pub enum SnowballingAction { SelectLeftPane, SelectRightPane, - SearchForSelected, + Search, NextItem, PrevItem, - ShowIncludedPublication(Publication), - ShowPendingPublication(Publication), } #[derive(Serialize, Deserialize)] @@ -76,7 +74,7 @@ pub struct SnowballingComponent { } impl SnowballingComponent { - fn next_list_item( + fn select_next_item_impl( list_state: &mut ListState, publications: &Vec, ) { @@ -93,7 +91,7 @@ impl SnowballingComponent { list_state.select(Some(i)); } - fn prev_list_item( + fn select_prev_item_impl( list_state: &mut ListState, publications: &Vec, ) { @@ -109,6 +107,71 @@ impl SnowballingComponent { }; list_state.select(Some(i)); } + + fn select_left_pane(&mut self) { + self.active_pane = ActivePane::IncludedPublications; + + if let None = self.included_list_state.selected() { + self.included_list_state.select(Some(0)); + } + } + + fn select_right_pane(&mut self) { + self.active_pane = ActivePane::PendingPublications; + + if let None = self.pending_list_state.selected() { + self.pending_list_state.select(Some(0)); + } + } + + fn search(&self) { + match self.active_pane { + ActivePane::IncludedPublications => { + if let Some(idx) = self.included_list_state.selected() { + open::that(&self.included_publications[idx].id).unwrap(); + } + } + ActivePane::PendingPublications => { + if let Some(idx) = self.pending_list_state.selected() { + open::that(&self.pending_publications[idx].id).unwrap(); + } + } + } + } + + fn next_item(&mut self) { + match self.active_pane { + ActivePane::IncludedPublications => { + Self::select_next_item_impl( + &mut self.included_list_state, + &self.included_publications, + ); + } + ActivePane::PendingPublications => { + Self::select_next_item_impl( + &mut self.pending_list_state, + &self.pending_publications, + ); + } + } + } + + fn prev_item(&mut self) { + match self.active_pane { + ActivePane::IncludedPublications => { + Self::select_prev_item_impl( + &mut self.included_list_state, + &self.included_publications, + ); + } + ActivePane::PendingPublications => { + Self::select_prev_item_impl( + &mut self.pending_list_state, + &self.pending_publications, + ); + } + } + } } impl Component for SnowballingComponent { @@ -118,72 +181,11 @@ impl Component for SnowballingComponent { _: &mpsc::UnboundedSender, ) -> Result<(), SendError> { 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(()), + SnowballingAction::SelectLeftPane => Ok(self.select_left_pane()), + SnowballingAction::SelectRightPane => Ok(self.select_right_pane()), + SnowballingAction::Search => Ok(self.search()), + SnowballingAction::NextItem => Ok(self.next_item()), + SnowballingAction::PrevItem => Ok(self.prev_item()), } } }