From 634643657e91ca0589dc2d3f330ef49bd5f5539c Mon Sep 17 00:00:00 2001 From: Zastian Pretorius Date: Fri, 27 Jan 2023 21:46:53 +0000 Subject: [PATCH 1/6] implementing consument api as a test --- src/anime/anime.rs | 58 +++++++++++------ src/anime/scraper.rs | 141 +++++++++++++++++++++++------------------- src/anime/trackers.rs | 48 -------------- 3 files changed, 115 insertions(+), 132 deletions(-) diff --git a/src/anime/anime.rs b/src/anime/anime.rs index e12bb50..cc2bcd7 100644 --- a/src/anime/anime.rs +++ b/src/anime/anime.rs @@ -1,8 +1,8 @@ use crate::{ - get_an_history, get_an_progress, get_anime_id, get_user_anime_progress, update_anime_progress, + get_an_history, get_an_progress, get_user_anime_progress, update_anime_progress, write_an_progress, }; -use crate::{get_anime_link, get_animes, get_image}; +use crate::{get_episode_link, get_episodes, get_image, search_anime}; use crate::{open_cast, open_video}; use crossterm::{ @@ -22,8 +22,6 @@ use tui::{ use unicode_width::UnicodeWidthStr; use viuer::{print_from_file, terminal_size, Config}; -use super::scraper::get_anime_info; - enum InputMode { Normal, Editing, @@ -90,8 +88,8 @@ struct App { input_mode: InputMode, /// History of recorded messages messages: StatefulList, + episodes: (Vec, Vec), title: String, - link: String, ep: u64, progress: i32, anime_id: i32, @@ -108,8 +106,8 @@ impl<'a> App { image: String::new(), input_mode: InputMode::Normal, messages: StatefulList::with_items(Vec::new()), + episodes: (Vec::new(), Vec::new()), title: String::new(), - link: String::new(), ep: 0, progress: 0, anime_id: 0, @@ -257,9 +255,13 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( app.progress = 0; let selected = app.messages.state.selected(); app.title = app.messages.items[selected.unwrap()].clone(); - app.link = app.animes.0[selected.unwrap()].clone(); - let anime_info = get_anime_info(&app.animes.0[selected.unwrap()]); - app.anime_id = get_anime_id(anime_info.0); + app.anime_id = app.animes.0[selected.unwrap()] + .clone() + .parse::() + .unwrap(); + app.episodes = get_episodes( + &app.animes.0[selected.unwrap()].parse::().unwrap(), + ); app.messages.items.clear(); if app.token == "local" || app.anime_id == 0 { app.progress = get_an_progress(&app.title) as i32; @@ -269,8 +271,8 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( get_user_anime_progress(app.anime_id, app.token.as_str()); app.messages.state.select(Some(app.progress as usize)); } - if anime_info.1 == 1 { - let link = get_anime_link(&app.link, 1); + if app.episodes.0.len() == 1 { + let link = get_episode_link(&app.episodes.1[0]); if !app.cast.0 { open_video((link, format!("{} Episode 1", &app.title))); } else { @@ -282,14 +284,24 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( let selected = app.messages.state.selected(); let image_url = app.animes.2[selected.unwrap()].clone(); if app.token == "local" || app.anime_id == 0 { - write_an_progress((&app.title, &app.link, &image_url), &1); + write_an_progress( + (&app.title, &app.anime_id.to_string(), &image_url), + &1, + ); } else { update_anime_progress(app.anime_id, 1, app.token.as_str()); - write_an_progress((&app.title, &app.link, &image_url), &1); + write_an_progress( + (&app.title, &app.anime_id.to_string(), &image_url), + &1, + ); } } else { - for ep in 1..anime_info.1 + 1 { - app.messages.push(format!("Episode {}", ep)); + for ep in 1..app.episodes.1.len() + 1 { + app.messages.push(format!( + "Episode {}: {}", + ep, + app.episodes.0[ep - 1] + )); } ep_select = true; } @@ -301,9 +313,11 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( .nth(selected.unwrap()) .unwrap() .replace("Episode ", "") + .split(":") + .collect::>()[0] .parse::() .unwrap(); - let link = get_anime_link(&app.link, app.ep); + let link = get_episode_link(&app.episodes.1[app.ep as usize - 1]); if !app.cast.0 { open_video((link, format!("{} Episode {}", &app.title, app.ep))); } else { @@ -315,14 +329,20 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( let image_url = &app.image; if app.ep > app.progress as u64 { if app.token == "local" || app.anime_id == 0 { - write_an_progress((&app.title, &app.link, &image_url), &app.ep); + write_an_progress( + (&app.title, &app.anime_id.to_string(), &image_url), + &app.ep, + ); } else { update_anime_progress( app.anime_id, app.ep as usize, app.token.as_str(), ); - write_an_progress((&app.title, &app.link, &image_url), &app.ep); + write_an_progress( + (&app.title, &app.anime_id.to_string(), &image_url), + &app.ep, + ); } app.progress = app.ep as i32; } @@ -333,7 +353,7 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( InputMode::Editing => match key.code { KeyCode::Enter => { //push app.input into app.messages with ' - app.animes = get_animes(app.input.drain(..).collect()); + app.animes = search_anime(app.input.drain(..).collect()); app.messages.items.clear(); for anime in &app.animes.1 { app.messages.push(anime.to_string()); diff --git a/src/anime/scraper.rs b/src/anime/scraper.rs index 2f43f8d..ce83e2a 100644 --- a/src/anime/scraper.rs +++ b/src/anime/scraper.rs @@ -1,14 +1,21 @@ use isahc::config::Configurable; use isahc::{ReadResponseExt, Request, RequestExt}; -use regex::Regex; use std::fs::File; use std::io::prelude::*; //use serde_json::json; -pub fn get_anime_html(url: &str) -> String { +pub fn search_anime(query: String) -> (Vec, Vec, Vec) { let req = Request::builder() - .uri(url) + .uri(format!( + "https://api.consumet.org/meta/anilist/{}", + query + .replace(" ", "%20") + .replace(":", "%3A") + .replace("!", "%21") + .replace("?", "%3F") + .replace("'", "%27") + )) .redirect_policy(isahc::config::RedirectPolicy::Follow) .header( "user-agent", @@ -16,74 +23,78 @@ pub fn get_anime_html(url: &str) -> String { ) .body(()) .unwrap(); - req.send().unwrap().text().unwrap() -} - -pub fn get_post(id: &str) -> String { - let resp = Request::builder() - .method("POST") - .uri("https://yugen.to/api/embed/") - .header("x-requested-with", "XMLHttpRequest") - .body(id) - .unwrap() - .send() - .unwrap() - .text(); - let resp: String = resp.as_ref().unwrap().to_string(); - resp -} + let json = req.send().unwrap().text().unwrap(); + let json: serde_json::Value = serde_json::from_str(&json).unwrap(); + let mut titles = Vec::new(); + let mut ids = Vec::new(); + let mut images = Vec::new(); + for i in 0..json["results"].as_array().unwrap().len() { + titles.push( + json["results"][i]["title"]["userPreferred"] + .as_str() + .unwrap() + .to_string(), + ); -pub fn get_animes(query: String) -> (Vec, Vec, Vec) { - let query = query.replace(" ", "+"); - let html = get_anime_html(&format!("https://yugen.to/search/?q={}", query)); - let re = Regex::new(r#"href="(/anime[^"]*)""#).unwrap(); - let mut animes_links = Vec::new(); - for cap in re.captures_iter(&html) { - animes_links.push(cap[1].to_string()); + ids.push(json["results"][i]["id"].as_str().unwrap().to_string()); + //convert ids to i32 + images.push(json["results"][i]["image"].as_str().unwrap().to_string()); } - let re = Regex::new(r#"/" title="([^"]*)""#).unwrap(); - let mut animes_names = Vec::new(); - for cap in re.captures_iter(&html) { - animes_names.push(cap[1].to_string()); - } - let re = Regex::new(r#"data-src="([^"]*)"#).unwrap(); - let mut animes_images = Vec::new(); - for cap in re.captures_iter(&html) { - animes_images.push(cap[1].to_string()); - } - (animes_links, animes_names, animes_images) + (ids, titles, images) } -pub fn get_anime_info(url: &str) -> (i32, u16) { - let url = format!("https://yugen.to{}watch", url); - let html = get_anime_html(&url); - //print html and exit - let re = Regex::new(r#""mal_id":(\d*)"#).unwrap(); - let mal_id = re.captures(&html).unwrap()[1].parse().unwrap(); - let re = - Regex::new(r#"Episodes(\d*)"#) - .unwrap(); - let episodes = re.captures(&html).unwrap()[1].parse().unwrap(); - (mal_id, episodes) +pub fn get_episodes(id: &i32) -> (Vec, Vec) { + let req = Request::builder() + .uri(format!( + "https://api.consumet.org/meta/anilist/info/{}?provider=gogoanime", + id + )) + .redirect_policy(isahc::config::RedirectPolicy::Follow) + .header( + "user-agent", + "Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0", + ) + .body(()) + .unwrap(); + let json = req.send().unwrap().text().unwrap(); + let json: serde_json::Value = serde_json::from_str(&json).unwrap(); + let mut titles = Vec::new(); + let mut ids = Vec::new(); + for i in 0..json["episodes"].as_array().unwrap().len() { + titles.push(json["episodes"][i]["title"].as_str().unwrap().to_string()); + ids.push(json["episodes"][i]["id"].as_str().unwrap().to_string()); + } + (titles, ids) } -pub fn get_anime_link(url: &str, episode: u64) -> String { - let url = &format!( - "https://yugen.to/watch{}{}/", - url.replace("/anime", ""), - episode - ); - let html = get_anime_html(url); - let re = Regex::new(r#"/e/([^/]*)"#).unwrap(); - let capture = re.captures(&html).unwrap(); - let id = &capture[1]; - let id = format!("id={}%3D&ac=0", id); - let json = get_post(&id); - let re = Regex::new(r#"hls": \["(.*)","#).unwrap(); - let capture = re.captures(&json).unwrap(); - let link = &capture[1]; - //return the link - link.to_string() +pub fn get_episode_link(ep_id: &str) -> String { + let req = Request::builder() + .uri(format!( + "https://api.consumet.org/meta/anilist/watch/{}?provider=gogoanime", + ep_id + )) + .redirect_policy(isahc::config::RedirectPolicy::Follow) + .header( + "user-agent", + "Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0", + ) + .body(()) + .unwrap(); + let json = req.send().unwrap().text().unwrap(); + let json: serde_json::Value = serde_json::from_str(&json).unwrap(); + let url = ""; + std::fs::write("test.json", json.to_string()).unwrap(); + for i in 0..json["sources"].as_array().unwrap().len() { + //return json["sources"][i]["url"].as_str().unwrap().to_string(); where json["sources"][i]["quality"].as_str().unwrap().contains("1080") + if json["sources"][i]["quality"] + .as_str() + .unwrap() + .contains("1080") + { + return json["sources"][i]["url"].as_str().unwrap().to_string(); + } + } + url.to_string() } pub fn get_image(url: &str, path: &str) { diff --git a/src/anime/trackers.rs b/src/anime/trackers.rs index 15f8577..33eb954 100644 --- a/src/anime/trackers.rs +++ b/src/anime/trackers.rs @@ -40,54 +40,6 @@ pub fn get_token() -> String { token } -pub fn get_anime_id(mal_id: i32) -> i32 { - const QUERY: &str = " -query ($id: Int, $search: Int) { - Media (id: $id, idMal: $search, type: ANIME) { - id - title { - native - romaji - english - } - } -} -"; - let json = json!({ - "query": QUERY, - "variables": { - "search": mal_id - } - }); - let resp = Request::builder() - .method("POST") - .uri("https://graphql.anilist.co/") - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .body(json.to_string()) - .unwrap() - .send() - .unwrap() - .text(); - let regex = regex::Regex::new(r#"id":(.*?),"#).unwrap(); - let resp: String = resp.as_ref().unwrap().to_string(); - //if error let id = 0 - let id = match regex.captures(&resp) { - Some(captures) => captures[1].parse::().unwrap(), - None => 0, - }; - - // let id = regex - // .captures(&resp) - // .unwrap() - // .get(1) - // .unwrap() - // .as_str() - // .parse::() - // .unwrap(); - id -} - //get the user id from the token fn get_user_id(token: &str) -> i32 { const QUERY: &str = "query { From edbebf4b4ec876ed8c803305aa25946595fb4e80 Mon Sep 17 00:00:00 2001 From: Zastian Pretorius Date: Tue, 31 Jan 2023 16:47:08 +0000 Subject: [PATCH 2/6] added provider switching and soft subs --- src/anime/anime.rs | 24 ++++++++++++++++++------ src/anime/player.rs | 32 ++++++++++++++++++++++---------- src/anime/scraper.rs | 34 +++++++++++++++++++++++++--------- src/main.rs | 16 ++++++++-------- 4 files changed, 73 insertions(+), 33 deletions(-) diff --git a/src/anime/anime.rs b/src/anime/anime.rs index cc2bcd7..6f1c19e 100644 --- a/src/anime/anime.rs +++ b/src/anime/anime.rs @@ -261,6 +261,7 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( .unwrap(); app.episodes = get_episodes( &app.animes.0[selected.unwrap()].parse::().unwrap(), + &app.provider, ); app.messages.items.clear(); if app.token == "local" || app.anime_id == 0 { @@ -272,12 +273,16 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( app.messages.state.select(Some(app.progress as usize)); } if app.episodes.0.len() == 1 { - let link = get_episode_link(&app.episodes.1[0]); + let link = get_episode_link(&app.episodes.1[0], &app.provider); if !app.cast.0 { - open_video((link, format!("{} Episode 1", &app.title))); + open_video(( + link.0, + format!("{} Episode 1", &app.title), + link.1, + )); } else { open_cast( - (link, format!("{} Episode 1", &app.title)), + (link.1, format!("{} Episode 1", &app.title)), &app.cast.1, ) } @@ -317,12 +322,19 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( .collect::>()[0] .parse::() .unwrap(); - let link = get_episode_link(&app.episodes.1[app.ep as usize - 1]); + let link = get_episode_link( + &app.episodes.1[app.ep as usize - 1], + &app.provider, + ); if !app.cast.0 { - open_video((link, format!("{} Episode {}", &app.title, app.ep))); + open_video(( + link.0, + format!("{} Episode {}", &app.title, app.ep), + link.1, + )); } else { open_cast( - (link, format!("{} Episode {}", &app.title, app.ep)), + (link.0, format!("{} Episode {}", &app.title, app.ep)), &app.cast.1, ) } diff --git a/src/anime/player.rs b/src/anime/player.rs index 665d777..0a3b6ce 100644 --- a/src/anime/player.rs +++ b/src/anime/player.rs @@ -8,16 +8,28 @@ use rust_cast::{ }; use std::str::FromStr; -pub fn open_video(link: (String, String)) { - let title = link.1; - let title = title.replace("-", " "); - let arg: String = format!("--force-media-title={}", title); - let _ = std::process::Command::new("mpv") - .arg(link.0) - .arg(arg) - .output() - .expect("failed to open mpv"); - +pub fn open_video(link: (String, String, String)) { + if link.2 == "null" { + let title = link.1; + let title = title.replace("-", " "); + let arg: String = format!("--force-media-title={}", title); + let _ = std::process::Command::new("mpv") + .arg(link.0) + .arg(arg) + .output() + .expect("failed to open mpv"); + } else { + let title = link.1; + let title = title.replace("-", " "); + let arg1: String = format!("--force-media-title={}", title); + let arg2: String = format!("--sub-files={}", link.2); + let _ = std::process::Command::new("mpv") + .arg(link.0) + .arg(arg1) + .arg(arg2) + .output() + .expect("failed to open mpv"); + } // clear terminal } diff --git a/src/anime/scraper.rs b/src/anime/scraper.rs index ce83e2a..c81e4da 100644 --- a/src/anime/scraper.rs +++ b/src/anime/scraper.rs @@ -43,11 +43,11 @@ pub fn search_anime(query: String) -> (Vec, Vec, Vec) { (ids, titles, images) } -pub fn get_episodes(id: &i32) -> (Vec, Vec) { +pub fn get_episodes(id: &i32, provider: &str) -> (Vec, Vec) { let req = Request::builder() .uri(format!( - "https://api.consumet.org/meta/anilist/info/{}?provider=gogoanime", - id + "https://api.consumet.org/meta/anilist/info/{}?provider={}", + id, provider )) .redirect_policy(isahc::config::RedirectPolicy::Follow) .header( @@ -67,11 +67,11 @@ pub fn get_episodes(id: &i32) -> (Vec, Vec) { (titles, ids) } -pub fn get_episode_link(ep_id: &str) -> String { +pub fn get_episode_link(ep_id: &str, provider: &str) -> (String, String) { let req = Request::builder() .uri(format!( - "https://api.consumet.org/meta/anilist/watch/{}?provider=gogoanime", - ep_id + "https://api.consumet.org/meta/anilist/watch/{}?provider={}", + ep_id, provider )) .redirect_policy(isahc::config::RedirectPolicy::Follow) .header( @@ -84,17 +84,33 @@ pub fn get_episode_link(ep_id: &str) -> String { let json: serde_json::Value = serde_json::from_str(&json).unwrap(); let url = ""; std::fs::write("test.json", json.to_string()).unwrap(); + let mut subtitle = String::new(); + let _error_vec = Vec::new(); + let sub_array = json["subtitles"].as_array().unwrap_or(&_error_vec); + for i in 0..sub_array.len() { + //set subtitle to lang = English + if json["subtitles"][i]["lang"].as_str().unwrap_or("null") == "English" { + subtitle = json["subtitles"][i]["url"] + .as_str() + .unwrap_or("null") + .to_string(); + // add \ before the first : in the url + subtitle = subtitle.replace(":", "\\:"); + } + } for i in 0..json["sources"].as_array().unwrap().len() { - //return json["sources"][i]["url"].as_str().unwrap().to_string(); where json["sources"][i]["quality"].as_str().unwrap().contains("1080") if json["sources"][i]["quality"] .as_str() .unwrap() .contains("1080") { - return json["sources"][i]["url"].as_str().unwrap().to_string(); + return ( + json["sources"][i]["url"].as_str().unwrap().to_string(), + subtitle, + ); } } - url.to_string() + (url.to_string(), subtitle) } pub fn get_image(url: &str, path: &str) { diff --git a/src/main.rs b/src/main.rs index d53215f..a010a16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,17 +29,17 @@ fn main() { if arg == "--provider" || arg == "-r" { if let Some(arg) = std::env::args().nth(count + 1) { //get the next argument and see if it is = to gogo of vrv - if arg == "vrv" { - provider = "vrv".to_string(); + if arg == "zoro" { + provider = "zoro".to_string(); count += 1; } else if arg == "gogo" { - provider = "gogo".to_string(); + provider = "gogoanime".to_string(); count += 1; } else { - provider = "gogo".to_string(); + provider = "zoro".to_string(); } } else { - provider = "vrv".to_string(); + provider = "zoro".to_string(); } } if arg == "--cast" || arg == "-C" { @@ -122,13 +122,13 @@ fn print_help() { ); println!( "if no provider is entered it will default to {}", - "vrv".green() + "gogo".green() ); println!( "if the -r argument is not used it will default to {}", - "gogo".green() + "zoro".green() ); - println!("the providers are {} or {}", "gogo".green(), "vrv".green()); + println!("the providers are {} or {}", "gogo".green(), "zoro".green()); println!(""); println!("help:\t\t{}", format_args!("{}", "-h --help".red())); //kill the program From 6fadcadba10ab9e132d54ee4f629944ff63b8b02 Mon Sep 17 00:00:00 2001 From: Zastian Pretorius Date: Wed, 1 Feb 2023 20:18:36 +0000 Subject: [PATCH 3/6] some scraper stuff --- src/anime/scraper.rs | 17 +++++++++-------- src/main.rs | 6 +++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/anime/scraper.rs b/src/anime/scraper.rs index c81e4da..6cc4577 100644 --- a/src/anime/scraper.rs +++ b/src/anime/scraper.rs @@ -82,7 +82,7 @@ pub fn get_episode_link(ep_id: &str, provider: &str) -> (String, String) { .unwrap(); let json = req.send().unwrap().text().unwrap(); let json: serde_json::Value = serde_json::from_str(&json).unwrap(); - let url = ""; + let mut url = String::new(); std::fs::write("test.json", json.to_string()).unwrap(); let mut subtitle = String::new(); let _error_vec = Vec::new(); @@ -98,16 +98,17 @@ pub fn get_episode_link(ep_id: &str, provider: &str) -> (String, String) { subtitle = subtitle.replace(":", "\\:"); } } + let mut highest_quality = 0; for i in 0..json["sources"].as_array().unwrap().len() { - if json["sources"][i]["quality"] + let quality = json["sources"][i]["quality"] .as_str() .unwrap() - .contains("1080") - { - return ( - json["sources"][i]["url"].as_str().unwrap().to_string(), - subtitle, - ); + .replace("p", "") + .parse::() + .unwrap_or(0); + if quality > highest_quality { + highest_quality = quality; + url = json["sources"][i]["url"].as_str().unwrap().to_string(); } } (url.to_string(), subtitle) diff --git a/src/main.rs b/src/main.rs index a010a16..26341ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ fn main() { let mut chapter: u32 = 0; //let search = option string let mut count = 0; - let mut provider: String = "gogo".to_string(); + let mut provider: String = "zoro".to_string(); let mut cast = (false, "0".to_string()); for arg in std::env::args() { if arg == "--help" || arg == "-h" { @@ -36,10 +36,10 @@ fn main() { provider = "gogoanime".to_string(); count += 1; } else { - provider = "zoro".to_string(); + provider = "gogo".to_string(); } } else { - provider = "zoro".to_string(); + provider = "gogo".to_string(); } } if arg == "--cast" || arg == "-C" { From 2b4c1079ac20d866fafb7e504b2c944a5c918fdc Mon Sep 17 00:00:00 2001 From: newbee1905 Date: Thu, 2 Feb 2023 22:26:18 +0700 Subject: [PATCH 4/6] refactor: refactor code layout by moving ui to another crates --- src/anime/anime.rs | 431 +------------------------------------------- src/ln/ln.rs | 360 +----------------------------------- src/main.rs | 1 + src/ui/app/anime.rs | 390 +++++++++++++++++++++++++++++++++++++++ src/ui/app/app.rs | 8 + src/ui/app/ln.rs | 307 +++++++++++++++++++++++++++++++ src/ui/app/mod.rs | 3 + src/ui/input.rs | 4 + src/ui/list.rs | 61 +++++++ src/ui/mod.rs | 3 + 10 files changed, 789 insertions(+), 779 deletions(-) create mode 100644 src/ui/app/anime.rs create mode 100644 src/ui/app/app.rs create mode 100644 src/ui/app/ln.rs create mode 100644 src/ui/app/mod.rs create mode 100644 src/ui/input.rs create mode 100644 src/ui/list.rs create mode 100644 src/ui/mod.rs diff --git a/src/anime/anime.rs b/src/anime/anime.rs index e12bb50..6f182d8 100644 --- a/src/anime/anime.rs +++ b/src/anime/anime.rs @@ -1,124 +1,12 @@ -use crate::{ - get_an_history, get_an_progress, get_anime_id, get_user_anime_progress, update_anime_progress, - write_an_progress, -}; -use crate::{get_anime_link, get_animes, get_image}; -use crate::{open_cast, open_video}; - use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + event::{DisableMouseCapture, EnableMouseCapture}, 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; -use viuer::{print_from_file, terminal_size, Config}; - -use super::scraper::get_anime_info; - -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, - animes: (Vec, Vec, Vec), - image: String, - /// Current input mode - input_mode: InputMode, - /// History of recorded messages - messages: StatefulList, - title: String, - link: String, - ep: u64, - progress: i32, - anime_id: i32, - token: String, - provider: String, - cast: (bool, String), -} +use tui::{backend::CrosstermBackend, Terminal}; -impl<'a> App { - fn default() -> App { - App { - input: String::new(), - animes: get_an_history(), - image: String::new(), - input_mode: InputMode::Normal, - messages: StatefulList::with_items(Vec::new()), - title: String::new(), - link: String::new(), - ep: 0, - progress: 0, - anime_id: 0, - token: String::new(), - provider: String::new(), - cast: (false, "0".to_string()), - } - } -} +use crate::ui::app::{anime::App, app::KamiApp}; pub fn anime_ui( token: String, @@ -133,11 +21,11 @@ pub fn anime_ui( let mut terminal = Terminal::new(backend)?; // create app and run it - let mut app = App::default(); + let mut app = App::new(); app.token = token; app.provider = provider; app.cast = cast; - let res = run_app(&mut terminal, app); + let res = app.run(&mut terminal); // restore terminal disable_raw_mode()?; @@ -154,312 +42,3 @@ pub fn anime_ui( Ok(()) } - -fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { - let mut ep_select = false; - fn change_image(app: &App) { - //save as f32 - let (width, height) = terminal_size().to_owned(); - let width = width as f32; - let height = height as f32; - let sixel_support = viuer::is_sixel_supported(); - let config = match sixel_support { - true => Config { - x: ((width / 2.0) + 1.0).round() as u16, - y: 2, - width: Some((width / 1.3).round() as u32), - height: Some((height * 1.5) as u32), - restore_cursor: true, - ..Default::default() - }, - false => Config { - x: ((width / 2.0) + 1.0).round() as u16, - y: 2, - width: Some(((width / 2.0) - 4.0).round() as u32), - height: Some((height / 1.3).round() as u32), - restore_cursor: true, - ..Default::default() - }, - }; - - let config_path = dirs::config_dir().unwrap().join("kami"); - let image_path = config_path.join("tmp.jpg"); - get_image(&app.image, &image_path.to_str().unwrap()); - print_from_file(image_path, &config).expect("Image printing failed."); - } - app.messages.items.clear(); - for anime in &app.animes.1 { - app.messages.push(anime.to_string()); - } - app.input_mode = InputMode::Normal; - - 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('i') => { - app.input_mode = InputMode::Editing; - } - KeyCode::Char('q') => { - return Ok(()); - } - KeyCode::Left => app.messages.unselect(), - KeyCode::Char('h') => app.messages.unselect(), - KeyCode::Down => match ep_select { - true => { - app.messages.next(); - } - false => { - app.messages.next(); - let selected = app.messages.state.selected(); - app.image = app.animes.2[selected.unwrap()].clone(); - change_image(&app); - } - }, - KeyCode::Char('j') => match ep_select { - true => { - app.messages.next(); - } - false => { - app.messages.next(); - let selected = app.messages.state.selected(); - app.image = app.animes.2[selected.unwrap()].clone(); - change_image(&app); - } - }, - KeyCode::Up => match ep_select { - true => { - app.messages.previous(); - } - false => { - app.messages.previous(); - let selected = app.messages.state.selected(); - app.image = app.animes.2[selected.unwrap()].clone(); - change_image(&app); - } - }, - KeyCode::Char('k') => match ep_select { - true => { - app.messages.previous(); - } - false => { - app.messages.previous(); - let selected = app.messages.state.selected(); - app.image = app.animes.2[selected.unwrap()].clone(); - change_image(&app); - } - }, - //if KeyCode::Enter => { - KeyCode::Enter => { - if ep_select == false { - app.progress = 0; - let selected = app.messages.state.selected(); - app.title = app.messages.items[selected.unwrap()].clone(); - app.link = app.animes.0[selected.unwrap()].clone(); - let anime_info = get_anime_info(&app.animes.0[selected.unwrap()]); - app.anime_id = get_anime_id(anime_info.0); - app.messages.items.clear(); - if app.token == "local" || app.anime_id == 0 { - app.progress = get_an_progress(&app.title) as i32; - app.messages.state.select(Some(app.progress as usize)); - } else { - app.progress = - get_user_anime_progress(app.anime_id, app.token.as_str()); - app.messages.state.select(Some(app.progress as usize)); - } - if anime_info.1 == 1 { - let link = get_anime_link(&app.link, 1); - if !app.cast.0 { - open_video((link, format!("{} Episode 1", &app.title))); - } else { - open_cast( - (link, format!("{} Episode 1", &app.title)), - &app.cast.1, - ) - } - let selected = app.messages.state.selected(); - let image_url = app.animes.2[selected.unwrap()].clone(); - if app.token == "local" || app.anime_id == 0 { - write_an_progress((&app.title, &app.link, &image_url), &1); - } else { - update_anime_progress(app.anime_id, 1, app.token.as_str()); - write_an_progress((&app.title, &app.link, &image_url), &1); - } - } else { - for ep in 1..anime_info.1 + 1 { - 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 = get_anime_link(&app.link, app.ep); - if !app.cast.0 { - open_video((link, format!("{} Episode {}", &app.title, app.ep))); - } else { - open_cast( - (link, format!("{} Episode {}", &app.title, app.ep)), - &app.cast.1, - ) - } - let image_url = &app.image; - if app.ep > app.progress as u64 { - if app.token == "local" || app.anime_id == 0 { - write_an_progress((&app.title, &app.link, &image_url), &app.ep); - } else { - update_anime_progress( - app.anime_id, - app.ep as usize, - app.token.as_str(), - ); - write_an_progress((&app.title, &app.link, &image_url), &app.ep); - } - app.progress = app.ep as i32; - } - } - } - _ => {} - }, - InputMode::Editing => match key.code { - KeyCode::Enter => { - //push app.input into app.messages with ' - app.animes = get_animes(app.input.drain(..).collect()); - app.messages.items.clear(); - for anime in &app.animes.1 { - app.messages.push(anime.to_string()); - } - 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 top_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[0]); - - 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("i", 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") - .border_type(BorderType::Rounded), - ) - .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, top_chunks[0], &mut app.messages.state); - let block = Block::default() - .borders(Borders::ALL) - .title("info") - .border_type(BorderType::Rounded); - f.render_widget(block, top_chunks[1]); - - 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/ln.rs b/src/ln/ln.rs index 0cdd130..46efb99 100644 --- a/src/ln/ln.rs +++ b/src/ln/ln.rs @@ -1,117 +1,13 @@ -use crate::ln::open_text::{open_bat, open_glow}; -use crate::ln::scraper::*; use crate::ln::tracker::*; +use crate::ui::app::{app::KamiApp, ln::App}; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + event::{DisableMouseCapture, EnableMouseCapture}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use std::fs::File; -use std::io::Write; -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, - 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, - } - } -} +use std::{error::Error, io}; +use tui::{backend::CrosstermBackend, Terminal}; pub fn ln_ui(chapter: u32, reader: String) -> Result<(), Box> { // setup terminal @@ -123,14 +19,15 @@ pub fn ln_ui(chapter: u32, reader: String) -> Result<(), Box> { let mut terminal = Terminal::new(backend)?; // create app and run it - let mut app = App::default(); + let mut app = App::new(); + app.reader = reader; 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, &*reader); + let res = app.run(&mut terminal); // restore terminal disable_raw_mode()?; @@ -147,246 +44,3 @@ pub fn ln_ui(chapter: u32, reader: String) -> Result<(), Box> { Ok(()) } - -fn run_app(terminal: &mut Terminal, mut app: App, reader: &str) -> 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('i') => { - 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); - 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); - 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(); - if app.current_page_number == 1 { - let progress = get_ln_progress(&app.title); - app.current_page_number = progress.0; - app.messages.state.select(Some(progress.1)); - } - 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); - 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 _ = match reader { - "bat" => open_bat(), - "glow" => open_glow(), - &_ => todo!(), - }; - write_ln_progress( - &app.title, - &app.current_page_number, - &app.messages.state.selected().unwrap(), - ); - 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("i", 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/main.rs b/src/main.rs index ae5c9d5..6ce9871 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod anime; mod helpers; mod ln; +mod ui; use anime::anime::anime_ui; use colored::Colorize; diff --git a/src/ui/app/anime.rs b/src/ui/app/anime.rs new file mode 100644 index 0000000..4a4dc7d --- /dev/null +++ b/src/ui/app/anime.rs @@ -0,0 +1,390 @@ +use crate::ui::{app::app::KamiApp, input::InputMode, list::StatefulList}; + +use crate::anime::player::{open_cast, open_video}; +use crate::anime::scraper::{get_anime_info, get_anime_link, get_animes, get_image}; +use crate::anime::trackers::{ + get_an_history, get_an_progress, get_anime_id, get_user_anime_progress, update_anime_progress, + write_an_progress, +}; + +use crossterm::event::{self, Event, KeyCode}; +use std::io; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, + Frame, Terminal, +}; +use unicode_width::UnicodeWidthStr; +use viuer::{print_from_file, terminal_size, Config}; + +pub struct App { + /// Current value of the input box + input: String, + animes: (Vec, Vec, Vec), + image: String, + /// Current input mode + input_mode: InputMode, + /// History of recorded messages + messages: StatefulList, + title: String, + link: String, + ep: u64, + progress: i32, + anime_id: i32, + pub token: String, + pub provider: String, + pub cast: (bool, String), +} + +impl<'a> KamiApp for App { + fn new() -> Self { + App { + input: String::new(), + animes: get_an_history(), + image: String::new(), + input_mode: InputMode::Normal, + messages: StatefulList::with_items(Vec::new()), + title: String::new(), + link: String::new(), + ep: 0, + progress: 0, + anime_id: 0, + token: String::new(), + provider: String::new(), + cast: (false, "0".to_string()), + } + } + + fn run(&mut self, terminal: &mut Terminal) -> io::Result<()> { + let mut ep_select = false; + fn change_image(app: &App) { + //save as f32 + let (width, height) = terminal_size().to_owned(); + let width = width as f32; + let height = height as f32; + let sixel_support = viuer::is_sixel_supported(); + let config = match sixel_support { + true => Config { + x: ((width / 2.0) + 1.0).round() as u16, + y: 2, + width: Some((width / 1.3).round() as u32), + height: Some((height * 1.5) as u32), + restore_cursor: true, + ..Default::default() + }, + false => Config { + x: ((width / 2.0) + 1.0).round() as u16, + y: 2, + width: Some(((width / 2.0) - 4.0).round() as u32), + height: Some((height / 1.3).round() as u32), + restore_cursor: true, + ..Default::default() + }, + }; + + let config_path = dirs::config_dir().unwrap().join("kami"); + let image_path = config_path.join("tmp.jpg"); + get_image(&app.image, &image_path.to_str().unwrap()); + print_from_file(image_path, &config).expect("Image printing failed."); + } + self.messages.items.clear(); + for anime in &self.animes.1 { + self.messages.push(anime.to_string()); + } + self.input_mode = InputMode::Normal; + + loop { + terminal.draw(|f| self.ui(f))?; + + if let Event::Key(key) = event::read()? { + match self.input_mode { + InputMode::Normal => match key.code { + KeyCode::Char('i') => { + self.input_mode = InputMode::Editing; + } + KeyCode::Char('q') => { + return Ok(()); + } + KeyCode::Left => self.messages.unselect(), + KeyCode::Char('h') => self.messages.unselect(), + KeyCode::Char('g') => self.messages.begin(), + KeyCode::Char('G') => self.messages.end(), + KeyCode::Down => match ep_select { + true => { + self.messages.next(); + } + false => { + self.messages.next(); + let selected = self.messages.state.selected(); + self.image = self.animes.2[selected.unwrap()].clone(); + change_image(&self); + } + }, + KeyCode::Char('j') => match ep_select { + true => { + self.messages.next(); + } + false => { + self.messages.next(); + let selected = self.messages.state.selected(); + self.image = self.animes.2[selected.unwrap()].clone(); + change_image(&self); + } + }, + KeyCode::Up => match ep_select { + true => { + self.messages.previous(); + } + false => { + self.messages.previous(); + let selected = self.messages.state.selected(); + self.image = self.animes.2[selected.unwrap()].clone(); + change_image(&self); + } + }, + KeyCode::Char('k') => match ep_select { + true => { + self.messages.previous(); + } + false => { + self.messages.previous(); + let selected = self.messages.state.selected(); + self.image = self.animes.2[selected.unwrap()].clone(); + change_image(&self); + } + }, + //if KeyCode::Enter => { + KeyCode::Enter => { + if ep_select == false { + self.progress = 0; + let selected = self.messages.state.selected(); + self.title = self.messages.items[selected.unwrap()].clone(); + self.link = self.animes.0[selected.unwrap()].clone(); + let anime_info = get_anime_info(&self.animes.0[selected.unwrap()]); + self.anime_id = get_anime_id(anime_info.0); + self.messages.items.clear(); + if self.token == "local" || self.anime_id == 0 { + self.progress = get_an_progress(&self.title) as i32; + self.messages.state.select(Some(self.progress as usize)); + } else { + self.progress = + get_user_anime_progress(self.anime_id, self.token.as_str()); + self.messages.state.select(Some(self.progress as usize)); + } + if anime_info.1 == 1 { + let link = get_anime_link(&self.link, 1); + if !self.cast.0 { + open_video((link, format!("{} Episode 1", &self.title))); + } else { + open_cast( + (link, format!("{} Episode 1", &self.title)), + &self.cast.1, + ) + } + let selected = self.messages.state.selected(); + let image_url = self.animes.2[selected.unwrap()].clone(); + if self.token == "local" || self.anime_id == 0 { + write_an_progress( + (&self.title, &self.link, &image_url), + &1, + ); + } else { + update_anime_progress( + self.anime_id, + 1, + self.token.as_str(), + ); + write_an_progress( + (&self.title, &self.link, &image_url), + &1, + ); + } + } else { + for ep in 1..anime_info.1 + 1 { + self.messages.push(format!("Episode {}", ep)); + } + ep_select = true; + } + } else { + let selected = self.messages.state.selected(); + self.ep = self + .messages + .iter() + .nth(selected.unwrap()) + .unwrap() + .replace("Episode ", "") + .parse::() + .unwrap(); + let link = get_anime_link(&self.link, self.ep); + if !self.cast.0 { + open_video(( + link, + format!("{} Episode {}", &self.title, self.ep), + )); + } else { + open_cast( + (link, format!("{} Episode {}", &self.title, self.ep)), + &self.cast.1, + ) + } + let image_url = &self.image; + if self.ep > self.progress as u64 { + if self.token == "local" || self.anime_id == 0 { + write_an_progress( + (&self.title, &self.link, &image_url), + &self.ep, + ); + } else { + update_anime_progress( + self.anime_id, + self.ep as usize, + self.token.as_str(), + ); + write_an_progress( + (&self.title, &self.link, &image_url), + &self.ep, + ); + } + self.progress = self.ep as i32; + } + } + } + _ => {} + }, + InputMode::Editing => match key.code { + KeyCode::Enter => { + //push self.input into self.messages with ' + self.animes = get_animes(self.input.drain(..).collect()); + self.messages.items.clear(); + for anime in &self.animes.1 { + self.messages.push(anime.to_string()); + } + ep_select = false; + self.input_mode = InputMode::Normal; + } + KeyCode::Char(c) => { + self.input.push(c); + } + KeyCode::Backspace => { + self.input.pop(); + } + KeyCode::Esc => { + self.input_mode = InputMode::Normal; + } + _ => {} + }, + } + } + } + } + + fn ui(&mut self, f: &mut Frame) { + 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 top_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(chunks[0]); + + let (msg, style) = match self.input_mode { + InputMode::Normal => ( + vec![ + Span::raw("Press "), + Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("i", 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 = self + .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") + .border_type(BorderType::Rounded), + ) + .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, top_chunks[0], &mut self.messages.state); + let block = Block::default() + .borders(Borders::ALL) + .title("info") + .border_type(BorderType::Rounded); + f.render_widget(block, top_chunks[1]); + + 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(self.input.as_ref()) + .style(match self.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 self.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 + self.input.width() as u16 + 1, + // Move one line down, from the border to the input line + chunks[2].y + 1, + ) + } + } + } +} diff --git a/src/ui/app/app.rs b/src/ui/app/app.rs new file mode 100644 index 0000000..2926d54 --- /dev/null +++ b/src/ui/app/app.rs @@ -0,0 +1,8 @@ +use std::io; +use tui::{backend::Backend, Frame, Terminal}; + +pub trait KamiApp { + fn new() -> Self; + fn run(&mut self, terminal: &mut Terminal) -> io::Result<()>; + fn ui(&mut self, f: &mut Frame); +} diff --git a/src/ui/app/ln.rs b/src/ui/app/ln.rs new file mode 100644 index 0000000..679b109 --- /dev/null +++ b/src/ui/app/ln.rs @@ -0,0 +1,307 @@ +use crate::ui::{app::app::KamiApp, input::InputMode, list::StatefulList}; + +use crate::ln::open_text::{open_bat, open_glow}; +use crate::ln::scraper::*; +use crate::ln::tracker::*; +use crossterm::event::{self, Event, KeyCode}; +use std::fs::File; +use std::io; +use std::io::Write; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, + Frame, Terminal, +}; + +use unicode_width::UnicodeWidthStr; + +pub struct App { + input: String, // Input box's value + input_mode: InputMode, + messages: StatefulList, // History of recorded messages + ln_titles: Vec, + ln_links: Vec, + title: String, + ln_id: String, + ln_chapters: Vec, + ln_chapters_links: Vec, + last_page: String, + current_page: String, + pub current_page_number: u32, + pub reader: String, +} + +impl<'a> KamiApp for App { + fn new() -> Self { + 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, + reader: "bat".to_string(), + } + } + + fn run(&mut self, terminal: &mut Terminal) -> io::Result<()> { + let mut chapter_select = false; + + loop { + terminal.draw(|f| self.ui(f))?; + if let Event::Key(key) = event::read()? { + match self.input_mode { + InputMode::Normal => match key.code { + KeyCode::Char('i') => { + self.input_mode = InputMode::Editing; + } + KeyCode::Char('q') => { + terminal.clear()?; + return Ok(()); + } + KeyCode::Left => self.messages.unselect(), + KeyCode::Char('g') => self.messages.begin(), + KeyCode::Char('G') => self.messages.end(), + KeyCode::Down => self.messages.next(), + KeyCode::Char('j') => self.messages.next(), + KeyCode::Up => self.messages.previous(), + KeyCode::Char('k') => self.messages.previous(), + + KeyCode::Char('h') => { + if self.current_page_number > 0 { + self.current_page_number -= 1; + } + + self.current_page = + get_ln_next_page(&self.ln_id, &self.current_page_number); + self.ln_chapters = get_ln_chapters(&self.current_page); + self.ln_chapters_links = get_ln_chapters_urls(&self.current_page); + self.messages.items.clear(); + for chapter in self.ln_chapters.iter() { + self.messages.push(chapter.to_string()); + } + } + + KeyCode::Char('l') => { + if self.current_page_number < self.last_page.parse::().unwrap() { + self.current_page_number += 1; + } + self.current_page = + get_ln_next_page(&self.ln_id, &self.current_page_number); + self.ln_chapters = get_ln_chapters(&self.current_page); + self.ln_chapters_links = get_ln_chapters_urls(&self.current_page); + self.messages.items.clear(); + for chapter in self.ln_chapters.iter() { + self.messages.push(chapter.to_string()); + } + } + //if KeyCode::Enter => { + KeyCode::Enter => { + if chapter_select == false { + let selected = self.messages.state.selected(); + self.title = self + .messages + .iter() + .nth(selected.unwrap()) + .unwrap() + .to_string(); + if self.current_page_number == 1 { + let progress = get_ln_progress(&self.title); + self.current_page_number = progress.0; + self.messages.state.select(Some(progress.1)); + } + let link = self.ln_links[selected.unwrap()].to_string(); + let html = get_html(&link); + self.ln_id = get_ln_id(&html).to_string(); + self.last_page = get_ln_last_page(&html); + self.current_page = get_ln_next_page( + &self.ln_id.to_string(), + &self.current_page_number, + ); + self.ln_chapters = get_ln_chapters(&self.current_page); + self.ln_chapters_links = get_ln_chapters_urls(&self.current_page); + self.messages.items.clear(); + for chapter in self.ln_chapters.iter() { + self.messages.push(chapter.to_string()); + } + chapter_select = true; + } else { + let selected = self.messages.state.selected(); + let chapter_url = + self.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 _ = match &*self.reader { + "bat" => open_bat(), + "glow" => open_glow(), + &_ => todo!(), + }; + write_ln_progress( + &self.title, + &self.current_page_number, + &self.messages.state.selected().unwrap(), + ); + terminal.clear()?; + } + } + _ => {} + }, + InputMode::Editing => match key.code { + KeyCode::Enter => { + //push self.input into self.messages with '1 + let search: String = self.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()); + self.ln_titles = get_ln_titles(&ln_list); + self.ln_links = get_ln_urls(&ln_list); + self.messages.items.clear(); + //remove index 0 of self.ln_titles and self.ln_links + self.ln_titles.remove(0); + self.ln_links.remove(0); + for ln in &self.ln_titles { + self.messages.push(ln.to_string()); + } + chapter_select = false; + self.input_mode = InputMode::Normal; + } + KeyCode::Char(c) => { + self.input.push(c); + } + KeyCode::Backspace => { + self.input.pop(); + } + KeyCode::Esc => { + self.input_mode = InputMode::Normal; + } + _ => {} + }, + } + } + } + } + + fn ui(&mut self, f: &mut Frame) { + 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 self.input_mode { + InputMode::Normal => ( + vec![ + Span::raw("Press "), + Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("i", 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 = self + .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 self.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(self.input.as_ref()) + .style(match self.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 self.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 + self.input.width() as u16 + 1, + // Move one line down, from the border to the input line + chunks[2].y + 1, + ) + } + } + } +} diff --git a/src/ui/app/mod.rs b/src/ui/app/mod.rs new file mode 100644 index 0000000..5bafc2c --- /dev/null +++ b/src/ui/app/mod.rs @@ -0,0 +1,3 @@ +pub mod anime; +pub mod app; +pub mod ln; diff --git a/src/ui/input.rs b/src/ui/input.rs new file mode 100644 index 0000000..4b11fc5 --- /dev/null +++ b/src/ui/input.rs @@ -0,0 +1,4 @@ +pub enum InputMode { + Normal, + Editing, +} diff --git a/src/ui/list.rs b/src/ui/list.rs new file mode 100644 index 0000000..a3d3690 --- /dev/null +++ b/src/ui/list.rs @@ -0,0 +1,61 @@ +use tui::widgets::ListState; + +pub struct StatefulList { + pub state: ListState, + pub items: Vec, +} + +impl StatefulList { + pub fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items, + } + } + + pub fn begin(&mut self) { + self.state.select(Some(0)) + } + + pub fn end(&mut self) { + self.state.select(Some(self.items.len() - 1)) + } + + pub 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)); + } + + pub 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)); + } + + pub fn unselect(&mut self) { + self.state.select(None); + } + pub fn push(&mut self, item: T) { + self.items.push(item); + } + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..0e36f36 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,3 @@ +pub mod app; +pub mod input; +pub mod list; From 1ad8e8d12ecb7bf4050231a22cbe64ecb1bcbb6c Mon Sep 17 00:00:00 2001 From: Zastian Pretorius Date: Fri, 3 Feb 2023 09:25:54 +0000 Subject: [PATCH 5/6] fixed the murge --- src/main.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9332a77..e3ea630 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ fn main() { let mut chapter: u32 = 0; //let search = option string let mut count = 0; - let mut provider: String = "gogo".to_string(); + let mut provider: String = "zoro".to_string(); let mut reader: String = "bat".to_string(); let mut cast = (false, "0".to_string()); for arg in std::env::args() { @@ -28,11 +28,11 @@ fn main() { if let Some(arg) = std::env::args().nth(count + 1) { //get the next argument and see if it is = to gogo of vrv match arg.as_str() { - "zoro" | "gogoanime" => { + "zoro" | "gogo" => { provider = arg; count += 1; } - &_ => provider = "gogoanime".to_string(), + &_ => provider = "gogo".to_string(), } } else { provider = "zoro".to_string(); @@ -48,14 +48,9 @@ fn main() { } &_ => reader = "bat".to_string(), } - } else { provider = "glow".to_string(); } - - } else { - provider = "gogo".to_string(); - } "--cast" | "-C" => { if let Some(arg) = std::env::args().nth(count + 1) { @@ -142,7 +137,7 @@ fn print_help() { "if the -r argument is not used it will default to {}", "zoro".green() ); - println!("the providers are {} or {}", "gogoanime".green(), "zoro".green()); + println!("the providers are {} or {}", "gogo".green(), "zoro".green()); println!(""); println!("reader:\t\t{}", format_args!("{}", "-R --reader".red())); println!( From 3ebb06cc1bdeb1977b779ccca6e09fa8ae2cd5b9 Mon Sep 17 00:00:00 2001 From: newbee1905 Date: Sun, 5 Feb 2023 02:43:08 +0700 Subject: [PATCH 6/6] chore: remove unused variables and use --- src/main.rs | 2 +- src/ui/app/anime.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index d74d129..0b6bc48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use colored::Colorize; //use ln::ui::ln_ui; use ln::ln::ln_ui; -use crate::anime::{player::*, scraper::*, trackers::*}; +use crate::anime::trackers::*; use crate::get_token; use crate::helpers::take_input::{int_input, string_input}; fn main() { diff --git a/src/ui/app/anime.rs b/src/ui/app/anime.rs index 9329a85..5bdbca3 100644 --- a/src/ui/app/anime.rs +++ b/src/ui/app/anime.rs @@ -31,7 +31,6 @@ pub struct App { messages: StatefulList, episodes: (Vec, Vec), title: String, - link: String, ep: u64, progress: i32, anime_id: i32, @@ -83,7 +82,6 @@ impl<'a> KamiApp for App { messages: StatefulList::with_items(Vec::new()), episodes: (Vec::new(), Vec::new()), title: String::new(), - link: String::new(), ep: 0, progress: 0, anime_id: 0,