Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0f81a9ef0 | |||
| 9d8936c379 |
@@ -1,9 +0,0 @@
|
||||
|
||||
[target.aarch64-linux-android]
|
||||
linker = "/home/domenico/Android/Sdk/ndk/30.0.14904198/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android36-clang"
|
||||
|
||||
[env]
|
||||
CC_aarch64_linux_android = "/home/domenico/Android/Sdk/ndk/30.0.14904198/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android36-clang"
|
||||
RUST_BACKTRACE="full"
|
||||
[build]
|
||||
rustflags = ["-A", "warnings"]
|
||||
@@ -1,5 +0,0 @@
|
||||
/target
|
||||
/downloads/anime/*
|
||||
/downloads/manga/*
|
||||
debug.log
|
||||
log.txt
|
||||
BIN
Binary file not shown.
Generated
-5254
File diff suppressed because it is too large
Load Diff
-38
@@ -1,38 +0,0 @@
|
||||
[package]
|
||||
name = "Aniyomi"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.89"
|
||||
color-eyre = "0.6.5"
|
||||
curl = "0.4.49"
|
||||
ratatui = "0.30.0"
|
||||
reqwest = { version = "0.13.2", features = ["rustls","blocking", "cookies"] }
|
||||
scraper = "0.25.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
strum = { version = "0.28", default-features = false, features = ["derive"] }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
toml = "1.0.4"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
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 = []
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
marp: true
|
||||
paginate: true
|
||||
---
|
||||
<!--- ESISTE UNA VERSIONE PDF più leggibile se questa risulta confusionaria --->
|
||||
# Inizializzazione
|
||||
1. Aggiungere il percorso di vlc alla variabile d'ambiente path.
|
||||
- Installare vlc se non già fatto.
|
||||
- Verificare se l'installazione è come da default in
|
||||
```C:\Program Files\VideoLAN\VLC```, altrimenti:
|
||||
- Cercare vlc sulla ricerca di windows (forse è chiamato in modi leggermente diversi, quali ad esempio VLC media player)
|
||||
- Apri percorso file
|
||||
- Tasto destro -> Apri percorso file sul file selezionato da windows
|
||||
- Ci troveremo nella cartella nella quale c'è un file chiamato vlc (questo così com'è scritto, non in modi diversi)
|
||||
|
||||
---
|
||||
|
||||
- Cerchiamo "modifica le variabili di ambiente relative al sistema", apriamo
|
||||
- clicchiamo su variabile d'ambiente in basso a destra
|
||||
- Doppio click su Path per aprire la variabile corrispondente
|
||||
- Aggiungiamo il percorso cliccando su nuovo e incollando il percorso, nel mio caso con un'installazione base è ```C:\Program Files\VideoLAN\VLC```
|
||||
|
||||
---
|
||||
# Spiegazione sui file
|
||||
## Formato toml
|
||||
Toml è un file di configurazione simile a yml che si pone come alternativa più semplice alla lettura del classico file json usato principalmente per il web.
|
||||
|
||||
---
|
||||
### Json
|
||||
Il formato di json è il seguente:
|
||||
```json
|
||||
{
|
||||
"key1": "value",
|
||||
"key2": {
|
||||
"key2.1": "value"
|
||||
},
|
||||
"key3": ["value", 5140]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### Yaml
|
||||
Il formato yml o yaml è il seguente:
|
||||
```yaml
|
||||
key1: value
|
||||
key2:
|
||||
key2.1: value
|
||||
key3:
|
||||
- value
|
||||
- 5140
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Toml
|
||||
Il formato toml è il seguente:
|
||||
```toml
|
||||
key1 = value
|
||||
|
||||
[key2]
|
||||
key2.1 = value
|
||||
|
||||
[[key3]]
|
||||
value
|
||||
|
||||
[[key3]]
|
||||
5140
|
||||
```
|
||||
|
||||
---
|
||||
### Perchè toml?
|
||||
Pur preferendo json perchè mi sembra il più facilmente intuibile anche se molto pieno di caratteri speciali che possono inizialmente creare confusione, useremo toml perchè nativamente supportato da rust, il linguaggio che ho usato per la creazione di questo programma.
|
||||
Yaml è molto più pulito di json ma ha spazi significativi, il che non lo rende così facile da scrivere su blocco note.
|
||||
|
||||
---
|
||||
|
||||
## Toml nei file qui presenti
|
||||
### File Library
|
||||
I file animelibrary.toml e mangalibrary.toml sono i due file designati alla gestione dei preferiti. Hanno formato:
|
||||
```toml
|
||||
[[AnimeSource]] # Scelta tra: Animeworld
|
||||
name = "Nome dell'opera"
|
||||
episodeList = ""
|
||||
```
|
||||
|
||||
```toml
|
||||
[[MangaSource]] # Scelta tra: Mangaworld, Mangago
|
||||
name = "Nome dell'opera"
|
||||
mangapage = ""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Formati accettati per episodeList e mangapage
|
||||
Saranno scritti in RegEx (ovvero regular expression), un linguaggio per scrivere pattern. Può essere testato su tool online come [regexr.com](https://regexr.com).
|
||||
I filtri successivamente sono corretti ma non precisi, potrebbero verificarsi degli errori e validare url non corretti o non validare alcuni corretti.
|
||||
|
||||
---
|
||||
##### Guida a RegEx 1/4
|
||||
Un pattern RegEx, talvolta posto tra /, è scritto come un insieme di pattern da corrispondere (match). I pattern che lo costituiscono possono essere semplici caratteri alfanumerici o caratteri speciali.
|
||||
I caratteri speciali hanno dei significati particolari e pertanto sono indicati, qualora si voglia trovare il match con quel determinato carattere speciale si usa una "escape string", ovvero si antepone \ al carattere speciale:
|
||||
- ```: -> \:```
|
||||
- ```? -> \?```
|
||||
- ```\ -> \\```
|
||||
|
||||
---
|
||||
##### Guida a RegEx 2/4
|
||||
I caratteri speciali più usati sono:
|
||||
- ```?```: carattere opzionale, indica che il match è valido anche se il carattere precedente non è matchato
|
||||
- ```.```: carattere universale, matcha ogni carattere
|
||||
- ```[]```: insieme, matcha ogni carattere nell'insieme. L'insieme può essere definito anche come una lista di cui sono indicati gli estremi seguendo la tabella dei codici dei caratteri che associa un numero a ogni carattere. All'atto pratico alcuni sono molto intuitivi: ```a-z``` alfabeto inglese minuscolo, ```A-Z``` alfabeto inglese maiuscolo, ```0-9``` cifre decimali.
|
||||
- ```^```: negazione, posto all'inizio di un insieme o all'inizio di un gruppo, nega il suo contenuto matchando tutto tranne quello i caratteri che il gruppo o l'insieme matcha
|
||||
|
||||
---
|
||||
##### Guida a RegEx 3/4
|
||||
Alcuni insiemi possono essere sintetizzati così:
|
||||
- ```\w```: ```[a-zA-Z0-9_]```
|
||||
- ```\s```: matcha ogni tipo di spazio e nuova riga (newline, carattere speciale ```\n```) e tabulazione (tab, carattere speciale ```\t```)
|
||||
- ```\d```: ```[0-9]```
|
||||
E per negare il gruppo si usa la lettera maiuscola:
|
||||
- ```\W```: ```[^a-zA-Z0-9_]```
|
||||
- ```\S```: matcha ogni carattere che non sia uno spazio, nuova riga o tab.
|
||||
- ```\D```: ```[^0-9]```
|
||||
|
||||
---
|
||||
##### Guida a RegEx 4/4
|
||||
Per ripere pattern più volte si seguono:
|
||||
- ```{min, max}```: matcha il pattern antecedente per un valore ```min``` di volte obbligatoriamente e fino a un valore ```max```.
|
||||
- ```+```: matcha almeno una volta il pattern. Non ha un limite di match possibili
|
||||
---
|
||||
|
||||
##### Animeworld
|
||||
```re
|
||||
https?\:\/\/www\.animeworld\.\w{2,3}\/play\/[a-zA-Z0-9\-]+\.\w+(\/\w+)?
|
||||
```
|
||||
Es.
|
||||
- ```https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx```
|
||||
- ```https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx/Xduvm5```
|
||||
|
||||
---
|
||||
|
||||
##### Mangaworld
|
||||
```re
|
||||
https?\:\/\/www\.mangaworld\.\w{2,3}\/manga\/\d+\/\w+
|
||||
```
|
||||
Es.
|
||||
- ```https://www.mangaworld.mx/manga/1683/kingdom```
|
||||
|
||||
---
|
||||
##### Mangago
|
||||
```re
|
||||
https?\:\/\/www\.mangago\.me\/read\-manga\/\w+\/
|
||||
```
|
||||
Es.
|
||||
- ```https://www.mangago.me/read-manga/medalist/```
|
||||
|
||||
---
|
||||
|
||||
### Config
|
||||
Il file config è il file di impostazioni del programma.
|
||||
#### [Anime]
|
||||
Modifica le impostazioni generali delle sorgenti anime.
|
||||
`source` definisce la sorgente di default da usare.
|
||||
Sorgenti disponibili:
|
||||
- Animeworld
|
||||
---
|
||||
|
||||
#### [Manga]
|
||||
Modifica le impostazioni generali delle sorgenti anime.
|
||||
`source` definisce la sorgente di default da usare.
|
||||
Sorgenti disponibili:
|
||||
- Mangaworld
|
||||
- Mangago `[W.I.P: known issue nuovi anime non trovati. Nella libreria funziona correttamente]`
|
||||
|
||||
---
|
||||
#### [Manga] reader
|
||||
L'opzione reader può essere configurata con una delle seguenti alternative.
|
||||
Es.
|
||||
`reader="Okular"`
|
||||
Alcune sono disponibili solo per alcuni sistemi operativi:
|
||||
- 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)
|
||||
- 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.
|
||||
|
||||
---
|
||||
#### [General]
|
||||
Permette mediante la scelta di una delle due opzioni di disattivare o attivare il menù di conferma dell'uscita dall'app.
|
||||
```toml
|
||||
askToQuit = false
|
||||
askToQuit = true
|
||||
```
|
||||
---
|
||||
|
||||
#### [Theme]
|
||||
Modifica i colori dell'app, dispositivi diversi possiedono configurazioni diverse del terminale quindi anche i colori possono vedersi diversamente.
|
||||
Su Linux uso un terminale quasi trasparente, su Windows ha sfondo nero, su MacOs il terminale è bianco.
|
||||
Alla versione corrente è supportato solo il formato hex #RRGGBB.
|
||||
Le opzioni disponibili sono le seguenti:
|
||||
```
|
||||
base_border anime_border manga_border
|
||||
menu_text menu_selection status_loading
|
||||
res_border res_text res_selection
|
||||
cmd_text cmd_keys list_border
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Extensions file
|
||||
I file di configurazione delle singole estensioni si trova sotto la cartella extensions. Ciò è utile per evitare di dover pubblicare una nuova versione ogni cambio di nome di dominio di uno qualsiasi delle source utilizzate. Esiste un file di configurazione per ogni sorgente divisa in anime e manga ma la struttura è analoga.
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
[[Animeworld]]
|
||||
name="Frieren: Beyond Journey's End 2"
|
||||
episodeList="https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx"
|
||||
MalUrl="https://myanimelist.net/anime/59978"
|
||||
@@ -1,3 +0,0 @@
|
||||
# cargo build --target aarch64-linux-android --release
|
||||
cargo build --release
|
||||
cargo build --target x86_64-pc-windows-gnu --release
|
||||
Binary file not shown.
@@ -1,7 +1,4 @@
|
||||
[[Animeworld]]
|
||||
name="Frieren: Beyond Journey's End 2"
|
||||
episodes="https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx"
|
||||
|
||||
[[Animeworld]]
|
||||
name="One Piece"
|
||||
episodes="https://www.animeworld.ac/play/one-piece-subita.qzG-LE/HPKmX1"
|
||||
episodeList="https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx"
|
||||
MalUrl="https://myanimelist.net/anime/59978"
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
[[Mangaworld]]
|
||||
name="Kingdom"
|
||||
episodes="https://www.mangaworld.mx/manga/1683/kingdom"
|
||||
mangapage="https://www.mangaworld.mx/manga/1683/kingdom"
|
||||
|
||||
|
||||
[[Mangaworld]]
|
||||
name="Chainsaw Man"
|
||||
episodes="https://www.mangaworld.mx/manga/1637/chainsaw-man"
|
||||
mangapage="https://www.mangaworld.mx/manga/1637/chainsaw-man"
|
||||
|
||||
|
||||
[[Mangago]]
|
||||
name="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/"
|
||||
mangapage="https://www.mangago.me/read-manga/medalist/"
|
||||
@@ -0,0 +1,13 @@
|
||||
[[Mangaworld]]
|
||||
name="Kingdom"
|
||||
mangapage="https://www.mangaworld.mx/manga/1683/kingdom"
|
||||
|
||||
|
||||
[[Mangaworld]]
|
||||
name="Chainsaw Man"
|
||||
mangapage="https://www.mangaworld.mx/manga/1637/chainsaw-man"
|
||||
|
||||
|
||||
[[Mangago]]
|
||||
name="Medalist"
|
||||
mangapage="https://www.mangago.me/read-manga/medalist/"
|
||||
-1332
File diff suppressed because it is too large
Load Diff
-232
@@ -1,232 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
use std::fs;
|
||||
|
||||
use std::result::Result::Ok;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::getConfig;
|
||||
use crate::source::mangago::Mangago;
|
||||
use crate::source::{AnimeSource, MangaSource, SourceType};
|
||||
use crate::source::animeworld::Animeworld;
|
||||
use crate::source::mangaworld::Mangaworld;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct Extension {
|
||||
Type: String,
|
||||
Info: ExtensionInfo,
|
||||
Sitemanager: ExtensionSitemanager
|
||||
}
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct ExtensionInfo {
|
||||
Name: String,
|
||||
Description: String,
|
||||
Language: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct ExtensionSitemanager {
|
||||
Hostname: String,
|
||||
Url: String,
|
||||
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
pub fn buildfromName(kind: SourceType,name: &str) -> Result<Extension, &str> {
|
||||
let f = format!("extensions/{}/{}.toml",kind.to_string(), name.to_lowercase());
|
||||
if let Ok(exists)= fs::exists(&f) {
|
||||
if exists {
|
||||
if let Ok(content)= fs::read_to_string(&f) {
|
||||
let parsed:Extension = toml::from_str(&content).unwrap();
|
||||
return Ok(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(name);
|
||||
}
|
||||
pub fn toAnimeSource(self) -> Box<dyn AnimeSource+ Send + Sync> {
|
||||
match self.Info.Name.trim().to_lowercase().as_str() {
|
||||
// "animeworld" => Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url),
|
||||
_ => Box::new(Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url))
|
||||
}
|
||||
}
|
||||
pub fn toMangaSource(self) -> Box<dyn MangaSource+ Send + Sync> {
|
||||
match self.Info.Name.trim().to_lowercase().as_str() {
|
||||
// "animeworld" => Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url),
|
||||
"mangago" => Box::new(Mangago::new(self.Sitemanager.Hostname, self.Sitemanager.Url)),
|
||||
_ => Box::new(Mangaworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url)),
|
||||
}
|
||||
}
|
||||
pub fn defaultAnimeSource() -> Result<Box<dyn AnimeSource>, String> {
|
||||
let def = &getConfig().Anime.source;
|
||||
if let Ok(ext) = Extension::buildfromName(SourceType::Anime, def) {
|
||||
return Ok(ext.toAnimeSource());
|
||||
}
|
||||
return Err(format!("No default file for {}",def));
|
||||
}
|
||||
pub fn defaultMangaeSource() -> Result<Box<dyn MangaSource>, String> {
|
||||
let def = &getConfig().Manga.source;
|
||||
if let Ok(ext) = Extension::buildfromName(SourceType::Manga, def) {
|
||||
return Ok(ext.toMangaSource());
|
||||
}
|
||||
return Err(format!("No default file for {}",def));
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use std::{ffi::OsString, fs::{create_dir_all, read_dir}};
|
||||
use serde::Deserialize;
|
||||
|
||||
// const DOWNLOADS: &str = "/downloads";
|
||||
const ANIME_FOLDER: &str = "./downloads/anime";
|
||||
const MANGA_FOLDER: &str = "./downloads/manga";
|
||||
|
||||
pub fn init_folders() {
|
||||
let _ = create_dir_all(ANIME_FOLDER);
|
||||
let _ = create_dir_all(MANGA_FOLDER);
|
||||
}
|
||||
|
||||
|
||||
pub fn list_folder(folder: &str) -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
|
||||
let r = read_dir(folder)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().is_dir())
|
||||
.map(|entry| entry.file_name())
|
||||
.collect::<Vec<OsString>>();
|
||||
Ok(r)
|
||||
}
|
||||
pub fn read_anime_folder() -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
|
||||
list_folder(ANIME_FOLDER)
|
||||
}
|
||||
pub fn read_manga_folder() -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
|
||||
list_folder(MANGA_FOLDER)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub Settings: Settings,
|
||||
pub Extension: Extensions,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Settings {
|
||||
pub Folders: Folders
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct Extensions {
|
||||
pub defaultanime: String,
|
||||
pub defaultmanga: String
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct Folders {
|
||||
pub basedir: String,
|
||||
pub extensions: String,
|
||||
pub anime: String,
|
||||
pub manga: String
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct ColorPalette {
|
||||
pub anime: String,
|
||||
pub manga: String,
|
||||
pub source: String,
|
||||
pub settings: String
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// impl Config {
|
||||
// pub fn defaultAnimeSource() -> Source {
|
||||
// Animeworld::new(hostname, url, sourcetype)
|
||||
// }
|
||||
// }
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
use std::{fs, u8};
|
||||
use ratatui::crossterm::event::KeyCode;
|
||||
use serde::Deserialize;
|
||||
#[derive(Deserialize)]
|
||||
pub struct KeybindsString {
|
||||
pub GoBack:String,
|
||||
pub Open:String,
|
||||
pub MenuUp:String,
|
||||
pub MenuDown:String,
|
||||
pub MenuLeft:String,
|
||||
pub MenuRight:String,
|
||||
pub AddFavourite:String,
|
||||
}
|
||||
pub struct Keybinds {
|
||||
pub GoBack:KeyCode,
|
||||
pub Open:KeyCode,
|
||||
pub MenuUp:KeyCode,
|
||||
pub MenuDown:KeyCode,
|
||||
pub MenuLeft:KeyCode,
|
||||
pub MenuRight:KeyCode,
|
||||
pub AddFavourite:KeyCode,
|
||||
}
|
||||
impl KeybindsString {
|
||||
pub fn build() -> Result<Self, String> {
|
||||
let f = "keybinds.toml";
|
||||
if let Ok(exists)= fs::exists(f) {
|
||||
if exists {
|
||||
if let Ok(content)= fs::read_to_string(f) {
|
||||
let parsed:KeybindsString = toml::from_str(&content).unwrap();
|
||||
return Ok(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(f.to_owned());
|
||||
}
|
||||
}
|
||||
impl Keybinds {
|
||||
pub fn build() -> Result<Keybinds, String> {
|
||||
let x: Result<Keybinds, String> = match KeybindsString::build() {
|
||||
Ok(ks) => Ok(
|
||||
Self {
|
||||
GoBack: ks.GoBack.to_key_code()?,
|
||||
Open: ks.Open.to_key_code()?,
|
||||
MenuUp: ks.MenuUp.to_key_code()?,
|
||||
MenuDown: ks.MenuDown.to_key_code()?,
|
||||
MenuLeft: ks.MenuLeft.to_key_code()?,
|
||||
MenuRight: ks.MenuRight.to_key_code()?,
|
||||
AddFavourite: ks.AddFavourite.to_key_code()?,
|
||||
}
|
||||
),
|
||||
Err(f) => Err(f),
|
||||
};
|
||||
x
|
||||
}
|
||||
pub fn buildOrDefault() -> Result<Keybinds, String> {
|
||||
let x: Result<Keybinds, String> = match KeybindsString::build() {
|
||||
Ok(ks) => Ok(
|
||||
Self {
|
||||
GoBack: ks.GoBack.to_key_code().unwrap_or(KeyCode::Esc),
|
||||
Open: ks.Open.to_key_code().unwrap_or(KeyCode::Enter),
|
||||
MenuUp: ks.MenuUp.to_key_code().unwrap_or(KeyCode::Up),
|
||||
MenuDown: ks.MenuDown.to_key_code().unwrap_or(KeyCode::Down),
|
||||
MenuLeft: ks.MenuLeft.to_key_code().unwrap_or(KeyCode::Left),
|
||||
MenuRight: ks.MenuRight.to_key_code().unwrap_or(KeyCode::Right),
|
||||
AddFavourite: ks.AddFavourite.to_key_code().unwrap_or(KeyCode::Char('p')),
|
||||
}
|
||||
),
|
||||
Err(f) => Err(f),
|
||||
};
|
||||
x
|
||||
}
|
||||
}
|
||||
trait StringToKeyCode {
|
||||
fn to_key_code(self) -> Result<KeyCode,String>;
|
||||
}
|
||||
|
||||
impl StringToKeyCode for String {
|
||||
fn to_key_code(self) -> Result<KeyCode, String> {
|
||||
match self.to_uppercase().as_ref() {
|
||||
"BACKSPACE"=>Ok(KeyCode::Backspace),
|
||||
"ENTER"=>Ok(KeyCode::Enter),
|
||||
"LEFT"=>Ok(KeyCode::Left),
|
||||
"RIGHT"=>Ok(KeyCode::Right),
|
||||
"UP"=>Ok(KeyCode::Up),
|
||||
"DOWN"=>Ok(KeyCode::Down),
|
||||
"HOME"=>Ok(KeyCode::Home),
|
||||
"END"=>Ok(KeyCode::End),
|
||||
"PAGEUP"=>Ok(KeyCode::PageUp),
|
||||
"PAGEDOWN"=>Ok(KeyCode::PageDown),
|
||||
"TAB"=>Ok(KeyCode::Tab),
|
||||
"BACKTAB"=>Ok(KeyCode::BackTab),
|
||||
"DELETE"=>Ok(KeyCode::Delete),
|
||||
"INSERT"=>Ok(KeyCode::Insert),
|
||||
"ESC"=>Ok(KeyCode::Esc),
|
||||
"CAPSLOCK"=>Ok(KeyCode::CapsLock),
|
||||
"SCROLLLOCK"=>Ok(KeyCode::ScrollLock),
|
||||
"NUMLOCK"=>Ok(KeyCode::NumLock),
|
||||
"PRINTSCREEN"=>Ok(KeyCode::PrintScreen),
|
||||
"PAUSE"=>Ok(KeyCode::Pause),
|
||||
"MENU"=>Ok(KeyCode::Menu),
|
||||
"KEYPADBEGIN"=>Ok(KeyCode::KeypadBegin),
|
||||
|
||||
a => {
|
||||
let chars: Vec<char> = a.chars().collect();
|
||||
return match chars.as_slice() {
|
||||
[c] => Ok(KeyCode::Char(*c)),
|
||||
['F', d] if d.is_ascii_digit() =>{
|
||||
let dg = d.to_digit(10).unwrap() as u8;
|
||||
Ok(KeyCode::F(dg))
|
||||
},
|
||||
_ => Err(format!("{} is invalid", self))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
|
||||
use std::result::Result::Ok;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize,Clone)]
|
||||
pub struct LibraryItem {
|
||||
pub name: String,
|
||||
pub episodes: String
|
||||
}
|
||||
|
||||
|
||||
impl LibraryItem {
|
||||
pub fn buildfromName(name: &str) -> Result<HashMap<String, Vec<LibraryItem>>, &str> {
|
||||
let f = format!("library/{}", name);
|
||||
if let Ok(exists)= fs::exists(&f) {
|
||||
if exists {
|
||||
if let Ok(content)= fs::read_to_string(&f) {
|
||||
let mut data: HashMap<String, Vec<LibraryItem>> = toml::from_str(&content).unwrap();
|
||||
for items in data.values_mut() {
|
||||
let mut hs = HashSet::new();
|
||||
items.retain(|item| {
|
||||
hs.insert(item.name.clone())
|
||||
});
|
||||
items.sort_by(|a,b| {
|
||||
a.name.cmp(&b.name)
|
||||
});
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(name);
|
||||
}
|
||||
}
|
||||
-233
@@ -1,233 +0,0 @@
|
||||
mod config;
|
||||
mod mangatab;
|
||||
mod extensions;
|
||||
use color_eyre::{Result};
|
||||
use ratatui::{
|
||||
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, macros::horizontal, style::{Color, Stylize, palette::tailwind}, text::{Line, Text}, widgets::{Block, List, ListDirection, ListItem, Padding, Paragraph, Tabs, Widget}
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
use crate::{mangatab::MangaTab, config::{getConfig, load_config, tailwindPaletteFromString}};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
load_config();
|
||||
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
selected_tab: SelectedTab
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
|
||||
enum SelectedTab {
|
||||
#[default]
|
||||
#[strum(to_string = "Anime")]
|
||||
Tab1,
|
||||
#[strum(to_string = "Manga")]
|
||||
Tab2,
|
||||
#[strum(to_string = "Source")]
|
||||
Tab3,
|
||||
#[strum(to_string = "Settings")]
|
||||
Tab4,
|
||||
}
|
||||
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
match key.code {
|
||||
KeyCode::Char('c') => self.quit(),
|
||||
_=>{}
|
||||
}
|
||||
}
|
||||
if key.modifiers.contains(KeyModifiers::SHIFT) {
|
||||
match key.code {
|
||||
KeyCode::Right => self.next_tab(),
|
||||
KeyCode::Left => self.previous_tab(),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.next();
|
||||
}
|
||||
|
||||
pub fn previous_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
}
|
||||
|
||||
pub fn subnext_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.subnext();
|
||||
}
|
||||
|
||||
pub fn subprevious_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.subprevious();
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||
fn previous(self) -> Self {
|
||||
let current_index: usize = self as usize;
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(previous_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Get the next tab, if there is no next tab return the current tab.
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min};
|
||||
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||
let [header_area, inner_area, footer_area] = vertical.areas(area);
|
||||
|
||||
let horizontal = Layout::horizontal([Min(0), Length(20)]);
|
||||
let [tabs_area, title_area] = horizontal.areas(header_area);
|
||||
|
||||
render_title(title_area, buf);
|
||||
self.render_tabs(tabs_area, buf);
|
||||
self.selected_tab.render(inner_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||
let titles = SelectedTab::iter().map(SelectedTab::title);
|
||||
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
|
||||
let selected_tab_index = self.selected_tab as usize;
|
||||
Tabs::new(titles)
|
||||
.highlight_style(highlight_style)
|
||||
.select(selected_tab_index)
|
||||
.padding("", "")
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
"Anime&Manga Scraper CLI".bold().render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Line::raw("Shift + ArrowKeys to choose the Tab | Press ESC to quit")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl Widget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// in a real app these might be separate widgets
|
||||
match self {
|
||||
Self::Tab1 => self.render_Anime(area, buf),
|
||||
Self::Tab2 => self.render_Manga(area, buf),
|
||||
Self::Tab3 => self.render_Sources(area, buf),
|
||||
Self::Tab4 => self.render_Settings(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Return tab's name as a styled `Line`
|
||||
fn title(self) -> Line<'static> {
|
||||
format!(" {self} ")
|
||||
.fg(tailwind::SLATE.c200)
|
||||
// .bg(self.palette().c900)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_Anime(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Anime Home Library: View saved and search for new one in all sources")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_Manga(self, area: Rect, buf: &mut Buffer) {
|
||||
// Paragraph::new("Manga Home Library: View saved and search for new one in all sources")
|
||||
// .block(self.block())
|
||||
// .render(area, buf);
|
||||
// INNER TAB FOR OPTIONS
|
||||
let titles = MangaTab::iter().map(MangaTab::title);
|
||||
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
|
||||
let selected_tab_index = self.selected_tab as usize;
|
||||
Tabs::new(titles)
|
||||
.highlight_style(highlight_style)
|
||||
.select(selected_tab_index)
|
||||
.padding("", "")
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
fn render_Sources(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Sources to source the web")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_Settings(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("For now just credits")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
|
||||
/// A block surrounding the tab's content
|
||||
fn block(self) -> Block<'static> {
|
||||
Block::bordered()
|
||||
.padding(Padding::horizontal(1))
|
||||
.border_style(self.palette().c500)
|
||||
}
|
||||
|
||||
fn palette(self) -> tailwind::Palette {
|
||||
match self {
|
||||
Self::Tab1 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.anime,tailwind::PURPLE),
|
||||
Self::Tab2 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.manga,tailwind::AMBER),
|
||||
Self::Tab3 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.source,tailwind::EMERALD),
|
||||
Self::Tab4 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.settings,tailwind::RED),
|
||||
}
|
||||
}
|
||||
}
|
||||
-1268
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
use ratatui::{
|
||||
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, macros::horizontal, style::{Color, Stylize, palette::tailwind}, text::{Line, Text}, widgets::{Block, List, ListDirection, ListItem, Padding, Paragraph, Tabs, Widget}
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Manga {
|
||||
selected_tab: MangaTab
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
|
||||
pub enum MangaTab {
|
||||
#[default]
|
||||
#[strum(to_string = "Latest")]
|
||||
Tab1,
|
||||
#[strum(to_string = "Top")]
|
||||
Tab2,
|
||||
#[strum(to_string = "Search")]
|
||||
Tab3
|
||||
}
|
||||
|
||||
impl MangaTab {
|
||||
pub fn title(self) -> Line<'static> {
|
||||
format!(" {self} ")
|
||||
.fg(tailwind::SLATE.c200)
|
||||
// .bg(self.palette().c900)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, style::{Color, Stylize, palette::tailwind}, symbols, text::Line, widgets::{Block, List, Padding, Paragraph, Tabs, Widget}
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
match key.code {
|
||||
KeyCode::Char('c') => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match key.code {
|
||||
KeyCode::Esc => self.quit(),
|
||||
// KeyCode::Up => self.up,
|
||||
// KeyCode
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let li = List::new([
|
||||
"1. Anime",
|
||||
"2. Manga",
|
||||
])
|
||||
.highlight_symbol(">>")
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
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)
|
||||
}
|
||||
-237
@@ -1,237 +0,0 @@
|
||||
use std::{collections::HashMap, fs, path::Path, process::{Command, Stdio}, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{config::getConfig, log, utils::utils::AsyncError};
|
||||
|
||||
pub mod animeworld;
|
||||
pub mod mangaworld;
|
||||
pub mod mangago;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SourceType {
|
||||
Anime, Manga
|
||||
}
|
||||
impl SourceType {
|
||||
pub fn to_string(self) -> String {
|
||||
match self {
|
||||
SourceType::Anime => "anime".to_owned(),
|
||||
SourceType::Manga => "manga".to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
pub enum SourceLanguages {
|
||||
It, Jp, Eng
|
||||
}
|
||||
|
||||
pub struct Series<S: AnimeSource> {
|
||||
episodes: Vec<Episode>,
|
||||
title: String,
|
||||
lang: SourceLanguages,
|
||||
url: String,
|
||||
source: S
|
||||
}
|
||||
pub struct Episode {
|
||||
pub title: String,
|
||||
pub index: usize,
|
||||
pub section: usize,
|
||||
pub url: String
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct EpisodeListItem {
|
||||
pub title: String,
|
||||
pub url: String
|
||||
}
|
||||
pub struct Comic<S: MangaSource> {
|
||||
episodes: Vec<Chapter>,
|
||||
title: String,
|
||||
lang: SourceLanguages,
|
||||
url: String,
|
||||
source: S
|
||||
}
|
||||
pub struct Chapter {
|
||||
pub title: String,
|
||||
pub index: usize,
|
||||
pub section: usize,
|
||||
pub url: String
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[async_trait]
|
||||
pub trait AnimeSource: Send + Sync {
|
||||
fn has_update_page(&self) -> bool;
|
||||
/*let mut items = hostname.split(".").collect::<Vec<&str>>();
|
||||
if let Some(f) = items.first() {
|
||||
if *f != "www" {
|
||||
items.insert(0, "www");
|
||||
}
|
||||
}
|
||||
Self {
|
||||
url: items.join("."),
|
||||
host: items[1..items.len() - 1].join("."),
|
||||
hostname: "https://".to_owned()+&items[1..].join("."),
|
||||
sourcetype
|
||||
} */
|
||||
fn new(hostname: String, url: String) -> Self where Self: Sized;
|
||||
|
||||
fn get_hostname(&self)->String;
|
||||
fn get_url(&self)->String;
|
||||
|
||||
/** {
|
||||
let p = std::cmp::max(1, page);
|
||||
let u = format!("{}/updated?page={}",self.url,p);
|
||||
let a = get(u).await?.error_for_status()?.text().await?;
|
||||
return Html::parse_document(&a);
|
||||
}*/
|
||||
async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError>;
|
||||
fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Episode>, AsyncError>;
|
||||
|
||||
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError>;
|
||||
fn extract_episode_from_search(&self, document: String) -> Result<Vec<Episode>, AsyncError>;
|
||||
|
||||
async fn listupdated(&self, page: usize) -> Result<Vec<Episode>, AsyncError> {
|
||||
let body = self.fetch_updated_page(page).await?;
|
||||
self.extract_episode_from_updated(body)
|
||||
}
|
||||
async fn search(&self, query: String, page: usize) -> Result<Vec<Episode>, AsyncError>{
|
||||
let body = self.fetch_search_page(query, page).await?;
|
||||
self.extract_episode_from_search(body)
|
||||
}
|
||||
|
||||
async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>;
|
||||
async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
|
||||
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>;
|
||||
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait MangaSource: Send + Sync {
|
||||
/*let mut items = hostname.split(".").collect::<Vec<&str>>();
|
||||
if let Some(f) = items.first() {
|
||||
if *f != "www" {
|
||||
items.insert(0, "www");
|
||||
}
|
||||
}
|
||||
Self {
|
||||
url: items.join("."),
|
||||
host: items[1..items.len() - 1].join("."),
|
||||
hostname: "https://".to_owned()+&items[1..].join("."),
|
||||
sourcetype
|
||||
} */
|
||||
fn new(hostname: String, url: String) -> Self where Self: Sized;
|
||||
|
||||
fn get_hostname(&self)->String;
|
||||
fn get_url(&self)->String;
|
||||
fn has_update_page(&self) -> bool;
|
||||
/** {
|
||||
let p = std::cmp::max(1, page);
|
||||
let u = format!("{}/updated?page={}",self.url,p);
|
||||
let a = get(u).await?.error_for_status()?.text().await?;
|
||||
return Html::parse_document(&a);
|
||||
}*/
|
||||
async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError>;
|
||||
fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Chapter>, AsyncError>;
|
||||
|
||||
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError>;
|
||||
fn extract_episode_from_search(&self, document: String) -> Result<Vec<Chapter>, AsyncError>;
|
||||
|
||||
async fn listupdated(&self, page: usize) -> Result<Vec<Chapter>, AsyncError> {
|
||||
let body = self.fetch_updated_page(page).await?;
|
||||
self.extract_episode_from_updated(body)
|
||||
}
|
||||
async fn search(&self, query: String, page: usize) -> Result<Vec<Chapter>, AsyncError>{
|
||||
let body = self.fetch_search_page(query, page).await?;
|
||||
self.extract_episode_from_search(body)
|
||||
}
|
||||
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>;
|
||||
// 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 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
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
|
||||
use std::{collections::HashMap, process::{Command, Stdio}};
|
||||
|
||||
use reqwest::get;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
use crate::{log, source::{AnimeSource, Episode, EpisodeListItem, SourceType}, utils::utils::AsyncError};
|
||||
|
||||
pub struct Animeworld {
|
||||
url: String,
|
||||
hostname:String,
|
||||
sourcetype: SourceType
|
||||
}
|
||||
|
||||
impl Animeworld {
|
||||
fn id_extract( s:&str) -> Option<&str> {
|
||||
s.split("/").last()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl AnimeSource for Animeworld {
|
||||
fn has_update_page(&self) -> bool {true}
|
||||
fn get_hostname(&self)->String {
|
||||
self.hostname.clone()
|
||||
}
|
||||
fn get_url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
fn new(hostname: String, url: String) -> Self {
|
||||
Self {
|
||||
url,
|
||||
hostname,
|
||||
sourcetype: crate::source::SourceType::Anime
|
||||
}
|
||||
}
|
||||
async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Episode>, AsyncError> {
|
||||
let body = self.fetch_updated_page(page).await?;
|
||||
self.extract_episode_from_updated(body)
|
||||
}
|
||||
fn extract_episode_from_search(&self, body: String) -> Result<Vec<crate::source::Episode>, AsyncError> {
|
||||
// let sel = Selector::parse("div.film-list").unwrap();
|
||||
let document = Html::parse_document(&body);
|
||||
let sel_inner = Selector::parse("div.inner").unwrap();
|
||||
let sel_episode_number = Selector::parse("div.ep").unwrap();
|
||||
let sel_anime_name = Selector::parse("a.name").unwrap();
|
||||
let cards = document.select(&sel_inner)
|
||||
.filter_map(|card| {
|
||||
let carde = card.select(&sel_episode_number).next()?;
|
||||
let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
|
||||
// log!("index: {}", index);
|
||||
let carda = card.select(&sel_anime_name).next()?;
|
||||
|
||||
// log!("Card a found");
|
||||
let url = carda.value().attr("href").unwrap_or("").trim().to_owned();
|
||||
// log!("Card url: {}",url);
|
||||
let title = carda.inner_html().trim().to_owned();
|
||||
// log!("Card title: {}", title);
|
||||
Some(Episode {
|
||||
title,
|
||||
index,
|
||||
url,
|
||||
section: 0
|
||||
})
|
||||
});
|
||||
Ok(cards.collect::<Vec<Episode>>())
|
||||
}
|
||||
fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Episode>, AsyncError> {
|
||||
self.extract_episode_from_search(body)
|
||||
}
|
||||
|
||||
|
||||
async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let id= Self::id_extract(&card.url).unwrap();
|
||||
let u = format!("{}/api/episode/serverPlayerAnimeWorld?id={}", self.url, id);
|
||||
let r = get(u).await?.error_for_status()?.text().await?;
|
||||
let b = Html::parse_document(&r);
|
||||
let select_source = Selector::parse("source").unwrap();
|
||||
Ok(b.select(&select_source).next().unwrap().value().attr("src").unwrap_or("").to_owned())
|
||||
}
|
||||
async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
if let Some(id) = Self::id_extract(url) {
|
||||
let u = format!("{}/api/episode/serverPlayerAnimeWorld?id={}", self.url, id);
|
||||
match Command::new("curl")
|
||||
.arg("-L")
|
||||
.arg(u).output() {
|
||||
Ok(output) => {
|
||||
// log!("{}");
|
||||
if let Ok(html) = String::from_utf8(output.stdout) {
|
||||
let b = Html::parse_document(&html);
|
||||
let select_source = Selector::parse("source").unwrap();
|
||||
if let Some(c) = b.select(&select_source).next() {
|
||||
return Ok(c.attr("src").unwrap_or("").to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(String::new())
|
||||
|
||||
}
|
||||
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError> {
|
||||
// if page < 1 { page = 1};
|
||||
let u = format!("{}?keyword={}&page={}",self.get_url(), query, page);
|
||||
let r = reqwest::get(u).await?.error_for_status()?.text().await?;
|
||||
return Ok(r);
|
||||
}
|
||||
|
||||
async fn search(&self, query: String, page: usize) -> Result<Vec<crate::source::Episode>, AsyncError>{
|
||||
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, AsyncError> {
|
||||
let p = std::cmp::max(1, page);
|
||||
let u = format!("{}/updated?page={}",self.get_url(),p);
|
||||
let a = get(u).await?.error_for_status()?.text().await?;
|
||||
return Ok(a);
|
||||
}
|
||||
|
||||
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>{
|
||||
let mut hs = HashMap::<String, EpisodeListItem>::new();
|
||||
if let Ok(a) = get(url).await?.error_for_status()?.text().await {
|
||||
let b = Html::parse_document(&a);
|
||||
let select_episodes = Selector::parse("li.episode a").unwrap();
|
||||
for ep in b.select(&select_episodes) {
|
||||
let alink = ep.value();
|
||||
hs.insert(ep.inner_html(), EpisodeListItem {
|
||||
title: format!("Ep. {:>4}",ep.inner_html()),
|
||||
url: alink.attr("href").unwrap_or("").trim().to_owned()
|
||||
});
|
||||
}
|
||||
}
|
||||
return Ok(hs);
|
||||
}
|
||||
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError> {
|
||||
if let Some(url) = Self::id_extract(&eli.url) {
|
||||
if let Ok(vlc_url) =self.episode_play_url(url).await {
|
||||
if !vlc_url.is_empty() {
|
||||
let _ = Command::new("vlc").arg(vlc_url)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
// async fn fetch_html(url: String) -> Result<String, reqwest::Error> {
|
||||
// get(url).await?.error_for_status()?.text().await
|
||||
// }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,232 +0,0 @@
|
||||
use std::{collections::HashMap, f64::consts::E, fmt::format, io::Read, sync::Arc};
|
||||
use crc32fast::Hasher;
|
||||
use reqwest::get;
|
||||
use scraper::{ElementRef, Html, Selector};
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
use crate::{log, source::{Chapter, EpisodeListItem, MangaMetadata, MangaSource, SourceType, openManga}, utils::utils::{AsyncError, MangaCbz}};
|
||||
|
||||
pub struct Mangaworld {
|
||||
url: String,
|
||||
hostname:String,
|
||||
sourcetype: SourceType
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MangaSource for Mangaworld {
|
||||
fn get_hostname(&self)->String {
|
||||
self.hostname.clone()
|
||||
}
|
||||
fn get_url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
fn has_update_page(&self) -> bool {true}
|
||||
fn new(hostname: String, url: String) -> Self {
|
||||
Self {
|
||||
url,
|
||||
hostname,
|
||||
sourcetype: crate::source::SourceType::Manga
|
||||
}
|
||||
}
|
||||
async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Chapter>, AsyncError> {
|
||||
let body = self.fetch_updated_page(page).await?;
|
||||
self.extract_episode_from_updated(body)
|
||||
}
|
||||
//https://www.mangaworld.mx/archive?keyword=
|
||||
fn extract_episode_from_search(&self, body: String) -> Result<Vec<crate::source::Chapter>, AsyncError> {
|
||||
let document = Html::parse_document(&body);
|
||||
let sel_inner = Selector::parse("div.entry").unwrap();
|
||||
// let sel_episode_number = Selector::parse("a.xanh").unwrap();
|
||||
let sel_anime_name = Selector::parse("a.manga-title").unwrap();
|
||||
let cards = document.select(&sel_inner)
|
||||
.filter_map(|card| {
|
||||
// let carde = card.select(&sel_episode_number).next()?;
|
||||
// let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
|
||||
// log!("index: {}", index);
|
||||
let carda = card.select(&sel_anime_name).next()?;
|
||||
|
||||
// log!("Card a found");
|
||||
let url = carda.value().attr("href").unwrap_or("").trim().to_owned();
|
||||
// log!("Card url: {}",url);
|
||||
let title = carda.inner_html().trim().to_owned();
|
||||
// log!("Card title: {}", title);
|
||||
Some(Chapter {
|
||||
title,
|
||||
index: 0,
|
||||
url,
|
||||
section: 0
|
||||
})
|
||||
});
|
||||
Ok(cards.collect::<Vec<Chapter>>())
|
||||
}
|
||||
//https://www.mangaworld.ac/
|
||||
fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Chapter>, AsyncError> {
|
||||
let document = Html::parse_document(&body);
|
||||
let sel_inner = Selector::parse("div.entry").unwrap();
|
||||
let sel_episode_number = Selector::parse("a.xanh").unwrap();
|
||||
let sel_anime_name = Selector::parse("a.manga-title").unwrap();
|
||||
let cards = document.select(&sel_inner)
|
||||
.filter_map(|card| {
|
||||
let carde = card.select(&sel_episode_number).next()?;
|
||||
let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
|
||||
let url = carde.value().attr("href").unwrap_or("").trim().to_owned();
|
||||
// log!("index: {}", index);
|
||||
let carda = card.select(&sel_anime_name).next()?;
|
||||
|
||||
// log!("Card a found");
|
||||
// log!("{}\t{}\t{}\t{}");
|
||||
// log!("Card url: {}",url);
|
||||
let title = carda.inner_html().trim().to_owned();
|
||||
// log!("Card title: {}", title);
|
||||
Some(Chapter {
|
||||
title,
|
||||
index,
|
||||
url,
|
||||
section: 0
|
||||
})
|
||||
});
|
||||
Ok(cards.collect::<Vec<Chapter>>())
|
||||
}
|
||||
|
||||
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError> {
|
||||
let u = format!("{}?keyword={}&page={}",self.get_url(), query, page);
|
||||
let r = reqwest::get(u).await?.error_for_status()?.text().await?;
|
||||
return Ok(r);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError> {
|
||||
let p = std::cmp::max(1, page);
|
||||
let u = format!("{}?page={}",self.get_url(), p);
|
||||
let a = get(u).await?.error_for_status()?.text().await?;
|
||||
return Ok(a);
|
||||
}
|
||||
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError> {
|
||||
let mut hs = HashMap::<String, EpisodeListItem>::new();
|
||||
if let Ok(a) = get(url).await?.error_for_status()?.text().await {
|
||||
let b = Html::parse_document(&a);
|
||||
let select_volume_element = Selector::parse(".chapters-wrapper .volume-element").unwrap();
|
||||
let select_volume_name = Selector::parse(".volume .volume-name").unwrap();
|
||||
let select_episodes = Selector::parse("div.chapter a.chap").unwrap();
|
||||
let select_span = Selector::parse("span").unwrap();
|
||||
|
||||
for vol in b.select(&select_volume_element) {
|
||||
if let Some(volname) = vol.select(&select_volume_name).next() {
|
||||
let vv:Vec<&str> = volname.text().collect();
|
||||
let volume = vv.join(" ");
|
||||
for ep in vol.select(&select_episodes) {
|
||||
if let Some(span) = ep.select(&select_span).next() {
|
||||
let alink = ep.value();
|
||||
let chapter = span.inner_html();
|
||||
let format_part = |input: &str, prefix: &str| -> String {
|
||||
input.split_whitespace().nth(1)
|
||||
.map(|num_str| {
|
||||
match num_str.split_once('.') {
|
||||
Some((a, b)) if !b.is_empty() => {
|
||||
format!("{} {:>4}.{}", prefix, a, b)
|
||||
},
|
||||
Some((_, _)) | None => {
|
||||
let m = num_str.trim_end_matches('.');
|
||||
format!("{} {:>4}", prefix, m)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let title_vol = format_part(&volume, "Volume");
|
||||
let title_cha = format_part(&chapter, "Capitolo");
|
||||
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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(hs);
|
||||
}
|
||||
|
||||
|
||||
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 }
|
||||
// }
|
||||
}
|
||||
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
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::panic;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::backtrace::Backtrace;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($($arg:tt)*) => {{
|
||||
let file_result = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("./debug.log");
|
||||
|
||||
if let Ok(mut file) = file_result {
|
||||
use std::io::Write;
|
||||
let _ = writeln!(file, $($arg)*);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
#[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 {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_sort(res: &HashMap<String, String>) -> Vec<&String> {
|
||||
let mut items = res.keys().collect::<Vec<&String>>();
|
||||
items.sort_by(|a,b|{
|
||||
let aname = a.splitn(2, "di ").nth(1).unwrap_or("");
|
||||
let bname = b.splitn(2, "di ").nth(1).unwrap_or("");
|
||||
match aname.cmp(bname) {
|
||||
std::cmp::Ordering::Equal => a.cmp(b),
|
||||
o => o
|
||||
}
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
// 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