This commit is contained in:
Domipoke
2026-06-05 18:36:02 +02:00
parent 758dc3fad2
commit cdaf0c4a05
19 changed files with 5199 additions and 408 deletions
Generated
+1450 -19
View File
File diff suppressed because it is too large Load Diff
+22 -1
View File
@@ -8,10 +8,31 @@ async-trait = "0.1.89"
color-eyre = "0.6.5" color-eyre = "0.6.5"
curl = "0.4.49" curl = "0.4.49"
ratatui = "0.30.0" ratatui = "0.30.0"
reqwest = { version = "0.13.2", features = ["rustls"] } reqwest = { version = "0.13.2", features = ["rustls","blocking", "cookies"] }
scraper = "0.25.0" scraper = "0.25.0"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
strum = { version = "0.28", default-features = false, features = ["derive"] } strum = { version = "0.28", default-features = false, features = ["derive"] }
tokio = { version = "1.50.0", features = ["full"] } tokio = { version = "1.50.0", features = ["full"] }
toml = "1.0.4" toml = "1.0.4"
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
zip = "8.5.0"
crc32fast = "1.5.0"
hex = "0.4.3"
regex = "1.12.3"
base64 = "0.22.1"
rquickjs = "0.11.0"
aes = "0.8"
cbc = "0.1"
block-padding = "0.3"
anyhow = "1.0.102"
cipher = { version = "0.5.1", features = ["block-padding"]}
once_cell = "1.21.4"
image = "0.25.10"
boa_engine = "0.21.1"
url = "2.5.8"
[features]
default = ["debug"]
debug = []
+39 -9
View File
@@ -1,12 +1,42 @@
[Anime]
source = "animeworld"
[Settings] [Manga]
source = "mangaworld"
# Le opzioni sono in base al sistema operativo in uso:
# Windows:
# Sumatra - Un lettore di pdf e file cbz gratuito e opensource. è estremamente leggero, soprattutto a confronto con google e altri browser. (11 MB di peso)
#// SumatraCBZ
# Linux:
# Okular - Lettore Pdf incluso con l'ambiente desktop KDE-Plasma
# Windows e Linux:
# Calibre - Miglior lettore epub che abbia mai provato, l'ho usato per tanto tempo su windows. Supporta anche Cbz e pdf e tanti altri tipi. Meno preferito rispetto a Sumatra nel caso non lo utiliziate per altro in quanto Calibre, possedendo tante funzionalità, non è così leggero. (300 MB circa)
# Encoding:
# pdf - scarica in formato pdf e lascia all'utente la possibilità di aprirlo come preferisce
# cbz - scarica in formato cbz e lascia all'utente la possibilità di aprirlo come preferisce
#
# Specifica sul formato cbz: Il formato cbz è un sostanzialmente una zip costituita da un file di metadata e un insieme di immagini numerate.
# A differenza del pdf ogni pagina del cbz è gestita come un'immagine.
reader="Okular"
# Don't care if pdf or cbz selected
programCmd="okular"
programArg=""
[Settings.Folders] [General]
basedir = "." # !! Should be an Absolute Path askToQuit = false
extensions = "./extensions"
anime = "./anime"
manga = "./manga"
[Extension]
defaultanime = "animeworld" # USELESS ## Cmd
defaultmanga = "mangaworld" # USELESS [Theme]
base_border = "#fef3c7"
anime_border = "#fef3c7"
manga_border = "#fef3c7"
menu_text = "#ffffff"
menu_selection = "#fbbf24"
res_border = "#fef3c7"
res_text = "#ffffff"
res_selection = "#fbbf24"
cmd_text = "#ffffff"
cmd_keys = "#7dcefe"
status_loading = "#fef3c7"
list_border = "#818cf8"
View File
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
Type = "Manga"
[Info]
Name = "Mangago"
Description = ""
Language = "EN/en"
[Sitemanager]
Hostname = "mangago.me"
Url = "https://www.mangag.me"
+4
View File
@@ -11,3 +11,7 @@ episodes="https://www.mangaworld.mx/manga/1637/chainsaw-man"
[[Mangago]] [[Mangago]]
name="Medalist" name="Medalist"
episodes="https://www.mangago.me/read-manga/medalist/" episodes="https://www.mangago.me/read-manga/medalist/"
[[Mangago]]
name="Atelier of the Witch Hat"
episodes="https://www.mangago.me/read-manga/tongari_boushi_no_atelier/"
View File
+803 -18
View File
@@ -1,6 +1,13 @@
use std::clone;
use boa_engine::builtins::boolean;
use ratatui::{crossterm::event::KeyCode, layout::{Constraint, Layout}, text::Text, widgets::{Block, Borders, List, ListItem, ListState, StatefulWidget, Widget}};
pub mod list { pub mod list {
use std::{collections::HashMap}; use std::{collections::HashMap};
use ratatui::{Frame, crossterm::event::KeyCode, layout::Rect, style::{Style, palette::tailwind::{self}}, widgets::{Block, BorderType, Borders, List, ListState}}; use ratatui::{Frame, crossterm::event::KeyCode, layout::Rect, style::{Style, Stylize}, widgets::{Block, BorderType, Borders, List, ListState}};
use crate::config::getConfig;
pub struct CurrentWidget { pub struct CurrentWidget {
pub render: Box<dyn Fn(&mut Frame, Rect)>, pub render: Box<dyn Fn(&mut Frame, Rect)>,
@@ -26,7 +33,7 @@ pub mod list {
pub action_type: ActionType, pub action_type: ActionType,
pub children_list: Vec<String>, pub children_list: Vec<String>,
pub children_hash: Option<HashMap<String, ListDiveChild>>, pub children_hash: Option<HashMap<String, ListDiveChild>>,
pub widget: SLWidget, pub widget: SLWidget
} }
impl ListDiveChild { impl ListDiveChild {
pub fn new(title: String, index:usize, action_type:ActionType, children: Option<HashMap<String, ListDiveChild>>, widget:SLWidget) -> Self { pub fn new(title: String, index:usize, action_type:ActionType, children: Option<HashMap<String, ListDiveChild>>, widget:SLWidget) -> Self {
@@ -153,14 +160,16 @@ pub mod list {
pub fn render_list(&mut self, frame: &mut Frame, area: Rect) { pub fn render_list(&mut self, frame: &mut Frame, area: Rect) {
let l = List::new(self.get_current_item_list().clone()) let l = List::new(self.get_current_item_list().clone())
.highlight_style(Style::new() .highlight_style(Style::new()
.fg(tailwind::AMBER.c900)) .fg(getConfig().Theme.menu_selection.to_ratatui()) // colore del testo del menu alla selezione
)
.fg(getConfig().Theme.menu_text.to_ratatui()) //colore del testo del menu non selezionato
.highlight_symbol("> ") .highlight_symbol("> ")
.highlight_spacing(ratatui::widgets::HighlightSpacing::Always) .highlight_spacing(ratatui::widgets::HighlightSpacing::Always)
.block( .block(
Block::new() Block::new()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::new().fg(tailwind::AMBER.c100)) .border_style(Style::new().fg(getConfig().Theme.base_border.to_ratatui()))
); );
frame.render_stateful_widget(l,area, &mut self.state); frame.render_stateful_widget(l,area, &mut self.state);
@@ -214,9 +223,9 @@ pub mod list {
pub mod AsyncUpdateAnimeList { pub mod AsyncUpdateAnimeList {
use std::{collections::HashMap, sync::Arc}; 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 ratatui::{buffer::Buffer, layout::Rect, style::{Style}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}};
use crate::{source::AnimeSource, utils::utils::{first_capital, hash_sort}}; use crate::{config::getConfig, source::AnimeSource, utils::utils::{first_capital, hash_sort}};
pub enum AsyncAnimeStateStatus { pub enum AsyncAnimeStateStatus {
Empty, Empty,
@@ -245,6 +254,13 @@ pub mod AsyncUpdateAnimeList {
animesource animesource
} }
} }
pub fn resetFrom(&mut self, newanimesource: AsyncAnimeState) {
self.animesource = newanimesource.animesource;
self.list_state.select_first();
self.page = 1;
self.res = None;
self.status = AsyncAnimeStateStatus::Empty;
}
} }
pub struct AsyncUpdateAnimeList { pub struct AsyncUpdateAnimeList {
trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<AsyncAnimeStateStatus>, trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<AsyncAnimeStateStatus>,
@@ -288,7 +304,7 @@ pub mod AsyncUpdateAnimeList {
Paragraph::new("Starting the request").block(block).render(area, buf); Paragraph::new("Starting the request").block(block).render(area, buf);
} }
AsyncAnimeStateStatus::Loading => { AsyncAnimeStateStatus::Loading => {
Paragraph::new("Loading").block(block.border_style(Style::new().fg(tailwind::AMBER.c200))).render(area, buf); Paragraph::new("Loading").block(block.border_style(Style::new().fg(getConfig().Theme.status_loading.to_ratatui()))).render(area, buf);
} }
AsyncAnimeStateStatus::Error(err) => { AsyncAnimeStateStatus::Error(err) => {
@@ -313,11 +329,11 @@ pub mod AsyncUpdateAnimeList {
} }
pub mod Library { pub mod Library {
use std::{collections::HashMap, hash::Hash, sync::Arc}; use std::{cmp::Ordering, 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 ratatui::{buffer::Buffer, layout::{Constraint, Layout, Rect}, style::{Style}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}};
use crate::{library::{self, LibraryItem}, source::{EpisodeListItem, MangaSource}, utils::utils::hash_sort}; use crate::{library::{self, LibraryItem}, log, source::{EpisodeListItem, MangaSource}, utils::utils::hash_sort};
pub struct Library { pub struct Library {
pub trasmitter_to_app:tokio::sync::mpsc::UnboundedSender<HashMap<String, EpisodeListItem>>, pub trasmitter_to_app:tokio::sync::mpsc::UnboundedSender<HashMap<String, EpisodeListItem>>,
pub library_type: LibraryType pub library_type: LibraryType
@@ -331,11 +347,28 @@ pub mod Library {
} }
pub fn sorted_keys_from_hash(hs: &HashMap<String, EpisodeListItem>) -> Vec<&String> { pub fn sorted_keys_from_hash(hs: &HashMap<String, EpisodeListItem>) -> Vec<&String> {
let mut items: Vec<&String> = hs.keys().collect::<Vec<&String>>(); let mut items: Vec<&String> = hs.keys().collect::<Vec<&String>>();
items.sort_by(|a: &&String,b: &&String| { // log!("{:#?}",items);
if let Ok(ai) = a.parse::<isize>() && let Ok(bi) = b.parse::<isize>() { items.sort_by(|ad: &&String,bd: &&String| {
return bi.cmp(&ai); let mut a_s = ad.split(".");
let a = a_s.nth(0).unwrap_or(ad);
let zero_string = "0".to_string();
let adec = a_s.nth(1).unwrap_or(&zero_string);
let mut b_s = bd.split(".");
let b = b_s.nth(0).unwrap_or(bd);
let bdec = a_s.nth(1).unwrap_or(&zero_string);
match (a.parse::<usize>(), b.parse::<usize>()) {
(Ok(uai), Ok(ubi)) => match ubi.cmp(&uai) {
Ordering::Equal => match (adec.parse::<usize>(), bdec.parse::<usize>()) {
(Ok(adecp), Ok(bdecp)) => adecp.cmp(&bdecp),
_ => Ordering::Less
},
a => a
},
(Ok(uai), Err(_))=>Ordering::Greater,
(Err(_), Ok(ubi))=>Ordering::Less,
(Err(_), Err(_))=>b.to_lowercase().cmp(&a.to_lowercase()),
} }
return b.cmp(a);
}); // ? REVERSE SORT - MAX TO MIN -> NEWEST FIRST }); // ? REVERSE SORT - MAX TO MIN -> NEWEST FIRST
items items
} }
@@ -448,9 +481,9 @@ pub mod Library {
pub mod AsyncUpdateMangaList { pub mod AsyncUpdateMangaList {
use std::{collections::HashMap, sync::Arc}; 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 ratatui::{buffer::Buffer, layout::Rect, style::{Style}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}};
use crate::{source::{MangaSource}, utils::utils::{first_capital, hash_sort}}; use crate::{config::getConfig, log, source::MangaSource, utils::utils::{first_capital, hash_sort}};
pub enum AsyncMangaStateStatus { pub enum AsyncMangaStateStatus {
Empty, Empty,
@@ -479,6 +512,13 @@ pub mod AsyncUpdateMangaList {
mangasource mangasource
} }
} }
pub fn resetFrom(&mut self, newmangasource: AsyncMangaState) {
self.mangasource = newmangasource.mangasource;
self.list_state.select_first();
self.page = 1;
self.res = None;
self.status = AsyncMangaStateStatus::Empty;
}
} }
pub struct AsyncUpdateMangaList { pub struct AsyncUpdateMangaList {
trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<AsyncMangaStateStatus>, trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<AsyncMangaStateStatus>,
@@ -490,7 +530,7 @@ pub mod AsyncUpdateMangaList {
} }
} }
pub fn formatEpisodeIndexTitle(index: usize, title:&str) -> String { pub fn formatEpisodeIndexTitle(index: usize, title:&str) -> String {
return format!("Ep. {:>4} di {}",index,title); return format!("Capitolo {:>4} di {}",index,title);
} }
} }
impl StatefulWidget for AsyncUpdateMangaList { impl StatefulWidget for AsyncUpdateMangaList {
@@ -503,6 +543,7 @@ pub mod AsyncUpdateMangaList {
state.status = AsyncMangaStateStatus::Loading; state.status = AsyncMangaStateStatus::Loading;
let tx = self.trasmitter_to_app.clone(); let tx = self.trasmitter_to_app.clone();
let source = state.mangasource.clone(); let source = state.mangasource.clone();
log!("{}", source.get_url());
let h = source.get_hostname(); let h = source.get_hostname();
let p = state.page.clone(); let p = state.page.clone();
tokio::spawn(async move { tokio::spawn(async move {
@@ -522,7 +563,7 @@ pub mod AsyncUpdateMangaList {
Paragraph::new("Starting the request").block(block).render(area, buf); Paragraph::new("Starting the request").block(block).render(area, buf);
} }
AsyncMangaStateStatus::Loading => { AsyncMangaStateStatus::Loading => {
Paragraph::new("Loading").block(block.border_style(Style::new().fg(tailwind::AMBER.c200))).render(area, buf); Paragraph::new("Loading").block(block.border_style(Style::new().fg(getConfig().Theme.status_loading.to_ratatui()))).render(area, buf);
} }
AsyncMangaStateStatus::Error(err) => { AsyncMangaStateStatus::Error(err) => {
@@ -545,3 +586,747 @@ pub mod AsyncUpdateMangaList {
} }
} }
} }
pub mod ListOption {
use std::{cell::RefCell, collections::HashMap, ops::Index, rc::Rc};
use boa_engine::ast::expression::Optional;
use ratatui::{buffer::Buffer, crossterm::event::KeyCode, layout::{Constraint, Layout, Rect, Rows}, style::{Style}, symbols::line, text::{Line, Span, Text}, widgets::{Block, BorderType, List, StatefulWidget, Table, Widget}};
use crate::{components::TextInputState, config::getConfig, log, utils::utils::ColorPickerOption};
pub struct ListOption {
pub sections: Vec<Section>
}
#[derive(Clone)]
pub struct Section {
pub label: String,
pub style: Style,
pub items: Vec<Rc<RefCell<Item>>>
}
impl Section {
pub fn new(label: String, style: Style) -> Self {
Section { label, style, items: vec![] }
}
pub fn add_item(mut self, item: Item) -> Self {
self.items.push(Rc::new(RefCell::new(item)));
self
}
pub fn add_items(self, items: Vec<Item>) -> Self {
items.into_iter().fold(self, |acc, x| acc.add_item(x))
}
}
pub struct Item {
pub label: String,
pub option: ItemOption
}
impl Item {
pub fn build(&self, prefix: &str, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, editingmode: bool) {
self.option.build(prefix, &self.label, editingmode, area, buf)
}
}
#[derive(Clone)]
pub struct ListOptionState {
pub selected_section: usize,
pub selected_item: usize,
pub itm_len: usize,
pub sec_len:usize,
pub edit_option: bool,
pub selected_item_ref: Option<Rc<RefCell<Item>>>
}
impl ListOptionState {
pub fn default() -> Self {
ListOptionState { selected_section: 0, selected_item: 0, itm_len: 0, sec_len: 0, selected_item_ref: None, edit_option: false }
}
pub fn selectnext(&mut self) {
if self.selected_item+1<self.itm_len {
self.selected_item = self.selected_item+1;
}
}
pub fn selectprev(&mut self) {
if self.selected_item>=1 {
self.selected_item = self.selected_item-1;
}
}
pub fn select_sec_next(&mut self) {
if self.selected_section+1<self.sec_len {
self.selected_section = self.selected_section+1;
self.selected_item=0;
}
}
pub fn select_sec_prev(&mut self) {
if self.selected_section>=1 {
self.selected_section = self.selected_section-1;
self.selected_item=0;
}
}
pub fn handle_input(&mut self, kc: KeyCode) -> bool {
if let Some(sel) = &self.selected_item_ref {
// match &item.borrow_mut().option {
// ItemOption::TextInputOption { get, set, state } => todo!(),
// ItemOption::SwitchOption { get, set, state } => todo!(),
// ItemOption::SelectOption { get, get_options, set, state } => todo!(),
// _ => todo!(),
// }
return match kc {
k @ (KeyCode::Enter) => {
if (self.edit_option){
let mut item = sel.borrow_mut();
match &mut item.option {
ItemOption::TextInputOption { get, set, state } => {
set(state.value.clone());
},
ItemOption::SwitchOption { get, set, state } => {},
ItemOption::SelectOption { get, get_options, set, state } => {
set(state.selected_value.clone());
},
_ => {},
}
self.edit_option=false;
} else {
self.edit_option=true;
}
true
},
k @ (KeyCode::Backspace | KeyCode::Char(_) | KeyCode::Delete | KeyCode::Left | KeyCode::Right) => {
let mut item = sel.borrow_mut();
match &mut item.option {
ItemOption::TextInputOption { get, set, state } => {
state.dispatchEvent(k);
},
ItemOption::SwitchOption { get, set, state } => {},
ItemOption::SelectOption { get, get_options, set, state } => {},
_ => {},
}
return true;
},
KeyCode::Down => {
let mut item = sel.borrow_mut();
match &mut item.option {
ItemOption::TextInputOption { get, set, state } => {},
ItemOption::SwitchOption { get, set, state } => {},
ItemOption::SelectOption { get, get_options, set, state } => {
if state.selected_index < state.values.len() - 1 {
state.selected_index = state.selected_index + 1;
if let Some(x) = state.values.get(state.selected_index) {
state.selected_value = x.to_string();
}
}
},
_ => {},
};
true
},
KeyCode::Up => {
let mut item = sel.borrow_mut();
match &mut item.option {
ItemOption::TextInputOption { get, set, state } => {},
ItemOption::SwitchOption { get, set, state } => {},
ItemOption::SelectOption { get, get_options, set, state } => {
if state.selected_index > 0 {
state.selected_index = state.selected_index.saturating_sub(1);
if let Some(x) = state.values.get(state.selected_index) {
state.selected_value = x.to_string();
}
}
},
_ => {},
};
true
},
_ => {false}
}
}
false
}
pub fn handle_control_inputs(&mut self, kc: KeyCode) -> bool {
if let Some(item) = &self.selected_item_ref {
// match &item.borrow_mut().option {
// ItemOption::TextInputOption { get, set, state } => todo!(),
// ItemOption::SwitchOption { get, set, state } => todo!(),
// ItemOption::SelectOption { get, get_options, set, state } => todo!(),
// _ => todo!(),
// }
return match kc {
KeyCode::Backspace => {
let mut item = item.borrow_mut();
match &mut item.option {
ItemOption::TextInputOption { get, set, state } => {
// state.value.insert(state.cursorpointer, c);
if state.cursorpointer > 0 {
let before = state.value[0..state.cursorpointer].to_owned();
let nbef = if let Some(spl) = before.rsplit_once(" ") {
format!("{} ", spl.0)
} else {
"".to_owned()
};
let after = state.value[state.cursorpointer..].to_owned();
state.value = format!("{}{}", nbef, after);
state.cursorpointer = nbef.len();
}
},
ItemOption::SwitchOption { get, set, state } => {},
ItemOption::SelectOption { get, get_options, set, state } => {},
_ => {},
}
return true;
}
_ => {false}
}
}
false
}
}
impl ListOption {
pub fn new() -> ListOption {
ListOption { sections: vec![]}
}
pub fn add_section(mut self, section: Section) -> Self {
self.sections.push(section);
self
}
pub fn add_sections(self, sections: Vec<Section>) -> Self {
sections.into_iter().fold(self, |acc, x| acc.add_section(x))
}
}
impl StatefulWidget for ListOption {
type State = ListOptionState;
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
if self.sections.len() <= 0 {return;}
state.sec_len=self.sections.len();
if let Some(sec) = match self.sections.get(state.selected_section) {
Some(s) => Some(s),
_ => self.sections.first()
} {
state.itm_len=sec.items.len();
let l = Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::new().fg(getConfig().Theme.list_border.to_ratatui()))
.title(sec.label.clone());//Line::styled(sec.label.clone(), sec.style);
let content_area = l.inner(area);
l.render(area, buf);
let h = (content_area.height) as usize;
let (nitems, start_index) = if sec.items.len() > h {
let half_h = (h / 2) as usize;
let si = state.selected_item.saturating_sub(half_h);
(h as usize, si.min(sec.items.len() - h ))
}else {
(sec.items.len(), 0)
};
let t = sec.items.iter().enumerate().skip(start_index).take(nitems);
for ((index,item), row_area) in t.zip(content_area.rows()) {
if index == state.selected_item {
state.selected_item_ref=Some(Rc::clone(item));
item.borrow().build(">> ", row_area, buf,state.edit_option);
} else {
item.borrow().build(" ", row_area, buf,false);
};
}
}
}
}
type GetOption<S> = Box<dyn Fn() -> S>;
type SetOption<S> = Box<dyn Fn(S) -> bool>;
pub enum ItemOption {
TextInput {
state: TextInputState
},
TextInputOption {
get: GetOption<String>,
set: SetOption<String>,
state: TextInputState
},
SwitchOption {
get: GetOption<bool>,
set: SetOption<bool>,
state: SwitchOptionState
},
SelectOption {
get: GetOption<String>,
get_options: GetOption<Vec<String>>,
set: SetOption<String>,
state: SelectOptionState
},
ColorPicker {
get: GetOption<ColorPickerOption>, // TODO: insere in <> a color stuct and manage the possibility to change only a number at time. The struct of the field is: label type_which_is_a_select r,g,b_or_other_color_opt
set: SetOption<ColorPickerOption>,
state: ColorPickerOptionState
},
DebugOption {}
}
impl ItemOption {
pub fn new_text_input(get: GetOption<String>, set: SetOption<String>) -> ItemOption {
let value = get();
ItemOption::TextInputOption {
get, set,
state: TextInputState::new(value)
}
}
pub fn new_text_input_with_state(state: TextInputState) -> ItemOption {
ItemOption::TextInput {
state
}
}
pub fn new_select_option(get: GetOption<String>, get_options: GetOption<Vec<String>>, set: SetOption<String>) -> ItemOption {
let value = get();
let values = get_options();
ItemOption::SelectOption {
get, set,
get_options,
state: SelectOptionState::new(values, value)
}
}
pub fn new_color_picker(get: GetOption<ColorPickerOption>, set: SetOption<ColorPickerOption>) -> ItemOption {
let value = get();
ItemOption::ColorPicker { get, set, state: ColorPickerOptionState::new(value) }
}
pub fn build(&self, prefix:&str,label:&str, editingmode:bool, area:Rect, buf: &mut Buffer) {
let lay = Layout::default().constraints(vec![
Constraint::Percentage(50),
Constraint::Percentage(50)
]).direction(ratatui::layout::Direction::Horizontal).split(area);
match &self {
ItemOption::SelectOption { get,get_options, set, state } => {
let opt = get();
let vals = get_options();
Text::raw(format!("{}{}",prefix, label)).render(lay[0], buf);
if editingmode {
Text::raw(format!("> {} {}/{}", state.selected_value, state.selected_index+1, vals.len())).render(lay[1], buf);
} else {
Text::raw(format!("> {}", opt)).render(lay[1], buf);
}
}
ItemOption::TextInputOption { get, set, state } => {
let opt = &state.value;
Text::raw(format!("{}{}",prefix, label)).render(lay[0], buf);
if editingmode {
if state.ins {
} else {
let mut v = opt.clone();
v.insert(state.cursorpointer, '|');
Text::raw(format!("<{}>", v)).render(lay[1], buf);
}
} else {
Text::raw(format!("<{}>", opt)).render(lay[1], buf);
}
},
ItemOption::SwitchOption { get, set, state } => {
},
_ => {
Text::raw(format!("{}{}",prefix, label)).render(lay[0], buf);
// let opt = get();
if editingmode {
} else {
Text::raw(format!("[{}]", "Debug")).render(lay[1], buf);
}
},
ItemOption::ColorPicker { get, set, state } => {
let opt = get();
if editingmode {
let r = format!("{:0<3}", opt.redString());
let g = format!("{:0<3}", opt.greenString());
let b = format!("{:0<3}", opt.blueString());
match opt._type {
crate::utils::utils::ColorPickerOptionType::RGB => {
Line::from(vec![
Span::styled("rgb", if state.selected_index==0 { Style::default().underlined() } else { Style::default() }),
Span::styled("rgb(", Style::default()),
Span::styled(r.chars().nth(0).unwrap_or_default().to_string(), if state.selected_index==1 { Style::default().underlined() } else { Style::default() }),
Span::styled(r.chars().nth(1).unwrap_or_default().to_string(), if state.selected_index==2 { Style::default().underlined() } else { Style::default() }),
Span::styled(r.chars().nth(2).unwrap_or_default().to_string(), if state.selected_index==3 { Style::default().underlined() } else { Style::default() }),
Span::styled(",", Style::default()),
Span::styled(g.chars().nth(0).unwrap_or_default().to_string(), if state.selected_index==4 { Style::default().underlined() } else { Style::default() }),
Span::styled(g.chars().nth(1).unwrap_or_default().to_string(), if state.selected_index==5 { Style::default().underlined() } else { Style::default() }),
Span::styled(g.chars().nth(2).unwrap_or_default().to_string(), if state.selected_index==6 { Style::default().underlined() } else { Style::default() }),
// Span::styled(, if state.selected_index>3 && state.selected_index<7 { Style::default().underlined() } else { Style::default() }),
Span::styled(",", Style::default()),
Span::styled(b.chars().nth(0).unwrap_or_default().to_string(), if state.selected_index==7 { Style::default().underlined() } else { Style::default() }),
Span::styled(b.chars().nth(1).unwrap_or_default().to_string(), if state.selected_index==8 { Style::default().underlined() } else { Style::default() }),
Span::styled(b.chars().nth(2).unwrap_or_default().to_string(), if state.selected_index==9 { Style::default().underlined() } else { Style::default() }),
Span::styled(")", Style::default()),
]).render(lay[1],buf)
},
crate::utils::utils::ColorPickerOptionType::HEX => {
Line::from(vec![
Span::styled("hex", if state.selected_index==0 { Style::default().underlined() } else { Style::default() }),
Span::styled("#", Style::default()),
Span::styled(r.chars().nth(0).unwrap_or_default().to_string(), if state.selected_index==1 { Style::default().underlined() } else { Style::default() }),
Span::styled(r.chars().nth(1).unwrap_or_default().to_string(), if state.selected_index==2 { Style::default().underlined() } else { Style::default() }),
Span::styled(",", Style::default()),
Span::styled(g.chars().nth(0).unwrap_or_default().to_string(), if state.selected_index==3 { Style::default().underlined() } else { Style::default() }),
Span::styled(g.chars().nth(1).unwrap_or_default().to_string(), if state.selected_index==4 { Style::default().underlined() } else { Style::default() }),
Span::styled(",", Style::default()),
Span::styled(b.chars().nth(0).unwrap_or_default().to_string(), if state.selected_index==5 { Style::default().underlined() } else { Style::default() }),
Span::styled(b.chars().nth(1).unwrap_or_default().to_string(), if state.selected_index==6 { Style::default().underlined() } else { Style::default() }),
]).render(lay[1],buf)
},
}
} else {
match opt._type {
crate::utils::utils::ColorPickerOptionType::RGB => {
Text::raw(format!("rgb({},{},{})", opt.redString(), opt.greenString(), opt.greenString())).render(lay[1], buf);
},
crate::utils::utils::ColorPickerOptionType::HEX => {
Text::raw(format!("#{}{}{}", opt.redString(), opt.greenString(), opt.greenString())).render(lay[1], buf);
},
}
}
},
}
}
pub fn buildOnlyItem(&self, editingmode:bool, area:Rect, buf: &mut Buffer, prefix:Option<&str>, suffix:Option<&str>) {
match &self {
ItemOption::SelectOption { get,get_options, set, state } => {
let opt = get();
let vals = get_options();
if editingmode {
Text::raw(format!("> {} {}/{}", state.selected_value, state.selected_index+1, vals.len())).render(area, buf);
} else {
Text::raw(format!("> {}", opt)).render(area, buf);
}
}
ItemOption::TextInputOption { get, set, state } => {
let opt = &state.value;
// Text::raw(format!("{}{}",prefix, label)).render(lay[0], buf); //TODO ??
if editingmode {
if state.ins {
} else {
let mut v = opt.clone();
v.insert(state.cursorpointer, '|');
Text::raw(format!("{}{}{}", prefix.unwrap_or(""), v, suffix.unwrap_or(""))).render(area, buf);
}
} else {
Text::raw(format!("{}{}{}", prefix.unwrap_or(""), opt, suffix.unwrap_or(""))).render(area, buf);
}
},
ItemOption::TextInput { state } => {
let opt = &state.value;
// Text::raw(format!("{}{}",prefix, label)).render(lay[0], buf); //TODO ??
if editingmode {
if state.ins {
} else {
let mut v = opt.clone();
v.insert(state.cursorpointer, '|');
Text::raw(format!("{}{}{}", prefix.unwrap_or(""), v, suffix.unwrap_or(""))).render(area, buf);
}
} else {
Text::raw(format!("{}{}{}", prefix.unwrap_or(""), opt, suffix.unwrap_or(""))).render(area, buf);
}
},
ItemOption::SwitchOption { get, set, state } => {
},
_ => {
// Text::raw(format!("{}{}",prefix, label)).render(lay[0], buf);
// let opt = get();
if editingmode {
} else {
Text::raw(format!("[{}]", "Debug")).render(area, buf);
}
},
ItemOption::ColorPicker { get, set, state } => {
},
}
}
}
pub struct SwitchOptionState {
value: bool
}
pub struct SelectOptionState {
values: Vec<String>,
selected_value: String,
selected_index: usize
}
impl SelectOptionState {
fn new(values: Vec<String>, value: String) -> SelectOptionState {
SelectOptionState {
selected_index: values.iter().position(|x| *x==value).unwrap_or(0),
selected_value: value,
values
}
}
}
pub struct ColorPickerOptionState {
value: ColorPickerOption,
selected_index: u8,
selected_index_sub: u8
}
impl ColorPickerOptionState {
fn new(value: ColorPickerOption) -> ColorPickerOptionState {
ColorPickerOptionState { value, selected_index: 0, selected_index_sub: 0 }
}
}
}
pub struct AskToQuit {
}
pub struct ListFuncOption {
pub name: String,
pub func: Box<dyn Fn()>,
pub style: Option<ratatui::style::Style>
}
pub struct AskToQuitState {
pub sel_index: usize,
pub items: Vec<ListFuncOption>
}
impl AskToQuitState {
pub fn get_selected_item(&self) -> Option<&ListFuncOption> {
return self.items.get(self.sel_index);
}
pub fn select_next(&mut self) {
if self.sel_index < self.items.len()-1 {
self.sel_index = self.sel_index+1;
}
}
pub fn select_previous(&mut self) {
if self.sel_index > 0 {
self.sel_index = self.sel_index.saturating_sub(1);
}
}
}
impl StatefulWidget for AskToQuit {
type State = AskToQuitState;
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
// let block = Block::default().title("Quit?").borders(Borders::ALL);
let sel = state.get_selected_item();
let sstyle = state.get_selected_item().and_then(|s| s.style);
let n = state.items.len();
let nn = n as u32;
let carea = area.centered(Constraint::Percentage(100), Constraint::Length(1));
// let mut li = List::new(list_items).block(block).highlight_style(sstyle.unwrap_or(ratatui::style::Style::new().bg(tailwind::WHITE).fg(tailwind::BLACK)));
let areas = Layout::default().direction(ratatui::layout::Direction::Horizontal).constraints(vec![Constraint::Ratio(1, nn); n]).split(carea);
for i in 0..n {
if let Some(item) = state.items.get(i) {
let sty = if state.sel_index==i {
item.style.unwrap_or(ratatui::style::Style::new().bg(ratatui::style::palette::tailwind::YELLOW.c200).black())
} else {
ratatui::style::Style::default()
};
Text::styled(item.name.clone(), sty).centered().render(areas[i], buf);
}
}
}
}
#[derive(Clone)]
pub struct TextInputState {
pub editingmode: bool,
pub value: String,
pub ins: bool,
pub cursorpointer: usize, // if ins this is put on the letter of index, otherwise it is put before
}
impl TextInputState {
pub fn new(value: String) -> Self {
TextInputState {
editingmode: false,
value: value.clone(),
ins: false,
cursorpointer: value.len()
}
}
pub fn dispatchEvent(&mut self, k: KeyCode) -> bool {
match k {
KeyCode::Char(c) => {
self.editingmode = true;
self.value.insert(self.cursorpointer, c);
self.cursorpointer = self.value.len().min(self.cursorpointer+1);
true
},
KeyCode::Backspace => {
if self.editingmode {
if self.cursorpointer > 0 {
self.value.remove(self.cursorpointer-1);
self.cursorpointer = self.cursorpointer.saturating_sub(1);
}
return true;
}
false
},
KeyCode::Delete => {
self.editingmode = true;
if self.cursorpointer < self.value.len() {
self.value.remove(self.cursorpointer);
}
true
}
KeyCode::Left => {
self.cursorpointer = self.cursorpointer.saturating_sub(1);
true
},
KeyCode::Right => {
self.cursorpointer = self.value.len().min(self.cursorpointer+1);
true
}
_ => {false}
}
}
}
pub mod Search {
use std::{collections::HashMap, hash::Hash, sync::Arc};
use ratatui::{buffer::Buffer, layout::{Constraint, Layout, Rect}, style::{Style}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}};
use crate::{components::{ListOption::Item, TextInputState}, library::{self, LibraryItem}, settings::ApplySettingsChange, source::{EpisodeListItem, MangaSource}, utils::utils::hash_sort};
pub struct Search {
pub trasmitter_to_app:tokio::sync::mpsc::UnboundedSender<SearchStateStatus>,
// pub trasmitter_to_app_input:tokio::sync::mpsc::UnboundedSender<String>,
pub library_type: SearchType
}
impl Search {
pub fn new(library_type: SearchType,trasmitter_to_app: tokio::sync::mpsc::UnboundedSender<SearchStateStatus>) -> Self {
Self {
library_type,
trasmitter_to_app
}
}
pub fn sorted_keys_from_hash(hs: &HashMap<String, EpisodeListItem>) -> Vec<&String> {
let mut items: Vec<&String> = hs.keys().collect::<Vec<&String>>();
items.sort_by(|a: &&String,b: &&String| {
if let (Ok(ai),Ok(bi)) = (a.parse::<isize>(), b.parse::<isize>()) {
return bi.cmp(&ai);
}
return b.to_lowercase().cmp(&a.to_lowercase());
}); // ? REVERSE SORT - MAX TO MIN -> NEWEST FIRST
items
}
}
pub enum SearchStateStatus {
Empty,
SearchFor(String),
SearchResult(usize),
}
pub struct SearchState {
pub list_state_source: ListState,
pub list_state_eps: ListState,
pub lib: HashMap<String, Vec<LibraryItem>>,
pub status: SearchStateStatus,
pub entry_selected: bool,
pub episodes_loaded: Option<HashMap<String, EpisodeListItem>>,
pub current_source: Option<String>,
pub text_input_state: TextInputState
}
impl SearchState {
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: SearchStateStatus::Empty,
episodes_loaded: None,
current_source: None,
text_input_state: TextInputState::new("".to_string())
}
}
}
#[derive(Clone,Copy)]
pub enum SearchType {
Anime, Manga
}
// TODO
impl StatefulWidget for Search {
type State = SearchState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let block = Block::default().title("Search").borders(Borders::ALL);
let area_v = Layout::default().direction(ratatui::layout::Direction::Vertical).constraints([
Constraint::Length(3),
Constraint::Percentage(100),
]).split(area);
let tx = self.trasmitter_to_app.clone();
let text_input = crate::components::ListOption::ItemOption::new_text_input_with_state(state.text_input_state.clone());
let lay = Layout::default().direction(ratatui::layout::Direction::Horizontal).constraints([
Constraint::Percentage(100),
Constraint::Min(30)
]).split(area_v[1]);
text_input.buildOnlyItem(
state.text_input_state.editingmode,
area_v[0].centered(Constraint::Percentage(100), Constraint::Ratio(1, 3)),
buf,
Some(">"),
None
);
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));
match &mut state.status {
SearchStateStatus::Empty => {
Widget::render(block_sc, lay[0], buf);
}
SearchStateStatus::SearchFor(v) =>{
Widget::render(block_sc, lay[0], buf);
},
SearchStateStatus::SearchResult(index) => {
let mut keys: Vec<&String> = state.lib.keys().collect();
keys.sort();
let i = index.clone();
let l = keys.len();
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).highlight_symbol(">> ");
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 items = Self::sorted_keys_from_hash(eps_hash);
let mut eps = Vec::with_capacity(items.len());
for it in items {
let s = eps_hash.get(it).and_then(|f| Some(f.title.clone())).unwrap_or(it.clone());
eps.push(ListItem::new(s));
}
let list_ep = List::new(eps).block(block_eps).highlight_symbol(">> ");;
StatefulWidget::render(list_ep, lay[1], buf, &mut state.list_state_eps);
} else {
Widget::render(block_eps, lay[1], buf);
}
}
}
}
}
}
-83
View File
@@ -1,83 +0,0 @@
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
}
+232
View File
@@ -0,0 +1,232 @@
use std::{default, fs, sync::{LazyLock, RwLock}};
use ratatui::style::palette::tailwind;
use serde::{Deserialize, Deserializer};
use std::sync::OnceLock;
use crate::{log, utils::utils::ColorPickerOption};
#[derive(Deserialize, Clone)]
pub struct Config {
pub Anime: AnimeSettings,
pub Manga: MangaSettings,
pub Theme: ThemeSettings,
pub General: GeneralSettings
}
#[derive(Deserialize, Clone)]
pub struct AnimeSettings {
pub source: String,
}
#[derive(Deserialize, Clone)]
pub struct GeneralSettings {
pub askToQuit: bool,
}
#[derive(Deserialize, Clone)]
pub struct MangaSettings {
#[serde(deserialize_with = "manga_reader_deserialize")]
pub reader: MangaReader,
pub source: String,
pub programCmd: String,
pub programArg: String
}
#[derive(Deserialize, Clone)]
pub enum MangaReader {
Sumatra, PDF, CBZ, Okular, Calibre
}
impl MangaReader {
pub fn to_str(&self) -> &str {
match self {
MangaReader::CBZ => "CBZ",
MangaReader::Calibre => "Calibre",
MangaReader::Okular => "Okular",
MangaReader::PDF => "PDF",
MangaReader::Sumatra => "Sumatra"
}
}
}
fn manga_reader_deserialize<'de,D>(deserializer: D) -> Result<MangaReader, D::Error> where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?.to_lowercase();
match s.as_str() {
"sumatra" => Ok(MangaReader::Sumatra),
"pdf" => Ok(MangaReader::PDF),
"cbz" => Ok(MangaReader::CBZ),
"calibre" => Ok(MangaReader::Calibre),
"okular" => Ok(MangaReader::Okular),
_ => {
log!("Invalid manga reader, using default option: PDF");
return Ok(MangaReader::PDF);
}
}
}
// #[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: LazyLock<RwLock<Config>> = LazyLock::new(|| {
RwLock::new(load_config())
});
pub fn getConfig() -> Config {
let lock = CONFIG.read().unwrap();
lock.clone()
}
pub fn load_config() -> Config {
let content = fs::read_to_string("config.toml").expect("Can't read config file");
let parsed:Config = toml::from_str(&content).unwrap();
parsed
}
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
}
pub fn setMangaSource(v:String) {
let mut conf = CONFIG.write().unwrap();
conf.Manga.source = v
}
pub fn setAnimeSource(v:String) {
let mut conf = CONFIG.write().unwrap();
conf.Anime.source = v
}
#[derive(Deserialize, Clone)]
pub struct Color {
red: u8,
green: u8,
blue: u8,
opacity: u8
}
impl Color {
pub fn to_u32(&self) -> u32 {
((self.opacity as u32) << 24) |
((self.red as u32) << 16) |
((self.green as u32) << 8) |
(self.blue as u32)
}
pub fn to_ratatui(&self) -> ratatui::style::Color {
// log!("{}", self.to_u32());
let x = ratatui::style::Color::from_u32(self.to_u32());
x
}
}
pub fn color_deserialize<'de,D>(deserializer: D) -> Result<Color, D::Error> where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?.to_lowercase();
log!("{:?}",s.get(1..3));
let hexred = match s.get(1..3) {
Some(res)=>match u8::from_str_radix(res, 16){
Ok(r) => r,
Err(_) => {
log!("Invalid hex color");
0
},
},
None => {
log!("Invalid hex color");
0
}
};
let hexgre = match s.get(3..5) {
Some(res)=>match u8::from_str_radix(res, 16){
Ok(r) => r,
Err(_) => {
log!("Invalid hex color");
0
},
},
None => {
log!("Invalid hex color");
0
}
};
let hexblu = match s.get(5..7) {
Some(res)=>match u8::from_str_radix(res, 16){
Ok(r) => r,
Err(_) => {
log!("Invalid hex color");
0
},
},
None => {
log!("Invalid hex color");
0
}
};
log!("{} {} {}", hexred, hexgre, hexblu);
return Ok(Color {
red: hexred,
green: hexgre,
blue: hexblu,
opacity: 255
})
}
#[derive(Deserialize, Clone)]
pub struct ThemeSettings {
#[serde(deserialize_with = "color_deserialize")]
pub base_border: Color,
#[serde(deserialize_with = "color_deserialize")]
pub anime_border: Color,
#[serde(deserialize_with = "color_deserialize")]
pub manga_border: Color,
#[serde(deserialize_with = "color_deserialize")]
pub menu_text: Color,
#[serde(deserialize_with = "color_deserialize")]
pub menu_selection: Color,
#[serde(deserialize_with = "color_deserialize")]
pub res_border: Color,
#[serde(deserialize_with = "color_deserialize")]
pub res_text: Color,
#[serde(deserialize_with = "color_deserialize")]
pub res_selection: Color,
#[serde(deserialize_with = "color_deserialize")]
pub cmd_text: Color,
#[serde(deserialize_with = "color_deserialize")]
pub status_loading: Color,
#[serde(deserialize_with = "color_deserialize")]
pub cmd_keys: Color,
#[serde(deserialize_with = "color_deserialize")]
pub list_border: Color,
}
+6 -3
View File
@@ -3,6 +3,8 @@ use std::fs;
use std::result::Result::Ok; use std::result::Result::Ok;
use serde::Deserialize; use serde::Deserialize;
use crate::config::getConfig;
use crate::source::mangago::Mangago;
use crate::source::{AnimeSource, MangaSource, SourceType}; use crate::source::{AnimeSource, MangaSource, SourceType};
use crate::source::animeworld::Animeworld; use crate::source::animeworld::Animeworld;
use crate::source::mangaworld::Mangaworld; use crate::source::mangaworld::Mangaworld;
@@ -49,19 +51,20 @@ impl Extension {
pub fn toMangaSource(self) -> Box<dyn MangaSource+ Send + Sync> { pub fn toMangaSource(self) -> Box<dyn MangaSource+ Send + Sync> {
match self.Info.Name.trim().to_lowercase().as_str() { match self.Info.Name.trim().to_lowercase().as_str() {
// "animeworld" => Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url), // "animeworld" => Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url),
"mangago" => Box::new(Mangago::new(self.Sitemanager.Hostname, self.Sitemanager.Url)),
_ => Box::new(Mangaworld::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> { pub fn defaultAnimeSource() -> Result<Box<dyn AnimeSource>, String> {
let def = "animeworld"; let def = &getConfig().Anime.source;
if let Ok(ext) = Extension::buildfromName(SourceType::Anime, def) { if let Ok(ext) = Extension::buildfromName(SourceType::Anime, def) {
return Ok(ext.toAnimeSource()); return Ok(ext.toAnimeSource());
} }
return Err(format!("No default file for {}",def)); return Err(format!("No default file for {}",def));
} }
pub fn defaultMangaeSource() -> Result<Box<dyn MangaSource>, String> { pub fn defaultMangaeSource() -> Result<Box<dyn MangaSource>, String> {
let def = "mangaworld"; let def = &getConfig().Manga.source;
if let Ok(ext) = Extension::buildfromName(SourceType::Anime, def) { if let Ok(ext) = Extension::buildfromName(SourceType::Manga, def) {
return Ok(ext.toMangaSource()); return Ok(ext.toMangaSource());
} }
return Err(format!("No default file for {}",def)); return Err(format!("No default file for {}",def));
+721 -113
View File
File diff suppressed because it is too large Load Diff
+123
View File
@@ -0,0 +1,123 @@
use ratatui::{crossterm::event::KeyEvent, style::Style, widgets::{List, StatefulWidget, Widget}};
use crate::{App, AppState, components::ListOption::{Item, ListOption, ListOptionState, Section}, config::{getConfig, load_config, setAnimeSource, setMangaSource}, source::getSources, utils::utils::ColorPickerOption};
// pub static testSection: crate::components::ListOption::Section = ;
#[derive(Clone)]
pub struct SettingsState {
pub listoption_sections: Vec<Section>,
listoptionstate: ListOptionState
// tx: tokio::sync::mpsc::UnboundedSender<ApplySettingsChange>
}
pub struct Settings {
}
impl Settings {
pub fn new() -> Settings {
Settings {
}
}
}
impl SettingsState {
pub fn new(tx: tokio::sync::mpsc::UnboundedSender<ApplySettingsChange>) -> SettingsState {
let mut v: Vec<Section> = vec![];
v.push(Self::buildSettings(tx.clone()));
v.push(Self::buildColorCustomization(tx.clone()));
SettingsState {
listoptionstate: ListOptionState::default(),
listoption_sections: v
}
}
pub fn buildSettings(tx: tokio::sync::mpsc::UnboundedSender<ApplySettingsChange>) -> Section {
let tx_a = tx.clone();
let tx_m = tx.clone();
let s = Section::new("Generali".to_owned(), Style::default())
.add_item(Item {
label: "Sorgente Anime predefinita".to_owned(),
option: crate::components::ListOption::ItemOption::new_select_option(Box::new(|| {
getConfig().Anime.source
}),Box::new( || {
getSources("anime")
}),Box::new(move |v| {
setAnimeSource(v.clone());
tx_a.send(ApplySettingsChange::setAnime(v));
true
})),
})
.add_item(Item {
label: "Sorgente Manga predefinita".to_owned(),
option: crate::components::ListOption::ItemOption::new_select_option(Box::new(|| {
getConfig().Manga.source
}),Box::new( || {
getSources("manga")
}),Box::new(move |v| {
setMangaSource(v.clone());
tx_m.send(ApplySettingsChange::setManga(v));
true
})),
});
s
}
pub fn buildColorCustomization(tx: tokio::sync::mpsc::UnboundedSender<ApplySettingsChange>) -> Section {
let tx_a = tx.clone();
let tx_m = tx.clone();
let s = Section::new("Theme".to_owned(), Style::default())
.add_item(Item {
label: "".to_owned(),
option: crate::components::ListOption::ItemOption::new_color_picker(Box::new(|| {
return ColorPickerOption::default(); // TODO
}), Box::new(move |c| {
tx_m.send(ApplySettingsChange::setColorTheme("example".to_owned(), c));
true
})),
});
s
}
pub fn selectnext(&mut self) {
self.listoptionstate.selectnext();
}
pub fn selectprev(&mut self) {
self.listoptionstate.selectprev();
}
pub fn select_sec_next(&mut self) {
self.listoptionstate.select_sec_next();
}
pub fn select_sec_prev(&mut self) {
self.listoptionstate.select_sec_prev();
}
pub fn is_editing(&self) -> bool {
self.listoptionstate.edit_option
}
pub fn handle_inputs(&mut self, k: ratatui::crossterm::event::KeyCode) -> bool {
self.listoptionstate.handle_input(k)
}
pub fn turn_editing_mode(&mut self, b:bool) {
self.listoptionstate.edit_option = b;
}
pub fn handle_control_inputs(&mut self, k:ratatui::crossterm::event::KeyCode) {
self.listoptionstate.handle_control_inputs(k);
}
}
impl StatefulWidget for Settings {
type State = SettingsState;
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
// Mutable but one creation only,
ListOption::new().add_sections(state.listoption_sections.clone())
.render(area, buf, &mut state.listoptionstate);
}
}
pub enum ApplySettingsChange {
setManga(String),
setAnime(String),
setAppStatus(AppState),
setColorTheme(String, crate::utils::utils::ColorPickerOption)
}
+108 -19
View File
@@ -1,11 +1,13 @@
use std::collections::HashMap; use std::{collections::HashMap, fs, path::Path, process::{Command, Stdio}, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use crate::{config::getConfig, log, utils::utils::AsyncError};
pub mod animeworld; pub mod animeworld;
pub mod mangaworld; pub mod mangaworld;
pub mod mangago;
pub const AVAIBLEANIMESOURCE: [&str; 1] = ["animeworld"];
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum SourceType { pub enum SourceType {
Anime, Manga Anime, Manga
@@ -58,6 +60,7 @@ pub struct Chapter {
#[async_trait] #[async_trait]
pub trait AnimeSource: Send + Sync { pub trait AnimeSource: Send + Sync {
fn has_update_page(&self) -> bool;
/*let mut items = hostname.split(".").collect::<Vec<&str>>(); /*let mut items = hostname.split(".").collect::<Vec<&str>>();
if let Some(f) = items.first() { if let Some(f) = items.first() {
if *f != "www" { if *f != "www" {
@@ -81,25 +84,25 @@ pub trait AnimeSource: Send + Sync {
let a = get(u).await?.error_for_status()?.text().await?; let a = get(u).await?.error_for_status()?.text().await?;
return Html::parse_document(&a); return Html::parse_document(&a);
}*/ }*/
async fn fetch_updated_page(&self, page: usize)-> Result<String, Box<dyn std::error::Error + Send + Sync>>; async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError>;
fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Episode>, Box<dyn std::error::Error + Send + Sync>>; fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Episode>, AsyncError>;
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, Box<dyn std::error::Error + Send + Sync>>; async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError>;
fn extract_episode_from_search(&self, document: String) -> Result<Vec<Episode>, Box<dyn std::error::Error + Send + Sync>>; fn extract_episode_from_search(&self, document: String) -> Result<Vec<Episode>, AsyncError>;
async fn listupdated(&self, page: usize) -> Result<Vec<Episode>, Box<dyn std::error::Error + Send + Sync>> { async fn listupdated(&self, page: usize) -> Result<Vec<Episode>, AsyncError> {
let body = self.fetch_updated_page(page).await?; let body = self.fetch_updated_page(page).await?;
self.extract_episode_from_updated(body) self.extract_episode_from_updated(body)
} }
async fn search(&self, query: String, page: usize) -> Result<Vec<Episode>, Box<dyn std::error::Error + Send + Sync>>{ async fn search(&self, query: String, page: usize) -> Result<Vec<Episode>, AsyncError>{
let body = self.fetch_search_page(query, page).await?; let body = self.fetch_search_page(query, page).await?;
self.extract_episode_from_search(body) self.extract_episode_from_search(body)
} }
async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>; 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 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 fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>;
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box<dyn std::error::Error + Send + Sync>>; async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError>;
} }
#[async_trait] #[async_trait]
@@ -120,29 +123,115 @@ pub trait MangaSource: Send + Sync {
fn get_hostname(&self)->String; fn get_hostname(&self)->String;
fn get_url(&self)->String; fn get_url(&self)->String;
fn has_update_page(&self) -> bool;
/** { /** {
let p = std::cmp::max(1, page); let p = std::cmp::max(1, page);
let u = format!("{}/updated?page={}",self.url,p); let u = format!("{}/updated?page={}",self.url,p);
let a = get(u).await?.error_for_status()?.text().await?; let a = get(u).await?.error_for_status()?.text().await?;
return Html::parse_document(&a); return Html::parse_document(&a);
}*/ }*/
async fn fetch_updated_page(&self, page: usize)-> Result<String, Box<dyn std::error::Error + Send + Sync>>; async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError>;
fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Chapter>, Box<dyn std::error::Error + Send + Sync>>; fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Chapter>, AsyncError>;
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, Box<dyn std::error::Error + Send + Sync>>; async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError>;
fn extract_episode_from_search(&self, document: String) -> Result<Vec<Chapter>, Box<dyn std::error::Error + Send + Sync>>; fn extract_episode_from_search(&self, document: String) -> Result<Vec<Chapter>, AsyncError>;
async fn listupdated(&self, page: usize) -> Result<Vec<Chapter>, Box<dyn std::error::Error + Send + Sync>> { async fn listupdated(&self, page: usize) -> Result<Vec<Chapter>, AsyncError> {
let body = self.fetch_updated_page(page).await?; let body = self.fetch_updated_page(page).await?;
self.extract_episode_from_updated(body) self.extract_episode_from_updated(body)
} }
async fn search(&self, query: String, page: usize) -> Result<Vec<Chapter>, Box<dyn std::error::Error + Send + Sync>>{ async fn search(&self, query: String, page: usize) -> Result<Vec<Chapter>, AsyncError>{
let body = self.fetch_search_page(query, page).await?; let body = self.fetch_search_page(query, page).await?;
self.extract_episode_from_search(body) 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 lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>;
// async fn chapter(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>; // 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>>; // async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box<dyn std::error::Error + Send + Sync>>; // async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError>;
async fn extract_pages_url(&self, chapter_url: String) -> Result<Vec<String>, AsyncError>;
async fn to_pdf(&self, metadata: MangaMetadata, img:Vec<String>) -> Result<String, AsyncError>;
async fn to_cbz(&self, metadata: MangaMetadata, img:Vec<String>) -> Result<String, AsyncError>;
// async fn chapterMetadata(&self, url: String) -> MangaMetadata;
}
pub fn openManga(so: Arc<Box<dyn MangaSource + Send + Sync>>, metadata: MangaMetadata) {
let c = getConfig().Manga.reader;
// log!("1");
tokio::spawn(async move {
// log!("2");
if let Ok(imgs) = so.extract_pages_url(metadata.url.clone()).await {
let path = match c {
crate::config::MangaReader::CBZ | crate::config::MangaReader::Sumatra | crate::config::MangaReader::Okular => {
so.to_cbz(metadata, imgs).await
},
crate::config::MangaReader::PDF | crate::config::MangaReader::Calibre => {
so.to_pdf(metadata, imgs).await
},
};
match path {
Err(k) => {
log!("{}",k);
// ex.send(k); // TODO Send error string to a tx<string>
},
Ok(s) => {
let command = getConfig().Manga.programCmd.clone();
let pargs = getConfig().Manga.programArg;
let args = pargs.split(" ").filter(|x| !x.is_empty());
let cmd = match c {
crate::config::MangaReader::Sumatra => {
let mut a = Command::new(command);
a.args(args).arg(s.clone());
Some(a)
},
crate::config::MangaReader::Okular => {
let mut a = Command::new(command);
a.args(args).arg(s.clone());
Some(a)
},
crate::config::MangaReader::Calibre => {
let mut a = Command::new(command);
a.args(args).arg(s.clone());
Some(a)
}
crate::config::MangaReader::PDF => None,
crate::config::MangaReader::CBZ => None,
};
match cmd {
Some(mut c) => {
let _ = c.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
None => {},
};
}
};
}
});
}
pub struct MangaMetadata {
pub title: Option<String>,
pub url: String,
pub chapter: Option<String>,
pub volume: Option<String>
}
pub fn getSources(folder: &str) -> Vec<String> {
let mut p = Path::new("./extensions").join(folder);
let mut res = vec![];
for files in fs::read_dir(p) {
for rf in files {
if let Ok(f) = rf {
if let Some(name) = f.path().file_stem() {
if let Some(s) = name.to_str() {
res.push(s.to_owned());
}
}
}
}
}
res
} }
+10 -9
View File
@@ -4,7 +4,7 @@ use std::{collections::HashMap, process::{Command, Stdio}};
use reqwest::get; use reqwest::get;
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use crate::{log, source::{AnimeSource, Episode, EpisodeListItem, SourceType}}; use crate::{log, source::{AnimeSource, Episode, EpisodeListItem, SourceType}, utils::utils::AsyncError};
pub struct Animeworld { pub struct Animeworld {
url: String, url: String,
@@ -19,6 +19,7 @@ impl Animeworld {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl AnimeSource for Animeworld { impl AnimeSource for Animeworld {
fn has_update_page(&self) -> bool {true}
fn get_hostname(&self)->String { fn get_hostname(&self)->String {
self.hostname.clone() self.hostname.clone()
} }
@@ -33,11 +34,11 @@ impl AnimeSource for Animeworld {
sourcetype: crate::source::SourceType::Anime sourcetype: crate::source::SourceType::Anime
} }
} }
async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Episode>, Box<dyn std::error::Error + Send + Sync>> { async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Episode>, AsyncError> {
let body = self.fetch_updated_page(page).await?; let body = self.fetch_updated_page(page).await?;
self.extract_episode_from_updated(body) 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>> { fn extract_episode_from_search(&self, body: String) -> Result<Vec<crate::source::Episode>, AsyncError> {
// let sel = Selector::parse("div.film-list").unwrap(); // let sel = Selector::parse("div.film-list").unwrap();
let document = Html::parse_document(&body); let document = Html::parse_document(&body);
let sel_inner = Selector::parse("div.inner").unwrap(); let sel_inner = Selector::parse("div.inner").unwrap();
@@ -64,7 +65,7 @@ impl AnimeSource for Animeworld {
}); });
Ok(cards.collect::<Vec<Episode>>()) 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>> { fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Episode>, AsyncError> {
self.extract_episode_from_search(body) self.extract_episode_from_search(body)
} }
@@ -99,26 +100,26 @@ impl AnimeSource for Animeworld {
Ok(String::new()) Ok(String::new())
} }
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError> {
// if page < 1 { page = 1}; // if page < 1 { page = 1};
let u = format!("{}?keyword={}&page={}",self.get_url(), query, page); let u = format!("{}?keyword={}&page={}",self.get_url(), query, page);
let r = reqwest::get(u).await?.error_for_status()?.text().await?; let r = reqwest::get(u).await?.error_for_status()?.text().await?;
return Ok(r); return Ok(r);
} }
async fn search(&self, query: String, page: usize) -> Result<Vec<crate::source::Episode>, Box<dyn std::error::Error + Send + Sync>>{ async fn search(&self, query: String, page: usize) -> Result<Vec<crate::source::Episode>, AsyncError>{
let body = self.fetch_search_page(query, page).await?; let body = self.fetch_search_page(query, page).await?;
self.extract_episode_from_search(body) self.extract_episode_from_search(body)
} }
async fn fetch_updated_page(&self, page: usize)-> Result<String, Box<dyn std::error::Error + Send + Sync>> { async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError> {
let p = std::cmp::max(1, page); let p = std::cmp::max(1, page);
let u = format!("{}/updated?page={}",self.get_url(),p); let u = format!("{}/updated?page={}",self.get_url(),p);
let a = get(u).await?.error_for_status()?.text().await?; let a = get(u).await?.error_for_status()?.text().await?;
return Ok(a); return Ok(a);
} }
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>>{ async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>{
let mut hs = HashMap::<String, EpisodeListItem>::new(); let mut hs = HashMap::<String, EpisodeListItem>::new();
if let Ok(a) = get(url).await?.error_for_status()?.text().await { if let Ok(a) = get(url).await?.error_for_status()?.text().await {
let b = Html::parse_document(&a); let b = Html::parse_document(&a);
@@ -133,7 +134,7 @@ impl AnimeSource for Animeworld {
} }
return Ok(hs); return Ok(hs);
} }
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError> {
if let Some(url) = Self::id_extract(&eli.url) { if let Some(url) = Self::id_extract(&eli.url) {
if let Ok(vlc_url) =self.episode_play_url(url).await { if let Ok(vlc_url) =self.episode_play_url(url).await {
if !vlc_url.is_empty() { if !vlc_url.is_empty() {
File diff suppressed because it is too large Load Diff
+117 -66
View File
@@ -1,10 +1,10 @@
use std::collections::HashMap; use std::{collections::HashMap, f64::consts::E, fmt::format, io::Read, sync::Arc};
use crc32fast::Hasher;
use reqwest::get; use reqwest::get;
use scraper::{Html, Selector}; use scraper::{ElementRef, Html, Selector};
use tokio::io::split; use zip::write::SimpleFileOptions;
use crate::source::{Chapter, EpisodeListItem, MangaSource, SourceType}; use crate::{log, source::{Chapter, EpisodeListItem, MangaMetadata, MangaSource, SourceType, openManga}, utils::utils::{AsyncError, MangaCbz}};
pub struct Mangaworld { pub struct Mangaworld {
url: String, url: String,
@@ -20,7 +20,7 @@ impl MangaSource for Mangaworld {
fn get_url(&self) -> String { fn get_url(&self) -> String {
self.url.clone() self.url.clone()
} }
fn has_update_page(&self) -> bool {true}
fn new(hostname: String, url: String) -> Self { fn new(hostname: String, url: String) -> Self {
Self { Self {
url, url,
@@ -28,12 +28,12 @@ impl MangaSource for Mangaworld {
sourcetype: crate::source::SourceType::Manga sourcetype: crate::source::SourceType::Manga
} }
} }
async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Chapter>, Box<dyn std::error::Error + Send + Sync>> { async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Chapter>, AsyncError> {
let body = self.fetch_updated_page(page).await?; let body = self.fetch_updated_page(page).await?;
self.extract_episode_from_updated(body) self.extract_episode_from_updated(body)
} }
//https://www.mangaworld.mx/archive?keyword= //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>> { fn extract_episode_from_search(&self, body: String) -> Result<Vec<crate::source::Chapter>, AsyncError> {
let document = Html::parse_document(&body); let document = Html::parse_document(&body);
let sel_inner = Selector::parse("div.entry").unwrap(); let sel_inner = Selector::parse("div.entry").unwrap();
// let sel_episode_number = Selector::parse("a.xanh").unwrap(); // let sel_episode_number = Selector::parse("a.xanh").unwrap();
@@ -60,7 +60,7 @@ impl MangaSource for Mangaworld {
Ok(cards.collect::<Vec<Chapter>>()) Ok(cards.collect::<Vec<Chapter>>())
} }
//https://www.mangaworld.ac/ //https://www.mangaworld.ac/
fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Chapter>, Box<dyn std::error::Error + Send + Sync>> { fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Chapter>, AsyncError> {
let document = Html::parse_document(&body); let document = Html::parse_document(&body);
let sel_inner = Selector::parse("div.entry").unwrap(); let sel_inner = Selector::parse("div.entry").unwrap();
let sel_episode_number = Selector::parse("a.xanh").unwrap(); let sel_episode_number = Selector::parse("a.xanh").unwrap();
@@ -69,11 +69,12 @@ impl MangaSource for Mangaworld {
.filter_map(|card| { .filter_map(|card| {
let carde = card.select(&sel_episode_number).next()?; let carde = card.select(&sel_episode_number).next()?;
let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0); let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
let url = carde.value().attr("href").unwrap_or("").trim().to_owned();
// log!("index: {}", index); // log!("index: {}", index);
let carda = card.select(&sel_anime_name).next()?; let carda = card.select(&sel_anime_name).next()?;
// log!("Card a found"); // log!("Card a found");
let url = carda.value().attr("href").unwrap_or("").trim().to_owned(); // log!("{}\t{}\t{}\t{}");
// log!("Card url: {}",url); // log!("Card url: {}",url);
let title = carda.inner_html().trim().to_owned(); let title = carda.inner_html().trim().to_owned();
// log!("Card title: {}", title); // log!("Card title: {}", title);
@@ -86,86 +87,62 @@ impl MangaSource for Mangaworld {
}); });
Ok(cards.collect::<Vec<Chapter>>()) 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(); async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError> {
// 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 u = format!("{}?keyword={}&page={}",self.get_url(), query, page);
let r = reqwest::get(u).await?.error_for_status()?.text().await?; let r = reqwest::get(u).await?.error_for_status()?.text().await?;
return Ok(r); 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>> {
async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError> {
let p = std::cmp::max(1, page); let p = std::cmp::max(1, page);
let u = format!("{}",self.get_url()); let u = format!("{}?page={}",self.get_url(), p);
let a = get(u).await?.error_for_status()?.text().await?; let a = get(u).await?.error_for_status()?.text().await?;
return Ok(a); return Ok(a);
} }
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>> { async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError> {
let mut hs = HashMap::<String, EpisodeListItem>::new(); let mut hs = HashMap::<String, EpisodeListItem>::new();
if let Ok(a) = get(url).await?.error_for_status()?.text().await { if let Ok(a) = get(url).await?.error_for_status()?.text().await {
let b = Html::parse_document(&a); 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_element = Selector::parse(".chapters-wrapper .volume-element").unwrap();
let select_volume_name = Selector::parse(".volume .volume-name").unwrap(); let select_volume_name = Selector::parse(".volume .volume-name").unwrap();
let select_episodes = Selector::parse("div.chapter a.chap").unwrap(); let select_episodes = Selector::parse("div.chapter a.chap").unwrap();
let select_span = Selector::parse("span").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) { for vol in b.select(&select_volume_element) {
if let Some(volname) = vol.select(&select_volume_name).next() { if let Some(volname) = vol.select(&select_volume_name).next() {
let volume = volname.text().next(); let vv:Vec<&str> = volname.text().collect();
let volume = vv.join(" ");
for ep in vol.select(&select_episodes) { for ep in vol.select(&select_episodes) {
if let Some(span) = ep.select(&select_span).next() { if let Some(span) = ep.select(&select_span).next() {
let alink = ep.value(); let alink = ep.value();
let chapter = span.inner_html(); let chapter = span.inner_html();
let x = volume let format_part = |input: &str, prefix: &str| -> String {
.and_then(|s| s.split_whitespace().nth(1)) input.split_whitespace().nth(1)
.and_then(|second| second.split_once('.')); .map(|num_str| {
let y = chapter.split_whitespace().nth(1) match num_str.split_once('.') {
.and_then(|second| second.split_once('.')); Some((a, b)) if !b.is_empty() => {
format!("{} {:>4}.{}", prefix, a, b)
let title_vol = match x {
Some((vola,volb)) => {
if volb.is_empty() {
format!("Volume {:>4}", vola)
} else {
format!("Volume {:>4}.{}", vola,volb)
}
}, },
_ => "".to_owned() Some((_, _)) | None => {
}; let m = num_str.trim_end_matches('.');
let title_cha = match y { format!("{} {:>4}", prefix, m)
Some((cha,chb)) => {
if chb.is_empty() {
format!("Capitolo {:>4}", cha)
} else {
format!("Capitolo {:>4}.{}", cha,chb)
} }
}, }
_ => "".to_owned() })
.unwrap_or_default()
}; };
let title_sep = if !title_vol.is_empty() && !title_cha.is_empty() {" "} else {""}; let title_vol = format_part(&volume, "Volume");
hs.insert(span.inner_html(), EpisodeListItem { let title_cha = format_part(&chapter, "Capitolo");
title: format!("{}{}{}", title_vol,title_sep, title_cha), let title = vec![title_vol, title_cha]
.into_iter()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
hs.insert(title.clone(), EpisodeListItem {
title,
url: alink.attr("href").unwrap_or("").trim().to_owned() url: alink.attr("href").unwrap_or("").trim().to_owned()
}); });
} }
@@ -175,7 +152,81 @@ impl MangaSource for Mangaworld {
} }
return Ok(hs); return Ok(hs);
} }
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(()) //TODO
async fn extract_pages_url(&self, chapter_url: String) -> Result<Vec<String>, AsyncError> {
let url = format!("{}/1?style=list", chapter_url);
let a = get(url).await?.error_for_status()?.text().await?;
let b = Html::parse_document(&a);
let sel = Selector::parse("#page img").unwrap();
let pages =b.select(&sel).filter_map(|img| img.value().attr("src")).map(|x| x.to_owned());
Ok(pages.collect())
} }
async fn to_pdf(&self, metadata: MangaMetadata, images:Vec<String>) -> Result<String, AsyncError> {
Ok("".to_owned())
} }
async fn to_cbz(&self, metadata: MangaMetadata, images:Vec<String>) -> Result<String, AsyncError> {
let bp = format!("./downloads/manga/{}/{}", metadata.title.unwrap_or("manga".to_owned()).replace(".", "_").replace("/", "_").to_ascii_lowercase(),metadata.chapter.unwrap_or("none".to_owned()));
let cbz = MangaCbz::new(bp.clone(), false)?;
let mut i:usize = 0;
std::fs::create_dir_all(&bp)?;
for img in images {
let mut h = Hasher::new();
let png = get(img).await?.bytes().await?;
h.update(&png); // TODO
let hs= h.finalize();
let b = cbz.has_page(i,hs);
log!("has page i={} b={}",i,b);
if !b {
let image_path = format!("{}/{}.png",bp,i);
std::fs::File::create(&image_path)?;
std::fs::write(&image_path, png)?;
cbz.add_page_path(image_path.clone())?;
std::fs::remove_file(image_path)?;
}
i=i+1;
}
std::fs::remove_dir_all(&bp)?;
Ok(cbz.path)
}
// async fn chapterMetadata(&self, url: String) -> MangaMetadata {
// //url = https://www.mangaworld.mx/manga/1637/chainsaw-man/read/5f9cc1d81db7241f07086b78
// if let Ok(res) = get(&url).await {
// if let Ok(body) = res.text().await {
// let html = Html::parse_document(&body);
// let reader_a = Selector::parse("#reader a").unwrap();
// let volopt = Selector::parse(".volume option").unwrap();
// let chaopt = Selector::parse(".chapter option").unwrap();
// let title = match html.select(&reader_a).next() {
// Some(t) => {
// let a:Vec<&str> = t.text().collect();
// let b = a.join(" ");
// let c = b.clone();
// let prefix = "Ritorna a";
// Some(b.clone().strip_prefix(prefix).map(|f| f.to_owned()).unwrap_or(c))
// },
// None => None,
// };
// let cv_num = |a: ElementRef| {
// let v:Vec<&str> = a.text().collect();
// let vs = v.join(" ");
// let last = vs.split(" ").last();
// last.map(|n| n.trim_start_matches("0").to_owned())
// };
// let volume = html.select(&volopt).find(|e| e.attr("selected").is_some()).and_then(cv_num);
// let chapter = html.select(&chaopt).find(|e| e.attr("selected").is_some()).and_then(cv_num);
// return MangaMetadata {
// title,
// url: Some(url),
// chapter: chapter,
// volume: volume
// };
// }
// }
// MangaMetadata { title: None, url: Some(url), chapter: None, volume: None }
// }
}
+154
View File
@@ -1,6 +1,18 @@
pub mod utils { pub mod utils {
use block_padding::ZeroPadding;
use ratatui::style::palette::tailwind;
use regex::Regex;
pub type AsyncError = Box<dyn std::error::Error + Send + Sync>;
use aes::{Aes128, cipher::BlockDecryptMut}; // Use Aes256 if your key is 32 bytes
use cbc::{Decryptor, cipher::KeyIvInit};
use std::collections::HashMap; use std::collections::HashMap;
use std::panic;
use std::fs::OpenOptions;
use std::io::Write;
use std::backtrace::Backtrace;
use std::process::{Command, Stdio};
#[cfg(feature = "debug")]
#[macro_export] #[macro_export]
macro_rules! log { macro_rules! log {
($($arg:tt)*) => {{ ($($arg:tt)*) => {{
@@ -16,6 +28,23 @@ pub mod utils {
}}; }};
} }
#[cfg(not(feature = "debug"))]
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {{}};
}
pub static JS_FILTERS: &[&str] = &[
"jQuery",
"document",
"getContext",
"toDataURL",
"getImageData",
"width",
"height",
];
pub fn first_capital(s: &str) -> String { pub fn first_capital(s: &str) -> String {
let mut chars = s.chars(); let mut chars = s.chars();
match chars.next() { match chars.next() {
@@ -36,4 +65,129 @@ pub mod utils {
}); });
items items
} }
// pub fn image_regex() -> Regex {
// return Regex::new(r#"var imgsrcs\s*=\s*['"]([a-zA-Z0-9+=/]+)['"]"#).unwrap();
// }
pub struct MangaCbz {
pub path: String,
pub pages: HashMap<usize, u32>
}
impl MangaCbz {
pub fn new(mut path: String, truncate: bool) -> Result<Self, AsyncError>{
if !path.ends_with(".cbz") {
path = path+".cbz";
}
let mut pages = HashMap::new();
let ex = std::fs::exists(&path).unwrap_or(true);
if truncate || !ex {
let p = std::path::Path::new(&path).parent().unwrap();
std::fs::create_dir_all(&p)?;
let file = std::fs::File::create(&path)?;
let zip = zip::ZipWriter::new(file);
zip.finish()?;
} else {
let file = std::fs::File::open(&path)?;
let mut zip = zip::ZipArchive::new(file)?;
for i in 0..zip.len() {
if let Ok(img) = zip.by_index(i) {
if let Some(s) = img.name().rsplitn(2,".").nth(0) {
if let Ok(u) = s.parse::<usize>() {
pages.insert(u, img.crc32());
}
}
}
};
}
return Ok(MangaCbz {
path,
pages
});
}
pub fn add_page_path(&self, image_path: String) -> Result<(),AsyncError>{
let sta = Command::new("7z")
.arg("a")
.arg(&self.path)
.arg(image_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
log!("{}",sta);
Ok(())
}
pub fn has_page(&self, i: usize, hs: u32) -> bool {
match self.pages.get(&i) {
Some(u) => u == &hs,
_ => false
}
}
}
// Found on stackoverflow
#[cfg(feature = "debug")]
pub fn setup_panic_hook() {
panic::set_hook(Box::new(|info| {
let backtrace = Backtrace::capture();
let msg = format!(
"=== PANIC ===\nMessage: {}\nLocation: {}\n\nBacktrace:\n{}\n",
info.payload()
.downcast_ref::<&str>()
.copied()
.or_else(|| info.payload().downcast_ref::<String>().map(|s| s.as_str()))
.unwrap_or("<no message>"),
info.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
.unwrap_or_else(|| "<unknown location>".to_owned()),
backtrace,
);
// also print to stderr so you see it in the terminal
eprintln!("{msg}");
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open("./panic.log")
{
let _ = writeln!(file, "{msg}");
}
}));
}
#[cfg(not(feature = "debug"))]
pub fn setup_panic_hook() {}
pub struct ColorPickerOption {
pub _type:ColorPickerOptionType,
red: u8,
green: u8,
blue: u8
}
pub enum ColorPickerOptionType {
RGB, HEX
}
impl ColorPickerOption {
pub fn default() -> ColorPickerOption {
ColorPickerOption { _type: ColorPickerOptionType::HEX, red: 0, green: 0, blue: 0 }
}
pub fn redString(&self) -> String {
match self._type {
ColorPickerOptionType::RGB => format!("{:0<3}", self.red.to_string()),
ColorPickerOptionType::HEX => format!("{:0x?}", self.red).get(2..).unwrap_or("00").to_owned(),
}
}
pub fn greenString(&self) -> String {
match self._type {
ColorPickerOptionType::RGB => format!("{:0<3}", self.green.to_string()),
ColorPickerOptionType::HEX => format!("{:0x?}", self.green).get(2..).unwrap_or("00").to_owned(),
}
}
pub fn blueString(&self) -> String {
match self._type {
ColorPickerOptionType::RGB => format!("{:0<3}", self.blue.to_string()),
ColorPickerOptionType::HEX => format!("{:0x?}", self.blue).get(2..).unwrap_or("00").to_owned(),
}
}
}
} }