feat: navigating the list of anime fetched
This commit is contained in:
parent
bf85615a28
commit
a3d317a64d
|
@ -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<SearchResults>,
|
||||
pub results: Vec<SearchResult>,
|
||||
}
|
||||
|
||||
pub async fn query_anime(query: String) -> Result<Search, reqwest::Error> {
|
||||
|
|
80
src/app.rs
80
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<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
|
||||
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<SearchResult>,
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
22
src/ui.rs
22
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<String> = 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);
|
||||
}
|
||||
|
|
Reference in a new issue