diff --git a/Cargo.lock b/Cargo.lock index 9760718..4d156e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,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", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -102,6 +113,9 @@ name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arg_enum_proc_macro" @@ -188,9 +202,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19135c0c7a60bfee564dbe44ab5ce0557c6bf3884e5291a50be76a15640c4fbd" +checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" dependencies = [ "arrayvec", ] @@ -264,6 +278,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -317,6 +340,25 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "calloop" version = "0.13.0" @@ -345,9 +387,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.27" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "jobserver", "libc", @@ -372,9 +414,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34e221e91c7eb5e8315b5c9cf1a61670938c0626451f954a51693ed44b37f45" +checksum = "0d0390889d58f934f01cd49736275b4c2da15bcfc328c78ff2349907e6cabf22" dependencies = [ "smallvec", "target-lexicon 0.13.2", @@ -392,6 +434,16 @@ 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", + "inout", +] + [[package]] name = "clipboard-win" version = "5.4.0" @@ -466,6 +518,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation" version = "0.9.4" @@ -563,6 +621,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -615,6 +682,16 @@ dependencies = [ "wgpu", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "ctor-lite" version = "0.1.0" @@ -627,6 +704,43 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "directories" version = "6.0.0" @@ -846,6 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1033,6 +1148,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -1061,9 +1186,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1397,6 +1524,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.3.1" @@ -1491,9 +1627,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ "base64", "bytes", @@ -1862,6 +1998,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -1873,6 +2018,17 @@ dependencies = [ "syn", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2011,9 +2167,9 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -2029,6 +2185,26 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "liblzma" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0791ab7e08ccc8e0ce893f6906eb2703ed8739d8e89b57c0714e71bad09024c8" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libm" version = "0.2.15" @@ -2046,6 +2222,15 @@ dependencies = [ "redox_syscall 0.5.13", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + [[package]] name = "lilt" version = "0.8.1" @@ -2320,6 +2505,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -2724,6 +2915,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[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.1" @@ -2805,6 +3006,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3104,9 +3311,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "a457e416a0f90d246a4c3288bd7a25b2304ca727f253f95be383dd17af56be8f" [[package]] name = "ring" @@ -3136,6 +3343,7 @@ dependencies = [ "strum 0.27.1", "strum_macros 0.27.1", "url", + "zip", ] [[package]] @@ -3375,6 +3583,17 @@ dependencies = [ "serde", ] +[[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 = "shlex" version = "1.3.0" @@ -3685,7 +3904,7 @@ version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" dependencies = [ - "cfg-expr 0.20.0", + "cfg-expr 0.20.1", "heck", "pkg-config", "toml", @@ -3777,6 +3996,25 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3843,15 +4081,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", + "slab", "socket2", "windows-sys 0.52.0", ] @@ -4023,6 +4263,12 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -5118,6 +5364,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -5152,6 +5412,78 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ab361742de920c5535880f89bbd611ee62002bf11341d16a5f057bb8ba6899" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap", + "liblzma", + "memchr", + "pbkdf2", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +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.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.4.12" @@ -5169,9 +5501,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7384255a918371b5af158218d131530f694de9ad3815ebdd0453a940485cb0fa" +checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index f6b93fd..471e93d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ reqwest = { version = "0.12.22", features = [ # "blocking", "gzip", "json"] } +zip = "4.2.0" [profile.release] strip = true # Automatically strip symbols from the binary. diff --git a/iced_video_player/src/video.rs b/iced_video_player/src/video.rs index c2bb3ae..f80bf34 100644 --- a/iced_video_player/src/video.rs +++ b/iced_video_player/src/video.rs @@ -49,7 +49,7 @@ impl Frame { Self(gst::Sample::builder().build()) } - pub fn readable(&self) -> Option> { + pub fn readable(&self) -> Option> { self.0.buffer().and_then(|x| x.map_readable().ok()) } } diff --git a/src/components/installer.rs b/src/components/installer.rs index fdc4ae8..9150757 100644 --- a/src/components/installer.rs +++ b/src/components/installer.rs @@ -8,7 +8,7 @@ use reqwest::Client; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING}; use serde::{Deserialize, Serialize}; -use crate::{ORGANIZATION, PRODUCT}; +use crate::{PossibleGames, ORGANIZATION, PRODUCT}; const KURO_BASE_URL: &str = "https://prod-alicdn-gamestarter.kurogame.com/launcher/launcher/50004_obOHXFrFanqsaIEOmuKroCcbZkQRBC7c/G153"; @@ -76,11 +76,11 @@ pub async fn refresh_install() -> Result<(), String> { ); if let Err(e) = kuro_result { - eprintln!("Error in Kuro install: {}", e); + eprintln!("Error in Kuro install: {e}"); } if let Err(e) = hoyo_result { - eprintln!("Error in Hoyo install: {}", e); + eprintln!("Error in Hoyo install: {e}"); } Ok(()) @@ -94,26 +94,26 @@ fn build_client() -> Result { .default_headers(headers) .gzip(true) .build() - .map_err(|e| format!("Failed to build HTTP client: {}", e)) + .map_err(|e| format!("Failed to build HTTP client: {e}")) } async fn refresh_kuro_install(proj_dirs: &ProjectDirs, client: &Client) -> Result<(), String> { - let data_dir_buf = proj_dirs.data_dir().join("kuro/wuwa"); + let data_dir_buf = proj_dirs.data_dir().join("kurogames\\wuwa"); let data_dir = data_dir_buf.as_path(); create_dir_all(data_dir) - .map_err(|e| format!("Failed to create directory: {}", e))?; + .map_err(|e| format!("Failed to create directory: {e}"))?; let kuro_url = KURO_BASE_URL.to_string(); let index_url = format!("{}{}", kuro_url, "/index.json"); - eprintln!("Fetching index from: {}", index_url); + eprintln!("Fetching index from: {index_url}"); let response = client .get(&index_url) .send() .await - .map_err(|e| format!("Failed to send HTTP request: {}", e))?; + .map_err(|e| format!("Failed to send HTTP request: {e}"))?; if !response.status().is_success() { return Err(format!("HTTP request failed with status {}", response.status())); @@ -125,10 +125,10 @@ async fn refresh_kuro_install(proj_dirs: &ProjectDirs, client: &Client) -> Resul .json() .await .map_err(|e| { - format!("Failed to parse JSON - {}", e) + format!("Failed to parse JSON - {e}") })?; - eprintln!("Successfully parsed index.json: {:?}", version_index); + eprintln!("Successfully parsed index.json: {version_index:?}"); let bg_info_url = format!("{}{}{}{}", "https://prod-alicdn-gamestarter.kurogame.com/launcher/50004_obOHXFrFanqsaIEOmuKroCcbZkQRBC7c/G153", @@ -137,13 +137,13 @@ async fn refresh_kuro_install(proj_dirs: &ProjectDirs, client: &Client) -> Resul "/en.json" ); - eprintln!("Fetching background info from: {}", bg_info_url); + eprintln!("Fetching background info from: {bg_info_url}"); let bg_response = client .get(&bg_info_url) .send() .await - .map_err(|e| format!("Failed to send background info request: {}", e))?; + .map_err(|e| format!("Failed to send background info request: {e}"))?; if !bg_response.status().is_success() { return Err(format!("Background info request failed with status {}", bg_response.status())); @@ -152,9 +152,9 @@ async fn refresh_kuro_install(proj_dirs: &ProjectDirs, client: &Client) -> Resul let bg_information: KuroBackgroundInformation = bg_response .json() .await - .map_err(|e| format!("Failed to parse background info JSON: {}", e))?; + .map_err(|e| format!("Failed to parse background info JSON: {e}"))?; - eprintln!("Successfully parsed background info: {:?}", bg_information); + eprintln!("Successfully parsed background info: {bg_information:?}"); update_file_if_needed(data_dir, client, &bg_information.background_file, "background").await?; update_file_if_needed(data_dir, client, &bg_information.slogan, "splash").await?; @@ -168,7 +168,7 @@ async fn update_file_if_needed(dir: &Path, client: &Client, file_url: &str, file let mut current_file = None; for path in dir.read_dir().unwrap() { let path = path.unwrap(); - if path.file_name().into_string().unwrap().starts_with(format!("{}_", file_type).as_str()) { + if path.file_name().into_string().unwrap().starts_with(format!("{file_type}_").as_str()) { current_file = Some(path); } } @@ -176,15 +176,15 @@ async fn update_file_if_needed(dir: &Path, client: &Client, file_url: &str, file }; let filename = extract_filename_from_url(file_url); - let expected_file = format!("{}_{}", file_type, filename); + let expected_file = format!("{file_type}_{filename}"); let file_path = dir.join(&expected_file); if let Some(file) = current_file { - if filename != file.file_name().into_string().unwrap().strip_prefix(format!("{}_", file_type).as_str()).unwrap() { + if filename == file.file_name().into_string().unwrap().strip_prefix(format!("{file_type}_").as_str()).unwrap() { + eprintln!("{} file already exists at: {}", file_type, file.path().display()); + } else { update_file(client, file_path, file_type, file_url).await?; remove_file(file.path()).unwrap(); - } else { - eprintln!("{} file already exists at: {}", file_type, file.path().display()); } } else { update_file(client, file_path, file_type, file_url).await?; @@ -194,15 +194,15 @@ async fn update_file_if_needed(dir: &Path, client: &Client, file_url: &str, file } async fn update_file(client: &Client, file_path: PathBuf, file_type: &str, file_url: &str) -> Result<(), String> { - eprintln!("Downloading {} file from: {}", file_type, file_url); + eprintln!("Downloading {file_type} file from: {file_url}"); let file_bytes = client .get(file_url) .send() .await - .map_err(|e| format!("Failed to send request for {} file: {}", file_type, e))? + .map_err(|e| format!("Failed to send request for {file_type} file: {e}"))? .bytes() .await - .map_err(|e| format!("Failed to get bytes for {} file: {}", file_type, e))?; + .map_err(|e| format!("Failed to get bytes for {file_type} file: {e}"))?; let mut file = OpenOptions::new() .write(true) @@ -212,10 +212,10 @@ async fn update_file(client: &Client, file_path: PathBuf, file_type: &str, file_ .map_err(|e| format!("Failed to create {} file ({}): {}", file_type, file_path.display(), e))?; file.write_all(&file_bytes) - .map_err(|e| format!("Failed to write {} file: {}", file_type, e))?; + .map_err(|e| format!("Failed to write {file_type} file: {e}"))?; file.flush() - .map_err(|e| format!("Failed to flush {} file: {}", file_type, e))?; + .map_err(|e| format!("Failed to flush {file_type} file: {e}"))?; eprintln!("Successfully downloaded {} file to: {}", file_type, file_path.display()); @@ -225,13 +225,13 @@ async fn update_file(client: &Client, file_path: PathBuf, file_type: &str, file_ async fn refresh_hoyo_install(proj_dirs: &ProjectDirs, client: &Client) -> Result<(), String> { let hoyo_url = "https://sg-hyp-api.hoyoverse.com/hyp/hyp-connect/api/getGames?launcher_id=VYTpXlbWo8&language=en-us"; - eprintln!("Fetching Hoyo launcher info from: {}", hoyo_url); + eprintln!("Fetching Hoyo launcher info from: {hoyo_url}"); let response = client .get(hoyo_url) .send() .await - .map_err(|e| format!("Failed to fetch Hoyo launcher info: {}", e))?; + .map_err(|e| format!("Failed to fetch Hoyo launcher info: {e}"))?; if !response.status().is_success() { return Err(format!("Hoyo launcher info request failed with status {}", response.status())); @@ -240,7 +240,7 @@ async fn refresh_hoyo_install(proj_dirs: &ProjectDirs, client: &Client) -> Resul let hoyo_launcher_info: HoyoLauncherInformation = response .json() .await - .map_err(|e| format!("Failed to parse Hoyo launcher info: {}", e))?; + .map_err(|e| format!("Failed to parse Hoyo launcher info: {e}"))?; eprintln!("Successfully parsed Hoyo launcher info"); @@ -255,11 +255,11 @@ async fn refresh_hoyo_install(proj_dirs: &ProjectDirs, client: &Client) -> Resul } }; - let data_dir_buf = proj_dirs.data_dir().join(format!("hoyoverse/{}", game_abbreviation)); + let data_dir_buf = proj_dirs.data_dir().join(format!("hoyoverse\\{game_abbreviation}")); let data_dir = data_dir_buf.as_path(); create_dir_all(data_dir) - .map_err(|e| format!("Failed to create directory for {}: {}", game_abbreviation, e))?; + .map_err(|e| format!("Failed to create directory for {game_abbreviation}: {e}"))?; eprintln!("Processing game: {} ({})", game.display.name, game_abbreviation); @@ -278,4 +278,13 @@ fn extract_filename_from_url(url: &str) -> String { Some(filename) => String::from(filename), None => String::from("unknown_file"), } +} + +pub async fn install_game(game: PossibleGames) -> Result<(), String> { + match game { + PossibleGames::WutheringWaves => { + crate::utils::wuthering_waves_installer::install_wuthering_waves().await + } + _ => Ok(()) + } } \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index 089d4ca..060646f 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1 +1,2 @@ -pub mod installer; \ No newline at end of file +pub mod installer; +pub mod settings; \ No newline at end of file diff --git a/src/components/settings.rs b/src/components/settings.rs new file mode 100644 index 0000000..b26e8c2 --- /dev/null +++ b/src/components/settings.rs @@ -0,0 +1,52 @@ +use iced::{Element, Length}; +use iced::widget::{Button, TextInput, Container, Column, Row, text}; + +#[derive(Debug, Clone)] +pub enum SettingsMessage { + GamePathChanged(String), + Save, + Cancel, +} + +#[derive(Debug, Clone, Default)] +pub struct SettingsModal { + pub game_path: String, + pub is_open: bool, +} + +impl SettingsModal { + pub fn new(game_path: String) -> Self { + Self { + game_path, + is_open: false, + } + } + + pub fn view(&self) -> Element<'_, SettingsMessage> { + if !self.is_open { + return Container::new(text("")).into(); + } + let input = TextInput::new( + "Enter game path...", + &self.game_path, + ) + .on_input(SettingsMessage::GamePathChanged) + .width(Length::Fill); + + let save = Button::new(text("Save")).on_press(SettingsMessage::Save); + let cancel = Button::new(text("Cancel")).on_press(SettingsMessage::Cancel); + + let content = Column::new() + .push(text("Settings").size(30)) + .push(input) + .push(Row::new().push(save).push(cancel).spacing(10)) + .spacing(20); + + Container::new(content) + .width(Length::Fixed(400.0)) + .height(Length::Fixed(200.0)) + .padding(20) + .center(Length::Fill) + .into() + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1b89b46..d488148 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ // #![windows_subsystem = "windows"] mod utils; mod components; - -use components::installer::refresh_install; +use components::settings::{SettingsModal, SettingsMessage}; +use components::installer::{install_game, refresh_install}; use directories::ProjectDirs; use ::image::{DynamicImage, ImageReader}; use iced::{ @@ -20,32 +20,27 @@ use std::{ pub const ORGANIZATION: &str = "ReversedRooms"; pub const PRODUCT: &str = "RoomsLauncher"; +// Trait for converting DynamicImage to image::Handle trait InterfaceImage { fn into_handle(self) -> image::Handle; } impl InterfaceImage for DynamicImage { fn into_handle(self) -> image::Handle { - // todo: figure out why images which are blurred are made slightly transparent. maybe even use this to my advantange. image::Handle::from_rgba(self.width(), self.height(), self.to_rgba8().into_raw()) } } pub fn main() -> iced::Result { - let segoe_assets = include_bytes!("../resources/segoe-mdl2-assets.ttf"); - let main_font = include_bytes!("../resources/QuodlibetSans-Regular.ttf"); - let icon_file = include_bytes!("../resources/icon.png"); - - let icon_image = ImageReader::new(Cursor::new(icon_file)) + let icon_image = ImageReader::new(Cursor::new(include_bytes!("../resources/icon.png"))) .with_guessed_format() .unwrap() .decode() .unwrap(); - let rgba_vec = icon_image.as_rgba8().unwrap().to_vec(); let settings = Settings { decorations: false, - icon: Some(icon::from_rgba(rgba_vec, icon_image.width(), icon_image.height()).unwrap()), + icon: Some(icon::from_rgba(icon_image.as_rgba8().unwrap().to_vec(), icon_image.width(), icon_image.height()).unwrap()), size: Size::new(0.0, 0.0), maximized: false, fullscreen: false, @@ -64,8 +59,8 @@ pub fn main() -> iced::Result { .subscription(Launcher::subscription) .title(Launcher::title) .window(settings) - .font(segoe_assets) - .font(main_font) + .font(include_bytes!("../resources/segoe-mdl2-assets.ttf")) + .font(include_bytes!("../resources/QuodlibetSans-Regular.ttf")) .window_size((1280.0, 720.0)) .run() } @@ -79,17 +74,11 @@ enum PossibleGames { GenshinImpact, } -trait IdentifibleGameType { - fn get_game_preferred_size(&self) -> (u32, u32); -} - -impl IdentifibleGameType for PossibleGames { - fn get_game_preferred_size(&self) -> (u32, u32) { +impl PossibleGames { + fn get_preferred_size(&self) -> (u32, u32) { match self { - PossibleGames::WutheringWaves => (1280, 760), - PossibleGames::HonkaiStarRail => (1280, 720), - PossibleGames::ZenlessZoneZero => (1280, 720), - PossibleGames::GenshinImpact => (1280, 720), + Self::WutheringWaves => (1280, 760), + _ => (1280, 720), } } } @@ -105,41 +94,61 @@ struct VideoBackground { first_frame: DynamicImage } +impl Clone for VideoBackground { + fn clone(&self) -> Self { + Self { + video: None, // Do not clone the video field + first_frame: self.first_frame.clone(), + } + } +} + #[derive(Debug)] enum LauncherBackground { Video(VideoBackground), Image(DynamicImage), } +impl Clone for LauncherBackground { + fn clone(&self) -> Self { + match self { + Self::Video(vb) => Self::Video(vb.clone()), + Self::Image(img) => Self::Image(img.clone()), + } + } +} + #[derive(Debug, Default)] struct State { selected_game: PossibleGames, installed_games: Vec, installed_game_servers: Vec, db_software_installed: bool, + downloaded_games: Vec, background: Option, splash_images: HashMap, - icon_images: HashMap + icon_images: HashMap, + notification: Option, + is_installing: bool, + game_path: String, + settings_modal: SettingsModal, } impl State { - fn get_background_element(&self) -> Option> { - if let Some(background) = &self.background { - match background { - LauncherBackground::Video(video_object) => { - if let Some(video) = &video_object.video { - Some(VideoPlayer::new(video).into()) - } else { - Some(image(video_object.first_frame.clone().into_handle()).into()) - } - }, - LauncherBackground::Image(img) => Some(image(img.clone().into_handle()).into()), - } - } else { - None - } + fn get_background_element(&'_ self) -> Option> { + self.background.as_ref().map(|bg| match bg { + LauncherBackground::Video(video_object) => { + if let Some(video) = &video_object.video { + VideoPlayer::new(video).into() + } else { + image(video_object.first_frame.clone().into_handle()).into() + } + }, + LauncherBackground::Image(img) => image(img.clone().into_handle()).into(), + }) } - fn get_game_icon_row(&self) -> Element { + + fn get_game_icon_row(&'_ self) -> Element<'_, Message> { container(row![ self.create_game_icon(&PossibleGames::WutheringWaves), self.create_game_icon(&PossibleGames::HonkaiStarRail), @@ -149,9 +158,11 @@ impl State { .spacing(10) ).align_x(Center).width(Length::Fill).into() } - fn create_game_icon(&self, game: &PossibleGames) -> Element { + + fn create_game_icon(&self, game: &PossibleGames) -> Element<'_, Message> { if let Some(icon) = self.icon_images.get(game) { - let img = if &self.selected_game == game { + let is_selected = &self.selected_game == game; + let img = if is_selected { image(icon.clone().into_handle()) .width(Length::Fixed(52.0)) .height(Length::Fixed(68.0)) @@ -161,54 +172,75 @@ impl State { .height(Length::Fixed(64.0)) }; - if game != &self.selected_game { - mouse_area( - img - ) - .on_press(Message::GameSelected(Some(game.clone()))) - .interaction(Interaction::Pointer) - .into() - } else { + if is_selected { opaque(img) + } else { + mouse_area(img) + .on_press(Message::GameSelected(Some(game.clone()))) + .interaction(Interaction::Pointer) + .into() } } else { text("loading").size(10).into() } } - fn get_splash(&self) -> Option> { - if let Some(splash) = self.splash_images.get(&self.selected_game) { - let preferred_size = self.selected_game.get_game_preferred_size(); - Some(image(splash.clone().into_handle()) - .width(Length::Fixed(preferred_size.0 as f32)) - .height(Length::Fixed(preferred_size.1 as f32)).into()) - } else { - None + + fn get_splash(&self) -> Option> { + self.splash_images.get(&self.selected_game).map(|splash| { + let (width, height) = self.selected_game.get_preferred_size(); + image(splash.clone().into_handle()) + .width(Length::Fixed(width as f32)) + .height(Length::Fixed(height as f32)) + .into() + }) + } + + fn is_game_downloaded(&self, game: &PossibleGames) -> bool { + self.downloaded_games.contains(game) + } + + fn mark_game_as_downloaded(&mut self, game: &PossibleGames) { + if !self.downloaded_games.contains(game) { + self.downloaded_games.push(game.clone()); } } - fn path() -> PathBuf { - let project_dirs = ProjectDirs::from("com", ORGANIZATION, PRODUCT).unwrap(); - let path = project_dirs.data_dir(); - - path.join("launcher-state.json").to_path_buf() + fn start_install(&mut self) { + self.is_installing = true; + } + fn finish_install(&mut self) { + self.is_installing = false; } - fn load(self) -> Result { - if let Ok(contents) = read_to_string(Self::path()) { - let saved_state: SavedState = serde_json::from_str(&contents).map_err(|_| LoadError::Format)?; + fn path() -> PathBuf { + ProjectDirs::from("com", ORGANIZATION, PRODUCT) + .unwrap() + .data_dir() + .join("launcher-state.json") + } - Ok(State { + fn load(self) -> Result> { + let background = self.background.clone(); + let splash_images = self.splash_images.clone(); + let icon_images = self.icon_images.clone(); + Ok(read_to_string(Self::path()) + .ok() + .and_then(|contents| serde_json::from_str::(&contents).ok()) + .map(|saved_state| Self { selected_game: saved_state.selected_game, installed_games: saved_state.installed_games, installed_game_servers: saved_state.installed_game_servers, db_software_installed: saved_state.db_software_installed, - background: self.background, - splash_images: self.splash_images, - icon_images: self.icon_images, + downloaded_games: saved_state.downloaded_games, + background, + splash_images, + icon_images, + notification: saved_state.notification, + is_installing: saved_state.is_installing, + game_path: saved_state.game_path.clone(), + settings_modal: SettingsModal::new(saved_state.game_path), }) - } else { - Ok(self) - } + .unwrap_or(self)) } fn save(&mut self) -> Result<(), SaveError> { @@ -217,22 +249,48 @@ impl State { installed_games: self.installed_games.clone(), installed_game_servers: self.installed_game_servers.clone(), db_software_installed: self.db_software_installed, + downloaded_games: self.downloaded_games.clone(), + notification: self.notification.clone(), + is_installing: self.is_installing, + game_path: self.game_path.clone(), }; - let json = serde_json::to_string_pretty(&saved_state).map_err(|_| SaveError::Format)?; - let path = Self::path(); - if let Some(dir) = path.parent() { create_dir_all(dir).map_err(|_| SaveError::Write)?; } + let json = serde_json::to_string_pretty(&saved_state).map_err(|_| SaveError::Format)?; let mut file = fs::File::create(path).map_err(|_| SaveError::Write)?; file.write_all(json.as_bytes()).map_err(|_| SaveError::Write)?; file.flush().map_err(|_| SaveError::Write)?; Ok(()) } + + fn show_notification(&mut self, msg: &str) { + self.notification = Some(msg.to_string()); + } + + fn dismiss_notification(&mut self) { + self.notification = None; + } + + fn update_settings(&mut self, msg: SettingsMessage) { + match msg { + SettingsMessage::GamePathChanged(path) => { + self.game_path = path.clone(); + self.settings_modal.game_path = path; + } + SettingsMessage::Save => { + self.settings_modal.is_open = false; + let _ = self.save(); + } + SettingsMessage::Cancel => { + self.settings_modal.is_open = false; + } + } + } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -241,17 +299,10 @@ struct SavedState { installed_games: Vec, installed_game_servers: Vec, db_software_installed: bool, -} - -impl From for Box { - fn from(val: SavedState) -> Self { - Box::new(State { selected_game: val.selected_game, installed_games: val.installed_games, installed_game_servers: val.installed_game_servers, db_software_installed: val.db_software_installed, ..State::default() }) - } -} - -#[derive(Debug, Clone)] -enum LoadError { - Format, + downloaded_games: Vec, + notification: Option, + is_installing: bool, + game_path: String, } #[derive(Debug, Clone)] @@ -267,45 +318,39 @@ enum Message { LoadIcons(HashMap), LoadSplashes(HashMap), LoadVideo(()), - RefreshInstall(Result<(), String>), + RefreshInstall, GameSelected(Option), + DownloadOrStart, + InstallGameResult(Result<(), String>), Close, - Minimize + Minimize, + DismissNotification, + OpenSettings, + Settings(SettingsMessage), } async fn get_icons() -> HashMap { - let mut icons: HashMap = HashMap::new(); - for game in PossibleGames::iter() { - let icon = get_game_icon_dynamic_image(&game); - icons.insert(game, icon); - }; - icons + PossibleGames::iter() + .map(|game| (game.clone(), get_game_icon_dynamic_image(&game))) + .collect() } async fn get_splashes() -> HashMap { - let mut splashes: HashMap = HashMap::new(); - for game in PossibleGames::iter() { - let splash = get_game_splash_dynamic_image(&game); - if let Some(splash) = splash {splashes.insert(game, splash)} else {continue}; - }; - splashes + PossibleGames::iter() + .filter_map(|game| get_game_splash_dynamic_image(&game).map(|splash| (game, splash))) + .collect() } -async fn empty() {} -async fn empty_option() -> Option {None} - impl Launcher { fn boot() -> (Self, Task) { ( Self::Loaded(Box::new(State::default().load().unwrap())), - { - let refresh_task = Task::perform(refresh_install(), Message::RefreshInstall); - refresh_task.chain(Task::batch([ - Task::perform(empty_option(), Message::GameSelected), + Task::perform(refresh_install(), |_| Message::RefreshInstall) + .chain(Task::batch([ + Task::perform(async { None:: }, Message::GameSelected), Task::perform(get_icons(), Message::LoadIcons), Task::perform(get_splashes(), Message::LoadSplashes), ])) - } ) } @@ -315,74 +360,96 @@ impl Launcher { fn update(&mut self, message: Message) -> Task { match self { - Launcher::Loaded(state) => { + Self::Loaded(state) => { match message { Message::DragWindow => { - window::get_latest().and_then(move |id: window::Id| { - window::drag(id) - }) + window::get_latest().and_then(move |id: window::Id| {window::drag(id)}) } Message::Close => { state.save().unwrap(); - window::get_latest().and_then(move |id: window::Id| { - window::close(id) - }) + window::get_latest().and_then(window::close) }, Message::Minimize => { - window::get_latest().and_then(move |id: window::Id| { - window::minimize(id, true) - }) + window::get_latest().and_then(|id| window::minimize(id, true)) }, - Message::EventOccurred(event) => { - if let Event::Window(window::Event::CloseRequested) = event { - state.save().unwrap(); - println!("hm"); - window::get_latest().and_then(window::close) - } else { - Task::none() - } + Message::EventOccurred(Event::Window(window::Event::CloseRequested)) => { + state.save().unwrap(); + window::get_latest().and_then(window::close) }, + Message::EventOccurred(_) => Task::none(), Message::LoadSplashes(splashes) => { state.splash_images = splashes; Task::none() }, Message::LoadVideo(()) => { - if let Ok(background) = get_game_background(&state.selected_game, true) { - state.background = Some(background); - - } else { - state.background = None - } - + state.background = get_game_background(&state.selected_game, true).ok(); Task::none() }, Message::LoadIcons(icons) => { state.icon_images = icons; Task::none() }, - Message::RefreshInstall(_result) => { - Task::none() - }, + Message::RefreshInstall => Task::none(), Message::GameSelected(game) => { if let Some(game) = game { state.selected_game = game; } - let (width, height) = state.selected_game.get_game_preferred_size(); - let task = window::get_latest().and_then(move |id: window::Id| { + let (width, height) = state.selected_game.get_preferred_size(); + let resize_task = window::get_latest().and_then(move |id| { window::resize(id, Size { width: width as f32, height: height as f32 }) }); if let Ok(background) = get_game_background(&state.selected_game, false) { if let LauncherBackground::Video(_) = background { state.background = Some(background); - return task.chain(Task::perform(empty(), Message::LoadVideo)); + return resize_task.chain(Task::perform(async {}, Message::LoadVideo)); } - state.background = Some(background) + state.background = Some(background); } else { - state.background = None + state.background = None; } - task + resize_task + }, + Message::DownloadOrStart => { + let selected_game = state.selected_game.clone(); + if state.is_game_downloaded(&selected_game) { + println!("Starting game: {selected_game:?}"); + // TODO: Add actual game starting logic here + Task::none() + } else { + println!("Downloading game: {selected_game:?}"); + state.start_install(); + Task::perform(install_game(selected_game), Message::InstallGameResult) + } + }, + Message::InstallGameResult(result) => { + state.finish_install(); + match result { + Ok(()) => { + let selected_game = state.selected_game.clone(); + state.mark_game_as_downloaded(&selected_game); + if selected_game == PossibleGames::WutheringWaves { + state.show_notification("All dependencies for Wuthering Waves were installed successfully."); + } + } + Err(e) => { + state.show_notification(&format!("Failed to install game: {e}")); + } + } + Task::none() + }, + Message::DismissNotification => { + state.dismiss_notification(); + Task::none() + }, + Message::OpenSettings => { + state.settings_modal.is_open = true; + Task::none() + }, + Message::Settings(msg) => { + state.update_settings(msg); + Task::none() }, } } @@ -390,15 +457,16 @@ impl Launcher { } fn subscription(&self) -> Subscription { - event::listen_with(|event, _status, _| { - match event { - Event::Window(window::Event::CloseRequested) => Some(Message::EventOccurred(event)), - _ => None, + event::listen_with(|event, _, _| { + if event == Event::Window(window::Event::CloseRequested) { + Some(Message::EventOccurred(event)) + } else { + None } }) } - fn view(&self) -> Element { + fn view(&'_ self) -> Element<'_, Message> { let mut font = Font::with_name("Quodlibet Sans"); font.weight = Weight::Normal; font.stretch = Stretch::Normal; @@ -406,9 +474,9 @@ impl Launcher { let mut bolded_font = Font::with_name("Quodlibet Sans"); bolded_font.weight = Weight::Bold; bolded_font.stretch = Stretch::Normal; - println!("Rerender triggered!"); + match self { - Launcher::Loaded(state) => { + Self::Loaded(state) => { let top_bar = stack![ mouse_area(container( stack![ @@ -437,10 +505,38 @@ impl Launcher { .interaction(Interaction::Move) ]; + let button_text = if state.is_game_downloaded(&state.selected_game) { + "Start" + } else if state.is_installing { + "Installing..." + } else { + "Download" + }; + + let download_button = button(text(button_text).size(25).font(bolded_font).align_x(Center)) + .padding(Padding { top: 10.0, right: 70.0, bottom: 10.0, left: 70.0 }) + .style(|_, _| button::Style { + text_color: Color::from_rgba8(0, 0, 0, 1.0), + background: Some(Color::from_rgba8(255, 255, 255, 1.0).into()), + border: border::rounded(5), + ..button::Style::default() + }) + .on_press(Message::DownloadOrStart); + + let settings_button = button(text("Settings").size(25).font(bolded_font).align_x(Center)) + .padding(Padding { top: 10.0, right: 30.0, bottom: 10.0, left: 30.0 }) + .style(|_, _| button::Style { + text_color: Color::from_rgba8(0, 0, 0, 1.0), + background: Some(Color::from_rgba8(255, 255, 255, 1.0).into()), + border: border::rounded(5), + ..button::Style::default() + }) + .on_press(Message::OpenSettings); + let bottom_bar = container(row![ text("The quick brown fox jumped over the lazy dog.").size(25).font(font), Space::new(Length::Fill, Length::Fixed(0.0)), - opaque(mouse_area(button(text("Start").size(25).font(bolded_font).align_x(Center)) + opaque(mouse_area(download_button .padding(Padding { top: 10.0, right: 70.0, bottom: 10.0, left: 70.0 }) .style(move |_, _| { button::Style { @@ -450,8 +546,10 @@ impl Launcher { ..button::Style::default() } })) - .interaction(Interaction::Pointer) - ) + .interaction(Interaction::Pointer)), + Space::new(Length::Fixed(20.0), Length::Fixed(0.0)), + opaque(mouse_area(settings_button) + .interaction(Interaction::Pointer)), ]) .align_y(Bottom) .width(Length::Fill) @@ -476,6 +574,31 @@ impl Launcher { final_stack = final_stack.push(user_area); + if let Some(ref msg) = state.notification { + let msgbox = container( + column![ + text(msg).size(22), + button("OK").on_press(Message::DismissNotification) + ] + .spacing(20) + .padding(20) + ) + .width(Length::Fixed(400.0)) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_| container::Style { + background: Some(Color::from_rgba8(0,0,0,0.85).into()), + border: border::rounded(10), + ..container::Style::default() + }); + final_stack = final_stack.push(center(msgbox)); + } + + if state.settings_modal.is_open { + let modal_elem = state.settings_modal.view().map(Message::Settings); + final_stack = final_stack.push(modal_elem); + } + final_stack.into() } } diff --git a/src/utils/img_utils.rs b/src/utils/img_utils.rs index 1efd616..461325f 100644 --- a/src/utils/img_utils.rs +++ b/src/utils/img_utils.rs @@ -9,22 +9,22 @@ pub fn is_in_rounded_rect(x: u32, y: u32, width: u32, height: u32, radius: f32) if x < radius && y < radius { let dx = x - radius; let dy = y - radius; - let distance = (dx.powi(2) + dy.powi(2)).sqrt(); + let distance = dx.hypot(dy); return distance <= radius; } else if x > width - radius && y < radius { let dx = x - (width - radius); let dy = y - radius; - let distance = (dx.powi(2) + dy.powi(2)).sqrt(); + let distance = dx.hypot(dy); return distance <= radius; } else if x < radius && y > height - radius { let dx = x - radius; let dy = y - (height - radius); - let distance = (dx.powi(2) + dy.powi(2)).sqrt(); + let distance = dx.hypot(dy); return distance <= radius; } else if x > width - radius && y > height - radius { let dx = x - (width - radius); let dy = y - (height - radius); - let distance = (dx.powi(2) + dy.powi(2)).sqrt(); + let distance = dx.hypot(dy); return distance <= radius; } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 6013471..5ebf445 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod img_utils; -pub mod visual_helper; \ No newline at end of file +pub mod visual_helper; +pub mod wuthering_waves_installer; \ No newline at end of file diff --git a/src/utils/visual_helper.rs b/src/utils/visual_helper.rs index 11d9a01..6a97c03 100644 --- a/src/utils/visual_helper.rs +++ b/src/utils/visual_helper.rs @@ -15,11 +15,7 @@ pub fn get_game_background(game: &PossibleGames, make_video: bool) -> Result Result { - let video_object = VideoBackground { - first_frame, - video: Some(video) - }; - - Ok(LauncherBackground::Video(video_object)) - }, - Err(err) => { - eprintln!("{:#?}", err); - Ok(LauncherBackground::Video(VideoBackground { - first_frame, - video: None - })) - }, - } + + let video = Video::new(&url::Url::from_file_path(background_path).unwrap()) + .map_err(|err| { + eprintln!("{err:#?}"); + std::io::Error::other("Video creation failed") + })?; + + Ok(LauncherBackground::Video(VideoBackground { + first_frame, + video: Some(video) + })) } else { - let img = ImageReader::new(Cursor::new(std::fs::read(background_path)?)) - .with_guessed_format() - .unwrap() - .decode() - .unwrap(); + let img = load_image_from_path(&background_path)?; Ok(LauncherBackground::Image(img)) } } fn get_asset_file(proj_dirs: &ProjectDirs, game: &PossibleGames, identifier: &str) -> Result { let game_dir = match game { - PossibleGames::WutheringWaves => proj_dirs.data_dir().join("kuro/wuwa"), - PossibleGames::ZenlessZoneZero => proj_dirs.data_dir().join("hoyoverse/zzz"), - PossibleGames::HonkaiStarRail => proj_dirs.data_dir().join("hoyoverse/hsr"), - PossibleGames::GenshinImpact => proj_dirs.data_dir().join("hoyoverse/gi"), + PossibleGames::WutheringWaves => proj_dirs.data_dir().join("kurogames\\wuwa"), + PossibleGames::ZenlessZoneZero => proj_dirs.data_dir().join("hoyoverse\\zzz"), + PossibleGames::HonkaiStarRail => proj_dirs.data_dir().join("hoyoverse\\hsr"), + PossibleGames::GenshinImpact => proj_dirs.data_dir().join("hoyoverse\\gi"), }; - if create_dir_all(&game_dir).is_err() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Game directory does not exist: {:?}", game_dir) - )); - } - - let entries = std::fs::read_dir(&game_dir)?; - - for entry in entries { - let entry = entry?; - let file_name = entry.file_name().into_string().unwrap_or_default(); - - if file_name.starts_with(format!("{}_", identifier).as_str()) { - return Ok(entry.path()); - } - } - - Err(std::io::Error::new( + create_dir_all(&game_dir).map_err(|_| std::io::Error::new( std::io::ErrorKind::NotFound, - format!("No background file found in {:?}", game_dir) - )) + format!("Failed to create game directory: {game_dir:?}") + ))?; + + std::fs::read_dir(&game_dir)? + .filter_map(std::result::Result::ok) + .find(|entry| { + entry.file_name() + .to_string_lossy() + .starts_with(&format!("{identifier}_")) + }) + .map(|entry| entry.path()) + .ok_or_else(|| std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("No {identifier} file found in {game_dir:?}") + )) +} + +fn load_image_from_path(path: &PathBuf) -> Result { + let data = std::fs::read(path)?; + let cursor = Cursor::new(data); + + ImageReader::new(cursor) + .with_guessed_format() + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid image format"))? + .decode() + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to decode image")) } pub fn get_game_splash_dynamic_image(game: &PossibleGames) -> Option { let proj_dirs = ProjectDirs::from("com", ORGANIZATION, PRODUCT).unwrap(); - let file_path = get_asset_file(&proj_dirs, game, "splash"); - - if let Ok(path) = file_path { - let data_cursor = Cursor::new(std::fs::read(path).unwrap()); - - Some(ImageReader::new(data_cursor) - .with_guessed_format() - .unwrap() - .decode() - .unwrap()) - } else {None} + get_asset_file(&proj_dirs, game, "splash") + .and_then(|path| load_image_from_path(&path)) + .ok() } pub fn get_game_icon_dynamic_image(game: &PossibleGames) -> DynamicImage { - let proj_dirs = ProjectDirs::from("com", ORGANIZATION, PRODUCT).unwrap(); - let file_data: &[u8] = match game { - PossibleGames::WutheringWaves => include_bytes!("../../resources/wutheringwaves-icon.png"), - PossibleGames::ZenlessZoneZero => &std::fs::read(get_asset_file(&proj_dirs, game, "icon").unwrap()).unwrap(), - PossibleGames::HonkaiStarRail => &std::fs::read(get_asset_file(&proj_dirs, game, "icon").unwrap()).unwrap(), - PossibleGames::GenshinImpact => &std::fs::read(get_asset_file(&proj_dirs, game, "icon").unwrap()).unwrap(), + let file_data: &[u8] = if game == &PossibleGames::WutheringWaves { include_bytes!("../../resources/wutheringwaves-icon.png") } else { + let proj_dirs = ProjectDirs::from("com", ORGANIZATION, PRODUCT).unwrap(); + &std::fs::read(get_asset_file(&proj_dirs, game, "icon").unwrap()).unwrap() }; - let data_cursor = Cursor::new(file_data); - - let img = ImageReader::new(data_cursor) - .with_guessed_format() - .unwrap() - .decode() - .unwrap() - .resize(128, 128, ::image::imageops::FilterType::Lanczos3); - - round_image(img) + let cursor = Cursor::new(file_data); + let img = ImageReader::new(cursor) + .with_guessed_format() .unwrap() + .decode() + .unwrap() + .resize(128, 128, ::image::imageops::FilterType::Lanczos3); + + round_image(img).unwrap() } fn rad(deg: f32) -> f32 { - deg * std::f32::consts::PI / 180.0 + deg.to_radians() } pub fn style_container(direction: f32, use_gradient: bool) -> container::Style { - let angle = rad(direction); - let gradient: Option = if use_gradient { - Some(gradient::Linear::new(angle) - .add_stop(0.0, Color::from_rgba8(0, 0, 0, 0.0)) - .add_stop(0.75, Color::from_rgba8(0, 0, 0, 0.75)).into()) - } else {None}; + let background = if use_gradient { + Some(gradient::Linear::new(rad(direction)) + .add_stop(0.0, Color::from_rgba8(0, 0, 0, 0.0)) + .add_stop(0.75, Color::from_rgba8(0, 0, 0, 0.75)) + .into()) + } else { + None + }; + container::Style { text_color: Color::from_rgba8(255, 255, 255, 1.0).into(), - background: gradient, + background, ..container::Style::default() } } \ No newline at end of file diff --git a/src/utils/wuthering_waves_installer.rs b/src/utils/wuthering_waves_installer.rs new file mode 100644 index 0000000..cd62227 --- /dev/null +++ b/src/utils/wuthering_waves_installer.rs @@ -0,0 +1,183 @@ +use std::env; +use std::fs; +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; + +use reqwest::Client; +use directories::ProjectDirs; + +const POSTGRES_URL: &str = "https://sbp.enterprisedb.com/getfile.jsp?fileid=1259603"; +const GIT_URL: &str = "https://github.com/git-for-windows/git/releases/download/v2.50.1.windows.1/Git-2.50.1-64-bit.exe"; +const PROTOC_RELEASES_API: &str = "https://api.github.com/repos/protocolbuffers/protobuf/releases/latest"; +const PROTOC_ASSET_KEYWORD: &str = "protoc-31.1-win64.zip"; +const WICKED_WAIFUS_REPO: &str = "https://git.xeondev.com/wickedwaifus/wicked-waifus-rs.git"; +const RUSTUP_URL: &str = "https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe"; + +/// Steps: +/// 0. Download and install Rust +/// 1. Download and install `PostgreSQL` 16.9 (user handles admin prompt) +/// 2. Download and install Protoc, add bin to PATH +/// 3. Download and install Git +/// 4. Clone wicked-waifus-rs repo recursively +/// 5. Build all required binaries in sequence +pub async fn install_wuthering_waves() -> Result<(), String> { + let client = Client::new(); + let proj_dirs = ProjectDirs::from("com", "ReversedRooms", "RoomsLauncher").ok_or("Could not get ProjectDirs")?; + let deps_dir = proj_dirs.data_dir().join("kurogames\\wuwa\\dependencies"); + std::fs::create_dir_all(&deps_dir).map_err(|e| e.to_string())?; + + // 0. Download and install Rust + let rustup_installer = deps_dir.join("rustup-init.exe"); + download_file(&client, RUSTUP_URL, &rustup_installer).await?; + let mut rustup_cmd = Command::new(&rustup_installer); + rustup_cmd.arg("-y"); + let mut child = rustup_cmd.spawn().map_err(|e| format!("Failed to run rustup-init: {e}"))?; + let status = child.wait().map_err(|e| format!("Failed to wait for rustup-init: {e}"))?; + if !status.success() { + return Err("rustup-init failed".into()); + } + println!("Rust installed"); + std::fs::remove_file(&rustup_installer).ok(); + + // 1. Download and install PostgreSQL (user handles admin prompt) + let postgres_installer = deps_dir.join("postgresql-installer.exe"); + download_file(&client, POSTGRES_URL, &postgres_installer).await?; + run_installer(&postgres_installer, false)?; // not silent + println!("PostgreSQL installed"); + std::fs::remove_file(&postgres_installer).ok(); + + // 2. Download and install Protoc, add bin to PATH + let protoc_zip = deps_dir.join("protoc.zip"); + let protoc_dir = deps_dir.join("protoc"); + download_latest_protoc(&client, &protoc_zip).await?; + unzip_file(&protoc_zip, &protoc_dir)?; + std::fs::remove_file(&protoc_zip).ok(); + add_bin_to_path(&protoc_dir.join("bin"))?; + println!("Protoc installed"); + + // 3. Download and install Git + let git_installer = deps_dir.join("git-installer.exe"); + download_file(&client, GIT_URL, &git_installer).await?; + run_installer(&git_installer, true)?; // silent + println!("Git installed"); + std::fs::remove_file(&git_installer).ok(); + + // 4. Clone wicked-waifus-rs repo recursively + let repo_dir = deps_dir.join("wicked-waifus-rs"); + if !repo_dir.exists() { + let status = Command::new("git") + .arg("clone") + .arg("--recursive") + .arg(WICKED_WAIFUS_REPO) + .arg(&repo_dir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|e| format!("Failed to run git clone: {e}"))?; + if !status.success() { + return Err("git clone failed".into()); + } + println!("Wicked Waifus repo cloned"); + } + + // 5. Build all required binaries in sequence + let bins = [ + "wicked-waifus-config-server", + "wicked-waifus-login-server", + "wicked-waifus-gateway-server", + "wicked-waifus-game-server", + ]; + for bin in &bins { + let status = Command::new("cargo") + .arg("build") + .arg("--bin") + .arg(bin) + .current_dir(&repo_dir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|e| format!("Failed to run cargo build for {bin}: {e}"))?; + if !status.success() { + return Err(format!("cargo build failed for {bin}")); + } + println!("{bin} built"); + } + + println!("All binaries built"); + + Ok(()) +} + +async fn download_file(client: &Client, url: &str, dest: &Path) -> Result<(), String> { + let resp = client.get(url) + .header("User-Agent", "rooms-launcher") + .send().await + .map_err(|e| format!("Failed to download {url}: {e}"))?; + let bytes = resp.bytes().await.map_err(|e| format!("Failed to read bytes: {e}"))?; + let mut file = fs::File::create(dest).map_err(|e| format!("Failed to create file: {e}"))?; + file.write_all(&bytes).map_err(|e| format!("Failed to write file: {e}"))?; + println!("Downloaded {url} to {}", dest.to_string_lossy()); + Ok(()) +} + +fn run_installer(installer: &Path, silent: bool) -> Result<(), String> { + let mut cmd = Command::new(installer); + if silent { + cmd.arg("/VERYSILENT"); + } + let mut child = cmd.spawn().map_err(|e| format!("Failed to run installer: {e}"))?; + let status = child.wait().map_err(|e| format!("Failed to wait for installer: {e}"))?; + if !status.success() { + return Err(format!("Installer failed: {installer:?}")); + } + println!("{installer:?} ran successfully"); + Ok(()) +} + +async fn download_latest_protoc(client: &Client, dest: &Path) -> Result<(), String> { + let resp = client.get(PROTOC_RELEASES_API) + .header("User-Agent", "rooms-launcher") + .send().await + .map_err(|e| format!("Failed to fetch Protoc releases: {e}"))?; + let json: serde_json::Value = resp.json().await.map_err(|e| format!("Failed to parse Protoc release JSON: {e}"))?; + let assets = json["assets"].as_array().ok_or("No assets in Protoc release JSON")?; + let asset = assets.iter().find(|a| a["name"].as_str().is_some_and(|n| n.contains(PROTOC_ASSET_KEYWORD))) + .ok_or("No zip asset found for Protoc")?; + let url = asset["browser_download_url"].as_str().ok_or("No download URL for Protoc asset")?; + download_file(client, url, dest).await?; + println!("Downloaded Protoc to {}", dest.to_string_lossy()); + Ok(()) +} + +fn unzip_file(zip_path: &Path, dest_dir: &Path) -> Result<(), String> { + let file = fs::File::open(zip_path).map_err(|e| format!("Failed to open zip: {e}"))?; + let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("Failed to read zip: {e}"))?; + fs::create_dir_all(dest_dir).map_err(|e| format!("Failed to create unzip dir: {e}"))?; + for i in 0..archive.len() { + let mut file = archive.by_index(i).map_err(|e| format!("Failed to get file in zip: {e}"))?; + let outpath = dest_dir.join(file.name()); + if file.is_dir() { + fs::create_dir_all(&outpath).map_err(|e| format!("Failed to create dir in zip: {e}"))?; + } else { + if let Some(p) = outpath.parent() { + fs::create_dir_all(p).map_err(|e| format!("Failed to create parent dir: {e}"))?; + } + let mut outfile = fs::File::create(&outpath).map_err(|e| format!("Failed to create file in zip: {e}"))?; + std::io::copy(&mut file, &mut outfile).map_err(|e| format!("Failed to extract file: {e}"))?; + } + } + println!("Unzipped {} to {}", zip_path.to_string_lossy(), dest_dir.to_string_lossy()); + Ok(()) +} + +fn add_bin_to_path(bin_dir: &Path) -> Result<(), String> { + let bin_str = bin_dir.to_str().ok_or("Invalid bin path")?; + let path = env::var("Path").unwrap_or_default(); + let new_path = format!("{bin_str};{path}"); + unsafe { + env::set_var("Path", &new_path); + } + println!("Added {} to Path", bin_dir.to_string_lossy()); + Ok(()) +} \ No newline at end of file