89 Commits

Author SHA1 Message Date
newbee1905
c2ca356940 Merge branch 'consumet' into clap 2023-02-06 02:21:30 +07:00
mrfluffy
676edb1d98 Merge pull request #27 from newbee1905/consumet
Suggestions about refactoring
2023-02-05 19:16:18 +00:00
newbee1905
21f81cb68d chore: remove test.json 2023-02-05 03:29:56 +07:00
newbee1905
7b605f40b8 refactor(wip): refactor scraper 2023-02-05 03:25:06 +07:00
newbee1905
578e773b04 Merge branch 'merge-an-ln-ui' into clap 2023-02-05 02:43:20 +07:00
newbee1905
3ebb06cc1b chore: remove unused variables and use 2023-02-05 02:43:08 +07:00
newbee1905
ba8c24dc4c Merge branch 'consumet' into merge-an-ln-ui 2023-02-05 02:41:17 +07:00
newbee1905
6bd2ad922c feat: Use subcommands instead of flags for mode 2023-02-05 02:37:40 +07:00
newbee1905
c0d17dd230 chore: merge with _real_ consumet branch 2023-02-05 00:37:45 +07:00
newbee1905
5b9f060da3 feat: Setting up clap 2023-02-04 15:10:25 +07:00
newbee1905
c289177f65 feat(wip): Setting up clap 2023-02-04 14:35:38 +07:00
newbee1905
e82cdc2040 chore: merge with consumet branch 2023-02-03 17:13:58 +07:00
Zastian Pretorius
1ad8e8d12e fixed the murge 2023-02-03 09:25:54 +00:00
mrfluffy
1bad59ccb9 Merge pull request #22 from mrfluffy-dev/main-consumet
rebase consumet and main
2023-02-02 08:23:04 -08:00
mrfluffy
bf399d4a7f Merge branch 'consumet' into main-consumet 2023-02-02 08:22:35 -08:00
mrfluffy
9f0df00b19 Merge pull request #19 from newbee1905/main
Add option to use glow to read light novel
2023-02-02 07:26:50 -08:00
newbee1905
e573b8066c Merge commit '03166de' into merge-an-ln-ui 2023-02-02 22:26:39 +07:00
newbee1905
2b4c1079ac refactor: refactor code layout by moving ui to another crates 2023-02-02 22:26:18 +07:00
newbee1905
03166deb7e chore: fix helpers 2023-02-02 22:24:54 +07:00
Zastian Pretorius
6fadcadba1 some scraper stuff 2023-02-01 20:18:36 +00:00
newbee1905
2dea2c86ec chore: add reader args to help 2023-02-01 22:49:11 +07:00
newbee1905
330eb6239a feat: add option to use glow instead of bat 2023-02-01 22:42:51 +07:00
newbee1905
652548057b feat: switch from if else to match case for handling args 2023-02-01 22:31:43 +07:00
Zastian Pretorius
edbebf4b4e added provider switching and soft subs 2023-01-31 16:47:08 +00:00
Zastian Pretorius
634643657e implementing consument api as a test 2023-01-27 21:46:53 +00:00
Zastian Pretorius
f435f290ac removed a debug print 2023-01-17 15:42:43 +00:00
mrfluffy
97fe7e30a7 Merge pull request #18 from mrfluffy-dev/img
added better anime history
2023-01-17 07:09:56 -08:00
Zastian Pretorius
e7438cb664 added better anime history 2023-01-17 15:08:57 +00:00
mrfluffy
2c3d883f1f Merge pull request #17 from mrfluffy-dev/img
added full image support
2023-01-16 14:19:48 -08:00
Zastian Pretorius
6b58d42af7 dynamic image sizing 2023-01-16 22:16:24 +00:00
Zastian Pretorius
7fc96dce60 added state check to navigation for images 2023-01-16 21:18:22 +00:00
Zastian Pretorius
d716fa1d49 made image path dinamic 2023-01-11 01:00:17 +00:00
Zastian Pretorius
4d0112ed86 added basic image support 2023-01-11 00:50:19 +00:00
Zastian Pretorius
26a8cb5e77 thanks coolanx for the amazing regex 2023-01-10 16:22:37 +00:00
Zastian Pretorius
929cca2ace figed scraper regex 2023-01-10 16:12:30 +00:00
Zastian Pretorius
cc4f530764 fixed my shit regex 2023-01-07 22:25:54 +00:00
Zastian Pretorius
b5bac31d64 version bump 2023-01-07 03:09:01 +00:00
Zastian Pretorius
1092bc4f75 Merge branch 'main' of github.com:mrfluffy-dev/kami 2023-01-07 02:58:00 +00:00
Zastian Pretorius
f39deb5321 she be working again 2023-01-07 02:56:52 +00:00
mrfluffy
e22047b142 I am back baby 2023-01-06 21:21:15 +00:00
mrfluffy
ea91edf8bd Update README.org 2022-12-23 21:41:59 +00:00
Zastian Pretorius
e2b7f53da1 added Chrome cast 2022-11-22 20:20:49 +00:00
Zastian Pretorius
061d2dfb7c did an oopsie 2022-11-04 14:43:10 +00:00
Zastian Pretorius
3abedb7360 added new providers 2022-11-04 14:37:07 +00:00
Zastian Pretorius
1fc1b7e867 fixed anime provider 2022-11-03 22:58:03 +00:00
Zastian Pretorius
a5f0ef0e40 I did an oopsie 2022-10-31 20:22:18 +00:00
Zastian Pretorius
6d09eb7e6b re formating in ln 2022-10-30 01:07:37 +00:00
Zastian Pretorius
ca8c8c12e8 added redirecting to light novels 2022-10-30 01:07:20 +00:00
Zastian Pretorius
52dab31fe3 added redirecting to anime 2022-10-30 01:06:59 +00:00
Zastian Pretorius
ed0e3dd50f added anime tracking 2022-10-29 20:11:12 +01:00
Zastian Pretorius
abe79ba96f Merge branch 'main' of github.com:mrfluffy-dev/kami 2022-10-29 18:17:36 +01:00
Zastian Pretorius
8363c57f73 added light novel tracking 2022-10-29 18:16:09 +01:00
mrfluffy
857a5daf5a Merge pull request #9 from justchokingaround/patch-1
docs: windows copy to path
2022-10-27 14:50:31 +01:00
Zastian Pretorius
e9de97e4da fixed config dir problem 2022-10-26 22:15:05 +01:00
Zastian Pretorius
1e02417c15 added condision check so that watching older episodes don't update anilist 2022-10-20 17:30:07 +01:00
Zastian Pretorius
d436e69a64 fixed the unlikely ocurence of slow gogo uploads 2022-10-18 17:09:45 +01:00
Zastian Pretorius
115d7e790f optimised episode range detection 2022-10-17 22:55:49 +01:00
Zastian Pretorius
cc0945e29a made anilist progress update if there is only one episode 2022-10-11 12:12:35 +01:00
Zastian Pretorius
3ff0a47ff7 coolansx you are amazing(make stacking even better) 2022-10-11 12:06:32 +01:00
Zastian Pretorius
8ad12ebcca implemented way better anilist tracking 2022-10-11 02:01:53 +01:00
Zastian Pretorius
c63bb81f40 fuck off gogo 2022-10-06 14:06:17 +01:00
Zastian Pretorius
759a8e48b3 fixed yet anothe gogo url change 2022-09-26 15:51:36 +01:00
Zastian Pretorius
e025163448 fixed link again 2022-09-09 23:24:43 +01:00
Zastian Pretorius
ae10585507 fixed ln flickering 2022-09-03 12:39:50 +01:00
Zastian Pretorius
2e9874b486 fixed tui flikering 2022-09-03 11:20:47 +01:00
chokerman
67eed7353a docs: windows copy to path 2022-09-03 10:38:22 +02:00
Zastian Pretorius
2193956e8b fixed bug to see last episode of anime 2022-08-22 21:29:30 +01:00
Zastian Pretorius
ebfb9b8ea6 changed gogoanime form .lu to .ee 2022-08-19 14:03:02 +01:00
Zastian Pretorius
8afe0fad98 made kami be able to handel uncensored anime 2022-08-16 22:27:18 +01:00
Zastian Pretorius
f5b37da2d1 fixed ln ui braking in tiling window managers 2022-08-11 18:24:50 +01:00
Zastian Pretorius
a7ddb5f09d fixed tiling window managers braking in anime 2022-08-11 18:23:41 +01:00
Zastian Pretorius
2256fd4000 fixed even more movie related problems 2022-08-11 18:20:01 +01:00
Zastian Pretorius
20ab508dad swithed to i to search in ln 2022-08-11 17:38:27 +01:00
Zastian Pretorius
b67369d568 fixed anilist token 2022-08-11 17:32:54 +01:00
Zastian Pretorius
95a6da290c fixed movies 2022-08-11 17:23:53 +01:00
Zastian Pretorius
8aaeaa756d bug fix 2022-08-11 17:11:04 +01:00
mrfluffy
0d2ff78743 Merge pull request #8 from mrfluffy-dev/tui
Tui
2022-08-11 17:04:29 +01:00
Zastian Pretorius
e79cdb07f5 fixed bug where season to of anime caused a crash 2022-08-11 16:52:54 +01:00
Zastian Pretorius
570a8dfc4b added help info foe ln page selection 2022-08-11 16:34:45 +01:00
Zastian Pretorius
3361f9ad93 did some error handeling on anime 2022-08-11 16:15:20 +01:00
Zastian Pretorius
3fb4ad1e93 added ln tui and removed old interface 2022-08-11 16:15:00 +01:00
Zastian Pretorius
2b998923d2 compleate switch to tui for anime 2022-08-10 18:27:32 +01:00
Zastian Pretorius
180b1dfb32 basic tui implementation 2022-08-10 15:24:45 +01:00
Zastian Pretorius
c5fb7b056a added the print back to show yopur currenrt episode 2022-08-08 22:56:59 +01:00
Zastian Pretorius
333b638d06 added resume functon for anime 2022-08-08 22:50:48 +01:00
Zastian Pretorius
77415b22b4 changed index from 1 to 0 2022-08-08 22:50:19 +01:00
Zastian Pretorius
84aa95aa6e changed readme and updated version number 2022-08-08 18:11:31 +01:00
Zastian Pretorius
62de278e8d added anilist tracking 2022-08-08 14:49:30 +01:00
Zastian Pretorius
f57e3f2df6 You are welcome 2022-08-03 12:45:49 +01:00
27 changed files with 2654 additions and 525 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
test.json

1111
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
name = "kami"
author = "mrfluffy-dev"
license = "GPL-3.0"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -14,3 +14,11 @@ isahc = "1.7.2"
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"
rust_cast = "0.17.0"
viuer = { version = "0.6", features = ["sixel"] }
clap = { version = "4.1.4", features = ["derive"] }
lazy_static = "1.4.0"

View File

@@ -1,15 +1,19 @@
#+title: Readme
#+OPTIONS: toc:2
* Table of content
1. [[#Why-use-kami][Why use kami]]
2. [[#Dependencies][Dependencies]]
3. [[#Install][Install]]
1. [[#IMPORTANT][IMPORTANT]]
2. [[#Why-use-kami][Why use kami]]
3. [[#Dependencies][Dependencies]]
4. [[#Install][Install]]
- [[#LinuxMac][Linux/mac]]
- [[#Windows][Windows]]
4. [[#Honorable-mentions][Honorable mentions]]
5. [[#Honorable-mentions][Honorable mentions]]
* IMPORTANT
remove all contents of ~$HOME/.config/kami/an_progress.json~ new version is not compatibal with old progress file.
* Why use kami
well its a fast and easy way to watch anime and read light novels right in your terminal no need to open a browser.
Well its a fast and easy way to watch anime and read light novels right in your terminal no need to open a browser.
Also rust is fast as fuck boiiiii.
It can keep your anime tracking up to date with anilist.
* Dependencies
1. [[https://github.com/sharkdp/bat][bat]]
2. [[https://mpv.io/][mpv]]
@@ -76,11 +80,12 @@ git clone https://github.com/mrfluffy-dev/kami.git && cd kami
#+begin_src shell
cargo build --release
#+end_src
7. copy kami to path
7. copy kami to path (for this to work, you need to use git bash and you need to run git bash in administrator mode0
#+begin_src
cp target/release/kami.exe /usr/bin/kami
#+end_src
8. open kami by using ~kami~
* Honorable mentions
- [[https://github.com/pystardust/ani-cli][ani-cli]] just a bunch of fucking nice people.
- [[https://docs.rs/][rust docs]] honestly its just so useful.
- [[https://github.com/pystardust/ani-cli][ani-cli]] Just a bunch of fucking nice people.
- [[https://docs.rs/][rust docs]] Honestly its just so useful.
- [[https://github.com/DemonKingSwarn/flix-cli][flix-cli]] For forcing me to make a release.

View File

@@ -1,93 +1,44 @@
use crate::main;
use crate::open_video;
use crate::{anime_ep_range, anime_link, anime_names};
use crate::{int_input, string_input};
use colored::Colorize;
//use crate
pub fn anime_stream(search: String, episode: u32) {
let query = if search != "" {
search
} else {
string_input("Search anime: ")
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{backend::CrosstermBackend, Terminal};
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");
}
}
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
if ep_range == 1 {
let link = anime_link(title, 1);
open_video(link);
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 {
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");
}
}
}
loop {
let link = anime_link(title, ep_num as u64);
open_video(link);
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);
} else if input == "q" {
std::process::exit(0);
} else {
println!("Invalid command");
}
}
use crate::ui::app::{anime::App, app::KamiApp};
pub fn anime_ui(
token: String,
provider: String,
cast: (bool, String),
) -> Result<(), Box<dyn Error>> {
// 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::new();
app.token = token;
app.provider = provider;
app.cast = cast;
let res = app.run(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}

View File

@@ -1,3 +1,4 @@
pub mod anime;
pub mod player;
pub mod scraper;
pub mod anime;
pub mod trackers;

View File

@@ -1,4 +1,15 @@
pub fn open_video(link: (String, String)) {
extern crate rust_cast;
use rust_cast::{
channels::{
media::{Media, StreamType},
receiver::CastDeviceApp,
},
CastDevice,
};
use std::str::FromStr;
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);
@@ -7,7 +18,77 @@ pub fn open_video(link: (String, String)) {
.arg(arg)
.output()
.expect("failed to open mpv");
// clear terminal
print!("\x1b[2J\x1b[1;1H");
} 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
}
const DEFAULT_DESTINATION_ID: &str = "receiver-0";
fn play_media(
device: &CastDevice,
app_to_run: &CastDeviceApp,
media: String,
media_type: String,
media_stream_type: StreamType,
) {
let app = device.receiver.launch_app(app_to_run).unwrap();
device
.connection
.connect(app.transport_id.as_str())
.unwrap();
let _status = device
.media
.load(
app.transport_id.as_str(),
app.session_id.as_str(),
&Media {
content_id: media,
content_type: media_type,
stream_type: media_stream_type,
duration: None,
metadata: None,
},
)
.unwrap();
}
pub fn open_cast(link: (String, String), ip: &str) {
let cast_device = match CastDevice::connect_without_host_verification(ip, 8009) {
Ok(cast_device) => cast_device,
Err(err) => panic!("Could not establish connection with Cast Device: {:?}", err),
};
cast_device
.connection
.connect(DEFAULT_DESTINATION_ID.to_string())
.unwrap();
cast_device.heartbeat.ping().unwrap();
// Play media and keep connection.
let media_stream_type = match "none" {
value @ "buffered" | value @ "live" | value @ "none" => {
StreamType::from_str(value).unwrap()
}
_ => panic!("Unsupported stream type!"),
};
play_media(
&cast_device,
&CastDeviceApp::from_str("default").unwrap(),
link.0.to_string(),
"".to_string(),
media_stream_type,
);
}

View File

@@ -1,92 +1,117 @@
use base64::{decode, encode};
use isahc::config::Configurable;
use isahc::{ReadResponseExt, Request, RequestExt};
use regex::Regex;
use std::fs::File;
use std::io::prelude::*;
pub fn get_anime_html(url: &str) -> String {
use crate::helpers::{fixing_text::htmlise, scraper};
//use serde_json::json;
pub fn search_anime(query: String) -> (Vec<String>, Vec<String>, Vec<String>) {
let req = Request::builder()
.uri(url)
.header(
"user-agent",
"Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0",
)
.uri(format!(
"https://api.consumet.org/meta/anilist/{}",
htmlise(query)
))
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("user-agent", *scraper::USER_AGENT)
.body(())
.unwrap();
let json = req.send().unwrap().text().unwrap();
let json: serde_json::Value = serde_json::from_str(&json).unwrap();
req.send().unwrap().text().unwrap()
let mut titles = Vec::new();
let mut ids = Vec::new();
let mut images = Vec::new();
for anime in json["results"].as_array().unwrap().iter() {
titles.push(
anime["title"]["userPreferred"]
.as_str()
.unwrap()
.to_string(),
);
ids.push(anime["id"].as_str().unwrap().to_string());
images.push(anime["image"].as_str().unwrap().to_string());
}
pub fn get_ep_location(url: &str) -> String {
let request = Request::builder()
.method("HEAD")
.uri(url)
.header(
"user-agent",
"Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0",
)
(ids, titles, images)
}
pub fn get_episodes(id: &i32, provider: &str) -> (Vec<String>, Vec<String>) {
let req = Request::builder()
.uri(format!(
"https://api.consumet.org/meta/anilist/info/{}?provider={}",
id, provider
))
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("user-agent", *scraper::USER_AGENT)
.body(())
.unwrap();
let response = request.send().unwrap();
let headers = response.headers();
let location = headers.get("location").unwrap();
location.to_str().unwrap().to_string()
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 episode in json["episodes"].as_array().unwrap().iter() {
titles.push(episode["title"].as_str().unwrap().to_string());
ids.push(episode["id"].as_str().unwrap().to_string());
}
(titles, ids)
}
pub fn anime_names(query: &str) -> Vec<String> {
let url = format!("https://gogoanime.lu//search.html?keyword={}", query);
//relpace all spaces with %20
let url = url.replace(' ', "%20");
let html = get_anime_html(&url);
let re = Regex::new(r#"(?m)/category/([^"]*)"#).unwrap();
let mut anime_list: Vec<String> = Vec::new();
for cap in re.captures_iter(&html) {
anime_list.push(cap.get(1).unwrap().as_str().trim().to_string());
}
anime_list.dedup();
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={}",
ep_id, provider
))
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("user-agent", *scraper::USER_AGENT)
.body(())
.unwrap();
let json = req.send().unwrap().text().unwrap();
let json: serde_json::Value = serde_json::from_str(&json).unwrap();
anime_list
}
let mut url = String::new();
std::fs::write("test.json", json.to_string()).unwrap();
pub fn anime_ep_range(anime_name: &str) -> u16 {
let url = format!("https://gogoanime.lu/category/{}", anime_name);
let re = Regex::new(r#"(?m)\s<a href="\#" class="active" ep_start = (.*?)</a>"#).unwrap();
let episodes = re
.captures_iter(&get_anime_html(&url))
.next()
.unwrap()
.get(1)
.unwrap()
let mut subtitle = String::new();
let _error_vec = Vec::new();
let sub_array = json["subtitles"].as_array().unwrap_or(&_error_vec);
for sub in sub_array.iter() {
//set subtitle to lang = English
if sub["lang"].as_str().unwrap_or("null") == "English" {
subtitle = sub["url"].as_str().unwrap_or("null").to_string();
// add \ before the first : in the url
subtitle = subtitle.replace(":", "\\:");
}
}
let mut highest_quality = 0;
for source in json["sources"].as_array().unwrap().iter() {
let quality = source["quality"]
.as_str()
.trim()
.to_string();
episodes
.split('-')
.nth(1)
.unwrap_or("0")
.parse::<u16>()
.unwrap_or(0)
.unwrap()
.replace("p", "")
.parse::<i32>()
.unwrap_or(0);
if quality > highest_quality {
highest_quality = quality;
url = source["url"].as_str().unwrap().to_string();
}
}
(url, subtitle)
}
pub fn anime_link(title: &str, ep: u64) -> (String, String) {
let url = format!("https://animixplay.to/v1/{}", title);
let html = get_anime_html(&url);
let re = Regex::new(r#"(?m)\?id=([^&]+)"#).unwrap();
let id1 = re
.captures_iter(&html)
.nth(ep as usize - 1)
.unwrap()
.get(1)
.unwrap()
.as_str()
.trim()
.to_string();
let title = format!("{} Episode {}", title.replace('-', " "), ep);
let encoded_id1 = encode(&id1);
let anime_id = encode(format!("{}LTXs3GrU8we9O{}", id1, encoded_id1));
let html = format!("https://animixplay.to/api/live{}", anime_id);
let url = get_ep_location(&html);
let url = url.split('#').nth(1).unwrap();
let url = std::str::from_utf8(&decode(url).unwrap())
.unwrap()
.to_string();
(url, title)
pub fn get_image(url: &str, path: &str) {
let url = url;
let mut response = isahc::get(url).unwrap();
let mut buffer = Vec::new();
response.copy_to(&mut buffer).unwrap();
let mut file = File::create(path).unwrap();
file.write_all(&buffer).unwrap();
}

233
src/anime/trackers.rs Normal file
View File

@@ -0,0 +1,233 @@
use crate::string_input;
use isahc::{ReadResponseExt, Request, RequestExt};
use serde_json::json;
use std::fs;
pub fn get_token() -> String {
//if not on windows create folder ~/.config/kami
let config_path = dirs::config_dir().unwrap().join("kami");
if !config_path.exists() {
fs::create_dir_all(&config_path).unwrap();
}
let token_path = config_path.join("token.txt");
if !token_path.exists() {
//create empty file
fs::File::create(&token_path).unwrap();
}
//read token from file
let token = fs::read_to_string(&token_path).unwrap();
if token.is_empty() {
//ask user if they want to add a token or track locally
let input = string_input(
"would you want to link anilist(sellecting no will track anime localy)? (y/n)",
);
if input == "y" {
println!("please go to the below link and copy and past the token below");
println!(
"https://anilist.co/api/v2/oauth/authorize?client_id=9121&response_type=token"
);
let token = string_input("token: ");
fs::write(&token_path, token).unwrap();
} else if input == "n" {
let token = "local";
fs::write(&token_path, token).unwrap();
} else {
println!("invalid input");
std::process::exit(1);
}
}
let token = fs::read_to_string(&token_path).unwrap();
token
}
//get the user id from the token
fn get_user_id(token: &str) -> i32 {
const QUERY: &str = "query {
Viewer {
id
}
}";
let json = json!({ "query": QUERY });
let resp = Request::builder()
.method("POST")
.uri("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(json.to_string())
.unwrap()
.send()
.unwrap()
.text();
//println!("{}", resp);
let regex = regex::Regex::new(r#"id":(.*?)}"#).unwrap();
let resp: String = resp.as_ref().unwrap().to_string();
let id = regex
.captures(&resp)
.unwrap()
.get(1)
.unwrap()
.as_str()
.parse::<i32>()
.unwrap();
id
}
pub fn get_user_anime_progress(anime_id: i32, token: &str) -> i32 {
let user_id = get_user_id(&token);
const QUERY: &str = "query ($user_id: Int, $media_id: Int) {
MediaList (userId: $user_id, mediaId: $media_id, type: ANIME) {
progress
}
}";
let json = json!({
"query": QUERY,
"variables": {
"user_id": user_id,
"media_id": anime_id,
}
});
let resp = Request::builder()
.method("POST")
.uri("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(json.to_string())
.unwrap()
.send()
.unwrap()
.text();
let regex = regex::Regex::new(r#"progress":(.*?)}"#).unwrap();
let resp: String = resp.as_ref().unwrap().to_string();
if resp.contains("errors") {
0
} else {
let progress = regex
.captures(&resp)
.unwrap()
.get(1)
.unwrap()
.as_str()
.parse::<i32>()
.unwrap();
progress
}
}
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) {
id
status
progress
}
}
";
let json = json!({
"query": UPDATE,
"variables": {
"mediaId": anime_id,
"status": "CURRENT",
"progress": progress
}
});
let _resp = Request::builder()
.method("POST")
.uri("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(json.to_string())
.unwrap()
.send()
.unwrap()
.text();
}
// local tracking
pub fn get_an_json() -> serde_json::Value {
let config_path = dirs::config_dir().unwrap().join("kami");
if !config_path.exists() {
fs::create_dir_all(&config_path).unwrap();
}
let json_path = config_path.join("an_progress.json");
if !json_path.exists() {
fs::File::create(&json_path).unwrap();
}
let json = fs::read_to_string(&json_path).unwrap();
let json: serde_json::Value = serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
json
}
pub fn write_an_progress(anime: (&str, &str, &str), progress: &u64) {
let config_path = dirs::config_dir().unwrap().join("kami");
let json_path = config_path.join("an_progress.json");
let json = fs::read_to_string(&json_path).unwrap();
let mut json: serde_json::Value =
serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
let mut title_json = serde_json::Map::new();
title_json.insert(
"progress".to_string(),
serde_json::Value::from(progress.clone()),
);
title_json.insert("link".to_string(), serde_json::Value::from(anime.1));
title_json.insert("image".to_string(), serde_json::Value::from(anime.2));
title_json.insert(
"updated".to_string(),
serde_json::Value::from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
),
);
//insert title_json into json
if json[anime.0].is_null() {
json[anime.0] = serde_json::Value::from(title_json);
} else {
json[anime.0]["progress"] = serde_json::Value::from(progress.clone());
json[anime.0]["link"] = serde_json::Value::from(anime.1);
json[anime.0]["image"] = serde_json::Value::from(anime.2);
json[anime.0]["updated"] = serde_json::Value::from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
);
}
let json = serde_json::to_string_pretty(&json).unwrap();
fs::write(&json_path, json).unwrap();
}
pub fn get_an_history() -> (Vec<String>, Vec<String>, Vec<String>) {
//get the titles, links, and images from the json
let json = get_an_json();
let mut titles = vec![];
let mut links = vec![];
let mut images = vec![];
let mut last_updated = vec![];
//if the json is empty, return empty vectors
if json.is_null() {
return (titles, links, images);
}
for (key, value) in json.as_object().unwrap() {
titles.push(key.to_string());
links.push(value["link"].as_str().unwrap().to_string());
images.push(value["image"].as_str().unwrap().to_string());
last_updated.push(value["updated"].as_u64().unwrap());
}
let mut indices: Vec<usize> = (0..last_updated.len()).collect();
indices.sort_by(|&a, &b| last_updated[b].cmp(&last_updated[a]));
titles = indices.iter().map(|&i| titles[i].clone()).collect();
links = indices.iter().map(|&i| links[i].clone()).collect();
images = indices.iter().map(|&i| images[i].clone()).collect();
(links, titles, images)
}
pub fn get_an_progress(title: &str) -> i32 {
let json = get_an_json();
let selected = json[title]["progress"].as_u64().unwrap_or(0);
selected as i32
}

View File

@@ -59,3 +59,12 @@ pub fn fix_html_encoding(ln_text: &Vec<String>) -> Vec<String> {
}
ln_text_new
}
pub fn htmlise(query: String) -> String {
query
.replace(" ", "%20")
.replace(":", "%3A")
.replace("!", "%21")
.replace("?", "%3F")
.replace("'", "%27")
}

View File

@@ -1,2 +1,3 @@
pub mod fixing_text;
pub mod scraper;
pub mod take_input;

4
src/helpers/scraper.rs Normal file
View File

@@ -0,0 +1,4 @@
lazy_static! {
pub static ref USER_AGENT: &'static str =
"Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0";
}

View File

@@ -1,37 +1,46 @@
use crate::{chapter_selector, get_full_text, open_bat, search_ln};
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;
}
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");
use crate::ln::tracker::*;
use crate::ui::app::{app::KamiApp, ln::App};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
//open temp.txt in cat for user to read
let _com = open_bat();
print!("\x1B[2J\x1B[1;1H");
use std::{error::Error, io};
use tui::{backend::CrosstermBackend, Terminal};
pub fn ln_ui(chapter: u32, reader: String) -> Result<(), Box<dyn Error>> {
// setup terminal
let _ = get_ln_json();
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::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 = app.run(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}

View File

@@ -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::<u32>().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::<u32>().is_ok() {
_chapter_number_int = chapter_number.parse::<u32>().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);
}
}
}

View File

@@ -1,5 +1,4 @@
pub mod menu;
pub mod ln;
pub mod open_text;
pub mod scraper;
pub mod search;
pub mod ln;
pub mod tracker;

View File

@@ -11,8 +11,7 @@ pub fn open_bat() -> Result<ExitStatus> {
home.drain(0..6);
home.drain(home.len() - 2..home.len());
path = format!("{}/AppData/Roaming/log_e", home).to_string();
}
else{
} else {
path = "/tmp/log_e".to_string();
}
@@ -37,3 +36,36 @@ pub fn open_bat() -> Result<ExitStatus> {
.spawn()?
.wait()
}
#[allow(unused_assignments)]
pub fn open_glow() -> Result<ExitStatus> {
let termsize::Size { rows: _, cols } = termsize::get().unwrap();
let mut path = String::new();
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());
path = format!("{}/AppData/Roaming/log_e", home).to_string();
} else {
path = "/tmp/log_e".to_string();
}
let soft_wrap = match Command::new("fold")
.arg("-s")
.arg("-w")
.arg((cols - 9).to_string())
.arg(path)
.stdout(Stdio::piped())
.spawn()
{
Err(why) => panic!("couldn't spawn wc: {}", why),
Ok(soft_wrap) => soft_wrap,
};
Command::new("glow")
.arg("-p")
.stdin(soft_wrap.stdout.unwrap())
.spawn()?
.wait()
}

View File

@@ -1,24 +1,26 @@
use isahc::config::Configurable;
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
pub fn get_html(url: &str) -> String {
let req = Request::builder()
.uri(url)
.header(
"user-agent",
"Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0",
)
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36")
.body(())
.unwrap();
req.send().unwrap().text().unwrap()
let mut res = req.send().unwrap();
let html = res.text().unwrap();
html
}
//using isahc::prelude::* make a php reqest to get the next page of the ln
pub fn get_ln_next_page(ln_id: &str, page: &str) -> String {
pub fn get_ln_next_page(ln_id: &str, page: &u32) -> String {
let url = "https://readlightnovels.net/wp-admin/admin-ajax.php".to_string();
let form = format!(
"action=tw_ajax&type=pagination&id={}.html&page={}",
@@ -28,14 +30,15 @@ pub fn get_ln_next_page(ln_id: &str, page: &str) -> String {
let req = Request::builder()
.method("POST")
.uri(url)
.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(form)
.unwrap();
req.send().unwrap().text().unwrap()
let resp = req.send().unwrap().text().unwrap();
resp
}
pub fn get_full_text(chapter_url: &str) -> String {
@@ -102,3 +105,47 @@ pub fn get_ln_text(chapter_url: &str) -> Vec<String> {
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<String> {
let re = Regex::new(r#"(?m)^\s*(<a href="[^"]*" title="[^"]*")"#).unwrap();
let mut ln_list: Vec<String> = 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<String>) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*<a href="[^"]*" title="([^"]*)""#).unwrap();
let mut ln_title: Vec<String> = 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<String>) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*<a href="([^"]*)""#).unwrap();
let mut ln_url: Vec<String> = 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<String> {
let re = Regex::new(r#"title=(.*?)>"#).unwrap();
let mut ln_list: Vec<String> = 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
}

View File

@@ -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::<Vec<String>>();
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::<usize>().is_ok() {
let ln_number = ln_number.trim().to_string();
let ln_number = ln_number.parse::<usize>().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<String> {
let re = Regex::new(r#"(?m)^\s*(<a href="[^"]*" title="[^"]*")"#).unwrap();
let mut ln_list: Vec<String> = 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<String>) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*<a href="[^"]*" title="([^"]*)""#).unwrap();
let mut ln_title: Vec<String> = 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<String>) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*<a href="([^"]*)""#).unwrap();
let mut ln_url: Vec<String> = 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<String> {
let re = Regex::new(r#"title=(.*?)>"#).unwrap();
let mut ln_list: Vec<String> = 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
}

50
src/ln/tracker.rs Normal file
View File

@@ -0,0 +1,50 @@
use serde_json;
use std::fs;
//
//
pub fn get_ln_json() -> serde_json::Value {
let config_path = dirs::config_dir().unwrap().join("kami");
if !config_path.exists() {
fs::create_dir_all(&config_path).unwrap();
}
let json_path = config_path.join("ln_progress.json");
if !json_path.exists() {
fs::File::create(&json_path).unwrap();
}
let json = fs::read_to_string(&json_path).unwrap();
let json: serde_json::Value = serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
json
}
pub fn write_ln_progress(title: &str, current_page: &u32, selected: &usize) {
let config_path = dirs::config_dir().unwrap().join("kami");
let json_path = config_path.join("ln_progress.json");
let json = fs::read_to_string(&json_path).unwrap();
let mut json: serde_json::Value =
serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
let mut title_json = serde_json::Map::new();
title_json.insert(
"current_page".to_string(),
serde_json::Value::from(current_page.clone()),
);
title_json.insert(
"selected".to_string(),
serde_json::Value::from(selected.clone()),
);
//insert title_json into json
if json[title].is_null() {
json[title] = serde_json::Value::from(title_json);
} else {
json[title]["current_page"] = serde_json::Value::from(current_page.clone());
json[title]["selected"] = serde_json::Value::from(selected.clone());
}
let json = serde_json::to_string_pretty(&json).unwrap();
fs::write(&json_path, json).unwrap();
}
pub fn get_ln_progress(title: &str) -> (u32, usize) {
let json = get_ln_json();
let current_page = json[title]["current_page"].as_u64().unwrap_or(1) as u32;
let selected = json[title]["selected"].as_u64().unwrap_or(0) as usize;
(current_page, selected)
}

View File

@@ -1,121 +1,83 @@
mod anime;
mod helpers;
mod ln;
mod ui;
use anime::anime::anime_stream;
use colored::Colorize;
use ln::{scraper::get_ln_next_page, ln::ln_read};
use ln::search::search_ln;
#[macro_use]
extern crate lazy_static;
use crate::anime::{
player::open_video,
scraper::{anime_ep_range, anime_link, anime_names},
};
use anime::anime::anime_ui;
use ln::ln::ln_ui;
use crate::anime::trackers::*;
use crate::get_token;
use crate::helpers::take_input::{int_input, string_input};
use crate::ln::{menu::chapter_selector, open_text::open_bat, scraper::get_full_text};
use clap::{Parser, Subcommand};
#[derive(Subcommand, Debug)]
enum KamiMode {
#[command(name = "ln", about = "Search Light Novel to read.")]
LightNovel,
#[command(about = "Search Anime to read.")]
Anime,
}
#[derive(Parser, Debug)]
#[command(author, version, about = "A scraper to read light novels and watch anime in your terminal.", long_about = None)]
struct Args {
#[command(subcommand)]
mode: Option<KamiMode>,
/// Use anime mode
#[arg(short, long)]
anime: bool,
/// Use light novel mode
#[arg(short, long)]
ln: bool,
#[arg(short, long, default_value_t = 0)]
chapter: u32,
#[arg(short = 'C', long, default_value = "0")]
cast: String,
/// Provider for anime or light novel
#[arg(short = 'r', long, default_value = "gogo")]
provider: String,
/// Text renderer for light novel
#[arg(short = 'R', long, default_value = "bat")]
reader: String,
}
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 search = option string
let mut search = String::new();
let mut count = 0;
for arg in std::env::args() {
if arg == "--help" || arg == "-h" {
help = true;
}
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;
}
}
let args = Args::parse();
}
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) {
chapter = arg.parse::<u32>().unwrap();
}else{
chapter = 0;
}
}
if arg == "--episode" || arg == "-e" {
if let Some(arg) = std::env::args().nth(count + 1) {
episode = arg.parse::<u32>().unwrap();
}else{
episode = 0;
}
}
count += 1;
}
if help == true{
print_help();
}
if anime == false && ln == false {
let mode = match &args.mode {
None => {
println!("1: Anime");
println!("2: Light Novel");
let a = int_input("pick your poison: ");
match a{
1 => anime = true,
2 => ln = true,
_=>println!("invalid option. ")
let opt = int_input("pick your poison: ");
match opt {
1 => &KamiMode::Anime,
2 => &KamiMode::LightNovel,
_ => {
println!("invalid option.");
std::process::exit(0);
}
}
}
Some(m) => m,
};
let _ = match mode {
&KamiMode::LightNovel => ln_ui(args.chapter, args.reader),
&KamiMode::Anime => {
let token = get_token();
anime_ui(token, args.provider, (args.cast == "0", args.cast))
}
};
}
if anime == true && ln == true {
println!("you can only use one of the arguments at a time");
std::process::exit(0);
}
if ln == true {
ln_read(&search, chapter);
} else if anime == true {
anime_stream(search, episode);
} 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!("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()));
println!("{}", "after this^^^ argument you can enter a chapter number".green());
println!("{}", "for exaple kami -c 200");
//print blank line
println!("");
println!("help:\t\t{}", format_args!("{}", "-h --help".red()));
//kill the program
std::process::exit(0);
}

411
src/ui/app/anime.rs Normal file
View File

@@ -0,0 +1,411 @@
use crate::ui::{app::app::KamiApp, input::InputMode, list::StatefulList};
use crate::anime::player::{open_cast, open_video};
use crate::anime::scraper::{get_episode_link, get_episodes, get_image, search_anime};
use crate::anime::trackers::{
get_an_history, get_an_progress, 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<String>, Vec<String>, Vec<String>),
image: String,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: StatefulList<String>,
episodes: (Vec<String>, Vec<String>),
title: String,
ep: u64,
progress: i32,
anime_id: i32,
pub token: String,
pub provider: String,
pub cast: (bool, String),
}
impl<'a> App {
fn change_image(&mut self) {
//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(&self.image, &image_path.to_str().unwrap());
print_from_file(image_path, &config).expect("Image printing failed.");
}
}
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()),
episodes: (Vec::new(), Vec::new()),
title: String::new(),
ep: 0,
progress: 0,
anime_id: 0,
token: String::new(),
provider: String::new(),
cast: (false, "0".to_string()),
}
}
fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
let mut ep_select = false;
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::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();
self.change_image();
}
},
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();
self.change_image();
}
},
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();
self.change_image();
}
},
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();
self.change_image();
}
},
//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.anime_id = self.animes.0[selected.unwrap()]
.clone()
.parse::<i32>()
.unwrap();
self.episodes = get_episodes(
&self.animes.0[selected.unwrap()].parse::<i32>().unwrap(),
&self.provider,
);
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 self.episodes.0.len() == 1 {
let link =
get_episode_link(&self.episodes.1[0], &self.provider);
if !self.cast.0 {
open_video((
link.0,
format!("{} Episode 1", &self.title),
link.1,
));
} else {
open_cast(
(link.1, 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.anime_id.to_string(), &image_url),
&1,
);
} else {
update_anime_progress(
self.anime_id,
1,
self.token.as_str(),
);
write_an_progress(
(&self.title, &self.anime_id.to_string(), &image_url),
&1,
);
}
} else {
for ep in 1..self.episodes.1.len() + 1 {
self.messages.push(format!(
"Episode {}: {}",
ep,
self.episodes.0[ep - 1]
));
}
ep_select = true;
}
} else {
let selected = self.messages.state.selected();
self.ep = self
.messages
.iter()
.nth(selected.unwrap())
.unwrap()
.replace("Episode ", "")
.split(":")
.collect::<Vec<&str>>()[0]
.parse::<u64>()
.unwrap();
let link = get_episode_link(
&self.episodes.1[self.ep as usize - 1],
&self.provider,
);
if !self.cast.0 {
open_video((
link.0,
format!("{} Episode {}", &self.title, self.ep),
link.1,
));
} else {
open_cast(
(link.0, 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.anime_id.to_string(), &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.anime_id.to_string(), &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 = search_anime(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<B: Backend>(&mut self, f: &mut Frame<B>) {
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<ListItem> = 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,
)
}
}
}
}

8
src/ui/app/app.rs Normal file
View File

@@ -0,0 +1,8 @@
use std::io;
use tui::{backend::Backend, Frame, Terminal};
pub trait KamiApp {
fn new() -> Self;
fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()>;
fn ui<B: Backend>(&mut self, f: &mut Frame<B>);
}

307
src/ui/app/ln.rs Normal file
View File

@@ -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<String>, // History of recorded messages
ln_titles: Vec<String>,
ln_links: Vec<String>,
title: String,
ln_id: String,
ln_chapters: Vec<String>,
ln_chapters_links: Vec<String>,
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<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> 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::<u32>().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<B: Backend>(&mut self, f: &mut Frame<B>) {
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<ListItem> = 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,
)
}
}
}
}

3
src/ui/app/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod anime;
pub mod app;
pub mod ln;

4
src/ui/input.rs Normal file
View File

@@ -0,0 +1,4 @@
pub enum InputMode {
Normal,
Editing,
}

61
src/ui/list.rs Normal file
View File

@@ -0,0 +1,61 @@
use tui::widgets::ListState;
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
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<Item = &T> {
self.items.iter()
}
}

3
src/ui/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod app;
pub mod input;
pub mod list;