01/04/26_00.35

This commit is contained in:
Domipoke
2026-04-01 00:35:07 +02:00
commit 2f90d71911
25 changed files with 6231 additions and 0 deletions
+536
View File
@@ -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
);
}
}
}
}
}
+83
View File
@@ -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
}
+69
View File
@@ -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));
}
}
+67
View File
@@ -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
View File
@@ -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))
};
}
}
}
}
+38
View File
@@ -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
View File
@@ -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
View File
@@ -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(&current) {
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, &current) {
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();
// }
// _ => {}
// }
// })
// }
// }
}
+29
View File
@@ -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()
}
}
+73
View File
@@ -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
View File
@@ -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>>;
}
+139
View File
@@ -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
// }
}
+179
View File
@@ -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);
}
}
+39
View File
@@ -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
}
}