diff --git a/src/ln/ln.rs b/src/ln/ln.rs index c6e7a3a..beaec6c 100644 --- a/src/ln/ln.rs +++ b/src/ln/ln.rs @@ -1,37 +1,376 @@ -use crate::{chapter_selector, get_full_text, open_bat, search_ln}; +use crate::ln::open_text::*; +use crate::ln::scraper::*; use std::fs::File; use std::io::Write; -pub fn ln_read(search: &str, chapter: u32) { - //convert search in to Option<&str> - let ln_url = search_ln(&search); - let chapter = chapter as f64; - let mut selected_page = 1; - if chapter != 0.0 { - selected_page = (chapter / 48.0).ceil() as u32; + +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, + } } - loop { - //make empty tuple called chapter_url with (String, u32, u32) - let chapter_url = chapter_selector(&ln_url, selected_page); - selected_page = chapter_url.1; - let full_text = get_full_text(&chapter_url.0); - if cfg!(target_os = "windows") { - use dirs::home_dir; - let mut home = format!("{:?}", home_dir()).replace("\\\\", "/"); - home.drain(0..6); - home.drain(home.len() - 2..home.len()); - let mut file = File::create(format!("{}/AppData/Roaming/log_e", home)) - .expect("Unable to create file"); - file.write_all(full_text.as_bytes()) - .expect("Unable to write to file"); - file.sync_all().expect("Unable to sync file"); - } else { - let mut file = File::create("/tmp/log_e").expect("Unable to create file"); - file.write_all(full_text.as_bytes()) - .expect("Unable to write to file"); - file.sync_all().expect("Unable to sync file"); + + fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, }; - //open temp.txt in cat for user to read - let _com = open_bat(); - print!("\x1B[2J\x1B[1;1H"); + 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, + ln_titles: Vec, + ln_links: Vec, + title: String, + ln_id: String, + ln_chapters: Vec, + ln_chapters_links: Vec, + last_page: String, + current_page: String, + current_page_number: u32, +} + +impl<'a> App { + fn default() -> App { + App { + input: String::new(), + input_mode: InputMode::Normal, + messages: StatefulList::with_items(Vec::new()), + ln_titles: Vec::new(), + ln_links: Vec::new(), + title: String::new(), + ln_id: String::new(), + ln_chapters: Vec::new(), + ln_chapters_links: Vec::new(), + last_page: String::new(), + current_page: String::new(), + current_page_number: 0, + } + } +} + +pub fn ln_ui(chapter: u32) -> 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 mut app = App::default(); + let chapter = chapter as f64; + app.current_page_number = 1; + if chapter != 0.0 { + app.current_page_number = (chapter / 48.0).ceil() as u32; + } + + 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 chapter_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('o') => { + app.input_mode = InputMode::Editing; + } + KeyCode::Char('q') => { + terminal.clear()?; + return Ok(()); + } + KeyCode::Left => app.messages.unselect(), + KeyCode::Char('h') => { + if app.current_page_number > 0 { + app.current_page_number -= 1; + } + app.current_page = + get_ln_next_page(&app.ln_id, &(app.current_page_number.to_string())); + app.ln_chapters = get_ln_chapters(&app.current_page); + app.ln_chapters_links = get_ln_chapters_urls(&app.current_page); + app.messages.items.clear(); + for chapter in app.ln_chapters.iter() { + app.messages.push(chapter.to_string()); + } + } + KeyCode::Down => app.messages.next(), + KeyCode::Char('j') => app.messages.next(), + KeyCode::Up => app.messages.previous(), + KeyCode::Char('k') => app.messages.previous(), + KeyCode::Char('l') => { + if app.current_page_number < app.last_page.parse::().unwrap() { + app.current_page_number += 1; + } + app.current_page = + get_ln_next_page(&app.ln_id, &(app.current_page_number.to_string())); + app.ln_chapters = get_ln_chapters(&app.current_page); + app.ln_chapters_links = get_ln_chapters_urls(&app.current_page); + app.messages.items.clear(); + for chapter in app.ln_chapters.iter() { + app.messages.push(chapter.to_string()); + } + } + //if KeyCode::Enter => { + KeyCode::Enter => { + if chapter_select == false { + let selected = app.messages.state.selected(); + app.title = app + .messages + .iter() + .nth(selected.unwrap()) + .unwrap() + .to_string(); + let link = app.ln_links[selected.unwrap()].to_string(); + let html = get_html(&link); + app.ln_id = get_ln_id(&html).to_string(); + app.last_page = get_ln_last_page(&html); + app.current_page = get_ln_next_page( + &app.ln_id.to_string(), + &app.current_page_number.to_string(), + ); + app.ln_chapters = get_ln_chapters(&app.current_page); + app.ln_chapters_links = get_ln_chapters_urls(&app.current_page); + app.messages.items.clear(); + for chapter in app.ln_chapters.iter() { + app.messages.push(chapter.to_string()); + } + chapter_select = true; + } else { + let selected = app.messages.state.selected(); + let chapter_url = app.ln_chapters_links[selected.unwrap()].to_string(); + let full_text = get_full_text(&chapter_url); + if cfg!(target_os = "windows") { + use dirs::home_dir; + let mut home = format!("{:?}", home_dir()).replace("\\\\", "/"); + home.drain(0..6); + home.drain(home.len() - 2..home.len()); + let mut file = + File::create(format!("{}/AppData/Roaming/log_e", home)) + .expect("Unable to create file"); + file.write_all(full_text.as_bytes()) + .expect("Unable to write to file"); + file.sync_all().expect("Unable to sync file"); + } else { + let mut file = + File::create("/tmp/log_e").expect("Unable to create file"); + file.write_all(full_text.as_bytes()) + .expect("Unable to write to file"); + file.sync_all().expect("Unable to sync file"); + }; + terminal.clear()?; + let _ = open_bat(); + terminal.clear()?; + } + } + _ => {} + }, + InputMode::Editing => match key.code { + KeyCode::Enter => { + //push app.input into app.messages with '1 + let search: String = app.input.drain(..).collect(); + let search = search.replace(" ", "+"); + let url = "https://readlightnovels.net/?s=".to_string(); + let url = format!("{}{}", url, search.trim()).trim().to_string(); + let html = get_html(&url); + let ln_list = get_ln_list(html.as_str()); + app.ln_titles = get_ln_titles(&ln_list); + app.ln_links = get_ln_urls(&ln_list); + app.messages.items.clear(); + //remove index 0 of app.ln_titles and app.ln_links + app.ln_titles.remove(0); + app.ln_links.remove(0); + for ln in &app.ln_titles { + app.messages.push(ln.to_string()); + } + chapter_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/ln/menu.rs b/src/ln/menu.rs deleted file mode 100644 index 76c1cbd..0000000 --- a/src/ln/menu.rs +++ /dev/null @@ -1,68 +0,0 @@ -use colored::Colorize; - -use crate::{ - helpers::take_input::string_input, - ln::{ - self, - scraper::{get_ln_chapters_urls, get_ln_id, get_ln_last_page}, - search::get_ln_chapters, - }, - main, page_selector, -}; - -pub fn chapter_selector(ln_url: &str, mut selected_page: u32) -> (String, u32) { - loop { - let ln_html = ln::scraper::get_html(ln_url); - let ln_id = get_ln_id(&ln_html); - let ln_last_page = get_ln_last_page(&ln_html); - let ln_page_html = page_selector(&ln_id, selected_page); - let ln_chapters = get_ln_chapters(&ln_page_html); - let ln_chapters_urls = get_ln_chapters_urls(&ln_page_html); - let mut count = 0; - ln_chapters.into_iter().for_each(|chaprer| { - if count % 2 == 0 { - println!( - "({})\t{}", - count.to_string().blue(), - format_args!("{}", chaprer.blue()) - ); - } else { - println!( - "({})\t{}", - count.to_string().yellow(), - format_args!("{}", chaprer.yellow()) - ); - } - count += 1; - }); - println!("{}\t{}", "n:".green(), "Go to next page".green()); - println!("{}\t{}", "b:".yellow(), "Go to previous page".yellow()); - println!("{}\t{}", "s:".green(), "Search another title".green()); - println!("{}\t{}", "q:".red(), "quit".red()); - let chapter_number = string_input("Which chapter do you want to read? "); - if chapter_number == "n" && selected_page < ln_last_page.parse::().unwrap() { - selected_page += 1; - print!("\x1B[2J\x1B[1;1H"); - } else if chapter_number == "b" && selected_page > 1 { - selected_page -= 1; - print!("\x1B[2J\x1B[1;1H"); - } else if chapter_number == "q" { - print!("\x1B[2J\x1B[1;1H"); - std::process::exit(0); - } else if chapter_number == "s" { - main(); - } else { - let chapter_number = chapter_number.trim().to_string(); - let mut _chapter_number_int = 0; - if chapter_number.parse::().is_ok() { - _chapter_number_int = chapter_number.parse::().unwrap(); - } else { - println!("{}", "Invalid option".red()); - continue; - } - let chapter_url = &ln_chapters_urls[_chapter_number_int as usize]; - let chapter_url = chapter_url.trim().to_string(); - return (chapter_url, selected_page); - } - } -} diff --git a/src/ln/mod.rs b/src/ln/mod.rs index ca48542..2960c4c 100644 --- a/src/ln/mod.rs +++ b/src/ln/mod.rs @@ -1,5 +1,3 @@ -pub mod menu; +pub mod ln; pub mod open_text; pub mod scraper; -pub mod search; -pub mod ln; diff --git a/src/ln/scraper.rs b/src/ln/scraper.rs index b97e0b9..1bc2e84 100644 --- a/src/ln/scraper.rs +++ b/src/ln/scraper.rs @@ -1,6 +1,8 @@ use isahc::{ReadResponseExt, Request, RequestExt}; use regex::Regex; +use crate::helpers::fixing_text::remove_after_dash; + use crate::helpers::fixing_text::fix_html_encoding; //gets the full html of the page @@ -102,3 +104,47 @@ pub fn get_ln_text(chapter_url: &str) -> Vec { fix_html_encoding(&ln_text) } + +//gets the list of ln's from the html and returns it as a vector of the ln's name and href +pub fn get_ln_list(html: &str) -> Vec { + let re = Regex::new(r#"(?m)^\s*( = Vec::new(); + for cap in re.captures_iter(html) { + ln_list.push(cap.get(1).unwrap().as_str().trim().to_string()); + } + ln_list +} +//gets the titles of the ln's from the html and returns it as a vector of the ln's name +pub fn get_ln_titles(ln_list: &Vec) -> Vec { + let re = Regex::new(r#"(?m)^\s* = Vec::new(); + for ln in ln_list { + for cap in re.captures_iter(ln) { + ln_title.push(cap.get(1).unwrap().as_str().to_string()); + } + } + ln_title +} + +//gets the urls of the ln's from the html and returns it as a vector of the ln's href +pub fn get_ln_urls(ln_list: &Vec) -> Vec { + let re = Regex::new(r#"(?m)^\s* = Vec::new(); + for ln in ln_list { + for cap in re.captures_iter(ln) { + ln_url.push(cap.get(1).unwrap().as_str().to_string()); + } + } + ln_url +} + +//gets the chapter titles from the html and returns it as a vector of the chapter's name +pub fn get_ln_chapters(html: &str) -> Vec { + let re = Regex::new(r#"title=(.*?)>"#).unwrap(); + let mut ln_list: Vec = Vec::new(); + for cap in re.captures_iter(html) { + ln_list.push(cap.get(1).unwrap().as_str().trim().to_string()); + } + ln_list = remove_after_dash(&ln_list); + ln_list +} diff --git a/src/ln/search.rs b/src/ln/search.rs deleted file mode 100644 index b634727..0000000 --- a/src/ln/search.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::helpers::{fixing_text::remove_after_dash, take_input::string_input}; -use colored::Colorize; -use regex::Regex; - -pub fn search_ln(search: &str) -> String { - let mut _is_n = false; - print!("\x1B[2J\x1B[1;1H"); - while !_is_n { - //if search is None, take input from user - let search_path = if search == "" { - string_input("What ln do you want to read? ") - } else { - search.to_string() - }; - let search_path = search_path.replace(' ', "+"); - let url = "https://readlightnovels.net/?s=".to_string(); - let url = format!("{}{}", url, search_path.trim()).trim().to_string(); - let html = crate::ln::scraper::get_html(&url).trim().to_string(); - let ln_list = get_ln_list(&html); - //remove first element of ln_list - let ln_list = ln_list - .iter() - .skip(1) - .map(|x| x.to_string()) - .collect::>(); - let ln_titles = get_ln_titles(&ln_list); - let ln_urls = get_ln_urls(&ln_list); - let mut count = 0; - ln_titles.into_iter().for_each(|ln| { - if count % 2 == 0 { - println!("({})\t{}", count.to_string().blue(), format_args!("{}", ln.blue())); - } else { - println!("({})\t{}", count.to_string().yellow(), format_args!("{}", ln.yellow())); - } - count += 1; - }); - println!("{}\t{}","s:".green(), "Search another title".green()); - let ln_number = string_input("Enter an option: "); - if ln_number != "s" && ln_number.parse::().is_ok() { - let ln_number = ln_number.trim().to_string(); - let ln_number = ln_number.parse::().unwrap(); - let ln_url = &ln_urls[ln_number]; - let ln_url = ln_url.trim().to_string(); - _is_n = true; - print!("\x1B[2J\x1B[1;1H"); - return ln_url; - } else { - print!("invalid input"); - } - print!("\x1B[2J\x1B[1;1H"); - } - "".to_string() -} - -//gets the list of ln's from the html and returns it as a vector of the ln's name and href -fn get_ln_list(html: &str) -> Vec { - let re = Regex::new(r#"(?m)^\s*( = Vec::new(); - for cap in re.captures_iter(html) { - ln_list.push(cap.get(1).unwrap().as_str().trim().to_string()); - } - ln_list -} -//gets the titles of the ln's from the html and returns it as a vector of the ln's name -fn get_ln_titles(ln_list: &Vec) -> Vec { - let re = Regex::new(r#"(?m)^\s* = Vec::new(); - for ln in ln_list { - for cap in re.captures_iter(ln) { - ln_title.push(cap.get(1).unwrap().as_str().to_string()); - } - } - ln_title -} - -//gets the urls of the ln's from the html and returns it as a vector of the ln's href -fn get_ln_urls(ln_list: &Vec) -> Vec { - let re = Regex::new(r#"(?m)^\s* = Vec::new(); - for ln in ln_list { - for cap in re.captures_iter(ln) { - ln_url.push(cap.get(1).unwrap().as_str().to_string()); - } - } - ln_url -} - -//gets the chapter titles from the html and returns it as a vector of the chapter's name -pub fn get_ln_chapters(html: &str) -> Vec { - let re = Regex::new(r#"title=(.*?)>"#).unwrap(); - let mut ln_list: Vec = Vec::new(); - for cap in re.captures_iter(html) { - ln_list.push(cap.get(1).unwrap().as_str().trim().to_string()); - } - ln_list = remove_after_dash(&ln_list); - ln_list -} diff --git a/src/main.rs b/src/main.rs index 5f67e5e..b83b4f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,8 @@ mod ln; use anime::anime::anime_ui; use colored::Colorize; -use ln::search::search_ln; -use ln::{ln::ln_read, scraper::get_ln_next_page}; +//use ln::ui::ln_ui; +use ln::ln::ln_ui; use crate::anime::{ player::open_video, @@ -13,14 +13,12 @@ use crate::anime::{ trackers::*, }; use crate::helpers::take_input::{int_input, string_input}; -use crate::ln::{menu::chapter_selector, open_text::open_bat, scraper::get_full_text}; fn main() { let mut help = false; let mut anime = false; let mut ln = false; let mut chapter: u32 = 0; //let search = option string - let mut search = String::new(); let mut count = 0; for arg in std::env::args() { if arg == "--help" || arg == "-h" { @@ -28,21 +26,9 @@ fn main() { } if arg == "--anime" || arg == "-a" { anime = true; - //look at the next argument and see if it is a search term - if let Some(arg) = std::env::args().nth(count + 1) { - if !arg.starts_with("-") { - search = arg; - } - } } if arg == "--ln" || arg == "-l" { ln = true; - //if let Some(arg) = std::env::args().nth(count + 1) and that arg does not start with a '-' set search to that arg - if let Some(arg) = std::env::args().nth(count + 1) { - if !arg.starts_with("-") { - search = arg; - } - } } if arg == "--chapter" || arg == "-c" { if let Some(arg) = std::env::args().nth(count + 1) { @@ -74,7 +60,8 @@ fn main() { std::process::exit(0); } if ln == true { - ln_read(&search, chapter); + //ln_read(&search, chapter); + _ = ln_ui(chapter); } else if anime == true { //anime_stream(search, episode, resume); _ = anime_ui(); @@ -83,36 +70,11 @@ fn main() { } } -fn page_selector(ln_id: &str, selected_page: u32) -> String { - get_ln_next_page(ln_id, &selected_page.to_string()) -} - fn print_help() { println!("anime:\t\t{}", format_args!("{}", "-a --anime".red())); - println!( - "{}", - "after this^^^ argument you can enter a search term".green() - ); - println!("{}", "for exaple kami -a \"one piece\""); //print blank line println!(""); - println!("episode:\t{}", format_args!("{}", "-e --episode".red())); - println!( - "{}", - "after this^^^ argument you can enter a chapter number".green() - ); - println!("{}", "for exaple kami -c 200"); - //print blank line - println!(""); - println!("resume:\t\t{}", format_args!("{}", "-r --resume".red())); - println!("{}", "only works with anime".green()); - println!(""); println!("light novel:\t{}", format_args!("{}", "-l --ln".red())); - println!( - "{}", - "after this^^^ argument you can enter a search term".green() - ); - println!("{}", "for exaple kami -l \"one piece\""); //print blank line println!(""); println!("chapter:\t{}", format_args!("{}", "-c --chapter".red()));