diff --git a/Cargo.lock b/Cargo.lock index 270b334..936f4c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,17 +6,32 @@ version = 4 name = "Aniyomi" version = "0.1.0" dependencies = [ + "aes", + "anyhow", "async-trait", + "base64", + "block-padding 0.3.3", + "boa_engine", + "cbc", + "cipher 0.5.1", "color-eyre", + "crc32fast", "curl", + "hex", + "image", + "once_cell", "openssl", "ratatui", + "regex", "reqwest", + "rquickjs", "scraper", "serde", "strum 0.28.0", "tokio", "toml", + "url", + "zip", ] [[package]] @@ -34,6 +49,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -43,6 +69,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -55,6 +99,38 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -87,6 +163,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-lc-rs" version = "1.16.1" @@ -145,6 +264,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -157,6 +282,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -166,6 +300,171 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "boa_ast" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6339a700715bda376f5ea65c76e8fe8fc880930d8b0638cea68e7f3da6538e0a" +dependencies = [ + "bitflags 2.11.0", + "boa_interner", + "boa_macros", + "boa_string", + "indexmap", + "num-bigint", + "rustc-hash", +] + +[[package]] +name = "boa_engine" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1521be326f8a5c8887e95d4ce7f002917a002a23f7b93b9a6a2bf50ed4157824" +dependencies = [ + "aligned-vec", + "arrayvec", + "bitflags 2.11.0", + "boa_ast", + "boa_gc", + "boa_interner", + "boa_macros", + "boa_parser", + "boa_string", + "bytemuck", + "cfg-if", + "cow-utils", + "dashmap", + "dynify", + "fast-float2", + "float16", + "futures-channel", + "futures-concurrency", + "futures-lite", + "hashbrown 0.16.1", + "icu_normalizer", + "indexmap", + "intrusive-collections", + "itertools", + "num-bigint", + "num-integer", + "num-traits", + "num_enum", + "paste", + "portable-atomic", + "rand 0.9.2", + "regress", + "rustc-hash", + "ryu-js", + "serde", + "serde_json", + "small_btree", + "static_assertions", + "tag_ptr", + "tap", + "thin-vec", + "thiserror 2.0.18", + "time", + "xsum", +] + +[[package]] +name = "boa_gc" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17323a98cf2e631afacf1a6d659c1212c48a68bacfa85afab0a66ade80582e51" +dependencies = [ + "boa_macros", + "boa_string", + "hashbrown 0.16.1", + "thin-vec", +] + +[[package]] +name = "boa_interner" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20510b8b02bcde9b0a01cf34c0c308c56156503d1d91cdab4c8cfbd292b747ea" +dependencies = [ + "boa_gc", + "boa_macros", + "hashbrown 0.16.1", + "indexmap", + "once_cell", + "phf 0.13.1", + "rustc-hash", + "static_assertions", +] + +[[package]] +name = "boa_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5822cb4f146d243060e588bc5a5f2e709683fdad3d7111f42c48e6b5c921d23d" +dependencies = [ + "cfg-if", + "cow-utils", + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "boa_parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd957fa9fa93e3a001a8aba5a5cd40c2bbfde486378be4c4b472fd304aaddb" +dependencies = [ + "bitflags 2.11.0", + "boa_ast", + "boa_interner", + "boa_macros", + "fast-float2", + "icu_properties", + "num-bigint", + "num-traits", + "regress", + "rustc-hash", +] + +[[package]] +name = "boa_string" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2da1d7f4a76fd9040788a122f0d807910800a7b86f5952e9244848c36511de" +dependencies = [ + "fast-float2", + "itoa", + "paste", + "rustc-hash", + "ryu-js", + "static_assertions", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.20.2" @@ -177,6 +476,26 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" @@ -184,6 +503,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + [[package]] name = "castaway" version = "0.2.4" @@ -193,6 +521,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "cc" version = "1.2.56" @@ -223,6 +560,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "crypto-common 0.2.1", + "inout 0.2.2", +] + [[package]] name = "cmake" version = "0.1.57" @@ -259,6 +616,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -283,6 +646,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.10.0" @@ -292,6 +661,35 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -318,6 +716,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cow-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -327,6 +731,40 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.29.0" @@ -354,6 +792,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -364,6 +808,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csscolorparser" version = "0.6.2" @@ -461,6 +914,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + [[package]] name = "deltae" version = "0.3.2" @@ -494,7 +967,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "syn 2.0.117", ] @@ -505,7 +978,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "crypto-common", + "crypto-common 0.1.7", + "subtle", ] [[package]] @@ -549,6 +1023,26 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dynify" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81acb15628a3e22358bf73de5e7e62360b8a777dbcb5fc9ac7dfa9ae73723747" +dependencies = [ + "dynify-macros", +] + +[[package]] +name = "dynify-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ego-tree" version = "0.10.0" @@ -570,6 +1064,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -595,6 +1109,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "eyre" version = "0.6.12" @@ -615,12 +1144,47 @@ dependencies = [ "regex", ] +[[package]] +name = "fast-float2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "filedescriptor" version = "0.8.3" @@ -650,6 +1214,33 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "float16" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bffafbd079d520191c7c2779ae9cf757601266cf4167d3f659ff09617ff8483" +dependencies = [ + "cfg-if", + "rustc_version 0.2.3", +] + [[package]] name = "fnv" version = "1.0.7" @@ -715,6 +1306,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset 0.5.7", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", ] [[package]] @@ -723,6 +1328,25 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -742,7 +1366,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -800,10 +1427,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "wasip2", "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -831,6 +1470,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -863,6 +1519,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.36.1" @@ -912,6 +1577,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -977,9 +1651,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", @@ -996,6 +1670,7 @@ checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", @@ -1003,43 +1678,48 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "8b24a59706036ba941c9476a55cd57b82b77f38a3c667d637ee7cabbc85eaedc" dependencies = [ + "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "f5a97b8ac6235e69506e8dacfb2adf38461d2ce6d3e9bd9c94c4cbc3cd4400a4" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", + "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -1049,6 +1729,8 @@ checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1089,6 +1771,46 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indenter" version = "0.3.4" @@ -1116,6 +1838,26 @@ dependencies = [ "rustversion", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding 0.3.3", + "generic-array", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding 0.4.2", + "hybrid-array", +] + [[package]] name = "instability" version = "0.3.11" @@ -1129,6 +1871,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "intrusive-collections" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +dependencies = [ + "memoffset", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1231,12 +1993,34 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libz-sys" version = "1.1.25" @@ -1291,6 +2075,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.16.3" @@ -1306,6 +2099,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rust2" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +dependencies = [ + "sha2", +] + [[package]] name = "mac" version = "0.1.1" @@ -1333,6 +2135,16 @@ dependencies = [ "web_atoms", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1373,6 +2185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1387,6 +2200,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -1406,6 +2229,15 @@ dependencies = [ "memoffset", ] +[[package]] +name = "no_std_io2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -1416,6 +2248,32 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -1433,6 +2291,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1442,6 +2320,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1462,9 +2362,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl" @@ -1541,6 +2441,12 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1564,6 +2470,28 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1718,6 +2646,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1736,6 +2684,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1757,6 +2718,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1782,6 +2749,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1791,6 +2767,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quinn" version = "0.11.9" @@ -1997,6 +3029,76 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2035,6 +3137,25 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "regress" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" +dependencies = [ + "hashbrown 0.16.1", + "memchr", +] + +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" +dependencies = [ + "serde", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -2043,8 +3164,12 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64", "bytes", + "cookie", + "cookie_store", "encoding_rs", + "futures-channel", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -2073,6 +3198,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "ring" version = "0.17.14" @@ -2087,6 +3218,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rquickjs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50dc6d6c587c339edb4769cf705867497a2baf0eca8b4645fa6ecd22f02c77a" +dependencies = [ + "rquickjs-core", +] + +[[package]] +name = "rquickjs-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bf7840285c321c3ab20e752a9afb95548c75cd7f4632a0627cea3507e310c1" +dependencies = [ + "hashbrown 0.16.1", + "relative-path", + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27344601ef27460e82d6a4e1ecb9e7e99f518122095f3c51296da8e9be2b9d83" +dependencies = [ + "cc", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -2099,13 +3259,22 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.27", ] [[package]] @@ -2208,6 +3377,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "ryu-js" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" + [[package]] name = "same-file" version = "1.0.6" @@ -2289,12 +3464,27 @@ dependencies = [ "smallvec", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.228" @@ -2356,6 +3546,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2413,6 +3614,21 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "1.0.2" @@ -2425,6 +3641,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "small_btree" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba60d2df92ba73864714808ca68c059734853e6ab722b40e1cf543ebb3a057a" +dependencies = [ + "arrayvec", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -2594,6 +3819,18 @@ dependencies = [ "libc", ] +[[package]] +name = "tag_ptr" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tendril" version = "0.4.3" @@ -2612,7 +3849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" dependencies = [ "fnv", - "nom", + "nom 7.1.3", "phf 0.11.3", "phf_codegen 0.11.3", ] @@ -2638,7 +3875,7 @@ dependencies = [ "fancy-regex", "filedescriptor", "finl_unicode", - "fixedbitset", + "fixedbitset 0.4.2", "hex", "lazy_static", "libc", @@ -2668,6 +3905,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "thin-vec" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6" + [[package]] name = "thiserror" version = "1.0.69" @@ -2717,6 +3960,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -2724,12 +3981,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", + "js-sys", "libc", "num-conv", "num_threads", "powerfmt", "serde_core", "time-core", + "time-macros", ] [[package]] @@ -2738,6 +3998,16 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2745,6 +4015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -2838,6 +4109,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_parser" version = "1.0.9+spec-1.1.0" @@ -2945,6 +4228,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.19.0" @@ -3016,6 +4305,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3040,6 +4335,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3200,7 +4506,7 @@ dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", - "semver", + "semver 1.0.27", ] [[package]] @@ -3244,6 +4550,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "wezterm-bidi" version = "0.2.3" @@ -3618,6 +4930,9 @@ name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" @@ -3699,7 +5014,7 @@ dependencies = [ "id-arena", "indexmap", "log", - "semver", + "semver 1.0.27", "serde", "serde_derive", "serde_json", @@ -3707,12 +5022,30 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xsum" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.1" @@ -3800,6 +5133,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -3816,8 +5150,105 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2726508a48f38dceb22b35ecbbd2430efe34ff05c62bd3285f965d7911b33464" +dependencies = [ + "aes", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.4.2", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "typed-path", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index ec374d2..8cb408c 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,31 @@ 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"] } +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"] } \ No newline at end of file +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 = [] \ No newline at end of file diff --git a/config.toml b/config.toml index f25c901..255861c 100644 --- a/config.toml +++ b/config.toml @@ -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] -basedir = "." # !! Should be an Absolute Path -extensions = "./extensions" -anime = "./anime" -manga = "./manga" +[General] +askToQuit = false -[Extension] -defaultanime = "animeworld" # USELESS -defaultmanga = "mangaworld" # USELESS + +## Cmd +[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" diff --git a/debug.log b/debug.log new file mode 100644 index 0000000..e69de29 diff --git a/downloads/manga/atelier of the witch hat/batch.cbz b/downloads/manga/atelier of the witch hat/batch.cbz new file mode 100644 index 0000000..3c4699c Binary files /dev/null and b/downloads/manga/atelier of the witch hat/batch.cbz differ diff --git a/extensions/manga/mangago.toml b/extensions/manga/mangago.toml new file mode 100644 index 0000000..1762cc0 --- /dev/null +++ b/extensions/manga/mangago.toml @@ -0,0 +1,10 @@ +Type = "Manga" + +[Info] +Name = "Mangago" +Description = "" +Language = "EN/en" + +[Sitemanager] +Hostname = "mangago.me" +Url = "https://www.mangag.me" diff --git a/library/mangalibrary.toml b/library/mangalibrary.toml index 3968506..94b8a2c 100644 --- a/library/mangalibrary.toml +++ b/library/mangalibrary.toml @@ -10,4 +10,8 @@ episodes="https://www.mangaworld.mx/manga/1637/chainsaw-man" [[Mangago]] name="Medalist" -episodes="https://www.mangago.me/read-manga/medalist/" \ No newline at end of file +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/" \ No newline at end of file diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/components.rs b/src/components.rs index 31d7dad..4dd44e9 100644 --- a/src/components.rs +++ b/src/components.rs @@ -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 { 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 render: Box, @@ -26,7 +33,7 @@ pub mod list { pub action_type: ActionType, pub children_list: Vec, pub children_hash: Option>, - pub widget: SLWidget, + pub widget: SLWidget } impl ListDiveChild { pub fn new(title: String, index:usize, action_type:ActionType, children: Option>, widget:SLWidget) -> Self { @@ -153,14 +160,16 @@ pub mod list { pub fn render_list(&mut self, frame: &mut Frame, area: Rect) { let l = List::new(self.get_current_item_list().clone()) .highlight_style(Style::new() - .fg(tailwind::AMBER.c900)) + .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_spacing(ratatui::widgets::HighlightSpacing::Always) .block( Block::new() .borders(Borders::ALL) .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); @@ -214,9 +223,9 @@ pub mod list { pub mod AsyncUpdateAnimeList { use std::{collections::HashMap, sync::Arc}; - use ratatui::{buffer::Buffer, layout::Rect, style::{Style, palette::tailwind}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}}; + use 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 { Empty, @@ -245,6 +254,13 @@ pub mod AsyncUpdateAnimeList { 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 { trasmitter_to_app: tokio::sync::mpsc::UnboundedSender, @@ -288,7 +304,7 @@ pub mod AsyncUpdateAnimeList { Paragraph::new("Starting the request").block(block).render(area, buf); } AsyncAnimeStateStatus::Loading => { - Paragraph::new("Loading").block(block.border_style(Style::new().fg(tailwind::AMBER.c200))).render(area, buf); + Paragraph::new("Loading").block(block.border_style(Style::new().fg(getConfig().Theme.status_loading.to_ratatui()))).render(area, buf); } AsyncAnimeStateStatus::Error(err) => { @@ -313,11 +329,11 @@ pub mod AsyncUpdateAnimeList { } 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 trasmitter_to_app:tokio::sync::mpsc::UnboundedSender>, pub library_type: LibraryType @@ -331,11 +347,28 @@ pub mod Library { } pub fn sorted_keys_from_hash(hs: &HashMap) -> Vec<&String> { let mut items: Vec<&String> = hs.keys().collect::>(); - items.sort_by(|a: &&String,b: &&String| { - if let Ok(ai) = a.parse::() && let Ok(bi) = b.parse::() { - return bi.cmp(&ai); + // log!("{:#?}",items); + items.sort_by(|ad: &&String,bd: &&String| { + 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::(), b.parse::()) { + (Ok(uai), Ok(ubi)) => match ubi.cmp(&uai) { + Ordering::Equal => match (adec.parse::(), bdec.parse::()) { + (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 items } @@ -448,9 +481,9 @@ pub mod Library { pub mod AsyncUpdateMangaList { use std::{collections::HashMap, sync::Arc}; - use ratatui::{buffer::Buffer, layout::Rect, style::{Style, palette::tailwind}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget}}; + use 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 { Empty, @@ -479,6 +512,13 @@ pub mod AsyncUpdateMangaList { 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 { trasmitter_to_app: tokio::sync::mpsc::UnboundedSender, @@ -490,7 +530,7 @@ pub mod AsyncUpdateMangaList { } } 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 { @@ -503,6 +543,7 @@ pub mod AsyncUpdateMangaList { state.status = AsyncMangaStateStatus::Loading; let tx = self.trasmitter_to_app.clone(); let source = state.mangasource.clone(); + log!("{}", source.get_url()); let h = source.get_hostname(); let p = state.page.clone(); tokio::spawn(async move { @@ -522,7 +563,7 @@ pub mod AsyncUpdateMangaList { Paragraph::new("Starting the request").block(block).render(area, buf); } AsyncMangaStateStatus::Loading => { - Paragraph::new("Loading").block(block.border_style(Style::new().fg(tailwind::AMBER.c200))).render(area, buf); + Paragraph::new("Loading").block(block.border_style(Style::new().fg(getConfig().Theme.status_loading.to_ratatui()))).render(area, buf); } AsyncMangaStateStatus::Error(err) => { @@ -542,6 +583,750 @@ 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
+ } + #[derive(Clone)] + pub struct Section { + pub label: String, + pub style: Style, + pub items: Vec>> + } + 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) -> 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>> + } + 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=1 { + self.selected_item = self.selected_item-1; + } + } + pub fn select_sec_next(&mut self) { + if self.selected_section+1=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
) -> 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 = Box S>; + type SetOption = Box bool>; + + pub enum ItemOption { + TextInput { + state: TextInputState + }, + TextInputOption { + get: GetOption, + set: SetOption, + state: TextInputState + }, + SwitchOption { + get: GetOption, + set: SetOption, + state: SwitchOptionState + }, + SelectOption { + get: GetOption, + get_options: GetOption>, + set: SetOption, + state: SelectOptionState + }, + ColorPicker { + get: GetOption, // 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, + state: ColorPickerOptionState + }, + DebugOption {} + } + impl ItemOption { + pub fn new_text_input(get: GetOption, set: SetOption) -> 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, get_options: GetOption>, set: SetOption) -> 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, set: SetOption) -> 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, + selected_value: String, + selected_index: usize + } + impl SelectOptionState { + fn new(values: Vec, 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, + pub style: Option +} +pub struct AskToQuitState { + pub sel_index: usize, + pub items: Vec +} +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, + // pub trasmitter_to_app_input:tokio::sync::mpsc::UnboundedSender, + pub library_type: SearchType + } + impl Search { + pub fn new(library_type: SearchType,trasmitter_to_app: tokio::sync::mpsc::UnboundedSender) -> Self { + Self { + library_type, + trasmitter_to_app + } + } + pub fn sorted_keys_from_hash(hs: &HashMap) -> Vec<&String> { + let mut items: Vec<&String> = hs.keys().collect::>(); + items.sort_by(|a: &&String,b: &&String| { + if let (Ok(ai),Ok(bi)) = (a.parse::(), b.parse::()) { + 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>, + pub status: SearchStateStatus, + pub entry_selected: bool, + pub episodes_loaded: Option>, + pub current_source: Option, + 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::>()).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); + } + } + } + + + } } } \ No newline at end of file diff --git a/src/config.md b/src/config.md deleted file mode 100644 index c39bca4..0000000 --- a/src/config.md +++ /dev/null @@ -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 = 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 -} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..69704e0 --- /dev/null +++ b/src/config.rs @@ -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 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> = 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 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, +} \ No newline at end of file diff --git a/src/extension.rs b/src/extension.rs index d553043..20d148b 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -3,6 +3,8 @@ 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; @@ -49,19 +51,20 @@ impl Extension { pub fn toMangaSource(self) -> Box { 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, String> { - let def = "animeworld"; + 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, String> { - let def = "mangaworld"; - if let Ok(ext) = Extension::buildfromName(SourceType::Anime, def) { + 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)); diff --git a/src/main.rs b/src/main.rs index d2d3476..2c0d509 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,27 @@ // mod config; // mod mangatab; mod extension; +mod settings; mod library; +use boa_engine::object::Ref; +use config::{getConfig, load_config}; +mod config; mod components; mod source; mod utils; mod folders; -use std::{any::Any, cell::RefCell, cmp, collections::HashMap, process::{Command, Stdio}, rc::Rc, sync::Arc, time::Duration, vec}; +use std::{cell::RefCell, collections::HashMap, process::{Command, Stdio}, rc::Rc, sync::Arc, time::Duration, vec}; use color_eyre::{Result}; use ratatui::{ - DefaultTerminal, Frame, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Direction, Layout, Rect}, style::Style, text::{Line, Span}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph} + DefaultTerminal, Frame, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Direction, Layout, Rect}, style::{Style, palette::tailwind}, text::{Line, Span, Text}, widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, StatefulWidget} }; -use crate::{components::{AsyncUpdateAnimeList::{self, AsyncAnimeState, AsyncAnimeStateStatus}, AsyncUpdateMangaList::{AsyncMangaState, AsyncMangaStateStatus}, Library::{self, LibraryState, LibraryStateStatus}, list::{ActionType, CurrentWidget, ListDiveChild, SelectableList}}, extension::Extension, source::{AVAIBLEANIMESOURCE, AnimeSource, EpisodeListItem, MangaSource, SourceType, animeworld, mangaworld}}; +use crate::{components::{AskToQuit, AskToQuitState, AsyncUpdateAnimeList::{self, AsyncAnimeState, AsyncAnimeStateStatus}, AsyncUpdateMangaList::{self, AsyncMangaState, AsyncMangaStateStatus}, Library::{self, LibraryState, LibraryStateStatus}, ListFuncOption, Search::{self, SearchState, SearchStateStatus}, list::{ActionType, CurrentWidget, ListDiveChild, SelectableList}}, extension::Extension, settings::{ApplySettingsChange, Settings, SettingsState}, source::{AnimeSource, EpisodeListItem, MangaMetadata, MangaSource, SourceType, animeworld, mangaworld, openManga}}; //use crate::{mangatab::MangaTab, config::{getConfig, load_config, tailwindPaletteFromString}}; #[tokio::main] async fn main() -> Result<()> { - //load_config(); + load_config(); + utils::utils::setup_panic_hook(); folders::init_folders(); color_eyre::install()?; let terminal = ratatui::init(); @@ -32,41 +37,71 @@ struct App { // pub anime_list: components::asyncListWidget::AsyncListWidget, anime: AnimeApp, manga: MangaApp, - library: LibraryApp + settings: SettingApp, + askToQuit: AskToQuitApp } struct AnimeApp { anime_source: Arc>, tx: tokio::sync::mpsc::UnboundedSender, rx: tokio::sync::mpsc::UnboundedReceiver, - anime_list_state: Rc> + anime_list_state: Rc>, + library: LibraryApp, + search: SearchApp } struct MangaApp { manga_source: Arc>, tx: tokio::sync::mpsc::UnboundedSender, rx: tokio::sync::mpsc::UnboundedReceiver, - manga_list_state: Rc> + manga_list_state: Rc>, + library: LibraryApp, + search: SearchApp } +struct SettingApp { + // tx: tokio::sync::mpsc::UnboundedSender, + rx: tokio::sync::mpsc::UnboundedReceiver, + state: Rc> +} + + +struct AskToQuitApp { + state: Rc>, +} + struct LibraryApp { tx: tokio::sync::mpsc::UnboundedSender>, rx: tokio::sync::mpsc::UnboundedReceiver>, library_list_state: Rc> } +struct SearchApp { + tx: tokio::sync::mpsc::UnboundedSender, + rx: tokio::sync::mpsc::UnboundedReceiver, + search_list_state: Rc> +} #[derive(Clone, Copy, PartialEq, Eq)] enum AppState { Running, - Quitting, + Settings, + AskToQuit, + Quitting } impl App { fn new()->App { - let (tx_a, rx_a) = tokio::sync::mpsc::unbounded_channel::(); - let (tx_m, rx_m) = tokio::sync::mpsc::unbounded_channel::(); - let (tx_l, rx_l) = tokio::sync::mpsc::unbounded_channel::>(); - let anime_source:Arc> = Arc::new(Extension::defaultAnimeSource().unwrap_or(Box::new(animeworld::Animeworld::new("animeworld.ac".to_owned(), "https://animeworld.ac".to_owned())))); // TODO Make anime source selectable - let manga_source:Arc> = Arc::new(Extension::defaultMangaeSource().unwrap_or(Box::new(mangaworld::Mangaworld::new("mangaworld.mx".to_owned(), "https://mangaworld.mx".to_owned())))); // TODO Make anime source selectable + let (tx_a, rx_a) = tokio::sync::mpsc::unbounded_channel::(); + let (tx_m, rx_m) = tokio::sync::mpsc::unbounded_channel::(); + let (tx_s, rx_s) = tokio::sync::mpsc::unbounded_channel::(); + let (tx_la, rx_la) = tokio::sync::mpsc::unbounded_channel::>(); + let (tx_las, rx_las) = tokio::sync::mpsc::unbounded_channel::(); + let (tx_lm, rx_lm) = tokio::sync::mpsc::unbounded_channel::>(); + let (tx_lms, rx_lms) = tokio::sync::mpsc::unbounded_channel::(); + let anime_source:Arc> = Arc::new(Extension::defaultAnimeSource().unwrap_or(Box::new(animeworld::Animeworld::new("animeworld.ac".to_owned(), "https://animeworld.ac".to_owned())))); // TODO Make anime source selectable + let manga_source:Arc> = Arc::new(Extension::defaultMangaeSource().unwrap_or(Box::new(mangaworld::Mangaworld::new("mangaworld.mx".to_owned(), "https://mangaworld.mx".to_owned())))); // TODO Make anime source selectable let a_s = anime_source.clone(); let m_s = manga_source.clone(); + // Asktoquit + let quit_tx_s = tx_s.clone(); + let stay_tx_s = tx_s.clone(); Self { state: AppState::Running, navigation_menu: SelectableList::default(), @@ -74,19 +109,52 @@ impl App { anime_source, anime_list_state: Rc::new(RefCell::new(AsyncAnimeState::new(a_s))), tx: tx_a, - rx: rx_a + rx: rx_a, + library: LibraryApp { + tx: tx_la, rx: rx_la, library_list_state: Rc::new(RefCell::new(LibraryState::new())) + }, + search: SearchApp { tx: tx_las, rx:rx_las, search_list_state: Rc::new(RefCell::new(SearchState::new())) } }, manga: MangaApp { manga_source, tx: tx_m, rx: rx_m, - manga_list_state: Rc::new(RefCell::new(AsyncMangaState::new(m_s))) + manga_list_state: Rc::new(RefCell::new(AsyncMangaState::new(m_s))), + library: LibraryApp { + tx: tx_lm, + rx: rx_lm, + library_list_state: Rc::new(RefCell::new(LibraryState::new())) + }, + search: SearchApp { tx: tx_lms, rx:rx_lms, search_list_state: Rc::new(RefCell::new(SearchState::new())) } }, - library: LibraryApp { - tx: tx_l, - rx: rx_l, - library_list_state: Rc::new(RefCell::new(LibraryState::new())) - } + settings: SettingApp { + state: Rc::new(RefCell::new(SettingsState::new(tx_s))), + rx: rx_s + }, + askToQuit: AskToQuitApp { state: Rc::new(RefCell::new(AskToQuitState { + sel_index: 0, + items: vec![ + ListFuncOption { + name: "Quit".to_owned(), + style: Some(ratatui::style::Style::default().bg(tailwind::RED.c300).white()), // White with c700 + func: Box::new(move || { + quit_tx_s.send(ApplySettingsChange::setAppStatus(AppState::Quitting)); + }) + }, + ListFuncOption { + name: "Stay".to_owned(), + style: Some(ratatui::style::Style::default().bg(tailwind::GREEN.c300).white()), // White with c700 + func: Box::new(move || { + stay_tx_s.send(ApplySettingsChange::setAppStatus(AppState::Running)); + }) + } + ] + }))} + // library: LibraryApp { + // tx: tx_l, + // rx: rx_l, + // library_list_state: Rc::new(RefCell::new(LibraryState::new())) + // } // anime_list: AsyncListWidget { // title: "Animeworld".into(), // state: AsyncState::Empty, @@ -95,16 +163,19 @@ impl App { // }, } } - fn withAnimeSource(&mut self, a: &str) { + fn with_anime_source(&mut self, a: &str) { if let Ok(nas) = extension::Extension::buildfromName(source::SourceType::Anime, a) { - let anime_s = nas.toAnimeSource(); - self.anime.anime_list_state=Rc::new(RefCell::new(AsyncAnimeState::new(Arc::new(anime_s)))) + let anime_s = Arc::new(nas.toAnimeSource()); + self.anime.anime_source=anime_s.clone(); + self.anime.anime_list_state.borrow_mut().resetFrom(AsyncAnimeState::new(anime_s)); } - - } - fn withMangaSource(&mut self, m: Arc>) { - + fn with_manga_source(&mut self, m: &str) { + if let Ok(nas) = extension::Extension::buildfromName(source::SourceType::Manga, m) { + let manga_s = Arc::new(nas.toMangaSource()); + self.manga.manga_source=manga_s.clone(); + self.manga.manga_list_state.borrow_mut().resetFrom(AsyncMangaState::new(manga_s)); + } } fn init(&mut self) { // let tx_clone = self.tx.clone(); @@ -126,33 +197,39 @@ impl App { 1, ActionType::Render, None, - Some(self.block_library(Library::LibraryType::Anime))//Some(self.new_anime_list()), - + Some(self.block_library_anime()),//Some(self.new_anime_list()), )) ])), - None, + None )), ("Manga".to_owned(), ListDiveChild::new( "Manga".to_owned(), 1, ActionType::SubMenu, Some(HashMap::from([ - ("Library".to_owned(), ListDiveChild::new( - "Library".to_owned(), + ("Nuovi".to_owned(), + ListDiveChild::new("Nuovi".to_owned(), 0, ActionType::Render, None, - Some(self.block_library(Library::LibraryType::Manga)) + Some(self.block_manga_updated()) )), - ("Search".to_owned(), ListDiveChild::new( - "Search".to_owned(), + ("Library".to_owned(), ListDiveChild::new( + "Library".to_owned(), 1, ActionType::Render, None, - None, - )) + Some(self.block_library_manga()) + )), + // ("Search".to_owned(), ListDiveChild::new( + // "Search".to_owned(), + // 2, + // ActionType::Render, + // None, + // Some(self.searchManga()) + // )) ])), - None, + None )), // ("Settings".to_owned(), ListDiveChild::new( // "Settings".to_owned(), 2, ActionType::SubMenu, Some(HashMap::from([ @@ -174,23 +251,48 @@ impl App { }, Err(_) => {} }; + match self.anime.library.rx.try_recv() { + Ok(h)=>{ + self.anime.library.library_list_state.borrow_mut().episodes_loaded = Some(h); + self.anime.library.library_list_state.borrow_mut().entry_selected = true; + } + Err(_) => {} + }; match self.manga.rx.try_recv() { Ok(a) => { self.manga.manga_list_state.borrow_mut().status=a; }, Err(_) => {} }; - match self.library.rx.try_recv() { + match self.manga.library.rx.try_recv() { Ok(h)=>{ - self.library.library_list_state.borrow_mut().episodes_loaded = Some(h); - self.library.library_list_state.borrow_mut().entry_selected = true; + self.manga.library.library_list_state.borrow_mut().episodes_loaded = Some(h); + self.manga.library.library_list_state.borrow_mut().entry_selected = true; } Err(_) => {} } + match self.settings.rx.try_recv() { + Ok(a) => { + match a { + ApplySettingsChange::setAnime(a) => { + self.with_anime_source(&a); + }, + ApplySettingsChange::setManga(m) => { + self.with_manga_source(&m); + } + ApplySettingsChange::setAppStatus(app_state) => { + self.state = app_state + }, + + _=> {} + } + }, + Err(_) => {}, + } } async fn run(&mut self, mut terminal: DefaultTerminal) -> Result<()> { self.init(); - while self.state == AppState::Running { + while self.state != AppState::Quitting { self.update(); // log!("Draw"); terminal.draw(|frame| self.draw(frame))?; @@ -201,57 +303,158 @@ impl App { fn handle_events(&mut self) -> Result<()> { if event::poll(Duration::from_millis(500))? { - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - match key.code { - k @ (KeyCode::Esc | KeyCode::Backspace) => { - if !self.navigation_menu.handle_input(k) { - if !self.navigation_menu.goback() { - self.quit() + match &self.state { + AppState::Running => { + 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.state = AppState::Quitting;return Ok(())}, + KeyCode::Char('s') => {self.state = AppState::Settings; return Ok(())}, + _=>{} } } - }, - // KeyCode::Backspace => {self.navigation_menu.goback();}, - k @ (KeyCode::Up | KeyCode::Char('w'))=> { - if self.navigation_menu.lastpath.is_empty() { - self.navigation_menu.state.select_previous(); - } else { - self.navigation_menu.handle_input(k); - } - }, - k @ (KeyCode::Down | KeyCode::Char('s'))=> { - if self.navigation_menu.lastpath.is_empty() { - self.navigation_menu.state.select_next(); - } else { - self.navigation_menu.handle_input(k); - } - }, - KeyCode::Enter => { - if self.navigation_menu.lastpath.is_empty() { - self.navigation_menu.enter(); - } else { - self.navigation_menu.handle_input(KeyCode::Enter); - } - }, - a => { - if !self.navigation_menu.lastpath.is_empty() { - self.navigation_menu.handle_input(a); + match key.code { + k @ (KeyCode::Esc | KeyCode::Backspace) => { + if !self.navigation_menu.handle_input(k) { + if !self.navigation_menu.goback() { + self.quit() + } else { + self.anime.anime_list_state.borrow_mut().status = AsyncAnimeStateStatus::Empty; + self.manga.manga_list_state.borrow_mut().status = AsyncMangaStateStatus::Empty; + } + } + }, + // KeyCode::Backspace => {self.navigation_menu.goback();}, + k @ (KeyCode::Up | KeyCode::Char('w'))=> { + if self.navigation_menu.lastpath.is_empty() { + self.navigation_menu.state.select_previous(); + } else { + self.navigation_menu.handle_input(k); + } + }, + k @ (KeyCode::Down | KeyCode::Char('s'))=> { + if self.navigation_menu.lastpath.is_empty() { + self.navigation_menu.state.select_next(); + } else { + self.navigation_menu.handle_input(k); + } + }, + KeyCode::Enter => { + if self.navigation_menu.lastpath.is_empty() { + self.navigation_menu.enter(); + } else { + self.navigation_menu.handle_input(KeyCode::Enter); + } + }, + a => { + if !self.navigation_menu.lastpath.is_empty() { + self.navigation_menu.handle_input(a); + } + } } } } - if key.modifiers.contains(KeyModifiers::CONTROL) { - match key.code { - KeyCode::Char('c') => self.quit(), - _=>{} + }, + AppState::Settings => { + 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();return Ok(())}, + _=>{} + } + } + let mut stg_state = self.settings.state.borrow_mut(); + + + if stg_state.is_editing() { + if key.modifiers.contains(KeyModifiers::CONTROL) { + match key.code { + + k @ (KeyCode::Backspace | KeyCode::Char(_) | KeyCode::Right | KeyCode::Left | KeyCode::Up | KeyCode::Down) => {stg_state.handle_control_inputs(k);return Ok(())}, + _=>{} + } + } + match key.code { + k @ (KeyCode::Esc ) => { + self.state = AppState::Settings; + // ? DO NOT SAVE + stg_state.turn_editing_mode(false); + }, + k @ (KeyCode::Backspace | KeyCode::Char(_) | KeyCode::Enter | KeyCode::Right | KeyCode::Left | KeyCode::Up | KeyCode::Down | KeyCode::Insert | KeyCode::Delete) => { + stg_state.handle_inputs(k); + } + _ => {} + } + } else { + if key.modifiers.contains(KeyModifiers::SHIFT) { + match key.code { + k @ (KeyCode::Up | KeyCode::Char('w')) => { + stg_state.select_sec_prev(); + return Ok(()); + }, + k @ (KeyCode::Down | KeyCode::Char('s')) => { + stg_state.select_sec_next(); + return Ok(()); + } + _=>{} + } + } + match key.code { + k @ (KeyCode::Esc | KeyCode::Backspace) => { + self.state = AppState::Running + }, + KeyCode::Enter => { + stg_state.turn_editing_mode(true) + } + k @ (KeyCode::Up | KeyCode::Char('w')) => { + stg_state.selectprev(); + }, + k @ (KeyCode::Down | KeyCode::Char('s')) => { + stg_state.selectnext(); + } + _ => {} + } + } } } - } + }, + AppState::Quitting => {} + AppState::AskToQuit => { + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + let mut ask_to_quit_state = self.askToQuit.state.borrow_mut(); + match key.code { + k @ (KeyCode::Esc | KeyCode::Backspace) => { + self.state = AppState::Running + }, + KeyCode::Enter => { + if let Some(s) = ask_to_quit_state.get_selected_item() { + (s.func)(); + } + } + k @ (KeyCode::Left | KeyCode::Char('a')) => { + ask_to_quit_state.select_previous(); + }, + k @ (KeyCode::Right | KeyCode::Char('d')) => { + ask_to_quit_state.select_next(); + } + _ => {} + } + } + } + }, } } Ok(()) } pub fn quit(&mut self) { - self.state = AppState::Quitting; + if (getConfig().General.askToQuit) { + self.state = AppState::AskToQuit; + } else { + self.state = AppState::Quitting; + } } fn draw(&mut self, frame: &mut Frame) { @@ -266,6 +469,46 @@ impl App { self.navigation_menu.render_list(frame, lay[0]); // self.navigation_menu(frame, lay[1]); self.navigation_menu.render_menu(frame, lay[1]); + + if self.state == AppState::Settings { + let popup_block = Block::bordered() + .border_type(BorderType::Rounded) + .border_style(Style::new().fg(tailwind::INDIGO.c200)) + .title("Settings"); + let centered_area = lay[1].centered(Constraint::Percentage(60), Constraint::Percentage(20)); + let innarea = popup_block.inner(centered_area); + // clears out any background in the area before rendering the popup + frame.render_widget(Clear, centered_area); + + let mut stg = Settings::new(); + let mut state = self.settings.state.borrow_mut(); + + frame.render_widget(popup_block, centered_area); + + frame.render_stateful_widget(stg, innarea, &mut *state); + } else if self.state == AppState::AskToQuit { + let popup_block = Block::bordered() + .border_type(BorderType::Rounded) + .border_style(Style::new().fg(tailwind::RED.c800)) + .title("Quit?"); + let centered_area = lay[1].centered(Constraint::Percentage(60), Constraint::Percentage(10)); + let innarea = popup_block.inner(centered_area); + // clears out any background in the area before rendering the popup + frame.render_widget(Clear, centered_area); + + let mut atq = AskToQuit {}; + let mut state = self.askToQuit.state.borrow_mut(); + + frame.render_widget(popup_block, centered_area); + + frame.render_stateful_widget(atq, innarea, &mut *state); + } + } +} + +impl Default for App { + fn default() -> Self { + Self::new() } } @@ -395,16 +638,22 @@ impl App { true }, KeyCode::Left | KeyCode::Char('a') => { - state.list_state.select(None); - state.page = std::cmp::max(state.page - 1, 1); - state.status = AsyncAnimeStateStatus::Empty; - true + if state.animesource.has_update_page() { + state.list_state.select(None); + state.page = std::cmp::max(state.page - 1, 1); + state.status = AsyncAnimeStateStatus::Empty; + return true; + } + false }, KeyCode::Right | KeyCode::Char('d') => { - state.list_state.select(None); - state.page = state.page + 1; - state.status = AsyncAnimeStateStatus::Empty; - true + if state.animesource.has_update_page() { + state.list_state.select(None); + state.page = state.page + 1; + state.status = AsyncAnimeStateStatus::Empty; + return true; + } + false }, KeyCode::Enter => { if let Some(u) = state.list_state.selected() && let Some(res) = state.res.clone() { @@ -445,14 +694,109 @@ impl App { }) } } + pub fn block_manga_updated(&mut self) -> CurrentWidget { + // let ls =.borrow_mut(); // + let render_ls = Rc::clone(&self.manga.manga_list_state); + let input_ls = Rc::clone(&self.manga.manga_list_state); + let tx = self.manga.tx.clone(); + // let sc = self.manga_source.clone(); + CurrentWidget { + render: Box::new(move |frame: &mut Frame, area: Rect| { + let b = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::new().fg(ratatui::style::Color::LightCyan)); + let lay = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Percentage(100), + Constraint::Min(3) + ]).split(area); + let mut state = render_ls.borrow_mut(); + let tx_clone = tx.clone(); - pub fn block_library(&mut self, lib_type: Library::LibraryType) -> CurrentWidget { + let l = AsyncUpdateMangaList::AsyncUpdateMangaList::new(tx_clone); + + frame.render_stateful_widget( l, lay[0], &mut *state); + let cmd_list = Self::generate_cmd_list(vec![ + (CmdListType::Multi, "W,A,S,D or Arrows","to move"), + (CmdListType::Single, "Enter","select"), + (CmdListType::Single, "Esc","Go Back"), + ]); //// NOT WORK with match: Keybinds configuration from file; + frame.render_widget( cmd_list, lay[1]); + + }), + handle_input: Box::new(move |k: KeyCode| { + let mut state = input_ls.borrow_mut(); + match state.status { + AsyncMangaStateStatus::Success(_) => match k { + KeyCode::Up | KeyCode::Char('w') => { + state.list_state.select_previous(); + true + }, + KeyCode::Down | KeyCode::Char('s')=> { + state.list_state.select_next(); + true + }, + KeyCode::Left | KeyCode::Char('a') => { + if state.mangasource.has_update_page() { + state.list_state.select(None); + state.page = std::cmp::max(state.page - 1, 1); + state.status = AsyncMangaStateStatus::Empty; + return true; + } + false + }, + KeyCode::Right | KeyCode::Char('d') => { + if state.mangasource.has_update_page() { + state.list_state.select(None); + state.page = state.page + 1; + state.status = AsyncMangaStateStatus::Empty; + return true; + } + false + }, + KeyCode::Enter => { + if let Some(u) = state.list_state.selected() && let Some(res) = state.res.clone() { + let items = utils::utils::hash_sort(&res); + if let Some(item) = items.get(u) { + if let Some(url) = res.get(*item) { + //source.episode_play_url(url); + let so: Arc> = state.mangasource.clone(); + let u = url.clone(); + let title = item.split_once("di"); + source::openManga(so, MangaMetadata { + title: title.map(|x| x.1.trim().to_owned()), url: url.clone(), chapter: title.map(|x| x.0.trim().trim_start_matches("Capitolo").trim().to_owned()), volume: None + }); + } + } + } + true + } + _ => {false} + }, + _=>{false} + } + // match k { + // KeyCode::Up => { + // state.list_state.select_previous(); + // }, + // KeyCode::Down => { + // state.list_state.select_next(); + // } + // _ => {} + // } + }) + } + } + pub fn block_library_anime(&mut self) -> CurrentWidget { // let render_ls = Rc::clone(list_state); // let input_ls = Rc::clone(list_state); - let render_ls = Rc::clone(&self.library.library_list_state); - let input_ls = Rc::clone(&self.library.library_list_state); - let tx_clone_render = self.library.tx.clone(); - let tx_clone_input = self.library.tx.clone(); + let lib_type = Library::LibraryType::Anime; + let render_ls = Rc::clone(&self.anime.library.library_list_state); + let input_ls = Rc::clone(&self.anime.library.library_list_state); + let tx_clone_render = self.anime.library.tx.clone(); + let tx_clone_input = self.anime.library.tx.clone(); CurrentWidget { render: Box::new(move |frame: &mut Frame, area: Rect| { let b = Block::new() @@ -538,10 +882,11 @@ impl App { KeyCode::Enter => { let state_bo = &state; if state_bo.entry_selected { - let kind = match lib_type { - Library::LibraryType::Anime => SourceType::Anime, - Library::LibraryType::Manga => SourceType::Manga - }; + // let kind = match lib_type { + // Library::LibraryType::Anime => SourceType::Anime, + // Library::LibraryType::Manga => SourceType::Manga + // }; + // let kind = SourceType::Anime; if let Some(u) = state_bo.list_state_eps.selected() && let Some(current) = state_bo.current_source.clone() && let Some(episode_loaded) = &state_bo.episodes_loaded { let items =Library::Library::sorted_keys_from_hash(&episode_loaded); if let Some(item) = items.get(u) { @@ -549,17 +894,17 @@ impl App { if let Some(eli) = episode_loaded.get(*item).clone() { let eli_c = (*eli).clone(); tokio::spawn(async move { - if let Ok(ex) = Extension::buildfromName(kind, ¤t){ - match kind { - SourceType::Anime => { - let s_a = ex.toAnimeSource(); - let _ = s_a.lib_ep_select(eli_c).await; - } - SourceType::Manga => { - let s_m = ex.toMangaSource(); - let _ = s_m.lib_ep_select(eli_c).await; - } - }; + if let Ok(ex) = Extension::buildfromName(SourceType::Anime, ¤t){ + // match kind { + // SourceType::Anime => { + let s_a = ex.toAnimeSource(); + let _ = s_a.lib_ep_select(eli_c).await; + // } + // SourceType::Manga => { + // let s_m = ex.toMangaSource(); + // let _ = s_m.lib_ep_select(eli_c).await; + // } + // }; } }); } @@ -567,26 +912,29 @@ impl App { } } else { if let Some(sel) = state_bo.list_state_source.selected() && let Some(current) = state_bo.current_source.clone() { - let kind = match lib_type { - Library::LibraryType::Anime => SourceType::Anime, - Library::LibraryType::Manga => SourceType::Manga - }; + // let kind = match lib_type { + // Library::LibraryType::Anime => SourceType::Anime, + // Library::LibraryType::Manga => SourceType::Manga + // }; + // let kind = SourceType::Anime; if let Some(u_list) = state_bo.lib.get(¤t) { if let Some(u) = u_list.get(sel) { let u_clone = u.clone(); // let lib_type_clone = lib_type.clone(); tokio::spawn(async move { - if let Ok(ex) = Extension::buildfromName(kind, ¤t) { - let hs = match kind { - SourceType::Anime => { - let s: Box = ex.toAnimeSource(); - s.lib_url_parse(u_clone.episodes).await - } - SourceType::Manga => { - let m: Box = ex.toMangaSource(); - m.lib_url_parse(u_clone.episodes).await - } - }; + if let Ok(ex) = Extension::buildfromName(SourceType::Anime, ¤t) { + // let hs = match kind { + // SourceType::Anime => { + // let s: Box = ex.toAnimeSource(); + // s.lib_url_parse(u_clone.episodes).await + // } + // SourceType::Manga => { + // let m: Box = ex.toMangaSource(); + // m.lib_url_parse(u_clone.episodes).await + // } + // }; + let a: Box = ex.toAnimeSource(); + let hs = a.lib_url_parse(u_clone.episodes).await; if let Ok(o) = hs { let _ = tx_clone_input_clone.send(o); } @@ -613,48 +961,308 @@ impl App { }) } } - // pub fn new_anime_list(&mut self) -> CurrentWidget { - // // let ls = Rc::new(RefCell::new(ListState::default())); - // // let render_ls = Rc::clone(&ls); - // // let input_ls = Rc::clone(&ls); - // CurrentWidget { render: Box::new(move |frame: &mut Frame, area: Rect| { - // // self.anime_list.render_asynclist(area, frame.buffer_mut()) - // }), handle_input: Box::new(move |k: KeyCode| {}) } - // } - // pub fn block_manga_library(&mut self) -> CurrentWidget { - // let ls = Rc::new(RefCell::new(ListState::default())); - // let render_ls = Rc::clone(&ls); - // let input_ls = Rc::clone(&ls); - // CurrentWidget { - // render: Box::new(move |frame: &mut Frame, area: Rect| { - // let b = Block::new() - // .borders(Borders::ALL) - // .border_type(BorderType::Rounded) - // .border_style(Style::new().fg(ratatui::style::Color::LightCyan)); - // // let fld = match read_manga_folder() { - // // Ok(a) => a, - // // _ => vec![] - // // }; - // // let items = fld.iter().enumerate().map(|(i, folder)| { - // // ListItem::new(folder.to_string_lossy().into_owned()) - // // }); - // // let l = List::new(items).block(b).highlight_style(Style::new().fg(tailwind::BLACK).bg(tailwind::AMBER.c200)); - // let mut state = render_ls.borrow_mut(); - // frame.render_stateful_widget(l, area, &mut *state); - // }), - // handle_input: Box::new(move |k: KeyCode| { - // let mut state = input_ls.borrow_mut(); - // match k { - // KeyCode::Up => { - // state.select_previous(); - // }, - // KeyCode::Down => { - // state.select_next(); - // } - // _ => {} - // } - // }) - // } - // } + pub fn block_library_manga(&mut self) -> CurrentWidget { + // let lib_type = Library::LibraryType::Manga; + // let render_ls = Rc::clone(list_state); + // let input_ls = Rc::clone(list_state); + let render_ls = Rc::clone(&self.manga.library.library_list_state); + let input_ls = Rc::clone(&self.manga.library.library_list_state); + let tx_clone_render = self.manga.library.tx.clone(); + let tx_clone_input = self.manga.library.tx.clone(); + CurrentWidget { + render: Box::new(move |frame: &mut Frame, area: Rect| { + let b = Block::new() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::new().fg(ratatui::style::Color::Cyan)); + let lay = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Percentage(100), + Constraint::Min(3) + ]).split(area); + let mut state = render_ls.borrow_mut(); + let l = Library::Library::new(Library::LibraryType::Manga,tx_clone_render.clone()); //TODO + let cmd_list = Self::generate_cmd_list(vec![ + (CmdListType::Multi, "W,A,S,D or Arrows","to move"), + (CmdListType::Single, "Enter","select"), + (CmdListType::Single, "Esc","Go Back"), + ]); + frame.render_widget( cmd_list, lay[1]); + //frame.render_widget(b, area); + frame.render_stateful_widget(l, lay[0], &mut *state); + }), handle_input: Box::new(move |k: KeyCode| { + let mut state = input_ls.borrow_mut(); + let tx_clone_input_clone = tx_clone_input.clone(); + match state.status { + LibraryStateStatus::Lib(p) => { + match k { + KeyCode::Left | KeyCode::Char('a') => { + if state.entry_selected { + + } else { + state.list_state_source.select(None); + if p>0 { + state.status = LibraryStateStatus::Lib(p-1); + } else { + state.status = LibraryStateStatus::Lib(state.lib.keys().len()-1); + } + } + true + }, + KeyCode::Right | KeyCode::Char('d') => { + if state.entry_selected { + + } else { + state.list_state_source.select(None); + state.status = LibraryStateStatus::Lib(p+1); + } + true + }, + KeyCode::Up | KeyCode::Char('w') => { + if state.entry_selected { + if state.list_state_eps.selected().is_none() { + state.list_state_eps.select_last(); + }else { + state.list_state_eps.select_previous(); + } + } else { + if state.list_state_source.selected().is_none() { + state.list_state_source.select_last(); + }else { + state.list_state_source.select_previous(); + } + } + true + }, + KeyCode::Down | KeyCode::Char('s')=> { + if state.entry_selected { + if state.list_state_eps.selected().is_none() { + state.list_state_eps.select_first(); + }else { + state.list_state_eps.select_next(); + } + } else { + if state.list_state_source.selected().is_none() { + state.list_state_source.select_first(); + }else { + state.list_state_source.select_next(); + } + } + true + }, + KeyCode::Enter => { + let state_bo = &state; + if state_bo.entry_selected { + // let kind = match lib_type { + // Library::LibraryType::Anime => SourceType::Anime, + // Library::LibraryType::Manga => SourceType::Manga + // }; + if let Some(u) = state_bo.list_state_eps.selected() && let Some(current) = state_bo.current_source.clone() && let Some(episode_loaded) = &state_bo.episodes_loaded { + let items = Library::Library::sorted_keys_from_hash(&episode_loaded); + if let Some(item) = items.get(u) { + //source.episode_play_url(url); + if let Some(eli) = episode_loaded.get(*item).clone() { + let eli_c = (*eli).clone(); + let title = state_bo.list_state_source.selected().and_then(|sel| state_bo.lib.get(¤t).and_then(|u_list| u_list.get(sel))).map(|x| x.name.clone()); + + tokio::spawn(async move { + if let Ok(ex) = Extension::buildfromName(SourceType::Manga, ¤t){ + let s_m: Box = ex.toMangaSource(); + let as_m = Arc::new(s_m); + let chp = eli_c.title.split_whitespace().last().map(|x| x.to_owned()); + openManga(as_m, MangaMetadata { title: title, url: eli_c.url, chapter: chp, volume: None}); + } + }); + } + } + } + } else { + if let Some(sel) = state_bo.list_state_source.selected() && let Some(current) = state_bo.current_source.clone() { + // let kind = match lib_type { + // Library::LibraryType::Anime => SourceType::Anime, + // Library::LibraryType::Manga => SourceType::Manga + // }; + if let Some(u_list) = state_bo.lib.get(¤t) { + if let Some(u) = u_list.get(sel) { + let u_clone = u.clone(); + // let lib_type_clone = lib_type.clone(); + tokio::spawn(async move { + if let Ok(ex) = Extension::buildfromName(SourceType::Manga, ¤t) { + // let hs = match kind { + // SourceType::Anime => { + // let s: Box = ex.toAnimeSource(); + // s.lib_url_parse(u_clone.episodes).await + // } + // SourceType::Manga => { + // let m: Box = ex.toMangaSource(); + // m.lib_url_parse(u_clone.episodes).await + // } + // }; + let m: Box = ex.toMangaSource(); + let hs = m.lib_url_parse(u_clone.episodes).await; + if let Ok(o) = hs { + let _ = tx_clone_input_clone.send(o); + } + } + }); + } + } + } + } + true + }, + KeyCode::Esc | KeyCode::Backspace => { + if state.entry_selected { + state.entry_selected = false; + return true; + } + false + } + _ => {false} + } + }, + _ => {false} + } + }) + } + } + /*** + * Input | + * List | ListEpisodes + * ----------------------- + * Keybinds Each Char(' ') | Backspace | Delete | Ins -> Input + * Enter if select -> List else to input to search + * --------------------------------------- + * On write on keyboard for input clear select. + */ + pub fn searchManga(&mut self) -> CurrentWidget { + let render_ls = Rc::clone(&self.manga.search.search_list_state); + let input_ls = Rc::clone(&self.manga.search.search_list_state); + let tx_clone_render = self.manga.search.tx.clone(); + let tx_clone_input = self.manga.search.tx.clone(); + CurrentWidget { + render: Box::new(move |frame: &mut Frame, area: Rect| { + let b = Block::new() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::new().fg(ratatui::style::Color::Cyan)); + let lay = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Percentage(100), + Constraint::Min(3) + ]).split(area); + let mut state = render_ls.borrow_mut(); + let l = Search::Search::new(Search::SearchType::Manga,tx_clone_render.clone()); //TODO + let cmd_list = Self::generate_cmd_list(vec![ + (CmdListType::Multi, "Arrows","to move"), + (CmdListType::Single, "Enter","select"), + (CmdListType::Single, "Esc","Go Back"), + ]); + frame.render_widget( cmd_list, lay[1]); + frame.render_stateful_widget(l, lay[0], &mut *state); + }), handle_input: Box::new(move |k: KeyCode| { + let mut state = input_ls.borrow_mut(); + let tx_clone_input_clone = tx_clone_input.clone(); + match state.status { + SearchStateStatus::Empty => { + match k { + ke @ (KeyCode::Char(_) | KeyCode::Left | KeyCode::Right | KeyCode::Backspace | KeyCode::Delete)=> { + state.text_input_state.dispatchEvent(ke) + }, + KeyCode::Enter => { + state.text_input_state.editingmode = false; + tx_clone_input_clone.send(SearchStateStatus::SearchFor(state.text_input_state.value.clone()) ); + true + } + _ => {false} + } + }, + SearchStateStatus::SearchFor(_) => { + match k { + ke @ (KeyCode::Char(_) | KeyCode::Left | KeyCode::Right | KeyCode::Backspace | KeyCode::Delete)=> { + state.text_input_state.dispatchEvent(ke) + }, + KeyCode::Enter => { + if state.text_input_state.editingmode { + state.text_input_state.editingmode = false; + tx_clone_input_clone.send(SearchStateStatus::SearchFor(state.text_input_state.value.clone()) ); + } else { + + } + true + }, + _ => {false} + } + }, + Search::SearchStateStatus::SearchResult(p) => { + match k { + KeyCode::Left => { + if state.entry_selected { + + } else { + + } + true + }, + KeyCode::Right => { + if state.entry_selected { + + } else { + + } + true + }, + KeyCode::Up => { + if state.entry_selected { + if state.list_state_eps.selected().is_none() { + state.list_state_eps.select_last(); + }else { + state.list_state_eps.select_previous(); + } + } else { + if state.list_state_source.selected().is_none() { + state.list_state_source.select_last(); + }else { + state.list_state_source.select_previous(); + } + } + true + }, + KeyCode::Down => { + if state.entry_selected { + if state.list_state_eps.selected().is_none() { + state.list_state_eps.select_first(); + }else { + state.list_state_eps.select_next(); + } + } else { + if state.list_state_source.selected().is_none() { + state.list_state_source.select_first(); + }else { + state.list_state_source.select_next(); + } + } + true + }, + KeyCode::Enter => { + false + }, + KeyCode::Esc | KeyCode::Backspace => { + if state.entry_selected { + state.entry_selected = false; + return true; + } + false + } + _ => {false} + } + }, + _ => {false} + } + }) + } + } } diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..f2d5f7f --- /dev/null +++ b/src/settings.rs @@ -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
, + listoptionstate: ListOptionState + // tx: tokio::sync::mpsc::UnboundedSender +} + +pub struct Settings { +} +impl Settings { + pub fn new() -> Settings { + Settings { + + } + } +} +impl SettingsState { + pub fn new(tx: tokio::sync::mpsc::UnboundedSender) -> SettingsState { + let mut v: Vec
= 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) -> 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) -> 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) +} \ No newline at end of file diff --git a/src/source.rs b/src/source.rs index 67f141a..c52bcdf 100644 --- a/src/source.rs +++ b/src/source.rs @@ -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 crate::{config::getConfig, log, utils::utils::AsyncError}; + pub mod animeworld; pub mod mangaworld; +pub mod mangago; -pub const AVAIBLEANIMESOURCE: [&str; 1] = ["animeworld"]; #[derive(Clone, Copy)] pub enum SourceType { Anime, Manga @@ -57,7 +59,8 @@ pub struct Chapter { #[async_trait] -pub trait AnimeSource: Send + Sync { +pub trait AnimeSource: Send + Sync { + fn has_update_page(&self) -> bool; /*let mut items = hostname.split(".").collect::>(); if let Some(f) = items.first() { if *f != "www" { @@ -81,25 +84,25 @@ pub trait AnimeSource: Send + Sync { let a = get(u).await?.error_for_status()?.text().await?; return Html::parse_document(&a); }*/ - async fn fetch_updated_page(&self, page: usize)-> Result>; - fn extract_episode_from_updated(&self, document: String) -> Result, Box>; + async fn fetch_updated_page(&self, page: usize)-> Result; + fn extract_episode_from_updated(&self, document: String) -> Result, AsyncError>; - async fn fetch_search_page(&self, query: String, page: usize) -> Result>; - fn extract_episode_from_search(&self, document: String) -> Result, Box>; + async fn fetch_search_page(&self, query: String, page: usize) -> Result; + fn extract_episode_from_search(&self, document: String) -> Result, AsyncError>; - async fn listupdated(&self, page: usize) -> Result, Box> { + async fn listupdated(&self, page: usize) -> Result, AsyncError> { let body = self.fetch_updated_page(page).await?; self.extract_episode_from_updated(body) } - async fn search(&self, query: String, page: usize) -> Result, Box>{ + async fn search(&self, query: String, page: usize) -> Result, 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>; async fn episode_play_url(&self, url: &str) -> Result>; - async fn lib_url_parse(&self, url: String) -> Result, Box>; - async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box>; + async fn lib_url_parse(&self, url: String) -> Result, AsyncError>; + async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError>; } #[async_trait] @@ -120,29 +123,115 @@ pub trait MangaSource: Send + Sync { 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>; - fn extract_episode_from_updated(&self, document: String) -> Result, Box>; + async fn fetch_updated_page(&self, page: usize)-> Result; + fn extract_episode_from_updated(&self, document: String) -> Result, AsyncError>; - async fn fetch_search_page(&self, query: String, page: usize) -> Result>; - fn extract_episode_from_search(&self, document: String) -> Result, Box>; + async fn fetch_search_page(&self, query: String, page: usize) -> Result; + fn extract_episode_from_search(&self, document: String) -> Result, AsyncError>; - async fn listupdated(&self, page: usize) -> Result, Box> { + async fn listupdated(&self, page: usize) -> Result, AsyncError> { let body = self.fetch_updated_page(page).await?; self.extract_episode_from_updated(body) } - async fn search(&self, query: String, page: usize) -> Result, Box>{ + async fn search(&self, query: String, page: usize) -> Result, 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, Box>; + async fn lib_url_parse(&self, url: String) -> Result, AsyncError>; // async fn chapter(&self, card: Episode) -> Result>; // async fn episode_play_url(&self, url: &str) -> Result>; - async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box>; + // async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError>; + async fn extract_pages_url(&self, chapter_url: String) -> Result, AsyncError>; + + async fn to_pdf(&self, metadata: MangaMetadata, img:Vec) -> Result; + async fn to_cbz(&self, metadata: MangaMetadata, img:Vec) -> Result; + // async fn chapterMetadata(&self, url: String) -> MangaMetadata; } + +pub fn openManga(so: Arc>, 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 + }, + 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, + pub url: String, + pub chapter: Option, + pub volume: Option +} + +pub fn getSources(folder: &str) -> Vec { + 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 +} \ No newline at end of file diff --git a/src/source/animeworld.rs b/src/source/animeworld.rs index 2f97cd4..34a10c4 100644 --- a/src/source/animeworld.rs +++ b/src/source/animeworld.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, process::{Command, Stdio}}; use reqwest::get; 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 { url: String, @@ -19,6 +19,7 @@ impl Animeworld { } #[async_trait::async_trait] impl AnimeSource for Animeworld { + fn has_update_page(&self) -> bool {true} fn get_hostname(&self)->String { self.hostname.clone() } @@ -33,11 +34,11 @@ impl AnimeSource for Animeworld { sourcetype: crate::source::SourceType::Anime } } - async fn listupdated(&self, page: usize) -> Result, Box> { + async fn listupdated(&self, page: usize) -> Result, AsyncError> { let body = self.fetch_updated_page(page).await?; self.extract_episode_from_updated(body) } - fn extract_episode_from_search(&self, body: String) -> Result, Box> { + fn extract_episode_from_search(&self, body: String) -> Result, AsyncError> { // let sel = Selector::parse("div.film-list").unwrap(); let document = Html::parse_document(&body); let sel_inner = Selector::parse("div.inner").unwrap(); @@ -64,7 +65,7 @@ impl AnimeSource for Animeworld { }); Ok(cards.collect::>()) } - fn extract_episode_from_updated(&self, body: String) -> Result, Box> { + fn extract_episode_from_updated(&self, body: String) -> Result, AsyncError> { self.extract_episode_from_search(body) } @@ -99,26 +100,26 @@ impl AnimeSource for Animeworld { Ok(String::new()) } - async fn fetch_search_page(&self, query: String, page: usize) -> Result> { + async fn fetch_search_page(&self, query: String, page: usize) -> Result { // 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, Box>{ + async fn search(&self, query: String, page: usize) -> Result, 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> { + async fn fetch_updated_page(&self, page: usize)-> Result { 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, Box>{ + async fn lib_url_parse(&self, url: String) -> Result, AsyncError>{ let mut hs = HashMap::::new(); if let Ok(a) = get(url).await?.error_for_status()?.text().await { let b = Html::parse_document(&a); @@ -133,7 +134,7 @@ impl AnimeSource for Animeworld { } return Ok(hs); } - async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box> { + 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() { diff --git a/src/source/mangago.rs b/src/source/mangago.rs new file mode 100644 index 0000000..7717237 --- /dev/null +++ b/src/source/mangago.rs @@ -0,0 +1,1332 @@ +use std::{collections::HashMap, f64::consts::E, fmt::format, io::Read, iter::Once, sync::OnceLock}; +use base64::{Engine, engine::general_purpose}; +use crc32fast::Hasher; +use hex::decode; +use regex::Regex; +use reqwest::{Client, Response, get, header::{HeaderMap, HeaderValue}}; +use scraper::{Element, Html, Selector}; +use anyhow::{Context, Result}; +use zip::write::SimpleFileOptions; + +use crate::{log, source::{Chapter, EpisodeListItem, MangaMetadata, MangaSource, SourceType}, utils::utils::{AsyncError, MangaCbz}}; +const UA: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"; + +pub static MANGAGO_CLIENT: OnceLock = OnceLock::new(); +fn get_client() -> Client { + MANGAGO_CLIENT.get_or_init(|| { + Client::builder() + .pool_max_idle_per_host(1) + .cookie_store(true) + .user_agent(UA) + .build().unwrap_or(Client::new()) + }).clone() +} +async fn fetch(url: String) -> Result { + let cl = get_client(); + cl.get(url).send().await?.error_for_status()?.text().await +} + +pub struct Mangago { + url: String, + hostname:String, + sourcetype: SourceType +} + +#[async_trait::async_trait] +impl MangaSource for Mangago { + // mangago.me + // https://www.mangago.me + fn new(hostname:String,url:String) -> Self { + Self { url, hostname, sourcetype: crate::source::SourceType::Manga } + } + + fn get_hostname(&self) -> String { + self.hostname.clone() + } + + fn get_url(&self) -> String { + self.url.clone() + } + fn has_update_page(&self) -> bool { + true + } + async fn listupdated(&self, page: usize) -> Result, AsyncError> { + let body = self.fetch_updated_page(page).await?; + self.extract_episode_from_updated(body) + } + // https://www.mangago.me/r/l_search/?name=query + fn extract_episode_from_search(&self, body: String) -> Result, AsyncError> { + let document = Html::parse_document(&body); + let sel_inner = Selector::parse("#search_list li").unwrap(); + // let sel_episode_number = Selector::parse("a.xanh").unwrap(); + let sel_anime_name = Selector::parse("a").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::().unwrap_or(0); + // log!("index: {}", index); + let carda = card.select(&sel_anime_name).last()?; + + // log!("Card a found"); + let url = carda.value().attr("href").unwrap_or("").trim().to_owned(); + // log!("Card url: {}",url); + let title = carda.text().filter_map(|e| { + match e.is_empty() { + true => None, + false => Some(e.trim().to_owned()), + } + }).collect::>().join(" "); + // log!("Card title: {}", title); + Some(Chapter { + title, + index: 0, + url, + section: 0 + }) + }); + Ok(cards.collect::>()) + } + // https://www.mangago.me/list/latest/all/p/ + fn extract_episode_from_updated(&self, body: String) -> Result, AsyncError> { + let document = Html::parse_document(&body); + let sel_inner = Selector::parse("#search_list li").unwrap(); + // let sel_episode_number = Selector::parse("a.xanh").unwrap(); + let sel_anime_name = Selector::parse("a").unwrap(); + let chico = Selector::parse(".chicho").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::().unwrap_or(0); + // log!("index: {}", index); + let carda = card.select(&sel_anime_name).last()?; + let lastindex= card.select(&chico).next()?; + let liinner = lastindex.inner_html(); + let indexstr = match liinner.rsplit_once(".") { + Some(a) => a.1, + None => &liinner, + }; + let index = match indexstr.trim().parse::() { + Ok(b) => b, + Err(_) => 0, + }; + // log!("Card a found"); + let url = lastindex.value().attr("href").unwrap_or("").trim().to_owned();//carda.value().attr("href").unwrap_or("").trim().to_owned(); + // log!("Card url: {}",url); + let title = carda.text().filter_map(|e| { + match e.is_empty() { + true => None, + false => Some(e.trim().to_owned()), + } + }).collect::>().join(" "); + // log!("Card title: {}", title); + Some(Chapter { + title, + index, + url, + section: 0 + }) + }); + Ok(cards.collect::>()) + } + async fn fetch_search_page(&self, query: String, page: usize) -> Result { + let cl = reqwest::Client::new(); + let p = std::cmp::max(1, page); + let u = format!("{}/r/l_search/?name={}&page={}",self.get_url(), query, p); + let r = cl.get(u).header("user-agent", UA).send().await?.error_for_status()?.text().await?; + return Ok(r); + } + async fn fetch_updated_page(&self, page: usize)-> Result { + let cl = reqwest::Client::new(); + let p = std::cmp::max(1, page); + let u = format!("{}/list/latest/all/{}",self.get_url(), p); + let a = cl.get(u).header("user-agent", UA).send().await?.error_for_status()?.text().await?; + return Ok(a); + } + // https://www.mangago.me/read-manga/tongari_boushi_no_atelier/ + async fn lib_url_parse(&self, url: String) -> Result, AsyncError> { + let cl = reqwest::Client::new(); + let mut hs = HashMap::::new(); + if let Ok(a) = cl.get(url).header("user-agent", UA).send().await?.error_for_status()?.text().await { + let b = Html::parse_document(&a); + let table_row_sel = Selector::parse("#chapter_table tr").unwrap(); + let chico = Selector::parse(".chico").unwrap(); + let table_row = b.select(&table_row_sel); + for tr in table_row { + if let Some(a) = tr.select(&chico).next() { + let url = a.value().attr("href").unwrap_or("").trim().to_owned(); + let title: String = a.text().collect(); + + let k = title.split_whitespace().find(|ch| ch.to_lowercase().starts_with("ch.")) + .and_then(|m| m.split_once(".")).and_then(|s| Some(s.1.to_owned())); + + hs.insert(k.unwrap_or("af".to_string()), EpisodeListItem { + title, + url + }); + } + } + } + return Ok(hs); + } + // TODO: https://claude.ai/chat/a3cece13-03d9-4992-b3cb-df73c3bb875b + async fn extract_pages_url(&self, chapter_url: String) -> Result, AsyncError> { + let doc = get_client().get(&chapter_url).send().await?.text().await?; + let cl = get_client(); + claude_refactor_mangago_tachiyomi_extension::get_chapter_image_urls(&doc, &chapter_url, &cl).await.map_err(Into::into) + } + async fn to_pdf(&self, metadata: MangaMetadata, images:Vec) -> Result { + + Ok("".to_owned()) + } + async fn to_cbz(&self, metadata: MangaMetadata, images:Vec) -> Result { + 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)?; + log!("{}", images.join("\n")); + for img in images { + let mut h = Hasher::new(); + let png = get_client().get(img).send().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) + } +} + + +struct SoJsonV4Deobfuscator {} +impl SoJsonV4Deobfuscator { + fn decode(jsf: String) -> Result { + if !jsf.starts_with("['sojson.v4']") { + return Err("Obfuscated code is not sojson.v4".to_owned());//throw IllegalArgumentException("Obfuscated code is not sojson.v4") + } + //let regex = Regex::new(r"[a-zA-Z]+").unwrap(); + let args= jsf[240..(jsf.len()- 59)].split(|c: char| c.is_alphabetic());//.split(®ex); + + return Ok(args.filter_map(|s| s.parse::().ok()) + .filter_map(std::char::from_u32) + .collect()); + } +} + + + +// mod MangagoAniyomi { +// use once_cell::sync::Lazy; +// use regex::Regex; +// static RE_DESCKEY: Lazy = Lazy::new(|| Regex::new(r"desckey=([^&]+)").unwrap()); +// static RE_COLS: Lazy = Lazy::new(|| Regex::new(r"cols=(\d+)").unwrap()); +// static RE_TOTAL_PAGES: Lazy = Lazy::new(|| Regex::new(r"total_pages\s*=\s*(\d+)").unwrap()); +// static RE_IMG_SRCS: Lazy = Lazy::new(|| Regex::new(r#"var imgsrcs\s*=\s*['"]([a-zA-Z0-9+=/]+)['"]"#).unwrap()); +// static RE_IMG_SRCS: Lazy = Lazy::new(|| {Regex::new(r#"var imgsrcs\s*=\s*['"]([a-zA-Z0-9+=/]+)['"]"#).unwrap()}); + +// static RE_COLS: Lazy = Lazy::new(|| {Regex::new(r#"var\s*widthnum\s*=\s*heightnum\s*=\s*(\d+);"#).unwrap()}); +// pub struct Page { +// pub index: usize, +// pub url: String, +// pub image_url: Option, +// } +// fn get_and_unscramble(client: &Client, url_str: &str) -> anyhow::Result> { +// let response = client.get(url_str).send()?; + +// let fragment = url_str.split_once('#').map(|(_, f)| f).unwrap_or(""); + +// if !fragment.contains("desckey=") { +// return Ok(response.bytes()?.to_vec()); +// } + +// let key = RE_DESCKEY.captures(fragment) +// .and_then(|cap| cap.get(1)) +// .map(|m| m.as_str()) +// .ok_or_else(|| anyhow::anyhow!("Missing desckey"))?; + +// let cols = RE_COLS.captures(fragment) +// .and_then(|cap| cap.get(1)) +// .and_then(|m| m.as_str().parse::().ok()) +// .ok_or_else(|| anyhow::anyhow!("Missing or invalid cols"))?; + +// let body_bytes = response.bytes()?.to_vec(); +// let unscrambled = unscramble_image(body_bytes, key, cols); + +// Ok(unscrambled) +// } +// fn page_list_parse(html_content: &str, base_url: &str, domain: &str) -> Result, Box> { +// let document = Html::parse_document(html_content); + +// let available_images = get_chapter_image_urls(&document); + +// if !available_images.is_empty() && available_images.iter().all(|s| !s.trim().is_empty()) { +// return Ok(available_images.into_iter().enumerate().map(|(idx, img)| { +// Page { index: idx, url: String::new(), image_url: Some(img) } +// }).collect()); +// } + +// let script_selector = Selector::parse("script").unwrap(); +// let total_pages = document.select(&script_selector) +// .find_map(|element| { +// let data = element.inner_html(); +// RE_TOTAL_PAGES.captures(&data) +// .and_then(|cap| cap[1].parse::().ok()) +// }) +// .ok_or("Total page count not found")?; + +// let input_selector = Selector::parse("input#curl").unwrap(); +// let url_template = document.select(&input_selector) +// .next() +// .and_then(|el| el.value().attr("value")) +// .map(|v| v.trim().trim_start_matches('/')) +// .ok_or("Input #curl not found")?; + +// if !url_template.contains("{page}") { +// return Err("No replaceable string in url template".into()); +// } + +// let current_url = Url::parse(base_url)?; +// let path_segments: Vec<&str> = current_url.path_segments().map(|s| s.collect()).unwrap_or_default(); +// let host = current_url.host_str().unwrap_or(""); +// let template_first_segment = url_template.split('/').next().unwrap_or(""); + +// let prefix = if host.ends_with(domain) +// && path_segments.len() > 3 +// && path_segments[0] == "read-manga" +// && path_segments[2] == template_first_segment +// { +// let slug = path_segments[1]; +// format!("{}/read-manga/{}", base_url.trim_end_matches('/'), slug) +// } else if !host.ends_with(domain) && path_segments.get(0) == Some(&template_first_segment) { +// format!("https://{}", host) +// } else { +// return Err("Unexpected Url structure".into()); +// }; + +// let mut pages: Vec = (1..=total_pages).map(|p| { +// let mut page_url = Url::parse(&prefix).unwrap(); +// let specific_path = url_template.replace("{page}", &p.to_string()); + +// page_url.set_path(&format!("{}/{}", page_url.path().trim_end_matches('/'), specific_path)); +// page_url.set_fragment(Some(&p.to_string())); + +// Page { +// index: p, +// url: page_url.to_string(), +// image_url: None, +// } +// }).collect(); + +// for (i, page) in pages.iter_mut().enumerate() { +// if let Some(img) = available_images.get(i) { +// if !img.trim().is_empty() { +// page.image_url = Some(img.clone()); +// } +// } +// } + +// Ok(pages) +// } +// fn get_chapter_image_urls(html: &str, client: &reqwest::blocking::Client) -> Result, Box> { +// let document = Html::parse_document(html); + +// // 1. Extract and Base64 decode images +// let script_sel = Selector::parse("script").unwrap(); +// let b64_data = document.select(&script_sel) +// .find_map(|el| { +// let data = el.inner_html(); +// RE_IMG_SRCS.captures(&data).map(|cap| cap[1].to_string()) +// }) +// .ok_or("Could not find 'imgsrcs' script")?; + +// let encrypted_images = general_purpose::STANDARD.decode(b64_data)?; + +// // 2. Fetch and De-obfuscate chapter.js +// let js_sel = Selector::parse("script[src*='chapter.js']").unwrap(); +// let js_url = document.select(&js_sel) +// .next() +// .and_then(|el| el.value().attr("src")) +// .ok_or("Could not find chapter.js src")?; + +// let raw_js = client.get(js_url).send()?.text()?; +// // NOTE: You must implement your own SoJsonV4 deobfuscator logic here +// let deobfuscated_js = so_json_v4_deobfuscator(&raw_js); + +// // 3. Extract Hex Keys (mimicking findHexEncodedVariable) +// let key_hex = extract_hex_var(&deobfuscated_js, "key")?; +// let iv_hex = extract_hex_var(&deobfuscated_js, "iv")?; +// let key = hex::decode(key_hex)?; +// let iv = hex::decode(iv_hex)?; + +// // 4. AES Decryption (AES/CBC/ZEROBYTEPADDING) +// let cipher = Aes128Cbc::new_from_slices(&key, &iv)?; +// let decrypted_data = cipher.decrypt_vec(&encrypted_images)?; +// let image_list_raw = String::from_utf8(decrypted_data)?; + +// // 5. Unscramble List (Another manual port of your logic) +// let image_list = unscramble_image_list(&image_list_raw, &deobfuscated_js); + +// // 6. JavaScript Execution (QuickJS) +// let cols = RE_COLS.captures(&deobfuscated_js) +// .map(|cap| cap[1].to_string()) +// .unwrap_or_default(); + +// // Extract the image key logic (JS string manipulation) +// let img_keys_js = extract_img_keys_logic(&deobfuscated_js); + +// let rt = Runtime::new()?; +// let ctx = Context::full(&rt)?; + +// let resolved_urls = ctx.with(|ctx| { +// // Mocking the bytecode and function setup +// let js_code = format!( +// "function getDescramblingKey(url) {{ {}; return key; }};", +// img_keys_js.replace("img.src", "url") +// ); +// ctx.eval::<(), _>(js_code).unwrap(); +// let get_key: rquickjs::Function = ctx.globals().get("getDescramblingKey").unwrap(); + +// image_list.split(',').map(|url| { +// if url.contains("cspiclink") { +// let desc_key: String = get_key.call((url,)).unwrap_or_else(|_| "".to_string()); +// format!("{}#desckey={}&cols={}", url, desc_key, cols) +// } else { +// url.to_string() +// } +// }).collect::>() +// }); + +// Ok(resolved_urls) +// } +// } + +//? Rust port of `getChapterImageUrls` and its dependencies from the Mangago Kotlin extension. +//? +//? # Required dependencies (Cargo.toml) +//? ```toml +//? [dependencies] +//? anyhow = "1" +//? base64 = "0.22" +//? aes = "0.8" +//? cbc = { version = "0.1", features = ["alloc"] } +//? cipher = { version = "0.4", features = ["block-padding"] } +//? hex = "0.4" +//? image = "0.25" +//? regex = "1" +//? once_cell = "1" +//? boa_engine = "0.19" # JS runtime replacing QuickJS / qjs.evaluate +//? reqwest = { version = "0.12", features = ["blocking"] } +//? ``` +pub mod old_claude_refactor_mangago_tachiyomi_extension { + use std::collections::HashSet; + use std::io::Cursor; + + use aes::Aes128; + use anyhow::{anyhow, bail, Context, Result}; + use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; + use cbc::cipher::{block_padding::ZeroPadding, BlockDecryptMut, KeyIvInit}; + use boa_engine::{Context as JsContext, Source}; + use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat}; + use once_cell::sync::Lazy; + use regex::Regex; + use reqwest::Client; + + // ── Third-party type provided by the caller ────────────────────────────────── + + /// Minimal stand-in for a parsed HTML document. + /// In a real integration swap this for `scraper::Html` or whatever DOM type + /// your HTTP layer already produces. + pub struct Document { + pub html: String, + pub location: String, + } + + // ─── Regex constants (compiled once) ───────────────────────────────────────── + + static IMG_SRCS_RE: Lazy = + Lazy::new(|| Regex::new(r#"var imgsrcs\s*=\s*['"]([a-zA-Z0-9+=/]+)['"]"#).unwrap()); + + static COLS_RE: Lazy = + Lazy::new(|| Regex::new(r"var\s*widthnum\s*=\s*heightnum\s*=\s*(\d+);").unwrap()); + + static KEY_LOCATION_RE: Lazy = + Lazy::new(|| Regex::new(r"str\.charAt\(\s*(\d+)\s*\)").unwrap()); + + static HEX_VAR_RE_KEY: Lazy = Lazy::new(|| { + Regex::new(r#"var key\s*=\s*CryptoJS\.enc\.Hex\.parse\("([0-9a-fA-F]+)"\)"#).unwrap() + }); + + static HEX_VAR_RE_IV: Lazy = Lazy::new(|| { + Regex::new(r#"var iv\s*=\s*CryptoJS\.enc\.Hex\.parse\("([0-9a-fA-F]+)"\)"#).unwrap() + }); + + // Substrings whose presence disqualifies a JS line from the key-extraction snippet. + const JS_FILTERS: &[&str] = &[ + "jQuery", + "document", + "getContext", + "toDataURL", + "getImageData", + "width", + "height", + ]; + + // ─── SoJsonV4Deobfuscator (provided) ───────────────────────────────────────── + + struct SoJsonV4Deobfuscator; + + impl SoJsonV4Deobfuscator { + fn decode(jsf: &str) -> Result { + if !jsf.starts_with("['sojson.v4']") { + bail!("Obfuscated code is not sojson.v4"); + } + let slice = &jsf[240..(jsf.len() - 59)]; + let decoded: String = slice + .split(|c: char| c.is_alphabetic()) + .filter_map(|s| s.parse::().ok()) + .filter_map(char::from_u32) + .collect(); + Ok(decoded) + } + } + + // ─── Helper: decode a hex string to bytes ──────────────────────────────────── + + fn decode_hex(s: &str) -> Result> { + if s.len() % 2 != 0 { + bail!("Hex string has odd length"); + } + hex::decode(s).context("Invalid hex string") + } + + // ─── Helper: find a hex-encoded CryptoJS variable ──────────────────────────── + + /// Extracts the hex payload from lines like: + /// `var key = CryptoJS.enc.Hex.parse("deadbeef...")` + fn find_hex_encoded_variable(js: &str, variable: &str) -> Result> { + // Build the regex on the fly so we can parameterise `variable`. + // In hot paths consider caching these with a HashMap<&str, Regex>. + let pattern = format!( + r#"var {variable}\s*=\s*CryptoJS\.enc\.Hex\.parse\("([0-9a-fA-F]+)"\)"# + ); + let re = Regex::new(&pattern).unwrap(); + let hex = re + .captures(js) + .and_then(|c| c.get(1)) + .map(|m| m.as_str()) + .ok_or_else(|| anyhow!("Could not find hex variable `{variable}` in JS"))?; + decode_hex(hex) + } + + // ─── AES-128-CBC decrypt with zero-byte padding ─────────────────────────────── + + /// Mirrors `Cipher.getInstance("AES/CBC/ZEROBYTEPADDING")`. + fn aes_cbc_decrypt_zero_pad(ciphertext: &[u8], key: &[u8], iv: &[u8]) -> Result> { + // cbc + ZeroPadding operates in-place on a Vec whose length is a block multiple. + let mut buf = ciphertext.to_vec(); + + type Aes128CbcDec = cbc::Decryptor; + + let decryptor = Aes128CbcDec::new_from_slices(key, iv) + .map_err(|e| anyhow!("AES init failed: {e}"))?; + + let plaintext = decryptor + .decrypt_padded_mut::(&mut buf) + .map_err(|e| anyhow!("AES decrypt failed: {e}"))?; + + Ok(plaintext.to_vec()) + } + + // ─── Image-list unscrambling ────────────────────────────────────────────────── + + /// Mirrors the Kotlin `String.unscramble(keys)` extension. + fn unscramble_string(s: &str, keys: &[usize]) -> String { + let mut chars: Vec = s.chars().collect(); + for &key in keys.iter().rev() { + let len = chars.len(); + let mut i = len.saturating_sub(1); + while i >= key { + if i % 2 != 0 { + chars.swap(i - key, i); + } + if i == 0 { + break; + } + i -= 1; + } + } + chars.into_iter().collect() + } + + /// Mirrors `unscrambleImageList`. + fn unscramble_image_list(image_list: &str, js: &str) -> String { + // Collect distinct char positions referenced by `str.charAt(N)` calls. + let key_locations: Vec = { + let mut seen = HashSet::new(); + KEY_LOCATION_RE + .captures_iter(js) + .filter_map(|c| c.get(1)?.as_str().parse::().ok()) + .filter(|&loc| seen.insert(loc)) // distinct, insertion-order preserved + .collect() + }; + + // Try to parse a digit at each key location; bail out on failure (already unscrambled). + let chars: Vec = image_list.chars().collect(); + + let unscramble_key: Vec = match key_locations + .iter() + .map(|&loc| { + chars + .get(loc) + .and_then(|c| c.to_digit(10)) + .map(|d| d as usize) + .ok_or(()) + }) + .collect::, _>>() + { + Ok(k) => k, + Err(_) => return image_list.to_owned(), // already unscrambled + }; + + // Remove the key-location characters (accounting for shrinking indices). + let mut img_chars: Vec = chars; + for (offset, &loc) in key_locations.iter().enumerate() { + let adjusted = loc - offset; + if adjusted < img_chars.len() { + img_chars.remove(adjusted); + } + } + + let intermediate: String = img_chars.into_iter().collect(); + unscramble_string(&intermediate, &unscramble_key) + } + + // ─── Image tile unscrambling ────────────────────────────────────────────────── + + /// Mirrors `unscrambleImage`. + /// `key` is the descrambling key string (e.g. `"3a1a0a2a..."`). + /// `cols` is the grid dimension (cols × cols tiles). + pub fn unscramble_image(image_bytes: &[u8], key: &str, cols: usize) -> Result> { + let src: DynamicImage = + image::load_from_memory(image_bytes).context("Failed to decode source image")?; + + let w = src.width() as usize; + let h = src.height() as usize; + let unit_w = w / cols; + let unit_h = h / cols; + + let mut result = DynamicImage::new_rgba8(w as u32, h as u32); + + let key_parts: Vec = key + .split('a') + .map(|s| if s.is_empty() { 0 } else { s.parse().unwrap_or(0) }) + .collect(); + + for idx in 0..(cols * cols) { + let key_val = key_parts.get(idx).copied().unwrap_or(0); + + let dst_row = key_val / cols; + let dst_col = key_val % cols; + let dx = (dst_col * unit_w) as u32; + let dy = (dst_row * unit_h) as u32; + + let src_row = idx / cols; + let src_col = idx % cols; + let sx = (src_col * unit_w) as u32; + let sy = (src_row * unit_h) as u32; + + let tile = src.crop_imm(sx, sy, unit_w as u32, unit_h as u32); + image::imageops::overlay(&mut result, &tile, dx as i64, dy as i64); + } + + let mut buf = Cursor::new(Vec::new()); + result + .write_to(&mut buf, ImageFormat::Jpeg) + .context("Failed to encode result image")?; + Ok(buf.into_inner()) + } + + // ─── JS evaluation (replacePos + descrambling key extraction) ──────────────── + + /// Evaluates the per-image JS snippet with Boa to recover the descrambling key. + /// Mirrors the QuickJs block in `getChapterImageUrls`. + fn get_descrambling_key(url: &str, img_js_snippet: &str) -> Result { + let replace_pos_js = r#" + function replacePos(strObj, pos, replacetext) { + return strObj.substr(0, pos) + replacetext + strObj.substring(pos + 1, strObj.length); + } + "#; + + let wrapper = format!( + r#" + {replace_pos_js} + function getDescramblingKey(url) {{ + {img_js_snippet} + return key; + }} + getDescramblingKey("{url}"); + "# + ); + + let mut js_ctx = JsContext::default(); + let result = js_ctx + .eval(Source::from_bytes(wrapper.as_bytes())) + .map_err(|e| anyhow!("JS eval error: {e}"))?; + + result + .as_string() + .map(|s| s.to_std_string_escaped()) + .ok_or_else(|| anyhow!("JS did not return a string for url `{url}`")) + } + + // ─── DOM helpers (thin wrappers; adapt to your actual HTML parser) ──────────── + + /// Finds the first `