01/04/26_00.35
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
|
||||
[target.aarch64-linux-android]
|
||||
linker = "/home/domenico/Android/Sdk/ndk/30.0.14904198/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android36-clang"
|
||||
[env]
|
||||
CC_aarch64_linux_android = "/home/domenico/Android/Sdk/ndk/30.0.14904198/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android36-clang"
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+3823
File diff suppressed because it is too large
Load Diff
Executable
+17
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "Aniyomi"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.89"
|
||||
color-eyre = "0.6.5"
|
||||
curl = "0.4.49"
|
||||
ratatui = "0.30.0"
|
||||
reqwest = { version = "0.13.2", features = ["rustls"] }
|
||||
scraper = "0.25.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
strum = { version = "0.28", default-features = false, features = ["derive"] }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
toml = "1.0.4"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
@@ -0,0 +1,3 @@
|
||||
# cargo build --target aarch64-linux-android --release
|
||||
cargo build --release
|
||||
cargo build --target x86_64-pc-windows-gnu --release
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
|
||||
[Settings]
|
||||
|
||||
[Settings.Folders]
|
||||
basedir = "." # !! Should be an Absolute Path
|
||||
extensions = "./extensions"
|
||||
anime = "./anime"
|
||||
manga = "./manga"
|
||||
|
||||
[Extension]
|
||||
defaultanime = "animeworld" # USELESS
|
||||
defaultmanga = "mangaworld" # USELESS
|
||||
@@ -0,0 +1,10 @@
|
||||
Type = "Anime"
|
||||
|
||||
[Info]
|
||||
Name = "Animeworld"
|
||||
Description = ""
|
||||
Language = "IT/it"
|
||||
|
||||
[Sitemanager]
|
||||
Hostname = "animeworld.ac"
|
||||
Url = "https://animeworld.ac"
|
||||
@@ -0,0 +1,10 @@
|
||||
Type = "Manga"
|
||||
|
||||
[Info]
|
||||
Name = "Mangaworld"
|
||||
Description = ""
|
||||
Language = "IT/it"
|
||||
|
||||
[Sitemanager]
|
||||
Hostname = "mangaworld.mx"
|
||||
Url = "https://www.mangaworld.mx"
|
||||
@@ -0,0 +1,3 @@
|
||||
[[Animeworld]]
|
||||
name="Frieren: Beyond Journey's End 2"
|
||||
episodes="https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx"
|
||||
@@ -0,0 +1,13 @@
|
||||
[[Mangaworld]]
|
||||
name="Kingdom"
|
||||
episodes="https://www.mangaworld.mx/manga/1683/kingdom"
|
||||
|
||||
|
||||
[[Mangaworld]]
|
||||
name="Chainsaw Man"
|
||||
episodes="https://www.mangaworld.mx/manga/1637/chainsaw-man"
|
||||
|
||||
|
||||
[[Mangago]]
|
||||
name="Medalist"
|
||||
episodes="https://www.mangago.me/read-manga/medalist/"
|
||||
@@ -0,0 +1,536 @@
|
||||
pub mod list {
|
||||
use std::{collections::HashMap};
|
||||
use ratatui::{Frame, crossterm::event::KeyCode, layout::Rect, style::{Style, palette::tailwind::{self}}, widgets::{Block, BorderType, Borders, List, ListState}};
|
||||
|
||||
pub struct CurrentWidget {
|
||||
pub render: Box<dyn Fn(&mut Frame, Rect)>,
|
||||
pub handle_input: Box<dyn Fn(KeyCode) -> bool>
|
||||
}
|
||||
type SLWidgetNotOption = CurrentWidget;
|
||||
type SLWidget = Option<SLWidgetNotOption>;
|
||||
type Callback = fn();
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SelectableList {
|
||||
children_list: Vec<String>,
|
||||
children_hash: HashMap<String, ListDiveChild>,
|
||||
path: Vec<String>,
|
||||
pub lastpath:String,
|
||||
pub state: ListState,
|
||||
pub widget: SLWidget
|
||||
}
|
||||
pub struct ListDiveChild {
|
||||
pub title: String,
|
||||
pub index: usize,
|
||||
pub action_type: ActionType,
|
||||
pub children_list: Vec<String>,
|
||||
pub children_hash: Option<HashMap<String, ListDiveChild>>,
|
||||
pub widget: SLWidget,
|
||||
}
|
||||
impl ListDiveChild {
|
||||
pub fn new(title: String, index:usize, action_type:ActionType, children: Option<HashMap<String, ListDiveChild>>, widget:SLWidget) -> Self {
|
||||
let childrenlist: Vec<String> = match children.as_ref() {
|
||||
Some(chs) => {
|
||||
let mut x:Vec<&ListDiveChild> = chs.values().collect();
|
||||
x.sort_by_key(|v| v.index);
|
||||
x.into_iter().map(|v| v.title.to_owned()).collect()
|
||||
},
|
||||
_ => vec![]
|
||||
};
|
||||
Self {
|
||||
title,
|
||||
index,
|
||||
action_type,
|
||||
children_list: childrenlist,
|
||||
children_hash: children,
|
||||
widget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ActionType {
|
||||
None,
|
||||
SubMenu,
|
||||
Render,
|
||||
}
|
||||
|
||||
impl SelectableList {
|
||||
pub fn new(
|
||||
children: HashMap<String, ListDiveChild>,widget:SLWidget
|
||||
) -> Self {
|
||||
let mut childrenlist = vec!["".to_string();children.keys().len()];
|
||||
for (_,v) in &children {
|
||||
childrenlist[v.index] = v.title.to_owned();
|
||||
}
|
||||
Self {
|
||||
children_hash: children,
|
||||
children_list: childrenlist,
|
||||
path: vec![],
|
||||
lastpath: "".to_owned(),
|
||||
state: ListState::default(),
|
||||
widget,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn get_current_item_list(&self) -> &Vec<String> {
|
||||
if self.path.len() > 0 {
|
||||
if let Some(first_path) = self.path.first() {
|
||||
let mut ocurr = self.children_hash.get(first_path);
|
||||
for i in 1..self.path.len() {
|
||||
if let Some(curr) = ocurr {
|
||||
if let Some(h) = &curr.children_hash {
|
||||
match self.path.get(i) {
|
||||
Some(p) => ocurr = h.get(p),
|
||||
_ => break
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(curr) = ocurr {
|
||||
return &curr.children_list;
|
||||
}
|
||||
}
|
||||
}
|
||||
return &self.children_list
|
||||
}
|
||||
fn get_current_widget(&self) -> Option<&SLWidgetNotOption> {
|
||||
let mut w = self.widget.as_ref();
|
||||
if self.path.len() > 0 {
|
||||
if let Some(first_path) = self.path.first() {
|
||||
let mut ocurr = self.children_hash.get(first_path);
|
||||
for i in 1..self.path.len() {
|
||||
ocurr = ocurr.and_then(|curr| curr.children_hash.as_ref())
|
||||
.and_then(|h| self.path.get(i).and_then(|p| h.get(p)));
|
||||
if let Some(cw) = ocurr.and_then(|c| c.widget.as_ref()) {
|
||||
w = Some(cw)
|
||||
}
|
||||
}
|
||||
if !self.lastpath.is_empty() {
|
||||
ocurr = ocurr.and_then(|curr| curr.children_hash.as_ref())
|
||||
.and_then(|h| h.get(&self.lastpath));
|
||||
if let Some(cw) = ocurr.and_then(|c| c.widget.as_ref()) {
|
||||
w = Some(cw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return w;
|
||||
}
|
||||
fn get_current_children_hash(&self) -> Option<&HashMap<String, ListDiveChild>>{
|
||||
if self.path.len() > 0 {
|
||||
if let Some(first_path) = self.path.first() {
|
||||
let mut ocurr = self.children_hash.get(first_path);
|
||||
for i in 1..self.path.len() {
|
||||
if let Some(curr) = ocurr {
|
||||
if let Some(h) = &curr.children_hash {
|
||||
match self.path.get(i) {
|
||||
Some(p) => ocurr = h.get(p),
|
||||
_ => break
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(curr) = ocurr {
|
||||
return curr.children_hash.as_ref();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(&self.children_hash);
|
||||
}
|
||||
|
||||
|
||||
pub fn render_list(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let l = List::new(self.get_current_item_list().clone())
|
||||
.highlight_style(Style::new()
|
||||
.fg(tailwind::AMBER.c900))
|
||||
.highlight_symbol("> ")
|
||||
.highlight_spacing(ratatui::widgets::HighlightSpacing::Always)
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().fg(tailwind::AMBER.c100))
|
||||
);
|
||||
|
||||
frame.render_stateful_widget(l,area, &mut self.state);
|
||||
}
|
||||
pub fn render_menu(&mut self, frame: &mut Frame, area: Rect) {
|
||||
if let Some(f) = self.get_current_widget() {
|
||||
(f.render)(frame, area)
|
||||
}
|
||||
}
|
||||
pub fn enter(&mut self) {
|
||||
if let Some(s) = self.state.selected()
|
||||
.and_then(|i| self.get_current_item_list().get(i)) {
|
||||
if let Some(ldc) = self.get_current_children_hash().and_then(|h| h.get(s)) {
|
||||
match ldc.action_type {
|
||||
ActionType::SubMenu => {
|
||||
self.path.push(s.to_owned());
|
||||
self.lastpath = "".to_owned();
|
||||
},
|
||||
ActionType::Render => {
|
||||
self.lastpath = s.to_owned();
|
||||
},
|
||||
ActionType::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn goback(&mut self)->bool {
|
||||
if self.path.len()>0 {
|
||||
if !self.lastpath.is_empty() {
|
||||
self.lastpath="".to_owned();
|
||||
} else {
|
||||
self.path.pop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
pub fn handle_input(&mut self, kc: ratatui::crossterm::event::KeyCode) -> bool {
|
||||
if let Some(gcw) = self.get_current_widget() {
|
||||
return (gcw.handle_input)(kc);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
pub mod AsyncUpdateAnimeList {
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use ratatui::{buffer::Buffer, layout::Rect, style::{Style, palette::tailwind}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}};
|
||||
|
||||
use crate::{source::AnimeSource, utils::utils::{first_capital, hash_sort}};
|
||||
|
||||
pub enum AsyncAnimeStateStatus {
|
||||
Empty,
|
||||
Loading,
|
||||
Success(HashMap<String,String>),
|
||||
Error(String)
|
||||
}
|
||||
pub struct AsyncAnimeState {
|
||||
pub list_state: ListState,
|
||||
pub status: AsyncAnimeStateStatus,
|
||||
pub page: usize,
|
||||
pub res: Option<HashMap<String, String>>,
|
||||
pub animesource: Arc<Box<dyn AnimeSource + Send + Sync>>
|
||||
}
|
||||
|
||||
impl AsyncAnimeState {
|
||||
pub fn new(animesource: Arc<Box<dyn AnimeSource + Send + Sync>>) -> Self {
|
||||
let mut list_state = ListState::default();
|
||||
list_state.select_first();
|
||||
|
||||
Self {
|
||||
list_state,
|
||||
status: AsyncAnimeStateStatus::Empty,
|
||||
page: 1,
|
||||
res: None,
|
||||
animesource
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct AsyncUpdateAnimeList {
|
||||
trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<AsyncAnimeStateStatus>,
|
||||
}
|
||||
impl AsyncUpdateAnimeList {
|
||||
pub fn new(trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<AsyncAnimeStateStatus>) -> Self {
|
||||
Self {
|
||||
trasmitter_to_app,
|
||||
}
|
||||
}
|
||||
pub fn formatEpisodeIndexTitle(index: usize, title:&str) -> String {
|
||||
return format!("Ep. {:>4} di {}",index,title);
|
||||
}
|
||||
}
|
||||
impl StatefulWidget for AsyncUpdateAnimeList {
|
||||
type State = AsyncAnimeState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let block = Block::default().title(format!(" {} - Page {} ", first_capital(&(state.animesource.get_hostname())), state.page)).borders(Borders::ALL);
|
||||
match &mut state.status {
|
||||
AsyncAnimeStateStatus::Empty => {
|
||||
state.status = AsyncAnimeStateStatus::Loading;
|
||||
let tx = self.trasmitter_to_app.clone();
|
||||
let source = state.animesource.clone();
|
||||
let h = source.get_hostname();
|
||||
let p = state.page.clone();
|
||||
tokio::spawn(async move {
|
||||
let res = source.listupdated(p).await;
|
||||
match res {
|
||||
Ok(o) => {
|
||||
let mut h = HashMap::new();
|
||||
for a in o {
|
||||
let s = Self::formatEpisodeIndexTitle(a.index,&a.title);
|
||||
h.insert(s.clone(), a.url.clone());
|
||||
}
|
||||
tx.send(AsyncAnimeStateStatus::Success(h))
|
||||
},
|
||||
Err(_e) => tx.send(AsyncAnimeStateStatus::Error("Failed to load".to_owned()))
|
||||
}
|
||||
});
|
||||
Paragraph::new("Starting the request").block(block).render(area, buf);
|
||||
}
|
||||
AsyncAnimeStateStatus::Loading => {
|
||||
Paragraph::new("Loading").block(block.border_style(Style::new().fg(tailwind::AMBER.c200))).render(area, buf);
|
||||
|
||||
}
|
||||
AsyncAnimeStateStatus::Error(err) => {
|
||||
Paragraph::new(format!("Error: {}", err)).block(block).render(area, buf);
|
||||
}
|
||||
AsyncAnimeStateStatus::Success(res) => {
|
||||
state.res = Some(res.clone());
|
||||
let items = hash_sort(&res);
|
||||
let list_items: Vec<ListItem> = items.iter()
|
||||
.map(|a| ListItem::new(format!("{}", a)))
|
||||
.collect();
|
||||
StatefulWidget::render(
|
||||
List::new(list_items).block(block.border_style(Style::new().fg(ratatui::style::Color::Green))).highlight_symbol(">> "),
|
||||
area,
|
||||
buf,
|
||||
&mut state.list_state
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod Library {
|
||||
use std::{collections::HashMap, hash::Hash, sync::Arc};
|
||||
|
||||
use ratatui::{buffer::Buffer, layout::{Constraint, Layout, Rect}, style::{Style, palette::tailwind}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}};
|
||||
|
||||
use crate::{library::{self, LibraryItem}, source::{EpisodeListItem, MangaSource}, utils::utils::hash_sort};
|
||||
pub struct Library {
|
||||
pub trasmitter_to_app:tokio::sync::mpsc::UnboundedSender<HashMap<String, EpisodeListItem>>,
|
||||
pub library_type: LibraryType
|
||||
}
|
||||
impl Library {
|
||||
pub fn new(library_type: LibraryType,trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<HashMap<String, EpisodeListItem>>) -> Self {
|
||||
Self {
|
||||
library_type,
|
||||
trasmitter_to_app,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub enum LibraryStateStatus {
|
||||
Empty,
|
||||
Lib(usize),
|
||||
}
|
||||
|
||||
|
||||
pub struct LibraryState {
|
||||
pub list_state_source: ListState,
|
||||
pub list_state_eps: ListState,
|
||||
pub lib: HashMap<String, Vec<LibraryItem>>,
|
||||
pub status: LibraryStateStatus,
|
||||
pub entry_selected: bool,
|
||||
pub episodes_loaded: Option<HashMap<String, EpisodeListItem>>,
|
||||
pub current_source: Option<String>
|
||||
}
|
||||
impl LibraryState {
|
||||
pub fn new() -> Self {
|
||||
let mut list_state_source = ListState::default();
|
||||
let mut list_state_eps = ListState::default();
|
||||
list_state_source.select_first();
|
||||
list_state_eps.select_first();
|
||||
|
||||
Self {
|
||||
lib: HashMap::new(),
|
||||
list_state_source,
|
||||
list_state_eps,
|
||||
entry_selected: false,
|
||||
status: LibraryStateStatus::Empty,
|
||||
episodes_loaded: None,
|
||||
current_source: None
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy)]
|
||||
pub enum LibraryType {
|
||||
Anime, Manga
|
||||
}
|
||||
// TODO
|
||||
impl StatefulWidget for Library {
|
||||
type State = LibraryState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let block = Block::default().title("Library").borders(Borders::ALL);
|
||||
let lay = Layout::default().direction(ratatui::layout::Direction::Horizontal).constraints([
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Min(30)
|
||||
]).split(area);
|
||||
match &mut state.status {
|
||||
LibraryStateStatus::Empty => {
|
||||
let f = match self.library_type {
|
||||
LibraryType::Anime => "animelibrary.toml",
|
||||
LibraryType::Manga => "mangalibrary.toml"
|
||||
};
|
||||
if let Ok(lib) = library::LibraryItem::buildfromName(f) {
|
||||
state.lib = lib;
|
||||
state.status = LibraryStateStatus::Lib(0);
|
||||
state.entry_selected = false;
|
||||
return Paragraph::new("Lib loaded").block(block).render(area, buf);
|
||||
} else {
|
||||
return Paragraph::new("No lib found. Edit ./library/animelibrary.toml or ./library/mangalibrary.toml. Format:\n[[Animeworld]]\nname=\"Frieren\"\nepisodes=\"https://url_to_a_generic_episode_of_animeworld_frieren_or_chapter_list_manga_page/\"").block(block).render(area, buf);
|
||||
}
|
||||
}
|
||||
LibraryStateStatus::Lib(index) => {
|
||||
let mut keys: Vec<&String> = state.lib.keys().collect();
|
||||
keys.sort();
|
||||
let i = index.clone();
|
||||
let l = keys.len();
|
||||
let block_sc = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().fg(ratatui::style::Color::Cyan));
|
||||
let block_eps = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().fg(ratatui::style::Color::Cyan));
|
||||
if let Some(k) = keys.get(i % l) {
|
||||
let key = *k;
|
||||
if let Some(a) = state.lib.get(key) {
|
||||
state.current_source = Some(key.clone());
|
||||
let list = List::new(a.iter().map(|li| ListItem::new(li.name.clone())).collect::<Vec<ListItem>>()).block(block_sc);
|
||||
StatefulWidget::render(list, lay[0], buf, &mut state.list_state_source);
|
||||
} else {
|
||||
Widget::render(block_sc, lay[0], buf);
|
||||
}
|
||||
} else {
|
||||
Widget::render(block_sc, lay[0], buf);
|
||||
}
|
||||
if state.entry_selected && let Some(eps_hash) = &state.episodes_loaded {
|
||||
let mut items = eps_hash.keys().collect::<Vec<&String>>();
|
||||
items.sort_by(|a,b| b.cmp(a)); // ? REVERSE SORT - MAX TO MIN -> NEWEST FIRST
|
||||
let mut eps = Vec::with_capacity(items.len());
|
||||
for it in items {
|
||||
eps.push(ListItem::new(it.clone()));
|
||||
}
|
||||
let list_ep = List::new(eps).block(block_eps);
|
||||
StatefulWidget::render(list_ep, lay[1], buf, &mut state.list_state_eps);
|
||||
} else {
|
||||
Widget::render(block_eps, lay[1], buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod AsyncUpdateMangaList {
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use ratatui::{buffer::Buffer, layout::Rect, style::{Style, palette::tailwind}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}};
|
||||
|
||||
use crate::{source::{MangaSource}, utils::utils::{first_capital, hash_sort}};
|
||||
|
||||
pub enum AsyncMangaStateStatus {
|
||||
Empty,
|
||||
Loading,
|
||||
Success(HashMap<String,String>),
|
||||
Error(String)
|
||||
}
|
||||
pub struct AsyncMangaState {
|
||||
pub list_state: ListState,
|
||||
pub status: AsyncMangaStateStatus,
|
||||
pub page: usize,
|
||||
pub res: Option<HashMap<String, String>>,
|
||||
pub mangasource: Arc<Box<dyn MangaSource + Send + Sync>>
|
||||
}
|
||||
|
||||
impl AsyncMangaState {
|
||||
pub fn new(mangasource: Arc<Box<dyn MangaSource + Send + Sync>>) -> Self {
|
||||
let mut list_state = ListState::default();
|
||||
list_state.select_first();
|
||||
|
||||
Self {
|
||||
list_state,
|
||||
status: AsyncMangaStateStatus::Empty,
|
||||
page: 1,
|
||||
res: None,
|
||||
mangasource
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct AsyncUpdateMangaList {
|
||||
trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<AsyncMangaStateStatus>,
|
||||
}
|
||||
impl AsyncUpdateMangaList {
|
||||
pub fn new(trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<AsyncMangaStateStatus>) -> Self {
|
||||
Self {
|
||||
trasmitter_to_app,
|
||||
}
|
||||
}
|
||||
pub fn formatEpisodeIndexTitle(index: usize, title:&str) -> String {
|
||||
return format!("Ep. {:>4} di {}",index,title);
|
||||
}
|
||||
}
|
||||
impl StatefulWidget for AsyncUpdateMangaList {
|
||||
type State = AsyncMangaState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let block = Block::default().title(format!(" {} - Page {} ", first_capital(&(state.mangasource.get_hostname())), state.page)).borders(Borders::ALL);
|
||||
match &mut state.status {
|
||||
AsyncMangaStateStatus::Empty => {
|
||||
state.status = AsyncMangaStateStatus::Loading;
|
||||
let tx = self.trasmitter_to_app.clone();
|
||||
let source = state.mangasource.clone();
|
||||
let h = source.get_hostname();
|
||||
let p = state.page.clone();
|
||||
tokio::spawn(async move {
|
||||
let res = source.listupdated(p).await;
|
||||
match res {
|
||||
Ok(o) => {
|
||||
let mut h = HashMap::new();
|
||||
for a in o {
|
||||
let s = Self::formatEpisodeIndexTitle(a.index,&a.title);
|
||||
h.insert(s.clone(), a.url.clone());
|
||||
}
|
||||
tx.send(AsyncMangaStateStatus::Success(h))
|
||||
},
|
||||
Err(_e) => tx.send(AsyncMangaStateStatus::Error("Failed to load".to_owned()))
|
||||
}
|
||||
});
|
||||
Paragraph::new("Starting the request").block(block).render(area, buf);
|
||||
}
|
||||
AsyncMangaStateStatus::Loading => {
|
||||
Paragraph::new("Loading").block(block.border_style(Style::new().fg(tailwind::AMBER.c200))).render(area, buf);
|
||||
|
||||
}
|
||||
AsyncMangaStateStatus::Error(err) => {
|
||||
Paragraph::new(format!("Error: {}", err)).block(block).render(area, buf);
|
||||
}
|
||||
AsyncMangaStateStatus::Success(res) => {
|
||||
state.res = Some(res.clone());
|
||||
let items = hash_sort(&res);
|
||||
let list_items: Vec<ListItem> = items.iter()
|
||||
.map(|a| ListItem::new(format!("{}", a)))
|
||||
.collect();
|
||||
StatefulWidget::render(
|
||||
List::new(list_items).block(block.border_style(Style::new().fg(ratatui::style::Color::Green))).highlight_symbol(">> "),
|
||||
area,
|
||||
buf,
|
||||
&mut state.list_state
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use std::{default, fs};
|
||||
use ratatui::style::palette::tailwind;
|
||||
use serde::Deserialize;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub Settings: Settings,
|
||||
pub Customization: Customization
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Settings {
|
||||
pub Folders: Folders
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Folders {
|
||||
pub basedir: String,
|
||||
pub extensions: String,
|
||||
pub anime: String,
|
||||
pub manga: String
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct Customization {
|
||||
pub ColorPalette: ColorPalette
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct ColorPalette {
|
||||
pub anime: String,
|
||||
pub manga: String,
|
||||
pub source: String,
|
||||
pub settings: String
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||
|
||||
pub fn getConfig() -> &'static Config {
|
||||
CONFIG.get().expect("No init")
|
||||
}
|
||||
|
||||
pub fn load_config() {
|
||||
let content = fs::read_to_string("config.toml").expect("Can't read config file");
|
||||
let parsed:Config = toml::from_str(&content).unwrap();
|
||||
CONFIG.set(parsed).ok();
|
||||
// Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn tailwindPaletteFromString(c: &str, def: tailwind::Palette) -> tailwind::Palette {
|
||||
let res = match c {
|
||||
"AMBER" => tailwind::AMBER,
|
||||
"BLUE" => tailwind::BLUE,
|
||||
"CYAN" => tailwind::CYAN,
|
||||
"EMERALD" => tailwind::EMERALD,
|
||||
"FUCHSIA" => tailwind::FUCHSIA,
|
||||
"GRAY" => tailwind::GRAY,
|
||||
"GREEN" => tailwind::GREEN,
|
||||
"INDIGO" => tailwind::INDIGO,
|
||||
"LIME" => tailwind::LIME,
|
||||
"NEUTRAL" => tailwind::NEUTRAL,
|
||||
"ORANGE" => tailwind::ORANGE,
|
||||
"PINK" => tailwind::PINK,
|
||||
"PURPLE" => tailwind::PURPLE,
|
||||
"RED" => tailwind::RED,
|
||||
"ROSE" => tailwind::ROSE,
|
||||
"SKY" => tailwind::SKY,
|
||||
"SLATE" => tailwind::SLATE,
|
||||
"STONE" => tailwind::STONE,
|
||||
"TEAL" => tailwind::TEAL,
|
||||
"VIOLET" => tailwind::VIOLET,
|
||||
"YELLOW" => tailwind::YELLOW,
|
||||
"ZINC" => tailwind::ZINC,
|
||||
_ => def
|
||||
};
|
||||
res
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use std::fs;
|
||||
|
||||
use std::result::Result::Ok;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::source::{AnimeSource, MangaSource, SourceType};
|
||||
use crate::source::animeworld::Animeworld;
|
||||
use crate::source::mangaworld::Mangaworld;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct Extension {
|
||||
Type: String,
|
||||
Info: ExtensionInfo,
|
||||
Sitemanager: ExtensionSitemanager
|
||||
}
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct ExtensionInfo {
|
||||
Name: String,
|
||||
Description: String,
|
||||
Language: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct ExtensionSitemanager {
|
||||
Hostname: String,
|
||||
Url: String,
|
||||
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
pub fn buildfromName(kind: SourceType,name: &str) -> Result<Extension, &str> {
|
||||
let f = format!("extensions/{}/{}.toml",kind.to_string(), name.to_lowercase());
|
||||
if let Ok(exists)= fs::exists(&f) {
|
||||
if exists {
|
||||
if let Ok(content)= fs::read_to_string(&f) {
|
||||
let parsed:Extension = toml::from_str(&content).unwrap();
|
||||
return Ok(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(name);
|
||||
}
|
||||
pub fn toAnimeSource(self) -> Box<dyn AnimeSource+ Send + Sync> {
|
||||
match self.Info.Name.trim().to_lowercase().as_str() {
|
||||
// "animeworld" => Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url),
|
||||
_ => Box::new(Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url))
|
||||
}
|
||||
}
|
||||
pub fn toMangaSource(self) -> Box<dyn MangaSource+ Send + Sync> {
|
||||
match self.Info.Name.trim().to_lowercase().as_str() {
|
||||
// "animeworld" => Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url),
|
||||
_ => Box::new(Mangaworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url)),
|
||||
}
|
||||
}
|
||||
pub fn defaultAnimeSource() -> Result<Box<dyn AnimeSource>, String> {
|
||||
let def = "animeworld";
|
||||
if let Ok(ext) = Extension::buildfromName(SourceType::Anime, def) {
|
||||
return Ok(ext.toAnimeSource());
|
||||
}
|
||||
return Err(format!("No default file for {}",def));
|
||||
}
|
||||
pub fn defaultMangaeSource() -> Result<Box<dyn MangaSource>, String> {
|
||||
let def = "mangaworld";
|
||||
if let Ok(ext) = Extension::buildfromName(SourceType::Anime, def) {
|
||||
return Ok(ext.toMangaSource());
|
||||
}
|
||||
return Err(format!("No default file for {}",def));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::{ffi::OsString, fs::{create_dir_all, read_dir}};
|
||||
use serde::Deserialize;
|
||||
|
||||
// const DOWNLOADS: &str = "/downloads";
|
||||
const ANIME_FOLDER: &str = "./downloads/anime";
|
||||
const MANGA_FOLDER: &str = "./downloads/manga";
|
||||
|
||||
pub fn init_folders() {
|
||||
let _ = create_dir_all(ANIME_FOLDER);
|
||||
let _ = create_dir_all(MANGA_FOLDER);
|
||||
}
|
||||
|
||||
|
||||
pub fn list_folder(folder: &str) -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
|
||||
let r = read_dir(folder)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().is_dir())
|
||||
.map(|entry| entry.file_name())
|
||||
.collect::<Vec<OsString>>();
|
||||
Ok(r)
|
||||
}
|
||||
pub fn read_anime_folder() -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
|
||||
list_folder(ANIME_FOLDER)
|
||||
}
|
||||
pub fn read_manga_folder() -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
|
||||
list_folder(MANGA_FOLDER)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub Settings: Settings,
|
||||
pub Extension: Extensions,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Settings {
|
||||
pub Folders: Folders
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct Extensions {
|
||||
pub defaultanime: String,
|
||||
pub defaultmanga: String
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct Folders {
|
||||
pub basedir: String,
|
||||
pub extensions: String,
|
||||
pub anime: String,
|
||||
pub manga: String
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct ColorPalette {
|
||||
pub anime: String,
|
||||
pub manga: String,
|
||||
pub source: String,
|
||||
pub settings: String
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// impl Config {
|
||||
// pub fn defaultAnimeSource() -> Source {
|
||||
// Animeworld::new(hostname, url, sourcetype)
|
||||
// }
|
||||
// }
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
use std::{fs, u8};
|
||||
use ratatui::crossterm::event::KeyCode;
|
||||
use serde::Deserialize;
|
||||
#[derive(Deserialize)]
|
||||
pub struct KeybindsString {
|
||||
pub GoBack:String,
|
||||
pub Open:String,
|
||||
pub MenuUp:String,
|
||||
pub MenuDown:String,
|
||||
pub MenuLeft:String,
|
||||
pub MenuRight:String,
|
||||
pub AddFavourite:String,
|
||||
}
|
||||
pub struct Keybinds {
|
||||
pub GoBack:KeyCode,
|
||||
pub Open:KeyCode,
|
||||
pub MenuUp:KeyCode,
|
||||
pub MenuDown:KeyCode,
|
||||
pub MenuLeft:KeyCode,
|
||||
pub MenuRight:KeyCode,
|
||||
pub AddFavourite:KeyCode,
|
||||
}
|
||||
impl KeybindsString {
|
||||
pub fn build() -> Result<Self, String> {
|
||||
let f = "keybinds.toml";
|
||||
if let Ok(exists)= fs::exists(f) {
|
||||
if exists {
|
||||
if let Ok(content)= fs::read_to_string(f) {
|
||||
let parsed:KeybindsString = toml::from_str(&content).unwrap();
|
||||
return Ok(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(f.to_owned());
|
||||
}
|
||||
}
|
||||
impl Keybinds {
|
||||
pub fn build() -> Result<Keybinds, String> {
|
||||
let x: Result<Keybinds, String> = match KeybindsString::build() {
|
||||
Ok(ks) => Ok(
|
||||
Self {
|
||||
GoBack: ks.GoBack.to_key_code()?,
|
||||
Open: ks.Open.to_key_code()?,
|
||||
MenuUp: ks.MenuUp.to_key_code()?,
|
||||
MenuDown: ks.MenuDown.to_key_code()?,
|
||||
MenuLeft: ks.MenuLeft.to_key_code()?,
|
||||
MenuRight: ks.MenuRight.to_key_code()?,
|
||||
AddFavourite: ks.AddFavourite.to_key_code()?,
|
||||
}
|
||||
),
|
||||
Err(f) => Err(f),
|
||||
};
|
||||
x
|
||||
}
|
||||
pub fn buildOrDefault() -> Result<Keybinds, String> {
|
||||
let x: Result<Keybinds, String> = match KeybindsString::build() {
|
||||
Ok(ks) => Ok(
|
||||
Self {
|
||||
GoBack: ks.GoBack.to_key_code().unwrap_or(KeyCode::Esc),
|
||||
Open: ks.Open.to_key_code().unwrap_or(KeyCode::Enter),
|
||||
MenuUp: ks.MenuUp.to_key_code().unwrap_or(KeyCode::Up),
|
||||
MenuDown: ks.MenuDown.to_key_code().unwrap_or(KeyCode::Down),
|
||||
MenuLeft: ks.MenuLeft.to_key_code().unwrap_or(KeyCode::Left),
|
||||
MenuRight: ks.MenuRight.to_key_code().unwrap_or(KeyCode::Right),
|
||||
AddFavourite: ks.AddFavourite.to_key_code().unwrap_or(KeyCode::Char('p')),
|
||||
}
|
||||
),
|
||||
Err(f) => Err(f),
|
||||
};
|
||||
x
|
||||
}
|
||||
}
|
||||
trait StringToKeyCode {
|
||||
fn to_key_code(self) -> Result<KeyCode,String>;
|
||||
}
|
||||
|
||||
impl StringToKeyCode for String {
|
||||
fn to_key_code(self) -> Result<KeyCode, String> {
|
||||
match self.to_uppercase().as_ref() {
|
||||
"BACKSPACE"=>Ok(KeyCode::Backspace),
|
||||
"ENTER"=>Ok(KeyCode::Enter),
|
||||
"LEFT"=>Ok(KeyCode::Left),
|
||||
"RIGHT"=>Ok(KeyCode::Right),
|
||||
"UP"=>Ok(KeyCode::Up),
|
||||
"DOWN"=>Ok(KeyCode::Down),
|
||||
"HOME"=>Ok(KeyCode::Home),
|
||||
"END"=>Ok(KeyCode::End),
|
||||
"PAGEUP"=>Ok(KeyCode::PageUp),
|
||||
"PAGEDOWN"=>Ok(KeyCode::PageDown),
|
||||
"TAB"=>Ok(KeyCode::Tab),
|
||||
"BACKTAB"=>Ok(KeyCode::BackTab),
|
||||
"DELETE"=>Ok(KeyCode::Delete),
|
||||
"INSERT"=>Ok(KeyCode::Insert),
|
||||
"ESC"=>Ok(KeyCode::Esc),
|
||||
"CAPSLOCK"=>Ok(KeyCode::CapsLock),
|
||||
"SCROLLLOCK"=>Ok(KeyCode::ScrollLock),
|
||||
"NUMLOCK"=>Ok(KeyCode::NumLock),
|
||||
"PRINTSCREEN"=>Ok(KeyCode::PrintScreen),
|
||||
"PAUSE"=>Ok(KeyCode::Pause),
|
||||
"MENU"=>Ok(KeyCode::Menu),
|
||||
"KEYPADBEGIN"=>Ok(KeyCode::KeypadBegin),
|
||||
|
||||
a => {
|
||||
let chars: Vec<char> = a.chars().collect();
|
||||
return match chars.as_slice() {
|
||||
[c] => Ok(KeyCode::Char(*c)),
|
||||
['F', d] if d.is_ascii_digit() =>{
|
||||
let dg = d.to_digit(10).unwrap() as u8;
|
||||
Ok(KeyCode::F(dg))
|
||||
},
|
||||
_ => Err(format!("{} is invalid", self))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
|
||||
use std::result::Result::Ok;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize,Clone)]
|
||||
pub struct LibraryItem {
|
||||
pub name: String,
|
||||
pub episodes: String
|
||||
}
|
||||
|
||||
|
||||
impl LibraryItem {
|
||||
pub fn buildfromName(name: &str) -> Result<HashMap<String, Vec<LibraryItem>>, &str> {
|
||||
let f = format!("library/{}", name);
|
||||
if let Ok(exists)= fs::exists(&f) {
|
||||
if exists {
|
||||
if let Ok(content)= fs::read_to_string(&f) {
|
||||
let mut data: HashMap<String, Vec<LibraryItem>> = toml::from_str(&content).unwrap();
|
||||
for items in data.values_mut() {
|
||||
let mut hs = HashSet::new();
|
||||
items.retain(|item| {
|
||||
hs.insert(item.name.clone())
|
||||
});
|
||||
items.sort_by(|a,b| {
|
||||
a.name.cmp(&b.name)
|
||||
});
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(name);
|
||||
}
|
||||
}
|
||||
Executable
+233
@@ -0,0 +1,233 @@
|
||||
mod config;
|
||||
mod mangatab;
|
||||
mod extensions;
|
||||
use color_eyre::{Result};
|
||||
use ratatui::{
|
||||
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, macros::horizontal, style::{Color, Stylize, palette::tailwind}, text::{Line, Text}, widgets::{Block, List, ListDirection, ListItem, Padding, Paragraph, Tabs, Widget}
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
use crate::{mangatab::MangaTab, config::{getConfig, load_config, tailwindPaletteFromString}};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
load_config();
|
||||
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
selected_tab: SelectedTab
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
|
||||
enum SelectedTab {
|
||||
#[default]
|
||||
#[strum(to_string = "Anime")]
|
||||
Tab1,
|
||||
#[strum(to_string = "Manga")]
|
||||
Tab2,
|
||||
#[strum(to_string = "Source")]
|
||||
Tab3,
|
||||
#[strum(to_string = "Settings")]
|
||||
Tab4,
|
||||
}
|
||||
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
match key.code {
|
||||
KeyCode::Char('c') => self.quit(),
|
||||
_=>{}
|
||||
}
|
||||
}
|
||||
if key.modifiers.contains(KeyModifiers::SHIFT) {
|
||||
match key.code {
|
||||
KeyCode::Right => self.next_tab(),
|
||||
KeyCode::Left => self.previous_tab(),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.next();
|
||||
}
|
||||
|
||||
pub fn previous_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
}
|
||||
|
||||
pub fn subnext_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.subnext();
|
||||
}
|
||||
|
||||
pub fn subprevious_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.subprevious();
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||
fn previous(self) -> Self {
|
||||
let current_index: usize = self as usize;
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(previous_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Get the next tab, if there is no next tab return the current tab.
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min};
|
||||
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||
let [header_area, inner_area, footer_area] = vertical.areas(area);
|
||||
|
||||
let horizontal = Layout::horizontal([Min(0), Length(20)]);
|
||||
let [tabs_area, title_area] = horizontal.areas(header_area);
|
||||
|
||||
render_title(title_area, buf);
|
||||
self.render_tabs(tabs_area, buf);
|
||||
self.selected_tab.render(inner_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||
let titles = SelectedTab::iter().map(SelectedTab::title);
|
||||
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
|
||||
let selected_tab_index = self.selected_tab as usize;
|
||||
Tabs::new(titles)
|
||||
.highlight_style(highlight_style)
|
||||
.select(selected_tab_index)
|
||||
.padding("", "")
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
"Anime&Manga Scraper CLI".bold().render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Line::raw("Shift + ArrowKeys to choose the Tab | Press ESC to quit")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl Widget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// in a real app these might be separate widgets
|
||||
match self {
|
||||
Self::Tab1 => self.render_Anime(area, buf),
|
||||
Self::Tab2 => self.render_Manga(area, buf),
|
||||
Self::Tab3 => self.render_Sources(area, buf),
|
||||
Self::Tab4 => self.render_Settings(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Return tab's name as a styled `Line`
|
||||
fn title(self) -> Line<'static> {
|
||||
format!(" {self} ")
|
||||
.fg(tailwind::SLATE.c200)
|
||||
// .bg(self.palette().c900)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_Anime(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Anime Home Library: View saved and search for new one in all sources")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_Manga(self, area: Rect, buf: &mut Buffer) {
|
||||
// Paragraph::new("Manga Home Library: View saved and search for new one in all sources")
|
||||
// .block(self.block())
|
||||
// .render(area, buf);
|
||||
// INNER TAB FOR OPTIONS
|
||||
let titles = MangaTab::iter().map(MangaTab::title);
|
||||
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
|
||||
let selected_tab_index = self.selected_tab as usize;
|
||||
Tabs::new(titles)
|
||||
.highlight_style(highlight_style)
|
||||
.select(selected_tab_index)
|
||||
.padding("", "")
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
fn render_Sources(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Sources to source the web")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_Settings(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("For now just credits")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
|
||||
/// A block surrounding the tab's content
|
||||
fn block(self) -> Block<'static> {
|
||||
Block::bordered()
|
||||
.padding(Padding::horizontal(1))
|
||||
.border_style(self.palette().c500)
|
||||
}
|
||||
|
||||
fn palette(self) -> tailwind::Palette {
|
||||
match self {
|
||||
Self::Tab1 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.anime,tailwind::PURPLE),
|
||||
Self::Tab2 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.manga,tailwind::AMBER),
|
||||
Self::Tab3 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.source,tailwind::EMERALD),
|
||||
Self::Tab4 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.settings,tailwind::RED),
|
||||
}
|
||||
}
|
||||
}
|
||||
+587
@@ -0,0 +1,587 @@
|
||||
// mod config;
|
||||
// mod mangatab;
|
||||
mod extension;
|
||||
mod library;
|
||||
mod components;
|
||||
mod source;
|
||||
mod utils;
|
||||
mod folders;
|
||||
use std::{any::Any, cell::RefCell, collections::HashMap, process::{Command, Stdio}, rc::Rc, sync::Arc, time::Duration, vec};
|
||||
use color_eyre::{Result};
|
||||
use ratatui::{
|
||||
DefaultTerminal, Frame, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Direction, Layout, Rect}, style::Style, text::{Line, Span}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}
|
||||
};
|
||||
use crate::{components::{AsyncUpdateAnimeList::{self, AsyncAnimeState, AsyncAnimeStateStatus}, AsyncUpdateMangaList::{AsyncMangaState, AsyncMangaStateStatus}, Library::{self, LibraryState, LibraryStateStatus}, list::{ActionType, CurrentWidget, ListDiveChild, SelectableList}}, extension::Extension, source::{AVAIBLEANIMESOURCE, AnimeSource, EpisodeListItem, MangaSource, SourceType, animeworld, mangaworld}};
|
||||
//use crate::{mangatab::MangaTab, config::{getConfig, load_config, tailwindPaletteFromString}};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
//load_config();
|
||||
folders::init_folders();
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal).await;
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
//#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
navigation_menu: SelectableList,
|
||||
// pub anime_list: components::asyncListWidget::AsyncListWidget,
|
||||
anime: AnimeApp,
|
||||
manga: MangaApp,
|
||||
library: LibraryApp
|
||||
}
|
||||
struct AnimeApp {
|
||||
anime_source: Arc<Box<dyn AnimeSource + Send + Sync + 'static>>,
|
||||
tx: tokio::sync::mpsc::UnboundedSender<AsyncAnimeStateStatus>,
|
||||
rx: tokio::sync::mpsc::UnboundedReceiver<AsyncAnimeStateStatus>,
|
||||
anime_list_state: Rc<RefCell<AsyncAnimeState>>
|
||||
}
|
||||
struct MangaApp {
|
||||
manga_source: Arc<Box<dyn MangaSource + Send + Sync + 'static>>,
|
||||
tx: tokio::sync::mpsc::UnboundedSender<AsyncMangaStateStatus>,
|
||||
rx: tokio::sync::mpsc::UnboundedReceiver<AsyncMangaStateStatus>,
|
||||
manga_list_state: Rc<RefCell<AsyncMangaState>>
|
||||
}
|
||||
struct LibraryApp {
|
||||
tx: tokio::sync::mpsc::UnboundedSender<HashMap<String, EpisodeListItem>>,
|
||||
rx: tokio::sync::mpsc::UnboundedReceiver<HashMap<String, EpisodeListItem>>,
|
||||
library_list_state: Rc<RefCell<LibraryState>>
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
Running,
|
||||
Quitting,
|
||||
}
|
||||
impl App {
|
||||
fn new()->App {
|
||||
let (tx_a, rx_a) = tokio::sync::mpsc::unbounded_channel::<AsyncAnimeStateStatus>();
|
||||
let (tx_m, rx_m) = tokio::sync::mpsc::unbounded_channel::<AsyncMangaStateStatus>();
|
||||
let (tx_l, rx_l) = tokio::sync::mpsc::unbounded_channel::<HashMap<String, EpisodeListItem>>();
|
||||
let anime_source:Arc<Box<dyn AnimeSource + Send + Sync + 'static>> = Arc::new(Extension::defaultAnimeSource().unwrap_or(Box::new(animeworld::Animeworld::new("animeworld.ac".to_owned(), "https://animeworld.ac".to_owned())))); // TODO Make anime source selectable
|
||||
let manga_source:Arc<Box<dyn MangaSource + Send + Sync + 'static>> = Arc::new(Extension::defaultMangaeSource().unwrap_or(Box::new(mangaworld::Mangaworld::new("mangaworld.mx".to_owned(), "https://mangaworld.mx".to_owned())))); // TODO Make anime source selectable
|
||||
let a_s = anime_source.clone();
|
||||
let m_s = manga_source.clone();
|
||||
Self {
|
||||
state: AppState::Running,
|
||||
navigation_menu: SelectableList::default(),
|
||||
anime: AnimeApp {
|
||||
anime_source,
|
||||
anime_list_state: Rc::new(RefCell::new(AsyncAnimeState::new(a_s))),
|
||||
tx: tx_a,
|
||||
rx: rx_a
|
||||
},
|
||||
manga: MangaApp {
|
||||
manga_source,
|
||||
tx: tx_m,
|
||||
rx: rx_m,
|
||||
manga_list_state: Rc::new(RefCell::new(AsyncMangaState::new(m_s)))
|
||||
},
|
||||
library: LibraryApp {
|
||||
tx: tx_l,
|
||||
rx: rx_l,
|
||||
library_list_state: Rc::new(RefCell::new(LibraryState::new()))
|
||||
}
|
||||
// anime_list: AsyncListWidget {
|
||||
// title: "Animeworld".into(),
|
||||
// state: AsyncState::Empty,
|
||||
// scroll_state: ListState::default(),
|
||||
// animeurlhash: HashMap::new()
|
||||
// },
|
||||
}
|
||||
}
|
||||
fn withAnimeSource(&mut self, a: &str) {
|
||||
if let Ok(nas) = extension::Extension::buildfromName(source::SourceType::Anime, a) {
|
||||
let anime_s = nas.toAnimeSource();
|
||||
self.anime.anime_list_state=Rc::new(RefCell::new(AsyncAnimeState::new(Arc::new(anime_s))))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
fn withMangaSource(&mut self, m: Arc<Box<dyn MangaSource + Send + Sync + 'static>>) {
|
||||
|
||||
}
|
||||
fn init(&mut self) {
|
||||
// let tx_clone = self.tx.clone();
|
||||
self.navigation_menu = SelectableList::new(HashMap::from([
|
||||
("Anime".to_owned(), ListDiveChild::new(
|
||||
"Anime".to_owned(),
|
||||
0,
|
||||
ActionType::SubMenu,
|
||||
Some(HashMap::from([
|
||||
("Nuovi".to_owned(), ListDiveChild::new(
|
||||
"Nuovi".to_owned(),
|
||||
0,
|
||||
ActionType::Render,
|
||||
None,
|
||||
Some(self.block_anime_updated()),
|
||||
)),
|
||||
("Libreria".to_owned(), ListDiveChild::new(
|
||||
"Libreria".to_owned(),
|
||||
1,
|
||||
ActionType::Render,
|
||||
None,
|
||||
Some(self.block_library(Library::LibraryType::Anime))//Some(self.new_anime_list()),
|
||||
|
||||
))
|
||||
])),
|
||||
None,
|
||||
)),
|
||||
("Manga".to_owned(), ListDiveChild::new(
|
||||
"Manga".to_owned(),
|
||||
1,
|
||||
ActionType::SubMenu,
|
||||
Some(HashMap::from([
|
||||
("Library".to_owned(), ListDiveChild::new(
|
||||
"Library".to_owned(),
|
||||
0,
|
||||
ActionType::Render,
|
||||
None,
|
||||
Some(self.block_library(Library::LibraryType::Manga))
|
||||
)),
|
||||
("Search".to_owned(), ListDiveChild::new(
|
||||
"Search".to_owned(),
|
||||
1,
|
||||
ActionType::Render,
|
||||
None,
|
||||
None,
|
||||
))
|
||||
])),
|
||||
None,
|
||||
)),
|
||||
// ("Settings".to_owned(), ListDiveChild::new(
|
||||
// "Settings".to_owned(), 2, ActionType::SubMenu, Some(HashMap::from([
|
||||
// ("Source".to_owned(), ListDiveChild::new(
|
||||
// "Source".to_owned(),
|
||||
// 0,
|
||||
// ActionType::Render,
|
||||
// None,
|
||||
// Some(self.settings_selectSource())
|
||||
// ))
|
||||
// ])), None))
|
||||
]),Some(Self::generate_block()));
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
match self.anime.rx.try_recv() {
|
||||
Ok(a) => {
|
||||
self.anime.anime_list_state.borrow_mut().status=a;
|
||||
},
|
||||
Err(_) => {}
|
||||
};
|
||||
match self.manga.rx.try_recv() {
|
||||
Ok(a) => {
|
||||
self.manga.manga_list_state.borrow_mut().status=a;
|
||||
},
|
||||
Err(_) => {}
|
||||
};
|
||||
match self.library.rx.try_recv() {
|
||||
Ok(h)=>{
|
||||
self.library.library_list_state.borrow_mut().episodes_loaded = Some(h);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
async fn run(&mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
self.init();
|
||||
while self.state == AppState::Running {
|
||||
self.update();
|
||||
// log!("Draw");
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if event::poll(Duration::from_millis(500))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
k @ (KeyCode::Esc | KeyCode::Backspace) => {
|
||||
if !self.navigation_menu.handle_input(k) {
|
||||
if !self.navigation_menu.goback() {
|
||||
self.quit()
|
||||
}
|
||||
}
|
||||
},
|
||||
// KeyCode::Backspace => {self.navigation_menu.goback();},
|
||||
k @ (KeyCode::Up | KeyCode::Char('w'))=> {
|
||||
if self.navigation_menu.lastpath.is_empty() {
|
||||
self.navigation_menu.state.select_previous();
|
||||
} else {
|
||||
self.navigation_menu.handle_input(k);
|
||||
}
|
||||
},
|
||||
k @ (KeyCode::Down | KeyCode::Char('s'))=> {
|
||||
if self.navigation_menu.lastpath.is_empty() {
|
||||
self.navigation_menu.state.select_next();
|
||||
} else {
|
||||
self.navigation_menu.handle_input(k);
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if self.navigation_menu.lastpath.is_empty() {
|
||||
self.navigation_menu.enter();
|
||||
} else {
|
||||
self.navigation_menu.handle_input(KeyCode::Enter);
|
||||
}
|
||||
},
|
||||
a => {
|
||||
if !self.navigation_menu.lastpath.is_empty() {
|
||||
self.navigation_menu.handle_input(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
match key.code {
|
||||
KeyCode::Char('c') => self.quit(),
|
||||
_=>{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame) {
|
||||
let lay = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(1)
|
||||
.constraints(vec![
|
||||
Constraint::Percentage(15),
|
||||
Constraint::Percentage(85),
|
||||
])
|
||||
.split(frame.area());
|
||||
self.navigation_menu.render_list(frame, lay[0]);
|
||||
// self.navigation_menu(frame, lay[1]);
|
||||
self.navigation_menu.render_menu(frame, lay[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget inside app
|
||||
*/
|
||||
pub enum CmdListType {
|
||||
Single,Multi
|
||||
}
|
||||
impl App {
|
||||
pub fn generate_cmd_list(list: Vec<(CmdListType,&str, &str)>) -> Paragraph<'static> {
|
||||
let b = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().fg(ratatui::style::Color::LightCyan));
|
||||
let mut string_v:Vec<Span<'_>> = Vec::with_capacity(list.len()*5);
|
||||
let cmd_style = Style::new().fg(ratatui::style::Color::LightCyan);
|
||||
let text_style = Style::new().fg(ratatui::style::Color::White);
|
||||
for l in list {
|
||||
match l.0 {
|
||||
CmdListType::Multi => {
|
||||
for key in l.1.split(" or ") {
|
||||
let mut sp= key.split(",");
|
||||
if let Some(first) = sp.next() {
|
||||
string_v.push(Span::styled("[",text_style));
|
||||
string_v.push(Span::styled(first.to_owned(),cmd_style));
|
||||
while let Some(k) = sp.next() {
|
||||
string_v.push(Span::styled(",",text_style));
|
||||
string_v.push(Span::styled(k.to_string(),cmd_style));
|
||||
}
|
||||
string_v.push(Span::styled("]",text_style));
|
||||
string_v.push(Span::styled(" or ",text_style));
|
||||
}
|
||||
}
|
||||
string_v.pop();
|
||||
string_v.push(Span::styled(" ",text_style));
|
||||
string_v.push(Span::styled(l.2.to_owned(),text_style));
|
||||
},
|
||||
CmdListType::Single => {
|
||||
string_v.push(Span::styled("[",text_style));
|
||||
string_v.push(Span::styled(l.1.to_owned(),cmd_style));
|
||||
string_v.push(Span::styled("] ",text_style));
|
||||
string_v.push(Span::styled(l.2.to_owned(),text_style));
|
||||
}
|
||||
}
|
||||
string_v.push(Span::styled(";",text_style));
|
||||
string_v.push(Span::styled(" ",text_style));
|
||||
}
|
||||
string_v.pop();
|
||||
let line: Line<'_> = Line::from(string_v);
|
||||
Paragraph::new(line).block(b).centered()
|
||||
// Paragraph::new(String::from("[W,A,S,D] or [Arrows] to move;\t[Enter] select;\t[Esc] Go Back")).block(b).centered()
|
||||
}
|
||||
pub fn generate_block() -> CurrentWidget {
|
||||
CurrentWidget {
|
||||
render: Box::new(|frame: &mut Frame, area: Rect| {
|
||||
let b = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().fg(ratatui::style::Color::Cyan));
|
||||
let lay = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Min(3)
|
||||
]).split(area);
|
||||
let cmd_list = Self::generate_cmd_list(vec![
|
||||
(CmdListType::Multi, "W,A,S,D or Arrows","to move"),
|
||||
(CmdListType::Single, "Enter","select"),
|
||||
(CmdListType::Single, "Esc","Go Back"),
|
||||
]);
|
||||
frame.render_widget(b, lay[0]);
|
||||
frame.render_widget(cmd_list, lay[1]);
|
||||
}),
|
||||
handle_input: Box::new(|_| {false})
|
||||
}
|
||||
}
|
||||
pub fn block_anime_updated(&mut self) -> CurrentWidget {
|
||||
// let ls =.borrow_mut(); //
|
||||
let render_ls = Rc::clone(&self.anime.anime_list_state);
|
||||
let input_ls = Rc::clone(&self.anime.anime_list_state);
|
||||
let tx = self.anime.tx.clone();
|
||||
// let sc = self.anime_source.clone();
|
||||
CurrentWidget {
|
||||
render: Box::new(move |frame: &mut Frame, area: Rect| {
|
||||
let b = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().fg(ratatui::style::Color::LightCyan));
|
||||
let lay = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Min(3)
|
||||
]).split(area);
|
||||
// let fld = match read_anime_folder() {
|
||||
// Ok(a) => a,
|
||||
// _ => vec::Vec::new()
|
||||
// };
|
||||
// let items = fld.iter().enumerate().map(|(i, folder)| {
|
||||
// ListItem::new(folder.to_string_lossy().into_owned())
|
||||
// });
|
||||
let mut state = render_ls.borrow_mut();
|
||||
let tx_clone = tx.clone();
|
||||
|
||||
let l = AsyncUpdateAnimeList::AsyncUpdateAnimeList::new(tx_clone);//AsyncListWidget::new("Animeworld".to_owned(), self.animeSource);
|
||||
|
||||
frame.render_stateful_widget( l, lay[0], &mut *state);
|
||||
let cmd_list = Self::generate_cmd_list(vec![
|
||||
(CmdListType::Multi, "W,A,S,D or Arrows","to move"),
|
||||
(CmdListType::Single, "Enter","select"),
|
||||
(CmdListType::Single, "Esc","Go Back"),
|
||||
]); //// NOT WORK with match: Keybinds configuration from file;
|
||||
frame.render_widget( cmd_list, lay[1]);
|
||||
|
||||
}),
|
||||
handle_input: Box::new(move |k: KeyCode| {
|
||||
let mut state = input_ls.borrow_mut();
|
||||
match state.status {
|
||||
AsyncAnimeStateStatus::Success(_) => match k {
|
||||
KeyCode::Up | KeyCode::Char('w') => {
|
||||
state.list_state.select_previous();
|
||||
true
|
||||
},
|
||||
KeyCode::Down | KeyCode::Char('s')=> {
|
||||
state.list_state.select_next();
|
||||
true
|
||||
},
|
||||
KeyCode::Left | KeyCode::Char('a') => {
|
||||
state.list_state.select(None);
|
||||
state.page = std::cmp::max(state.page - 1, 1);
|
||||
state.status = AsyncAnimeStateStatus::Empty;
|
||||
true
|
||||
},
|
||||
KeyCode::Right | KeyCode::Char('d') => {
|
||||
state.list_state.select(None);
|
||||
state.page = state.page + 1;
|
||||
state.status = AsyncAnimeStateStatus::Empty;
|
||||
true
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
if let Some(u) = state.list_state.selected() && let Some(res) = state.res.clone() {
|
||||
let items = utils::utils::hash_sort(&res);
|
||||
if let Some(item) = items.get(u) {
|
||||
if let Some(url) = res.get(*item) {
|
||||
//source.episode_play_url(url);
|
||||
let so = state.animesource.clone();
|
||||
let u = url.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(vlc_url) = so.episode_play_url(&u).await {
|
||||
if !vlc_url.is_empty() {
|
||||
let _ = Command::new("vlc").arg(vlc_url)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => {false}
|
||||
},
|
||||
_=>{false}
|
||||
}
|
||||
// match k {
|
||||
// KeyCode::Up => {
|
||||
// state.list_state.select_previous();
|
||||
// },
|
||||
// KeyCode::Down => {
|
||||
// state.list_state.select_next();
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_library(&mut self, lib_type: Library::LibraryType) -> CurrentWidget {
|
||||
// let render_ls = Rc::clone(list_state);
|
||||
// let input_ls = Rc::clone(list_state);
|
||||
let render_ls = Rc::clone(&self.library.library_list_state);
|
||||
let input_ls = Rc::clone(&self.library.library_list_state);
|
||||
let tx_clone_render = self.library.tx.clone();
|
||||
let tx_clone_input = self.library.tx.clone();
|
||||
CurrentWidget {
|
||||
render: Box::new(move |frame: &mut Frame, area: Rect| {
|
||||
let b = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().fg(ratatui::style::Color::Cyan));
|
||||
let mut state = render_ls.borrow_mut();
|
||||
let l = Library::Library::new(lib_type,tx_clone_render.clone()); //TODO
|
||||
|
||||
//frame.render_widget(b, area);
|
||||
frame.render_stateful_widget(l, area, &mut *state);
|
||||
}), handle_input: Box::new(move |k: KeyCode| {
|
||||
let mut state = input_ls.borrow_mut();
|
||||
let tx_clone_input_clone = tx_clone_input.clone();
|
||||
match state.status {
|
||||
LibraryStateStatus::Lib(p) => {
|
||||
match k {
|
||||
KeyCode::Left | KeyCode::Char('a') => {
|
||||
state.list_state_source.select(None);
|
||||
state.status = LibraryStateStatus::Lib(p-1);
|
||||
true
|
||||
}
|
||||
KeyCode::Right | KeyCode::Char('d') => {
|
||||
state.list_state_source.select(None);
|
||||
state.status = LibraryStateStatus::Lib(p+1);
|
||||
true
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('w') => {
|
||||
if state.entry_selected {
|
||||
state.list_state_eps.select_previous();
|
||||
} else {
|
||||
state.list_state_source.select_previous();
|
||||
}
|
||||
true
|
||||
},
|
||||
KeyCode::Down | KeyCode::Char('s')=> {
|
||||
if state.entry_selected {
|
||||
state.list_state_eps.select_next();
|
||||
} else {
|
||||
state.list_state_source.select_next();
|
||||
}
|
||||
true
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
if state.entry_selected {
|
||||
|
||||
} else {
|
||||
let state_bo = &state;
|
||||
if let Some(sel) = state_bo.list_state_source.selected() && let Some(current) = state_bo.current_source.clone() {
|
||||
let kind = match lib_type {
|
||||
Library::LibraryType::Anime => SourceType::Anime,
|
||||
Library::LibraryType::Manga => SourceType::Manga
|
||||
};
|
||||
if let Some(u_list) = state_bo.lib.get(¤t) {
|
||||
if let Some(u) = u_list.get(sel) {
|
||||
let u_clone = u.clone();
|
||||
let lib_type_clone = lib_type.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(ex) = Extension::buildfromName(kind, ¤t) {
|
||||
let hs = match lib_type_clone {
|
||||
Library::LibraryType::Anime => {
|
||||
let s: Box<dyn AnimeSource + Send + Sync> = ex.toAnimeSource();
|
||||
s.lib_url_parse(u_clone.episodes).await
|
||||
}
|
||||
Library::LibraryType::Manga => {
|
||||
let m: Box<dyn MangaSource + Send + Sync> = ex.toMangaSource();
|
||||
m.lib_url_parse(u_clone.episodes).await
|
||||
}
|
||||
};
|
||||
if let Ok(o) = hs {
|
||||
tx_clone_input_clone.send(o);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => {false}
|
||||
}
|
||||
},
|
||||
_ => {false}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// pub fn new_anime_list(&mut self) -> CurrentWidget {
|
||||
// // let ls = Rc::new(RefCell::new(ListState::default()));
|
||||
// // let render_ls = Rc::clone(&ls);
|
||||
// // let input_ls = Rc::clone(&ls);
|
||||
// CurrentWidget { render: Box::new(move |frame: &mut Frame, area: Rect| {
|
||||
// // self.anime_list.render_asynclist(area, frame.buffer_mut())
|
||||
// }), handle_input: Box::new(move |k: KeyCode| {}) }
|
||||
// }
|
||||
// pub fn block_manga_library(&mut self) -> CurrentWidget {
|
||||
// let ls = Rc::new(RefCell::new(ListState::default()));
|
||||
// let render_ls = Rc::clone(&ls);
|
||||
// let input_ls = Rc::clone(&ls);
|
||||
// CurrentWidget {
|
||||
// render: Box::new(move |frame: &mut Frame, area: Rect| {
|
||||
// let b = Block::new()
|
||||
// .borders(Borders::ALL)
|
||||
// .border_type(BorderType::Rounded)
|
||||
// .border_style(Style::new().fg(ratatui::style::Color::LightCyan));
|
||||
// // let fld = match read_manga_folder() {
|
||||
// // Ok(a) => a,
|
||||
// // _ => vec![]
|
||||
// // };
|
||||
// // let items = fld.iter().enumerate().map(|(i, folder)| {
|
||||
// // ListItem::new(folder.to_string_lossy().into_owned())
|
||||
// // });
|
||||
// // let l = List::new(items).block(b).highlight_style(Style::new().fg(tailwind::BLACK).bg(tailwind::AMBER.c200));
|
||||
// let mut state = render_ls.borrow_mut();
|
||||
// frame.render_stateful_widget(l, area, &mut *state);
|
||||
// }),
|
||||
// handle_input: Box::new(move |k: KeyCode| {
|
||||
// let mut state = input_ls.borrow_mut();
|
||||
// match k {
|
||||
// KeyCode::Up => {
|
||||
// state.select_previous();
|
||||
// },
|
||||
// KeyCode::Down => {
|
||||
// state.select_next();
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use ratatui::{
|
||||
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, macros::horizontal, style::{Color, Stylize, palette::tailwind}, text::{Line, Text}, widgets::{Block, List, ListDirection, ListItem, Padding, Paragraph, Tabs, Widget}
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Manga {
|
||||
selected_tab: MangaTab
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
|
||||
pub enum MangaTab {
|
||||
#[default]
|
||||
#[strum(to_string = "Latest")]
|
||||
Tab1,
|
||||
#[strum(to_string = "Top")]
|
||||
Tab2,
|
||||
#[strum(to_string = "Search")]
|
||||
Tab3
|
||||
}
|
||||
|
||||
impl MangaTab {
|
||||
pub fn title(self) -> Line<'static> {
|
||||
format!(" {self} ")
|
||||
.fg(tailwind::SLATE.c200)
|
||||
// .bg(self.palette().c900)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, style::{Color, Stylize, palette::tailwind}, symbols, text::Line, widgets::{Block, List, Padding, Paragraph, Tabs, Widget}
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
match key.code {
|
||||
KeyCode::Char('c') => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match key.code {
|
||||
KeyCode::Esc => self.quit(),
|
||||
// KeyCode::Up => self.up,
|
||||
// KeyCode
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let li = List::new([
|
||||
"1. Anime",
|
||||
"2. Manga",
|
||||
])
|
||||
.highlight_symbol(">>")
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub mod animeworld;
|
||||
pub mod mangaworld;
|
||||
|
||||
pub const AVAIBLEANIMESOURCE: [&str; 1] = ["animeworld"];
|
||||
#[derive(Clone)]
|
||||
pub enum SourceType {
|
||||
Anime, Manga
|
||||
}
|
||||
impl SourceType {
|
||||
pub fn to_string(self) -> String {
|
||||
match self {
|
||||
SourceType::Anime => "anime".to_owned(),
|
||||
SourceType::Manga => "manga".to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
pub enum SourceLanguages {
|
||||
It, Jp, Eng
|
||||
}
|
||||
|
||||
pub struct Series<S: AnimeSource> {
|
||||
episodes: Vec<Episode>,
|
||||
title: String,
|
||||
lang: SourceLanguages,
|
||||
url: String,
|
||||
source: S
|
||||
}
|
||||
pub struct Episode {
|
||||
pub title: String,
|
||||
pub index: usize,
|
||||
pub section: usize,
|
||||
pub url: String
|
||||
}
|
||||
pub struct EpisodeListItem {
|
||||
title: String,
|
||||
url: String
|
||||
}
|
||||
pub struct Comic<S: MangaSource> {
|
||||
episodes: Vec<Chapter>,
|
||||
title: String,
|
||||
lang: SourceLanguages,
|
||||
url: String,
|
||||
source: S
|
||||
}
|
||||
pub struct Chapter {
|
||||
pub title: String,
|
||||
pub index: usize,
|
||||
pub section: usize,
|
||||
pub url: String
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[async_trait]
|
||||
pub trait AnimeSource: Send + Sync {
|
||||
/*let mut items = hostname.split(".").collect::<Vec<&str>>();
|
||||
if let Some(f) = items.first() {
|
||||
if *f != "www" {
|
||||
items.insert(0, "www");
|
||||
}
|
||||
}
|
||||
Self {
|
||||
url: items.join("."),
|
||||
host: items[1..items.len() - 1].join("."),
|
||||
hostname: "https://".to_owned()+&items[1..].join("."),
|
||||
sourcetype
|
||||
} */
|
||||
fn new(hostname: String, url: String) -> Self where Self: Sized;
|
||||
|
||||
fn get_hostname(&self)->String;
|
||||
fn get_url(&self)->String;
|
||||
|
||||
/** {
|
||||
let p = std::cmp::max(1, page);
|
||||
let u = format!("{}/updated?page={}",self.url,p);
|
||||
let a = get(u).await?.error_for_status()?.text().await?;
|
||||
return Html::parse_document(&a);
|
||||
}*/
|
||||
async fn fetch_updated_page(&self, page: usize)-> Result<String, Box<dyn std::error::Error + Send + Sync>>;
|
||||
fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Episode>, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
|
||||
fn extract_episode_from_search(&self, document: String) -> Result<Vec<Episode>, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
async fn listupdated(&self, page: usize) -> Result<Vec<Episode>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let body = self.fetch_updated_page(page).await?;
|
||||
self.extract_episode_from_updated(body)
|
||||
}
|
||||
async fn search(&self, query: String, page: usize) -> Result<Vec<Episode>, Box<dyn std::error::Error + Send + Sync>>{
|
||||
let body = self.fetch_search_page(query, page).await?;
|
||||
self.extract_episode_from_search(body)
|
||||
}
|
||||
|
||||
async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>;
|
||||
async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
|
||||
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait MangaSource: Send + Sync {
|
||||
/*let mut items = hostname.split(".").collect::<Vec<&str>>();
|
||||
if let Some(f) = items.first() {
|
||||
if *f != "www" {
|
||||
items.insert(0, "www");
|
||||
}
|
||||
}
|
||||
Self {
|
||||
url: items.join("."),
|
||||
host: items[1..items.len() - 1].join("."),
|
||||
hostname: "https://".to_owned()+&items[1..].join("."),
|
||||
sourcetype
|
||||
} */
|
||||
fn new(hostname: String, url: String) -> Self where Self: Sized;
|
||||
|
||||
fn get_hostname(&self)->String;
|
||||
fn get_url(&self)->String;
|
||||
|
||||
/** {
|
||||
let p = std::cmp::max(1, page);
|
||||
let u = format!("{}/updated?page={}",self.url,p);
|
||||
let a = get(u).await?.error_for_status()?.text().await?;
|
||||
return Html::parse_document(&a);
|
||||
}*/
|
||||
async fn fetch_updated_page(&self, page: usize)-> Result<String, Box<dyn std::error::Error + Send + Sync>>;
|
||||
fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Chapter>, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
|
||||
fn extract_episode_from_search(&self, document: String) -> Result<Vec<Chapter>, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
async fn listupdated(&self, page: usize) -> Result<Vec<Chapter>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let body = self.fetch_updated_page(page).await?;
|
||||
self.extract_episode_from_updated(body)
|
||||
}
|
||||
async fn search(&self, query: String, page: usize) -> Result<Vec<Chapter>, Box<dyn std::error::Error + Send + Sync>>{
|
||||
let body = self.fetch_search_page(query, page).await?;
|
||||
self.extract_episode_from_search(body)
|
||||
}
|
||||
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>>;
|
||||
// async fn chapter(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>;
|
||||
// async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
|
||||
use std::{collections::HashMap, process::Command};
|
||||
|
||||
use reqwest::get;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
use crate::source::{AnimeSource, Episode, EpisodeListItem, SourceType};
|
||||
|
||||
pub struct Animeworld {
|
||||
url: String,
|
||||
hostname:String,
|
||||
sourcetype: SourceType
|
||||
}
|
||||
|
||||
impl Animeworld {
|
||||
fn id_extract( s:&str) -> Option<&str> {
|
||||
s.split("/").last()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl AnimeSource for Animeworld {
|
||||
fn get_hostname(&self)->String {
|
||||
self.hostname.clone()
|
||||
}
|
||||
fn get_url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
fn new(hostname: String, url: String) -> Self {
|
||||
Self {
|
||||
url,
|
||||
hostname,
|
||||
sourcetype: crate::source::SourceType::Anime
|
||||
}
|
||||
}
|
||||
async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Episode>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let body = self.fetch_updated_page(page).await?;
|
||||
self.extract_episode_from_updated(body)
|
||||
}
|
||||
fn extract_episode_from_search(&self, body: String) -> Result<Vec<crate::source::Episode>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// let sel = Selector::parse("div.film-list").unwrap();
|
||||
let document = Html::parse_document(&body);
|
||||
let sel_inner = Selector::parse("div.inner").unwrap();
|
||||
let sel_episode_number = Selector::parse("div.ep").unwrap();
|
||||
let sel_anime_name = Selector::parse("a.name").unwrap();
|
||||
let cards = document.select(&sel_inner)
|
||||
.filter_map(|card| {
|
||||
let carde = card.select(&sel_episode_number).next()?;
|
||||
let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
|
||||
// log!("index: {}", index);
|
||||
let carda = card.select(&sel_anime_name).next()?;
|
||||
|
||||
// log!("Card a found");
|
||||
let url = carda.value().attr("href").unwrap_or("").trim().to_owned();
|
||||
// log!("Card url: {}",url);
|
||||
let title = carda.inner_html().trim().to_owned();
|
||||
// log!("Card title: {}", title);
|
||||
Some(Episode {
|
||||
title,
|
||||
index,
|
||||
url,
|
||||
section: 0
|
||||
})
|
||||
});
|
||||
Ok(cards.collect::<Vec<Episode>>())
|
||||
}
|
||||
fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Episode>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.extract_episode_from_search(body)
|
||||
}
|
||||
|
||||
|
||||
async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let id= Self::id_extract(&card.url).unwrap();
|
||||
let u = format!("{}/api/episode/serverPlayerAnimeWorld?id={}", self.url, id);
|
||||
let r = get(u).await?.error_for_status()?.text().await?;
|
||||
let b = Html::parse_document(&r);
|
||||
let select_source = Selector::parse("source").unwrap();
|
||||
Ok(b.select(&select_source).next().unwrap().value().attr("src").unwrap_or("").to_owned())
|
||||
}
|
||||
async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
if let Some(id) = Self::id_extract(url) {
|
||||
let u = format!("{}/api/episode/serverPlayerAnimeWorld?id={}", self.url, id);
|
||||
match Command::new("curl")
|
||||
.arg("-L")
|
||||
.arg(u).output() {
|
||||
Ok(output) => {
|
||||
// log!("{}");
|
||||
if let Ok(html) = String::from_utf8(output.stdout) {
|
||||
let b = Html::parse_document(&html);
|
||||
let select_source = Selector::parse("source").unwrap();
|
||||
if let Some(c) = b.select(&select_source).next() {
|
||||
return Ok(c.attr("src").unwrap_or("").to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(String::new())
|
||||
|
||||
}
|
||||
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// if page < 1 { page = 1};
|
||||
let u = format!("{}?keyword={}&page={}",self.get_url(), query, page);
|
||||
let r = reqwest::get(u).await?.error_for_status()?.text().await?;
|
||||
return Ok(r);
|
||||
}
|
||||
|
||||
async fn search(&self, query: String, page: usize) -> Result<Vec<crate::source::Episode>, Box<dyn std::error::Error + Send + Sync>>{
|
||||
let body = self.fetch_search_page(query, page).await?;
|
||||
self.extract_episode_from_search(body)
|
||||
}
|
||||
|
||||
async fn fetch_updated_page(&self, page: usize)-> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let p = std::cmp::max(1, page);
|
||||
let u = format!("{}/updated?page={}",self.get_url(),p);
|
||||
let a = get(u).await?.error_for_status()?.text().await?;
|
||||
return Ok(a);
|
||||
}
|
||||
|
||||
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>>{
|
||||
let mut hs = HashMap::<String, EpisodeListItem>::new();
|
||||
if let Ok(a) = get(url).await?.error_for_status()?.text().await {
|
||||
let b = Html::parse_document(&a);
|
||||
let select_episodes = Selector::parse("li.episode a").unwrap();
|
||||
for ep in b.select(&select_episodes) {
|
||||
let alink = ep.value();
|
||||
hs.insert(ep.inner_html(), EpisodeListItem {
|
||||
title: format!("Ep. {:>4}",ep.inner_html()),
|
||||
url: alink.attr("href").unwrap_or("").trim().to_owned()
|
||||
});
|
||||
}
|
||||
}
|
||||
return Ok(hs);
|
||||
}
|
||||
// async fn fetch_html(url: String) -> Result<String, reqwest::Error> {
|
||||
// get(url).await?.error_for_status()?.text().await
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use reqwest::get;
|
||||
use scraper::{Html, Selector};
|
||||
use tokio::io::split;
|
||||
|
||||
use crate::source::{Chapter, EpisodeListItem, MangaSource, SourceType};
|
||||
|
||||
pub struct Mangaworld {
|
||||
url: String,
|
||||
hostname:String,
|
||||
sourcetype: SourceType
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MangaSource for Mangaworld {
|
||||
fn get_hostname(&self)->String {
|
||||
self.hostname.clone()
|
||||
}
|
||||
fn get_url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
fn new(hostname: String, url: String) -> Self {
|
||||
Self {
|
||||
url,
|
||||
hostname,
|
||||
sourcetype: crate::source::SourceType::Manga
|
||||
}
|
||||
}
|
||||
async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Chapter>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let body = self.fetch_updated_page(page).await?;
|
||||
self.extract_episode_from_updated(body)
|
||||
}
|
||||
//https://www.mangaworld.mx/archive?keyword=
|
||||
fn extract_episode_from_search(&self, body: String) -> Result<Vec<crate::source::Chapter>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let document = Html::parse_document(&body);
|
||||
let sel_inner = Selector::parse("div.entry").unwrap();
|
||||
// let sel_episode_number = Selector::parse("a.xanh").unwrap();
|
||||
let sel_anime_name = Selector::parse("a.manga-title").unwrap();
|
||||
let cards = document.select(&sel_inner)
|
||||
.filter_map(|card| {
|
||||
// let carde = card.select(&sel_episode_number).next()?;
|
||||
// let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
|
||||
// log!("index: {}", index);
|
||||
let carda = card.select(&sel_anime_name).next()?;
|
||||
|
||||
// log!("Card a found");
|
||||
let url = carda.value().attr("href").unwrap_or("").trim().to_owned();
|
||||
// log!("Card url: {}",url);
|
||||
let title = carda.inner_html().trim().to_owned();
|
||||
// log!("Card title: {}", title);
|
||||
Some(Chapter {
|
||||
title,
|
||||
index: 0,
|
||||
url,
|
||||
section: 0
|
||||
})
|
||||
});
|
||||
Ok(cards.collect::<Vec<Chapter>>())
|
||||
}
|
||||
//https://www.mangaworld.ac/
|
||||
fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Chapter>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let document = Html::parse_document(&body);
|
||||
let sel_inner = Selector::parse("div.entry").unwrap();
|
||||
let sel_episode_number = Selector::parse("a.xanh").unwrap();
|
||||
let sel_anime_name = Selector::parse("a.manga-title").unwrap();
|
||||
let cards = document.select(&sel_inner)
|
||||
.filter_map(|card| {
|
||||
let carde = card.select(&sel_episode_number).next()?;
|
||||
let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
|
||||
// log!("index: {}", index);
|
||||
let carda = card.select(&sel_anime_name).next()?;
|
||||
|
||||
// log!("Card a found");
|
||||
let url = carda.value().attr("href").unwrap_or("").trim().to_owned();
|
||||
// log!("Card url: {}",url);
|
||||
let title = carda.inner_html().trim().to_owned();
|
||||
// log!("Card title: {}", title);
|
||||
Some(Chapter {
|
||||
title,
|
||||
index,
|
||||
url,
|
||||
section: 0
|
||||
})
|
||||
});
|
||||
Ok(cards.collect::<Vec<Chapter>>())
|
||||
}
|
||||
// async fn episode_openable_link(&self, card: Chapter) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// let id= Self::id_extract(&card.url).unwrap();
|
||||
// let u = format!("{}/api/episode/serverPlayerAnimeWorld?id={}", self.url, id);
|
||||
// let r = get(u).await?.error_for_status()?.text().await?;
|
||||
// let b = Html::parse_document(&r);
|
||||
// let select_source = Selector::parse("source").unwrap();
|
||||
// Ok(b.select(&select_source).next().unwrap().value().attr("src").unwrap_or("").to_owned())
|
||||
// }
|
||||
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// if page < 1 { page = 1};
|
||||
let u = format!("{}?keyword={}&page={}",self.get_url(), query, page);
|
||||
let r = reqwest::get(u).await?.error_for_status()?.text().await?;
|
||||
return Ok(r);
|
||||
}
|
||||
|
||||
// async fn search(&self, query: String, page: usize) -> Result<Vec<crate::source::Episode>, Box<dyn std::error::Error + Send + Sync>>{
|
||||
// let body = self.fetch_search_page(query, page).await?;
|
||||
// self.extract_episode_from_search(body)
|
||||
// }
|
||||
|
||||
async fn fetch_updated_page(&self, page: usize)-> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let p = std::cmp::max(1, page);
|
||||
let u = format!("{}",self.get_url());
|
||||
let a = get(u).await?.error_for_status()?.text().await?;
|
||||
return Ok(a);
|
||||
}
|
||||
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut hs = HashMap::<String, EpisodeListItem>::new();
|
||||
if let Ok(a) = get(url).await?.error_for_status()?.text().await {
|
||||
let b = Html::parse_document(&a);
|
||||
// let select_chapters_wrapper = Selector::parse("").unwrap();
|
||||
let select_volume_element = Selector::parse(".chapters-wrapper .volume-element").unwrap();
|
||||
let select_volume_name = Selector::parse(".volume .volume-name").unwrap();
|
||||
let select_episodes = Selector::parse("div.chapter a.chap").unwrap();
|
||||
let select_span = Selector::parse("span").unwrap();
|
||||
// for ep in b.select(&select_episodes) {
|
||||
// // let alink = ep.value();
|
||||
// if let Some(span) = ep.select(&select_span).next() {
|
||||
// hs.insert(span.inner_html(), EpisodeListItem {
|
||||
// title: format!("Volume {:>4} Capitolo {:>4}",,span.inner_html()),
|
||||
// url: alink.attr("href").unwrap_or("").trim().to_owned()
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
for vol in b.select(&select_volume_element) {
|
||||
if let Some(volname) = vol.select(&select_volume_name).next() {
|
||||
let volume = volname.text().next();
|
||||
for ep in vol.select(&select_episodes) {
|
||||
if let Some(span) = ep.select(&select_span).next() {
|
||||
let alink = ep.value();
|
||||
let chapter = span.inner_html();
|
||||
let x = volume
|
||||
.and_then(|s| s.split_whitespace().nth(1))
|
||||
.and_then(|second| second.split_once('.'));
|
||||
let y = chapter.split_whitespace().nth(1)
|
||||
.and_then(|second| second.split_once('.'));
|
||||
|
||||
let title_vol = match x {
|
||||
Some((vola,volb)) => {
|
||||
if volb.is_empty() {
|
||||
format!("Volume {:>4}", vola)
|
||||
} else {
|
||||
format!("Volume {:>4}.{}", vola,volb)
|
||||
}
|
||||
},
|
||||
_ => "".to_owned()
|
||||
};
|
||||
let title_cha = match y {
|
||||
Some((cha,chb)) => {
|
||||
if chb.is_empty() {
|
||||
format!("Capitolo {:>4}", cha)
|
||||
} else {
|
||||
format!("Capitolo {:>4}.{}", cha,chb)
|
||||
}
|
||||
},
|
||||
_ => "".to_owned()
|
||||
};
|
||||
let title_sep = if !title_vol.is_empty() && !title_cha.is_empty() {" "} else {""};
|
||||
hs.insert(span.inner_html(), EpisodeListItem {
|
||||
title: format!("{}{}{}", title_vol,title_sep, title_cha),
|
||||
url: alink.attr("href").unwrap_or("").trim().to_owned()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(hs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
pub mod utils {
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($($arg:tt)*) => {{
|
||||
let file_result = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("./debug.log");
|
||||
|
||||
if let Ok(mut file) = file_result {
|
||||
use std::io::Write;
|
||||
let _ = writeln!(file, $($arg)*);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn first_capital(s: &str) -> String {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_sort(res: &HashMap<String, String>) -> Vec<&String> {
|
||||
let mut items = res.keys().collect::<Vec<&String>>();
|
||||
items.sort_by(|a,b|{
|
||||
let aname = a.splitn(2, "di ").nth(1).unwrap_or("");
|
||||
let bname = b.splitn(2, "di ").nth(1).unwrap_or("");
|
||||
match aname.cmp(bname) {
|
||||
std::cmp::Ordering::Equal => a.cmp(b),
|
||||
o => o
|
||||
}
|
||||
});
|
||||
items
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user