feat: navigating the list of anime fetched
This commit is contained in:
parent
76f1ee06f6
commit
bb4b309ad9
|
@ -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> {
|
||||||
|
|
80
src/app.rs
80
src/app.rs
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
22
src/ui.rs
22
src/ui.rs
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue