0.1.0
This commit is contained in:
Generated
+1450
-19
File diff suppressed because it is too large
Load Diff
+22
-1
@@ -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
@@ -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"
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
Type = "Manga"
|
||||||
|
|
||||||
|
[Info]
|
||||||
|
Name = "Mangago"
|
||||||
|
Description = ""
|
||||||
|
Language = "EN/en"
|
||||||
|
|
||||||
|
[Sitemanager]
|
||||||
|
Hostname = "mangago.me"
|
||||||
|
Url = "https://www.mangag.me"
|
||||||
@@ -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/"
|
||||||
+803
-18
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+123
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user