feat: navigating the list of anime fetched

This commit is contained in:
Muhammad Nauman Raza 2024-05-28 20:53:41 +01:00
parent 76f1ee06f6
commit bb4b309ad9
Signed by: devraza
GPG key ID: 91EAD6081011574B
4 changed files with 110 additions and 13 deletions

View file

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub struct SearchResults { pub struct SearchResult {
pub id: String, pub id: String,
pub title: String, pub title: String,
pub url: String, pub url: String,
@ -15,7 +15,7 @@ pub struct SearchResults {
pub struct Search { pub struct Search {
pub currentPage: String, pub currentPage: String,
pub hasNextPage: bool, pub hasNextPage: bool,
pub results: Vec<SearchResults>, pub results: Vec<SearchResult>,
} }
pub async fn query_anime(query: String) -> Result<Search, reqwest::Error> { pub async fn query_anime(query: String) -> Result<Search, reqwest::Error> {

View file

@ -1,10 +1,68 @@
use std::error; use std::error;
use tui_input::Input; use tui_input::Input;
use ratatui::widgets::ListState;
use crate::api::SearchResult;
/// Application result type. /// Application result type.
pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>; pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
struct ListItem {
title: String,
released: String,
}
pub struct ResultList {
pub state: ListState,
pub items: Vec<SearchResult>,
pub last_selected: Option<usize>,
}
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 /// Application state
pub struct App { pub struct App {
/// Current value of the input box /// Current value of the input box
@ -12,7 +70,11 @@ pub struct App {
/// Is the app running? /// Is the app running?
pub running: bool, pub running: bool,
/// Results from the search query /// Results from the search query
pub results: String, pub results: Vec<SearchResult>,
/// Which chunk is currently selected?
pub chunk: usize,
/// Which list item is currently selected?
pub list: ResultList,
} }
impl Default for App { impl Default for App {
@ -20,7 +82,13 @@ impl Default for App {
App { App {
input: Input::default(), input: Input::default(),
running: true, 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) { pub fn quit(&mut self) {
self.running = false; 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));
}
} }

View file

@ -1,5 +1,6 @@
use crate::api::query_anime; use crate::api::query_anime;
use crate::app::{App, AppResult}; use crate::app::{App, AppResult, ResultList};
use crate::handler::KeyCode::*;
use crossterm::event::*; use crossterm::event::*;
use tui_input::backend::crossterm::EventHandler; 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(); app.quit();
} }
KeyEvent { code: KeyCode::Enter, .. } => { 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.reset();
} }
_ => { _ => {
app.input.handle_event(&Event::Key(key_event)); 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(()) Ok(())
} }

View file

@ -46,12 +46,20 @@ pub fn render(app: &mut App, frame: &mut Frame) {
.title("Search (Anime)"), .title("Search (Anime)"),
); );
frame.render_widget(input, chunks[1]); 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) let raw_results = &*app.list.items;
.block(Block::default().borders(Borders::ALL).title("Results")); let mut results: Vec<String> = Vec::new();
frame.render_widget(messages, chunks[2]); 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);
} }