diff --git a/Cargo.lock b/Cargo.lock index 3f8c0ba..441a251 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "castaway" version = "0.1.2" @@ -111,6 +117,47 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi 0.3.9", +] + +[[package]] +name = "crossterm" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9f7409c70a38a56216480fba371ee460207dd8926ccf5b4160591759559170" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi 0.3.9", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "curl" version = "0.4.43" @@ -319,11 +366,14 @@ version = "0.5.1" dependencies = [ "base64", "colored", + "crossterm 0.24.0", "dirs", "isahc", "regex", "serde_json", "termsize", + "tui", + "unicode-width", ] [[package]] @@ -370,6 +420,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -397,6 +457,18 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "numtoa" version = "0.1.0" @@ -434,6 +506,29 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -565,6 +660,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.142" @@ -582,6 +683,36 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.6" @@ -599,6 +730,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + [[package]] name = "socket2" version = "0.4.4" @@ -723,6 +860,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "tui" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.23.2", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -744,6 +894,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "url" version = "2.2.2" diff --git a/Cargo.toml b/Cargo.toml index a96a541..0b5c891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,6 @@ base64 = "0.13" termsize = "0.1.6" dirs = "4.0" serde_json = "1.0.83" +tui = "0.18.0" +crossterm = "0.24.0" +unicode-width = "0.1.9" diff --git a/src/anime/anime.rs b/src/anime/anime.rs index 9e2f217..f8c9642 100644 --- a/src/anime/anime.rs +++ b/src/anime/anime.rs @@ -1,106 +1,316 @@ -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); + let title = app.title.replace("tv-", ""); + let title = title.replace("dub", ""); + app.anime_id = get_anime_id(&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/player.rs b/src/anime/player.rs index 284f24d..b63b7c3 100644 --- a/src/anime/player.rs +++ b/src/anime/player.rs @@ -9,5 +9,4 @@ pub fn open_video(link: (String, String)) { .expect("failed to open mpv"); // clear terminal - print!("\x1b[2J\x1b[1;1H"); } diff --git a/src/anime/scraper.rs b/src/anime/scraper.rs index f643c54..2825a11 100644 --- a/src/anime/scraper.rs +++ b/src/anime/scraper.rs @@ -31,7 +31,7 @@ pub fn get_ep_location(url: &str) -> String { location.to_str().unwrap().to_string() } -pub fn anime_names(query: &str) -> Vec { +pub fn anime_names(query: String) -> Vec { let url = format!("https://gogoanime.lu//search.html?keyword={}", query); //relpace all spaces with %20 let url = url.replace(' ', "%20"); 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/ln/ln.rs b/src/ln/ln.rs index c6e7a3a..aca5b79 100644 --- a/src/ln/ln.rs +++ b/src/ln/ln.rs @@ -1,37 +1,380 @@ -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, "), + Span::styled("h", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to go to the previous page, "), + Span::styled("l", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to go to the next page."), + ], + 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 2069c29..b83b4f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,10 @@ mod anime; mod helpers; mod ln; -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}; +//use ln::ui::ln_ui; +use ln::ln::ln_ui; use crate::anime::{ player::open_video, @@ -13,17 +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 mut episode: u32 = 0; - let mut resume = false; //let search = option string - let mut search = String::new(); let mut count = 0; for arg in std::env::args() { if arg == "--help" || arg == "-h" { @@ -31,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) { @@ -54,16 +37,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; } @@ -87,44 +60,21 @@ 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_stream(search, episode, resume); + _ = anime_ui(); } else { println!("Invalid argument"); } } -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()));