Update dependencies and refactor code for improved functionality and performance

- Added new dependencies including `aes`, `bzip2`, `zip`, and others in `Cargo.lock`.
- Updated existing dependencies to their latest versions.
- Refactored image handling in `video.rs` for better readability and performance.
- Enhanced game installation logic in `installer.rs` to support new game types.
- Introduced settings management in the application state.
- Improved error handling and logging throughout the codebase.
- Cleaned up unused code and optimized existing functions for better efficiency.
This commit is contained in:
Yuhki 2025-07-09 10:27:17 +02:00
parent 37a4172f3c
commit 0ae31d8627
11 changed files with 976 additions and 292 deletions

366
Cargo.lock generated
View file

@ -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",
]

View file

@ -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.

View file

@ -49,7 +49,7 @@ impl Frame {
Self(gst::Sample::builder().build())
}
pub fn readable(&self) -> Option<gst::BufferMap<gst::buffer::Readable>> {
pub fn readable(&self) -> Option<gst::BufferMap<'_, gst::buffer::Readable>> {
self.0.buffer().and_then(|x| x.map_readable().ok())
}
}

View file

@ -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<Client, String> {
.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(())
}
}

View file

@ -1 +1,2 @@
pub mod installer;
pub mod installer;
pub mod settings;

View file

@ -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()
}
}

View file

@ -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<PossibleGames>,
installed_game_servers: Vec<PossibleGames>,
db_software_installed: bool,
downloaded_games: Vec<PossibleGames>,
background: Option<LauncherBackground>,
splash_images: HashMap<PossibleGames, DynamicImage>,
icon_images: HashMap<PossibleGames, DynamicImage>
icon_images: HashMap<PossibleGames, DynamicImage>,
notification: Option<String>,
is_installing: bool,
game_path: String,
settings_modal: SettingsModal,
}
impl State {
fn get_background_element(&self) -> Option<Element<Message>> {
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<Element<'_, Message>> {
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<Message> {
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<Message> {
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<Element<Message>> {
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<Element<'_, Message>> {
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<State, LoadError> {
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<Self, Box<dyn std::error::Error>> {
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::<SavedState>(&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<PossibleGames>,
installed_game_servers: Vec<PossibleGames>,
db_software_installed: bool,
}
impl From<SavedState> for Box<State> {
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<PossibleGames>,
notification: Option<String>,
is_installing: bool,
game_path: String,
}
#[derive(Debug, Clone)]
@ -267,45 +318,39 @@ enum Message {
LoadIcons(HashMap<PossibleGames, DynamicImage>),
LoadSplashes(HashMap<PossibleGames, DynamicImage>),
LoadVideo(()),
RefreshInstall(Result<(), String>),
RefreshInstall,
GameSelected(Option<PossibleGames>),
DownloadOrStart,
InstallGameResult(Result<(), String>),
Close,
Minimize
Minimize,
DismissNotification,
OpenSettings,
Settings(SettingsMessage),
}
async fn get_icons() -> HashMap<PossibleGames, DynamicImage> {
let mut icons: HashMap<PossibleGames, DynamicImage> = 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<PossibleGames, DynamicImage> {
let mut splashes: HashMap<PossibleGames, DynamicImage> = 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<PossibleGames> {None}
impl Launcher {
fn boot() -> (Self, Task<Message>) {
(
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::<PossibleGames> }, 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<Message> {
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<Message> {
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<Message> {
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()
}
}

View file

@ -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;
}

View file

@ -1,2 +1,3 @@
pub mod img_utils;
pub mod visual_helper;
pub mod visual_helper;
pub mod wuthering_waves_installer;

View file

@ -15,11 +15,7 @@ pub fn get_game_background(game: &PossibleGames, make_video: bool) -> Result<Lau
if background_path.extension().unwrap().to_str().unwrap() == "mp4" {
let first_frame_path = get_asset_file(&proj_dirs, game, "first_frame_image")?;
let first_frame = ImageReader::new(Cursor::new(std::fs::read(first_frame_path)?))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
let first_frame = load_image_from_path(&first_frame_path)?;
if !make_video {
return Ok(LauncherBackground::Video(VideoBackground {
@ -27,116 +23,102 @@ pub fn get_game_background(game: &PossibleGames, make_video: bool) -> Result<Lau
video: None
}));
}
match Video::new(&url::Url::from_file_path(background_path).unwrap()) {
Ok(video) => {
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<PathBuf, std::io::Error> {
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<DynamicImage, std::io::Error> {
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<DynamicImage> {
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<iced::Background> = 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()
}
}

View file

@ -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(())
}