diff --git a/src/anime/anime.rs b/src/anime/anime.rs index 25f6653..e2c41f6 100644 --- a/src/anime/anime.rs +++ b/src/anime/anime.rs @@ -1,106 +1,314 @@ -use crate::main; use crate::open_video; use crate::{anime_ep_range, anime_link, anime_names}; use crate::{get_anime_id, get_token, get_user_anime_progress, update_anime_progress}; -use crate::{int_input, string_input}; -use colored::Colorize; -//use crate -pub fn anime_stream(search: String, episode: u32, resume: bool) { - let token = get_token(); - let query = if search != "" { - search - } else { - string_input("Search anime: ") - }; - let anime_list = anime_names(query); - let mut count = 0; - print!("\x1B[2J\x1B[1;1H"); - anime_list.iter().for_each(|anime| { - if count % 2 == 0 { - println!( - "({})\t{}", - format_args!("{}", count.to_string().blue()), - format_args!("{}", anime.blue()) - ); - } else { - println!( - "({})\t{}", - format_args!("{}", count.to_string().yellow()), - format_args!("{}", anime.yellow()) - ); - } - count += 1; - }); - let mut anime_num: usize = usize::MAX; - while anime_num == usize::max_value() || anime_num > anime_list.len() { - anime_num = int_input("Enter anime number: "); - if anime_num > anime_list.len() { - println!("Invalid anime number"); +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{error::Error, io}; +use tui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, + Frame, Terminal, +}; +use unicode_width::UnicodeWidthStr; + +enum InputMode { + Normal, + Editing, +} + +struct StatefulList { + state: ListState, + items: Vec, +} + +impl StatefulList { + fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items, } } - let title = &anime_list[anime_num]; - let ep_range = anime_ep_range(title); - // if there is only one episode, then don't ask user to choose episode - let id = get_anime_id(&title); - if ep_range == 1 { - let link = anime_link(title, 1); - open_video(link); - update_anime_progress(id, &title.replace("-", " "), 1, &token); - main(); - } else { - let mut ep_num: usize = usize::MAX; - if episode > ep_range.into() { - println!("Invalid episode number"); - main(); - } else if episode != 0 { - ep_num = episode as usize; - } else { - let current_progress = get_user_anime_progress(id, &token); - if resume && current_progress != 0 { - ep_num = (current_progress + 1) as usize; - } else { - println!("you are currently on episode {}", current_progress); - println!("select episode 1-{}: ", ep_range); - while ep_num == usize::max_value() || ep_num > ep_range as usize { - ep_num = int_input("Enter episode number: "); - if ep_num > ep_range as usize { - println!("Invalid episode number"); - } + + fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 } } + None => 0, + }; + self.state.select(Some(i)); + } + + fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + fn unselect(&mut self) { + self.state.select(None); + } + fn push(&mut self, item: T) { + self.items.push(item); + } + fn iter(&self) -> impl Iterator { + self.items.iter() + } +} + +struct App { + /// Current value of the input box + input: String, + /// Current input mode + input_mode: InputMode, + /// History of recorded messages + messages: StatefulList, + title: String, + ep: u64, + progress: i32, + anime_id: i32, + token: String, +} + +impl<'a> App { + fn default() -> App { + App { + input: String::new(), + input_mode: InputMode::Normal, + messages: StatefulList::with_items(Vec::new()), + title: String::new(), + ep: 0, + progress: 0, + anime_id: 0, + token: String::new(), } - loop { - let link = anime_link(title, ep_num as u64); - open_video(link); - let id = get_anime_id(&title.replace("-", " ")); - println!("{}", get_user_anime_progress(id, &token)); - update_anime_progress(id, &title.replace("-", " "), ep_num, &token); - println!("{}", "n: next episode".green()); - println!("{}", "p: previous episode".yellow()); - println!("{}", "s: search another anime".green()); - println!("{}", "q: quit".red()); - let input = string_input("Enter command: "); - if input == "n" { - if ep_num == ep_range as usize { - println!("No more episodes"); - } else { - ep_num += 1; - } - } else if input == "p" { - if ep_num == 1 { - println!("No previous episodes"); - } else { - ep_num -= 1; - } - } else if input == "s" { - //remove all the arguments - anime_stream("".to_string(), 0, false); - } else if input == "q" { - std::process::exit(0); - } else { - println!("Invalid command"); + } +} + +pub fn anime_ui() -> Result<(), Box> { + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // create app and run it + let app = App::default(); + let res = run_app(&mut terminal, app); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} + +fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { + let mut ep_select = false; + app.token = get_token(); + loop { + terminal.draw(|f| ui(f, &mut app))?; + + if let Event::Key(key) = event::read()? { + match app.input_mode { + InputMode::Normal => match key.code { + KeyCode::Char('o') => { + app.input_mode = InputMode::Editing; + } + KeyCode::Char('q') => { + return Ok(()); + } + KeyCode::Left => app.messages.unselect(), + KeyCode::Char('h') => app.messages.unselect(), + KeyCode::Down => app.messages.next(), + KeyCode::Char('j') => app.messages.next(), + KeyCode::Up => app.messages.previous(), + KeyCode::Char('k') => app.messages.previous(), + //if KeyCode::Enter => { + KeyCode::Enter => { + if ep_select == false { + let selected = app.messages.state.selected(); + app.title = app + .messages + .iter() + .nth(selected.unwrap()) + .unwrap() + .to_string(); + let ep_range = anime_ep_range(&app.title); + app.anime_id = get_anime_id(&app.title); + app.messages.items.clear(); + app.progress = + get_user_anime_progress(app.anime_id, app.token.as_str()); + //set app.messages.state.selected to app.progress + app.messages.state.select(Some(app.progress as usize)); + for ep in 1..ep_range { + app.messages.push(format!("Episode {}", ep)); + } + ep_select = true; + } else { + let selected = app.messages.state.selected(); + app.ep = app + .messages + .iter() + .nth(selected.unwrap()) + .unwrap() + .replace("Episode ", "") + .parse::() + .unwrap(); + let link = anime_link(&app.title, app.ep); + open_video(link); + update_anime_progress( + app.anime_id, + app.ep as usize, + app.token.as_str(), + ); + } + } + _ => {} + }, + InputMode::Editing => match key.code { + KeyCode::Enter => { + //push app.input into app.messages with '1 + let anime_list = anime_names(app.input.drain(..).collect()); + app.messages.items.clear(); + for anime in anime_list { + app.messages.push(anime); + } + ep_select = false; + app.input_mode = InputMode::Normal; + } + KeyCode::Char(c) => { + app.input.push(c); + } + KeyCode::Backspace => { + app.input.pop(); + } + KeyCode::Esc => { + app.input_mode = InputMode::Normal; + } + _ => {} + }, } } } } + +fn ui(f: &mut Frame, app: &mut App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Min(1), + Constraint::Length(1), + Constraint::Length(3), + ] + .as_ref(), + ) + .split(f.size()); + let block = Block::default() + .borders(Borders::ALL) + .title("kami") + .border_type(BorderType::Rounded); + f.render_widget(block, f.size()); + + let (msg, style) = match app.input_mode { + InputMode::Normal => ( + vec![ + Span::raw("Press "), + Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("o", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to search."), + ], + Style::default().add_modifier(Modifier::RAPID_BLINK), + ), + InputMode::Editing => ( + vec![ + Span::raw("Press "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to stop editing, "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to select."), + ], + Style::default(), + ), + }; + + let messages: Vec = app + .messages + .iter() + .enumerate() + .map(|(i, m)| { + let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))]; + ListItem::new(content) + }) + .collect(); + let messages = List::new(messages) + .block(Block::default().borders(Borders::ALL).title("list")) + .style(Style::default().fg(Color::White)) + .highlight_style( + Style::default() + .bg(Color::Rgb(183, 142, 241)) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">>"); + f.render_stateful_widget(messages, chunks[0], &mut app.messages.state); + + let mut text = Text::from(Spans::from(msg)); + text.patch_style(style); + let help_message = Paragraph::new(text); + f.render_widget(help_message, chunks[1]); + + let input = Paragraph::new(app.input.as_ref()) + .style(match app.input_mode { + InputMode::Normal => Style::default(), + InputMode::Editing => Style::default().fg(Color::Rgb(183, 142, 241)), + }) + .block(Block::default().borders(Borders::all()).title("Input")); + f.render_widget(input, chunks[2]); + match app.input_mode { + InputMode::Normal => + // Hide the cursor. `Frame` does this by default, so we don't need to do anything here + {} + + InputMode::Editing => { + // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering + f.set_cursor( + // Put cursor past the end of the input text + chunks[2].x + app.input.width() as u16 + 1, + // Move one line down, from the border to the input line + chunks[2].y + 1, + ) + } + } +} diff --git a/src/anime/trackers.rs b/src/anime/trackers.rs index 77796ea..927f90c 100644 --- a/src/anime/trackers.rs +++ b/src/anime/trackers.rs @@ -147,7 +147,7 @@ pub fn get_user_anime_progress(anime_id: i32, token: &str) -> i32 { } } -pub fn update_anime_progress(anime_id: i32, anime: &str, progress: usize, token: &str) { +pub fn update_anime_progress(anime_id: i32, progress: usize, token: &str) { const UPDATE: &str = " mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int) { SaveMediaListEntry (mediaId: $mediaId, status: $status, progress: $progress) { @@ -176,5 +176,4 @@ mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int) { .send() .unwrap() .text(); - println!("updated progress of {} to episode {}", anime, progress); } diff --git a/src/main.rs b/src/main.rs index d1de902..5f67e5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,8 @@ mod anime; mod helpers; mod ln; -mod ui; -use anime::anime::anime_stream; +use anime::anime::anime_ui; use colored::Colorize; use ln::search::search_ln; use ln::{ln::ln_read, scraper::get_ln_next_page}; @@ -15,14 +14,11 @@ use crate::anime::{ }; use crate::helpers::take_input::{int_input, string_input}; use crate::ln::{menu::chapter_selector, open_text::open_bat, scraper::get_full_text}; -use crate::ui::anime_ui::ui_anime; fn main() { let mut help = false; let mut anime = false; let mut ln = false; let mut chapter: u32 = 0; - let mut episode: u32 = 0; - let mut resume = false; //let search = option string let mut search = String::new(); let mut count = 0; @@ -55,16 +51,6 @@ fn main() { chapter = 0; } } - if arg == "--episode" || arg == "-e" { - if let Some(arg) = std::env::args().nth(count + 1) { - episode = arg.parse::().unwrap(); - } else { - episode = 0; - } - } - if arg == "--resume" || arg == "-r" { - resume = true; - } count += 1; } @@ -91,7 +77,7 @@ fn main() { ln_read(&search, chapter); } else if anime == true { //anime_stream(search, episode, resume); - ui_anime(); + _ = anime_ui(); } else { println!("Invalid argument"); } diff --git a/src/ui/anime_ui.rs b/src/ui/anime_ui.rs deleted file mode 100644 index d9658b7..0000000 --- a/src/ui/anime_ui.rs +++ /dev/null @@ -1,286 +0,0 @@ -use crate::open_video; -use crate::{anime_ep_range, anime_link, anime_names}; -use crate::{get_anime_id, get_token, get_user_anime_progress, update_anime_progress}; - -use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use std::{error::Error, io}; -use tui::{ - backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Span, Spans, Text}, - widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, - Frame, Terminal, -}; -use unicode_width::UnicodeWidthStr; - -enum InputMode { - Normal, - Editing, -} - -struct StatefulList { - state: ListState, - items: Vec, -} - -impl StatefulList { - fn with_items(items: Vec) -> StatefulList { - StatefulList { - state: ListState::default(), - items, - } - } - - fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.items.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - self.items.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - fn unselect(&mut self) { - self.state.select(None); - } - fn push(&mut self, item: T) { - self.items.push(item); - } - fn iter(&self) -> impl Iterator { - self.items.iter() - } -} - -struct App { - /// Current value of the input box - input: String, - /// Current input mode - input_mode: InputMode, - /// History of recorded messages - messages: StatefulList, - title: String, - ep: u64, -} - -impl<'a> App { - fn default() -> App { - App { - input: String::new(), - input_mode: InputMode::Normal, - messages: StatefulList::with_items(Vec::new()), - title: String::new(), - ep: 0, - } - } -} - -pub fn ui_anime() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // create app and run it - let app = App::default(); - let res = run_app(&mut terminal, app); - - // restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{:?}", err) - } - - Ok(()) -} - -fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { - let mut ep_select = false; - loop { - terminal.draw(|f| ui(f, &mut app))?; - - if let Event::Key(key) = event::read()? { - match app.input_mode { - InputMode::Normal => match key.code { - KeyCode::Char('e') => { - app.input_mode = InputMode::Editing; - } - KeyCode::Char('q') => { - return Ok(()); - } - KeyCode::Left => app.messages.unselect(), - KeyCode::Down => app.messages.next(), - KeyCode::Up => app.messages.previous(), - //if KeyCode::Enter => { - KeyCode::Enter => { - if ep_select == false { - let selected = app.messages.state.selected(); - app.title = app - .messages - .iter() - .nth(selected.unwrap()) - .unwrap() - .to_string(); - let ep_range = anime_ep_range(&app.title); - app.messages.items.clear(); - for ep in 1..ep_range { - app.messages.push(format!("Episode {}", ep)); - } - ep_select = true; - } else { - let selected = app.messages.state.selected(); - let ep = app.messages.iter().nth(selected.unwrap()).unwrap(); - let ep = ep.replace("Episode ", ""); - let ep = ep.parse::().unwrap(); - let link = anime_link(&app.title, ep); - open_video(link); - } - } - _ => {} - }, - InputMode::Editing => match key.code { - KeyCode::Enter => { - //push app.input into app.messages with '1 - let anime_list = anime_names(app.input.drain(..).collect()); - app.messages.items.clear(); - for anime in anime_list { - app.messages.push(anime); - } - } - KeyCode::Char(c) => { - app.input.push(c); - } - KeyCode::Backspace => { - app.input.pop(); - } - KeyCode::Esc => { - app.input_mode = InputMode::Normal; - } - _ => {} - }, - } - } - } -} - -fn ui(f: &mut Frame, app: &mut App) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints( - [ - Constraint::Length(1), - Constraint::Length(3), - Constraint::Min(1), - ] - .as_ref(), - ) - .split(f.size()); - let block = Block::default() - .borders(Borders::ALL) - .title("kami") - .border_type(BorderType::Rounded); - f.render_widget(block, f.size()); - - let (msg, style) = match app.input_mode { - InputMode::Normal => ( - vec![ - Span::raw("Press "), - Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to exit, "), - Span::styled("e", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to start editing."), - ], - Style::default().add_modifier(Modifier::RAPID_BLINK), - ), - InputMode::Editing => ( - vec![ - Span::raw("Press "), - Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to stop editing, "), - Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to record the message"), - ], - Style::default(), - ), - }; - let mut text = Text::from(Spans::from(msg)); - text.patch_style(style); - let help_message = Paragraph::new(text); - f.render_widget(help_message, chunks[0]); - - let input = Paragraph::new(app.input.as_ref()) - .style(match app.input_mode { - InputMode::Normal => Style::default(), - InputMode::Editing => Style::default().fg(Color::Yellow), - }) - .block(Block::default().borders(Borders::ALL).title("Input")); - f.render_widget(input, chunks[1]); - match app.input_mode { - InputMode::Normal => - // Hide the cursor. `Frame` does this by default, so we don't need to do anything here - {} - - InputMode::Editing => { - // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering - f.set_cursor( - // Put cursor past the end of the input text - chunks[1].x + app.input.width() as u16 + 1, - // Move one line down, from the border to the input line - chunks[1].y + 1, - ) - } - } - - let messages: Vec = app - .messages - .iter() - .enumerate() - .map(|(i, m)| { - let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))]; - ListItem::new(content) - }) - .collect(); - let messages = List::new(messages) - .block(Block::default().borders(Borders::ALL).title("Messages")) - .style(Style::default().fg(Color::White)) - .highlight_style( - Style::default() - .bg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">>"); - f.render_stateful_widget(messages, chunks[2], &mut app.messages.state); -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs deleted file mode 100644 index 7bd1780..0000000 --- a/src/ui/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod anime_ui;