From a3d317a64da3a7fa5651bc5e791332b0c3b6c9f0 Mon Sep 17 00:00:00 2001 From: Muhammad Nauman Raza Date: Tue, 28 May 2024 20:53:41 +0100 Subject: [PATCH] feat: navigating the list of anime fetched --- src/api.rs | 4 +-- src/app.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/handler.rs | 17 +++++++++-- src/ui.rs | 22 +++++++++----- 4 files changed, 110 insertions(+), 13 deletions(-) diff --git a/src/api.rs b/src/api.rs index eacb160..dcf96fb 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] #[allow(non_snake_case)] -pub struct SearchResults { +pub struct SearchResult { pub id: String, pub title: String, pub url: String, @@ -15,7 +15,7 @@ pub struct SearchResults { pub struct Search { pub currentPage: String, pub hasNextPage: bool, - pub results: Vec, + pub results: Vec, } pub async fn query_anime(query: String) -> Result { diff --git a/src/app.rs b/src/app.rs index 6254074..8c38321 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,68 @@ use std::error; use tui_input::Input; +use ratatui::widgets::ListState; + +use crate::api::SearchResult; /// Application result type. pub type AppResult = std::result::Result>; +struct ListItem { + title: String, + released: String, +} + +pub struct ResultList { + pub state: ListState, + pub items: Vec, + pub last_selected: Option, +} + +impl ResultList { + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => self.last_selected.unwrap_or(0), + }; + self.state.select(Some(i)); + } + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => self.last_selected.unwrap_or(0), + }; + self.state.select(Some(i)); + } + pub fn unselect(&mut self) { + let offset = self.state.offset(); + self.last_selected = self.state.selected(); + self.state.select(None); + *self.state.offset_mut() = offset; + } +} + +impl ListItem { + fn new(title: &str, released: &str) -> Self { + Self { + title: title.to_string(), + released: released.to_string(), + } + } +} + /// Application state pub struct App { /// Current value of the input box @@ -12,7 +70,11 @@ pub struct App { /// Is the app running? pub running: bool, /// Results from the search query - pub results: String, + pub results: Vec, + /// Which chunk is currently selected? + pub chunk: usize, + /// Which list item is currently selected? + pub list: ResultList, } impl Default for App { @@ -20,7 +82,13 @@ impl Default for App { App { input: Input::default(), running: true, - results: "".to_string(), + results: vec![], + chunk: 1, + list: ResultList { + state: ListState::default(), + items: Vec::new(), + last_selected: None + }, } } } @@ -38,4 +106,12 @@ impl App { pub fn quit(&mut self) { self.running = false; } + + pub fn go_top(&mut self) { + self.list.state.select(Some(0)); + } + + pub fn go_bottom(&mut self) { + self.list.state.select(Some(self.list.items.len() - 1)); + } } diff --git a/src/handler.rs b/src/handler.rs index e11f8d4..3a1faa9 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,5 +1,6 @@ use crate::api::query_anime; -use crate::app::{App, AppResult}; +use crate::app::{App, AppResult, ResultList}; +use crate::handler::KeyCode::*; use crossterm::event::*; use tui_input::backend::crossterm::EventHandler; @@ -18,12 +19,24 @@ pub async fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult< app.quit(); } KeyEvent { code: KeyCode::Enter, .. } => { - app.results = format!("{:#?}", query_anime(app.input.to_string()).await.unwrap().results); + app.list = ResultList { + state: app.list.state.clone(), + items: query_anime(app.input.to_string()).await.unwrap().results, + last_selected: app.list.last_selected, + }; + app.chunk = 2; app.input.reset(); } _ => { app.input.handle_event(&Event::Key(key_event)); } } + match key_event.code { + Char('j') | Down => app.list.next(), + Char('k') | Up => app.list.previous(), + Char('g') => app.go_top(), + Char('G') => app.go_bottom(), + _ => {} + } Ok(()) } diff --git a/src/ui.rs b/src/ui.rs index 88ed747..f3ca4f2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -46,12 +46,20 @@ pub fn render(app: &mut App, frame: &mut Frame) { .title("Search (Anime)"), ); frame.render_widget(input, chunks[1]); - frame.set_cursor( - chunks[1].x + ((app.input.visual_cursor()).max(scroll) - scroll) as u16 + 1, - chunks[1].y + 1, - ); - let messages = Paragraph::new(&*app.results) - .block(Block::default().borders(Borders::ALL).title("Results")); - frame.render_widget(messages, chunks[2]); + let raw_results = &*app.list.items; + let mut results: Vec = Vec::new(); + for i in raw_results { + results.push(format!("{}\n{}", i.title, i.releaseDate)); + } + + let list = List::new(results) + .block(Block::bordered().title("Search Results")) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) + .highlight_symbol("┃ ") + .repeat_highlight_symbol(true) + .direction(ListDirection::TopToBottom); + + frame.render_stateful_widget(list, chunks[2], &mut app.list.state); }