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