From 64b3ec5f9054cc58c7e0d2c6f164018a3c4554e0 Mon Sep 17 00:00:00 2001 From: Wyvest Date: Mon, 23 Feb 2026 17:15:26 -0500 Subject: [PATCH] Squashed commit of the following: commit 9d138882dec56d9f823a22dd2284b0f264a6b35a Author: Wyvest Date: Mon Feb 23 17:14:58 2026 -0500 Watch cluster dirs, toggle & custom-bundle support Enable filesystem watches and add support for directory-based packages, toggleable packages, and custom bundles. Highlights: - Enable tauri-plugin-fs "watch" feature and add fs:allow-watch capability. - Frontend: add useAllClusterDirWatch hook to watch cluster folders and call bindings.core.syncCluster on changes; add useCustomBundle to synthesize a "[Custom]" bundle for installed-but-unbundled packages and surface it in mods/datapacks/resource-packs/shaders routes via ModList/ModCard changes. - ModCard/ModList: introduce toggle mode (useToggleMode) to treat some bundles as toggleable; implement UI toggle behavior and skip interactivity for directory-based bundles. - Bindings: expose syncCluster and togglePackage in bindings.gen for IPC usage. - Backend: extend sync_cluster to detect directories, register directory-based packages as local, and reconcile/unlink DB links for files removed from disk; support removing directories when unlinking packages. - Backend API: add toggle_package to rename package files (.disabled toggling), update central store path, and return the new enabled state; add Tauri commands for sync_cluster and toggle_package. Reason: allow resourcepacks/shaders/datapacks stored as directories to be recognized as local packages, provide automatic sync on filesystem changes, and let users enable/disable packages by toggling their filenames. commit 1b100f7ed2242d21f3fbb6229745cddbd3199db7 Merge: 4ea4216 6a2352c Author: Wyvest Date: Mon Feb 23 06:30:25 2026 -0500 Merge branch 'oneclient/main' into bruhhh commit 4ea42169ad430bc8b127f829c528c258f49b5a3a Author: Wyvest Date: Wed Dec 31 03:48:13 2025 +0900 Fix external mod tracking commit cb456f4f6d50165d05091ae65c9611ebf956df07 Merge: 8628380 18c37f8 Author: Wyvest Date: Wed Dec 31 01:48:00 2025 +0900 Merge branch 'oneclient/main' into bruhhh commit 86283800ab265361540f4cf20e9a843cb2c4f5f0 Author: Wyvest Date: Sun Dec 28 18:19:57 2025 +0900 h --- Cargo.lock | 286 +++++++++++++++--- Cargo.toml | 2 +- .../desktop/capabilities/default.json | 10 +- apps/oneclient/frontend/src/bindings.gen.ts | 12 +- .../src/components/Bundle/ModCard.tsx | 55 +++- .../src/components/Bundle/ModList.tsx | 24 +- .../frontend/src/hooks/useClusterDirWatch.ts | 46 +++ .../frontend/src/hooks/useCustomBundle.ts | 119 ++++++++ .../src/routes/app/cluster/datapacks.tsx | 15 +- .../frontend/src/routes/app/cluster/mods.tsx | 13 +- .../src/routes/app/cluster/resource-packs.tsx | 13 +- .../src/routes/app/cluster/shaders.tsx | 15 +- .../frontend/src/routes/app/route.tsx | 3 + packages/core/src/api/cluster/sync.rs | 116 ++++++- packages/core/src/api/packages/mod.rs | 67 +++- packages/core/src/api/tauri/commands.rs | 31 ++ 16 files changed, 740 insertions(+), 87 deletions(-) create mode 100644 apps/oneclient/frontend/src/hooks/useClusterDirWatch.ts create mode 100644 apps/oneclient/frontend/src/hooks/useCustomBundle.ts diff --git a/Cargo.lock b/Cargo.lock index c8d855b7..39da9f10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,11 +510,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -726,7 +726,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "cairo-sys-rs", "glib", "libc", @@ -1039,7 +1039,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "core-foundation 0.10.0", "core-graphics-types", "foreign-types", @@ -1052,7 +1052,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "core-foundation 0.10.0", "libc", ] @@ -1337,7 +1337,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block2 0.6.1", "libc", "objc2 0.6.3", @@ -1349,7 +1349,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "objc2 0.6.3", ] @@ -1648,6 +1648,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "filetime" version = "0.2.25" @@ -1741,6 +1750,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "funty" version = "2.0.0" @@ -2095,7 +2113,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -2108,7 +2126,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -2608,6 +2626,26 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.11.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.13" @@ -2787,11 +2825,31 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "serde", "unicode-segmentation", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "kuchikiki" version = "0.8.2" @@ -2898,7 +2956,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "libc", "redox_syscall", ] @@ -3082,6 +3140,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -3119,7 +3178,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys", @@ -3155,7 +3214,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -3168,7 +3227,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -3205,6 +3264,47 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d88b1a7538054351c8258338df7c931a590513fb3745e8c15eb9ff4199b8d1" +dependencies = [ + "file-id", + "log", + "notify", + "notify-types", + "walkdir", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", + "serde", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -3355,7 +3455,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block2 0.6.1", "libc", "objc2 0.6.3", @@ -3376,7 +3476,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-foundation 0.3.2", ] @@ -3387,7 +3487,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-foundation 0.3.2", ] @@ -3398,7 +3498,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "dispatch2 0.3.0", "objc2 0.6.3", ] @@ -3409,7 +3509,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "dispatch2 0.3.0", "objc2 0.6.3", "objc2-core-foundation", @@ -3442,7 +3542,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-core-foundation", "objc2-core-graphics", @@ -3454,7 +3554,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-core-foundation", "objc2-core-graphics", @@ -3482,7 +3582,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3494,7 +3594,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block2 0.6.1", "libc", "objc2 0.6.3", @@ -3507,7 +3607,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3518,7 +3618,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3530,7 +3630,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-app-kit", "objc2-foundation 0.3.2", @@ -3542,7 +3642,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3555,7 +3655,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -3567,7 +3667,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block2 0.6.1", "objc2 0.6.3", "objc2-cloud-kit", @@ -3597,7 +3697,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "block2 0.6.1", "objc2 0.6.3", "objc2-app-kit", @@ -4657,7 +4757,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", ] [[package]] @@ -4672,7 +4772,7 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", ] [[package]] @@ -4942,7 +5042,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4955,7 +5055,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.9.3", @@ -5276,10 +5376,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -5295,10 +5396,19 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -5802,7 +5912,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.6.0", + "bitflags 2.11.0", "byteorder", "bytes", "chrono", @@ -5849,7 +5959,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.6.0", + "bitflags 2.11.0", "byteorder", "chrono", "crc", @@ -6081,7 +6191,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6115,7 +6225,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "core-foundation 0.10.0", "core-graphics", "crossbeam-channel", @@ -6380,6 +6490,8 @@ dependencies = [ "anyhow", "dunce", "glob", + "notify", + "notify-debouncer-full", "percent-encoding", "schemars", "serde", @@ -6487,7 +6599,7 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a27a3fe49de72adbe0d84aee33c89a0b059722cd0b42aaeab29eaaee7f7535cd" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "log", "serde", "serde_json", @@ -7427,7 +7539,7 @@ version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "rustix 0.38.44", "wayland-backend", "wayland-scanner", @@ -7439,7 +7551,7 @@ version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7451,7 +7563,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7896,6 +8008,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -7944,13 +8065,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-version" version = "0.1.1" @@ -7978,6 +8116,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -7996,6 +8140,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -8014,12 +8164,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -8038,6 +8200,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -8056,6 +8224,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -8074,6 +8248,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -8092,6 +8272,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" @@ -8126,7 +8312,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2554f855..68254b24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ tauri-build = { version = "=2.2.0" } tauri-plugin-clipboard-manager = { version = "=2.2.2" } tauri-plugin-deep-link = { version = "=2.2.1" } tauri-plugin-dialog = { version = "=2.2.1" } -tauri-plugin-fs = { version = "=2.2.1" } +tauri-plugin-fs = { version = "=2.2.1", features = ["watch"] } tauri-plugin-os = { version = "=2.2.1" } tauri-plugin-single-instance = { version = "=2.2.3" } tauri-plugin-updater = { version = "=2.7.1" } diff --git a/apps/oneclient/desktop/capabilities/default.json b/apps/oneclient/desktop/capabilities/default.json index 79db1d31..b72d4d5c 100644 --- a/apps/oneclient/desktop/capabilities/default.json +++ b/apps/oneclient/desktop/capabilities/default.json @@ -37,14 +37,14 @@ "fs:allow-download-write", "fs:allow-data-read-recursive", "fs:allow-data-write-recursive", - - "os:allow-arch", - "os:deny-exe-extension", - "os:allow-family", + "fs:allow-watch", "os:deny-hostname", "os:allow-locale", "os:allow-os-type", "os:allow-platform", - "os:allow-version" + "os:allow-version", + "os:allow-arch", + "os:deny-exe-extension", + "os:allow-family" ] } diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 151c3887..5eb347f2 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -303,11 +303,8 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'oneclient':'{"getClustersGroupedByMajor":[],"checkForUpdate":[],"extractBundleOverrides":["bundle_path","cluster_id"],"installUpdate":[],"getBundlesFor":["cluster_id"],"getVersions":[]}', 'core':'{"getRunningProcesses":[],"installModpack":["modpack","cluster_id"],"removeCluster":["id"],"launchCluster":["id","uuid","search_for_java"],"setDefaultUser":["uuid"],"killProcess":["pid"],"getWorlds":["id"],"createSettingsProfile":["name"],"removeCape":["access_token"],"getScreenshots":["id"],"getLoadersForVersion":["mc_version"],"getLogs":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"refreshAccount":["uuid"],"getGameVersions":[],"getRunningProcessesByClusterId":["cluster_id"],"updateClusterProfile":["name","profile"],"getUser":["uuid"],"isClusterRunning":["cluster_id"],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getClusterById":["id"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"convertUsernameUUID":["username_uuid"],"openMsaLogin":[],"createCluster":["options"],"getLogByName":["id","name"],"getProfileOrDefault":["name"],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"updateClusterById":["id","request"],"getUsersFromAuthor":["provider","author"],"getClusters":[],"getPackageBody":["provider","body"],"removePackage":["cluster_id","package_hash"],"changeCape":["access_token","cape_uuid"],"open":["input"],"refreshAccounts":[],"writeSettings":["setting"],"searchPackages":["provider","query"],"getMultiplePackages":["provider","slugs"],"getDefaultUser":["fallback"],"getUsers":[],"getLinkedPackages":["cluster_id"],"changeSkin":["access_token","skin_url","skin_variant"],"setClusterStage":["id","stage"],"fetchLoggedInProfile":["access_token"],"setDiscordRPCMessage":["message"],"removeUser":["uuid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getArch":[],"openDevTools":[],"getType":[],"getPackageVersion":[],"getGitCommitHash":[],"getFamily":[],"getPlatform":[],"getLocale":[],"getOsVersion":[],"isInDev":[],"getBuildTimestamp":[]}' } -export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, -'core': { getClusters: () => Promise, +const ARGS_MAP = { 'core':'{"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"syncCluster":["cluster_id"],"getGlobalProfile":[],"updateClusterProfile":["name","profile"],"setClusterStage":["id","stage"],"getClusters":[],"refreshAccounts":[],"getLinkedPackages":["cluster_id"],"removePackage":["cluster_id","package_hash"],"removeCluster":["id"],"isClusterRunning":["cluster_id"],"getPackage":["provider","slug"],"getDefaultUser":["fallback"],"getPackageBody":["provider","body"],"readSettings":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"fetchMinecraftProfile":["uuid"],"installModpack":["modpack","cluster_id"],"changeSkin":["access_token","skin_url","skin_variant"],"refreshAccount":["uuid"],"convertUsernameUUID":["username_uuid"],"open":["input"],"getLogByName":["id","name"],"getUser":["uuid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"launchCluster":["id","uuid","search_for_java"],"getScreenshots":["id"],"writeSettings":["setting"],"getRunningProcesses":[],"changeCape":["access_token","cape_uuid"],"createCluster":["options"],"getClusterById":["id"],"togglePackage":["cluster_id","package_hash"],"removeCape":["access_token"],"getGameVersions":[],"setDiscordRPCMessage":["message"],"getLoadersForVersion":["mc_version"],"killProcess":["pid"],"getUsersFromAuthor":["provider","author"],"getLogs":["id"],"fetchLoggedInProfile":["access_token"],"getUsers":[],"updateClusterById":["id","request"],"openMsaLogin":[],"getMultiplePackages":["provider","slugs"],"removeUser":["uuid"],"getRunningProcessesByClusterId":["cluster_id"],"getProfileOrDefault":["name"],"getWorlds":["id"],"createSettingsProfile":["name"],"searchPackages":["provider","query"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"setDefaultUser":["uuid"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"checkForUpdate":[],"installUpdate":[],"extractBundleOverrides":["bundle_path","cluster_id"],"getVersions":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'debug':'{"getPlatform":[],"getArch":[],"getPackageVersion":[],"isInDev":[],"getOsVersion":[],"openDevTools":[],"getLocale":[],"getType":[],"getGitCommitHash":[],"getBuildTimestamp":[],"getFamily":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}' } +export type Router = { 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, createCluster: (options: CreateCluster) => Promise, @@ -346,8 +343,10 @@ getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise, downloadExternalPackage: (package: ExternalPackage, clusterId: number, force: boolean | null, skipCompatibility: boolean | null) => Promise, getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise, +syncCluster: (clusterId: number) => Promise, getLinkedPackages: (clusterId: number) => Promise, removePackage: (clusterId: number, packageHash: string) => Promise, +togglePackage: (clusterId: number, packageHash: string) => Promise, installModpack: (modpack: ModpackArchive, clusterId: number) => Promise, fetchMinecraftProfile: (uuid: string) => Promise, fetchLoggedInProfile: (accessToken: string) => Promise, @@ -358,6 +357,9 @@ removeCape: (accessToken: string) => Promise, convertUsernameUUID: (usernameUuid: string) => Promise, setDiscordRPCMessage: (message: string) => Promise, open: (input: string) => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, 'oneclient': { getClustersGroupedByMajor: () => Promise>, getBundlesFor: (clusterId: number) => Promise, getVersions: () => Promise, diff --git a/apps/oneclient/frontend/src/components/Bundle/ModCard.tsx b/apps/oneclient/frontend/src/components/Bundle/ModCard.tsx index 263ef379..6543ac5b 100644 --- a/apps/oneclient/frontend/src/components/Bundle/ModCard.tsx +++ b/apps/oneclient/frontend/src/components/Bundle/ModCard.tsx @@ -91,6 +91,7 @@ export interface ModCardContextApi { useVerticalGridLayout?: boolean; mods?: Array; installedPackages?: Array; + useToggleMode?: boolean; } const DEFAULT_MOD_CARD_CONTEXT: ModCardContextApi = {}; @@ -101,7 +102,7 @@ export function useModCardContext() { } export function ModCard({ file, cluster }: ModCardProps) { - const { enableClickToDownload, onClickOnMod, useVerticalGridLayout, mods, installedPackages } = useModCardContext(); + const { enableClickToDownload, onClickOnMod, useVerticalGridLayout, mods, installedPackages, useToggleMode } = useModCardContext(); const queryClient = useQueryClient(); const [modMetadata, setModMetadata] = useState({ name: 'Loading...', description: null, author: null, iconURL: null, url: null, managed: false, packageSlug: null }); @@ -110,7 +111,12 @@ export function ModCard({ file, cluster }: ModCardProps) { }, [file, useVerticalGridLayout]); const kind = file.kind; - + // Directory-based packages (folders dropped into resourcepacks/shaderpacks/datapacks) + // cannot be toggled via .disabled rename — skip toggle mode for them. + const isDirectory = useMemo(() => + 'External' in kind && !kind.External.name.includes('.'), + [kind]); + const effectiveToggleMode = useToggleMode && !isDirectory; const isInstalled = useMemo(() => { if (installedPackages) if ('Managed' in kind) { @@ -124,10 +130,20 @@ export function ModCard({ file, cluster }: ModCardProps) { return mods?.includes(file) ?? false; }, [installedPackages, kind, mods, file]); - const [isSelected, setSelected] = useState(isInstalled); - useEffect(() => { - setSelected(isInstalled); - }, [isInstalled]); + // In toggle mode, "selected" = enabled (file_name doesn't end with .disabled) + const isFileEnabled = useMemo(() => { + if (!effectiveToggleMode) + return true; + if ('Managed' in kind) { + const primary = kind.Managed[1].files.find(f => f.primary) ?? kind.Managed[1].files[0]; + return !primary?.file_name.endsWith('.disabled'); + } + return !kind.External.name.endsWith('.disabled'); + }, [effectiveToggleMode, kind]); + const [isSelected, setSelected] = useState(effectiveToggleMode ? isFileEnabled : isInstalled); + useEffect(() => { + setSelected(effectiveToggleMode ? isFileEnabled : isInstalled); + }, [isInstalled, effectiveToggleMode, isFileEnabled]); const download = useCommandMut(async () => { if ('Managed' in kind) { @@ -162,8 +178,33 @@ export function ModCard({ file, cluster }: ModCardProps) { await bindings.core.removePackage(cluster.id, hash); }); + const toggle = useCommandMut(async () => { + let hash: string | undefined; + if ('Managed' in kind) { + const [_, version] = kind.Managed; + const primary = version.files.find(f => f.primary) ?? version.files[0]; + hash = primary.sha1; + } + else { + hash = kind.External.sha1; + } + + if (hash) + await bindings.core.togglePackage(cluster.id, hash); + }); + const handleOnClick = () => { - if (enableClickToDownload) + // Folder-based packages (resource packs, shader packs, data packs as directories) + // cannot be toggled or removed — treat them as entirely non-interactive. + if (isDirectory) + return; + if (effectiveToggleMode) + (async () => { + await toggle.mutateAsync(); + await queryClient.invalidateQueries({ queryKey: ['getLinkedPackages', cluster.id] }); + })(); + + else if (enableClickToDownload) (async () => { if (isInstalled) await remove.mutateAsync(); diff --git a/apps/oneclient/frontend/src/components/Bundle/ModList.tsx b/apps/oneclient/frontend/src/components/Bundle/ModList.tsx index 3cf10d3f..ff22e990 100644 --- a/apps/oneclient/frontend/src/components/Bundle/ModList.tsx +++ b/apps/oneclient/frontend/src/components/Bundle/ModList.tsx @@ -13,9 +13,10 @@ interface ModListProps { cluster: ClusterModel; selectedTab?: string; onTabChange?: (value: string) => void; + toggleBundlePaths?: Set; } -export function ModList({ bundles, cluster, selectedTab, onTabChange }: ModListProps) { +export function ModList({ bundles, cluster, selectedTab, onTabChange, toggleBundlePaths }: ModListProps) { const ctx = useContext(ModCardContext); const useVerticalGridLayout = ctx?.useVerticalGridLayout ?? false; const { createSetting } = useSettings(); @@ -37,11 +38,22 @@ export function ModList({ bundles, cluster, selectedTab, onTabChange }: ModListP - {nonEmptyBundles.map(bundleData => ( - - - - ))} + {nonEmptyBundles.map((bundleData) => { + const isToggle = toggleBundlePaths?.has(bundleData.path) ?? false; + const content = ; + + return ( + + {isToggle + ? ( + + {content} + + ) + : content} + + ); + })} ); diff --git a/apps/oneclient/frontend/src/hooks/useClusterDirWatch.ts b/apps/oneclient/frontend/src/hooks/useClusterDirWatch.ts new file mode 100644 index 00000000..63c02060 --- /dev/null +++ b/apps/oneclient/frontend/src/hooks/useClusterDirWatch.ts @@ -0,0 +1,46 @@ +import { useLastPlayedClusters } from '@/hooks/useClusters'; +import { bindings } from '@/main'; +import { useQueryClient } from '@tanstack/react-query'; +import { watch } from '@tauri-apps/plugin-fs'; +import { useEffect } from 'react'; + +/** + * Watches ALL cluster directories for file-system changes (files added, + * removed, renamed). Should be mounted once at the app root so that syncing + * happens regardless of which page the user is on. + */ +export function useAllClusterDirWatch() { + const { data: clusters } = useLastPlayedClusters(); + const queryClient = useQueryClient(); + + useEffect(() => { + if (clusters.length === 0) + return; + + const unwatchers: Array<() => void> = []; + + for (const cluster of clusters) + bindings.folders.fromCluster(cluster.folder_name).then((clusterDir) => { + watch( + clusterDir, + async () => { + try { + await bindings.core.syncCluster(cluster.id); + await queryClient.invalidateQueries({ queryKey: ['getLinkedPackages', cluster.id] }); + } + catch (e) { + console.error('[watch] sync failed for cluster', cluster.id, ':', e); + } + }, + { delayMs: 500, recursive: true }, + ) + .then(fn => unwatchers.push(fn)) + .catch(e => console.error('[watch] failed to register watch:', e)); + }).catch(e => console.error('[watch] failed to resolve cluster dir:', e)); + + return () => { + for (const unwatch of unwatchers) + unwatch(); + }; + }, [clusters, queryClient]); +} diff --git a/apps/oneclient/frontend/src/hooks/useCustomBundle.ts b/apps/oneclient/frontend/src/hooks/useCustomBundle.ts new file mode 100644 index 00000000..4d25b366 --- /dev/null +++ b/apps/oneclient/frontend/src/hooks/useCustomBundle.ts @@ -0,0 +1,119 @@ +import type { ClusterModel, ManagedVersion, ModpackArchive, ModpackFile, PackageModel, PackageType } from '@/bindings.gen'; +import { bindings } from '@/main'; +import { useCommandSuspense } from '@onelauncher/common'; +import { useMemo } from 'react'; + +/** Finds packages that are installed in the cluster but not part of any bundle, filtered by type. */ +export function findCustomPackages( + bundles: Array, + installedPackages: Array, + packageType: PackageType, + excludeDependencies: boolean = false, +): Array { + const bundleKeys: Set = new Set(); + const bundleHashes: Set = new Set(); + // Collect dependency project IDs to exclude hidden bundle deps (mods only). + // e.g. fabric-language-kotlin is hidden in the PolyMrPack manifest but + // listed as a required dependency of visible bundle mods. + const bundleDependencyIds: Set = new Set(); + + bundles.forEach((b) => { + b.manifest.files.forEach((f) => { + if ('Managed' in f.kind) { + bundleKeys.add(`${f.kind.Managed[0].provider}:${f.kind.Managed[0].id}`); + if (excludeDependencies) + f.kind.Managed[1].dependencies.forEach((dep) => { + if (dep.project_id != null) + bundleDependencyIds.add(dep.project_id); + }); + } + else { + bundleHashes.add(f.kind.External.sha1); + } + }); + }); + + return installedPackages.filter((pkg) => { + if (pkg.package_type !== packageType) + return false; + if (bundleKeys.has(`${pkg.provider}:${pkg.package_id}`) || bundleHashes.has(pkg.hash)) + return false; + if (excludeDependencies && bundleDependencyIds.has(pkg.package_id)) + return false; + return true; + }); +} + +/** + * Builds a synthetic "[Custom]" ModpackArchive for installed packages that are + * not part of any bundle. Batch-fetches Modrinth metadata so icons/descriptions + * are available. + */ +export function useCustomBundle( + bundles: Array, + installedPackages: Array, + cluster: ClusterModel, + packageType: PackageType, +): ModpackArchive | null { + const customInstalledPackages = useMemo( + () => findCustomPackages(bundles, installedPackages, packageType, packageType === 'mod'), + [bundles, installedPackages, packageType], + ); + + const modrinthIds = useMemo( + () => customInstalledPackages.filter(p => p.provider === 'Modrinth').map(p => p.package_id), + [customInstalledPackages], + ); + + const { data: fetchedManagedPackages } = useCommandSuspense( + ['getMultiplePackages', 'Modrinth', modrinthIds], + () => bindings.core.getMultiplePackages('Modrinth', modrinthIds), + ); + + return useMemo(() => { + if (customInstalledPackages.length === 0) + return null; + + const managedPackageMap = new Map(fetchedManagedPackages.map(mp => [mp.id, mp])); + + const files: Array = customInstalledPackages.map((pkg) => { + const managed = managedPackageMap.get(pkg.package_id); + if (managed) { + const stubVersion: ManagedVersion = { + version_id: pkg.version_id, + project_id: pkg.package_id, + display_name: pkg.display_name, + display_version: pkg.display_version, + changelog: null, + dependencies: [], + mc_versions: pkg.mc_versions, + release_type: 'release', + loaders: pkg.mc_loader, + published: pkg.published_at, + downloads: 0, + files: [{ sha1: pkg.hash, url: '', file_name: pkg.file_name, primary: true, size: 0 }], + }; + return { enabled: true, kind: { Managed: [managed, stubVersion] as [typeof managed, ManagedVersion] }, overrides: null }; + } + return { + enabled: true, + kind: { External: { name: pkg.file_name, url: '', sha1: pkg.hash, size: 0, package_type: pkg.package_type } }, + overrides: { name: pkg.display_name, icon: pkg.icon ?? null, authors: null, description: null }, + }; + }); + + return { + path: '__custom__', + format: 'PolyMrPack', + manifest: { + name: '[Custom]', + version: '0.0.0', + loader: cluster.mc_loader, + loader_version: cluster.mc_loader_version ?? '', + mc_version: cluster.mc_version, + enabled: true, + files, + }, + }; + }, [customInstalledPackages, fetchedManagedPackages, cluster]); +} diff --git a/apps/oneclient/frontend/src/routes/app/cluster/datapacks.tsx b/apps/oneclient/frontend/src/routes/app/cluster/datapacks.tsx index 72cb6c15..d3cc4d83 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/datapacks.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/datapacks.tsx @@ -1,7 +1,8 @@ import type { ModCardContextApi } from '@/components'; import { ModCardContext, ModList } from '@/components'; -import { getFilePackageType } from '@/routes/app/cluster/mods'; +import { useCustomBundle } from '@/hooks/useCustomBundle'; import { bindings } from '@/main'; +import { getFilePackageType } from '@/routes/app/cluster/mods'; import { useCommandSuspense } from '@onelauncher/common'; import { createFileRoute } from '@tanstack/react-router'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; @@ -22,19 +23,27 @@ function RouteComponent() { manifest: { ...b.manifest, files: b.manifest.files.filter(f => getFilePackageType(f) === 'datapack') }, })), [bundles]); + const customBundle = useCustomBundle(bundles, installedPackages, cluster, 'datapack'); + + const allBundles = useMemo(() => + customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles, + [filteredBundles, customBundle]); + + const customTogglePaths = useMemo(() => new Set(['__custom__']), []); + const context = useMemo(() => ({ enableClickToDownload: true, installedPackages, }), [installedPackages]); - if (filteredBundles.every(b => b.manifest.files.length === 0)) + if (allBundles.every(b => b.manifest.files.length === 0)) return

No data packs found in {cluster.name}

; return (
- +
diff --git a/apps/oneclient/frontend/src/routes/app/cluster/mods.tsx b/apps/oneclient/frontend/src/routes/app/cluster/mods.tsx index 5109af44..a3ed78e1 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/mods.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/mods.tsx @@ -1,6 +1,7 @@ import type { ModpackFile } from '@/bindings.gen'; import type { ModCardContextApi } from '@/components'; import { ModCardContext, ModList } from '@/components'; +import { useCustomBundle } from '@/hooks/useCustomBundle'; import { bindings } from '@/main'; import { useCommandSuspense } from '@onelauncher/common'; import { createFileRoute } from '@tanstack/react-router'; @@ -28,19 +29,27 @@ function RouteComponent() { manifest: { ...b.manifest, files: b.manifest.files.filter(f => getFilePackageType(f) === 'mod') }, })), [bundles]); + const customBundle = useCustomBundle(bundles, installedPackages, cluster, 'mod'); + + const allBundles = useMemo(() => + customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles, + [filteredBundles, customBundle]); + + const customTogglePaths = useMemo(() => new Set(['__custom__']), []); + const context = useMemo(() => ({ enableClickToDownload: true, installedPackages, }), [installedPackages]); - if (filteredBundles.every(b => b.manifest.files.length === 0)) + if (allBundles.every(b => b.manifest.files.length === 0)) return

No mods found in {cluster.name}

; return (
- +
diff --git a/apps/oneclient/frontend/src/routes/app/cluster/resource-packs.tsx b/apps/oneclient/frontend/src/routes/app/cluster/resource-packs.tsx index def40e2b..fea360b5 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/resource-packs.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/resource-packs.tsx @@ -1,5 +1,6 @@ import type { ModCardContextApi } from '@/components'; import { ModCardContext, ModList } from '@/components'; +import { useCustomBundle } from '@/hooks/useCustomBundle'; import { getFilePackageType } from '@/routes/app/cluster/mods'; import { bindings } from '@/main'; import { useCommandSuspense } from '@onelauncher/common'; @@ -22,19 +23,27 @@ function RouteComponent() { manifest: { ...b.manifest, files: b.manifest.files.filter(f => getFilePackageType(f) === 'resourcepack') }, })), [bundles]); + const customBundle = useCustomBundle(bundles, installedPackages, cluster, 'resourcepack'); + + const allBundles = useMemo(() => + customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles, + [filteredBundles, customBundle]); + + const customTogglePaths = useMemo(() => new Set(['__custom__']), []); + const context = useMemo(() => ({ enableClickToDownload: true, installedPackages, }), [installedPackages]); - if (filteredBundles.every(b => b.manifest.files.length === 0)) + if (allBundles.every(b => b.manifest.files.length === 0)) return

No resource packs found in {cluster.name}

; return (
- +
diff --git a/apps/oneclient/frontend/src/routes/app/cluster/shaders.tsx b/apps/oneclient/frontend/src/routes/app/cluster/shaders.tsx index 358cb675..2cc99efe 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/shaders.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/shaders.tsx @@ -1,7 +1,8 @@ import type { ModCardContextApi } from '@/components'; import { ModCardContext, ModList } from '@/components'; -import { getFilePackageType } from '@/routes/app/cluster/mods'; +import { useCustomBundle } from '@/hooks/useCustomBundle'; import { bindings } from '@/main'; +import { getFilePackageType } from '@/routes/app/cluster/mods'; import { useCommandSuspense } from '@onelauncher/common'; import { createFileRoute } from '@tanstack/react-router'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; @@ -22,19 +23,27 @@ function RouteComponent() { manifest: { ...b.manifest, files: b.manifest.files.filter(f => getFilePackageType(f) === 'shader') }, })), [bundles]); + const customBundle = useCustomBundle(bundles, installedPackages, cluster, 'shader'); + + const allBundles = useMemo(() => + customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles, + [filteredBundles, customBundle]); + + const customTogglePaths = useMemo(() => new Set(['__custom__']), []); + const context = useMemo(() => ({ enableClickToDownload: true, installedPackages, }), [installedPackages]); - if (filteredBundles.every(b => b.manifest.files.length === 0)) + if (allBundles.every(b => b.manifest.files.length === 0)) return

No shaders found in {cluster.name}

; return (
- +
diff --git a/apps/oneclient/frontend/src/routes/app/route.tsx b/apps/oneclient/frontend/src/routes/app/route.tsx index 7d27e21e..47db046d 100644 --- a/apps/oneclient/frontend/src/routes/app/route.tsx +++ b/apps/oneclient/frontend/src/routes/app/route.tsx @@ -1,5 +1,6 @@ import type { PropsWithChildren } from 'react'; import { GameBackground, LoaderSuspense, Navbar } from '@/components'; +import { useAllClusterDirWatch } from '@/hooks/useClusterDirWatch'; import { useActiveCluster } from '@/hooks/useClusters'; import { getVersionInfoOrDefault } from '@/utils/versionMap'; import { createFileRoute, Outlet } from '@tanstack/react-router'; @@ -11,6 +12,8 @@ export const Route = createFileRoute('/app')({ }); function RouteComponent() { + useAllClusterDirWatch(); + return ( diff --git a/packages/core/src/api/cluster/sync.rs b/packages/core/src/api/cluster/sync.rs index f79586ba..e333f980 100644 --- a/packages/core/src/api/cluster/sync.rs +++ b/packages/core/src/api/cluster/sync.rs @@ -94,7 +94,12 @@ pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult = Vec::new(); let mut found_hashes = HashSet::new(); for package_type in PackageType::iter() { @@ -106,10 +111,11 @@ pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult