diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 16a2db82..bb505709 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -249,6 +249,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -479,6 +485,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -597,6 +609,32 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics" version = "0.25.0" @@ -605,8 +643,19 @@ checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.10.0", "core-foundation 0.10.1", - "core-graphics-types", - "foreign-types", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", "libc", ] @@ -648,6 +697,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -876,6 +944,20 @@ dependencies = [ "objc2", ] +[[package]] +name = "display-info" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba4b5ddb26d674c9cd40b7a747e42658ffe1289843615b838532f660e0e3dd0" +dependencies = [ + "anyhow", + "core-graphics 0.23.2", + "fxhash", + "widestring", + "windows 0.52.0", + "xcb", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -887,6 +969,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -943,6 +1034,12 @@ dependencies = [ "tendril 0.5.0", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -994,6 +1091,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "embed-resource" version = "3.0.6" @@ -1098,6 +1201,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1119,7 +1239,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "memoffset", + "memoffset 0.9.1", "rustc_version", ] @@ -1171,6 +1291,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1178,7 +1307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1192,6 +1321,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1478,6 +1613,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.18.4" @@ -1663,6 +1808,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1717,6 +1873,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -2009,6 +2171,24 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png 0.17.16", + "qoi", + "tiff", +] + [[package]] name = "image" version = "0.25.9" @@ -2079,10 +2259,12 @@ name = "integrated-mod-manager" version = "4.0.0" dependencies = [ "futures-util", + "image 0.24.9", "log", "mime_guess", "once_cell", "reqwest 0.13.1", + "screenshots", "serde", "serde_json", "tauri", @@ -2105,6 +2287,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2191,6 +2384,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -2272,6 +2474,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2308,6 +2516,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" dependencies = [ + "cc", "pkg-config", ] @@ -2321,6 +2530,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.12" @@ -2332,6 +2547,22 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "libwayshot" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896d0e594158b7f5188034836a6c4886492078352c39760786e54f1b796caaea" +dependencies = [ + "image 0.24.9", + "log", + "memmap2", + "nix", + "thiserror 1.0.69", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2434,6 +2665,24 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2548,6 +2797,19 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2604,6 +2866,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.1" @@ -2986,6 +3258,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -3228,7 +3506,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.13.0", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -3267,7 +3545,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", "rustix", "windows-sys 0.61.2", @@ -3387,6 +3665,29 @@ dependencies = [ "psl-types", ] +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + [[package]] name = "pxfm" version = "0.1.27" @@ -3396,6 +3697,33 @@ dependencies = [ "num-traits", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3585,12 +3913,47 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4003,6 +4366,25 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "screenshots" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038df8746dbf7d8b70715d638470db956794e0f3d08608e4197f4053c2da1620" +dependencies = [ + "anyhow", + "core-graphics 0.22.3", + "dbus", + "display-info", + "fxhash", + "image 0.24.9", + "libwayshot", + "percent-encoding", + "widestring", + "windows 0.51.1", + "xcb", +] + [[package]] name = "security-framework" version = "3.5.1" @@ -4582,7 +4964,7 @@ dependencies = [ "bitflags 2.10.0", "block2", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.25.0", "crossbeam-channel", "dbus", "dispatch2", @@ -4607,7 +4989,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -4659,7 +5041,7 @@ dependencies = [ "heck 0.5.0", "http", "http-range", - "image", + "image 0.25.9", "jni", "libc", "log", @@ -4691,7 +5073,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -4895,7 +5277,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows", + "windows 0.61.3", "zbus", ] @@ -5027,7 +5409,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -5052,7 +5434,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -5190,6 +5572,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.47" @@ -5594,7 +5987,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ - "memoffset", + "memoffset 0.9.1", "tempfile", "winapi", ] @@ -5899,6 +6292,80 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b48e27457e8da3b2260ac60d0a94512f5cba36448679f3747c0865b7893ed8" +dependencies = [ + "cc", + "downcast-rs", + "io-lifetimes", + "nix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489c9654770f674fc7e266b3c579f4053d7551df0ceb392f153adb1f9ed06ac8" +dependencies = [ + "bitflags 1.3.2", + "nix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b28101e5ca94f70461a6c2d610f76d85ad223d042dd76585ab23d3422dd9b4d" +dependencies = [ + "bitflags 1.3.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce991093320e4a6a525876e6b629ab24da25f9baef0c2e0080ad173ec89588a" +dependencies = [ + "bitflags 1.3.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b873b257fbc32ec909c0eb80dea312076a67014e65e245f5eb69a6b8ab330e" +dependencies = [ + "proc-macro2", + "quick-xml 0.28.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b2a02ac608e07132978689a6f9bf4214949c85998c247abadd4f4129b1aa06" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -6001,7 +6468,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -6025,10 +6492,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -6075,6 +6554,26 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -6097,6 +6596,24 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -6234,6 +6751,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6285,6 +6811,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6342,6 +6883,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6360,6 +6907,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6378,6 +6931,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6408,6 +6967,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6426,6 +6991,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6444,6 +7015,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6462,6 +7039,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6558,7 +7141,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -6612,6 +7195,17 @@ dependencies = [ "rustix", ] +[[package]] +name = "xcb" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", +] + [[package]] name = "xkeysym" version = "0.2.1" @@ -6800,6 +7394,15 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "5.9.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c6e321ee..341d3a57 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -44,6 +44,8 @@ tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] } # Windows-specific dependencies for hotreload functionality (legacy keybd_event only) [target.'cfg(windows)'.dependencies] +image = "0.24.9" +screenshots = "0.8.10" winapi = { version = "0.3.9", features = [ "winuser", "processthreadsapi", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b61d99a5..63f5b489 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,6 +19,7 @@ use tauri_plugin_tracing::{tracing, Builder as Tracing, LevelFilter, MaxFileSize mod hotreload; mod image_server; mod logger_utils; +mod preview_capture; const PROGRESS_UPDATE_THRESHOLD: u64 = 1024; const BUFFER_SIZE: usize = 8192; @@ -994,7 +995,13 @@ pub fn run() { hotreload::focus_mod_manager_send_f10_return_to_game, hotreload::set_window_target, hotreload::is_game_process_running, - hotreload::check_hotreload_dependencies + hotreload::check_hotreload_dependencies, + preview_capture::enter_preview_capture_overlay, + preview_capture::pause_preview_capture_overlay, + preview_capture::capture_preview_screen_region, + preview_capture::cancel_preview_capture_overlay, + preview_capture::save_mod_preview_stage, + preview_capture::discard_preview_capture_stage ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/preview_capture.rs b/src-tauri/src/preview_capture.rs new file mode 100644 index 00000000..d64cae9d --- /dev/null +++ b/src-tauri/src/preview_capture.rs @@ -0,0 +1,466 @@ +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +const PREVIEW_EXTENSIONS: [&str; 5] = ["png", "jpg", "jpeg", "webp", "gif"]; + +#[derive(Deserialize)] +pub struct ScreenCaptureCrop { + x: i32, + y: i32, + width: u32, + height: u32, +} + +#[derive(Serialize, Clone)] +pub struct CaptureBounds { + left: i32, + top: i32, + width: u32, + height: u32, +} + +#[derive(Clone)] +struct StoredWindowState { + x: i32, + y: i32, + width: u32, + height: u32, +} + +static WINDOW_STATE_BEFORE_CAPTURE: Lazy>> = + Lazy::new(|| Mutex::new(None)); + +fn timestamp() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() +} + +#[cfg(windows)] +fn game_title_fragment(game: &str) -> &'static str { + match game { + "ZZ" => "zenlesszonezero", + "GI" => "genshin impact", + "SR" => "honkai: star rail", + "EF" => "endfield", + _ => "wuthering waves", + } +} + +#[cfg(windows)] +fn find_game_window(game: &str) -> Result { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + use winapi::shared::windef::HWND; + use winapi::um::winuser::{EnumWindows, GetWindowTextW, IsWindowVisible}; + + struct FindData { + target: String, + hwnd: HWND, + } + + unsafe extern "system" fn enum_window(hwnd: HWND, data_ptr: isize) -> i32 { + let data = &mut *(data_ptr as *mut FindData); + if IsWindowVisible(hwnd) == 0 { + return 1; + } + + let mut buffer = [0u16; 512]; + let length = GetWindowTextW(hwnd, buffer.as_mut_ptr(), buffer.len() as i32); + if length <= 0 { + return 1; + } + + let title = OsString::from_wide(&buffer[..length as usize]) + .to_string_lossy() + .to_lowercase(); + if title.contains(&data.target) { + data.hwnd = hwnd; + return 0; + } + 1 + } + + let mut data = FindData { + target: game_title_fragment(game).to_string(), + hwnd: std::ptr::null_mut(), + }; + unsafe { + EnumWindows(Some(enum_window), &mut data as *mut _ as isize); + } + + if data.hwnd.is_null() { + Err("Game window not found. Open the game before starting a capture.".to_string()) + } else { + Ok(data.hwnd) + } +} + +#[cfg(windows)] +fn window_bounds(hwnd: winapi::shared::windef::HWND) -> Result { + use winapi::shared::windef::RECT; + use winapi::um::winuser::GetWindowRect; + + let mut rect = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + unsafe { + if GetWindowRect(hwnd, &mut rect) == 0 { + return Err("Could not read the game window bounds.".to_string()); + } + } + + Ok(CaptureBounds { + left: rect.left, + top: rect.top, + width: (rect.right - rect.left).max(1) as u32, + height: (rect.bottom - rect.top).max(1) as u32, + }) +} + +#[cfg(windows)] +fn screen_for_point(x: i32, y: i32) -> Result { + screenshots::Screen::all() + .map_err(|error| error.to_string())? + .into_iter() + .find(|screen| { + let info = &screen.display_info; + x >= info.x + && x < info.x + info.width as i32 + && y >= info.y + && y < info.y + info.height as i32 + }) + .ok_or_else(|| "No display was found for the selected area.".to_string()) +} + +#[cfg(windows)] +fn restore_main_window(app_handle: &tauri::AppHandle) { + use tauri::Manager; + + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.set_ignore_cursor_events(false); + let _ = window.set_always_on_top(false); + let _ = window.unminimize(); + let _ = window.show(); + if let Ok(mut stored_state) = WINDOW_STATE_BEFORE_CAPTURE.lock() { + if let Some(stored) = stored_state.take() { + let _ = window.set_position(tauri::PhysicalPosition::new(stored.x, stored.y)); + let _ = window.set_size(tauri::PhysicalSize::new(stored.width, stored.height)); + } + } + let _ = window.set_focus(); + } +} + +#[cfg(windows)] +#[tauri::command] +pub fn enter_preview_capture_overlay( + app_handle: tauri::AppHandle, + game: String, +) -> Result { + use tauri::Manager; + use winapi::um::winuser::SetForegroundWindow; + + let game_window = find_game_window(&game)?; + let bounds = window_bounds(game_window)?; + let main_window = app_handle + .get_webview_window("main") + .ok_or_else(|| "Main window not found.".to_string())?; + + if let (Ok(position), Ok(size)) = (main_window.outer_position(), main_window.outer_size()) { + if let Ok(mut stored_state) = WINDOW_STATE_BEFORE_CAPTURE.lock() { + if stored_state.is_none() { + *stored_state = Some(StoredWindowState { + x: position.x, + y: position.y, + width: size.width, + height: size.height, + }); + } + } + } + + unsafe { + SetForegroundWindow(game_window); + } + std::thread::sleep(Duration::from_millis(200)); + + main_window + .set_always_on_top(true) + .map_err(|error| error.to_string())?; + main_window + .set_position(tauri::PhysicalPosition::new(bounds.left, bounds.top)) + .map_err(|error| error.to_string())?; + main_window + .set_size(tauri::PhysicalSize::new(bounds.width, bounds.height)) + .map_err(|error| error.to_string())?; + main_window.show().map_err(|error| error.to_string())?; + main_window.set_focus().map_err(|error| error.to_string())?; + + Ok(bounds) +} + +#[cfg(not(windows))] +#[tauri::command] +pub fn enter_preview_capture_overlay( + _app_handle: tauri::AppHandle, + _game: String, +) -> Result { + Err("Screen capture is currently supported on Windows only.".to_string()) +} + +#[cfg(windows)] +#[tauri::command] +pub fn pause_preview_capture_overlay( + app_handle: tauri::AppHandle, + game: String, +) -> Result<(), String> { + use tauri::Manager; + use winapi::um::winuser::{GetAsyncKeyState, SetForegroundWindow, VK_RBUTTON}; + + let game_window = find_game_window(&game)?; + let main_window = app_handle + .get_webview_window("main") + .ok_or_else(|| "Main window not found.".to_string())?; + main_window + .set_ignore_cursor_events(true) + .map_err(|error| error.to_string())?; + unsafe { + SetForegroundWindow(game_window); + } + + loop { + let pressed = unsafe { (GetAsyncKeyState(VK_RBUTTON) as u16 & 0x8000) != 0 }; + if !pressed { + break; + } + std::thread::sleep(Duration::from_millis(16)); + } + + main_window + .set_ignore_cursor_events(false) + .map_err(|error| error.to_string())?; + main_window.set_focus().map_err(|error| error.to_string())?; + Ok(()) +} + +#[cfg(not(windows))] +#[tauri::command] +pub fn pause_preview_capture_overlay( + _app_handle: tauri::AppHandle, + _game: String, +) -> Result<(), String> { + Ok(()) +} + +#[cfg(windows)] +#[tauri::command] +pub fn capture_preview_screen_region( + app_handle: tauri::AppHandle, + crop: ScreenCaptureCrop, +) -> Result { + use tauri::Manager; + + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.hide(); + } + std::thread::sleep(Duration::from_millis(180)); + + let result = (|| { + let screen = screen_for_point(crop.x, crop.y)?; + let info = &screen.display_info; + let image = screen + .capture_area( + crop.x - info.x, + crop.y - info.y, + crop.width.max(1), + crop.height.max(1), + ) + .map_err(|error| error.to_string())?; + let stage_path = + std::env::temp_dir().join(format!("imm-preview-capture-{}.png", timestamp())); + image.save(&stage_path).map_err(|error| error.to_string())?; + Ok(stage_path.to_string_lossy().to_string()) + })(); + + restore_main_window(&app_handle); + result +} + +#[cfg(not(windows))] +#[tauri::command] +pub fn capture_preview_screen_region( + _app_handle: tauri::AppHandle, + _crop: ScreenCaptureCrop, +) -> Result { + Err("Screen capture is currently supported on Windows only.".to_string()) +} + +#[cfg(windows)] +#[tauri::command] +pub fn cancel_preview_capture_overlay(app_handle: tauri::AppHandle) -> Result<(), String> { + restore_main_window(&app_handle); + Ok(()) +} + +#[cfg(not(windows))] +#[tauri::command] +pub fn cancel_preview_capture_overlay(_app_handle: tauri::AppHandle) -> Result<(), String> { + Ok(()) +} + +fn existing_previews(mod_directory: &Path) -> Vec { + PREVIEW_EXTENSIONS + .iter() + .map(|extension| mod_directory.join(format!("preview.{}", extension))) + .filter(|path| path.is_file()) + .collect() +} + +fn backup_previews(mod_directory: &Path, previews: &[PathBuf]) -> Result, String> { + if previews.is_empty() { + return Ok(None); + } + + let backup_directory = mod_directory + .join("_preview_backups") + .join(timestamp().to_string()); + std::fs::create_dir_all(&backup_directory).map_err(|error| error.to_string())?; + for preview in previews { + if let Some(file_name) = preview.file_name() { + std::fs::copy(preview, backup_directory.join(file_name)) + .map_err(|error| error.to_string())?; + } + } + Ok(Some(backup_directory)) +} + +fn validated_stage_path(stage_path: String) -> Result { + let stage = PathBuf::from(stage_path); + let file_name = stage + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + if stage.parent() != Some(std::env::temp_dir().as_path()) + || !file_name.starts_with("imm-preview-capture-") + || stage.extension().and_then(|extension| extension.to_str()) != Some("png") + { + return Err("Invalid capture staging path.".to_string()); + } + Ok(stage) +} + +#[cfg(windows)] +#[tauri::command] +pub fn save_mod_preview_stage( + stage_path: String, + mod_path: String, +) -> Result, String> { + let stage = validated_stage_path(stage_path)?; + if !stage.is_file() { + return Err("Captured image not found.".to_string()); + } + + let mod_directory = Path::new(&mod_path); + if !mod_directory.is_dir() { + return Err("Mod folder not found.".to_string()); + } + + let image = image::open(&stage).map_err(|error| error.to_string())?; + let final_image = if image.height() > 1200 { + let height = 1200; + let width = ((image.width() as f32 / image.height() as f32) * height as f32) + .round() + .max(1.0) as u32; + image.resize(width, height, image::imageops::FilterType::Lanczos3) + } else { + image + }; + + let pending_path = mod_directory.join(format!(".preview-capture-{}.png", timestamp())); + final_image + .save(&pending_path) + .map_err(|error| error.to_string())?; + + let previews = existing_previews(mod_directory); + let backup = backup_previews(mod_directory, &previews)?; + for preview in previews { + std::fs::remove_file(preview).map_err(|error| error.to_string())?; + } + + let destination = mod_directory.join("preview.png"); + std::fs::rename(&pending_path, &destination).map_err(|error| error.to_string())?; + let _ = std::fs::remove_file(stage); + + Ok(backup.map(|path| path.to_string_lossy().to_string())) +} + +#[cfg(not(windows))] +#[tauri::command] +pub fn save_mod_preview_stage( + _stage_path: String, + _mod_path: String, +) -> Result, String> { + Err("Screen capture is currently supported on Windows only.".to_string()) +} + +#[tauri::command] +pub fn discard_preview_capture_stage(stage_path: String) -> Result<(), String> { + let stage = validated_stage_path(stage_path)?; + if stage.exists() { + std::fs::remove_file(stage).map_err(|error| error.to_string())?; + } + Ok(()) +} + +#[cfg(all(test, windows))] +mod tests { + use super::*; + use image::{ImageBuffer, Rgba}; + + #[test] + fn saving_a_capture_backs_up_the_previous_preview() { + let mod_directory = tempfile::tempdir().unwrap(); + let old_preview = mod_directory.path().join("preview.png"); + ImageBuffer::from_pixel(2, 2, Rgba([10u8, 20, 30, 255])) + .save(&old_preview) + .unwrap(); + + let stage = std::env::temp_dir().join(format!( + "imm-preview-capture-test-{}-{}.png", + std::process::id(), + timestamp() + )); + ImageBuffer::from_pixel(4, 3, Rgba([40u8, 50, 60, 255])) + .save(&stage) + .unwrap(); + + let backup = save_mod_preview_stage( + stage.to_string_lossy().to_string(), + mod_directory.path().to_string_lossy().to_string(), + ) + .unwrap() + .map(PathBuf::from) + .unwrap(); + + assert!(backup.join("preview.png").is_file()); + let saved_preview = image::open(mod_directory.path().join("preview.png")).unwrap(); + assert_eq!((saved_preview.width(), saved_preview.height()), (4, 3)); + assert!(!stage.exists()); + } + + #[test] + fn discard_rejects_paths_outside_the_capture_staging_area() { + let unrelated_file = tempfile::NamedTempFile::new().unwrap(); + let path = unrelated_file.path().to_string_lossy().to_string(); + assert!(discard_preview_capture_stage(path).is_err()); + assert!(unrelated_file.path().is_file()); + } +} diff --git a/src/App.css b/src/App.css index af738fe5..bd7df7cb 100644 --- a/src/App.css +++ b/src/App.css @@ -50,6 +50,17 @@ z-index: 1000; } +body.preview-capture-transparent, +body.preview-capture-transparent #root { + background: transparent !important; + background-color: transparent !important; +} + +body.preview-capture-transparent::before, +body.preview-capture-transparent::after { + display: none !important; +} + @import "tailwindcss"; @import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); diff --git a/src/App.tsx b/src/App.tsx index 506c827f..af629f4d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import { MOD_LIST, ONLINE, PROGRESS_OVERLAY, + PREVIEW_CAPTURE_OVERLAY, RIGHT_SIDEBAR_OPEN, RIGHT_SLIDEOVER_OPEN, SCALE, @@ -47,6 +48,7 @@ import { } from "./components/ui/alert-dialog"; import { OPTIMIZE_TARGET } from "./utils/consts"; import { scaleHandler } from "./utils/ResizeHandles"; +import CapturePreviewModal from "./_Main/components/CapturePreviewModal"; // import { Button } from "./components/ui/button"; const animateProps = { initial: { opacity: 0, filter: "blur(6px)" }, @@ -78,6 +80,7 @@ function App() { const setModList = useSetAtom(MOD_LIST); const modCheckProgress = useAtomValue(MOD_CHECK_PROGRESS); const progressOverlay = useAtomValue(PROGRESS_OVERLAY); + const captureOverlayActive = useAtomValue(PREVIEW_CAPTURE_OVERLAY); const [_, setShowModeSwitch] = useState(false); const [previousOnline, setPreviousOnline] = useState(online); const initializedRef = useRef(false); @@ -148,6 +151,10 @@ function App() { throw new Error(err); } }, [err]); + useEffect(() => { + document.body.classList.toggle("preview-capture-transparent", captureOverlayActive); + return () => document.body.classList.remove("preview-capture-transparent"); + }, [captureOverlayActive]); useEffect(() => { if (scale) { scaleHandler(scale); @@ -213,7 +220,12 @@ function App() { [rightSidebarOpen] ); return ( -
+ <> +
-
+
+ + ); } export default App; diff --git a/src/_Main/components/CapturePreviewModal.tsx b/src/_Main/components/CapturePreviewModal.tsx new file mode 100644 index 00000000..04a37ce3 --- /dev/null +++ b/src/_Main/components/CapturePreviewModal.tsx @@ -0,0 +1,318 @@ +import { addToast } from "@/_Toaster/ToastProvider"; +import { Button } from "@/components/ui/button"; +import { managedSRC } from "@/utils/consts"; +import { saveConfigs } from "@/utils/filesys"; +import { + DATA, + GAME, + LAST_UPDATED, + MOD_LIST, + PREVIEW_CAPTURE_OVERLAY, + PREVIEW_CAPTURE_TARGET, + SOURCE, +} from "@/utils/vars"; +import { convertFileSrc, invoke } from "@tauri-apps/api/core"; +import { isAbsolute, join } from "@tauri-apps/api/path"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { CheckIcon, Loader2Icon, RotateCcwIcon, XIcon } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from "react"; + +interface Selection { + x: number; + y: number; + width: number; + height: number; +} + +interface CaptureBounds { + left: number; + top: number; + width: number; + height: number; +} + +function clamp(value: number, minimum: number, maximum: number) { + return Math.max(minimum, Math.min(maximum, value)); +} + +function selectionFromPoints( + startX: number, + startY: number, + currentX: number, + currentY: number, + widthLimit: number, + heightLimit: number +): Selection { + const safeStartX = clamp(startX, 0, widthLimit); + const safeStartY = clamp(startY, 0, heightLimit); + const safeCurrentX = clamp(currentX, 0, widthLimit); + const safeCurrentY = clamp(currentY, 0, heightLimit); + return { + x: Math.min(safeStartX, safeCurrentX), + y: Math.min(safeStartY, safeCurrentY), + width: Math.abs(safeCurrentX - safeStartX), + height: Math.abs(safeCurrentY - safeStartY), + }; +} + +function pointerInBounds(event: ReactMouseEvent, bounds: CaptureBounds) { + const elementBounds = event.currentTarget.getBoundingClientRect(); + return { + x: elementBounds.width ? ((event.clientX - elementBounds.left) / elementBounds.width) * bounds.width : 0, + y: elementBounds.height ? ((event.clientY - elementBounds.top) / elementBounds.height) * bounds.height : 0, + }; +} + +async function discardStage(path: string) { + if (path) await invoke("discard_preview_capture_stage", { stagePath: path }).catch(() => {}); +} + +function CapturePreviewModal() { + const [target, setTarget] = useAtom(PREVIEW_CAPTURE_TARGET); + const setOverlayActive = useSetAtom(PREVIEW_CAPTURE_OVERLAY); + const game = useAtomValue(GAME); + const source = useAtomValue(SOURCE); + const setLastUpdated = useSetAtom(LAST_UPDATED); + const setData = useSetAtom(DATA); + const setModList = useSetAtom(MOD_LIST); + const dragStart = useRef<{ x: number; y: number } | null>(null); + const [phase, setPhase] = useState<"idle" | "selecting" | "capturing" | "confirming">("idle"); + const [bounds, setBounds] = useState(null); + const [selection, setSelection] = useState(null); + const [dragging, setDragging] = useState(false); + const [stagePath, setStagePath] = useState(""); + const [saving, setSaving] = useState(false); + const [cameraControlActive, setCameraControlActive] = useState(false); + const stageUrl = useMemo(() => (stagePath ? convertFileSrc(stagePath) : ""), [stagePath]); + + const closeCapture = useCallback(async () => { + await invoke("cancel_preview_capture_overlay").catch(() => {}); + await discardStage(stagePath); + setOverlayActive(false); + setPhase("idle"); + setBounds(null); + setSelection(null); + setStagePath(""); + setCameraControlActive(false); + setTarget(null); + }, [setOverlayActive, setTarget, stagePath]); + + const beginSelection = useCallback(async () => { + if (!target) return; + await discardStage(stagePath); + setStagePath(""); + setSelection(null); + setPhase("selecting"); + setOverlayActive(true); + try { + const captureBounds = await invoke("enter_preview_capture_overlay", { game }); + setBounds(captureBounds); + } catch (error) { + addToast({ type: "error", message: String(error) }); + await closeCapture(); + } + }, [closeCapture, game, setOverlayActive, stagePath, target]); + + useEffect(() => { + if (target && phase === "idle") beginSelection(); + }, [beginSelection, phase, target]); + + useEffect(() => { + if (!target) return; + const handleEscape = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + event.preventDefault(); + closeCapture(); + }; + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + }, [closeCapture, target]); + + const captureSelection = async (nextSelection: Selection) => { + if (!bounds || nextSelection.width < 8 || nextSelection.height < 8) return; + setDragging(false); + setPhase("capturing"); + try { + const path = await invoke("capture_preview_screen_region", { + crop: { + x: Math.round(bounds.left + nextSelection.x), + y: Math.round(bounds.top + nextSelection.y), + width: Math.round(nextSelection.width), + height: Math.round(nextSelection.height), + }, + }); + setStagePath(path); + setOverlayActive(false); + setPhase("confirming"); + } catch (error) { + addToast({ type: "error", message: String(error) }); + await closeCapture(); + } + }; + + const pauseForCameraControl = async () => { + if (phase !== "selecting") return; + setCameraControlActive(true); + setDragging(false); + dragStart.current = null; + try { + await invoke("pause_preview_capture_overlay", { game }); + } catch (error) { + addToast({ type: "error", message: String(error) }); + } finally { + setCameraControlActive(false); + } + }; + + const savePreview = async () => { + if (!target || !stagePath) return; + setSaving(true); + try { + const modPath = (await isAbsolute(target.path)) ? target.path : await join(source, managedSRC, target.path); + const backupPath = await invoke("save_mod_preview_stage", { stagePath, modPath }); + setData((previous) => { + if (!previous[target.path]?.crop) return previous; + const { crop: _crop, ...modData } = previous[target.path]; + return { ...previous, [target.path]: modData }; + }); + setModList((previous) => + previous.map((mod) => { + if (mod.path !== target.path || !mod.crop) return mod; + const { crop: _crop, ...nextMod } = mod; + return nextMod; + }) + ); + saveConfigs(); + setLastUpdated(Date.now()); + addToast({ + type: "success", + message: backupPath ? "Preview saved. The previous image was backed up." : "Preview image saved.", + }); + await closeCapture(); + } catch (error) { + addToast({ type: "error", message: String(error) }); + } finally { + setSaving(false); + } + }; + + if (!target) return null; + + if (phase === "selecting" || phase === "capturing") { + return ( +
event.preventDefault()} + onMouseDown={(event) => { + if (!bounds || phase !== "selecting") return; + if (event.button === 2) { + event.preventDefault(); + pauseForCameraControl(); + return; + } + if (event.button !== 0) return; + dragStart.current = pointerInBounds(event, bounds); + setSelection(null); + setDragging(true); + }} + onMouseMove={(event) => { + if (!bounds || !dragging || !dragStart.current || phase !== "selecting") return; + const pointer = pointerInBounds(event, bounds); + setSelection( + selectionFromPoints( + dragStart.current.x, + dragStart.current.y, + pointer.x, + pointer.y, + bounds.width, + bounds.height + ) + ); + }} + onMouseUp={(event) => { + if (event.button !== 0 || !bounds || !dragging || !dragStart.current || phase !== "selecting") return; + const pointer = pointerInBounds(event, bounds); + const nextSelection = selectionFromPoints( + dragStart.current.x, + dragStart.current.y, + pointer.x, + pointer.y, + bounds.width, + bounds.height + ); + dragStart.current = null; + setDragging(false); + if (nextSelection.width < 8 || nextSelection.height < 8) { + setSelection(null); + return; + } + setSelection(nextSelection); + captureSelection(nextSelection); + }} + > +
+ {cameraControlActive + ? "Camera control active - release the right mouse button to resume" + : "Drag to select a preview - hold the right mouse button to control the game camera - Esc cancels"} +
+ {selection && bounds && ( +
+ )} + {phase === "capturing" && ( +
+
+ Capturing preview... +
+
+ )} +
+ ); + } + + if (phase === "confirming") { + return ( +
+
+
+
+

Confirm preview

+

{target.name}

+

Use this crop as the mod preview?

+
+ +
+
+ {stageUrl ? ( + + ) : ( + + )} +
+
+ + +
+
+
+ ); + } + + return null; +} + +export default CapturePreviewModal; diff --git a/src/_RightSidebar/components/ModPreview.tsx b/src/_RightSidebar/components/ModPreview.tsx index 2f76c47b..1072341c 100644 --- a/src/_RightSidebar/components/ModPreview.tsx +++ b/src/_RightSidebar/components/ModPreview.tsx @@ -2,9 +2,9 @@ import { Button } from "@/components/ui/button"; import { DialogContent } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { savePreviewImageFromData } from "@/utils/filesys"; -import { TEXT_DATA } from "@/utils/vars"; -import { useAtomValue } from "jotai"; -import { UploadIcon } from "lucide-react"; +import { PREVIEW_CAPTURE_TARGET, TEXT_DATA } from "@/utils/vars"; +import { useAtomValue, useSetAtom } from "jotai"; +import { CameraIcon, UploadIcon } from "lucide-react"; import { useCallback, useEffect, useRef } from "react"; import { useDropzone } from "react-dropzone"; function ModPreview({ @@ -16,6 +16,7 @@ function ModPreview({ isBlank: boolean; }) { const textData = useAtomValue(TEXT_DATA); + const setCaptureTarget = useSetAtom(PREVIEW_CAPTURE_TARGET); const onDrop = useCallback( async (acceptedFiles: File[]) => { if (acceptedFiles.length == 0) return; @@ -108,7 +109,17 @@ function ModPreview({ }} type="button" > - {textData._RightSideBar._components._ModPreferences.Select} + {textData._RightSideBar._components._ModPreferences.Select} + + diff --git a/src/utils/vars.ts b/src/utils/vars.ts index 482bd960..09953bf9 100644 --- a/src/utils/vars.ts +++ b/src/utils/vars.ts @@ -28,11 +28,17 @@ interface UpdateInfo { body: string; raw: Update | null; } +export interface PreviewCaptureTarget { + path: string; + name: string; +} const OPTIMIZED = atomWithStorage("imm-optimized", {} as Record); const DEV_HIDE_PREVIEWS = atomWithStorage("imm-dev-hide-previews", false); const SCALE = atomWithStorage("imm-scale", 0); const BLUR = atomWithStorage("imm-blur", 1); const ANIMATIONS = atomWithStorage("imm-animations", true); +const PREVIEW_CAPTURE_TARGET = atom(null); +const PREVIEW_CAPTURE_OVERLAY = atom(false); const BACKUP_INI = atomWithStorage("imm-backup-ini", false); const INIT_DONE = atom(false); const MAIN_FUNC_STATUS = atom(""); @@ -203,5 +209,7 @@ export { SCALE, BLUR, ANIMATIONS, + PREVIEW_CAPTURE_TARGET, + PREVIEW_CAPTURE_OVERLAY, BACKUP_INI, };