You've already forked AstralRinth
Compare commits
1483 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10c70c5f3e | |||
| 06a9f61afb | |||
| 1daf1f451b | |||
| 448f22bf1f | |||
| 9ff96b3318 | |||
| 9b78384226 | |||
| eba23d70ba | |||
| f23f220a0c | |||
| c3ca512395 | |||
| 49dd844fe1 | |||
| 56536abb24 | |||
| 97de723f5c | |||
| f5e2c8ad32 | |||
| 419caea01b | |||
| 916ee80fe3 | |||
| 91e1387f30 | |||
| ac5daad280 | |||
| 308fe9298c | |||
| 87eec7741b | |||
| f8ebcf8f17 | |||
| e9a917012f | |||
| 4c9ee5b1c4 | |||
| caeab46ca6 | |||
| 8e6004fdd5 | |||
| bc79e2a623 | |||
| 50b2b9567c | |||
| e0ea14226e | |||
| d2b85c9f8e | |||
| d33f00d2b1 | |||
| 1e53d3c44f | |||
| b3257a0614 | |||
| e9dd2e4dbb | |||
| f07b57a0a5 | |||
| 9d1b02de4d | |||
| 3165ab2a6c | |||
| af99cb8109 | |||
| f130a7e64b | |||
| 9678770cba | |||
| 486b467af2 | |||
| 336050f4df | |||
| 2bb1ef775c | |||
| b88bd246f8 | |||
| 24d082e6a6 | |||
| 2e28aef1a5 | |||
| a4fad0c1e2 | |||
| 5ed322d281 | |||
| 8591bc8ebc | |||
| 01fd18d550 | |||
| 3aaa2ef071 | |||
| a3aeeac2c3 | |||
| dfba212be4 | |||
| 3877999e53 | |||
| 46a7cf490d | |||
| b5f7406998 | |||
| 3679d2c786 | |||
| f48a2f64db | |||
| fe80ac10fc | |||
| cf0d260802 | |||
| 503d34ee0f | |||
| bfbe66f73b | |||
| 64e17c7c1b | |||
| c082594825 | |||
| e5831d38eb | |||
| 1cabfe3e85 | |||
| 36423eb5b5 | |||
| 7d15fd3ac0 | |||
| c1780eef7d | |||
| d2a66bb2b0 | |||
| 98b1730e19 | |||
| 180cef6eaa | |||
| b828fa17de | |||
| 72a4e86c26 | |||
| 93f8da1666 | |||
| f474940321 | |||
| 83b0586fd2 | |||
| 543d25e2d6 | |||
| bc5a761312 | |||
| 3258d7dbdf | |||
| b5d1aeda85 | |||
| 1cedbe5fda | |||
| 2c9bf58d1f | |||
| a92b5b08df | |||
| 01d3fb47c4 | |||
| 9404d46782 | |||
| 926c72de42 | |||
| 9729737d7d | |||
| 97a8c11b50 | |||
| 4d654162eb | |||
| 33b1419bdf | |||
| c28ba2e6a4 | |||
| c7ba6ba8b2 | |||
| 7366c32df3 | |||
| 707e219ff8 | |||
| dfe12d4ecb | |||
| c653228fe7 | |||
| 352a196795 | |||
| cef9b1efe5 | |||
| 7d6c54cff9 | |||
| bd97ace974 | |||
| 58ad58f958 | |||
| d907083d83 | |||
| 8371ff641a | |||
| b1cd16f966 | |||
| 40a06921ea | |||
| a7dc063e08 | |||
| 64b61d8fd0 | |||
| 5e7d4cc838 | |||
| a0c80b13a4 | |||
| 3c051f5b1d | |||
| 940a796ba5 | |||
| 6ee5e4df19 | |||
| d61397097c | |||
| cfe45b368c | |||
| 6b0a0c1897 | |||
| f27691340a | |||
| c3a58aba9e | |||
| 1550dfb3f0 | |||
| 4762a0a725 | |||
| cf82943766 | |||
| b527e8f8f2 | |||
| 3c53b5793b | |||
| e6afc6f4f3 | |||
| 05699a90d6 | |||
| 142d560f76 | |||
| e80e27884e | |||
| 75788938b5 | |||
| 2570cf1bd7 | |||
| b0cca873b6 | |||
| 325926ad9b | |||
| 34b87991bc | |||
| cc8d556448 | |||
| 71286f7b2b | |||
| c29973ec1a | |||
| 8c95f0bb81 | |||
| 627ab6734b | |||
| 36e1cbbf3e | |||
| 93fe87e57f | |||
| 02363c27a2 | |||
| 67e1743d6c | |||
| cef20abceb | |||
| 62cedab6dd | |||
| ed5a74a9d3 | |||
| e2318ee776 | |||
| a29c1c61c4 | |||
| 206813b74c | |||
| 99611d22c7 | |||
| 11b2b6e6c0 | |||
| f49951084e | |||
| 047b8c3bf7 | |||
| 5c1ffd9ff2 | |||
| 670b3c17b6 | |||
| ec7c538888 | |||
| b88341e3a8 | |||
| 2048d8008a | |||
| 527f2f800b | |||
| c7d3229fd3 | |||
| ec49a3b051 | |||
| cf1d948030 | |||
| fe8fa4b6f7 | |||
| 2c62cf1d12 | |||
| 84b91f32f8 | |||
| 64edf2ddeb | |||
| 2f248027d6 | |||
| f96520638b | |||
| 9e5d29ced6 | |||
| 3e15d0b287 | |||
| bcce7e28fd | |||
| 3889d0f5ec | |||
| 6f44c5b039 | |||
| ea967845d9 | |||
| a6967cf9cb | |||
| 5bf92863b0 | |||
| 71b6ecc10c | |||
| ed28bc7551 | |||
| 3e4197db7c | |||
| e12230ff59 | |||
| aeb9f5a075 | |||
| 8b17441f40 | |||
| f9d47e8edc | |||
| a58bc3dc21 | |||
| 1e46444fb0 | |||
| 1511e55597 | |||
| 5727e156ed | |||
| 657186398d | |||
| 3ab2273782 | |||
| 893ec00fc6 | |||
| aa7dd1d210 | |||
| f8733b0488 | |||
| d077d44540 | |||
| 4e1a61d8b6 | |||
| 71dee4de40 | |||
| f74fad0cae | |||
| 07e81ac036 | |||
| 6e7835fb35 | |||
| 2f95c4c441 | |||
| 451b2d0e44 | |||
| 215643c846 | |||
| d8b1415f9c | |||
| 3eeb549d20 | |||
| c3fe7b4232 | |||
| 079a10bba9 | |||
| 3c3d5702ba | |||
| a34576a2c3 | |||
| d8e4915a31 | |||
| ed723fa186 | |||
| d6c8d4475b | |||
| 302b60d89c | |||
| f106dc580f | |||
| 244c263e40 | |||
| 48bb44155d | |||
| 3f2e76ae7e | |||
| 6479eca0e9 | |||
| 8767bc9184 | |||
| d1185414b6 | |||
| 510439acbf | |||
| c564495e11 | |||
| 8dd1490c8a | |||
| b72bc18a6b | |||
| cee942dcef | |||
| 02a7774722 | |||
| e9eb98f97e | |||
| e5bbd9d409 | |||
| 83dddfd512 | |||
| 0ffdabb2a3 | |||
| d87f93fdd5 | |||
| f6fa486dea | |||
| 78d978b22e | |||
| 71559d62c8 | |||
| 538eda6976 | |||
| 882b01c7c9 | |||
| a192f7857e | |||
| 3bf0f91cf0 | |||
| 3083dcd932 | |||
| c8c79a6c74 | |||
| f5462b6dd8 | |||
| 1ddbae40b7 | |||
| a4f3c63fcc | |||
| 02e10be4db | |||
| e0056bfc40 | |||
| 612934bf34 | |||
| 86d377b915 | |||
| ad99ac039b | |||
| 6d3fdb680c | |||
| 840b556c51 | |||
| 12e5f02e57 | |||
| ca1b36efde | |||
| a5417e0851 | |||
| 45398c546c | |||
| 7e769c720b | |||
| c1c86e3b72 | |||
| c7602602e5 | |||
| 07f9e3aedc | |||
| a79b8e0777 | |||
| 671f6d264a | |||
| e231df1f97 | |||
| 3052a14d95 | |||
| e8665f43ca | |||
| cba4550be4 | |||
| 384556a810 | |||
| a082e8597c | |||
| 7048a35e9f | |||
| c166ce52b3 | |||
| 9c99518497 | |||
| 758ed818c8 | |||
| 83e45d7a5c | |||
| 77b30b27fe | |||
| 3d7aea5a45 | |||
| fd5d2797b3 | |||
| ec85d9de1c | |||
| 22415a4cc6 | |||
| 871672d8bf | |||
| e8dc3c3150 | |||
| 56dae8f104 | |||
| ae9ca4db18 | |||
| 4eeb53c429 | |||
| c69f24f94d | |||
| de07bcff7d | |||
| beff44767e | |||
| 5875e4332f | |||
| f9c078d29d | |||
| 99ac6b87b1 | |||
| 159c6205ef | |||
| bb4862daa6 | |||
| 6ed56a4756 | |||
| 93b21bb107 | |||
| b442fa4cca | |||
| 118046d690 | |||
| b6bca2aaeb | |||
| dcab665455 | |||
| 2f311643a0 | |||
| e13a89dd72 | |||
| 565ac2cb53 | |||
| 7d6f77bebf | |||
| b53887997c | |||
| 9a499af03e | |||
| f6edc3ab58 | |||
| c1d7aa494c | |||
| a4c8154438 | |||
| 7dbbbe590f | |||
| 8a72ee9968 | |||
| 2da2035a6f | |||
| 4c59a5e51d | |||
| 678f8049e3 | |||
| eb9c3477ff | |||
| f857d19aee | |||
| 5b59e39a8a | |||
| 9015ff0971 | |||
| 1fd58e0a5a | |||
| 4348664618 | |||
| 596fd81348 | |||
| 388ba61d15 | |||
| be618d96f4 | |||
| c2359275ff | |||
| edbb3fbd55 | |||
| 9403462915 | |||
| 264aade726 | |||
| 4ddb5640cf | |||
| 1875b89556 | |||
| 38a39feef1 | |||
| 7e149e1cf1 | |||
| ea723f719c | |||
| 5abcfe6c38 | |||
| e9dfe1b7f0 | |||
| dfb6814095 | |||
| a80cc7e47b | |||
| fc90e1098e | |||
| 747fe04888 | |||
| ab7f649177 | |||
| b39544b50e | |||
| d65e465543 | |||
| ba1d374be6 | |||
| 620894aecb | |||
| 85ae1f2074 | |||
| 3f8fd9cb56 | |||
| a2eed001b2 | |||
| 6afda48e70 | |||
| e8be67d41f | |||
| 548357c92c | |||
| 453369ca07 | |||
| faf593b2af | |||
| e3d6a498d0 | |||
| 42cdcc7df9 | |||
| e043a232bc | |||
| c44ead2dbe | |||
| 11ac27f71f | |||
| 6862cf5ab2 | |||
| 16e1bf4611 | |||
| 77e4c41480 | |||
| cb93c641d6 | |||
| eebb353547 | |||
| 694ab09a01 | |||
| da47c50320 | |||
| bee4391df1 | |||
| d1b122fb21 | |||
| 281bf066de | |||
| 68fde3ff97 | |||
| 7d6d938b99 | |||
| 065759d1b8 | |||
| 9b3fe6390e | |||
| 2236dd8ade | |||
| 3a44def301 | |||
| 176d4301c3 | |||
| 3e32901737 | |||
| ab623dc325 | |||
| bb6e24640c | |||
| 15fc6d4e38 | |||
| ed2f04322f | |||
| b9e7b54b4e | |||
| dd51c08a18 | |||
| 5244060588 | |||
| 9483656881 | |||
| 7c642f7078 | |||
| 3c2cc7568d | |||
| 7b5c746757 | |||
| 5b5c8c06e3 | |||
| 4d68f3cea4 | |||
| 546b117437 | |||
| 3d5f29a7a2 | |||
| 37b0f7ff98 | |||
| 1603796856 | |||
| 7c9a9f22d4 | |||
| d38d23dbf3 | |||
| f12bd7b4b8 | |||
| baee34b0b6 | |||
| 74bad7456c | |||
| a6f67581d7 | |||
| 02be7a8b82 | |||
| d5ad1cb823 | |||
| b666747bc2 | |||
| c1b0e4a692 | |||
| 0713814d0c | |||
| 0b8b4fb516 | |||
| d6e366b488 | |||
| 0682cc3c4f | |||
| 693a371d61 | |||
| a2a97d1313 | |||
| 71e4f7cb91 | |||
| 1a51e58297 | |||
| a9c417d1d1 | |||
| de4f0bffb0 | |||
| e71a8c10fa | |||
| c4b3c6e8d6 | |||
| 54c45ac9f3 | |||
| e5f600ddd7 | |||
| 4a7525d0a1 | |||
| d0d3aaf09b | |||
| bb3506823d | |||
| 3091021194 | |||
| dc96043adc | |||
| 92bf2e5c29 | |||
| a6d359e9c1 | |||
| ddd1a36506 | |||
| 933fdba388 | |||
| fa4711ff7b | |||
| c52abece44 | |||
| 35033ccc03 | |||
| 97734a98f9 | |||
| 3978e3dead | |||
| 5939d2a4e7 | |||
| 4224ef45b3 | |||
| e2bfed177d | |||
| 7f92706e5f | |||
| 90deb7310e | |||
| 4cd6c1a72d | |||
| 3a8561cf35 | |||
| a3f80dcb66 | |||
| 0ee58867e8 | |||
| d4c2fdb9ef | |||
| e6b061f38c | |||
| 87122cf9bd | |||
| fc87506745 | |||
| 6ba41ba17f | |||
| 628634772e | |||
| b68aeddedc | |||
| c5a0c71424 | |||
| 4394092928 | |||
| ef1ffa6577 | |||
| b11b54cbc9 | |||
| 36f62a3285 | |||
| 381ea51cce | |||
| 706eb800cb | |||
| f1648298c4 | |||
| 3c3cde1908 | |||
| 274325d97c | |||
| da48a12551 | |||
| bf24ed8d12 | |||
| 0731654a1c | |||
| 81f19eeb8d | |||
| 4b4282cfbf | |||
| 7b3471944d | |||
| d2abeb434c | |||
| 0aecfa3140 | |||
| f9004dc2f6 | |||
| cf7c77700a | |||
| c09f7fd5e6 | |||
| 67fd759d9b | |||
| a3eb981058 | |||
| 92eddbe832 | |||
| 9e6a6cd385 | |||
| 3c5bd0756d | |||
| 00e81adbbd | |||
| 2128fa7ade | |||
| 93c81631a9 | |||
| 3b604cfdc0 | |||
| 922b72d1a4 | |||
| 1d10af09f5 | |||
| cf1b5f5e2d | |||
| 61754efca4 | |||
| 235934abd7 | |||
| 22d1b900f6 | |||
| 1cfbefff02 | |||
| 7852529915 | |||
| c556624d0e | |||
| 87c86c7d0d | |||
| 58c1e225c8 | |||
| 900a4df1b7 | |||
| 5b968a1486 | |||
| 3a917631d5 | |||
| 63ea8230ba | |||
| 496bbae8a0 | |||
| 1848ba3b29 | |||
| 681ae5d1d8 | |||
| d0c7575a23 | |||
| d9c7608ade | |||
| 7d3935a38d | |||
| a67f596524 | |||
| 7fa0a277c6 | |||
| 01c9dee612 | |||
| d50a8efb26 | |||
| be5ebacd84 | |||
| 989f282de3 | |||
| 8a2125ef16 | |||
| 31b541007d | |||
| 4792985e52 | |||
| 86c0937616 | |||
| 51deba8cd1 | |||
| b2d40af9cd | |||
| fc382e957b | |||
| c9547bb988 | |||
| adef71b89a | |||
| c44cc38b3a | |||
| 455a4f527d | |||
| f918df2d7a | |||
| c8279481f8 | |||
| d14360aba5 | |||
| 991b4d8c13 | |||
| cc9059fb4a | |||
| 32d76b8025 | |||
| ba06c89a0e | |||
| 52d46b8aaa | |||
| bdc204eebd | |||
| 7d92e4ec7f | |||
| f0224dfff7 | |||
| 1c1683adb6 | |||
| 407e6217f5 | |||
| 83ea7f684b | |||
| 3b21944a75 | |||
| 086508be23 | |||
| 8b04303eca | |||
| 2b8175ad66 | |||
| 0c98f6bf45 | |||
| 9a8712c76e | |||
| f62c60a681 | |||
| 9b2f0c88cd | |||
| 01b8ee6909 | |||
| 5a51a755eb | |||
| 4cfac2c8a2 | |||
| f6fcdd336f | |||
| 5594771ad8 | |||
| 5d04992a28 | |||
| c9c8079853 | |||
| 97051cc64d | |||
| 0a04478149 | |||
| 789ec8966c | |||
| 51a83b4536 | |||
| 73abe272d1 | |||
| 4a0c610fc5 | |||
| 913dee9090 | |||
| 43eb53eda5 | |||
| c381adff85 | |||
| ace2659861 | |||
| 507d03eeba | |||
| d4932d3089 | |||
| 4b6de7526c | |||
| b95e4ced22 | |||
| 200b4f56c6 | |||
| 83d53dafe7 | |||
| 98175a58a6 | |||
| 20cbe1ad8f | |||
| 9d5d34fde8 | |||
| 2d5c26896f | |||
| ea3bb334a8 | |||
| 024e079a7d | |||
| d902b281f7 | |||
| c4a0008708 | |||
| 835f80ee50 | |||
| 155f4091a6 | |||
| e1ee9c364b | |||
| 0029a22569 | |||
| 211ec20970 | |||
| 34997bada5 | |||
| 63daac917b | |||
| 51ceb9d851 | |||
| 51066c476a | |||
| 47ef7ee42e | |||
| 6fba33d443 | |||
| 017f6a5afb | |||
| 1ab722411a | |||
| 45387e5fb6 | |||
| e362de45fb | |||
| bacc10d2f5 | |||
| 5b49af1fe8 | |||
| f052ecd702 | |||
| 33ff2a0759 | |||
| ec81bcb13c | |||
| b6b4bc21f1 | |||
| 9a83db2e67 | |||
| 30c48718e2 | |||
| 8328a0d61a | |||
| b62bc6f3b8 | |||
| 0e752ab415 | |||
| 9f558404bd | |||
| 4be2f77bb0 | |||
| b3fbd884e0 | |||
| 8c0edf669d | |||
| f01c901445 | |||
| 2a91fc31f1 | |||
| d1e4c1039f | |||
| 9432d6d5e8 | |||
| c053c00bd7 | |||
| 9d0df74475 | |||
| 09e989a4c4 | |||
| d4ef5f36c3 | |||
| a9e0655859 | |||
| e7eb4899a1 | |||
| 76ba11d966 | |||
| f22e49e4f5 | |||
| 3c1bd86dcc | |||
| 4adbd0b843 | |||
| cec35dcb60 | |||
| a536d795f3 | |||
| e3e04931cf | |||
| 3e505e0d96 | |||
| dd4b054d95 | |||
| e80d7730ca | |||
| 0facf26b04 | |||
| 37eac92329 | |||
| 90438a1ad5 | |||
| e962521492 | |||
| 45a397d52b | |||
| d9d7750781 | |||
| 1101e71fdd | |||
| 34b4ae283e | |||
| a8c5e036d0 | |||
| 4eb0f0c206 | |||
| a3bc35c303 | |||
| 57e012f9b7 | |||
| 8ab1895d8a | |||
| 13e5529f00 | |||
| 428efde36d | |||
| a978873bff | |||
| b005c1f522 | |||
| b6c22d6ca6 | |||
| 50064c4ed6 | |||
| 48248eafdc | |||
| b34564a770 | |||
| d713cea180 | |||
| 695233e736 | |||
| 8789d7b057 | |||
| 510ea6cde4 | |||
| b1954be2c7 | |||
| 9105a68923 | |||
| 06e2f59a94 | |||
| ddb013e024 | |||
| 3f5e3b1d8b | |||
| 323090966b | |||
| 16204d30f8 | |||
| 34cbc7e0c1 | |||
| 5d6593a9da | |||
| ab753a82bc | |||
| 880e759336 | |||
| 84b386141c | |||
| e9a4cc60ca | |||
| d5869c514e | |||
| de0a03b2e9 | |||
| 3f6c79b00d | |||
| 56c8bb1950 | |||
| f81f951814 | |||
| 345ada27c0 | |||
| e3395a7366 | |||
| 3552c8280b | |||
| 11f00be606 | |||
| a207daef0d | |||
| ddd395ed4f | |||
| 04a7ae55e7 | |||
| 5a33d462f6 | |||
| cefa7b90ed | |||
| 0875a8a0bc | |||
| 5624e86c2b | |||
| c622a9281f | |||
| 937d385241 | |||
| 3ec9bcf7e7 | |||
| 3dd5314062 | |||
| d77823c5f4 | |||
| 41b48bd353 | |||
| 6225ece6be | |||
| 3f8805b953 | |||
| 6d68c0983f | |||
| 3ac0eb896c | |||
| 9c309e6da2 | |||
| deaa57fa15 | |||
| 8053fec29f | |||
| 78aca7e5c0 | |||
| 728f8db7b9 | |||
| 4c14339b4b | |||
| 16ac2aae6b | |||
| 6d68d50699 | |||
| 400c571fe6 | |||
| e57c15b3ce | |||
| 7cb7e881fa | |||
| 2cf82349a5 | |||
| 19a26942af | |||
| 638bb55649 | |||
| eef238c1bb | |||
| 3e5ef753e0 | |||
| 6d1b0eef15 | |||
| 75754230a9 | |||
| e9bc01b0c7 | |||
| 572800d9ca | |||
| 03658b6a62 | |||
| f998a8dca5 | |||
| 0d6bee3a5b | |||
| 7ce334aeb2 | |||
| fe3a1b2058 | |||
| 662b73211a | |||
| c453a03a02 | |||
| 05f8113bd0 | |||
| 63dfc3f636 | |||
| c96a303d8a | |||
| 2d7e87a4cb | |||
| fa421b4b83 | |||
| 79217e78b4 | |||
| bdd808c279 | |||
| 986a7e6216 | |||
| b54fcaa0b1 | |||
| 1cf782c298 | |||
| fb1050e409 | |||
| 5c29a8c7dd | |||
| 09dead50d2 | |||
| 772e0ee220 | |||
| 86b0de3cee | |||
| d174d96b74 | |||
| 1d193ed01b | |||
| adf365d99d | |||
| f3f48c3c6f | |||
| e072f2237b | |||
| 306eee3a21 | |||
| ca1d66d070 | |||
| 7e1400d111 | |||
| 7595e77170 | |||
| 08fcc61d35 | |||
| b36801c5ed | |||
| 4ed1a1ae7f | |||
| 04db01cb55 | |||
| 8f5185de1c | |||
| 9f6db31785 | |||
| a869086ce9 | |||
| 2af6a1b36f | |||
| d4381f513f | |||
| a281f13f15 | |||
| a9641dadff | |||
| c94dde9b47 | |||
| 976644d1e6 | |||
| 11fe90a69b | |||
| 1108086854 | |||
| 5aea892a39 | |||
| 3f3e6f5199 | |||
| 2efcd383bb | |||
| faec9c2965 | |||
| a0e8c7f924 | |||
| 6efdfdf17e | |||
| aec268c6e9 | |||
| 2c096a85d6 | |||
| 240e5455cc | |||
| c538a9ec6d | |||
| 72458f5c41 | |||
| 82e4eb7b40 | |||
| c9bfc4e9b6 | |||
| 1ea96df00e | |||
| 75c5316dc3 | |||
| 4ee7623837 | |||
| f65479ee15 | |||
| a903e46be9 | |||
| 4497131206 | |||
| 4871abfb3a | |||
| b0ed808745 | |||
| 169224560b | |||
| 106edc3a51 | |||
| ede405c650 | |||
| 1dd1629884 | |||
| 0070c9877b | |||
| eb208eed8b | |||
| c37bf75853 | |||
| 7838008396 | |||
| 454c708fd6 | |||
| 3ffa78aa07 | |||
| 7dba9cbe54 | |||
| 716c4e9a21 | |||
| f85a2d3ec1 | |||
| d055dc68dc | |||
| 50a87ba933 | |||
| 6030cb560c | |||
| c498230ebf | |||
| 4bbc5905e4 | |||
| 40f5db64d8 | |||
| 8d72a42be5 | |||
| 61c8cd75cd | |||
| b46f6d0141 | |||
| 915d8c68bf | |||
| 21045142cd | |||
| f171752109 | |||
| 82f00f961e | |||
| b55b7fdc1c | |||
| 0b60060f65 | |||
| b0f1266a8b | |||
| 863ff2e228 | |||
| 579aa5967f | |||
| 3bf5a6ebec | |||
| ff222aa168 | |||
| ea17534f77 | |||
| b91d581928 | |||
| 4f6cb7f26c | |||
| c1da3e7e95 | |||
| 040c568fdb | |||
| 7a78565c97 | |||
| 62e56eb27e | |||
| 8175120c4c | |||
| 6221fe5e08 | |||
| 4a8f882063 | |||
| 17db55a0bc | |||
| a1d9268d00 | |||
| 14d227a1a3 | |||
| 3fd6ce1b6d | |||
| 7eb1b38cc7 | |||
| 2e9730ea1f | |||
| a6cd4dfc0f | |||
| 0cf28c6392 | |||
| 7c2327ce16 | |||
| 099011a177 | |||
| 61d4a34f0f | |||
| 5b890dcd8a | |||
| c2bd88377b | |||
| efcc0d87b5 | |||
| f3033956cf | |||
| e26291943c | |||
| 3fc18feacf | |||
| 09a0b34df3 | |||
| 937be840c4 | |||
| fef6df1321 | |||
| daf804947c | |||
| 477d77cdc1 | |||
| 9bb012a439 | |||
| d1650bb3c4 | |||
| 2ce22c18bf | |||
| b48443c65b | |||
| b7e7e5e603 | |||
| fca5b7b544 | |||
| 3a40ee8713 | |||
| 9e4317a262 | |||
| 7fb6401613 | |||
| d332032e53 | |||
| 560f21c0fe | |||
| 2f99628d94 | |||
| ad3edf541b | |||
| b07a1659b4 | |||
| 1a16d61511 | |||
| 366a0a6366 | |||
| 91b08e7380 | |||
| 9924faab84 | |||
| 9f356beec3 | |||
| afe5f773e0 | |||
| 3e246f12de | |||
| 042451bad6 | |||
| 30106d5f82 | |||
| e0d159c010 | |||
| 45519f5dbb | |||
| 3843ed6690 | |||
| 061c52c274 | |||
| 1bbb01bd42 | |||
| 3cabc3b967 | |||
| 7de4e55bad | |||
| 1f21d66140 | |||
| 3adee66899 | |||
| 67a6cd24cc | |||
| a952318c77 | |||
| 336832ec40 | |||
| 543bd5acf7 | |||
| 6a0bf5858e | |||
| 11a75e7657 | |||
| 88635d8da8 | |||
| 934936eba8 | |||
| 53ec2c5306 | |||
| cace1a54cd | |||
| 803c17de31 | |||
| 537eadef0c | |||
| 39f2b0ecb6 | |||
| 1e9e13aebb | |||
| 67835b04a8 | |||
| 3f93041ca2 | |||
| 0663b8adb0 | |||
| 1f48f5b5af | |||
| 0268600044 | |||
| 8fb38ba0f2 | |||
| 85c65e697d | |||
| 563997e060 | |||
| 2d5568ecec | |||
| a64c4201bb | |||
| 51d5ed771c | |||
| 539132a527 | |||
| 9958600121 | |||
| 9ad01723a2 | |||
| 8448bacae7 | |||
| c21e98a2a8 | |||
| 5bbc3872f3 | |||
| 8d894541e8 | |||
| dc16a65b62 | |||
| 514c6f6e34 | |||
| 609e3896eb | |||
| fd08dff1e7 | |||
| 6425ab8c57 | |||
| e123e51c66 | |||
| 21fad12a21 | |||
| 924a77eb3f | |||
| 7aaf99a0c8 | |||
| 91accd5578 | |||
| 147f19f11e | |||
| 73ff6df73c | |||
| 0de780b7c9 | |||
| f49f889536 | |||
| b3f598aa1d | |||
| cd1b5dcd3d | |||
| 79b7d269b0 | |||
| 40ac726930 | |||
| ddcc14d99f | |||
| 3dd2de5f18 | |||
| 0a8f489234 | |||
| 1d64b2e22a | |||
| 251e89fe5a | |||
| 4fbbc2b1cf | |||
| d5b7ac3542 | |||
| fec395a4cf | |||
| 16c0dadc4a | |||
| 779092c0b7 | |||
| 9aa06fbc26 | |||
| cfd2977c21 | |||
| 27fc0796a4 | |||
| b1438bd460 | |||
| 267e0cb636 | |||
| d471ef6763 | |||
| cea5cfa4ab | |||
| 56356e8260 | |||
| 41e4086973 | |||
| 0f1f27d450 | |||
| a558064f9d | |||
| c421249767 | |||
| 8eff939039 | |||
| e3444a3456 | |||
| 16a6f7b352 | |||
| 79c2633011 | |||
| 783aaa6553 | |||
| ddf51c9596 | |||
| a63b6b27d5 | |||
| 60e0953616 | |||
| f7c86f9fc9 | |||
| cac3b46652 | |||
| fe684ab903 | |||
| 8592761493 | |||
| dfe087df20 | |||
| 82119a9fc9 | |||
| b9ec1b42dc | |||
| 7345fa401b | |||
| be3208c5a1 | |||
| b56f39ce07 | |||
| 0178fddc38 | |||
| 31417a2aa1 | |||
| f333a75221 | |||
| bcf14a4c51 | |||
| 130c2863ab | |||
| e59664426b | |||
| 2f0ef07944 | |||
| 9af19d01e5 | |||
| e837d9fa30 | |||
| 93b79759c7 | |||
| 4becb2a822 | |||
| 134a621d0d | |||
| 089cca60ce | |||
| 20484ed7aa | |||
| 763a38812f | |||
| 7ccc32675b | |||
| 26feaf753a | |||
| 94c0003c19 | |||
| c27f787c91 | |||
| 70e2138248 | |||
| 590ba3ce55 | |||
| 29671347a0 | |||
| 386e6e50da | |||
| 880ed21bcd | |||
| bbc31ef077 | |||
| 9a13e977a0 | |||
| a5602ff18c | |||
| 5901c5a535 | |||
| cca1dd7e37 | |||
| 127e01cc96 | |||
| 1dcb38cb57 | |||
| 98b4970680 | |||
| 9706f1597b | |||
| f8a5a77daa | |||
| 1efdceacfd | |||
| b998c71337 | |||
| 6a6adb3480 | |||
| a694aeed32 | |||
| 8182b795de | |||
| 608ab988f0 | |||
| a261598e89 | |||
| 11a1918a2e | |||
| 67fb825937 | |||
| 4289f8b52d | |||
| fb1ba51a2b | |||
| cb47bc97c7 | |||
| 06e1bc9dd6 | |||
| af39a1769c | |||
| 60ffa75653 | |||
| 7674433f88 | |||
| 7437a833ef | |||
| 1bad1a57b0 | |||
| 3437387885 | |||
| 23d098eee5 | |||
| 4636372ff4 | |||
| 4592786de8 | |||
| f054f39c5d | |||
| 6e47de06bb | |||
| c38751a38a | |||
| 2d218d79c6 | |||
| 5a41a35716 | |||
| 644554f1e9 | |||
| 3765a6ded8 | |||
| 92698e4bb5 | |||
| 17f395ee55 | |||
| b11934054d | |||
| 40cbe92dbc | |||
| 9139c23469 | |||
| 932f4ce662 | |||
| 1fbd39c920 | |||
| 27abe2b42f | |||
| ece15a97a0 | |||
| 97a9c24768 | |||
| b7f0988399 | |||
| 4c1020d2ba | |||
| 00f9cf0e2c | |||
| 1dd7e3bcdc | |||
| 3ac3122b31 | |||
| 6b5f8a41e7 | |||
| 8b39ba491a | |||
| c74460fffa | |||
| 5000c4067b | |||
| af33950bbe | |||
| 075331b26c | |||
| f31b74f7fd | |||
| bcc36362be | |||
| 632b27dc21 | |||
| cf6f3736eb | |||
| aaaef8f39e | |||
| 3f8dd1a79c | |||
| 363f47f269 | |||
| a0f23a2bca | |||
| 08e316a2b2 | |||
| 9aaf5fb87e | |||
| bcca66b12c | |||
| ccb24ce8eb | |||
| 5dd6c804d0 | |||
| ab886a5ea8 | |||
| 03b0eba695 | |||
| 707ff2146b | |||
| 8d80433c2c | |||
| a547f7a9b0 | |||
| f78fbe3215 | |||
| f375913c62 | |||
| a4015d9df3 | |||
| 977de0e18a | |||
| c379e4b173 | |||
| eeed4e572d | |||
| 79502a19d6 | |||
| 3dbfd69bdd | |||
| 19393a38bb | |||
| 859d7f57cf | |||
| 24bec6baba | |||
| 63d8f70e20 | |||
| 8a30b7978d | |||
| 4a9f0b8a0e | |||
| 0e17427a58 | |||
| ad3b5aec69 | |||
| 4b17eb5d35 | |||
| 6a70acef25 | |||
| e58456eed4 | |||
| 12940fc207 | |||
| 7796273529 | |||
| 7cc9d8183d | |||
| 231e95792e | |||
| 905eae8403 | |||
| 868fda1703 | |||
| 4b86c4ee8a | |||
| 752f68124c | |||
| 699a049c69 | |||
| 8e720ccef5 | |||
| 03b49284e1 | |||
| ac6c26a5f9 | |||
| cc963cfc40 | |||
| fa7d1d7942 | |||
| d1ffed564d | |||
| e719ae2f42 | |||
| 5db5bf4c4c | |||
| b23d3e674f | |||
| 75e3994c6e | |||
| 71e28e1ea5 | |||
| 6dbd1e5236 | |||
| 77afdb1cc4 | |||
| 7fa442fb28 | |||
| 2535156dac | |||
| 8972c9a198 | |||
| 03ed64c99f | |||
| 4cd8ccd319 | |||
| ea594ec27c | |||
| 2a61916d1e | |||
| e66b131a5d | |||
| 0c66fa3f12 | |||
| aec49cff7c | |||
| c88bdda3e6 | |||
| 1a72d55e2d | |||
| 55eae7ec7e | |||
| 351b3da337 | |||
| 9ee0626e8b | |||
| e9735bd9ba | |||
| 15a7815ec3 | |||
| 6919c8dea9 | |||
| f32558cf97 | |||
| ad705fa66f | |||
| 87f8773401 | |||
| 3c578108de | |||
| cb5600ad45 | |||
| 59e48ea2b1 | |||
| 7658e1c653 | |||
| 9589e23118 | |||
| dbc64afe48 | |||
| 7e682c22bb | |||
| fd80f1217d | |||
| d98394d8d5 | |||
| e303655727 | |||
| 95de8977d4 | |||
| 92e91a0606 | |||
| 98269842f3 | |||
| ab6e9dd5d7 | |||
| a13647b9e2 | |||
| 2af7ecc077 | |||
| bea0ba017c | |||
| f874856452 | |||
| b96c5cd5ab | |||
| 7e84659249 | |||
| 24504cb94d | |||
| 6fe4235358 | |||
| 04f0f53104 | |||
| c169b48228 | |||
| beff2fcaa9 | |||
| 9315af9b20 | |||
| 4d11dc821b | |||
| 8fd40f46c5 | |||
| 28e9f017e3 | |||
| beb1bdb31f | |||
| 895b040ad7 | |||
| 54747aa628 | |||
| 53c9699b46 | |||
| 671fd22389 | |||
| bddc40e601 | |||
| 324ad65d7c | |||
| 7eace32d93 | |||
| a5108ecc5d | |||
| a538b99c18 | |||
| f6f66a313f | |||
| d5f756fd86 | |||
| b4eba5a0d5 | |||
| d418eaee12 | |||
| f466470d06 | |||
| 3f55711f9e | |||
| bb9ce52c9d | |||
| 14af3d0763 | |||
| d43451e398 | |||
| 2492b11ec0 | |||
| 4228a193e9 | |||
| 47020f34b6 | |||
| 5c00cb06f1 | |||
| e6edf07eae | |||
| 5d7bd3b177 | |||
| 71d63fbe17 | |||
| f33efed91b | |||
| d41b31c775 | |||
| 20281c4efc | |||
| afcdb1d0a1 | |||
| f3060cd9b4 | |||
| 1a1b9f54df | |||
| 716f293e8e | |||
| f5825f1065 | |||
| 5b44454e18 | |||
| b425c66832 | |||
| 0b8762cd0a | |||
| ff50964f25 | |||
| 36d0760a3e | |||
| 4def0e8407 | |||
| 6da190ed01 | |||
| 8149618187 | |||
| 902d749293 | |||
| 1491642209 | |||
| 7bc2c1dd4d | |||
| 9f11759292 | |||
| cef425b6be | |||
| 3fc55184a7 | |||
| 67e090565e | |||
| d8d9720495 | |||
| 9361acb78e | |||
| 58aac642a9 | |||
| af3b829449 | |||
| 567e31401d | |||
| b95ece04c4 | |||
| 3dfd035b50 | |||
| 01b19424cd | |||
| cb3130f998 | |||
| 4d3e1ade67 | |||
| 1b33a3619f | |||
| ea607c1a04 | |||
| fda06cfc60 | |||
| 0d61945956 | |||
| c017038f71 | |||
| a323bf6c25 | |||
| e2f07a7848 | |||
| 0511a14bd9 | |||
| 8f8a4af9eb | |||
| 9ed094a1e7 | |||
| aa6de3cc80 | |||
| f5aece1fb1 | |||
| 79aa41fd7a | |||
| bd918c7616 | |||
| d23b925bb9 | |||
| 8aaddb9d8a | |||
| f48eaee336 | |||
| 749fd32307 | |||
| 2e95a8a117 | |||
| 2194ae774c | |||
| 052637d402 | |||
| c1a092e55c | |||
| bd3342badf | |||
| d832ca1e5a | |||
| 5b7f025094 | |||
| d0c67b368a | |||
| c43d359561 | |||
| 8b2a89d4e0 | |||
| 8aede4e082 | |||
| 3d80201112 | |||
| 8d14f34994 | |||
| 6f34130633 | |||
| 5a699eec22 | |||
| 9fa490aa6a | |||
| d119b301d0 | |||
| 15c31f04a3 | |||
| 48e5319134 | |||
| 8058993578 | |||
| 28337c88f6 | |||
| a6d08e9d50 | |||
| 7943f77655 | |||
| dc4ef332f8 | |||
| 652f2e241f | |||
| 5fd27bcb65 | |||
| 8fa01b937d | |||
| 8b98087936 | |||
| 7afe35a6cd | |||
| debaf1381c | |||
| 697468e910 | |||
| 46c325f78a | |||
| 0ac42344e7 | |||
| df261dad95 | |||
| d30643b5a0 | |||
| ab95dcf951 | |||
| ab539a313f | |||
| a2c07c92f8 | |||
| 0925abfd1c | |||
| 8cf42471a3 | |||
| 006b19e3c9 | |||
| ca36d11570 | |||
| c612c8b009 | |||
| f9cf3d5ef9 | |||
| e7d933411e | |||
| 44cbbd9ed7 | |||
| 87dbb6dcbc | |||
| 3d1cafdcec | |||
| e114c7466e | |||
| 20059e6cf0 | |||
| 6b10b4d30b | |||
| a47dde972c | |||
| e8b0c9df4c | |||
| b8bc2c4cb6 | |||
| 328500d381 | |||
| f56672fb68 | |||
| d3459e4b12 | |||
| 07703e49ef | |||
| 08011161c8 | |||
| 9b29694907 | |||
| 805c0b86a5 | |||
| d19bf82cb1 | |||
| 2e6cff7efc | |||
| b2ff2d8737 | |||
| eaa4b44a16 | |||
| 76d0ef03e7 | |||
| ee8c47adcb | |||
| 5d3ca3ba02 | |||
| 235717b01c | |||
| 518f7adafb | |||
| 490b994d7b | |||
| 14eac461be | |||
| 9af1391e0e | |||
| bcfa6941e4 | |||
| 5ffe14f058 | |||
| 166d14e7e1 | |||
| b03d754a57 | |||
| 674f29959d | |||
| 3e735b99eb | |||
| 74d2d85cb5 | |||
| 3a92adfb82 | |||
| af4c627a04 | |||
| 1e725e6d03 | |||
| 1454e3351e | |||
| 6f59f4c110 | |||
| 8e0732bf01 | |||
| 0cf3c1a88e | |||
| 8a3171d7c4 | |||
| e25d726da4 | |||
| 11e99cb9d3 | |||
| 632b09ff3f | |||
| 713571d50e | |||
| 4ad6daa45c | |||
| 9b5f172170 | |||
| 4f789a0ebc | |||
| ee3ac37967 | |||
| 2aabcf36ee | |||
| 82697278dc | |||
| 0bc6502443 | |||
| 5ffcc48d75 | |||
| b81e727204 | |||
| 9ea43a12fd | |||
| b279c43069 | |||
| 9497ba70a4 | |||
| c02b809601 | |||
| 1d000bb238 | |||
| df1499047c | |||
| 80eb297284 | |||
| 58645b9ba9 | |||
| 544f63512a | |||
| 3b8cd661bc | |||
| 8af65f58d9 | |||
| ab79e84398 | |||
| cf190d86d5 | |||
| ca0c16b1fe | |||
| 17c9e4a721 | |||
| d7f1029b54 | |||
| ad208536b0 | |||
| 553db55c7b | |||
| d22c9e24f4 | |||
| e31197f649 | |||
| 0dee21814d | |||
| 0657e4466f | |||
| 13dbb4c57e | |||
| 4c6290ead6 | |||
| 99493b9917 | |||
| 72a52eb7b1 | |||
| b33e12c71d | |||
| 82d86839c7 | |||
| 3a20e15340 | |||
| 1c89b84314 | |||
| 8d36c14554 | |||
| 6387fb21c6 | |||
| c7d0839bfb | |||
| 2b43e26a85 | |||
| 175b90be5a | |||
| 13103b4950 | |||
| 8804478221 | |||
| b8982a6d17 | |||
| ff88724d01 | |||
| 7dffb352d5 | |||
| 1df6e29aa1 | |||
| 5deb4179ad | |||
| 358cf31c87 | |||
| 7cea4b21a8 | |||
| 7846fd00aa | |||
| cebc195fe0 | |||
| 6db1d66591 | |||
| 8052fda840 | |||
| ae58f3844d | |||
| acd4b1696a | |||
| 5ea78b78c2 | |||
| f90998157d | |||
| 634000cdb6 | |||
| 5fd8c38c1c | |||
| 15892a88d3 | |||
| 32793c50e1 | |||
| 0e0ca1971a | |||
| bb9af18eed | |||
| d4516d3527 | |||
| 87de47fe5e | |||
| 7d76fe1b6a | |||
| 46d30e491a | |||
| 059c0618f1 | |||
| 7ef60fcafe | |||
| ec17e79014 | |||
| e351d674f4 | |||
| f555fa916a | |||
| dbe38cb4e7 | |||
| 2e40e26116 | |||
| ae25a15abd | |||
| 0f755b94ce | |||
| bcf46d440b | |||
| 526561f2de | |||
| a8caa1afc3 | |||
| 98e9a8473d | |||
| 936395484e | |||
| 0c3e23db96 | |||
| 013ba4d86d | |||
| 93813c448c | |||
| c20b869e62 | |||
| 56c556821b | |||
| 44267619b6 | |||
| 10afd673db | |||
| 90043fe84d | |||
| a6a98ff63e | |||
| 911652133b | |||
| cee1b5f522 | |||
| 62f5a23fcb | |||
| 5a10292add | |||
| 3f606a08aa | |||
| 2d5d747202 | |||
| eb595cdc3e | |||
| 7516ff9e47 | |||
| 572cd065ed | |||
| df9bbe3ba0 | |||
| 362fd7f32a | |||
| 76dc8a0897 | |||
| 4723de6269 | |||
| e15fa35bad | |||
| 7716a0c524 | |||
| 2cc6bc8ce4 | |||
| 5d19d31b2c | |||
| c1b95ede07 | |||
| 058185c7fd | |||
| 6fb125cf0f | |||
| a945e9b005 | |||
| b943638afb | |||
| 207dc0e2bb | |||
| 359fbd4738 | |||
| adf831dab9 | |||
| efeac22d14 | |||
| 591d98a9eb | |||
| 77472d9a09 | |||
| 789d666515 | |||
| d917bff6ef | |||
| 4e69cd8bde | |||
| b71e4cc6f9 | |||
| a56ab6adb9 | |||
| f1b67c9584 | |||
| 3d32640b83 | |||
| 6dfb599e14 | |||
| 332a543f66 | |||
| 1ef96c447e | |||
| 1ec92b5f97 | |||
| f0a4532051 | |||
| f7700acce4 | |||
| 87a3e2d022 | |||
| 5d17663040 | |||
| cff3c72f94 | |||
| fadf475f06 | |||
| 7228499737 | |||
| bca467a634 | |||
| 14f6450cf4 | |||
| 14bf06e4bd | |||
| cb72d2ac80 | |||
| 3c79607d1f | |||
| 97bd18c7b3 | |||
| 8af0288274 | |||
| 167072de0c | |||
| 2df37be9a7 | |||
| 34d85a03b2 | |||
| 17cf5e3132 | |||
| 36ad1f16e4 | |||
| 5d4f334505 | |||
| 1fdb5ba748 | |||
| c5e67a5c6f | |||
| e2e21c1496 | |||
| 6da942ccbb | |||
| 26df6f51ef | |||
| 6caf794ae1 | |||
| 2692953e31 | |||
| 242fd713ab | |||
| 7a12c4d5e2 | |||
| 0ab4dec62d | |||
| f256ef43c0 | |||
| 3ecb20afd6 | |||
| 1e10f24efe | |||
| 006fd7c7f5 | |||
| 1e8e001eb8 | |||
| 585935c799 | |||
| e0cde2d6ff | |||
| a64c3360d2 | |||
| e4e77dc0d2 | |||
| 8ba6467f21 | |||
| a2b2711204 | |||
| 088cb54317 | |||
| ab57926e44 | |||
| 35cd79727a | |||
| c47bcf665d |
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||||
|
|
||||||
[build]
|
[target.x86_64-pc-windows-msvc]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
linker = "rust-lld"
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: api-module
|
||||||
|
description: Add a new API endpoint module to packages/api-client from an OpenAPI schema. Use when adding new backend endpoints, creating API client modules, or when an openapi.yml is provided.
|
||||||
|
argument-hint: <path-to-openapi.yml>
|
||||||
|
---
|
||||||
|
|
||||||
|
Refer to the standard: @standards/frontend/ADDING_API_MODULES.md
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Read the OpenAPI schema** at `$ARGUMENTS` — identify the endpoints, request/response shapes, and path parameters.
|
||||||
|
2. **Read the standard above** for naming conventions, type rules, and the module registration pattern.
|
||||||
|
3. **Determine the service and version** — the URL path prefix tells you which service directory and version namespace to use (e.g. `/v3/projects` → `labrinth/v3/`).
|
||||||
|
4. **Define types in `types.ts`** — types must match the API response 1:1. Use the OpenAPI schema as the source of truth. Do not reshape or rename fields.
|
||||||
|
5. **Create the module class** — extend `BaseModule`, implement each endpoint as a method. Use the correct HTTP verb and request options pattern from the standard.
|
||||||
|
6. **Register in `MODULE_REGISTRY`** — add the module entry so it's auto-instantiated on the client.
|
||||||
|
7. **Export types** from the service's barrel `index.ts`.
|
||||||
|
8. **Verify** — check that the module compiles and the types are accessible from `@modrinth/api-client`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: cross-platform-pages
|
||||||
|
description: Convert a page to the cross-platform page system so it works in both the website and the desktop app. Use when moving a page into packages/ui/src/layouts/, creating shared or wrapped layouts, or setting up DI contracts for platform abstraction.
|
||||||
|
argument-hint: <path-to-page>
|
||||||
|
---
|
||||||
|
|
||||||
|
Refer to the standards: @standards/frontend/CROSS_PLATFORM_PAGES.md and @standards/frontend/DEPENDENCY_INJECTION.md
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Read the target page** at `$ARGUMENTS` and understand its data sources, mutations, and navigation.
|
||||||
|
2. **Read the standards above** to understand the shared vs wrapped distinction and the DI pattern.
|
||||||
|
3. **Decide the category:**
|
||||||
|
- **Wrapped** (`layouts/wrapped/`) — if the page uses the same API source on both platforms (e.g. web requests, not Tauri plugins). Just move the page component into `packages/ui` and import it from both frontends.
|
||||||
|
- **Shared** (`layouts/shared/`) — if the page has different data-fetching logic per platform (e.g. website uses `api-client`, app uses Tauri `invoke`). Requires a DI contract.
|
||||||
|
4. **For shared layouts:**
|
||||||
|
- Define a DI contract interface in `providers/` capturing all platform-specific operations.
|
||||||
|
- Create the layout component that injects the context and handles all UI logic.
|
||||||
|
- Extract reusable stateful logic (search, filtering, selection) into `composables/`.
|
||||||
|
- Implement the contract separately in each frontend (`apps/frontend/`, `apps/app-frontend/`).
|
||||||
|
5. **For wrapped pages:**
|
||||||
|
- Move the page component into `packages/ui/src/layouts/wrapped/` matching the route structure.
|
||||||
|
- Replace any platform-specific imports with shared utilities.
|
||||||
|
- Import and render the wrapped page from both frontends as a simple component.
|
||||||
|
- If the layout uses TanStack Query for initial route paint with `ReadyTransition` / `useReadyState`, each platform route shell must call `ensureQueryData` for those queries with matching keys and fetchers — see **Platform route shells: prefetch with `ensureQueryData`** in `standards/frontend/CROSS_PLATFORM_PAGES.md`.
|
||||||
|
6. **Verify** the page renders correctly by checking for missing imports and that all DI contracts are satisfied.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: figma-mcp
|
||||||
|
description: Use the Figma MCP server to translate a Figma design into a Vue page or component layout. Use when the user provides a Figma URL, asks to implement a design, or wants to draft a page layout from Figma.
|
||||||
|
argument-hint: <figma-url>
|
||||||
|
---
|
||||||
|
|
||||||
|
Refer to the standard: @standards/frontend/FIGMA_MCP_USAGE.md
|
||||||
|
Also read @packages/ui/CLAUDE.md for color token mapping and component conventions.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Parse the Figma URL** from `$ARGUMENTS` — extract the `fileKey` and `nodeId`. Convert `-` to `:` in the node ID.
|
||||||
|
2. **Read the standards above** for the available tools, adaptation rules, and color usage.
|
||||||
|
3. **Call `get_design_context`** with the extracted `nodeId` and `fileKey`, using `clientLanguages: "typescript,html,css"` and `clientFrameworks: "vue"`. This is always the first tool to call.
|
||||||
|
5. **Adapt the output to the Modrinth codebase:**
|
||||||
|
- Map Figma color variables to `surface-*` / `text-*` tokens — never use Figma's aliased names directly.
|
||||||
|
- Check `packages/ui/src/components/` for existing components that match elements in the design (buttons, cards, modals, inputs, etc.).
|
||||||
|
- Check `packages/assets/styles/variables.scss` for tokens not exposed in Figma.
|
||||||
|
- Match spacing values exactly from the design.
|
||||||
|
6. **Use `get_screenshot`** if you need a closer visual reference of specific nodes.
|
||||||
|
7. **Use `get_variable_defs`** to verify which design tokens are applied to ambiguous elements.
|
||||||
|
8. **Build the component** as a Vue SFC using Tailwind classes and the project's existing component library.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: i18n-pass
|
||||||
|
description: Perform an i18n localization pass on changed files or a pull request, converting hard-coded English strings to the @modrinth/ui i18n system. Use when internationalizing a set of changes, reviewing a PR for untranslated strings, or converting a specific component.
|
||||||
|
argument-hint: [file-path-or-pr-number]
|
||||||
|
---
|
||||||
|
|
||||||
|
Refer to the standard: @standards/frontend/INTERNATIONALIZATION.md
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Identify the scope of changes:**
|
||||||
|
- If `$ARGUMENTS` is a PR number, run `gh pr diff $ARGUMENTS` to get the changed files.
|
||||||
|
- If `$ARGUMENTS` is a file path, use that directly.
|
||||||
|
- If no argument, check `git diff` for uncommitted changes.
|
||||||
|
2. **Read the standard above** for the message definition pattern, ICU format rules, and `IntlFormatted` usage.
|
||||||
|
3. **Filter to Vue SFCs** — only `.vue` files need i18n passes. Skip non-component files.
|
||||||
|
4. **For each file, scan for hard-coded strings:**
|
||||||
|
- `<template>`: inner text, `alt`, `placeholder`, `aria-label`, button labels, tooltip text.
|
||||||
|
- `<script>`: string literals passed to user-visible UI (notification messages, dropdown labels, error messages).
|
||||||
|
- Skip: dynamic expressions, HTML tag names, CSS classes, internal identifiers, log messages.
|
||||||
|
5. **Define messages** with `defineMessages` — use descriptive, stable `id`s based on the component's domain (e.g. `project.settings.title`).
|
||||||
|
6. **Replace strings in templates** with `formatMessage()` calls, or `<IntlFormatted>` for strings containing links or markup.
|
||||||
|
7. **Handle ICU edge cases** — add a space before `}}` if an ICU placeholder ends at a Vue template delimiter boundary.
|
||||||
|
8. **Verify** no hard-coded English strings remain in the changed templates. Do not alter logic, layout, or reactivity.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: review-changelog
|
||||||
|
description: Review the latest changelog entry in packages/blog/changelog.ts against the project's changelog style guide and flag bullets that need rewriting. Use when checking a freshly added changelog entry before opening a PR, or when the user asks to review/lint the latest changelog.
|
||||||
|
argument-hint: [product?]
|
||||||
|
---
|
||||||
|
|
||||||
|
Refer to the standard: @standards/maintaining/CHANGELOG.md
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Locate the latest entry:**
|
||||||
|
- Open `packages/blog/changelog.ts`.
|
||||||
|
- The latest entries are at the top of the `VERSIONS` array.
|
||||||
|
- If `$ARGUMENTS` specifies a product (`web`, `hosting`, `app`), review the most recent entry for that product. Otherwise, review the most recent entry overall, plus any sibling entries sharing the same `date` (coordinated releases ship together).
|
||||||
|
|
||||||
|
2. **Read the standard above** in full before reviewing. The bullet rules, section/verb agreement, and "Don't" list are the source of truth.
|
||||||
|
|
||||||
|
3. **Check the entry shell:**
|
||||||
|
- `date` is a valid ISO 8601 timestamp.
|
||||||
|
- `product` is one of `web`, `hosting`, `app`.
|
||||||
|
- `version` is present for `app` entries and omitted for `web`/`hosting`.
|
||||||
|
- Section headings use `## Added`, `## Changed`, `## Fixed`, `## Security` (or a featured-release linked heading). Flag legacy `## Improvements`.
|
||||||
|
|
||||||
|
4. **Review each bullet** against the standard. For each bullet, check:
|
||||||
|
- Voice/tense matches the section heading.
|
||||||
|
- Opening verb agrees with its section.
|
||||||
|
- Describes observable behavior, not implementation.
|
||||||
|
- Specific enough to identify the surface (names the tab/page/modal).
|
||||||
|
- One sentence, ends with a period, sentence case.
|
||||||
|
- Uses branded names (Modrinth App, Modrinth Hosting) correctly.
|
||||||
|
- No filler ("issue with", "issue where", "various", "some"), no vague intensifiers, no apologies, no PR/commit references (unless crediting a third-party contributor with a linked GitHub profile).
|
||||||
|
- Not a duplicate sub-fix of a bigger change already listed.
|
||||||
|
|
||||||
|
5. **Report findings** as a short list grouped by entry. For each problem bullet, show the original line and a suggested rewrite. If the entry is clean, say so explicitly. Do not edit the file unless the user asks - this skill is a review pass, not a rewrite pass.
|
||||||
|
|
||||||
|
6. **If the user then asks to apply fixes**, edit `packages/blog/changelog.ts` directly using the suggested rewrites. Preserve tab indentation and template literal formatting.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: tanstack-query
|
||||||
|
description: Convert a page or component from useAsyncData/manual ref patterns to TanStack Query for server state management. Use when migrating data fetching to useQuery/useMutation, adding cache invalidation, or replacing useAsyncData with TanStack Query.
|
||||||
|
argument-hint: <path-to-file>
|
||||||
|
---
|
||||||
|
|
||||||
|
Refer to the standard: @standards/frontend/FETCHING_DATA.md
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Read the target file** at `$ARGUMENTS` and identify all data-fetching patterns: `useAsyncData`, `useFetch`, manual `ref()` + `await`, or `onMounted` fetch calls.
|
||||||
|
2. **Read the standard above** for the query/mutation patterns, query key conventions, and optimistic update approach.
|
||||||
|
3. **Convert queries:**
|
||||||
|
- Replace `useAsyncData` / `useFetch` / manual fetches with `useQuery`.
|
||||||
|
- Use the `api-client` via `injectModrinthClient()` for the `queryFn`.
|
||||||
|
- Design query keys with the `['resource', 'version', ...params]` convention.
|
||||||
|
- Use `computed` query keys for reactive parameters.
|
||||||
|
- Use the `enabled` option for conditional queries that depend on other data.
|
||||||
|
4. **Convert mutations:**
|
||||||
|
- Replace manual `try/catch` + `ref` patterns with `useMutation`.
|
||||||
|
- Add `onSuccess` handlers that invalidate or update related query caches.
|
||||||
|
- Consider optimistic updates for UI-critical mutations (follow the pattern in the standard).
|
||||||
|
5. **Clean up:**
|
||||||
|
- Remove manual loading/error `ref()`s that are now handled by TanStack Query's return values (`isPending`, `isError`, `error`).
|
||||||
|
- Remove manual `onMounted` fetch calls.
|
||||||
|
- Ensure SSR compatibility — queries in Nuxt pages are automatically awaited during SSR.
|
||||||
|
6. **Verify** the page still renders correctly and that cache invalidation triggers re-fetches where expected.
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
.gitignore
|
||||||
+12
-5
@@ -3,16 +3,23 @@ root = true
|
|||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = tab
|
||||||
indent_size = 2
|
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
max_line_length = 100
|
max_line_length = 100
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
|
indent_size = 2
|
||||||
max_line_length = off
|
max_line_length = off
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.{rs,java,kts}]
|
[*.{toml,json}]
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
|
|
||||||
|
# YAML requires space indentation by spec
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_style = space
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
name: "🐞 AstralRinth Bug Report"
|
||||||
|
description: Report a bug specific to the AstralRinth fork, launcher, or fork integrations.
|
||||||
|
labels: [bug, astralrinth]
|
||||||
|
type: bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Use this form only for issues specific to AstralRinth.
|
||||||
|
If the problem is upstream Modrinth behavior and not caused by this fork, mention that clearly in the description.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirmations
|
||||||
|
attributes:
|
||||||
|
label: Please confirm the following
|
||||||
|
options:
|
||||||
|
- label: I checked the existing issues for duplicates
|
||||||
|
required: true
|
||||||
|
- label: I reproduced this on the latest AstralRinth build available to me
|
||||||
|
required: true
|
||||||
|
- label: I understand this tracker is for AstralRinth-specific issues, not general Modrinth support
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: issue-type
|
||||||
|
attributes:
|
||||||
|
label: What area is affected?
|
||||||
|
options:
|
||||||
|
- Launcher UI
|
||||||
|
- Authentication
|
||||||
|
- Offline account flow
|
||||||
|
- Ely.by integration
|
||||||
|
- Minecraft launch
|
||||||
|
- Update system
|
||||||
|
- Installation or first launch
|
||||||
|
- Performance
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: AstralRinth version
|
||||||
|
description: Specify the launcher version, tag, or build name you are using.
|
||||||
|
placeholder: "Example: v0.14.801, v0.14.902 ..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: install-source
|
||||||
|
attributes:
|
||||||
|
label: Installation source
|
||||||
|
description: Where did you get this build from?
|
||||||
|
options:
|
||||||
|
- Git Astralium (You're here)
|
||||||
|
- Telegram
|
||||||
|
- Black Minecraft
|
||||||
|
- External web resource (Other)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: account-type
|
||||||
|
attributes:
|
||||||
|
label: Account type involved
|
||||||
|
options:
|
||||||
|
- Not related to accounts
|
||||||
|
- Microsoft account
|
||||||
|
- Offline account
|
||||||
|
- Ely.by account
|
||||||
|
- Multiple account types
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: bug-description
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: Describe the actual problem. Include visible errors, broken behavior, and what makes this AstralRinth-specific.
|
||||||
|
placeholder: "Example: The launcher closes after selecting an offline account and pressing Play..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: What did you expect to happen?
|
||||||
|
placeholder: "Example: The launcher should continue to instance startup and open Minecraft."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
placeholder: |
|
||||||
|
1. Open AstralRinth
|
||||||
|
2. Add an offline account
|
||||||
|
3. Open an instance
|
||||||
|
4. Press Play
|
||||||
|
5. Observe the problem
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs or error text
|
||||||
|
description: Paste launcher logs, console output, panic text, or updater/auth errors if available.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: system-details
|
||||||
|
attributes:
|
||||||
|
label: System details
|
||||||
|
description: Add anything relevant such as OS version, architecture, GPU, JVM, or whether this is a portable install.
|
||||||
|
placeholder: Windows 11 24H2, x64, portable install, Java 21 bundled
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Screenshots, videos, related commits, comparisons with upstream Modrinth, or anything else that may help.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 🫶 Support Portal
|
- name: 💎 Support AstralRinth Telegram
|
||||||
about: Get support using through our portal.
|
about: Get support in our Telegram chat.
|
||||||
|
url: https://astralium.su/product/astralrinth/support
|
||||||
|
- name: 🫶 Support Modrinth Portal
|
||||||
|
about: Get support using through our support website.
|
||||||
url: https://support.modrinth.com
|
url: https://support.modrinth.com
|
||||||
- name: 💬 Chat
|
|
||||||
about: Join our Discord server to chat about Modrinth.
|
|
||||||
url: https://discord.modrinth.com
|
|
||||||
- name: 🛣️ Roadmap
|
|
||||||
about: View our Roadmap. Please do not open issues for items on our roadmap.
|
|
||||||
url: https://roadmap.modrinth.com
|
|
||||||
- name: 📚 Documentation
|
|
||||||
about: Useful documentation about Modrinth's API
|
|
||||||
url: https://docs.modrinth.com
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
applyTo: '**/*.vue'
|
||||||
|
---
|
||||||
|
|
||||||
|
You are given a Nuxt/Vue single-file component (.vue). Your task is to convert every hard-coded natural-language string in the <template> into our localization system using vue-i18n with utilities from `@modrinth/ui`.
|
||||||
|
|
||||||
|
Please follow these rules precisely:
|
||||||
|
|
||||||
|
1. Identify translatable strings
|
||||||
|
|
||||||
|
- Scan the <template> for all user-visible strings (inner text, alt attributes, placeholders, button labels, etc.). Do not extract dynamic expressions (like {{ user.name }}) or HTML tags. Only extract static human-readable text.
|
||||||
|
- There may be strings within the <script> block, e.g dropdown option labels, notifications etc.
|
||||||
|
|
||||||
|
2. Create message definitions
|
||||||
|
|
||||||
|
- In the <script setup> block, import `defineMessage` or `defineMessages` from `@modrinth/ui`.
|
||||||
|
- For each extracted string, define a message with a unique `id` (use a descriptive prefix based on the component path, e.g. `auth.welcome.long-title`) and a `defaultMessage` equal to the original English string.
|
||||||
|
Example:
|
||||||
|
const messages = defineMessages({
|
||||||
|
welcomeTitle: { id: 'auth.welcome.title', defaultMessage: 'Welcome' },
|
||||||
|
welcomeDescription: { id: 'auth.welcome.description', defaultMessage: 'You're now part of the community…' },
|
||||||
|
})
|
||||||
|
|
||||||
|
3. Handle variables and ICU formats
|
||||||
|
|
||||||
|
- Replace dynamic parts with ICU placeholders: "Hello, ${user.name}!" → `{name}` and defaultMessage: 'Hello, {name}!'
|
||||||
|
- For numbers/dates/times, use ICU options (e.g., currency): `{price, number, ::currency/USD}`
|
||||||
|
- For plurals/selects, use ICU: `'{count, plural, one {# message} other {# messages}}'`
|
||||||
|
|
||||||
|
4. Rich-text messages (links/markup)
|
||||||
|
|
||||||
|
- In `defaultMessage`, wrap link/markup ranges with tags, e.g.:
|
||||||
|
"By creating an account, you agree to our <terms-link>Terms</terms-link> and <privacy-link>Privacy Policy</privacy-link>."
|
||||||
|
- Render rich-text messages with `<IntlFormatted>` from `@modrinth/ui` using named slots:
|
||||||
|
<IntlFormatted :message-id="messages.tosLabel">
|
||||||
|
<template #terms-link="{ children }">
|
||||||
|
<NuxtLink to="/terms">
|
||||||
|
<component :is="() => children" />
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
<template #privacy-link="{ children }">
|
||||||
|
<NuxtLink to="/privacy">
|
||||||
|
<component :is="() => children" />
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
</IntlFormatted>
|
||||||
|
- For simple emphasis: `'Welcome to <strong>Modrinth</strong>!'` with a slot:
|
||||||
|
<template #strong="{ children }">
|
||||||
|
<strong><component :is="() => children" /></strong>
|
||||||
|
</template>
|
||||||
|
- For more complex child handling, use `normalizeChildren` from `@modrinth/ui`:
|
||||||
|
<template #bold="{ children }">
|
||||||
|
<strong><component :is="() => normalizeChildren(children)" /></strong>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
5. Formatting in templates
|
||||||
|
|
||||||
|
- Import and use `useVIntl()` from `@modrinth/ui`; prefer `formatMessage` for simple strings:
|
||||||
|
`const { formatMessage } = useVIntl()`
|
||||||
|
`<button>{{ formatMessage(messages.welcomeTitle) }}</button>`
|
||||||
|
- Pass variables as a second argument:
|
||||||
|
`{{ formatMessage(messages.greeting, { name: user.name }) }}`
|
||||||
|
|
||||||
|
6. Naming conventions and id stability
|
||||||
|
|
||||||
|
- Make `id`s descriptive and stable (e.g., `error.generic.default.title`). Group related messages with `defineMessages`.
|
||||||
|
|
||||||
|
7. Avoid Vue/ICU delimiter collisions
|
||||||
|
|
||||||
|
- If an ICU placeholder would end right before `}}` in a Vue template, insert a space so it becomes `} }` to avoid parsing issues.
|
||||||
|
|
||||||
|
8. Update imports and remove literals
|
||||||
|
|
||||||
|
- Ensure imports from `@modrinth/ui` are present: `defineMessage`/`defineMessages`, `useVIntl`, `IntlFormatted`, and optionally `normalizeChildren`.
|
||||||
|
- Replace all hard-coded strings with `formatMessage(...)` or `<IntlFormatted>` and remove the literals.
|
||||||
|
|
||||||
|
9. Preserve functionality
|
||||||
|
|
||||||
|
- Do not change logic, layout, reactivity, or bindings—only refactor strings into i18n.
|
||||||
|
|
||||||
|
Use existing patterns from our codebase:
|
||||||
|
|
||||||
|
- Variables/plurals: see `apps/frontend/src/pages/frog.vue`
|
||||||
|
- Rich-text link tags: see `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue`
|
||||||
|
|
||||||
|
When you finish, there should be no hard-coded English strings left in the template—everything comes from `formatMessage` or `<IntlFormatted>`.
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# MIT License
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 CARIAD SE
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
name: 'Merge Queue CI Check Skipper'
|
||||||
|
description: 'Outputs `skip-check` as `true` if this is running as part of merge queue checks and the same checks have already been executed in the PR itself.'
|
||||||
|
inputs:
|
||||||
|
secret:
|
||||||
|
description: 'Optional GitHub Secret that can access branch protection rules using the administration:read permission'
|
||||||
|
required: false
|
||||||
|
outputs:
|
||||||
|
skip-check:
|
||||||
|
description: 'Skip Check (boolean)'
|
||||||
|
value: ${{ steps.passed-checks.outputs.can-skip-checks }}
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract PR Number and Commit ID
|
||||||
|
id: extract-pr-info
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const githubRef = process.env.GITHUB_REF;
|
||||||
|
const regex = /^refs\/heads\/gh-readonly-queue\/([a-zA-Z0-9.\-_\/]+)\/pr-(\d+)-([a-f0-9]+)$/;
|
||||||
|
|
||||||
|
if (regex.test(githubRef)) {
|
||||||
|
const [, targetBranchName, prNumber, commitId] = githubRef.match(regex);
|
||||||
|
core.setOutput('targetBranchName', targetBranchName);
|
||||||
|
core.setOutput('prNumber', prNumber);
|
||||||
|
core.setOutput('commitId', commitId);
|
||||||
|
} else {
|
||||||
|
console.log(`GITHUB_REF is not a merge queue ref, setting CAN_SKIP_CHECKS to false: ${githubRef}`);
|
||||||
|
core.exportVariable('CAN_SKIP_CHECKS', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Print PR Number and Target Commit ID
|
||||||
|
if: env.CAN_SKIP_CHECKS != 'false'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Target Branch Name: ${{ steps.extract-pr-info.outputs.targetBranchName }}"
|
||||||
|
echo "PR Number: ${{ steps.extract-pr-info.outputs.prNumber }}"
|
||||||
|
echo "Target Commit ID: ${{ steps.extract-pr-info.outputs.commitId }}"
|
||||||
|
|
||||||
|
- name: Check if merge queue entry was enqueued as head of the queue
|
||||||
|
if: env.CAN_SKIP_CHECKS != 'false'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
targetBranchHead=$(git rev-parse origin/${{ steps.extract-pr-info.outputs.targetBranchName }})
|
||||||
|
if [[ "$targetBranchHead" != "${{ steps.extract-pr-info.outputs.commitId }}" ]]; then
|
||||||
|
echo "'${{ steps.extract-pr-info.outputs.targetBranchName }}' branch commit ID does not match PR commit ID. This Merge Queue run was not head of the queue when it was enqueued. Setting CAN_SKIP_CHECKS to false."
|
||||||
|
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "This merge queue entry is targeting '${{ steps.extract-pr-info.outputs.targetBranchName }}' directly."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get PR Branch
|
||||||
|
id: get-pr-branch
|
||||||
|
if: env.CAN_SKIP_CHECKS != 'false'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
prNumber=${{ steps.extract-pr-info.outputs.prNumber }}
|
||||||
|
branchName=$(gh pr view ${prNumber} --json headRefName -q '.headRefName')
|
||||||
|
echo "prBranch=$branchName" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Print PR Branch
|
||||||
|
if: env.CAN_SKIP_CHECKS != 'false'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "PR Branch: ${{ steps.get-pr-branch.outputs.prBranch }}"
|
||||||
|
|
||||||
|
- name: Check if PR branch contains the Merge Queue target commit ID
|
||||||
|
if: env.CAN_SKIP_CHECKS != 'false'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Get the branch name from previous steps
|
||||||
|
branch_name="origin/${{ steps.get-pr-branch.outputs.prBranch }}"
|
||||||
|
commit_id="${{ steps.extract-pr-info.outputs.commitId }}"
|
||||||
|
|
||||||
|
# Check if the branch history contains the commit
|
||||||
|
if git branch -r --contains "$commit_id" | grep -q "$branch_name"; then
|
||||||
|
echo "Branch '$branch_name' contains commit '$commit_id'. It is up to date with ${{ steps.extract-pr-info.outputs.targetBranchName }}."
|
||||||
|
else
|
||||||
|
echo "Branch '$branch_name' does not contain commit '$commit_id'. It is outdated. Setting CAN_SKIP_CHECKS to false."
|
||||||
|
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Compare PR Branch with Current Branch
|
||||||
|
if: env.CAN_SKIP_CHECKS != 'false'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if git diff --quiet "origin/${{ steps.get-pr-branch.outputs.prBranch }}"; then
|
||||||
|
echo "No differences found. PR branch is identical with this merge queue branch."
|
||||||
|
else
|
||||||
|
echo "Differences detected. PR branch has been updated after PR was added to merge queue. Setting CAN_SKIP_CHECKS to false."
|
||||||
|
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Compute/publish skip result
|
||||||
|
id: passed-checks
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
env:
|
||||||
|
SECRET: ${{ inputs.secret }}
|
||||||
|
with:
|
||||||
|
github-token: ${{ inputs.secret != '' && inputs.secret || github.token }}
|
||||||
|
script: |
|
||||||
|
if (process.env.CAN_SKIP_CHECKS == "false") {
|
||||||
|
console.log("Setting CAN_SKIP_CHECKS to false");
|
||||||
|
core.setOutput("can-skip-checks", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretProvided = !!process.env.SECRET;
|
||||||
|
if (!secretProvided) {
|
||||||
|
console.log("secret input not set, assuming all checks have passed. Ensure 'Require status checks to pass before merging' is enabled, or provide a secret with administration:read.");
|
||||||
|
core.setOutput("can-skip-checks", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: branchProtection } = await github.rest.repos.getBranchProtection({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
branch: "${{ steps.extract-pr-info.outputs.targetBranchName }}",
|
||||||
|
});
|
||||||
|
const requiredCheckNames = branchProtection.required_status_checks.contexts;
|
||||||
|
console.log(`requiredCheckNames = ${requiredCheckNames}`);
|
||||||
|
|
||||||
|
const { data: checks } = await github.rest.checks.listForRef({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
ref: "refs/heads/${{ steps.get-pr-branch.outputs.prBranch }}",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`checks.check_runs = ${checks.check_runs.map(check => `${check.status},${check.conclusion},${check.name};`)}`);
|
||||||
|
const nonSuccessfulChecks = checks.check_runs.filter(check => check.status !== "completed" || check.conclusion !== "success");
|
||||||
|
const nonSuccessfulCheckNames = nonSuccessfulChecks.map(check => check.name);
|
||||||
|
console.log(`nonSuccessfulCheckNames = ${nonSuccessfulCheckNames}`);
|
||||||
|
|
||||||
|
const missingChecks = requiredCheckNames.filter(checkName => nonSuccessfulCheckNames.includes(checkName));
|
||||||
|
|
||||||
|
if (missingChecks.length > 0) {
|
||||||
|
console.log(`Required checks not passed, cannot skip merge queue checks. Setting CAN_SKIP_CHECKS to false. Missing checks: ${missingChecks.join(', ')}`);
|
||||||
|
core.setOutput("can-skip-checks", false);
|
||||||
|
} else {
|
||||||
|
console.log("No missing checks. Setting CAN_SKIP_CHECKS to true.");
|
||||||
|
core.setOutput("can-skip-checks", true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
This pull request is created according to the `.github/workflows/i18n-pull.yml` file.
|
||||||
|
|
||||||
|
- 🌐 [Contribute to translations on Crowdin](https://translate.modrinth.com/)
|
||||||
|
- 🔄 [Dispatch this workflow again to update this PR](https://github.com/Modrinth/code/actions/workflows/i18n-pull.yml)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Copying
|
||||||
|
|
||||||
|
Modrinth's Github workflows are licensed under the MIT License, which is provided in the file [LICENSE](./LICENSE).
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2025 Rinth, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
name: AstralRinth App build
|
name: AstralRinth App Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- master
|
||||||
|
- prod
|
||||||
|
- release
|
||||||
|
- beta
|
||||||
- feature*
|
- feature*
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- release-*
|
||||||
|
- beta-*
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/astralrinth-build.yml
|
- .github/workflows/astralrinth-build.yml
|
||||||
- 'apps/app/**'
|
- 'apps/app/**'
|
||||||
@@ -16,23 +21,54 @@ on:
|
|||||||
- 'packages/assets/**'
|
- 'packages/assets/**'
|
||||||
- 'packages/ui/**'
|
- 'packages/ui/**'
|
||||||
- 'packages/utils/**'
|
- 'packages/utils/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- 'turbo.jsonc'
|
||||||
|
- 'mise.toml'
|
||||||
|
- 'rust-toolchain.toml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: astralrinth-build-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: ${{ matrix.label }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [macos-latest, windows-latest, ubuntu-latest]
|
|
||||||
include:
|
include:
|
||||||
- platform: macos-latest
|
- runner: ubuntu-latest
|
||||||
artifact-target-name: universal-apple-darwin
|
label: 🐧 Linux x86_64 Build
|
||||||
- platform: windows-latest
|
target: x86_64-unknown-linux-gnu
|
||||||
artifact-target-name: x86_64-pc-windows-msvc
|
rust_targets: x86_64-unknown-linux-gnu
|
||||||
- platform: ubuntu-latest
|
artifact_name: astralrinth-bundle-linux-x86_64
|
||||||
artifact-target-name: x86_64-unknown-linux-gnu
|
- runner: windows-latest
|
||||||
|
label: 🪟 Windows x86_64 Build
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
rust_targets: x86_64-pc-windows-msvc
|
||||||
|
artifact_name: astralrinth-bundle-windows-x86_64
|
||||||
|
- runner: windows-latest
|
||||||
|
label: 🪟 Windows aarch64 Build
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
rust_targets: aarch64-pc-windows-msvc
|
||||||
|
artifact_name: astralrinth-bundle-windows-aarch64
|
||||||
|
- runner: macos-latest
|
||||||
|
label: 🍎 macOS x86_64 Build
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
rust_targets: x86_64-apple-darwin
|
||||||
|
artifact_name: astralrinth-bundle-macos-x86_64
|
||||||
|
- runner: macos-latest
|
||||||
|
label: 🍎 macOS aarch64 Build
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
rust_targets: aarch64-apple-darwin
|
||||||
|
artifact_name: astralrinth-bundle-macos-aarch64
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📥 Check out code
|
- name: 📥 Check out code
|
||||||
@@ -40,12 +76,49 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: 🔍 Validate Git config does not introduce CRLF
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🔍 Checking Git config for CRLF settings..."
|
||||||
|
|
||||||
|
autocrlf=$(git config --get core.autocrlf || echo "unset")
|
||||||
|
eol_setting=$(git config --get core.eol || echo "unset")
|
||||||
|
|
||||||
|
echo "core.autocrlf = $autocrlf"
|
||||||
|
echo "core.eol = $eol_setting"
|
||||||
|
|
||||||
|
if [ "$autocrlf" = "true" ]; then
|
||||||
|
echo "⚠️ WARNING: core.autocrlf is set to 'true'. Consider setting it to 'input' or 'false'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$eol_setting" = "crlf" ]; then
|
||||||
|
echo "⚠️ WARNING: core.eol is set to 'crlf'. Consider unsetting it or setting it to 'lf'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 🔍 Check migration files line endings (LF only)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🔍 Scanning migration SQL files for CR characters (\\r)..."
|
||||||
|
|
||||||
|
if grep -Iq $'\r' packages/app-lib/migrations/*.sql; then
|
||||||
|
echo "❌ ERROR: Some migration files contain CR characters; expected LF-only files."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ All migration files use LF line endings."
|
||||||
|
|
||||||
- name: 🧰 Setup Rust toolchain
|
- name: 🧰 Setup Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.artifact-target-name }}
|
target: ${{ matrix.rust_targets }}
|
||||||
|
|
||||||
- name: 🧰 Install pnpm
|
- name: ☕ Setup Java 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: 🧰 Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: 🧰 Setup Node.js
|
- name: 🧰 Setup Node.js
|
||||||
@@ -54,64 +127,61 @@ jobs:
|
|||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|
||||||
- name: 🧰 Install Linux build dependencies
|
- name: 🧰 Setup mise
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
uses: jdx/mise-action@v2
|
||||||
run: |
|
with:
|
||||||
sudo apt-get update
|
install: true
|
||||||
sudo apt-get install -yq \
|
cache: true
|
||||||
libgtk-3-dev \
|
|
||||||
libwebkit2gtk-4.1-dev \
|
- name: 🦀 Cache Rust build artifacts
|
||||||
libayatana-appindicator3-dev \
|
uses: Swatinem/rust-cache@v2
|
||||||
librsvg2-dev \
|
with:
|
||||||
xdg-utils \
|
workspaces: |
|
||||||
openjdk-11-jdk
|
. -> target
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
- name: 💨 Setup Turbo cache
|
- name: 💨 Setup Turbo cache
|
||||||
uses: rharkor/caching-for-turbo@v1.8
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
|
|
||||||
- name: 🧰 Install dependencies
|
- name: 🐧 Install Linux build dependencies
|
||||||
run: pnpm install
|
if: matrix.runner == 'ubuntu-latest'
|
||||||
|
|
||||||
- name: ✍️ Set up Windows code signing (jsign)
|
|
||||||
if: matrix.platform == 'windows' && env.SIGN_WINDOWS_BINARIES == 'true'
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
choco install jsign --ignore-dependencies
|
sudo apt-get update
|
||||||
|
sudo apt-get install -yq \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
file \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
libssl-dev \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
patchelf \
|
||||||
|
xdg-utils
|
||||||
|
|
||||||
- name: 🗑️ Clean up cached bundles
|
- name: ⚙️ Set application environment
|
||||||
|
shell: bash
|
||||||
|
run: cp packages/app-lib/.env.prod packages/app-lib/.env
|
||||||
|
|
||||||
|
- name: 📦 Install dependencies
|
||||||
|
shell: bash
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: 🧹 Clean previous bundles
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
rm -rf target/release/bundle
|
rm -rf target/release/bundle
|
||||||
rm -rf target/*/release/bundle || true
|
rm -rf target/*/release/bundle || true
|
||||||
|
|
||||||
- name: 🔨 Build macOS app
|
- name: 🔨 Build app
|
||||||
if: matrix.platform == 'macos-latest'
|
shell: bash
|
||||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
run: mise exec -- pnpm --filter @modrinth/app exec tauri build --target ${{ matrix.target }}
|
||||||
env:
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: 🔨 Build Linux app
|
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
|
||||||
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
|
|
||||||
env:
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: 🔨 Build Windows app
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
|
||||||
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
|
|
||||||
env:
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: 📤 Upload app bundles
|
- name: 📤 Upload app bundles
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: App bundle (${{ matrix.artifact-target-name }})
|
name: ${{ matrix.artifact_name }}
|
||||||
path: |
|
path: |
|
||||||
target/release/bundle/**
|
target/${{ matrix.target }}/release/bundle/**
|
||||||
target/*/release/bundle/**
|
if-no-files-found: error
|
||||||
|
|||||||
+31
-5
@@ -9,7 +9,6 @@ tmp
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
.c9/
|
.c9/
|
||||||
@@ -23,6 +22,15 @@ node_modules
|
|||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
!.vscode/i18n-ally-custom-framework.yml
|
||||||
|
|
||||||
|
# IDE - IntelliJ
|
||||||
|
.idea/*
|
||||||
|
!.idea/code.iml
|
||||||
|
!.idea/gradle.xml
|
||||||
|
!.idea/icon.svg
|
||||||
|
!.idea/modules.xml
|
||||||
|
!.idea/vcs.xml
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
@@ -56,8 +64,26 @@ generated
|
|||||||
# app testing dir
|
# app testing dir
|
||||||
app-playground-data/*
|
app-playground-data/*
|
||||||
|
|
||||||
# soley because i need the PORT to be 3002 due to WSL stuff
|
|
||||||
.env
|
|
||||||
apps/frontend/.env
|
|
||||||
|
|
||||||
.astro
|
.astro
|
||||||
|
.claude/*
|
||||||
|
!.claude/skills/
|
||||||
|
.letta
|
||||||
|
|
||||||
|
# labrinth demo fixtures
|
||||||
|
apps/labrinth/fixtures/demo
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
|
|
||||||
|
.wrangler/
|
||||||
|
|
||||||
|
# frontend robots.txt
|
||||||
|
apps/frontend/src/public/robots.txt
|
||||||
|
|
||||||
|
# Oh My Code
|
||||||
|
.omc/
|
||||||
|
|
||||||
|
# Local dry-run output for scripts/build-theseus-release-notes.mjs
|
||||||
|
/test_result.md
|
||||||
|
|
||||||
|
packages/tooling-config/script-utils/import-sort.js
|
||||||
|
|||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
Generated
+7
-2
@@ -10,11 +10,16 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/rust-common/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/packages/path-util/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-log/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-maxmind/examples" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-maxmind/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-util/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/packages/muralpay/src" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
Generated
-7
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DiscordProjectSettings">
|
|
||||||
<option name="show" value="PROJECT_FILES" />
|
|
||||||
<option name="description" value="" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1"/>
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/app-lib/java"/>
|
||||||
|
<option name="gradleJvm" value="#JAVA_HOME"/>
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/packages/app-lib/java"/>
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+4
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="512" height="514" viewBox="0 0 512 514" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M503.16 323.56C514.55 281.47 515.32 235.91 503.2 190.76C466.57 54.2299 326.04 -26.8001 189.33 9.77991C83.8101 38.0199 11.3899 128.07 0.689941 230.47H43.99C54.29 147.33 113.74 74.7298 199.75 51.7098C306.05 23.2598 415.13 80.6699 453.17 181.38L411.03 192.65C391.64 145.8 352.57 111.45 306.3 96.8198L298.56 140.66C335.09 154.13 364.72 184.5 375.56 224.91C391.36 283.8 361.94 344.14 308.56 369.17L320.09 412.16C390.25 383.21 432.4 310.3 422.43 235.14L464.41 223.91C468.91 252.62 467.35 281.16 460.55 308.07L503.16 323.56Z" fill="#00af5c"/>
|
||||||
|
<path d="M321.99 504.22C185.27 540.8 44.7501 459.77 8.11011 323.24C3.84011 307.31 1.17 291.33 0 275.46H43.27C44.36 287.37 46.4699 299.35 49.6799 311.29C53.0399 323.8 57.45 335.75 62.79 347.07L101.38 323.92C98.1299 316.42 95.39 308.6 93.21 300.47C69.17 210.87 122.41 118.77 212.13 94.7601C229.13 90.2101 246.23 88.4401 262.93 89.1501L255.19 133C244.73 133.05 234.11 134.42 223.53 137.25C157.31 154.98 118.01 222.95 135.75 289.09C136.85 293.16 138.13 297.13 139.59 300.99L188.94 271.38L174.07 231.95L220.67 184.08L279.57 171.39L296.62 192.38L269.47 219.88L245.79 227.33L228.87 244.72L237.16 267.79C237.16 267.79 253.95 285.63 253.98 285.64L277.7 279.33L294.58 260.79L331.44 249.12L342.42 273.82L304.39 320.45L240.66 340.63L212.08 308.81L162.26 338.7C187.8 367.78 226.2 383.93 266.01 380.56L277.54 423.55C218.13 431.41 160.1 406.82 124.05 361.64L85.6399 384.68C136.25 451.17 223.84 484.11 309.61 461.16C371.35 444.64 419.4 402.56 445.42 349.38L488.06 364.88C457.17 431.16 398.22 483.82 321.99 504.22Z" fill="#00af5c"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
Generated
-26
@@ -1,26 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="KotlinJavaRuntime" type="repository">
|
|
||||||
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0" />
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0-javadoc.jar!/" />
|
|
||||||
</JAVADOC>
|
|
||||||
<SOURCES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0-sources.jar!/" />
|
|
||||||
</SOURCES>
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
Generated
+13
-6
@@ -1,8 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml"
|
||||||
</modules>
|
filepath="$PROJECT_DIR$/.idea/code.iml"/>
|
||||||
</component>
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/theseus.iml"
|
||||||
</project>
|
filepath="$PROJECT_DIR$/.idea/modules/theseus.iml"/>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/theseus.main.iml"
|
||||||
|
filepath="$PROJECT_DIR$/.idea/modules/theseus.main.iml"/>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/theseus.test.iml"
|
||||||
|
filepath="$PROJECT_DIR$/.idea/modules/theseus.test.iml"/>
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|||||||
Generated
+12
-10
@@ -1,12 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CommitMessageInspectionProfile">
|
<component name="CommitMessageInspectionProfile">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="CommitFormat" enabled="true" level="WARNING"
|
||||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
enabled_by_default="true"/>
|
||||||
</profile>
|
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING"
|
||||||
</component>
|
enabled_by_default="true"/>
|
||||||
<component name="VcsDirectoryMappings">
|
</profile>
|
||||||
<mapping directory="" vcs="Git" />
|
</component>
|
||||||
</component>
|
<component name="VcsDirectoryMappings">
|
||||||
</project>
|
<mapping directory="" vcs="Git"/>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|||||||
@@ -1,2 +1,11 @@
|
|||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
auto-install-peers=true
|
auto-install-peers=true
|
||||||
|
public-hoist-pattern[]=prettier-plugin-*
|
||||||
|
public-hoist-pattern[]=@prettier/plugin-*
|
||||||
|
public-hoist-pattern[]=eslint
|
||||||
|
public-hoist-pattern[]=@eslint/*
|
||||||
|
public-hoist-pattern[]=eslint-plugin-*
|
||||||
|
public-hoist-pattern[]=@nuxt/eslint-config
|
||||||
|
public-hoist-pattern[]=typescript-eslint
|
||||||
|
public-hoist-pattern[]=vue-eslint-parser
|
||||||
|
public-hoist-pattern[]=globals
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Cargo.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
.github/**/*.png
|
||||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["esbenp.prettier-vscode", "Vue.volar", "rust-lang.rust-analyzer"]
|
"recommendations": ["esbenp.prettier-vscode", "Vue.volar", "rust-lang.rust-analyzer", "lokalise.i18n-ally"]
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
languageIds:
|
||||||
|
- vue
|
||||||
|
- typescript
|
||||||
|
- javascript
|
||||||
|
- typescriptreact
|
||||||
|
|
||||||
|
usageMatchRegex:
|
||||||
|
- id:\s*['"]({key})['"]
|
||||||
|
|
||||||
|
monopoly: true
|
||||||
Vendored
+43
-7
@@ -1,9 +1,45 @@
|
|||||||
{
|
{
|
||||||
"prettier.endOfLine": "lf",
|
"prettier.endOfLine": "lf",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||||
"editor.detectIndentation": true,
|
"editor.detectIndentation": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.insertSpaces": false,
|
||||||
"source.fixAll.eslint": "explicit"
|
"files.eol": "\n",
|
||||||
}
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[scss]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[rust]": {
|
||||||
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
|
},
|
||||||
|
"css.lint.unknownAtRules": "ignore",
|
||||||
|
"scss.lint.unknownAtRules": "ignore",
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"packages/ui/src/locales",
|
||||||
|
"apps/frontend/src/locales",
|
||||||
|
"packages/moderation/src/locales"
|
||||||
|
],
|
||||||
|
"i18n-ally.pathMatcher": "{locale}/index.{ext}",
|
||||||
|
"i18n-ally.keystyle": "flat",
|
||||||
|
"i18n-ally.sourceLanguage": "en-US",
|
||||||
|
"i18n-ally.namespace": false,
|
||||||
|
"i18n-ally.includeSubfolders": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Modrinth Monorepo
|
||||||
|
|
||||||
|
This is the Modrinth monorepo — it contains all Modrinth projects, both frontend and backend. When entering a project, either to edit or analyse, you should read it's CLAUDE.md.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Monorepo tooling:** [Turborepo](https://turbo.build/) (`turbo.jsonc`) + [pnpm workspaces](https://pnpm.io/workspaces) (`pnpm-workspace.yaml`)
|
||||||
|
- **Frontend:** Vue 3 / Nuxt 3, Tailwind CSS v3
|
||||||
|
- **Backend:** Rust (Labrinth API), Postgres, Clickhouse
|
||||||
|
- **Indentation:** Use TAB everywhere, never spaces
|
||||||
|
|
||||||
|
### Apps (`apps/`)
|
||||||
|
|
||||||
|
| App | Description |
|
||||||
|
| ----------------- | ------------------------------ |
|
||||||
|
| `frontend` | Main Modrinth website (Nuxt 3) |
|
||||||
|
| `app-frontend` | Desktop/app frontend (Vue 3) |
|
||||||
|
| `app` | Desktop/app shell (Tauri) |
|
||||||
|
| `app-playground` | Testing playground for app |
|
||||||
|
| `labrinth` | Backend API service |
|
||||||
|
| `daedalus_client` | Daedalus client implementation |
|
||||||
|
| `docs` | Documentation site (Astro) |
|
||||||
|
|
||||||
|
### Packages (`packages/`)
|
||||||
|
|
||||||
|
| Package | Description |
|
||||||
|
| ------------------ | ----------------------------------------------------- |
|
||||||
|
| `ui` | Shared Vue component library (`@modrinth/ui`) |
|
||||||
|
| `assets` | Styling and auto-generated icons (`@modrinth/assets`) |
|
||||||
|
| `api-client` | API client for Nuxt, Tauri, and Node/browser |
|
||||||
|
| `app-lib` | Shared app library |
|
||||||
|
| `blog` | Blog system and changelog data |
|
||||||
|
| `utils` | Shared utility functions (mostly deprecated) |
|
||||||
|
| `moderation` | Moderation utilities |
|
||||||
|
| `daedalus` | Daedalus protocol |
|
||||||
|
| `tooling-config` | ESLint, Prettier, TypeScript configs |
|
||||||
|
| `ariadne` | Analytics library |
|
||||||
|
| `modrinth-log` | Logging utilities |
|
||||||
|
| `modrinth-maxmind` | MaxMind GeoIP |
|
||||||
|
| `modrinth-util` | General utilities |
|
||||||
|
| `muralpay` | Payment processing |
|
||||||
|
| `path-util` | Path utilities |
|
||||||
|
| `sqlx-tracing` | SQLx query tracing |
|
||||||
|
|
||||||
|
## Pre-PR Commands
|
||||||
|
|
||||||
|
Run these from the **root** folder before opening a pull request - do not run these after each prompt the user gives you, only run when asked, ask the user a question if they want to run it if the user indicates that they are about to create a pull request.
|
||||||
|
|
||||||
|
- **Website:** `pnpm prepr:frontend:web`
|
||||||
|
- **App frontend:** `pnpm prepr:frontend:app`
|
||||||
|
- **Frontend libs:** `pnpm prepr:frontend:lib`
|
||||||
|
- **All frontend (app+web):** `pnpm prepr`
|
||||||
|
- **Labrinth (backend):** See `apps/labrinth/CLAUDE.md`
|
||||||
|
|
||||||
|
The website and app `prepr` commands
|
||||||
|
|
||||||
|
## Dev Commands
|
||||||
|
|
||||||
|
- **Website:** `pnpm web:dev` (copy `.env` template in `apps/frontend/` first)
|
||||||
|
- **App:** `pnpm app:dev` (copy `.env` template in `packages/app-lib/` first)
|
||||||
|
- **Storybook (packages/ui):** `pnpm storybook`
|
||||||
|
|
||||||
|
## Project-Specific Instructions
|
||||||
|
|
||||||
|
Each project may have its own `CLAUDE.md` with detailed instructions:
|
||||||
|
|
||||||
|
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
|
||||||
|
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
|
||||||
|
|
||||||
|
## Code Guidelines
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
- DO NOT use "heading" comments like: `=== Helper methods ===`.
|
||||||
|
- Use doc comments, but avoid inline comments unless ABSOLUTELY necessary for clarity. Code should aim to be self documenting!
|
||||||
|
|
||||||
|
## Bash Guidelines
|
||||||
|
|
||||||
|
### Output handling
|
||||||
|
- DO NOT pipe output through `head`, `tail`, `less`, or `more`
|
||||||
|
- NEVER use `| head -n X` or `| tail -n X` to truncate output
|
||||||
|
- IMPORTANT: Run commands directly without pipes when possible
|
||||||
|
- IMPORTANT: If you need to limit output, use command-specific flags (e.g. `git log -n 10` instead of `git log | head -10`)
|
||||||
|
- ALWAYS read the full output — never pipe through filters
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to
|
||||||
|
- For Frontend, when doing lint checks, only use the `prepr` commands, do not use `typecheck` or `tsc` etc.
|
||||||
|
- Types in `@modrinth/utils` are considered highly outdated, if a component needs them, check if you can switch said component to use types from `packages/api-client`
|
||||||
|
- When provided problems, do not say "I didn't introduce these problems" (shifting the blame/effort) - just fix them.
|
||||||
|
|
||||||
|
## Edit Tool - Whitespace Handling (CLAUDE ONLY)
|
||||||
|
|
||||||
|
The Read tool uses `→` to mark where line numbers end and file content begins.
|
||||||
|
|
||||||
|
**Rule:** Copy the EXACT whitespace that appears after the `→` marker.
|
||||||
|
- Whatever appears between `→` and the code text is what's actually in the file
|
||||||
|
- That whitespace must be used EXACTLY in Edit tool's old_string
|
||||||
|
- Don't count arrows, don't interpret - just copy what's after the `→`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
14→ private byte tag;
|
||||||
|
For Edit, use: ` private byte tag;` (copy everything after →, including the two tabs)
|
||||||
|
|
||||||
|
**If Edit fails:** Stop and explain the problem. Do not attempt sed/awk/bash workarounds.
|
||||||
|
|
||||||
|
**IMPORTANT**: Trust the Read tool output. Copy what's after `→` into Edit immediately. DO NOT verify with sed/od/grep first - that's wasting time and the instructions already tell you to stop if Edit fails, not to pre-verify.
|
||||||
|
|
||||||
|
## Standards
|
||||||
|
|
||||||
|
Standards available at the @standards/ folder.
|
||||||
+10
-2
@@ -2,12 +2,20 @@
|
|||||||
|
|
||||||
All packages in this repository are licensed under their respective licenses. For more information, refer to the LICENSE file in each package.
|
All packages in this repository are licensed under their respective licenses. For more information, refer to the LICENSE file in each package.
|
||||||
|
|
||||||
For detailed information, consult each package's COPYING.md file, if available.
|
For detailed information, consult each package's COPYING.md, LICENSE.txt, or LICENSE file, if available.
|
||||||
|
|
||||||
## Modrinth Branding
|
## Modrinth Branding
|
||||||
|
|
||||||
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
||||||
|
|
||||||
All rights reserved. © 2020-2024 Rinth, Inc.
|
> All rights reserved. © 2020-2025 Rinth, Inc.
|
||||||
|
|
||||||
|
This includes, but may not be limited to, the following files:
|
||||||
|
|
||||||
|
- .idea/icon.svg
|
||||||
|
- .github/api_cover.png
|
||||||
|
- .github/app_cover.png
|
||||||
|
- .github/monorepo_cover.png
|
||||||
|
- .github/web_cover.png
|
||||||
|
|
||||||
If you fork this repository, you must remove all Modrinth branding assets from your fork.
|
If you fork this repository, you must remove all Modrinth branding assets from your fork.
|
||||||
|
|||||||
Generated
+4229
-2312
File diff suppressed because it is too large
Load Diff
+142
-83
@@ -8,113 +8,156 @@ members = [
|
|||||||
"packages/app-lib",
|
"packages/app-lib",
|
||||||
"packages/ariadne",
|
"packages/ariadne",
|
||||||
"packages/daedalus",
|
"packages/daedalus",
|
||||||
|
"packages/labrinth-derive",
|
||||||
|
"packages/modrinth-log",
|
||||||
|
"packages/modrinth-maxmind",
|
||||||
|
"packages/modrinth-util",
|
||||||
|
"packages/path-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
rust-version = "1.90.0"
|
||||||
|
repository = "https://github.com/modrinth/code"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-cors = "0.7.1"
|
actix-cors = "0.7.1"
|
||||||
actix-files = "0.6.6"
|
actix-files = "0.6.8"
|
||||||
actix-http = "3.11.0"
|
actix-http = "3.11.2"
|
||||||
actix-multipart = "0.7.2"
|
actix-multipart = "0.7.2"
|
||||||
actix-rt = "2.10.0"
|
actix-rt = "2.11.0"
|
||||||
actix-web = "4.11.0"
|
actix-web = "4.11.0"
|
||||||
actix-web-prom = "0.10.0"
|
actix-web-prom = "0.10.0"
|
||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
|
arc-swap = "1.7.1"
|
||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
ariadne = { path = "packages/ariadne" }
|
ariadne = { path = "packages/ariadne" }
|
||||||
async_zip = "0.0.17"
|
async-compression = { version = "0.4.32", default-features = false }
|
||||||
async-compression = { version = "0.4.25", default-features = false }
|
async-minecraft-ping = { path = "packages/async-minecraft-ping" }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||||
"runtime-tokio-hyper-rustls",
|
"runtime-tokio-hyper-rustls",
|
||||||
] }
|
] }
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.89"
|
||||||
async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
async-tungstenite = { version = "0.31.0", default-features = false, features = [
|
||||||
"futures-03-sink",
|
"futures-03-sink"
|
||||||
] }
|
] }
|
||||||
async-walkdir = "2.1.0"
|
async-walkdir = "2.1.0"
|
||||||
|
async_zip = "0.0.18"
|
||||||
|
aws-sdk-s3 = { version = "=1.122.0", default-features = false, features = [
|
||||||
|
"default-https-client",
|
||||||
|
"rt-tokio",
|
||||||
|
"rustls",
|
||||||
|
] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.4"
|
||||||
bytemuck = "1.23.0"
|
bytemuck = "1.24.0"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
censor = "0.3.0"
|
censor = "0.3.0"
|
||||||
chardetng = "0.1.17"
|
chardetng = "0.1.17"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.42"
|
||||||
clap = "4.5.40"
|
cidre = { version = "0.15.0", default-features = false, features = [
|
||||||
clickhouse = "0.13.3"
|
"macos_15_0"
|
||||||
|
] }
|
||||||
|
clap = "4.5.48"
|
||||||
|
clickhouse = "0.14.0"
|
||||||
|
color-eyre = "0.6.5"
|
||||||
color-thief = "0.2.2"
|
color-thief = "0.2.2"
|
||||||
console-subscriber = "0.4.1"
|
const_format = "0.2.34"
|
||||||
|
core-foundation = "0.10.1"
|
||||||
|
core-graphics = "0.24.0"
|
||||||
daedalus = { path = "packages/daedalus" }
|
daedalus = { path = "packages/daedalus" }
|
||||||
|
darling = { version = "0.23" }
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.2"
|
||||||
deadpool-redis = "0.21.1"
|
deadpool-redis = { git = "https://github.com/modrinth/deadpool", rev = "db5fb00b036ecc8fe5f18853c559b745ffe47bde", version = "0.22.1" }
|
||||||
|
derive_more = "2.1.1"
|
||||||
|
directories = "6.0.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
discord-rich-presence = "0.2.5"
|
discord-rich-presence = "1.0.0"
|
||||||
dotenv-build = "0.1.1"
|
dotenv-build = "0.1.1"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
encoding_rs = "0.8.35"
|
encoding_rs = "0.8.35"
|
||||||
enumset = "1.1.6"
|
enumset = "1.1.10"
|
||||||
flate2 = "1.1.2"
|
eyre = "0.6.12"
|
||||||
|
flate2 = "1.1.4"
|
||||||
fs4 = { version = "0.13.1", default-features = false }
|
fs4 = { version = "0.13.1", default-features = false }
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = "0.3.31"
|
||||||
|
futures-lite = "2.6.1"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
hashlink = "0.10.0"
|
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
hyper-tls = "0.6.0"
|
hyper = "1.7.0"
|
||||||
hyper-util = "0.1.14"
|
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||||
iana-time-zone = "0.1.63"
|
"aws-lc-rs",
|
||||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
"http1",
|
||||||
indexmap = "2.9.0"
|
"native-tokio",
|
||||||
indicatif = "0.17.11"
|
"tls12",
|
||||||
|
] }
|
||||||
|
hyper-util = "0.1.17"
|
||||||
|
iana-time-zone = "0.1.64"
|
||||||
|
image = { version = "0.25.8", default-features = false, features = ["rayon"] }
|
||||||
|
indexmap = "2.11.4"
|
||||||
|
indicatif = "0.18.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jemalloc_pprof = "0.7.0"
|
jemalloc_pprof = "0.8.1"
|
||||||
json-patch = { version = "4.0.0", default-features = false }
|
json-patch = { version = "4.1.0", default-features = false }
|
||||||
lettre = { version = "0.11.17", default-features = false, features = [
|
lettre = { version = "0.11.19", default-features = false, features = [
|
||||||
|
"aws-lc-rs",
|
||||||
"builder",
|
"builder",
|
||||||
"hostname",
|
"hostname",
|
||||||
"pool",
|
"pool",
|
||||||
"ring",
|
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
"smtp-transport",
|
"smtp-transport",
|
||||||
|
"tokio1",
|
||||||
|
"tokio1-rustls",
|
||||||
] }
|
] }
|
||||||
maxminddb = "0.26.0"
|
maxminddb = "0.26.0"
|
||||||
meilisearch-sdk = { version = "0.28.0", default-features = false }
|
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||||
|
modrinth-log = { path = "packages/modrinth-log" }
|
||||||
|
modrinth-util = { path = "packages/modrinth-util" }
|
||||||
|
muralpay = { path = "packages/muralpay" }
|
||||||
murmur2 = "0.1.0"
|
murmur2 = "0.1.0"
|
||||||
native-dialog = "0.9.0"
|
native-dialog = "0.9.2"
|
||||||
notify = { version = "8.0.0", default-features = false }
|
notify = { version = "8.2.0", default-features = false }
|
||||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||||
|
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
|
parking_lot = "0.12.5"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
png = "0.17.16"
|
path-util = { path = "packages/path-util" }
|
||||||
|
phf = { version = "0.13.1", features = ["macros"] }
|
||||||
|
png = "0.18.0"
|
||||||
|
proc-macro2 = { version = "1.0" }
|
||||||
prometheus = "0.14.0"
|
prometheus = "0.14.0"
|
||||||
quartz_nbt = "0.2.9"
|
quartz_nbt = "0.2.9"
|
||||||
quick-xml = "0.37.5"
|
quick-xml = "0.38.3"
|
||||||
|
quote = { version = "1.0" }
|
||||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
redis = "0.32.7"
|
||||||
regex = "1.11.1"
|
regex = "1.12.2"
|
||||||
reqwest = { version = "0.12.20", default-features = false }
|
reqwest = { version = "0.12.24", default-features = false }
|
||||||
rgb = "0.8.50"
|
rgb = "0.8.52"
|
||||||
rust_decimal = { version = "1.37.2", features = [
|
rust_decimal = { version = "1.39.0", features = [
|
||||||
"serde-with-float",
|
"serde-with-float",
|
||||||
"serde-with-str",
|
"serde-with-str"
|
||||||
] }
|
] }
|
||||||
rust_iso3166 = "0.1.14"
|
rust_iso3166 = "0.1.14"
|
||||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
rust-s3 = { version = "0.37.0", default-features = false, features = [
|
||||||
"fail-on-err",
|
"fail-on-err",
|
||||||
"tags",
|
"tags",
|
||||||
"tokio-rustls-tls",
|
"tokio-rustls-tls",
|
||||||
] }
|
] }
|
||||||
|
rustls = "0.23.32"
|
||||||
rusty-money = "0.4.1"
|
rusty-money = "0.4.1"
|
||||||
sentry = { version = "0.41.0", default-features = false, features = [
|
secrecy = "0.10.3"
|
||||||
|
sentry = { version = "0.45.0", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"contexts",
|
"contexts",
|
||||||
"debug-images",
|
"debug-images",
|
||||||
@@ -122,57 +165,70 @@ sentry = { version = "0.41.0", default-features = false, features = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
] }
|
] }
|
||||||
sentry-actix = "0.41.0"
|
serde = "1.0.228"
|
||||||
serde = "1.0.219"
|
serde_bytes = "0.11.19"
|
||||||
serde_bytes = "0.11.17"
|
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
serde_ini = "0.2.0"
|
serde_ini = "0.2.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.145"
|
||||||
serde_with = "3.13.0"
|
serde_with = "3.15.0"
|
||||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
spdx = "0.10.8"
|
shlex = "1.3.0"
|
||||||
|
spdx = "0.12.0"
|
||||||
sqlx = { version = "0.8.6", default-features = false }
|
sqlx = { version = "0.8.6", default-features = false }
|
||||||
sysinfo = { version = "0.35.2", default-features = false }
|
sqlx-tracing = { path = "packages/sqlx-tracing" }
|
||||||
|
strum = "0.27.2"
|
||||||
|
syn = { version = "2.0" }
|
||||||
|
sysinfo = { version = "0.37.2", default-features = false }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tauri = "2.6.1"
|
tauri = "2.8.5"
|
||||||
tauri-build = "2.3.0"
|
tauri-build = "2.4.1"
|
||||||
tauri-plugin-deep-link = "2.4.0"
|
tauri-plugin-deep-link = "2.4.3"
|
||||||
tauri-plugin-dialog = "2.3.0"
|
tauri-plugin-dialog = "2.4.0"
|
||||||
tauri-plugin-http = "2.5.0"
|
tauri-plugin-fs = "2.4.5"
|
||||||
tauri-plugin-opener = "2.4.0"
|
tauri-plugin-http = "2.5.7"
|
||||||
tauri-plugin-os = "2.3.0"
|
tauri-plugin-opener = "2.5.0"
|
||||||
tauri-plugin-single-instance = "2.3.0"
|
tauri-plugin-os = "2.3.1"
|
||||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
tauri-plugin-single-instance = "2.3.4"
|
||||||
|
tauri-plugin-updater = { git = "https://github.com/modrinth/plugins-workspace", rev = "0d30f2aa28ec668ce187d527da1c475da3c01cbc", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"zip",
|
"zip",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-window-state = "2.3.0"
|
tauri-plugin-window-state = "2.4.0"
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.23.0"
|
||||||
theseus = { path = "packages/app-lib" }
|
theseus = { path = "packages/app-lib" }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.17"
|
||||||
tikv-jemalloc-ctl = "0.6.0"
|
tikv-jemalloc-ctl = "0.6.0"
|
||||||
tikv-jemallocator = "0.6.0"
|
tikv-jemallocator = "0.6.0"
|
||||||
tokio = "1.45.1"
|
tokio = "1.47.1"
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tokio-util = "0.7.15"
|
tokio-util = "0.7.16"
|
||||||
totp-rs = "5.7.0"
|
totp-rs = "5.7.0"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-actix-web = "0.7.18"
|
tracing-actix-web = { version = "0.7.19", default-features = false }
|
||||||
|
tracing-ecs = "0.5.0"
|
||||||
tracing-error = "0.2.1"
|
tracing-error = "0.2.1"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.20"
|
||||||
url = "2.5.4"
|
typed-path = "0.12.0"
|
||||||
|
url = "2.5.7"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = "1.17.0"
|
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
|
||||||
|
utoipa-actix-web = { version = "0.1.2" }
|
||||||
|
utoipa-scalar = { version = "0.3.0", default-features = false }
|
||||||
|
uuid = "1.18.1"
|
||||||
validator = "0.20.0"
|
validator = "0.20.0"
|
||||||
webp = { version = "0.3.0", default-features = false }
|
webp = { version = "0.3.1", default-features = false }
|
||||||
whoami = "1.6.0"
|
webview2-com = "0.38.0" # Should be updated in lockstep with wry
|
||||||
|
whoami = "1.6.1"
|
||||||
|
windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62
|
||||||
|
windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
woothee = "0.13.0"
|
woothee = "0.13.0"
|
||||||
yaserde = "0.12.0"
|
yaserde = "0.12.0"
|
||||||
zip = { version = "4.2.0", default-features = false, features = [
|
zbus = "5.11.0"
|
||||||
|
zip = { version = "6.0.0", default-features = false, features = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate",
|
"deflate",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
@@ -200,7 +256,7 @@ manual_assert = "warn"
|
|||||||
manual_instant_elapsed = "warn"
|
manual_instant_elapsed = "warn"
|
||||||
manual_is_variant_and = "warn"
|
manual_is_variant_and = "warn"
|
||||||
manual_let_else = "warn"
|
manual_let_else = "warn"
|
||||||
map_unwrap_or = "warn"
|
map_unwrap_or = "allow"
|
||||||
match_bool = "warn"
|
match_bool = "warn"
|
||||||
needless_collect = "warn"
|
needless_collect = "warn"
|
||||||
negative_feature_names = "warn"
|
negative_feature_names = "warn"
|
||||||
@@ -210,16 +266,15 @@ read_zero_byte_vec = "warn"
|
|||||||
redundant_clone = "warn"
|
redundant_clone = "warn"
|
||||||
redundant_feature_names = "warn"
|
redundant_feature_names = "warn"
|
||||||
redundant_type_annotations = "warn"
|
redundant_type_annotations = "warn"
|
||||||
|
result_large_err = "allow"
|
||||||
todo = "warn"
|
todo = "warn"
|
||||||
|
too_many_arguments = "allow"
|
||||||
|
uninlined_format_args = "warn"
|
||||||
unnested_or_patterns = "warn"
|
unnested_or_patterns = "warn"
|
||||||
wildcard_dependencies = "warn"
|
wildcard_dependencies = "warn"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[profile.dev.package.sqlx-macros]
|
||||||
# Turn warnings into errors by default
|
opt-level = 3
|
||||||
warnings = "deny"
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
wry = { git = "https://github.com/modrinth/wry", rev = "21db186" }
|
|
||||||
|
|
||||||
# Optimize for speed and reduce size on release builds
|
# Optimize for speed and reduce size on release builds
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -227,7 +282,11 @@ opt-level = "s" # Optimize for binary size
|
|||||||
strip = true # Remove debug symbols
|
strip = true # Remove debug symbols
|
||||||
lto = true # Enables link to optimizations
|
lto = true # Enables link to optimizations
|
||||||
panic = "abort" # Strip expensive panic clean-up logic
|
panic = "abort" # Strip expensive panic clean-up logic
|
||||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
# Specific profile for labrinth production builds
|
||||||
opt-level = 3
|
[profile.release-labrinth]
|
||||||
|
inherits = "release"
|
||||||
|
opt-level = 2
|
||||||
|
strip = false # Keep debug symbols for Sentry
|
||||||
|
lto = "thin" # Enable LTO but keep compile times reasonable
|
||||||
|
panic = "unwind" # Don't exit the whole app on panic in production
|
||||||
|
|||||||
@@ -1,76 +1,134 @@
|
|||||||
# Navigation in this README
|
# 📘 Navigation
|
||||||
- [Install instructions](#install-instructions)
|
|
||||||
- [Features](#features)
|
- [🔧 Install Instructions](#install-instructions)
|
||||||
- [Getting started](#getting-started)
|
- [✨ Features](#features)
|
||||||
- [Disclaimer](#disclaimer)
|
- [🚀 Getting Started](#getting-started)
|
||||||
- [Donate](#support-our-project-crypto-wallets)
|
- [🛠️ Разработка](#development)
|
||||||
|
- [⚠️ Disclaimer](#disclaimer)
|
||||||
|
- [💰 Donate](#support-our-project-crypto-wallets)
|
||||||
|
|
||||||
|
## Other languages
|
||||||
|
> [Русский](readme/ru_ru/README.md)
|
||||||
|
|
||||||
|
## Support channel
|
||||||
|
> [Telegram](https://astralium.su/product/astralrinth/support)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# About Project
|
# About Project
|
||||||
|
|
||||||
## AstralRinth • Empowering Your Minecraft Adventure
|
## **AstralRinth • Empowering Your Minecraft Experience**
|
||||||
Welcome to AR • Fork of Modrinth, the ultimate game launcher designed to enhance your Minecraft experience through the Modrinth platform and their API. Whether you're a graphical interface enthusiast, or a developer integrating Modrinth projects, Theseus core is your gateway to a new level of Minecraft gaming.
|
|
||||||
|
|
||||||
## About Software
|
**AstralRinth** — a powerful fork of Modrinth, reimagined to enhance your Minecraft journey. Whether you're a GUI enthusiast or a developer building with Modrinth’s API, **Theseus Core** is your launchpad into a new era of Minecraft gameplay.
|
||||||
Introducing AstralRinth, a specialized variant of Theseus dedicated to implementing offline authorization for an even more flexible and user-centric Minecraft Modrinth experience. Roam the Minecraft realms without the constraints of online authentication, thanks to AstralRinth.
|
|
||||||
|
|
||||||
## AR • Unlocking Minecraft's Boundless Horizon
|
## **About the Software**
|
||||||
Dive into the extraordinary world of AstralRinth, a fork of the original project with a unique focus on providing a free trial experience for Minecraft, all without the need for a license. Currently boasting:
|
|
||||||
|
|
||||||
# Install instructions
|
**AstralRinth** is a dedicated branch of the Modrinth (a.k.a Theseus) project, focused on **offline authentication**, offering you more flexibility and control. Play Minecraft without the need for constant online verification — a user-first approach to modern modded gaming.
|
||||||
- To install our application, you need to download a file for your operating system from our available releases or development builds • [Download variants here](https://git.astralium.su/didirus/AstralRinth/releases)
|
|
||||||
- After you have downloaded the required executable file or archive, then open it
|
|
||||||
|
|
||||||
### Downloadable file extensions
|
---
|
||||||
- `.msi` format for Windows OS system _(Supported popular latest versions of Microsoft Windows)_
|
|
||||||
- `.dmg` format for MacOS system _(Works on Macos Ventura / Sonoma / Sequoia, but it should be works on older OS builds)_
|
|
||||||
- `.deb` format for Linux OS systems _(Since there are quite a few distributions, we do not guarantee
|
|
||||||
|
|
||||||
### Installation subjects
|
# Install Instructions
|
||||||
- Builds in releases that are signed with the following prefixes are not recommended for installation and may contain errors:
|
|
||||||
- `dev`
|
To install the launcher:
|
||||||
- `nightly`
|
|
||||||
- `dirty`
|
1. Visit the [releases page](https://astralium.su/product/astralrinth/source) to download the correct version for your system.
|
||||||
- `dirty-dev`
|
2. Run the downloaded file or extract and launch it, depending on the format.
|
||||||
- `dirty-nightly`
|
|
||||||
- `dirty_dev`
|
### Downloadable File Extensions
|
||||||
- `dirty_nightly`
|
|
||||||
- Auto-updating takes place through parsing special versions from releases, so we also distribute clean types of `.msi, .dmg and .deb`
|
| Extension | OS | Notes |
|
||||||
|
| --------- | ------- | --------------------------------------------------------------------- |
|
||||||
|
| `.msi` | Windows | Supported on all recent Windows versions (10/11) |
|
||||||
|
| `.dmg` | macOS | Works on Ventura, Sonoma, Sequoia, Tahoe _(may also support older versions)_ |
|
||||||
|
| `.deb` | Linux | Basic support; compatibility may vary by distribution |
|
||||||
|
|
||||||
|
### Installation Warnings
|
||||||
|
|
||||||
|
Avoid using builds with these prefixes — they may be unstable or experimental:
|
||||||
|
|
||||||
|
- `dev`
|
||||||
|
- `nightly`
|
||||||
|
- `dirty`
|
||||||
|
- `dirty-dev`
|
||||||
|
- `dirty-nightly`
|
||||||
|
- `dirty_dev`
|
||||||
|
- `dirty_nightly`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
### Featured enhancement in AR
|
> _The launcher provides an opportunity to use the well-known Modrinth, but with an improved user experience._
|
||||||
- AstralRinth offers a range of authorization options, giving users the flexibility to log in with valid licenses or even a pirate account without auth credentials breaks (_Unlike MultiMC Cracked and similar software_). Experience Minecraft on your terms, breaking free from traditional licensing constraints (_Popular in Russian Federation_).
|
|
||||||
|
|
||||||
### Easy to use
|
## Included exclusive features
|
||||||
- Using the launcher is intuitive, any user can figure it out.
|
|
||||||
|
|
||||||
### Update notifies
|
- No ads in the entire launcher.
|
||||||
- We have implemented notifications about the release of new updates on our Git. The launcher can also download them for you and try to install them.
|
- Custom `.svg` vector icons for a distinct UI.
|
||||||
|
- Improved compatibility with both licensed and offline accounts.
|
||||||
|
- Use **official microsoft accounts** or **offline accounts**.
|
||||||
|
- Supports license-free access for testing or personal use.
|
||||||
|
- No dependence on official authentication services.
|
||||||
|
- Discord Rich Presence integration:
|
||||||
|
- Dynamic status messages.
|
||||||
|
- In-game timer and AFK counter.
|
||||||
|
- Strict disabling of statistics and other Modrinth metrics.
|
||||||
|
- Optimized archive/package size.
|
||||||
|
- Integrated update fetcher for seamless version management.
|
||||||
|
- Built-in update alerts for new versions posted on Git Astralium.
|
||||||
|
- Automatic download and installation capabilities.
|
||||||
|
- Database migration fixes, when error occurred (Interactive Mode) (Modrinth issue)
|
||||||
|
- Ely.by full integration
|
||||||
|
- The official account skin system is managed by ely.by
|
||||||
|
- Offline accounts must install AuthLib through the instance settings
|
||||||
|
|
||||||
### Enhancements
|
---
|
||||||
- Custom .SVG vectors for a personalized touch.
|
|
||||||
- Improved compatibility for both pirate and licensed accounts.
|
|
||||||
- Beautiful Discord RPC with random messages while playing, along with an in-game timer and AFK counter.
|
|
||||||
- Forced disabling of statistics collection (modrinch metrics) with a hard patch from AstralRinth, ensuring it remains deactivated regardless of the configuration setting.
|
|
||||||
- Removal of advertisements from all launcher views.
|
|
||||||
- Optimization of packages (archives).
|
|
||||||
- Integrated update fetching feature
|
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
To begin your AstralRinth adventure, follow these steps:
|
|
||||||
1. **Download Your OS Version**: Head over to our [releases page](https://git.astralium.su/didirus/AstralRinth/releases/) to find the right file for your operating system.
|
To begin using AstralRinth:
|
||||||
- **Choosing the Correct File**: Ensure you select the file that matches your OS requirements.
|
|
||||||
- [**How select file**](#downloadable-file-extensions)
|
1. **Download Latest Release**
|
||||||
- [**How select release**](#installation-subjects)
|
|
||||||
2. **Authentication**: Log in with a valid license or, for testing, try using a pirate account to see AstralRinth in action.
|
- Go to the [releases page](https://astralium.su/product/astralrinth)
|
||||||
3. **Launch Minecraft**: Start your journey by launching Minecraft through AstralRinth and enjoy the adventures that await.
|
- [How to choose a file](#downloadable-file-extensions)
|
||||||
- **Choosing java installation**: The launcher will try to automatically detect the recommended JVM version for running the game, but you can configure everything in the launcher settings.
|
- [How to choose a release](#installation-warnings)
|
||||||
|
|
||||||
|
2. **Log in or create new offline account**
|
||||||
|
|
||||||
|
- Use your official Microsoft account (MSA), or test using a non-licensed account (Offline).
|
||||||
|
|
||||||
|
3. **Launch Minecraft**
|
||||||
|
- Start Minecraft from the launcher.
|
||||||
|
- The launcher will auto-detect the recommended JVM version.
|
||||||
|
- You can also configure Java manually in the settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
Before continue you need to install `mise` tool for easy install libraries and running application on any OS (Windows, macOS, Linux)
|
||||||
|
|
||||||
|
1. `mise activate`
|
||||||
|
2. `mise install`
|
||||||
|
3. `mise exec rust -- rustup toolchain install stable`
|
||||||
|
4. `mise exec pnpm -- pnpm install`
|
||||||
|
5. `mise exec rust -- cargo install tauri-cli --version "^2.5.0"`
|
||||||
|
6. `mise exec pnpm -- pnpm app:dev` — Development (unoptimized)
|
||||||
|
7. `mise exec pnpm -- pnpm app:build` — Production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
- AstralRinth is a project intended for experimentation and educational purposes only. It does not endorse or support piracy, and users are encouraged to obtain valid licenses for a fully-supported Minecraft experience.
|
|
||||||
- Users are reminded to respect licensing agreements and support the developers of Minecraft.
|
|
||||||
|
|
||||||
# Support our Project (Crypto Wallets)
|
- **AstralRinth** is intended **solely for educational and experimental use**.
|
||||||
- BTC (Telegram): 14g6asNYzcUoaQtB8B2QGKabgEvn55wfLj
|
- We **do not condone piracy** — users are encouraged to purchase a legitimate Minecraft license.
|
||||||
- USDT TRC20 (Telegram): TMSmv1D5Fdf4fipUpwBCdh16WevrV45vGr
|
- Respect all relevant licensing agreements and support Minecraft developers.
|
||||||
- TONCOIN (Telegram): UQAqUJ2_hVBI6k_gPyfp_jd-1K0OS61nIFPZuJWN9BwGAvKe
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Support Our Project (Crypto Wallets)
|
||||||
|
|
||||||
|
If you'd like to support development, you can donate via the following crypto wallets:
|
||||||
|
|
||||||
|
- Toncoin (TON): UQA5pGOJhIz9UAVEOh5t2ur1QVbNr_FC1eq9bOb3GwTgaiqk
|
||||||
|
- USDT (TON): UQA5pGOJhIz9UAVEOh5t2ur1QVbNr_FC1eq9bOb3GwTgaiqk
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
[files]
|
||||||
|
extend-exclude = [
|
||||||
|
"**/src/locales/",
|
||||||
|
"apps/frontend/",
|
||||||
|
"patches/",
|
||||||
|
"packages/utils/",
|
||||||
|
"packages/ui/",
|
||||||
|
"packages/blog/",
|
||||||
|
# contains licenses like `CC-BY-ND-4.0`
|
||||||
|
"packages/moderation/src/data/stages/license.ts",
|
||||||
|
# contains payment card IDs like `IY1VMST1MOXS` which are flagged
|
||||||
|
"apps/labrinth/src/queue/payouts/mod.rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[default.extend-words]
|
||||||
|
# Terms Of Use in `tou-link`
|
||||||
|
tou = "tou"
|
||||||
|
# Google Ad Manager
|
||||||
|
gam = "gam"
|
||||||
|
# short for "constants"
|
||||||
|
consts = "consts"
|
||||||
|
# short for "Copy"
|
||||||
|
Cpy = "Cpy"
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
**/dist
|
**/dist
|
||||||
*.gltf
|
*.gltf
|
||||||
|
src/locales/
|
||||||
|
src/assets/**/*.svg
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
# Copying
|
# Copying
|
||||||
|
|
||||||
The source code of the theseus repository is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
|
The source code of Modrinth App's frontend is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
|
||||||
|
|
||||||
## Modrinth logo
|
## Modrinth logo
|
||||||
|
|
||||||
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
||||||
|
|
||||||
> All rights reserved. © 2020-2023 Rinth, Inc.
|
> All rights reserved. © 2020-2025 Rinth, Inc.
|
||||||
|
|
||||||
This includes, but may not be limited to, the following files:
|
|
||||||
|
|
||||||
- theseus_gui/src-tauri/icons/\*
|
|
||||||
|
|||||||
@@ -1,22 +1,2 @@
|
|||||||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
|
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||||
import { fixupPluginRules } from '@eslint/compat'
|
export default config
|
||||||
import turboPlugin from 'eslint-plugin-turbo'
|
|
||||||
|
|
||||||
export default createConfigForNuxt().append([
|
|
||||||
{
|
|
||||||
name: 'turbo',
|
|
||||||
plugins: {
|
|
||||||
turbo: fixupPluginRules(turboPlugin),
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'turbo/no-undeclared-env-vars': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'modrinth',
|
|
||||||
rules: {
|
|
||||||
'vue/html-self-closing': 'off',
|
|
||||||
'vue/multi-word-component-names': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" class="dark-mode">
|
<html lang="en" class="dark-mode">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>AstralRinth App</title>
|
<title>AstralRinth App</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
|
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script src="https://tally.so/widgets/embed.js" async></script>
|
||||||
</body>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,64 +1,71 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app-frontend",
|
"name": "@modrinth/app-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0-local",
|
"version": "1.0.0-local",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"tsc:check": "vue-tsc --noEmit",
|
"tsc:check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
"test": "vue-tsc --noEmit"
|
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/app-frontend",
|
||||||
},
|
"test": "vue-tsc --noEmit"
|
||||||
"dependencies": {
|
},
|
||||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
"dependencies": {
|
||||||
"@modrinth/assets": "workspace:*",
|
"@modrinth/api-client": "workspace:^",
|
||||||
"@modrinth/ui": "workspace:*",
|
"@modrinth/assets": "workspace:*",
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/ui": "workspace:*",
|
||||||
"@sentry/vue": "^8.27.0",
|
"@modrinth/utils": "workspace:*",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@sentry/vue": "^8.27.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@sfirew/minecraft-motd-parser": "^1.1.6",
|
||||||
"@tauri-apps/plugin-http": "^2.5.0",
|
"@tanstack/vue-query": "5.90.7",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
"@tauri-apps/plugin-http": "~2.5.7",
|
||||||
"@types/three": "^0.172.0",
|
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||||
"@vintl/vintl": "^4.4.1",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@vueuse/core": "^11.1.0",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"dayjs": "^1.11.10",
|
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||||
"floating-vue": "^5.2.2",
|
"@types/three": "^0.172.0",
|
||||||
"ofetch": "^1.3.4",
|
"@vueuse/core": "^11.1.0",
|
||||||
"pinia": "^2.1.7",
|
"dayjs": "^1.11.10",
|
||||||
"posthog-js": "^1.158.2",
|
"floating-vue": "^5.2.2",
|
||||||
"three": "^0.172.0",
|
"fuse.js": "^6.6.2",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"intl-messageformat": "^10.7.7",
|
||||||
"vue": "^3.5.13",
|
"ofetch": "^1.3.4",
|
||||||
"vue-multiselect": "3.0.0",
|
"overlayscrollbars": "^2.15.1",
|
||||||
"vue-router": "4.3.0",
|
"pinia": "^3.0.0",
|
||||||
"vue-virtual-scroller": "v2.0.0-beta.8"
|
"posthog-js": "^1.158.2",
|
||||||
},
|
"three": "^0.172.0",
|
||||||
"devDependencies": {
|
"vite-svg-loader": "^5.1.0",
|
||||||
"@eslint/compat": "^1.1.1",
|
"vue": "^3.5.13",
|
||||||
"@formatjs/cli": "^6.2.12",
|
"vue-i18n": "^10.0.0",
|
||||||
"@nuxt/eslint-config": "^0.5.6",
|
"vue-router": "^4.6.0",
|
||||||
"@taijased/vue-render-tracker": "^1.0.7",
|
"vue-virtual-scroller": "v2.0.0-beta.8",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"vuedraggable": "^4.1.0"
|
||||||
"autoprefixer": "^10.4.19",
|
},
|
||||||
"eslint": "^9.9.1",
|
"devDependencies": {
|
||||||
"eslint-config-custom": "workspace:*",
|
"@eslint/compat": "^1.1.1",
|
||||||
"eslint-plugin-turbo": "^2.5.4",
|
"@formatjs/cli": "^6.2.12",
|
||||||
"postcss": "^8.4.39",
|
"@modrinth/tooling-config": "workspace:*",
|
||||||
"prettier": "^3.2.5",
|
"@nuxt/eslint-config": "^0.5.6",
|
||||||
"sass": "^1.74.1",
|
"@taijased/vue-render-tracker": "^1.0.7",
|
||||||
"tailwindcss": "^3.4.4",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"tsconfig": "workspace:*",
|
"autoprefixer": "^10.4.19",
|
||||||
"typescript": "^5.5.4",
|
"eslint": "^9.9.1",
|
||||||
"vite": "^5.4.6",
|
"eslint-plugin-turbo": "^2.5.4",
|
||||||
"vue-tsc": "^2.1.6"
|
"postcss": "^8.4.39",
|
||||||
},
|
"prettier": "^3.2.5",
|
||||||
"packageManager": "pnpm@9.4.0",
|
"sass": "^1.74.1",
|
||||||
"web-types": "../../web-types.json"
|
"tailwindcss": "^3.4.4",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"vite": "^8.0.0",
|
||||||
|
"vue-component-type-helpers": "^3.1.8",
|
||||||
|
"vue-tsc": "^2.1.6"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.4.0",
|
||||||
|
"web-types": "../../web-types.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+1510
-716
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
+12
-12
@@ -1,18 +1,18 @@
|
|||||||
|
export { default as ATLauncherIcon } from './atlauncher.svg'
|
||||||
export { default as BuyMeACoffeeIcon } from './bmac.svg'
|
export { default as BuyMeACoffeeIcon } from './bmac.svg'
|
||||||
export { default as DiscordIcon } from './discord.svg'
|
export { default as DiscordIcon } from './discord.svg'
|
||||||
|
export { default as GDLauncherIcon } from './gdlauncher.png'
|
||||||
|
export { default as GithubIcon } from './github.svg'
|
||||||
|
export { default as GitLabIcon } from './gitlab.svg'
|
||||||
|
export { default as GoogleIcon } from './google.svg'
|
||||||
export { default as KoFiIcon } from './kofi.svg'
|
export { default as KoFiIcon } from './kofi.svg'
|
||||||
|
export { default as MastodonIcon } from './mastodon.svg'
|
||||||
|
export { default as MicrosoftIcon } from './microsoft.svg'
|
||||||
|
export { default as MultiMCIcon } from './multimc.webp'
|
||||||
|
export { default as OpenCollectiveIcon } from './opencollective.svg'
|
||||||
export { default as PatreonIcon } from './patreon.svg'
|
export { default as PatreonIcon } from './patreon.svg'
|
||||||
export { default as PaypalIcon } from './paypal.svg'
|
export { default as PaypalIcon } from './paypal.svg'
|
||||||
export { default as OpenCollectiveIcon } from './opencollective.svg'
|
|
||||||
export { default as TwitterIcon } from './twitter.svg'
|
|
||||||
export { default as GithubIcon } from './github.svg'
|
|
||||||
export { default as MastodonIcon } from './mastodon.svg'
|
|
||||||
export { default as RedditIcon } from './reddit.svg'
|
|
||||||
export { default as GoogleIcon } from './google.svg'
|
|
||||||
export { default as MicrosoftIcon } from './microsoft.svg'
|
|
||||||
export { default as SteamIcon } from './steam.svg'
|
|
||||||
export { default as GitLabIcon } from './gitlab.svg'
|
|
||||||
export { default as ATLauncherIcon } from './atlauncher.svg'
|
|
||||||
export { default as GDLauncherIcon } from './gdlauncher.png'
|
|
||||||
export { default as MultiMCIcon } from './multimc.webp'
|
|
||||||
export { default as PrismIcon } from './prism.svg'
|
export { default as PrismIcon } from './prism.svg'
|
||||||
|
export { default as RedditIcon } from './reddit.svg'
|
||||||
|
export { default as SteamIcon } from './steam.svg'
|
||||||
|
export { default as TwitterIcon } from './twitter.svg'
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
export { default as SwapIcon } from './arrow-left-right.svg'
|
|
||||||
export { default as ToggleIcon } from './toggle.svg'
|
|
||||||
export { default as PackageIcon } from './package.svg'
|
|
||||||
export { default as VersionIcon } from './milestone.svg'
|
|
||||||
export { default as TextInputIcon } from './text-cursor-input.svg'
|
|
||||||
export { default as AddProjectImage } from './add-project.svg'
|
export { default as AddProjectImage } from './add-project.svg'
|
||||||
export { default as NewInstanceImage } from './new-instance.svg'
|
export { default as SwapIcon } from './arrow-left-right.svg'
|
||||||
export { default as MenuIcon } from './menu.svg'
|
export { default as MenuIcon } from './menu.svg'
|
||||||
export { default as ChatIcon } from './messages-square.svg'
|
export { default as ChatIcon } from './messages-square.svg'
|
||||||
export { default as Pirate } from './pirate.svg'
|
export { default as VersionIcon } from './milestone.svg'
|
||||||
export { default as Microsoft } from './microsoft.svg'
|
export { default as NewInstanceImage } from './new-instance.svg'
|
||||||
export { default as PirateShip } from './pirate-ship.svg'
|
export { default as PackageIcon } from './package.svg'
|
||||||
|
export { default as TextInputIcon } from './text-cursor-input.svg'
|
||||||
|
export { default as ToggleIcon } from './toggle.svg'
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<svg height="800px" width="800px" version="1.1" id="Layer_1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 511.672 511.672" xml:space="preserve">
|
|
||||||
<path style="fill:#ED5564;" d="M227.674,44.901c0,0,0.047-0.031,0.141-0.109c-0.031,0.031-3.342,2.437-9.088,3.514
|
|
||||||
c-7.745,1.437-16.208-0.109-25.14-4.591c-31.386-15.771-68.175-0.968-69.721-0.344l0.016,0.062c-3.88,1.593-6.621,5.403-6.621,9.869
|
|
||||||
c0,5.887,4.771,10.649,10.657,10.649c1.694,0,3.295-0.406,4.716-1.109c4.466-1.624,30.816-10.416,51.381-0.078
|
|
||||||
c10.954,5.497,20.706,7.371,28.919,7.371c17.028,0,27.42-8.073,28.044-8.573L227.674,44.901z"/>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#7F4545;" d="M234.514,31.973c-5.887,0-10.657,4.778-10.657,10.665v351.704h21.322V42.638
|
|
||||||
C245.179,36.751,240.401,31.973,234.514,31.973z"/>
|
|
||||||
<path style="fill:#7F4545;" d="M511.672,319.655c0-5.887-4.777-10.665-10.648-10.665c-1.031,0-2.016,0.156-2.951,0.422l0,0
|
|
||||||
l-0.234,0.062l0,0l-108.213,31.073l5.871,20.487l108.463-31.137l0,0C508.408,328.618,511.672,324.512,511.672,319.655z"/>
|
|
||||||
</g>
|
|
||||||
<path style="fill:#A85D5D;" d="M10.689,308.99l99.725,33.385c0,0,39.116,41.239,124.1,41.239c85,0,106.611-29.138,106.611-29.138
|
|
||||||
l85.258-24.484c9.588,160.21-122.656,149.561-122.656,149.561H115.294c-86.171-5.996-73.633-88.568-73.633-88.568l-13.187-9.728
|
|
||||||
l-4.208-18.021L0,351.635L10.689,308.99z"/>
|
|
||||||
<path style="fill:#965353;" d="M426.664,335.317c-5.871,132.337-122.938,122.905-122.938,122.905H115.294
|
|
||||||
c-57.409-3.981-71.001-41.973-73.68-66.879c-0.765,5.84-9.205,82.432,73.68,88.209h188.433
|
|
||||||
C303.727,479.553,433.004,489.968,426.664,335.317z"/>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#434A54;" d="M277.166,415.594c0,5.887,4.763,10.649,10.649,10.649c5.888,0,10.649-4.763,10.649-10.649
|
|
||||||
s-4.762-10.665-10.649-10.665C281.929,404.929,277.166,409.707,277.166,415.594z"/>
|
|
||||||
<path style="fill:#434A54;" d="M234.514,415.594c0,5.887,4.778,10.649,10.665,10.649s10.657-4.763,10.657-10.649
|
|
||||||
s-4.77-10.665-10.657-10.665S234.514,409.707,234.514,415.594z"/>
|
|
||||||
<path style="fill:#434A54;" d="M191.877,415.594c0,5.887,4.771,10.649,10.657,10.649s10.665-4.763,10.665-10.649
|
|
||||||
s-4.778-10.665-10.665-10.665S191.877,409.707,191.877,415.594z"/>
|
|
||||||
<path style="fill:#434A54;" d="M149.24,415.594c0,5.887,4.771,10.649,10.657,10.649s10.657-4.763,10.657-10.649
|
|
||||||
s-4.771-10.665-10.657-10.665S149.24,409.707,149.24,415.594z"/>
|
|
||||||
<path style="fill:#434A54;" d="M99.569,330.305C114.942,207.719,74.616,95.869,74.616,95.869h260.169
|
|
||||||
c80.105,93.783,24.953,234.561,24.953,234.561C262.832,285.849,99.569,330.305,99.569,330.305z"/>
|
|
||||||
</g>
|
|
||||||
<g style="opacity:0.1;">
|
|
||||||
<path style="fill:#FFFFFF;" d="M334.785,95.869h-21.314c69.206,81.026,37.445,197.147,27.529,227.222
|
|
||||||
c6.434,2.123,12.695,4.56,18.738,7.339C359.738,330.43,414.891,189.652,334.785,95.869z"/>
|
|
||||||
</g>
|
|
||||||
<path style="fill:#E6E9ED;" d="M170.555,196.477c0-35.321,28.638-63.959,63.959-63.959c35.329,0,63.951,28.638,63.951,63.959
|
|
||||||
c0,18.941-8.213,35.961-21.299,47.672v26.952h-85.289v-26.952C178.792,232.438,170.555,215.418,170.555,196.477z"/>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#434A54;" d="M250.503,196.477c0,5.887,4.778,10.665,10.665,10.665c5.888,0,10.658-4.778,10.658-10.665
|
|
||||||
s-4.771-10.665-10.658-10.665C255.282,185.812,250.503,190.59,250.503,196.477z"/>
|
|
||||||
<path style="fill:#434A54;" d="M197.21,196.477c0,5.887,4.771,10.665,10.657,10.665s10.657-4.778,10.657-10.665
|
|
||||||
s-4.771-10.665-10.657-10.665S197.21,190.59,197.21,196.477z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#CCD1D9;" d="M277.166,271.085c-0.016-5.871-4.777-10.649-10.673-10.649c-5.887,0-10.657,4.778-10.657,10.665
|
|
||||||
h21.33V271.085z"/>
|
|
||||||
<path style="fill:#CCD1D9;" d="M255.836,271.085c0-5.871-4.77-10.649-10.657-10.649s-10.665,4.778-10.665,10.665h21.322V271.085z" />
|
|
||||||
<path style="fill:#CCD1D9;" d="M234.514,271.085c0-5.871-4.771-10.649-10.657-10.649s-10.657,4.778-10.657,10.665h21.314V271.085z" />
|
|
||||||
<path style="fill:#CCD1D9;" d="M213.199,271.085c-0.008-5.871-4.778-10.649-10.665-10.649s-10.657,4.778-10.657,10.665h21.322
|
|
||||||
L213.199,271.085L213.199,271.085z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg width="10mm" height="10mm" viewBox="0 0 10 10" version="1.1" id="svg26662" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" sodipodi:docname="pir.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview id="namedview26664" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:document-units="mm" showgrid="false" inkscape:zoom="10.35098" inkscape:cx="16.375261" inkscape:cy="42.073312" inkscape:window-width="1488" inkscape:window-height="1230" inkscape:window-x="2794" inkscape:window-y="123" inkscape:window-maximized="0" inkscape:current-layer="layer1" />
|
|
||||||
<defs id="defs26659" />
|
|
||||||
<g inkscape:label="Слой 1" inkscape:groupmode="layer" id="layer1">
|
|
||||||
<path id="path26647" style="fill:#e7f9fb;fill-opacity:1;stroke:none;stroke-width:0.734686;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:5.8;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" d="M 0.953646,0 C 0.4245958,0 0,0.42377 0,0.950056 V 9.051658 C 0,9.577943 0.4245958,9.9999995 0.953646,9.9999995 H 9.044545 C 9.573595,9.9999995 10,9.577943 10,9.051658 V 0.950056 C 10,0.42377 9.573595,0 9.044545,0 Z m 4.0319653,0.680202 c 0.7122257,0 1.1971907,0.171336 1.6235677,0.587681 C 7.149602,1.795597 7.303455,2.529484 7.076165,3.503527 6.954393,4.024672 6.743646,4.411034 6.384675,4.767983 6.046182,5.105514 5.7597,5.274565 5.369942,5.364516 4.7470347,5.510152 4.177518,5.313116 3.618915,4.763985 3.260174,4.411034 3.0504322,4.028385 2.9274247,3.503527 2.6985267,2.5272 2.8504547,1.797596 3.392573,1.267883 3.8051351,0.866387 4.2970505,0.680202 4.9856113,0.680202 Z M 4.1253339,2.785916 C 3.7727965,2.786202 3.5578276,2.980097 3.5578276,3.29821 c 0,0.197035 0.074385,0.320683 0.2711164,0.455467 C 4.0618628,3.91359 4.4444125,3.833637 4.5886441,3.596619 4.6788251,3.450984 4.6839941,3.154002 4.6001321,3.01265 4.5082281,2.855593 4.3583101,2.784203 4.1277751,2.784203 Z m 1.7295441,0 c -0.20506,0 -0.404923,0.09423 -0.493868,0.26557 C 5.226026,3.311345 5.34091,3.641452 5.60714,3.779948 5.859588,3.90845 6.185962,3.822778 6.372268,3.574345 6.463308,3.45441 6.469338,3.164568 6.383758,3.033211 6.273759,2.864731 6.062581,2.784774 5.85752,2.784774 Z M 1.8659927,5.307119 c 0.00862,-5.71e-4 0.020104,0 0.03073,0 0.2179844,2.86e-4 0.5876389,0.16848 1.6846262,0.695051 C 4.3488321,6.369399 4.9972712,6.669808 5.018122,6.669808 5.038222,6.669237 5.68049,6.371113 6.440564,6.008738 7.200522,5.64465 7.88377,5.323396 7.958155,5.323396 8.14742,5.283416 8.403142,5.409066 8.509492,5.577544 8.663431,5.82598 8.604552,6.167795 8.374795,6.361404 8.316205,6.409954 6.898963,7.091579 5.226428,7.875439 2.6791977,9.069933 2.1554033,9.312944 1.9972712,9.312944 c -0.2153996,0 -0.2679571,0 -0.3915391,-0.114223 C 1.3150575,8.936006 1.3506989,8.509952 1.6827021,8.264656 1.7519171,8.213256 2.1996325,7.990519 2.6777048,7.764356 3.1557197,7.54162 3.5582872,7.334304 3.5756915,7.334304 c 0.01436,0 -0.3826933,-0.217025 -0.8872167,-0.448042 C 2.1839513,6.654959 1.7142365,6.421657 1.646802,6.369971 1.4819495,6.24718 1.4026824,6.080128 1.4026824,5.863103 c 0,-0.219881 0.09535,-0.394643 0.2731268,-0.491448 0.063758,-0.03141 0.1203366,-0.05711 0.1901261,-0.06282 z M 7.094115,7.62329 7.661622,7.905994 c 0.784456,0.383506 0.930354,0.522573 0.930354,0.881807 0,0.219881 -0.105403,0.397498 -0.280307,0.480311 -0.151641,0.06568 -0.331859,0.06853 -0.513628,0 C 7.59815,9.193862 5.819639,8.388875 5.788334,8.354036 c -0.01436,0 0.273414,-0.182758 0.639363,-0.378651 z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" version="1.1" viewBox="0 0 1793 199">
|
|
||||||
<g>
|
|
||||||
<g id="Layer_1">
|
|
||||||
<g id="green" fill="var(--color-brand)">
|
|
||||||
<path d="M1184.1,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
|
||||||
<path d="M1291.1,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
|
||||||
<path d="M1357.2,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
|
||||||
<path d="M1460,165.3l-40.8-95.1h23.2l35.1,83.9h-11.4l36.3-83.9h21.4l-40.8,95.1h-23Z"/>
|
|
||||||
<path d="M1579.6,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
|
||||||
<path d="M1645.7,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
|
||||||
<path d="M1749.9,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
|
||||||
<g>
|
|
||||||
<path d="M9.8,143l63.4-38.1-5.8-15.3,18.1-18.6,22.9-4.9,6.6,8.2-10.6,10.7-9.2,2.9-6.6,6.8,3.2,9,6.5,6.9,9.2-2.5,6.6-7.2,14.3-4.5,4.3,9.6-14.8,18.1-24.8,7.8-11.1-12.4-63.6,38.2c-3-3.9-6.5-9.4-8.8-14.7ZM192.8,65.4l-50.4,13.6c2.8,7.4,3.7,11.7,4.5,16.5l50.3-13.6c-.8-5.4-2.2-10.8-4.4-16.5Z" fill-rule="evenodd"/>
|
|
||||||
<path d="M17.3,106.5c3.6,42.1,38.9,75.2,82,75.2s60.7-18.9,74-46.3l16.4,5.7c-15.8,34.1-50.3,57.9-90.4,57.9S3.6,158.2,0,106.5h17.3ZM.3,89.4C5.3,39.2,47.8,0,99.3,0s99.5,44.6,99.5,99.5-1.1,17.4-3.3,25.5l-16.3-5.7c1.6-6.5,2.4-13.1,2.4-19.8,0-45.4-36.9-82.3-82.3-82.3S22.6,48.7,17.6,89.4H.3Z" fill-rule="evenodd"/>
|
|
||||||
<path d="M99,51.6c-26.4,0-47.9,21.5-47.9,48s21.5,48,48,48,2.7,0,4-.2l4.8,16.8c-2.9.4-5.8.6-8.8.6-36,0-65.2-29.2-65.2-65.2S63.1,34.4,99,34.4s1.8,0,2.7,0l-2.7,17.1ZM118.6,37.4c26.4,8.3,45.6,33,45.6,62.2s-16.4,50.2-39.8,60l-4.8-16.7c16.2-7.7,27.4-24.2,27.4-43.3s-13-38.1-31.1-44.9l2.7-17.2Z" fill-rule="evenodd"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g id="black" fill="currentColor">
|
|
||||||
<path d="M354.8,69.2c12,0,21.7,3.4,28.6,10.4,7,7.2,10.6,17.5,10.6,31.5v54.8h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,5-6.8,12.2-6.8,21.3v48.5h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,4.8-6.8,12-6.8,21.3v48.5h-22.4v-95.6h21.3v12.2c3.6-4.3,8.1-7.5,13.4-9.8,5.4-2.3,11.3-3.4,17.9-3.4s13.6,1.3,19.2,3.9c5.5,2.9,9.8,6.8,13.1,12,3.9-5,8.9-8.9,15.2-11.8,6.3-2.7,13.1-4.1,20.6-4.1ZM466,167.2c-9.7,0-18.4-2.1-26.1-6.3-7.6-4-13.8-10.1-18.1-17.5-4.5-7.3-6.6-15.7-6.6-25.2s2.1-17.9,6.6-25.2c4.3-7.4,10.6-13.4,18.1-17.4,7.7-4.1,16.5-6.3,26.1-6.3s18.6,2.1,26.3,6.3c7.7,4.1,13.8,10,18.3,17.4,4.3,7.3,6.4,15.7,6.4,25.2s-2.1,17.9-6.4,25.2c-4.5,7.5-10.6,13.4-18.3,17.5-7.7,4.1-16.5,6.3-26.3,6.3h0ZM466,148c8.2,0,15-2.7,20.4-8.2,5.4-5.5,8.1-12.7,8.1-21.7s-2.7-16.1-8.1-21.7c-5.4-5.5-12.2-8.2-20.4-8.2s-15,2.7-20.2,8.2c-5.4,5.5-8.1,12.7-8.1,21.7s2.7,16.1,8.1,21.7c5.2,5.5,12,8.2,20.2,8.2ZM631.5,33.1v132.8h-21.5v-12.3c-3.7,4.4-8.3,7.9-13.6,10.2-5.5,2.3-11.5,3.4-18.1,3.4s-17.4-2-24.7-6.1c-7.3-4.1-13.2-9.8-17.4-17.4-4.1-7.3-6.3-15.9-6.3-25.6s2.1-18.3,6.3-25.6c4.1-7.3,10-13.1,17.4-17.2,7.3-4.1,15.6-6.1,24.7-6.1s12.2,1.1,17.4,3.2c5.2,2.1,9.8,5.4,13.4,9.7v-49h22.4ZM581.1,148c5.4,0,10.2-1.3,14.5-3.8,4.3-2.3,7.7-5.9,10.2-10.4,2.5-4.5,3.8-9.8,3.8-15.7s-1.3-11.3-3.8-15.7c-2.5-4.5-5.9-8.1-10.2-10.6-4.3-2.3-9.1-3.6-14.5-3.6s-10.2,1.3-14.5,3.6c-4.3,2.5-7.7,6.1-10.2,10.6-2.5,4.5-3.8,9.8-3.8,15.7s1.3,11.3,3.8,15.7c2.5,4.5,5.9,8.1,10.2,10.4,4.3,2.5,9.1,3.8,14.5,3.8ZM681.6,84.3c6.4-10,17.7-15,34-15v21.3c-1.7-.3-3.4-.5-5.2-.5-8.8,0-15.6,2.5-20.4,7.5-4.8,5.2-7.3,12.5-7.3,22v46.4h-22.4v-95.6h21.3v14h0ZM734.1,70.3h22.4v95.6h-22.4v-95.6ZM745.4,54.6c-4.1,0-7.5-1.3-10.2-3.9-2.7-2.4-4.2-5.9-4.1-9.5,0-3.8,1.4-7,4.1-9.7,2.7-2.5,6.1-3.8,10.2-3.8s7.5,1.3,10.2,3.6c2.7,2.5,4.1,5.5,4.1,9.3s-1.3,7.2-3.9,9.8c-2.7,2.7-6.3,4.1-10.4,4.1ZM839.5,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4v-95.6h21.3v12.3c3.8-4.5,8.4-7.7,14-10,5.5-2.3,12-3.4,19-3.4ZM964.8,160.7c-2.8,2.2-6,3.9-9.5,4.8-3.9,1.1-7.9,1.6-12,1.6-10.6,0-18.6-2.7-24.3-8.2-5.7-5.5-8.6-13.4-8.6-24v-46h-15.7v-17.9h15.7v-21.8h22.4v21.8h25.6v17.9h-25.6v45.5c0,4.7,1.1,8.2,3.4,10.6,2.3,2.5,5.5,3.8,9.8,3.8s9.1-1.3,12.5-3.9l6.3,15.9ZM1036.9,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4V33.1h22.4v48.3c3.8-3.9,8.2-7,13.8-9.1,5.4-2,11.5-3,18.1-3Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 7.0 KiB |
@@ -2,159 +2,234 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import '@modrinth/ui/src/styles/tailwind-utilities.css';
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-minecraft {
|
.font-minecraft {
|
||||||
font-family: 'bundled-minecraft-font-mrapp', monospace;
|
font-family: 'bundled-minecraft-font-mrapp', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: var(--font-standard, sans-serif), sans-serif;
|
font-family: var(--font-standard, sans-serif), sans-serif;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--view-width: calc(100% - 5rem);
|
--view-width: calc(100% - 5rem);
|
||||||
--expanded-view-width: calc(100% - 13rem);
|
--expanded-view-width: calc(100% - 13rem);
|
||||||
|
--medal-promotion-bg: #000;
|
||||||
|
--medal-promotion-bg-orange: rgba(208, 246, 255, 0.25);
|
||||||
|
--medal-promotion-text-orange: #42abff;
|
||||||
|
--medal-promotion-bg-gradient: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(66, 170, 255, 0.15),
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-divider {
|
.card-divider {
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--color-button-bg);
|
color: var(--color-button-bg);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: var(--gap-sm) 0;
|
margin: var(--gap-sm) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-wrap {
|
.no-wrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-select {
|
.no-select {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-link);
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
&:hover {
|
will-change: filter;
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
padding-block: var(--gap-sm);
|
padding-block: var(--gap-sm);
|
||||||
padding-inline: var(--gap-lg);
|
padding-inline: var(--gap-lg);
|
||||||
width: min-content;
|
width: min-content;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.1rem;
|
width: 1.1rem;
|
||||||
height: 1.1rem;
|
height: 1.1rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.featured {
|
&.featured {
|
||||||
background-color: var(--color-brand-highlight);
|
background-color: var(--color-brand-highlight);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: auto;
|
scrollbar-width: auto;
|
||||||
scrollbar-color: var(--color-scrollbar) var(--color-bg);
|
scrollbar-color: var(--color-scrollbar) var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chrome, Edge, and Safari */
|
/* Chrome, Edge, and Safari */
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
border: 3px solid transparent;
|
border: 3px solid transparent;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar:hover {
|
*::-webkit-scrollbar:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
background-color: var(--color-scrollbar);
|
background-color: var(--color-scrollbar);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
border: 5px solid transparent;
|
border: 5px solid transparent;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighted {
|
.highlighted {
|
||||||
box-shadow: 0 0 1rem var(--color-brand) !important;
|
box-shadow: 0 0 1rem var(--color-brand) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gecko {
|
.gecko {
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-shadow {
|
.card-shadow {
|
||||||
box-shadow: var(--shadow-card);
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From the Bootstrap project
|
||||||
|
// The MIT License (MIT)
|
||||||
|
// Copyright (c) 2011-2023 The Bootstrap Authors
|
||||||
|
// https://github.com/twbs/bootstrap/blob/2f617215755b066904248525a8c56ea425dde871/scss/mixins/_visually-hidden.scss#L8
|
||||||
|
.visually-hidden {
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0, 0, 0, 0) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
border: 0 !important;
|
||||||
|
|
||||||
|
&:not(caption) {
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import '@modrinth/assets/omorphia.scss';
|
@import '@modrinth/assets/omorphia.scss';
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-radius: var(--size-rounded-sm);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
// safari iOS rounds inputs by default
|
||||||
|
// set the appearance to none to prevent this
|
||||||
|
appearance: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
background: var(--color-button-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
border: none;
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-inset-sm),
|
||||||
|
0 0 0 0 transparent;
|
||||||
|
transition: box-shadow 0.1s ease-in-out;
|
||||||
|
min-height: 36px;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 transparent,
|
||||||
|
0 0 0 0.25rem var(--color-brand-shadow);
|
||||||
|
color: var(--color-button-text-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled,
|
||||||
|
&[disabled='true'] {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus::placeholder {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-button-text);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
img {
|
img {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,46 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
FolderOpenIcon,
|
EyeIcon,
|
||||||
PlayIcon,
|
FolderOpenIcon,
|
||||||
PlusIcon,
|
PlayIcon,
|
||||||
TrashIcon,
|
PlusIcon,
|
||||||
StopCircleIcon,
|
SearchIcon,
|
||||||
EyeIcon,
|
StopCircleIcon,
|
||||||
SearchIcon,
|
TrashIcon,
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, DropdownSelect } from '@modrinth/ui'
|
import {
|
||||||
import { formatCategoryHeader } from '@modrinth/utils'
|
Accordion,
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
DropdownSelect,
|
||||||
|
formatLoader,
|
||||||
|
injectNotificationManager,
|
||||||
|
StyledInput,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
|
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||||
import { duplicate, remove } from '@/helpers/profile.js'
|
import { duplicate, remove } from '@/helpers/profile.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const instanceOptions = ref(null)
|
const instanceOptions = ref(null)
|
||||||
const instanceComponents = ref(null)
|
const instanceComponents = ref(null)
|
||||||
@@ -39,335 +49,308 @@ const currentDeleteInstance = ref(null)
|
|||||||
const confirmModal = ref(null)
|
const confirmModal = ref(null)
|
||||||
|
|
||||||
async function deleteProfile() {
|
async function deleteProfile() {
|
||||||
if (currentDeleteInstance.value) {
|
if (currentDeleteInstance.value) {
|
||||||
instanceComponents.value = instanceComponents.value.filter(
|
instanceComponents.value = instanceComponents.value.filter(
|
||||||
(x) => x.instance.path !== currentDeleteInstance.value,
|
(x) => x.instance.path !== currentDeleteInstance.value,
|
||||||
)
|
)
|
||||||
await remove(currentDeleteInstance.value).catch(handleError)
|
await remove(currentDeleteInstance.value).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateProfile(p) {
|
async function duplicateProfile(p) {
|
||||||
await duplicate(p).catch(handleError)
|
await duplicate(p).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRightClick = (event, profilePathId) => {
|
const handleRightClick = (event, profilePathId) => {
|
||||||
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
|
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
|
||||||
const baseOptions = [
|
const baseOptions = [
|
||||||
{ name: 'add_content' },
|
{ name: 'add_content' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ name: 'edit' },
|
{ name: 'edit' },
|
||||||
{ name: 'duplicate' },
|
{ name: 'duplicate' },
|
||||||
{ name: 'open' },
|
{ name: 'open' },
|
||||||
{ name: 'copy' },
|
{ name: 'copy' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
instanceOptions.value.showMenu(
|
instanceOptions.value.showMenu(
|
||||||
event,
|
event,
|
||||||
item,
|
item,
|
||||||
item.playing
|
item.playing
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'stop',
|
name: 'stop',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
name: 'play',
|
name: 'play',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionsClick = async (args) => {
|
const handleOptionsClick = async (args) => {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'play':
|
case 'play':
|
||||||
args.item.play(null, 'InstanceGridContextMenu')
|
args.item.play(null, 'InstanceGridContextMenu')
|
||||||
break
|
break
|
||||||
case 'stop':
|
case 'stop':
|
||||||
args.item.stop(null, 'InstanceGridContextMenu')
|
args.item.stop(null, 'InstanceGridContextMenu')
|
||||||
break
|
break
|
||||||
case 'add_content':
|
case 'add_content':
|
||||||
await args.item.addContent()
|
await args.item.addContent()
|
||||||
break
|
break
|
||||||
case 'edit':
|
case 'edit':
|
||||||
await args.item.seeInstance()
|
await args.item.seeInstance()
|
||||||
break
|
break
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
if (args.item.instance.install_stage == 'installed')
|
if (args.item.instance.install_stage == 'installed')
|
||||||
await duplicateProfile(args.item.instance.path)
|
await duplicateProfile(args.item.instance.path)
|
||||||
break
|
break
|
||||||
case 'open':
|
case 'open':
|
||||||
await args.item.openFolder()
|
await args.item.openFolder()
|
||||||
break
|
break
|
||||||
case 'copy':
|
case 'copy':
|
||||||
await navigator.clipboard.writeText(args.item.instance.path)
|
await navigator.clipboard.writeText(args.item.instance.path)
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
currentDeleteInstance.value = args.item.instance.path
|
currentDeleteInstance.value = args.item.instance.path
|
||||||
confirmModal.value.show()
|
confirmModal.value.show()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const state = useStorage(
|
||||||
|
`${props.label}-grid-display-state`,
|
||||||
|
{
|
||||||
|
group: 'Group',
|
||||||
|
sortBy: 'Name',
|
||||||
|
collapsedGroups: [],
|
||||||
|
},
|
||||||
|
localStorage,
|
||||||
|
{ mergeDefaults: true },
|
||||||
|
)
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const group = ref('Group')
|
const collapsedSectionKeys = computed(() => new Set(state.value.collapsedGroups ?? []))
|
||||||
const sortBy = ref('Name')
|
|
||||||
|
const getSectionKey = (sectionName) => `${state.value.group}:${sectionName}`
|
||||||
|
|
||||||
|
const isSectionCollapsed = (sectionName) => {
|
||||||
|
return collapsedSectionKeys.value.has(getSectionKey(sectionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSectionCollapsed = (sectionName, collapsed) => {
|
||||||
|
const sectionKey = getSectionKey(sectionName)
|
||||||
|
const collapsedSections = new Set(state.value.collapsedGroups ?? [])
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
collapsedSections.add(sectionKey)
|
||||||
|
} else {
|
||||||
|
collapsedSections.delete(sectionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.value.collapsedGroups = [...collapsedSections]
|
||||||
|
}
|
||||||
|
|
||||||
const filteredResults = computed(() => {
|
const filteredResults = computed(() => {
|
||||||
const instances = props.instances.filter((instance) => {
|
const { group = 'Group', sortBy = 'Name' } = state.value
|
||||||
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
|
||||||
})
|
|
||||||
|
|
||||||
if (sortBy.value === 'Name') {
|
const instances = props.instances.filter((instance) => {
|
||||||
instances.sort((a, b) => {
|
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
||||||
return a.name.localeCompare(b.name)
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sortBy.value === 'Game version') {
|
if (sortBy === 'Name') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return a.game_version.localeCompare(b.game_version)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Last played') {
|
if (sortBy === 'Game version') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Date created') {
|
if (sortBy === 'Last played') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Date modified') {
|
if (sortBy === 'Date created') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceMap = new Map()
|
if (sortBy === 'Date modified') {
|
||||||
|
instances.sort((a, b) => {
|
||||||
|
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (group.value === 'Loader') {
|
const instanceMap = new Map()
|
||||||
instances.forEach((instance) => {
|
|
||||||
const loader = formatCategoryHeader(instance.loader)
|
|
||||||
if (!instanceMap.has(loader)) {
|
|
||||||
instanceMap.set(loader, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceMap.get(loader).push(instance)
|
if (group === 'Loader') {
|
||||||
})
|
instances.forEach((instance) => {
|
||||||
} else if (group.value === 'Game version') {
|
const loader = formatLoader(formatMessage, instance.loader)
|
||||||
instances.forEach((instance) => {
|
if (!instanceMap.has(loader)) {
|
||||||
if (!instanceMap.has(instance.game_version)) {
|
instanceMap.set(loader, [])
|
||||||
instanceMap.set(instance.game_version, [])
|
}
|
||||||
}
|
|
||||||
|
|
||||||
instanceMap.get(instance.game_version).push(instance)
|
instanceMap.get(loader).push(instance)
|
||||||
})
|
})
|
||||||
} else if (group.value === 'Group') {
|
} else if (group === 'Game version') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
if (instance.groups.length === 0) {
|
if (!instanceMap.has(instance.game_version)) {
|
||||||
instance.groups.push('None')
|
instanceMap.set(instance.game_version, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const category of instance.groups) {
|
instanceMap.get(instance.game_version).push(instance)
|
||||||
if (!instanceMap.has(category)) {
|
})
|
||||||
instanceMap.set(category, [])
|
} else if (group === 'Group') {
|
||||||
}
|
instances.forEach((instance) => {
|
||||||
|
if (instance.groups.length === 0) {
|
||||||
|
instance.groups.push('None')
|
||||||
|
}
|
||||||
|
|
||||||
instanceMap.get(category).push(instance)
|
for (const category of instance.groups) {
|
||||||
}
|
if (!instanceMap.has(category)) {
|
||||||
})
|
instanceMap.set(category, [])
|
||||||
} else {
|
}
|
||||||
return instanceMap.set('None', instances)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
instanceMap.get(category).push(instance)
|
||||||
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
|
}
|
||||||
if (sortBy.value === 'Name') {
|
})
|
||||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
} else {
|
||||||
// None should always be first
|
return instanceMap.set('None', instances)
|
||||||
if (a[0] === 'None' && b[0] !== 'None') {
|
}
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (a[0] !== 'None' && b[0] === 'None') {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return a[0].localeCompare(b[0])
|
|
||||||
})
|
|
||||||
instanceMap.clear()
|
|
||||||
sortedEntries.forEach((entry) => {
|
|
||||||
instanceMap.set(entry[0], entry[1])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return instanceMap
|
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
||||||
|
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
|
||||||
|
if (sortBy === 'Name') {
|
||||||
|
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||||
|
// None should always be first
|
||||||
|
if (a[0] === 'None' && b[0] !== 'None') {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (a[0] !== 'None' && b[0] === 'None') {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return a[0].localeCompare(b[0])
|
||||||
|
})
|
||||||
|
instanceMap.clear()
|
||||||
|
sortedEntries.forEach((entry) => {
|
||||||
|
instanceMap.set(entry[0], entry[1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
|
||||||
|
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
|
||||||
|
if (group === 'Game version') {
|
||||||
|
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||||
|
return a[0].localeCompare(b[0], undefined, { numeric: true })
|
||||||
|
})
|
||||||
|
instanceMap.clear()
|
||||||
|
sortedEntries.forEach((entry) => {
|
||||||
|
instanceMap.set(entry[0], entry[1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return instanceMap
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="iconified-input flex-1">
|
<StyledInput
|
||||||
<SearchIcon />
|
v-model="search"
|
||||||
<input v-model="search" type="text" placeholder="Search" />
|
:icon="SearchIcon"
|
||||||
<Button class="r-btn" @click="() => (search = '')">
|
type="text"
|
||||||
<XIcon />
|
placeholder="Search"
|
||||||
</Button>
|
clearable
|
||||||
</div>
|
wrapper-class="flex-1"
|
||||||
<DropdownSelect
|
/>
|
||||||
v-slot="{ selected }"
|
<DropdownSelect
|
||||||
v-model="sortBy"
|
v-slot="{ selected }"
|
||||||
name="Sort Dropdown"
|
v-model="state.sortBy"
|
||||||
class="max-w-[16rem]"
|
name="Sort Dropdown"
|
||||||
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
class="max-w-[16rem]"
|
||||||
placeholder="Select..."
|
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
||||||
>
|
placeholder="Select..."
|
||||||
<span class="font-semibold text-primary">Sort by: </span>
|
>
|
||||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
<span class="font-semibold text-primary">Sort by: </span>
|
||||||
</DropdownSelect>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
<DropdownSelect
|
</DropdownSelect>
|
||||||
v-slot="{ selected }"
|
<DropdownSelect
|
||||||
v-model="group"
|
v-slot="{ selected }"
|
||||||
class="max-w-[16rem]"
|
v-model="state.group"
|
||||||
name="Group Dropdown"
|
class="max-w-[16rem]"
|
||||||
:options="['Group', 'Loader', 'Game version', 'None']"
|
name="Group Dropdown"
|
||||||
placeholder="Select..."
|
:options="['Group', 'Loader', 'Game version', 'None']"
|
||||||
>
|
placeholder="Select..."
|
||||||
<span class="font-semibold text-primary">Group by: </span>
|
>
|
||||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
<span class="font-semibold text-primary">Group by: </span>
|
||||||
</DropdownSelect>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
</div>
|
</DropdownSelect>
|
||||||
<div
|
</div>
|
||||||
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
<Accordion
|
||||||
key,
|
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
||||||
value,
|
key,
|
||||||
}))"
|
value,
|
||||||
:key="instanceSection.key"
|
}))"
|
||||||
class="row"
|
:key="instanceSection.key"
|
||||||
>
|
:divider="instanceSection.key !== 'None'"
|
||||||
<div v-if="instanceSection.key !== 'None'" class="divider">
|
:open-by-default="!isSectionCollapsed(instanceSection.key)"
|
||||||
<p>{{ instanceSection.key }}</p>
|
class="row"
|
||||||
<hr aria-hidden="true" />
|
@on-open="setSectionCollapsed(instanceSection.key, false)"
|
||||||
</div>
|
@on-close="setSectionCollapsed(instanceSection.key, true)"
|
||||||
<section class="instances">
|
>
|
||||||
<Instance
|
<template v-if="instanceSection.key !== 'None'" #title>
|
||||||
v-for="instance in instanceSection.value"
|
<span class="text-base">{{ instanceSection.key }}</span>
|
||||||
ref="instanceComponents"
|
</template>
|
||||||
:key="instance.path + instance.install_stage"
|
<section class="instances">
|
||||||
:instance="instance"
|
<Instance
|
||||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
v-for="instance in instanceSection.value"
|
||||||
/>
|
ref="instanceComponents"
|
||||||
</section>
|
:key="instance.path + instance.install_stage"
|
||||||
</div>
|
:instance="instance"
|
||||||
<ConfirmModalWrapper
|
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||||
ref="confirmModal"
|
/>
|
||||||
title="Are you sure you want to delete this instance?"
|
</section>
|
||||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
</Accordion>
|
||||||
:has-to-type="false"
|
<ConfirmDeleteInstanceModal ref="confirmModal" @delete="deleteProfile" />
|
||||||
proceed-label="Delete"
|
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||||
@proceed="deleteProfile"
|
<template #play> <PlayIcon /> Play </template>
|
||||||
/>
|
<template #stop> <StopCircleIcon /> Stop </template>
|
||||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
<template #add_content> <PlusIcon /> Add content </template>
|
||||||
<template #play> <PlayIcon /> Play </template>
|
<template #edit> <EyeIcon /> View instance </template>
|
||||||
<template #stop> <StopCircleIcon /> Stop </template>
|
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
||||||
<template #add_content> <PlusIcon /> Add content </template>
|
<template #delete> <TrashIcon /> Delete </template>
|
||||||
<template #edit> <EyeIcon /> View instance </template>
|
<template #open> <FolderOpenIcon /> Open folder </template>
|
||||||
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
||||||
<template #delete> <TrashIcon /> Delete </template>
|
</ContextMenu>
|
||||||
<template #open> <FolderOpenIcon /> Open folder </template>
|
|
||||||
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
|
||||||
</ContextMenu>
|
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
width: 100%;
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
background-color: var(--color-gray);
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: inherit;
|
|
||||||
margin: 1rem 1rem 0 !important;
|
|
||||||
padding: 1rem;
|
|
||||||
width: calc(100% - 2rem);
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
input {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-dropdown {
|
|
||||||
width: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-dropdown {
|
|
||||||
width: 15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-dropdown {
|
|
||||||
width: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labeled_button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.instances {
|
.instances {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
||||||
import { useLoading } from '@/store/state.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
throttle: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
duration: {
|
|
||||||
type: Number,
|
|
||||||
default: 1000,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 2,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: 'var(--loading-bar-gradient)',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const indicator = useLoadingIndicator({
|
|
||||||
duration: props.duration,
|
|
||||||
throttle: props.throttle,
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => indicator.clear)
|
|
||||||
|
|
||||||
const loading = useLoading()
|
|
||||||
|
|
||||||
watch(loading, (newValue) => {
|
|
||||||
if (newValue.barEnabled) {
|
|
||||||
if (newValue.loading) {
|
|
||||||
indicator.start()
|
|
||||||
} else {
|
|
||||||
indicator.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function useLoadingIndicator(opts) {
|
|
||||||
const progress = ref(0)
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const step = computed(() => 10000 / opts.duration)
|
|
||||||
|
|
||||||
let _timer = null
|
|
||||||
let _throttle = null
|
|
||||||
|
|
||||||
function start() {
|
|
||||||
clear()
|
|
||||||
progress.value = 0
|
|
||||||
if (opts.throttle) {
|
|
||||||
_throttle = setTimeout(() => {
|
|
||||||
isLoading.value = true
|
|
||||||
_startTimer()
|
|
||||||
}, opts.throttle)
|
|
||||||
} else {
|
|
||||||
isLoading.value = true
|
|
||||||
_startTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function finish() {
|
|
||||||
progress.value = 100
|
|
||||||
_hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
clearInterval(_timer)
|
|
||||||
clearTimeout(_throttle)
|
|
||||||
_timer = null
|
|
||||||
_throttle = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function _increase(num) {
|
|
||||||
progress.value = Math.min(100, progress.value + num)
|
|
||||||
}
|
|
||||||
|
|
||||||
function _hide() {
|
|
||||||
clear()
|
|
||||||
setTimeout(() => {
|
|
||||||
isLoading.value = false
|
|
||||||
setTimeout(() => {
|
|
||||||
progress.value = 0
|
|
||||||
}, 400)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
function _startTimer() {
|
|
||||||
_timer = setInterval(() => {
|
|
||||||
_increase(step.value)
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { progress, isLoading, start, finish, clear }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="loading-indicator-bar"
|
|
||||||
:style="{
|
|
||||||
'--_width': `${indicator.progress.value}%`,
|
|
||||||
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
|
|
||||||
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
|
|
||||||
top: `0`,
|
|
||||||
right: `0`,
|
|
||||||
left: `${props.offsetWidth}`,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
width: `var(--_width)`,
|
|
||||||
height: `var(--_height)`,
|
|
||||||
borderRadius: `var(--_height)`,
|
|
||||||
// opacity: `var(--_opacity)`,
|
|
||||||
background: `${props.color}`,
|
|
||||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
|
||||||
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
|
|
||||||
zIndex: 6,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.loading-indicator-bar::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
width: var(--_width);
|
|
||||||
bottom: 0;
|
|
||||||
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
|
|
||||||
opacity: calc(var(--_opacity) * 0.1);
|
|
||||||
z-index: 5;
|
|
||||||
transition:
|
|
||||||
width 0.1s ease-in-out,
|
|
||||||
opacity 0.1s ease-out;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,52 +1,55 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
FolderOpenIcon,
|
DownloadIcon,
|
||||||
PlayIcon,
|
ExternalIcon,
|
||||||
PlusIcon,
|
EyeIcon,
|
||||||
TrashIcon,
|
FolderOpenIcon,
|
||||||
DownloadIcon,
|
GlobeIcon,
|
||||||
GlobeIcon,
|
PlayIcon,
|
||||||
StopCircleIcon,
|
PlusIcon,
|
||||||
ExternalIcon,
|
StopCircleIcon,
|
||||||
EyeIcon,
|
TrashIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import { HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import { HeadingLink } from '@modrinth/ui'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
|
import LegacyProjectCard from '@/components/ui/LegacyProjectCard.vue'
|
||||||
|
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process.js'
|
||||||
|
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
|
import { injectContentInstall } from '@/providers/content-install'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
const { install: installVersion } = injectContentInstall()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
canPaginate: Boolean,
|
canPaginate: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const actualInstances = computed(() =>
|
const actualInstances = computed(() =>
|
||||||
props.instances.filter(
|
props.instances.filter(
|
||||||
(x) => (x && x.instances && x.instances[0] && x.show === undefined) || x.show,
|
(x) => (x && x.instances && x.instances[0] && x.show === undefined) || x.show,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const modsRow = ref(null)
|
const modsRow = ref(null)
|
||||||
@@ -58,124 +61,131 @@ const deleteConfirmModal = ref(null)
|
|||||||
const currentDeleteInstance = ref(null)
|
const currentDeleteInstance = ref(null)
|
||||||
|
|
||||||
async function deleteProfile() {
|
async function deleteProfile() {
|
||||||
if (currentDeleteInstance.value) {
|
if (currentDeleteInstance.value) {
|
||||||
await remove(currentDeleteInstance.value).catch(handleError)
|
await remove(currentDeleteInstance.value).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateProfile(p) {
|
async function duplicateProfile(p) {
|
||||||
await duplicate(p).catch(handleError)
|
await duplicate(p).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInstanceRightClick = async (event, passedInstance) => {
|
const handleInstanceRightClick = async (event, passedInstance) => {
|
||||||
const baseOptions = [
|
const baseOptions = [
|
||||||
{ name: 'add_content' },
|
{ name: 'add_content' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ name: 'edit' },
|
{ name: 'edit' },
|
||||||
{ name: 'duplicate' },
|
{ name: 'duplicate' },
|
||||||
{ name: 'open_folder' },
|
{ name: 'open_folder' },
|
||||||
{ name: 'copy_path' },
|
{ name: 'copy_path' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
|
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
|
||||||
|
|
||||||
const options =
|
const options =
|
||||||
runningProcesses.length > 0
|
runningProcesses.length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'stop',
|
name: 'stop',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
name: 'play',
|
name: 'play',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
|
|
||||||
instanceOptions.value.showMenu(event, passedInstance, options)
|
instanceOptions.value.showMenu(event, passedInstance, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProjectClick = (event, passedInstance) => {
|
const handleProjectClick = (event, passedInstance) => {
|
||||||
instanceOptions.value.showMenu(event, passedInstance, [
|
instanceOptions.value.showMenu(event, passedInstance, [
|
||||||
{
|
{
|
||||||
name: 'install',
|
name: 'install',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'open_link',
|
name: 'open_link',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'copy_link',
|
name: 'copy_link',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionsClick = async (args) => {
|
const handleOptionsClick = async (args) => {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'play':
|
case 'play':
|
||||||
await run(args.item.path).catch((err) =>
|
await run(args.item.path).catch((err) =>
|
||||||
handleSevereError(err, { profilePath: args.item.path }),
|
handleSevereError(err, { profilePath: args.item.path }),
|
||||||
)
|
)
|
||||||
trackEvent('InstanceStart', {
|
trackEvent('InstanceStart', {
|
||||||
loader: args.item.loader,
|
loader: args.item.loader,
|
||||||
game_version: args.item.game_version,
|
game_version: args.item.game_version,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'stop':
|
case 'stop':
|
||||||
await kill(args.item.path).catch(handleError)
|
await kill(args.item.path).catch(handleError)
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: args.item.loader,
|
loader: args.item.loader,
|
||||||
game_version: args.item.game_version,
|
game_version: args.item.game_version,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'add_content':
|
case 'add_content':
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||||
query: { i: args.item.path },
|
query: { i: args.item.path },
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'edit':
|
case 'edit':
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
path: `/instance/${encodeURIComponent(args.item.path)}`,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
|
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
currentDeleteInstance.value = args.item.path
|
currentDeleteInstance.value = args.item.path
|
||||||
deleteConfirmModal.value.show()
|
deleteConfirmModal.value.show()
|
||||||
break
|
break
|
||||||
case 'open_folder':
|
case 'open_folder':
|
||||||
await showProfileInFolder(args.item.path)
|
await showProfileInFolder(args.item.path)
|
||||||
break
|
break
|
||||||
case 'copy_path':
|
case 'copy_path':
|
||||||
await navigator.clipboard.writeText(args.item.path)
|
await navigator.clipboard.writeText(args.item.path)
|
||||||
break
|
break
|
||||||
case 'install': {
|
case 'install': {
|
||||||
await installVersion(args.item.project_id, null, null, 'ProjectCardContextMenu')
|
await installVersion(
|
||||||
|
args.item.project_id,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'ProjectCardContextMenu',
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
).catch(handleError)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'open_link':
|
case 'open_link':
|
||||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||||
break
|
break
|
||||||
case 'copy_link':
|
case 'copy_link':
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxInstancesPerCompactRow = ref(1)
|
const maxInstancesPerCompactRow = ref(1)
|
||||||
@@ -183,184 +193,177 @@ const maxInstancesPerRow = ref(1)
|
|||||||
const maxProjectsPerRow = ref(1)
|
const maxProjectsPerRow = ref(1)
|
||||||
|
|
||||||
const calculateCardsPerRow = () => {
|
const calculateCardsPerRow = () => {
|
||||||
if (rows.value.length === 0) {
|
if (rows.value.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate how many cards fit in one row
|
// Calculate how many cards fit in one row
|
||||||
const containerWidth = rows.value[0].clientWidth
|
const containerWidth = rows.value[0].clientWidth
|
||||||
// Convert container width from pixels to rem
|
// Convert container width from pixels to rem
|
||||||
const containerWidthInRem =
|
const containerWidthInRem =
|
||||||
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||||
|
|
||||||
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
|
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
|
||||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||||
|
|
||||||
if (maxInstancesPerRow.value < 5) {
|
if (maxInstancesPerRow.value < 5) {
|
||||||
maxInstancesPerRow.value *= 2
|
maxInstancesPerRow.value *= 2
|
||||||
}
|
}
|
||||||
if (maxInstancesPerCompactRow.value < 5) {
|
if (maxInstancesPerCompactRow.value < 5) {
|
||||||
maxInstancesPerCompactRow.value *= 2
|
maxInstancesPerCompactRow.value *= 2
|
||||||
}
|
}
|
||||||
if (maxProjectsPerRow.value < 3) {
|
if (maxProjectsPerRow.value < 3) {
|
||||||
maxProjectsPerRow.value *= 2
|
maxProjectsPerRow.value *= 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowContainer = ref(null)
|
const rowContainer = ref(null)
|
||||||
const resizeObserver = ref(null)
|
const resizeObserver = ref(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calculateCardsPerRow()
|
calculateCardsPerRow()
|
||||||
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
||||||
if (rowContainer.value) {
|
if (rowContainer.value) {
|
||||||
resizeObserver.value.observe(rowContainer.value)
|
resizeObserver.value.observe(rowContainer.value)
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', calculateCardsPerRow)
|
window.addEventListener('resize', calculateCardsPerRow)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', calculateCardsPerRow)
|
window.removeEventListener('resize', calculateCardsPerRow)
|
||||||
if (rowContainer.value) {
|
if (rowContainer.value) {
|
||||||
resizeObserver.value.unobserve(rowContainer.value)
|
resizeObserver.value.unobserve(rowContainer.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModalWrapper
|
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="deleteProfile" />
|
||||||
ref="deleteConfirmModal"
|
<div ref="rowContainer" class="flex flex-col gap-4">
|
||||||
title="Are you sure you want to delete this instance?"
|
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
||||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
<HeadingLink class="mt-1" :to="row.route">
|
||||||
:has-to-type="false"
|
{{ row.label }}
|
||||||
proceed-label="Delete"
|
</HeadingLink>
|
||||||
@proceed="deleteProfile"
|
<section
|
||||||
/>
|
v-if="row.instance"
|
||||||
<div ref="rowContainer" class="flex flex-col gap-4">
|
ref="modsRow"
|
||||||
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
class="instances"
|
||||||
<HeadingLink class="mt-1" :to="row.route">
|
:class="{ compact: row.compact }"
|
||||||
{{ row.label }}
|
>
|
||||||
</HeadingLink>
|
<Instance
|
||||||
<section
|
v-for="(instance, instanceIndex) in row.instances.slice(
|
||||||
v-if="row.instance"
|
0,
|
||||||
ref="modsRow"
|
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
||||||
class="instances"
|
)"
|
||||||
:class="{ compact: row.compact }"
|
:key="row.label + instance.path"
|
||||||
>
|
:instance="instance"
|
||||||
<Instance
|
:compact="row.compact"
|
||||||
v-for="(instance, instanceIndex) in row.instances.slice(
|
:first="instanceIndex === 0"
|
||||||
0,
|
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
||||||
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
/>
|
||||||
)"
|
</section>
|
||||||
:key="row.label + instance.path"
|
<section v-else ref="modsRow" class="projects">
|
||||||
:instance="instance"
|
<LegacyProjectCard
|
||||||
:compact="row.compact"
|
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
|
||||||
:first="instanceIndex === 0"
|
:key="project?.project_id"
|
||||||
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
ref="instanceComponents"
|
||||||
/>
|
class="item"
|
||||||
</section>
|
:project="project"
|
||||||
<section v-else ref="modsRow" class="projects">
|
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
|
||||||
<ProjectCard
|
/>
|
||||||
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
|
</section>
|
||||||
:key="project?.project_id"
|
</div>
|
||||||
ref="instanceComponents"
|
</div>
|
||||||
class="item"
|
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||||
:project="project"
|
<template #play> <PlayIcon /> Play </template>
|
||||||
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
|
<template #stop> <StopCircleIcon /> Stop </template>
|
||||||
/>
|
<template #add_content> <PlusIcon /> Add content </template>
|
||||||
</section>
|
<template #edit> <EyeIcon /> View instance </template>
|
||||||
</div>
|
<template #delete> <TrashIcon /> Delete </template>
|
||||||
</div>
|
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
||||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
||||||
<template #play> <PlayIcon /> Play </template>
|
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
||||||
<template #stop> <StopCircleIcon /> Stop </template>
|
<template #install> <DownloadIcon /> Install </template>
|
||||||
<template #add_content> <PlusIcon /> Add content </template>
|
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||||
<template #edit> <EyeIcon /> View instance </template>
|
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||||
<template #delete> <TrashIcon /> Delete </template>
|
</ContextMenu>
|
||||||
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
|
||||||
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
|
||||||
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
|
||||||
<template #install> <DownloadIcon /> Install </template>
|
|
||||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
|
||||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
|
||||||
</ContextMenu>
|
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
gap: var(--gap-xs);
|
gap: var(--gap-xs);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instances {
|
.instances {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||||
grid-gap: 0.75rem;
|
grid-gap: 0.75rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects {
|
.projects {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
grid-gap: 0.75rem;
|
grid-gap: 0.75rem;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,62 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownIcon, PlusIcon, FolderOpenIcon } from '@modrinth/assets'
|
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { add_project_from_path } from '@/helpers/profile.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { add_project_from_path } from '@/helpers/profile.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleAddContentFromFile = async () => {
|
const handleAddContentFromFile = async () => {
|
||||||
const newProject = await open({ multiple: true })
|
const newProject = await open({ multiple: true })
|
||||||
if (!newProject) return
|
if (!newProject) return
|
||||||
|
|
||||||
for (const project of newProject) {
|
for (const project of newProject) {
|
||||||
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
|
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchContent = async () => {
|
const handleSearchContent = async () => {
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
||||||
query: { i: props.instance.path },
|
query: { i: props.instance.path },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="joined-buttons">
|
<div class="joined-buttons">
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="handleSearchContent">
|
<button @click="handleSearchContent">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
Install content
|
Install content
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'from_file',
|
id: 'from_file',
|
||||||
action: handleAddContentFromFile,
|
action: handleAddContentFromFile,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
<template #from_file>
|
<template #from_file>
|
||||||
<FolderOpenIcon />
|
<FolderOpenIcon />
|
||||||
<span class="no-wrap"> Add from file </span>
|
<span class="no-wrap"> Add from file </span>
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,607 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ButtonStyled
|
||||||
|
v-if="hasActiveLoadingBars && !hasVisibleActiveDownloadToasts"
|
||||||
|
color="brand"
|
||||||
|
type="transparent"
|
||||||
|
circular
|
||||||
|
>
|
||||||
|
<button v-tooltip="formatMessage(messages.viewActiveDownloads)" @click="openDownloadToast()">
|
||||||
|
<DownloadIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<div v-if="offline" class="flex items-center gap-1">
|
||||||
|
<UnplugIcon class="text-secondary" />
|
||||||
|
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
|
||||||
|
</div>
|
||||||
|
<ButtonStyled color="brand" type="outlined" hover-color-fill="background">
|
||||||
|
<button
|
||||||
|
v-if="showUpdatePill"
|
||||||
|
type="button"
|
||||||
|
class="!h-[34px] overflow-hidden text-sm !transition-[width,opacity,transform,background-color,color,filter] !duration-200 ease-out"
|
||||||
|
:class="[
|
||||||
|
updatePillWidthClass,
|
||||||
|
{
|
||||||
|
'update-pill-ready-hidden': finishedDownloading && !animateReadyPill,
|
||||||
|
'update-pill-ready-visible': finishedDownloading && animateReadyPill,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:disabled="isUpdateDownloading"
|
||||||
|
:aria-busy="isUpdateDownloading"
|
||||||
|
@click="handleUpdateClick"
|
||||||
|
>
|
||||||
|
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
|
||||||
|
<DownloadIcon v-else />
|
||||||
|
<span v-if="isUpdateDownloading">
|
||||||
|
{{ formatMessage(messages.downloadingUpdate) }}
|
||||||
|
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ updateLabel }}</span>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<div
|
||||||
|
class="flex border-solid border-surface-5 text-sm items-center gap-2 py-1.5 px-3 rounded-xl border"
|
||||||
|
>
|
||||||
|
<template v-if="selectedProcess">
|
||||||
|
<OnlineIndicatorIcon />
|
||||||
|
<div class="text-contrast flex items-center gap-2">
|
||||||
|
<router-link
|
||||||
|
v-tooltip="formatMessage(messages.viewInstance)"
|
||||||
|
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
||||||
|
class="hover:underline"
|
||||||
|
>
|
||||||
|
{{ selectedProcess.profile.name }}
|
||||||
|
</router-link>
|
||||||
|
<Dropdown
|
||||||
|
v-if="currentProcesses.length > 1"
|
||||||
|
placement="bottom"
|
||||||
|
:triggers="['click']"
|
||||||
|
:hide-triggers="['click']"
|
||||||
|
@show="showProfiles = true"
|
||||||
|
@hide="showProfiles = false"
|
||||||
|
>
|
||||||
|
<ButtonStyled type="transparent" circular size="small">
|
||||||
|
<button
|
||||||
|
v-tooltip="
|
||||||
|
showProfiles
|
||||||
|
? formatMessage(messages.hideMoreRunningInstances)
|
||||||
|
: formatMessage(messages.showMoreRunningInstances)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<DropdownIcon :class="{ 'rotate-180': !!showProfiles }" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<template #popper>
|
||||||
|
<div class="flex w-[20rem] max-h-[24rem] flex-col gap-2 overflow-auto">
|
||||||
|
<div
|
||||||
|
v-for="process in currentProcesses"
|
||||||
|
:key="process.uuid"
|
||||||
|
class="flex w-full items-center gap-2 rounded-xl bg-surface-4 p-2 text-sm"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-tooltip.left="
|
||||||
|
process.uuid === selectedProcess.uuid
|
||||||
|
? formatMessage(messages.primaryInstance)
|
||||||
|
: formatMessage(messages.makePrimaryInstance)
|
||||||
|
"
|
||||||
|
class="flex flex-grow items-center gap-2"
|
||||||
|
:class="{
|
||||||
|
'active:scale-95 transition-transform': process.uuid !== selectedProcess.uuid,
|
||||||
|
}"
|
||||||
|
:disabled="process.uuid === selectedProcess.uuid"
|
||||||
|
@click="selectProcess(process)"
|
||||||
|
>
|
||||||
|
<OnlineIndicatorIcon />
|
||||||
|
<span class="mr-auto text-contrast flex items-center gap-2">
|
||||||
|
{{ process.profile.name }}
|
||||||
|
<StarIcon v-if="process.uuid === selectedProcess.uuid" class="text-orange" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-tooltip="formatMessage(messages.stopInstance)"
|
||||||
|
class="active:scale-95 flex"
|
||||||
|
@click.stop="stop(process)"
|
||||||
|
>
|
||||||
|
<StopCircleIcon class="text-red size-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-tooltip="formatMessage(messages.viewLogs)"
|
||||||
|
class="active:scale-95 flex"
|
||||||
|
@click.stop="goToTerminal(process.profile.path)"
|
||||||
|
>
|
||||||
|
<TerminalSquareIcon class="text-secondary size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-tooltip="formatMessage(messages.stopInstance)"
|
||||||
|
class="active:scale-95 flex"
|
||||||
|
@click="stop(selectedProcess)"
|
||||||
|
>
|
||||||
|
<StopCircleIcon class="text-red size-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-tooltip="formatMessage(messages.viewLogs)"
|
||||||
|
class="active:scale-95 flex"
|
||||||
|
@click="goToTerminal()"
|
||||||
|
>
|
||||||
|
<TerminalSquareIcon class="text-secondary size-5" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="size-2 rounded-full bg-secondary" />
|
||||||
|
<span class="text-secondary"> {{ formatMessage(messages.noInstancesRunning) }} </span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
DownloadIcon,
|
||||||
|
DropdownIcon,
|
||||||
|
OnlineIndicatorIcon,
|
||||||
|
RefreshCwIcon,
|
||||||
|
StarIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TerminalSquareIcon,
|
||||||
|
UnplugIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
ButtonStyled,
|
||||||
|
defineMessages,
|
||||||
|
injectNotificationManager,
|
||||||
|
injectPopupNotificationManager,
|
||||||
|
type PopupNotification,
|
||||||
|
type PopupNotificationProgressItem,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { Dropdown } from 'floating-vue'
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { loading_listener, process_listener } from '@/helpers/events'
|
||||||
|
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||||
|
import { get_many as getInstances } from '@/helpers/profile.js'
|
||||||
|
import type { LoadingBar } from '@/helpers/state'
|
||||||
|
import { progress_bars_list } from '@/helpers/state'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import {
|
||||||
|
appUpdateState,
|
||||||
|
downloadAvailableAppUpdate,
|
||||||
|
installAvailableAppUpdate,
|
||||||
|
} from '@/providers/app-update'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
const popupNotificationManager = injectPopupNotificationManager()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const showProfiles = ref(false)
|
||||||
|
|
||||||
|
interface RunningProcess {
|
||||||
|
uuid: string
|
||||||
|
profile_path: string
|
||||||
|
profile: GameInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
offline: {
|
||||||
|
id: 'app.action-bar.offline',
|
||||||
|
defaultMessage: 'Offline',
|
||||||
|
},
|
||||||
|
viewInstance: {
|
||||||
|
id: 'app.action-bar.view-instance',
|
||||||
|
defaultMessage: 'View instance',
|
||||||
|
},
|
||||||
|
showMoreRunningInstances: {
|
||||||
|
id: 'app.action-bar.show-more-running-instances',
|
||||||
|
defaultMessage: 'Show more running instances',
|
||||||
|
},
|
||||||
|
hideMoreRunningInstances: {
|
||||||
|
id: 'app.action-bar.hide-more-running-instances',
|
||||||
|
defaultMessage: 'Hide more running instances',
|
||||||
|
},
|
||||||
|
primaryInstance: {
|
||||||
|
id: 'app.action-bar.primary-instance',
|
||||||
|
defaultMessage: 'Primary instance',
|
||||||
|
},
|
||||||
|
makePrimaryInstance: {
|
||||||
|
id: 'app.action-bar.make-primary-instance',
|
||||||
|
defaultMessage: 'Make primary instance',
|
||||||
|
},
|
||||||
|
stopInstance: {
|
||||||
|
id: 'app.action-bar.stop-instance',
|
||||||
|
defaultMessage: 'Stop instance',
|
||||||
|
},
|
||||||
|
viewLogs: {
|
||||||
|
id: 'app.action-bar.view-logs',
|
||||||
|
defaultMessage: 'View logs',
|
||||||
|
},
|
||||||
|
noInstancesRunning: {
|
||||||
|
id: 'app.action-bar.no-instances-running',
|
||||||
|
defaultMessage: 'No instances running',
|
||||||
|
},
|
||||||
|
downloadingJava: {
|
||||||
|
id: 'app.action-bar.downloading-java',
|
||||||
|
defaultMessage: 'Downloading Java {version}',
|
||||||
|
},
|
||||||
|
downloads: {
|
||||||
|
id: 'app.action-bar.downloads',
|
||||||
|
defaultMessage: 'Downloads',
|
||||||
|
},
|
||||||
|
viewActiveDownloads: {
|
||||||
|
id: 'app.action-bar.view-active-downloads',
|
||||||
|
defaultMessage: 'View active downloads',
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: 'app.action-bar.update',
|
||||||
|
defaultMessage: 'Update',
|
||||||
|
},
|
||||||
|
downloadingUpdate: {
|
||||||
|
id: 'app.action-bar.downloading-update',
|
||||||
|
defaultMessage: 'Downloading update',
|
||||||
|
},
|
||||||
|
reloadToUpdate: {
|
||||||
|
id: 'app.action-bar.reload-to-update',
|
||||||
|
defaultMessage: 'Reload to update',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
downloading,
|
||||||
|
downloadPercent,
|
||||||
|
downloadProgress,
|
||||||
|
finishedDownloading,
|
||||||
|
isVisible: isUpdateVisible,
|
||||||
|
metered,
|
||||||
|
restarting,
|
||||||
|
} = appUpdateState
|
||||||
|
|
||||||
|
const isUpdateDownloading = computed(
|
||||||
|
() =>
|
||||||
|
downloading.value ||
|
||||||
|
(downloadProgress.value > 0 && downloadProgress.value < 1 && !finishedDownloading.value),
|
||||||
|
)
|
||||||
|
const showUpdatePill = computed(
|
||||||
|
() => isUpdateVisible.value && (finishedDownloading.value || metered.value),
|
||||||
|
)
|
||||||
|
const animateReadyPill = ref(false)
|
||||||
|
const updateLabel = computed(() => {
|
||||||
|
if (isUpdateDownloading.value) {
|
||||||
|
return formatMessage(messages.downloadingUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finishedDownloading.value) {
|
||||||
|
return formatMessage(messages.reloadToUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatMessage(messages.update)
|
||||||
|
})
|
||||||
|
const updatePillWidthClass = computed(() => {
|
||||||
|
if (isUpdateDownloading.value) {
|
||||||
|
return 'w-[219px]'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finishedDownloading.value) {
|
||||||
|
return 'w-[166px]'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '!w-[96px]'
|
||||||
|
})
|
||||||
|
let readyPillAnimationFrame: number | null = null
|
||||||
|
watch([showUpdatePill, finishedDownloading], async ([show, ready], [wasShown, wasReady]) => {
|
||||||
|
if (readyPillAnimationFrame !== null) {
|
||||||
|
cancelAnimationFrame(readyPillAnimationFrame)
|
||||||
|
readyPillAnimationFrame = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!show || !ready) {
|
||||||
|
animateReadyPill.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasShown && wasReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
animateReadyPill.value = false
|
||||||
|
await nextTick()
|
||||||
|
readyPillAnimationFrame = requestAnimationFrame(() => {
|
||||||
|
animateReadyPill.value = true
|
||||||
|
readyPillAnimationFrame = null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
async function handleUpdateClick() {
|
||||||
|
if (isUpdateDownloading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finishedDownloading.value) {
|
||||||
|
await installAvailableAppUpdate()
|
||||||
|
} else {
|
||||||
|
await downloadAvailableAppUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentProcesses = ref<RunningProcess[]>([])
|
||||||
|
const selectedProcess = ref<RunningProcess | undefined>()
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
const processes = ((await getRunningProcesses().catch((error) => {
|
||||||
|
handleError(error)
|
||||||
|
return []
|
||||||
|
})) ?? []) as Array<{ uuid: string; profile_path: string }>
|
||||||
|
const paths = processes.map((process) => process.profile_path)
|
||||||
|
const profiles: GameInstance[] = await getInstances(paths).catch((error) => {
|
||||||
|
handleError(error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
currentProcesses.value = processes
|
||||||
|
.map((process) => {
|
||||||
|
const profile = profiles.find((item) => process.profile_path === item.path)
|
||||||
|
if (!profile) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...process,
|
||||||
|
profile,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((process): process is RunningProcess => process !== null)
|
||||||
|
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
|
||||||
|
selectedProcess.value = currentProcesses.value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await refresh()
|
||||||
|
|
||||||
|
const offline = ref(!navigator.onLine)
|
||||||
|
function handleOffline() {
|
||||||
|
offline.value = true
|
||||||
|
}
|
||||||
|
function handleOnline() {
|
||||||
|
offline.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('offline', handleOffline)
|
||||||
|
window.addEventListener('online', handleOnline)
|
||||||
|
})
|
||||||
|
|
||||||
|
const unlistenProcess = await process_listener(async () => {
|
||||||
|
await refresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
const stop = async (process: RunningProcess) => {
|
||||||
|
try {
|
||||||
|
await killProcess(process.uuid).catch(handleError)
|
||||||
|
|
||||||
|
trackEvent('InstanceStop', {
|
||||||
|
loader: process.profile.loader,
|
||||||
|
game_version: process.profile.game_version,
|
||||||
|
source: 'AppBar',
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToTerminal(path?: string) {
|
||||||
|
const selectedPath = path ?? selectedProcess.value?.profile.path
|
||||||
|
if (!selectedPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push(`/instance/${encodeURIComponent(selectedPath)}/logs`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLoadingBars = ref<LoadingBar[]>([])
|
||||||
|
const currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
|
||||||
|
const notificationId = ref<string | number | null>(null)
|
||||||
|
const dismissed = ref(false)
|
||||||
|
|
||||||
|
function getLoadingBarKey(loadingBar: LoadingBar): string {
|
||||||
|
return `${loadingBar.loading_bar_uuid ?? loadingBar.id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoadingProgress(loadingBar: LoadingBar): number {
|
||||||
|
if (!loadingBar.total || loadingBar.total <= 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.min(1, (loadingBar.current ?? 0) / (loadingBar.total ?? 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoadingText(loadingBar: LoadingBar): string {
|
||||||
|
const percent = Math.floor(getLoadingProgress(loadingBar) * 100)
|
||||||
|
return loadingBar.message ? `${percent}% ${loadingBar.message}` : `${percent}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayIconUrl(icon: string | null | undefined): string | null {
|
||||||
|
if (!icon) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) {
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
return convertFileSrc(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNotification(): PopupNotification | null {
|
||||||
|
if (!notificationId.value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const notification = popupNotificationManager
|
||||||
|
.getNotifications()
|
||||||
|
.find((notification) => notification.id === notificationId.value)
|
||||||
|
return notification ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNotification(): void {
|
||||||
|
if (!notificationId.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
popupNotificationManager.removeNotification(notificationId.value)
|
||||||
|
notificationId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDownloadItems(): PopupNotificationProgressItem[] {
|
||||||
|
return currentLoadingBars.value.map((bar) => ({
|
||||||
|
id: getLoadingBarKey(bar),
|
||||||
|
title: bar.title ?? '',
|
||||||
|
text: getLoadingText(bar),
|
||||||
|
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
|
||||||
|
progress: getLoadingProgress(bar),
|
||||||
|
waiting: !bar.total || bar.total <= 0,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasVisibleActiveDownloadToasts = computed(() => !!getNotification())
|
||||||
|
const hasActiveLoadingBars = computed(() => currentLoadingBars.value.length > 0)
|
||||||
|
|
||||||
|
function updateNotification(resummon = false): void {
|
||||||
|
if (resummon) {
|
||||||
|
dismissed.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLoadingBars.value.length === 0) {
|
||||||
|
removeNotification()
|
||||||
|
dismissed.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationId.value && !getNotification()) {
|
||||||
|
notificationId.value = null
|
||||||
|
dismissed.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dismissed.value && !resummon) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let notif = getNotification()
|
||||||
|
const progressItems = buildDownloadItems()
|
||||||
|
|
||||||
|
if (notif) {
|
||||||
|
notif.title = formatMessage(messages.downloads)
|
||||||
|
notif.text = undefined
|
||||||
|
notif.progressItems = progressItems
|
||||||
|
notif.progress = undefined
|
||||||
|
notif.waiting = undefined
|
||||||
|
} else {
|
||||||
|
notif = popupNotificationManager.addPopupNotification({
|
||||||
|
title: formatMessage(messages.downloads),
|
||||||
|
type: 'download',
|
||||||
|
autoCloseMs: null,
|
||||||
|
progressItems,
|
||||||
|
})
|
||||||
|
notificationId.value = notif.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLoadingBars(loadingBar: LoadingBar): LoadingBar {
|
||||||
|
const formatted = { ...loadingBar }
|
||||||
|
if (formatted.bar_type?.type === 'java_download') {
|
||||||
|
formatted.title = formatMessage(messages.downloadingJava, {
|
||||||
|
version: formatted.bar_type.version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (formatted.bar_type?.profile_path) {
|
||||||
|
formatted.title = formatted.bar_type.profile_path
|
||||||
|
}
|
||||||
|
if (formatted.bar_type?.pack_name) {
|
||||||
|
formatted.title = formatted.bar_type.pack_name
|
||||||
|
}
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLoadingBars() {
|
||||||
|
const bars: Record<string, LoadingBar> = await progress_bars_list().catch((error) => {
|
||||||
|
handleError(error)
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
currentLoadingBars.value = Object.values(bars)
|
||||||
|
.map(formatLoadingBars)
|
||||||
|
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
|
||||||
|
|
||||||
|
const profilePaths = Array.from(
|
||||||
|
new Set(
|
||||||
|
currentLoadingBars.value
|
||||||
|
.map((bar) => bar.bar_type?.profile_path)
|
||||||
|
.filter((path): path is string => !!path),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const profiles = profilePaths.length
|
||||||
|
? await getInstances(profilePaths).catch((error) => {
|
||||||
|
handleError(error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
const profileIconUrls = new Map(
|
||||||
|
profiles.map((profile) => [profile.path, getDisplayIconUrl(profile.icon_path)]),
|
||||||
|
)
|
||||||
|
currentLoadingBarIconUrls.value = Object.fromEntries(
|
||||||
|
currentLoadingBars.value.map((bar) => {
|
||||||
|
const barIconUrl = getDisplayIconUrl(bar.bar_type?.icon)
|
||||||
|
const profileIconUrl = bar.bar_type?.profile_path
|
||||||
|
? profileIconUrls.get(bar.bar_type.profile_path)
|
||||||
|
: null
|
||||||
|
return [getLoadingBarKey(bar), barIconUrl ?? profileIconUrl ?? null]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
currentLoadingBars.value.sort((a, b) => {
|
||||||
|
const aKey = `${a.loading_bar_uuid ?? a.id ?? ''}`
|
||||||
|
const bKey = `${b.loading_bar_uuid ?? b.id ?? ''}`
|
||||||
|
return aKey.localeCompare(bKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
updateNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshLoadingBars()
|
||||||
|
|
||||||
|
const unlistenLoading = await loading_listener(async () => {
|
||||||
|
await refreshLoadingBars()
|
||||||
|
})
|
||||||
|
|
||||||
|
function openDownloadToast() {
|
||||||
|
updateNotification(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectProcess(process: RunningProcess) {
|
||||||
|
selectedProcess.value = process
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeNotification()
|
||||||
|
dismissed.value = false
|
||||||
|
window.removeEventListener('offline', handleOffline)
|
||||||
|
window.removeEventListener('online', handleOnline)
|
||||||
|
unlistenProcess()
|
||||||
|
unlistenLoading()
|
||||||
|
if (readyPillAnimationFrame !== null) {
|
||||||
|
cancelAnimationFrame(readyPillAnimationFrame)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.update-pill-ready-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-pill-ready-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,63 +1,158 @@
|
|||||||
<template>
|
<template>
|
||||||
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
|
<div
|
||||||
<Button v-if="false" class="breadcrumbs__back transparent" icon-only @click="$router.back()">
|
ref="outerRef"
|
||||||
<ChevronLeftIcon />
|
data-tauri-drag-region
|
||||||
</Button>
|
class="min-w-0 overflow-hidden pl-3"
|
||||||
<Button
|
:class="{ 'breadcrumb-fade-mask': isOverflowing }"
|
||||||
v-if="false"
|
:style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined"
|
||||||
class="breadcrumbs__forward transparent"
|
@mouseenter="onMouseEnter"
|
||||||
icon-only
|
@mouseleave="onMouseLeave"
|
||||||
@click="$router.forward()"
|
>
|
||||||
>
|
<div
|
||||||
<ChevronRightIcon />
|
ref="innerRef"
|
||||||
</Button>
|
data-tauri-drag-region
|
||||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
class="flex w-fit items-center gap-1"
|
||||||
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
:class="{ 'breadcrumbs-scroll': isAnimating }"
|
||||||
<router-link
|
@animationiteration="onAnimationIteration"
|
||||||
v-if="breadcrumb.link"
|
>
|
||||||
:to="{
|
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
|
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||||
query: breadcrumb.query,
|
<router-link
|
||||||
}"
|
v-if="breadcrumb.link"
|
||||||
class="text-primary"
|
:to="{
|
||||||
>{{
|
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
|
||||||
breadcrumb.name.charAt(0) === '?'
|
query: breadcrumb.query,
|
||||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
}"
|
||||||
: breadcrumb.name
|
class="shrink-0 whitespace-nowrap text-primary"
|
||||||
}}
|
>
|
||||||
</router-link>
|
{{ resolveLabel(breadcrumb.name) }}
|
||||||
<span
|
</router-link>
|
||||||
v-else
|
<span
|
||||||
data-tauri-drag-region
|
v-else
|
||||||
class="text-contrast font-semibold cursor-default select-none"
|
data-tauri-drag-region
|
||||||
>{{
|
class="shrink-0 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
|
||||||
breadcrumb.name.charAt(0) === '?'
|
>
|
||||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
{{ resolveLabel(breadcrumb.name) }}
|
||||||
: breadcrumb.name
|
</span>
|
||||||
}}</span
|
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5 shrink-0" />
|
||||||
>
|
</template>
|
||||||
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from '@modrinth/assets'
|
import { ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { computed } from 'vue'
|
|
||||||
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
|
|
||||||
|
interface Breadcrumb {
|
||||||
|
name: string
|
||||||
|
link?: string
|
||||||
|
query?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const breadcrumbData = useBreadcrumbs()
|
const breadcrumbData = useBreadcrumbs()
|
||||||
const breadcrumbs = computed(() => {
|
|
||||||
const additionalContext =
|
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
||||||
route.meta.useContext === true
|
const additionalContext =
|
||||||
? breadcrumbData.context
|
route.meta.useContext === true
|
||||||
: route.meta.useRootContext === true
|
? breadcrumbData.context
|
||||||
? breadcrumbData.rootContext
|
: route.meta.useRootContext === true
|
||||||
: null
|
? breadcrumbData.rootContext
|
||||||
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
: null
|
||||||
|
const crumbs = (route.meta.breadcrumb ?? []) as Breadcrumb[]
|
||||||
|
return additionalContext ? [additionalContext as Breadcrumb, ...crumbs] : crumbs
|
||||||
|
})
|
||||||
|
|
||||||
|
function resolveLabel(name: string): string {
|
||||||
|
return name.charAt(0) === '?' ? breadcrumbData.getName(name.slice(1)) : name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overflow detection
|
||||||
|
const outerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const innerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const isOverflowing = ref(false)
|
||||||
|
const isAnimating = ref(false)
|
||||||
|
const overflowAmount = ref(0)
|
||||||
|
|
||||||
|
let hovered = false
|
||||||
|
let stopping = false
|
||||||
|
|
||||||
|
function checkOverflow() {
|
||||||
|
if (!outerRef.value || !innerRef.value) return
|
||||||
|
const overflow = innerRef.value.scrollWidth - outerRef.value.clientWidth
|
||||||
|
isOverflowing.value = overflow > 0
|
||||||
|
overflowAmount.value = overflow + 12
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseEnter() {
|
||||||
|
hovered = true
|
||||||
|
stopping = false
|
||||||
|
if (isOverflowing.value) {
|
||||||
|
isAnimating.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseLeave() {
|
||||||
|
hovered = false
|
||||||
|
if (isAnimating.value) {
|
||||||
|
stopping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAnimationIteration() {
|
||||||
|
if (stopping && !hovered) {
|
||||||
|
isAnimating.value = false
|
||||||
|
stopping = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkOverflow()
|
||||||
|
resizeObserver = new ResizeObserver(checkOverflow)
|
||||||
|
if (outerRef.value) resizeObserver.observe(outerRef.value)
|
||||||
|
if (innerRef.value) resizeObserver.observe(innerRef.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(breadcrumbs, () => {
|
||||||
|
requestAnimationFrame(checkOverflow)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.breadcrumb-fade-mask {
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent,
|
||||||
|
black 12px,
|
||||||
|
black calc(100% - 12px),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs-scroll {
|
||||||
|
animation: breadcrumb-scroll 10s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes breadcrumb-scroll {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
35%,
|
||||||
|
65% {
|
||||||
|
transform: translateX(var(--scroll-distance));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-show="shown" ref="contextMenu" class="context-menu" :style="{
|
<div
|
||||||
left: left,
|
v-show="shown"
|
||||||
top: top,
|
ref="contextMenu"
|
||||||
}">
|
class="context-menu"
|
||||||
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
:style="{
|
||||||
<hr v-if="option.type === 'divider'" class="divider" />
|
left: left,
|
||||||
<div v-else-if="!(isLinkedData(item) && option.name === `add_content`)" class="item clickable"
|
top: top,
|
||||||
:class="[option.color ?? 'base']">
|
}"
|
||||||
<slot :name="option.name" />
|
>
|
||||||
</div>
|
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
||||||
</div>
|
<hr v-if="option.type === 'divider'" class="divider" />
|
||||||
</div>
|
<div
|
||||||
</transition>
|
v-else-if="!(isLinkedData(item) && option.name === `add_content`)"
|
||||||
|
class="item clickable"
|
||||||
|
:class="[option.color ?? 'base']"
|
||||||
|
>
|
||||||
|
<slot :name="option.name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
||||||
|
|
||||||
@@ -28,141 +36,146 @@ const top = ref('0px')
|
|||||||
const shown = ref(false)
|
const shown = ref(false)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
showMenu: (event, passedItem, passedOptions) => {
|
showMenu: (event, passedItem, passedOptions) => {
|
||||||
item.value = passedItem
|
item.value = passedItem
|
||||||
options.value = passedOptions
|
options.value = passedOptions
|
||||||
|
|
||||||
const menuWidth = contextMenu.value.clientWidth
|
// show to get dimensions
|
||||||
const menuHeight = contextMenu.value.clientHeight
|
shown.value = true
|
||||||
|
|
||||||
if (menuWidth + event.pageX >= window.innerWidth) {
|
// then, adjust position if overflowing
|
||||||
left.value = event.pageX - menuWidth + 2 + 'px'
|
nextTick(() => {
|
||||||
} else {
|
const menuWidth = contextMenu.value?.clientWidth || 200
|
||||||
left.value = event.pageX - 2 + 'px'
|
const menuHeight = contextMenu.value?.clientHeight || 100
|
||||||
}
|
const minFromEdge = 10
|
||||||
|
|
||||||
if (menuHeight + event.pageY >= window.innerHeight) {
|
if (event.pageX + menuWidth + minFromEdge >= window.innerWidth) {
|
||||||
top.value = event.pageY - menuHeight + 2 + 'px'
|
left.value = Math.max(minFromEdge, event.pageX - menuWidth - minFromEdge) + 'px'
|
||||||
} else {
|
} else {
|
||||||
top.value = event.pageY - 2 + 'px'
|
left.value = event.pageX + minFromEdge + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
shown.value = true
|
if (event.pageY + menuHeight + minFromEdge >= window.innerHeight) {
|
||||||
},
|
top.value = Math.max(minFromEdge, event.pageY - menuHeight - minFromEdge) + 'px'
|
||||||
|
} else {
|
||||||
|
top.value = event.pageY + minFromEdge + 'px'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLinkedData = (item) => {
|
const isLinkedData = (item) => {
|
||||||
if (item.instance != undefined && item.instance.linked_data) {
|
if (item.instance != undefined && item.instance.linked_data) {
|
||||||
return true
|
return true
|
||||||
} else if (item != undefined && item.linked_data) {
|
} else if (item != undefined && item.linked_data) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideContextMenu = () => {
|
const hideContextMenu = () => {
|
||||||
shown.value = false
|
shown.value = false
|
||||||
emit('menu-closed')
|
emit('menu-closed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionClicked = (option) => {
|
const optionClicked = (option) => {
|
||||||
emit('option-clicked', {
|
emit('option-clicked', {
|
||||||
item: item.value,
|
item: item.value,
|
||||||
option: option,
|
option: option,
|
||||||
})
|
})
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEscKeyRelease = (event) => {
|
const onEscKeyRelease = (event) => {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||||
if (
|
if (
|
||||||
contextMenu.value &&
|
contextMenu.value &&
|
||||||
contextMenu.value.$el !== event.target &&
|
contextMenu.value.$el !== event.target &&
|
||||||
!elements.includes(contextMenu.value.$el)
|
!elements.includes(contextMenu.value.$el)
|
||||||
) {
|
) {
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('click', handleClickOutside)
|
window.addEventListener('click', handleClickOutside)
|
||||||
document.body.addEventListener('keyup', onEscKeyRelease)
|
document.body.addEventListener('keyup', onEscKeyRelease)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('click', handleClickOutside)
|
window.removeEventListener('click', handleClickOutside)
|
||||||
document.removeEventListener('keyup', onEscKeyRelease)
|
document.removeEventListener('keyup', onEscKeyRelease)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.context-menu {
|
.context-menu {
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
box-shadow: var(--shadow-floating);
|
box-shadow: var(--shadow-floating);
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000000;
|
z-index: 1000000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: var(--gap-sm);
|
padding: var(--gap-sm);
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
padding: var(--gap-sm);
|
padding: var(--gap-sm);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
&.base {
|
&.base {
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: var(--color-brand);
|
background-color: var(--color-brand);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
background-color: var(--color-red);
|
background-color: var(--color-red);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.contrast {
|
&.contrast {
|
||||||
background-color: var(--color-orange);
|
background-color: var(--color-orange);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
margin: var(--gap-sm);
|
margin: var(--gap-sm);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
DropdownIcon,
|
CopyIcon,
|
||||||
XIcon,
|
DropdownIcon,
|
||||||
HammerIcon,
|
HammerIcon,
|
||||||
LogInIcon,
|
LogInIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
CopyIcon,
|
WrenchIcon,
|
||||||
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { ChatIcon } from '@/assets/icons'
|
import { ChatIcon } from '@/assets/icons'
|
||||||
import { ButtonStyled, Collapsible } from '@modrinth/ui'
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
|
||||||
import { install } from '@/helpers/profile.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||||
|
import { install } from '@/helpers/profile.js'
|
||||||
|
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const errorModal = ref()
|
const errorModal = ref()
|
||||||
const error = ref()
|
const error = ref()
|
||||||
@@ -26,115 +29,116 @@ const errorCollapsed = ref(false)
|
|||||||
|
|
||||||
const title = ref('An error occurred')
|
const title = ref('An error occurred')
|
||||||
const errorType = ref('unknown')
|
const errorType = ref('unknown')
|
||||||
const supportLink = ref('https://support.modrinth.com')
|
const supportLink = ref('https://astralium.su/product/astralrinth/support')
|
||||||
const metadata = ref({})
|
const metadata = ref({})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
async show(errorVal, context, canClose = true, source = null) {
|
async show(errorVal, context, canClose = true, source = null) {
|
||||||
closable.value = canClose
|
console.log(errorVal, context, canClose, source)
|
||||||
|
closable.value = canClose
|
||||||
|
|
||||||
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
||||||
title.value = 'Unable to sign in to Minecraft'
|
title.value = 'Unable to sign in to Minecraft'
|
||||||
errorType.value = 'minecraft_auth'
|
errorType.value = 'minecraft_auth'
|
||||||
supportLink.value =
|
supportLink.value =
|
||||||
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
|
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
|
||||||
|
|
||||||
if (
|
if (
|
||||||
errorVal.message.includes('existing connection was forcibly closed') ||
|
errorVal.message.includes('existing connection was forcibly closed') ||
|
||||||
errorVal.message.includes('error sending request for url')
|
errorVal.message.includes('error sending request for url')
|
||||||
) {
|
) {
|
||||||
metadata.value.network = true
|
metadata.value.network = true
|
||||||
}
|
}
|
||||||
if (errorVal.message.includes('because the target machine actively refused it')) {
|
if (errorVal.message.includes('because the target machine actively refused it')) {
|
||||||
metadata.value.hostsFile = true
|
metadata.value.hostsFile = true
|
||||||
}
|
}
|
||||||
} else if (errorVal.message && errorVal.message.includes('User is not logged in')) {
|
} else if (errorVal.message && errorVal.message.includes('User is not logged in')) {
|
||||||
title.value = 'Sign in to Minecraft'
|
title.value = 'Sign in to Minecraft'
|
||||||
errorType.value = 'minecraft_sign_in'
|
errorType.value = 'minecraft_sign_in'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
} else if (errorVal.message && errorVal.message.includes('Move directory error:')) {
|
} else if (errorVal.message && errorVal.message.includes('Move directory error:')) {
|
||||||
title.value = 'Could not change app directory'
|
title.value = 'Could not change app directory'
|
||||||
errorType.value = 'directory_move'
|
errorType.value = 'directory_move'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
|
|
||||||
if (errorVal.message.includes('directory is not writeable')) {
|
if (errorVal.message.includes('directory is not writable')) {
|
||||||
metadata.value.readOnly = true
|
metadata.value.readOnly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorVal.message.includes('Not enough space')) {
|
if (errorVal.message.includes('Not enough space')) {
|
||||||
metadata.value.notEnoughSpace = true
|
metadata.value.notEnoughSpace = true
|
||||||
}
|
}
|
||||||
} else if (errorVal.message && errorVal.message.includes('No loader version selected for')) {
|
} else if (errorVal.message && errorVal.message.includes('No loader version selected for')) {
|
||||||
title.value = 'No loader selected'
|
title.value = 'No loader selected'
|
||||||
errorType.value = 'no_loader_version'
|
errorType.value = 'no_loader_version'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
metadata.value.profilePath = context.profilePath
|
metadata.value.profilePath = context.profilePath
|
||||||
} else if (source === 'state_init') {
|
} else if (source === 'state_init') {
|
||||||
title.value = 'Error initializing Modrinth App'
|
title.value = 'Error initializing Modrinth App'
|
||||||
errorType.value = 'state_init'
|
errorType.value = 'state_init'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
} else {
|
} else {
|
||||||
title.value = 'An error occurred'
|
title.value = 'An error occurred'
|
||||||
errorType.value = 'unknown'
|
errorType.value = 'unknown'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
metadata.value = {}
|
metadata.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
error.value = errorVal
|
error.value = errorVal
|
||||||
errorModal.value.show()
|
errorModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadingMinecraft = ref(false)
|
const loadingMinecraft = ref(false)
|
||||||
async function loginMinecraft() {
|
async function loginMinecraft() {
|
||||||
try {
|
try {
|
||||||
loadingMinecraft.value = true
|
loadingMinecraft.value = true
|
||||||
const loggedIn = await login_flow()
|
const loggedIn = await login_flow()
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
await set_default_user(loggedIn.profile.id).catch(handleError)
|
await set_default_user(loggedIn.profile.id).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
||||||
loadingMinecraft.value = false
|
loadingMinecraft.value = false
|
||||||
errorModal.value.hide()
|
errorModal.value.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loadingMinecraft.value = false
|
loadingMinecraft.value = false
|
||||||
handleSevereError(err)
|
handleSevereError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelDirectoryChange() {
|
async function cancelDirectoryChange() {
|
||||||
try {
|
try {
|
||||||
await cancel_directory_change()
|
await cancel_directory_change()
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retryDirectoryChange() {
|
function retryDirectoryChange() {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadingRepair = ref(false)
|
const loadingRepair = ref(false)
|
||||||
async function repairInstance() {
|
async function repairInstance() {
|
||||||
loadingRepair.value = true
|
loadingRepair.value = true
|
||||||
try {
|
try {
|
||||||
await install(metadata.value.profilePath, false)
|
await install(metadata.value.profilePath, false)
|
||||||
errorModal.value.hide()
|
errorModal.value.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleSevereError(err)
|
handleSevereError(err)
|
||||||
}
|
}
|
||||||
loadingRepair.value = false
|
loadingRepair.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasDebugInfo = computed(
|
const hasDebugInfo = computed(
|
||||||
() =>
|
() =>
|
||||||
errorType.value === 'directory_move' ||
|
errorType.value === 'directory_move' ||
|
||||||
errorType.value === 'minecraft_auth' ||
|
errorType.value === 'minecraft_auth' ||
|
||||||
errorType.value === 'state_init' ||
|
errorType.value === 'state_init' ||
|
||||||
errorType.value === 'no_loader_version',
|
errorType.value === 'no_loader_version',
|
||||||
)
|
)
|
||||||
|
|
||||||
const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error message.')
|
const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error message.')
|
||||||
@@ -142,236 +146,255 @@ const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error
|
|||||||
const copied = ref(false)
|
const copied = ref(false)
|
||||||
|
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
copied.value = true
|
copied.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copied.value = false
|
copied.value = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
|
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
|
||||||
<div class="modal-body">
|
<div class="modal-body max-w-[550px]">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
<template v-if="errorType === 'minecraft_auth'">
|
<template v-if="errorType === 'minecraft_auth'">
|
||||||
<template v-if="metadata.network">
|
<template v-if="metadata.network">
|
||||||
<h3>Network issues</h3>
|
<h3>Network issues</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like there were issues with the Modrinth App connecting to Microsoft's
|
It looks like there were issues with the Modrinth App connecting to Microsoft's
|
||||||
servers. This is often the result of a poor connection, so we recommend trying again
|
servers. This is often the result of a poor connection, so we recommend trying again
|
||||||
to see if it works. If issues continue to persist, follow the steps in
|
to see if it works. If issues continue to persist, follow the steps in
|
||||||
<a
|
<a
|
||||||
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
|
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
|
||||||
>
|
>
|
||||||
our support article
|
our support article
|
||||||
</a>
|
</a>
|
||||||
to troubleshoot.
|
to troubleshoot.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="metadata.hostsFile">
|
<template v-else-if="metadata.hostsFile">
|
||||||
<h3>Network issues</h3>
|
<h3>Network issues</h3>
|
||||||
<p>
|
<p>
|
||||||
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
|
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
|
||||||
remote server rejected the connection. This may indicate that these services are
|
remote server rejected the connection. This may indicate that these services are
|
||||||
blocked by the hosts file. Please visit
|
blocked by the hosts file. Please visit
|
||||||
<a
|
<a
|
||||||
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
|
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
|
||||||
>
|
>
|
||||||
our support article
|
our support article
|
||||||
</a>
|
</a>
|
||||||
for steps on how to fix the issue.
|
for steps on how to fix the issue.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h3>Try another Microsoft account</h3>
|
<h3>Try another Microsoft account</h3>
|
||||||
<p>
|
<p>
|
||||||
Double check you've signed in with the right account. You may own Minecraft on a
|
Double check you've signed in with the right account. You may own Minecraft on a
|
||||||
different Microsoft account.
|
different Microsoft account.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Try another account
|
<LogInIcon /> Try another account
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
|
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
|
||||||
<p>
|
<p>
|
||||||
Try signing in with the
|
Try signing in with the
|
||||||
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
|
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
|
||||||
first. Once you're done, come back here and sign in!
|
first. Once you're done, come back here and sign in!
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Try signing in again
|
<LogInIcon /> Try signing in again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="errorType === 'directory_move'">
|
<template v-if="errorType === 'directory_move'">
|
||||||
<template v-if="metadata.readOnly">
|
<template v-if="metadata.readOnly">
|
||||||
<h3>Change directory permissions</h3>
|
<h3>Change directory permissions</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like the Modrinth App is unable to write to the directory you selected.
|
It looks like the Modrinth App is unable to write to the directory you selected.
|
||||||
Please adjust the permissions of the directory and try again or cancel the directory
|
Please adjust the permissions of the directory and try again or cancel the directory
|
||||||
change.
|
change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="metadata.notEnoughSpace">
|
<template v-else-if="metadata.notEnoughSpace">
|
||||||
<h3>Not enough space</h3>
|
<h3>Not enough space</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like there is not enough space on the disk containing the directory you
|
It looks like there is not enough space on the disk containing the directory you
|
||||||
selected. Please free up some space and try again or cancel the directory change.
|
selected. Please free up some space and try again or cancel the directory change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p>
|
<p>
|
||||||
The Modrinth App is unable to migrate to the new directory you selected. Please
|
The Modrinth App is unable to migrate to the new directory you selected. Please
|
||||||
contact support for help or cancel the directory change.
|
contact support for help or cancel the directory change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn" @click="retryDirectoryChange">
|
<button class="btn" @click="retryDirectoryChange">
|
||||||
<UpdatedIcon /> Retry directory change
|
<UpdatedIcon /> Retry directory change
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" @click="cancelDirectoryChange">
|
<button class="btn btn-danger" @click="cancelDirectoryChange">
|
||||||
<XIcon /> Cancel directory change
|
<XIcon /> Cancel directory change
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="errorType === 'minecraft_sign_in'">
|
<div v-else-if="errorType === 'minecraft_sign_in'">
|
||||||
<p>
|
<p>
|
||||||
To play this instance, you must sign in through Microsoft below. If you don't have a
|
To play this instance, you must sign in through Microsoft below. If you don't have a
|
||||||
Minecraft account, you can purchase the game on the
|
Minecraft account, you can purchase the game on the
|
||||||
<a href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
|
<a href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
|
||||||
>Minecraft website</a
|
>Minecraft website</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Sign in to Minecraft
|
<LogInIcon /> Sign in to Minecraft
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-else-if="errorType === 'state_init'">
|
<template v-else-if="errorType === 'state_init'">
|
||||||
<p>
|
<p>
|
||||||
Modrinth App failed to load correctly. This may be because of a corrupted file, or
|
Modrinth App failed to load correctly. This may be because of a corrupted file, or
|
||||||
because the app is missing crucial files.
|
because the app is missing crucial files.
|
||||||
</p>
|
</p>
|
||||||
<p>You may be able to fix it through one of the following ways:</p>
|
<p>You may be able to fix it through one of the following ways:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
|
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
|
||||||
<li>Redownloading the app.</li>
|
<li>Redownloading the app.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="errorType === 'no_loader_version'">
|
<template v-else-if="errorType === 'no_loader_version'">
|
||||||
<p>The Modrinth App failed to find the loader version for this instance.</p>
|
<p>The Modrinth App failed to find the loader version for this instance.</p>
|
||||||
<p>To resolve this, you need to repair the instance. Click the button below to do so.</p>
|
<p>To resolve this, you need to repair the instance. Click the button below to do so.</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingRepair" @click="repairInstance">
|
<button class="btn btn-primary" :disabled="loadingRepair" @click="repairInstance">
|
||||||
<HammerIcon /> Repair instance
|
<HammerIcon /> Repair instance
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ debugInfo }}
|
{{ debugInfo }}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="hasDebugInfo">
|
<template v-if="hasDebugInfo">
|
||||||
<hr />
|
<div class="w-full h-[1px] bg-surface-5 mb-3"></div>
|
||||||
<p>
|
<p>
|
||||||
If nothing is working and you need help, visit
|
If nothing is working and you need help, visit
|
||||||
<a :href="supportLink">our support page</a>
|
<a :href="supportLink">our support page</a>
|
||||||
and start a chat using the widget in the bottom right and we will be more than happy to
|
and start a chat using the widget in the bottom right and we will be more than happy to
|
||||||
assist! Make sure to provide the following debug information to the agent:
|
assist! Make sure to provide the following debug information to the agent:
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="closable">
|
<ButtonStyled v-if="closable">
|
||||||
<button @click="errorModal.hide()"><XIcon /> Close</button>
|
<button @click="errorModal.hide()"><XIcon /> Close</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="hasDebugInfo">
|
</div>
|
||||||
<button :disabled="copied" @click="copyToClipboard(debugInfo)">
|
<template v-if="hasDebugInfo">
|
||||||
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
|
<div class="flex flex-col gap-2">
|
||||||
<template v-else> <CopyIcon /> Copy debug info </template>
|
<div class="w-full h-[1px] bg-surface-5"></div>
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
<div class="overflow-clip">
|
||||||
</div>
|
<button
|
||||||
<template v-if="hasDebugInfo">
|
class="flex items-center justify-between w-full bg-transparent border-0 py-4 cursor-pointer"
|
||||||
<div class="bg-button-bg rounded-xl mt-2 overflow-clip">
|
@click="errorCollapsed = !errorCollapsed"
|
||||||
<button
|
>
|
||||||
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
|
<span class="flex items-center gap-2 text-contrast font-extrabold m-0">
|
||||||
@click="errorCollapsed = !errorCollapsed"
|
<WrenchIcon class="h-4 w-4" />
|
||||||
>
|
Debug information
|
||||||
<span class="text-contrast font-extrabold m-0">Debug information:</span>
|
</span>
|
||||||
<DropdownIcon
|
<DropdownIcon
|
||||||
class="h-5 w-5 text-secondary transition-transform"
|
class="h-5 w-5 text-secondary transition-transform"
|
||||||
:class="{ 'rotate-180': !errorCollapsed }"
|
:class="{ 'rotate-180': !errorCollapsed }"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<Collapsible :collapsed="errorCollapsed">
|
<Collapsible :collapsed="errorCollapsed">
|
||||||
<pre class="m-0 px-4 py-3 bg-bg rounded-none">{{ debugInfo }}</pre>
|
<div
|
||||||
</Collapsible>
|
class="p-3 bg-surface-2 rounded-2xl text-xs grid grid-cols-[1fr_auto] max-w-full items-start"
|
||||||
</div>
|
>
|
||||||
</template>
|
<div
|
||||||
</div>
|
class="m-0 p-0 rounded-none bg-transparent text-sm font-mono break-words overflow-auto"
|
||||||
</ModalWrapper>
|
>
|
||||||
|
{{ debugInfo }}
|
||||||
|
</div>
|
||||||
|
<ButtonStyled circular>
|
||||||
|
<button
|
||||||
|
v-tooltip="'Copy debug info'"
|
||||||
|
:disabled="copied"
|
||||||
|
@click="copyToClipboard(debugInfo)"
|
||||||
|
>
|
||||||
|
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||||
|
<template v-else> <CopyIcon /> </template>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.light-mode {
|
.light-mode {
|
||||||
--color-orange-bg: rgba(255, 163, 71, 0.2);
|
--color-orange-bg: rgba(255, 163, 71, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode,
|
.dark-mode,
|
||||||
.oled-mode {
|
.oled-mode {
|
||||||
--color-orange-bg: rgba(224, 131, 37, 0.2);
|
--color-orange-bg: rgba(224, 131, 37, 0.2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.cta-button {
|
.cta-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-banner {
|
.warning-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: var(--gap-lg);
|
padding: var(--gap-lg);
|
||||||
background-color: var(--color-orange-bg);
|
background-color: var(--color-orange-bg);
|
||||||
border: 2px solid var(--color-orange);
|
border: 2px solid var(--color-orange);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-banner__title {
|
.warning-banner__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--color-orange);
|
color: var(--color-orange);
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,25 +1,67 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { XIcon, PlusIcon } from '@modrinth/assets'
|
import { WrenchIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button, Checkbox } from '@modrinth/ui'
|
import {
|
||||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
Accordion,
|
||||||
|
ButtonStyled,
|
||||||
|
Checkbox,
|
||||||
|
commonMessages,
|
||||||
|
defineMessages,
|
||||||
|
injectNotificationManager,
|
||||||
|
StyledInput,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { save } from '@tauri-apps/plugin-dialog'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
header: { id: 'app.export-modal.header', defaultMessage: 'Export modpack' },
|
||||||
|
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack Name' },
|
||||||
|
modpackNamePlaceholder: {
|
||||||
|
id: 'app.export-modal.modpack-name-placeholder',
|
||||||
|
defaultMessage: 'Modpack name',
|
||||||
|
},
|
||||||
|
versionNumberLabel: {
|
||||||
|
id: 'app.export-modal.version-number-label',
|
||||||
|
defaultMessage: 'Version number',
|
||||||
|
},
|
||||||
|
versionNumberPlaceholder: {
|
||||||
|
id: 'app.export-modal.version-number-placeholder',
|
||||||
|
defaultMessage: '1.0.0',
|
||||||
|
},
|
||||||
|
descriptionPlaceholder: {
|
||||||
|
id: 'app.export-modal.description-placeholder',
|
||||||
|
defaultMessage: 'Enter modpack description...',
|
||||||
|
},
|
||||||
|
selectFilesLabel: {
|
||||||
|
id: 'app.export-modal.select-files-label',
|
||||||
|
defaultMessage: 'Configure which files are included in this export',
|
||||||
|
},
|
||||||
|
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
|
||||||
|
includeFile: {
|
||||||
|
id: 'app.export-modal.include-file-accessibility-label',
|
||||||
|
defaultMessage: 'Include "{file}"?',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
exportModal.value.show()
|
exportModal.value.show()
|
||||||
initFiles()
|
initFiles()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const exportModal = ref(null)
|
const exportModal = ref(null)
|
||||||
@@ -28,276 +70,185 @@ const exportDescription = ref('')
|
|||||||
const versionInput = ref('1.0.0')
|
const versionInput = ref('1.0.0')
|
||||||
const files = ref([])
|
const files = ref([])
|
||||||
const folders = ref([])
|
const folders = ref([])
|
||||||
const showingFiles = ref(false)
|
|
||||||
|
|
||||||
const initFiles = async () => {
|
const initFiles = async () => {
|
||||||
const newFolders = new Map()
|
const newFolders = new Map()
|
||||||
const sep = '/'
|
const sep = '/'
|
||||||
files.value = []
|
files.value = []
|
||||||
await get_pack_export_candidates(props.instance.path).then((filePaths) =>
|
await get_pack_export_candidates(props.instance.path).then((filePaths) =>
|
||||||
filePaths
|
filePaths
|
||||||
.map((folder) => ({
|
.map((folder) => ({
|
||||||
path: folder,
|
path: folder,
|
||||||
name: folder.split(sep).pop(),
|
name: folder.split(sep).pop(),
|
||||||
selected:
|
selected:
|
||||||
folder.startsWith('mods') ||
|
folder.startsWith('mods') ||
|
||||||
folder.startsWith('datapacks') ||
|
folder.startsWith('datapacks') ||
|
||||||
folder.startsWith('resourcepacks') ||
|
folder.startsWith('resourcepacks') ||
|
||||||
folder.startsWith('shaderpacks') ||
|
folder.startsWith('shaderpacks') ||
|
||||||
folder.startsWith('config'),
|
folder.startsWith('config'),
|
||||||
disabled:
|
disabled:
|
||||||
folder === 'profile.json' ||
|
folder === 'profile.json' ||
|
||||||
folder.startsWith('modrinth_logs') ||
|
folder.startsWith('modrinth_logs') ||
|
||||||
folder.startsWith('.fabric'),
|
folder.startsWith('.fabric') ||
|
||||||
}))
|
folder.startsWith('__MACOSX'),
|
||||||
.filter((pathData) => !pathData.path.includes('.DS_Store'))
|
}))
|
||||||
.forEach((pathData) => {
|
.forEach((pathData) => {
|
||||||
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
||||||
if (parent !== '') {
|
if (parent !== '') {
|
||||||
if (newFolders.has(parent)) {
|
if (newFolders.has(parent)) {
|
||||||
newFolders.get(parent).push(pathData)
|
newFolders.get(parent).push(pathData)
|
||||||
} else {
|
} else {
|
||||||
newFolders.set(parent, [pathData])
|
newFolders.set(parent, [pathData])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
files.value.push(pathData)
|
files.value.push(pathData)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
showingMore: false,
|
showingMore: false,
|
||||||
},
|
},
|
||||||
value,
|
value,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
await initFiles()
|
await initFiles()
|
||||||
|
|
||||||
const exportPack = async () => {
|
const exportPack = async () => {
|
||||||
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
||||||
folders.value.forEach((args) => {
|
folders.value.forEach((args) => {
|
||||||
args[1].forEach((child) => {
|
args[1].forEach((child) => {
|
||||||
if (child.selected) {
|
if (child.selected) {
|
||||||
filesToExport.push(child.path)
|
filesToExport.push(child.path)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const outputPath = await open({
|
const outputPath = await save({
|
||||||
directory: true,
|
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
|
||||||
multiple: false,
|
filters: [
|
||||||
})
|
{
|
||||||
|
name: 'Modrinth Modpack',
|
||||||
|
extensions: ['mrpack'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
if (outputPath) {
|
if (outputPath) {
|
||||||
export_profile_mrpack(
|
export_profile_mrpack(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
outputPath,
|
||||||
filesToExport,
|
filesToExport,
|
||||||
versionInput.value,
|
versionInput.value,
|
||||||
exportDescription.value,
|
exportDescription.value,
|
||||||
nameInput.value,
|
nameInput.value,
|
||||||
).catch((err) => handleError(err))
|
).catch((err) => handleError(err))
|
||||||
exportModal.value.hide()
|
exportModal.value.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="exportModal" header="Export modpack">
|
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
|
||||||
<div class="modal-body">
|
<div class="flex flex-col gap-4 w-[40rem]">
|
||||||
<div class="labeled_input">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<p>Modpack Name</p>
|
<div class="labeled_input">
|
||||||
<div class="iconified-input">
|
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||||
<PackageIcon />
|
<StyledInput
|
||||||
<input v-model="nameInput" type="text" placeholder="Modpack name" class="input" />
|
v-model="nameInput"
|
||||||
<Button class="r-btn" @click="nameInput = ''">
|
:icon="PackageIcon"
|
||||||
<XIcon />
|
type="text"
|
||||||
</Button>
|
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
|
||||||
</div>
|
clearable
|
||||||
</div>
|
/>
|
||||||
<div class="labeled_input">
|
</div>
|
||||||
<p>Version number</p>
|
<div class="labeled_input">
|
||||||
<div class="iconified-input">
|
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
|
||||||
<VersionIcon />
|
<StyledInput
|
||||||
<input v-model="versionInput" type="text" placeholder="1.0.0" class="input" />
|
v-model="versionInput"
|
||||||
<Button class="r-btn" @click="versionInput = ''">
|
:icon="VersionIcon"
|
||||||
<XIcon />
|
type="text"
|
||||||
</Button>
|
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
|
||||||
</div>
|
clearable
|
||||||
</div>
|
/>
|
||||||
<div class="adjacent-input">
|
</div>
|
||||||
<div class="labeled_input">
|
</div>
|
||||||
<p>Description</p>
|
<div class="flex flex-col gap-2">
|
||||||
|
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
|
||||||
<div class="textarea-wrapper">
|
<StyledInput
|
||||||
<textarea v-model="exportDescription" placeholder="Enter modpack description..." />
|
v-model="exportDescription"
|
||||||
</div>
|
multiline
|
||||||
</div>
|
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
<div class="table">
|
<Accordion
|
||||||
<div class="table-head">
|
class="w-full bg-surface-4 border border-solid border-surface-5 rounded-2xl overflow-clip"
|
||||||
<div class="table-cell row-wise">
|
button-class="p-4 w-full border-b border-solid border-b-surface-5 bg-surface-2 -mb-px hover:brightness-[--hover-brightness] group"
|
||||||
Select files and folders to include in pack
|
>
|
||||||
<Button
|
<template #title>
|
||||||
class="sleek-primary collapsed-button"
|
<span class="flex items-center gap-3 text-contrast group-active:scale-[0.98]">
|
||||||
icon-only
|
<WrenchIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||||
@click="() => (showingFiles = !showingFiles)"
|
Configure which files are included in this export
|
||||||
>
|
</span>
|
||||||
<PlusIcon v-if="!showingFiles" />
|
</template>
|
||||||
<XIcon v-else />
|
<div class="flex flex-col [&>*:nth-child(even)]:bg-surface-3">
|
||||||
</Button>
|
<div v-for="[path, children] in folders" :key="path.name" class="flex flex-col">
|
||||||
</div>
|
<Accordion
|
||||||
</div>
|
class="flex flex-col"
|
||||||
<div v-if="showingFiles" class="table-content">
|
button-class="flex gap-3 pr-4 hover:bg-surface-5 group"
|
||||||
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
|
>
|
||||||
<div class="table-cell file-entry">
|
<template #title>
|
||||||
<div class="file-primary">
|
<Checkbox
|
||||||
<Checkbox
|
:model-value="children.every((child) => child.selected)"
|
||||||
:model-value="children.every((child) => child.selected)"
|
:indeterminate="
|
||||||
:label="path.name"
|
!children.every((child) => child.selected) &&
|
||||||
class="select-checkbox"
|
children.some((child) => child.selected)
|
||||||
:disabled="children.every((x) => x.disabled)"
|
"
|
||||||
@update:model-value="
|
:description="formatMessage(messages.includeFile, { file: path.name })"
|
||||||
(newValue) => children.forEach((child) => (child.selected = newValue))
|
class="pl-4 py-2"
|
||||||
"
|
:disabled="children.every((x) => x.disabled)"
|
||||||
/>
|
@update:model-value="
|
||||||
<Checkbox
|
(newValue) => children.forEach((child) => (child.selected = newValue))
|
||||||
v-model="path.showingMore"
|
"
|
||||||
class="select-checkbox dropdown"
|
@click.stop
|
||||||
collapsing-toggle-style
|
/>
|
||||||
/>
|
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
|
||||||
</div>
|
</template>
|
||||||
<div v-if="path.showingMore" class="file-secondary">
|
<div v-for="child in children" :key="child.path">
|
||||||
<div v-for="child in children" :key="child.path" class="file-secondary-row">
|
<Checkbox
|
||||||
<Checkbox
|
v-model="child.selected"
|
||||||
v-model="child.selected"
|
:label="child.name"
|
||||||
:label="child.name"
|
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
|
||||||
class="select-checkbox"
|
:disabled="child.disabled"
|
||||||
:disabled="child.disabled"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Checkbox
|
||||||
</div>
|
v-for="file in files"
|
||||||
<div v-for="file in files" :key="file.path" class="table-row">
|
:key="file.path"
|
||||||
<div class="table-cell file-entry">
|
v-model="file.selected"
|
||||||
<div class="file-primary">
|
:label="file.name"
|
||||||
<Checkbox
|
:disabled="file.disabled"
|
||||||
v-model="file.selected"
|
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
|
||||||
:label="file.name"
|
/>
|
||||||
:disabled="file.disabled"
|
</div>
|
||||||
class="select-checkbox"
|
</Accordion>
|
||||||
/>
|
<div class="flex items-center justify-end gap-2">
|
||||||
</div>
|
<ButtonStyled type="outlined">
|
||||||
</div>
|
<button @click="exportModal.hide">
|
||||||
</div>
|
<XIcon />
|
||||||
</div>
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
</div>
|
</button>
|
||||||
<div class="button-row push-right">
|
</ButtonStyled>
|
||||||
<Button @click="exportModal.hide">
|
<ButtonStyled color="brand">
|
||||||
<XIcon />
|
<button @click="exportPack">
|
||||||
Cancel
|
<PackageIcon />
|
||||||
</Button>
|
{{ formatMessage(messages.exportButton) }}
|
||||||
<Button color="primary" @click="exportPack">
|
</button>
|
||||||
<PackageIcon />
|
</ButtonStyled>
|
||||||
Export
|
</div>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</ModalWrapper>
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.labeled_input {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-checkbox {
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
|
|
||||||
button.checkbox {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dropdown {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-content {
|
|
||||||
max-height: 18rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
border: 1px solid var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-entry {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-primary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-secondary {
|
|
||||||
margin-left: var(--gap-xl);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
height: 100%;
|
|
||||||
vertical-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-secondary-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-wise {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea-wrapper {
|
|
||||||
// margin-top: 1rem;
|
|
||||||
height: 12rem;
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
max-height: 12rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,51 +1,52 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
DownloadIcon,
|
||||||
|
GameIcon,
|
||||||
|
PlayIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TimerIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
|
||||||
DownloadIcon,
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
GameIcon,
|
|
||||||
PlayIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
TimerIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { finish_install, kill, run } from '@/helpers/profile'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { handleError } from '@/store/state.js'
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { finish_install, kill, run } from '@/helpers/profile'
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
first: {
|
first: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const modLoading = computed(
|
const modLoading = computed(
|
||||||
() =>
|
() =>
|
||||||
loading.value ||
|
loading.value ||
|
||||||
currentEvent.value === 'installing' ||
|
currentEvent.value === 'installing' ||
|
||||||
(currentEvent.value === 'launched' && !playing.value),
|
(currentEvent.value === 'launched' && !playing.value),
|
||||||
)
|
)
|
||||||
const installing = computed(() => props.instance.install_stage.includes('installing'))
|
const installing = computed(() => props.instance.install_stage.includes('installing'))
|
||||||
const installed = computed(() => props.instance.install_stage === 'installed')
|
const installed = computed(() => props.instance.install_stage === 'installed')
|
||||||
@@ -53,78 +54,78 @@ const installed = computed(() => props.instance.install_stage === 'installed')
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const seeInstance = async () => {
|
const seeInstance = async () => {
|
||||||
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
|
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkProcess = async () => {
|
const checkProcess = async () => {
|
||||||
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
||||||
|
|
||||||
playing.value = runningProcesses.length > 0
|
playing.value = runningProcesses.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const play = async (e, context) => {
|
const play = async (e, context) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await run(props.instance.path)
|
await run(props.instance.path)
|
||||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
trackEvent('InstancePlay', {
|
trackEvent('InstanceStart', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: context,
|
source: context,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const stop = async (e, context) => {
|
const stop = async (e, context) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
playing.value = false
|
playing.value = false
|
||||||
|
|
||||||
await kill(props.instance.path).catch(handleError)
|
await kill(props.instance.path).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: context,
|
source: context,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const repair = async (e) => {
|
const repair = async (e) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
|
|
||||||
await finish_install(props.instance)
|
await finish_install(props.instance).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openFolder = async () => {
|
const openFolder = async () => {
|
||||||
await showProfileInFolder(props.instance.path)
|
await showProfileInFolder(props.instance.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addContent = async () => {
|
const addContent = async () => {
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||||
query: { i: props.instance.path },
|
query: { i: props.instance.path },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
play,
|
play,
|
||||||
stop,
|
stop,
|
||||||
seeInstance,
|
seeInstance,
|
||||||
openFolder,
|
openFolder,
|
||||||
addContent,
|
addContent,
|
||||||
instance: props.instance,
|
instance: props.instance,
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentEvent = ref(null)
|
const currentEvent = ref(null)
|
||||||
|
|
||||||
const unlisten = await process_listener((e) => {
|
const unlisten = await process_listener((e) => {
|
||||||
if (e.profile_path_id === props.instance.path) {
|
if (e.profile_path_id === props.instance.path) {
|
||||||
currentEvent.value = e.event
|
currentEvent.value = e.event
|
||||||
if (e.event === 'finished') {
|
if (e.event === 'finished') {
|
||||||
playing.value = false
|
playing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => checkProcess())
|
onMounted(() => checkProcess())
|
||||||
@@ -132,118 +133,118 @@ onUnmounted(() => unlisten())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-if="compact">
|
<template v-if="compact">
|
||||||
<div
|
<div
|
||||||
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
|
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
|
||||||
@click="seeInstance"
|
@click="seeInstance"
|
||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="48px"
|
size="48px"
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
alt="Mod card"
|
alt="Mod card"
|
||||||
/>
|
/>
|
||||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||||
<span class="line-clamp-2">{{ instance.name }}</span>
|
<span class="line-clamp-2">{{ instance.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
||||||
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
||||||
<button v-tooltip="'Instance is loading...'" disabled>
|
<button v-tooltip="'Instance is loading...'" disabled>
|
||||||
<SpinnerIcon class="animate-spin" />
|
<SpinnerIcon class="animate-spin" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
|
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Play'"
|
v-tooltip="'Play'"
|
||||||
@click="(e) => play(e, 'InstanceCard')"
|
@click="(e) => play(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<!-- Translate for optical centering -->
|
<!-- Translate for optical centering -->
|
||||||
<PlayIcon class="translate-x-[1px]" />
|
<PlayIcon class="translate-x-[1px]" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||||
<TimerIcon />
|
<TimerIcon />
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
<template v-if="instance.last_played">
|
<template v-if="instance.last_played">
|
||||||
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else> Never played </template>
|
<template v-else> Never played </template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
||||||
@click="seeInstance"
|
@click="seeInstance"
|
||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center">
|
<div class="relative flex items-center justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="48px"
|
size="48px"
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
alt="Mod card"
|
alt="Mod card"
|
||||||
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||||
/>
|
/>
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Stop'"
|
v-tooltip="'Stop'"
|
||||||
:class="{ 'scale-100 opacity-100': playing }"
|
:class="{ 'scale-100 opacity-100': playing }"
|
||||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||||
@click="(e) => stop(e, 'InstanceCard')"
|
@click="(e) => stop(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<SpinnerIcon
|
<SpinnerIcon
|
||||||
v-else-if="modLoading || installing"
|
v-else-if="modLoading || installing"
|
||||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||||
class="animate-spin w-8 h-8"
|
class="animate-spin w-8 h-8"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
|
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Repair'"
|
v-tooltip="'Repair'"
|
||||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||||
@click="(e) => repair(e)"
|
@click="(e) => repair(e)"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else size="large" color="brand" circular>
|
<ButtonStyled v-else size="large" color="brand" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Play'"
|
v-tooltip="'Play'"
|
||||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||||
@click="(e) => play(e, 'InstanceCard')"
|
@click="(e) => play(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<PlayIcon class="translate-x-[2px]" />
|
<PlayIcon class="translate-x-[2px]" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
|
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
||||||
<GameIcon class="shrink-0" />
|
<GameIcon class="shrink-0" />
|
||||||
<span class="text-sm capitalize">
|
<span class="text-sm capitalize">
|
||||||
{{ instance.loader }} {{ instance.game_version }}
|
{{ instance.loader }} {{ instance.game_version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,674 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ModalWrapper ref="modal" header="Creating an instance">
|
|
||||||
<div class="modal-header">
|
|
||||||
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
|
|
||||||
</div>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<div v-if="creationType === 'custom'" class="modal-body">
|
|
||||||
<div class="image-upload">
|
|
||||||
<Avatar :src="display_icon" size="md" :rounded="true" />
|
|
||||||
<div class="image-input">
|
|
||||||
<Button @click="upload_icon()">
|
|
||||||
<UploadIcon />
|
|
||||||
Select icon
|
|
||||||
</Button>
|
|
||||||
<Button :disabled="!display_icon" @click="reset_icon">
|
|
||||||
<XIcon />
|
|
||||||
Remove icon
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-row">
|
|
||||||
<p class="input-label">Name</p>
|
|
||||||
<input
|
|
||||||
v-model="profile_name"
|
|
||||||
autocomplete="off"
|
|
||||||
class="text-input"
|
|
||||||
type="text"
|
|
||||||
maxlength="100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="input-row">
|
|
||||||
<p class="input-label">Loader</p>
|
|
||||||
<Chips v-model="loader" :items="loaders" />
|
|
||||||
</div>
|
|
||||||
<div class="input-row">
|
|
||||||
<p class="input-label">Game version</p>
|
|
||||||
<div class="versions">
|
|
||||||
<multiselect
|
|
||||||
v-model="game_version"
|
|
||||||
class="selector"
|
|
||||||
:options="game_versions"
|
|
||||||
:multiple="false"
|
|
||||||
:searchable="true"
|
|
||||||
placeholder="Select game version"
|
|
||||||
open-direction="top"
|
|
||||||
:show-labels="false"
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
v-if="showAdvanced"
|
|
||||||
v-model="showSnapshots"
|
|
||||||
class="filter-checkbox"
|
|
||||||
label="Include snapshots"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showAdvanced && loader !== 'vanilla'" class="input-row">
|
|
||||||
<p class="input-label">Loader version</p>
|
|
||||||
<Chips v-model="loader_version" :items="['stable', 'latest', 'other']" />
|
|
||||||
</div>
|
|
||||||
<div v-if="showAdvanced && loader_version === 'other' && loader !== 'vanilla'">
|
|
||||||
<div v-if="game_version" class="input-row">
|
|
||||||
<p class="input-label">Select version</p>
|
|
||||||
<multiselect
|
|
||||||
v-model="specified_loader_version"
|
|
||||||
class="selector"
|
|
||||||
:options="selectable_versions"
|
|
||||||
:searchable="true"
|
|
||||||
placeholder="Select loader version"
|
|
||||||
open-direction="top"
|
|
||||||
:show-labels="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else class="input-row">
|
|
||||||
<p class="warning">Select a game version before you select a loader version</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-group push-right">
|
|
||||||
<Button @click="toggle_advanced">
|
|
||||||
<CodeIcon />
|
|
||||||
{{ showAdvanced ? 'Hide advanced' : 'Show advanced' }}
|
|
||||||
</Button>
|
|
||||||
<Button @click="hide()">
|
|
||||||
<XIcon />
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" :disabled="!check_valid || creating" @click="create_instance()">
|
|
||||||
<PlusIcon v-if="!creating" />
|
|
||||||
{{ creating ? 'Creating...' : 'Create' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="creationType === 'from file'" class="modal-body">
|
|
||||||
<Button @click="openFile"> <FolderOpenIcon /> Import from file </Button>
|
|
||||||
<div class="info"><InfoIcon /> Or drag and drop your .mrpack file</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="modal-body">
|
|
||||||
<Chips
|
|
||||||
v-model="selectedProfileType"
|
|
||||||
:items="profileOptions"
|
|
||||||
:format-label="(profile) => profile?.name"
|
|
||||||
/>
|
|
||||||
<div class="path-selection">
|
|
||||||
<h3>{{ selectedProfileType.name }} path</h3>
|
|
||||||
<div class="path-input">
|
|
||||||
<div class="iconified-input">
|
|
||||||
<FolderOpenIcon />
|
|
||||||
<input
|
|
||||||
v-model="selectedProfileType.path"
|
|
||||||
type="text"
|
|
||||||
placeholder="Path to launcher"
|
|
||||||
@change="setPath"
|
|
||||||
/>
|
|
||||||
<Button class="r-btn" @click="() => (selectedLauncherPath = '')">
|
|
||||||
<XIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button icon-only @click="selectLauncherPath">
|
|
||||||
<FolderSearchIcon />
|
|
||||||
</Button>
|
|
||||||
<Button icon-only @click="reload">
|
|
||||||
<UpdatedIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table">
|
|
||||||
<div class="table-head table-row">
|
|
||||||
<div class="toggle-all table-cell">
|
|
||||||
<Checkbox
|
|
||||||
class="select-checkbox"
|
|
||||||
:model-value="
|
|
||||||
profiles.get(selectedProfileType.name)?.every((child) => child.selected)
|
|
||||||
"
|
|
||||||
@update:model-value="
|
|
||||||
(newValue) =>
|
|
||||||
profiles
|
|
||||||
.get(selectedProfileType.name)
|
|
||||||
?.forEach((child) => (child.selected = newValue))
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="name-cell table-cell">Profile name</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
profiles.get(selectedProfileType.name) &&
|
|
||||||
profiles.get(selectedProfileType.name).length > 0
|
|
||||||
"
|
|
||||||
class="table-content"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(profile, index) in profiles.get(selectedProfileType.name)"
|
|
||||||
:key="index"
|
|
||||||
class="table-row"
|
|
||||||
>
|
|
||||||
<div class="checkbox-cell table-cell">
|
|
||||||
<Checkbox v-model="profile.selected" class="select-checkbox" />
|
|
||||||
</div>
|
|
||||||
<div class="name-cell table-cell">
|
|
||||||
{{ profile.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="table-content empty">No profiles found</div>
|
|
||||||
</div>
|
|
||||||
<div class="button-row">
|
|
||||||
<Button
|
|
||||||
:disabled="
|
|
||||||
loading ||
|
|
||||||
!Array.from(profiles.values())
|
|
||||||
.flatMap((e) => e)
|
|
||||||
.some((e) => e.selected)
|
|
||||||
"
|
|
||||||
color="primary"
|
|
||||||
@click="next"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
loading
|
|
||||||
? 'Importing...'
|
|
||||||
: Array.from(profiles.values())
|
|
||||||
.flatMap((e) => e)
|
|
||||||
.some((e) => e.selected)
|
|
||||||
? `Import ${
|
|
||||||
Array.from(profiles.values())
|
|
||||||
.flatMap((e) => e)
|
|
||||||
.filter((e) => e.selected).length
|
|
||||||
} profiles`
|
|
||||||
: 'Select profiles to import'
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
<ProgressBar
|
|
||||||
v-if="loading"
|
|
||||||
:progress="(importedProfiles / (totalProfiles + 0.0001)) * 100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import {
|
|
||||||
CodeIcon,
|
|
||||||
FolderOpenIcon,
|
|
||||||
FolderSearchIcon,
|
|
||||||
InfoIcon,
|
|
||||||
PlusIcon,
|
|
||||||
UpdatedIcon,
|
|
||||||
UploadIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { Avatar, Button, Checkbox, Chips } from '@modrinth/ui'
|
|
||||||
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
|
||||||
import { get_loaders } from '@/helpers/tags'
|
|
||||||
import { create } from '@/helpers/profile'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import Multiselect from 'vue-multiselect'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
|
|
||||||
import {
|
|
||||||
get_default_launcher_path,
|
|
||||||
get_importable_instances,
|
|
||||||
import_instance,
|
|
||||||
} from '@/helpers/import.js'
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
|
||||||
|
|
||||||
const profile_name = ref('')
|
|
||||||
const game_version = ref('')
|
|
||||||
const loader = ref('vanilla')
|
|
||||||
const loader_version = ref('stable')
|
|
||||||
const specified_loader_version = ref('')
|
|
||||||
const icon = ref(null)
|
|
||||||
const display_icon = ref(null)
|
|
||||||
const showAdvanced = ref(false)
|
|
||||||
const creating = ref(false)
|
|
||||||
const showSnapshots = ref(false)
|
|
||||||
const creationType = ref('custom')
|
|
||||||
const isShowing = ref(false)
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
show: async () => {
|
|
||||||
game_version.value = ''
|
|
||||||
specified_loader_version.value = ''
|
|
||||||
profile_name.value = ''
|
|
||||||
creating.value = false
|
|
||||||
showAdvanced.value = false
|
|
||||||
showSnapshots.value = false
|
|
||||||
loader.value = 'vanilla'
|
|
||||||
loader_version.value = 'stable'
|
|
||||||
icon.value = null
|
|
||||||
display_icon.value = null
|
|
||||||
isShowing.value = true
|
|
||||||
modal.value.show()
|
|
||||||
|
|
||||||
unlistener.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
|
||||||
// Only if modal is showing
|
|
||||||
if (!isShowing.value) return
|
|
||||||
if (event.payload.type !== 'drop') return
|
|
||||||
if (creationType.value !== 'from file') return
|
|
||||||
hide()
|
|
||||||
const { paths } = event.payload
|
|
||||||
if (paths && paths.length > 0 && paths[0].endsWith('.mrpack')) {
|
|
||||||
await create_profile_and_install_from_file(paths[0]).catch(handleError)
|
|
||||||
trackEvent('InstanceCreate', {
|
|
||||||
source: 'CreationModalFileDrop',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
trackEvent('InstanceCreateStart', { source: 'CreationModal' })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const unlistener = ref(null)
|
|
||||||
const hide = () => {
|
|
||||||
isShowing.value = false
|
|
||||||
modal.value.hide()
|
|
||||||
if (unlistener.value) {
|
|
||||||
unlistener.value()
|
|
||||||
unlistener.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (unlistener.value) {
|
|
||||||
unlistener.value()
|
|
||||||
unlistener.value = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const [
|
|
||||||
fabric_versions,
|
|
||||||
forge_versions,
|
|
||||||
quilt_versions,
|
|
||||||
neoforge_versions,
|
|
||||||
all_game_versions,
|
|
||||||
loaders,
|
|
||||||
] = await Promise.all([
|
|
||||||
get_loader_versions('fabric').then(shallowRef).catch(handleError),
|
|
||||||
get_loader_versions('forge').then(shallowRef).catch(handleError),
|
|
||||||
get_loader_versions('quilt').then(shallowRef).catch(handleError),
|
|
||||||
get_loader_versions('neo').then(shallowRef).catch(handleError),
|
|
||||||
get_game_versions().then(shallowRef).catch(handleError),
|
|
||||||
get_loaders()
|
|
||||||
.then((value) =>
|
|
||||||
value
|
|
||||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
|
||||||
.map((item) => item.name.toLowerCase()),
|
|
||||||
)
|
|
||||||
.then(ref)
|
|
||||||
.catch(handleError),
|
|
||||||
])
|
|
||||||
loaders.value.unshift('vanilla')
|
|
||||||
|
|
||||||
const game_versions = computed(() => {
|
|
||||||
return all_game_versions.value.versions
|
|
||||||
.filter((item) => {
|
|
||||||
let defaultVal = item.type === 'release' || showSnapshots.value
|
|
||||||
if (loader.value === 'fabric') {
|
|
||||||
defaultVal &= fabric_versions.value.gameVersions.some((x) => item.id === x.id)
|
|
||||||
} else if (loader.value === 'forge') {
|
|
||||||
defaultVal &= forge_versions.value.gameVersions.some((x) => item.id === x.id)
|
|
||||||
} else if (loader.value === 'quilt') {
|
|
||||||
defaultVal &= quilt_versions.value.gameVersions.some((x) => item.id === x.id)
|
|
||||||
} else if (loader.value === 'neoforge') {
|
|
||||||
defaultVal &= neoforge_versions.value.gameVersions.some((x) => item.id === x.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultVal
|
|
||||||
})
|
|
||||||
.map((item) => item.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
const modal = ref(null)
|
|
||||||
|
|
||||||
const check_valid = computed(() => {
|
|
||||||
return (
|
|
||||||
profile_name.value.trim() &&
|
|
||||||
game_version.value &&
|
|
||||||
game_versions.value.includes(game_version.value)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const create_instance = async () => {
|
|
||||||
creating.value = true
|
|
||||||
const loader_version_value =
|
|
||||||
loader_version.value === 'other' ? specified_loader_version.value : loader_version.value
|
|
||||||
const loaderVersion = loader.value === 'vanilla' ? null : loader_version_value ?? 'stable'
|
|
||||||
|
|
||||||
hide()
|
|
||||||
creating.value = false
|
|
||||||
|
|
||||||
await create(
|
|
||||||
profile_name.value,
|
|
||||||
game_version.value,
|
|
||||||
loader.value,
|
|
||||||
loader.value === 'vanilla' ? null : loader_version_value ?? 'stable',
|
|
||||||
icon.value,
|
|
||||||
).catch(handleError)
|
|
||||||
|
|
||||||
trackEvent('InstanceCreate', {
|
|
||||||
profile_name: profile_name.value,
|
|
||||||
game_version: game_version.value,
|
|
||||||
loader: loader.value,
|
|
||||||
loader_version: loaderVersion,
|
|
||||||
has_icon: !!icon.value,
|
|
||||||
source: 'CreationModal',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const upload_icon = async () => {
|
|
||||||
const res = await open({
|
|
||||||
multiple: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Image',
|
|
||||||
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
icon.value = res.path ?? res
|
|
||||||
|
|
||||||
if (!icon.value) return
|
|
||||||
display_icon.value = convertFileSrc(icon.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset_icon = () => {
|
|
||||||
icon.value = null
|
|
||||||
display_icon.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectable_versions = computed(() => {
|
|
||||||
if (game_version.value) {
|
|
||||||
if (loader.value === 'fabric') {
|
|
||||||
return fabric_versions.value.gameVersions[0].loaders.map((item) => item.id)
|
|
||||||
} else if (loader.value === 'forge') {
|
|
||||||
return forge_versions.value.gameVersions
|
|
||||||
.find((item) => item.id === game_version.value)
|
|
||||||
.loaders.map((item) => item.id)
|
|
||||||
} else if (loader.value === 'quilt') {
|
|
||||||
return quilt_versions.value.gameVersions[0].loaders.map((item) => item.id)
|
|
||||||
} else if (loader.value === 'neoforge') {
|
|
||||||
return neoforge_versions.value.gameVersions
|
|
||||||
.find((item) => item.id === game_version.value)
|
|
||||||
.loaders.map((item) => item.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggle_advanced = () => {
|
|
||||||
showAdvanced.value = !showAdvanced.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const openFile = async () => {
|
|
||||||
const newProject = await open({ multiple: false })
|
|
||||||
if (!newProject) return
|
|
||||||
hide()
|
|
||||||
await create_profile_and_install_from_file(newProject.path ?? newProject).catch(handleError)
|
|
||||||
|
|
||||||
trackEvent('InstanceCreate', {
|
|
||||||
source: 'CreationModalFileOpen',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const profiles = ref(
|
|
||||||
new Map([
|
|
||||||
['MultiMC', []],
|
|
||||||
['GDLauncher', []],
|
|
||||||
['ATLauncher', []],
|
|
||||||
['Curseforge', []],
|
|
||||||
['PrismLauncher', []],
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const importedProfiles = ref(0)
|
|
||||||
const totalProfiles = ref(0)
|
|
||||||
|
|
||||||
const selectedProfileType = ref('MultiMC')
|
|
||||||
const profileOptions = ref([
|
|
||||||
{ name: 'MultiMC', path: '' },
|
|
||||||
{ name: 'GDLauncher', path: '' },
|
|
||||||
{ name: 'ATLauncher', path: '' },
|
|
||||||
{ name: 'Curseforge', path: '' },
|
|
||||||
{ name: 'PrismLauncher', path: '' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// Attempt to get import profiles on default paths
|
|
||||||
const promises = profileOptions.value.map(async (option) => {
|
|
||||||
const path = await get_default_launcher_path(option.name).catch(handleError)
|
|
||||||
if (!path || path === '') return
|
|
||||||
|
|
||||||
// Try catch to allow failure and simply ignore default path attempt
|
|
||||||
try {
|
|
||||||
const instances = await get_importable_instances(option.name, path)
|
|
||||||
|
|
||||||
if (!instances) return
|
|
||||||
profileOptions.value.find((profile) => profile.name === option.name).path = path
|
|
||||||
profiles.value.set(
|
|
||||||
option.name,
|
|
||||||
instances.map((name) => ({ name, selected: false })),
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
// Allow failure silently
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
const selectLauncherPath = async () => {
|
|
||||||
selectedProfileType.value.path = await open({ multiple: false, directory: true })
|
|
||||||
|
|
||||||
if (selectedProfileType.value.path) {
|
|
||||||
await reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = async () => {
|
|
||||||
const instances = await get_importable_instances(
|
|
||||||
selectedProfileType.value.name,
|
|
||||||
selectedProfileType.value.path,
|
|
||||||
).catch(handleError)
|
|
||||||
if (instances) {
|
|
||||||
profiles.value.set(
|
|
||||||
selectedProfileType.value.name,
|
|
||||||
instances.map((name) => ({ name, selected: false })),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
profiles.value.set(selectedProfileType.value.name, [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setPath = () => {
|
|
||||||
profileOptions.value.find((profile) => profile.name === selectedProfileType.value.name).path =
|
|
||||||
selectedProfileType.value.path
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = async () => {
|
|
||||||
importedProfiles.value = 0
|
|
||||||
totalProfiles.value = Array.from(profiles.value.values())
|
|
||||||
.map((profiles) => profiles.filter((profile) => profile.selected).length)
|
|
||||||
.reduce((a, b) => a + b, 0)
|
|
||||||
loading.value = true
|
|
||||||
for (const launcher of Array.from(profiles.value.entries()).map(([launcher, profiles]) => ({
|
|
||||||
launcher,
|
|
||||||
path: profileOptions.value.find((option) => option.name === launcher).path,
|
|
||||||
profiles,
|
|
||||||
}))) {
|
|
||||||
for (const profile of launcher.profiles.filter((profile) => profile.selected)) {
|
|
||||||
await import_instance(launcher.launcher, launcher.path, profile.name)
|
|
||||||
.catch(handleError)
|
|
||||||
.then(() => console.log(`Successfully Imported ${profile.name} from ${launcher.launcher}`))
|
|
||||||
profile.selected = false
|
|
||||||
importedProfiles.value++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
margin-top: var(--gap-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-label {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: bolder;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-input {
|
|
||||||
width: 20rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-upload {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-input {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(button.checkbox) {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector {
|
|
||||||
max-width: 20rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labeled-divider {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labeled-divider:after {
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
content: 'Or';
|
|
||||||
color: var(--color-base);
|
|
||||||
padding: var(--gap-sm);
|
|
||||||
position: relative;
|
|
||||||
top: -0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-selection {
|
|
||||||
padding: var(--gap-xl);
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-input {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
:deep(input) {
|
|
||||||
width: 100%;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
border: 1px solid var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row {
|
|
||||||
grid-template-columns: min-content auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-content {
|
|
||||||
max-height: calc(5 * (18px + 2rem));
|
|
||||||
height: calc(5 * (18px + 2rem));
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-checkbox {
|
|
||||||
button.checkbox {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
|
|
||||||
.transparent {
|
|
||||||
padding: var(--gap-sm) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bolder;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-divider {
|
|
||||||
margin: var(--gap-md) var(--gap-lg) 0 var(--gap-lg);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,53 +1,57 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
|
||||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, FormattedTag } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
game_version: string
|
game_version: string
|
||||||
loader: string
|
loader: string
|
||||||
path: string
|
path: string
|
||||||
install_stage: string
|
install_stage: string
|
||||||
icon_path?: string
|
icon_path?: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<{
|
const props = withDefaults(
|
||||||
instance: Instance
|
defineProps<{
|
||||||
}>()
|
instance: Instance
|
||||||
|
backTab?: string
|
||||||
|
}>(),
|
||||||
|
{ backTab: undefined },
|
||||||
|
)
|
||||||
|
|
||||||
|
const instanceLink = computed(() => {
|
||||||
|
const base = `/instance/${encodeURIComponent(props.instance.path)}`
|
||||||
|
return props.backTab ? `${base}/${props.backTab}` : base
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
||||||
<router-link
|
<router-link :to="instanceLink" tabindex="-1" class="flex flex-col gap-4 text-primary">
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
<span class="flex items-center gap-2">
|
||||||
tabindex="-1"
|
<Avatar
|
||||||
class="flex flex-col gap-4 text-primary"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
>
|
:alt="instance.name"
|
||||||
<span class="flex items-center gap-2">
|
size="48px"
|
||||||
<Avatar
|
/>
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
<span class="flex flex-col gap-2">
|
||||||
:alt="instance.name"
|
<span class="font-extrabold bold text-contrast">
|
||||||
size="48px"
|
{{ instance.name }}
|
||||||
/>
|
</span>
|
||||||
<span class="flex flex-col gap-2">
|
<span class="text-secondary flex items-center gap-2 font-semibold">
|
||||||
<span class="font-extrabold bold text-contrast">
|
<GameIcon class="h-5 w-5 text-secondary" />
|
||||||
{{ instance.name }}
|
<FormattedTag :tag="instance.loader" enforce-type="loader" />
|
||||||
</span>
|
{{ instance.game_version }}
|
||||||
<span class="text-secondary flex items-center gap-2 font-semibold">
|
</span>
|
||||||
<GameIcon class="h-5 w-5 text-secondary" />
|
</span>
|
||||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
</span>
|
||||||
</span>
|
</router-link>
|
||||||
</span>
|
<ButtonStyled>
|
||||||
</span>
|
<router-link :to="instanceLink"> <LeftArrowIcon /> Back to instance </router-link>
|
||||||
</router-link>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
</div>
|
||||||
<router-link :to="`/instance/${encodeURIComponent(instance.path)}`">
|
|
||||||
<LeftArrowIcon /> Back to instance
|
|
||||||
</router-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@@ -1,95 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
||||||
<div class="auto-detect-modal">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="table">
|
<Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path">
|
||||||
<div class="table-row table-head">
|
<template #cell-version="{ value }">
|
||||||
<div class="table-cell table-text">Version</div>
|
<span class="font-semibold text-primary">{{ value }}</span>
|
||||||
<div class="table-cell table-text">Path</div>
|
</template>
|
||||||
<div class="table-cell table-text">Actions</div>
|
<template #cell-path="{ value }">
|
||||||
</div>
|
<span v-tooltip="value" class="block truncate font-mono text-xs">{{ value }}</span>
|
||||||
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
|
</template>
|
||||||
<div class="table-cell table-text">
|
<template #cell-actions="{ row }">
|
||||||
<span>{{ javaInstall.version }}</span>
|
<div class="flex items-center justify-end">
|
||||||
</div>
|
<ButtonStyled v-if="currentSelected.path === row.path">
|
||||||
<div v-tooltip="javaInstall.path" class="table-cell table-text">
|
<button class="!shadow-none" disabled><CheckIcon /> Selected</button>
|
||||||
<span>{{ javaInstall.path }}</span>
|
</ButtonStyled>
|
||||||
</div>
|
<ButtonStyled v-else>
|
||||||
<div class="table-cell table-text manage">
|
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button>
|
||||||
<Button v-if="currentSelected.path === javaInstall.path" disabled
|
</ButtonStyled>
|
||||||
><CheckIcon /> Selected</Button
|
</div>
|
||||||
>
|
</template>
|
||||||
<Button v-else @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</Button>
|
<template #empty-state>
|
||||||
</div>
|
<div class="p-4 text-secondary">No java installations found!</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
|
</Table>
|
||||||
<div class="table-cell table-text">No java installations found!</div>
|
<div class="flex justify-end">
|
||||||
</div>
|
<ButtonStyled type="outlined">
|
||||||
</div>
|
<button
|
||||||
<div class="input-group push-right">
|
class="!shadow-none !border-surface-4 !border"
|
||||||
<Button @click="$refs.detectJavaModal.hide()">
|
@click="$refs.detectJavaModal.hide()"
|
||||||
<XIcon />
|
>
|
||||||
Cancel
|
<XIcon />
|
||||||
</Button>
|
Cancel
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</ButtonStyled>
|
||||||
</ModalWrapper>
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
|
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { find_filtered_jres } from '@/helpers/jre.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { find_filtered_jres } from '@/helpers/jre.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const chosenInstallOptions = ref([])
|
const chosenInstallOptions = ref([])
|
||||||
const detectJavaModal = ref(null)
|
const detectJavaModal = ref(null)
|
||||||
const currentSelected = ref({})
|
const currentSelected = ref({})
|
||||||
|
const javaInstallColumns = [
|
||||||
|
{ key: 'version', label: 'Version', width: '9rem' },
|
||||||
|
{ key: 'path', label: 'Path' },
|
||||||
|
{ key: 'actions', label: 'Actions', align: 'right', width: '10rem' },
|
||||||
|
]
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: async (version, currentSelectedJava) => {
|
show: async (version, currentSelectedJava) => {
|
||||||
chosenInstallOptions.value = await find_filtered_jres(version).catch(handleError)
|
chosenInstallOptions.value = await find_filtered_jres(version).catch(handleError)
|
||||||
|
|
||||||
currentSelected.value = currentSelectedJava
|
currentSelected.value = currentSelectedJava
|
||||||
if (!currentSelected.value) {
|
if (!currentSelected.value) {
|
||||||
currentSelected.value = { path: '', version: '' }
|
currentSelected.value = { path: '', version: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
detectJavaModal.value.show()
|
detectJavaModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['submit'])
|
const emit = defineEmits(['submit'])
|
||||||
|
|
||||||
function setJavaInstall(javaInstall) {
|
function setJavaInstall(javaInstall) {
|
||||||
emit('submit', javaInstall)
|
emit('submit', javaInstall)
|
||||||
detectJavaModal.value.hide()
|
detectJavaModal.value.hide()
|
||||||
trackEvent('JavaAutoDetect', {
|
trackEvent('JavaAutoDetect', {
|
||||||
path: javaInstall.path,
|
path: javaInstall.path,
|
||||||
version: javaInstall.version,
|
version: javaInstall.version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.auto-detect-modal {
|
|
||||||
.table {
|
|
||||||
.table-row {
|
|
||||||
grid-template-columns: 1fr 4fr min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inherit;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.manage {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,220 +1,256 @@
|
|||||||
<template>
|
<template>
|
||||||
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
|
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
|
||||||
<div class="toggle-setting" :class="{ compact }">
|
<div :id="props.id" class="toggle-setting" :class="{ compact }">
|
||||||
<input
|
<div class="input-with-status">
|
||||||
autocomplete="off"
|
<StyledInput
|
||||||
:disabled="props.disabled"
|
autocomplete="off"
|
||||||
:value="props.modelValue ? props.modelValue.path : ''"
|
:disabled="props.disabled"
|
||||||
type="text"
|
:model-value="props.modelValue ? props.modelValue.path : ''"
|
||||||
class="installation-input"
|
:placeholder="placeholder ?? '/path/to/java'"
|
||||||
:placeholder="placeholder ?? '/path/to/java'"
|
wrapper-class="installation-input"
|
||||||
@input="
|
@update:model-value="
|
||||||
(val) => {
|
(val) => {
|
||||||
emit('update:modelValue', {
|
emit('update:modelValue', {
|
||||||
...props.modelValue,
|
...props.modelValue,
|
||||||
path: val.target.value,
|
path: val,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<span class="installation-buttons">
|
<ButtonStyled
|
||||||
<Button
|
:color="
|
||||||
v-if="props.version"
|
!hoveringTest && !testingJava
|
||||||
:disabled="props.disabled || installingJava"
|
? testingJavaSuccess === true
|
||||||
@click="reinstallJava"
|
? 'green'
|
||||||
>
|
: 'red'
|
||||||
<DownloadIcon />
|
: 'standard'
|
||||||
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
"
|
||||||
</Button>
|
color-fill="text"
|
||||||
<Button :disabled="props.disabled" @click="autoDetect">
|
>
|
||||||
<SearchIcon />
|
<button
|
||||||
Detect
|
class="!shadow-none"
|
||||||
</Button>
|
:disabled="testingJava || props.disabled"
|
||||||
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
@click="runTest(props.modelValue?.path)"
|
||||||
<FolderSearchIcon />
|
@mouseenter="!props.disabled && (hoveringTest = true)"
|
||||||
Browse
|
@mouseleave="hoveringTest = false"
|
||||||
</Button>
|
>
|
||||||
<Button v-if="testingJava" disabled> Testing... </Button>
|
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||||
<Button v-else-if="testingJavaSuccess === true">
|
<CheckCircleIcon
|
||||||
<CheckIcon class="test-success" />
|
v-else-if="testingJavaSuccess === true && !hoveringTest"
|
||||||
Success
|
class="h-4 w-4"
|
||||||
</Button>
|
/>
|
||||||
<Button v-else-if="testingJavaSuccess === false">
|
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
|
||||||
<XIcon class="test-fail" />
|
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
|
||||||
Failed
|
</button>
|
||||||
</Button>
|
</ButtonStyled>
|
||||||
<Button v-else :disabled="props.disabled" @click="testJava">
|
</div>
|
||||||
<PlayIcon />
|
<span class="installation-buttons">
|
||||||
Test
|
<ButtonStyled v-if="props.version">
|
||||||
</Button>
|
<button
|
||||||
</span>
|
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined"
|
||||||
</div>
|
class="!shadow-none"
|
||||||
|
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
|
||||||
|
@click="reinstallJava"
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
|
||||||
|
<SearchIcon />
|
||||||
|
Detect
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||||
|
<FolderSearchIcon />
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
SearchIcon,
|
CheckCircleIcon,
|
||||||
PlayIcon,
|
DownloadIcon,
|
||||||
CheckIcon,
|
FolderSearchIcon,
|
||||||
XIcon,
|
RefreshCwIcon,
|
||||||
FolderSearchIcon,
|
SearchIcon,
|
||||||
DownloadIcon,
|
SpinnerIcon,
|
||||||
|
XCircleIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||||
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||||
import { handleError } from '@/store/state.js'
|
import useJavaTest from '@/composables/useJavaTest'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
version: {
|
id: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
modelValue: {
|
version: {
|
||||||
type: Object,
|
type: Number,
|
||||||
default: () => ({
|
required: false,
|
||||||
path: '',
|
default: null,
|
||||||
version: '',
|
},
|
||||||
}),
|
modelValue: {
|
||||||
},
|
type: Object,
|
||||||
disabled: {
|
default: () => ({
|
||||||
type: Boolean,
|
path: '',
|
||||||
required: false,
|
version: '',
|
||||||
default: false,
|
}),
|
||||||
},
|
},
|
||||||
placeholder: {
|
disabled: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: false,
|
||||||
},
|
},
|
||||||
compact: {
|
placeholder: {
|
||||||
type: Boolean,
|
type: String,
|
||||||
default: false,
|
required: false,
|
||||||
},
|
default: null,
|
||||||
|
},
|
||||||
|
compact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const testingJava = ref(false)
|
const {
|
||||||
const testingJavaSuccess = ref(null)
|
testingJava,
|
||||||
|
javaTestResult: testingJavaSuccess,
|
||||||
|
testJavaInstallationDebounced,
|
||||||
|
testJavaInstallation,
|
||||||
|
} = useJavaTest()
|
||||||
|
|
||||||
const installingJava = ref(false)
|
const installingJava = ref(false)
|
||||||
|
const hoveringTest = ref(false)
|
||||||
|
let hasInitialized = false
|
||||||
|
|
||||||
async function testJava() {
|
async function runTest(path) {
|
||||||
testingJava.value = true
|
await testJavaInstallation(path, props.version, true)
|
||||||
testingJavaSuccess.value = await test_jre(
|
|
||||||
props.modelValue ? props.modelValue.path : '',
|
|
||||||
1,
|
|
||||||
props.version,
|
|
||||||
)
|
|
||||||
testingJava.value = false
|
|
||||||
|
|
||||||
trackEvent('JavaTest', {
|
|
||||||
path: props.modelValue ? props.modelValue.path : '',
|
|
||||||
success: testingJavaSuccess.value,
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
testingJavaSuccess.value = null
|
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue?.path,
|
||||||
|
(newPath) => {
|
||||||
|
if (newPath) {
|
||||||
|
if (!hasInitialized) {
|
||||||
|
testJavaInstallation(newPath, props.version, false)
|
||||||
|
hasInitialized = true
|
||||||
|
} else {
|
||||||
|
testJavaInstallationDebounced(newPath, props.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
async function handleJavaFileInput() {
|
async function handleJavaFileInput() {
|
||||||
const filePath = await open()
|
const filePath = await open()
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
path: filePath.path ?? filePath,
|
path: filePath.path ?? filePath,
|
||||||
version: props.version.toString(),
|
version: props.version.toString(),
|
||||||
architecture: 'x86',
|
parsed_version: props.version,
|
||||||
}
|
architecture: 'x86',
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trackEvent('JavaManualSelect', {
|
trackEvent('JavaManualSelect', {
|
||||||
version: props.version,
|
version: props.version,
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('update:modelValue', result)
|
emit('update:modelValue', result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const detectJavaModal = ref(null)
|
const detectJavaModal = ref(null)
|
||||||
async function autoDetect() {
|
async function autoDetect() {
|
||||||
if (!props.compact) {
|
if (!props.compact) {
|
||||||
detectJavaModal.value.show(props.version, props.modelValue)
|
detectJavaModal.value.show(props.version, props.modelValue)
|
||||||
} else {
|
} else {
|
||||||
const versions = await find_filtered_jres(props.version).catch(handleError)
|
const versions = await find_filtered_jres(props.version).catch(handleError)
|
||||||
if (versions.length > 0) {
|
if (versions.length > 0) {
|
||||||
emit('update:modelValue', versions[0])
|
emit('update:modelValue', versions[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reinstallJava() {
|
async function reinstallJava() {
|
||||||
installingJava.value = true
|
installingJava.value = true
|
||||||
const path = await auto_install_java(props.version).catch(handleError)
|
const path = await auto_install_java(props.version).catch(handleError)
|
||||||
let result = await get_jre(path)
|
let result = await get_jre(path)
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
path: path,
|
path: path,
|
||||||
version: props.version.toString(),
|
version: props.version.toString(),
|
||||||
architecture: 'x86',
|
parsed_version: props.version,
|
||||||
}
|
architecture: 'x86',
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trackEvent('JavaReInstall', {
|
trackEvent('JavaReInstall', {
|
||||||
path: path,
|
path: path,
|
||||||
version: props.version,
|
version: props.version,
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('update:modelValue', result)
|
emit('update:modelValue', result)
|
||||||
installingJava.value = false
|
installingJava.value = false
|
||||||
|
runTest(result.path)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.input-with-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.installation-input {
|
.installation-input {
|
||||||
width: 100% !important;
|
flex: 1 1 0;
|
||||||
flex-grow: 1;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-setting {
|
.toggle-setting {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.installation-buttons {
|
.installation-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-success {
|
|
||||||
color: var(--color-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-fail {
|
|
||||||
color: var(--color-red);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup>
|
||||||
|
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, FormattedTag, TagItem, useCompactNumber } from '@modrinth/ui'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { formatCompactNumber } = useCompactNumber()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
project: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const featuredCategory = computed(() => {
|
||||||
|
if (props.project.display_categories.includes('optimization')) {
|
||||||
|
return 'optimization'
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.project.display_categories[0] ?? props.project.categories[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
const toColor = computed(() => {
|
||||||
|
let color = props.project.color
|
||||||
|
|
||||||
|
color >>>= 0
|
||||||
|
const b = color & 0xff
|
||||||
|
const g = (color >>> 8) & 0xff
|
||||||
|
const r = (color >>> 16) & 0xff
|
||||||
|
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
||||||
|
})
|
||||||
|
|
||||||
|
const toTransparent = computed(() => {
|
||||||
|
let color = props.project.color
|
||||||
|
|
||||||
|
color >>>= 0
|
||||||
|
const b = color & 0xff
|
||||||
|
const g = (color >>> 8) & 0xff
|
||||||
|
const r = (color >>> 16) & 0xff
|
||||||
|
return (
|
||||||
|
'linear-gradient(rgba(' +
|
||||||
|
[r, g, b, 0.03].join(',') +
|
||||||
|
'), 65%, rgba(' +
|
||||||
|
[r, g, b, 0.3].join(',') +
|
||||||
|
'))'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
|
||||||
|
@click="router.push(`/project/${project.slug}`)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
||||||
|
:style="{
|
||||||
|
'background-color': (project.featured_gallery ?? project.gallery[0]) ? null : toColor,
|
||||||
|
'background-image': `url(${
|
||||||
|
project.featured_gallery ??
|
||||||
|
project.gallery[0] ??
|
||||||
|
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
||||||
|
})`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="badges-wrapper"
|
||||||
|
:class="{
|
||||||
|
'no-image': !project.featured_gallery && !project.gallery[0],
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-center gap-2 px-4 py-3">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<Avatar size="48px" :src="project.icon_url" />
|
||||||
|
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||||
|
<span class="line-clamp-2">{{ project.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
|
||||||
|
{{ project.description }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
{{ formatCompactNumber(project.downloads) }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||||
|
>
|
||||||
|
<HeartIcon />
|
||||||
|
{{ formatCompactNumber(project.follows) }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 pr-2">
|
||||||
|
<TagIcon />
|
||||||
|
<TagItem>
|
||||||
|
<FormattedTag :tag="featuredCategory" />
|
||||||
|
</TagItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -1,33 +1,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { CheckIcon } from '@modrinth/assets'
|
import { CheckIcon } from '@modrinth/assets'
|
||||||
import { Button, Badge } from '@modrinth/ui'
|
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
|
||||||
import { releaseColor } from '@/helpers/utils'
|
|
||||||
import { SwapIcon } from '@/assets/icons/index.js'
|
import { SwapIcon } from '@/assets/icons/index.js'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||||
|
import { releaseColor } from '@/helpers/utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
versions: {
|
versions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
modpackVersionModal.value.show()
|
modpackVersionModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['finish-install'])
|
const emit = defineEmits(['finish-install'])
|
||||||
|
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
return props.versions
|
return props.versions
|
||||||
})
|
})
|
||||||
|
|
||||||
const modpackVersionModal = ref(null)
|
const modpackVersionModal = ref(null)
|
||||||
@@ -36,160 +37,163 @@ const installing = computed(() => props.instance.install_stage !== 'installed')
|
|||||||
const inProgress = ref(false)
|
const inProgress = ref(false)
|
||||||
|
|
||||||
const switchVersion = async (versionId) => {
|
const switchVersion = async (versionId) => {
|
||||||
modpackVersionModal.value.hide()
|
modpackVersionModal.value.hide()
|
||||||
inProgress.value = true
|
inProgress.value = true
|
||||||
await update_managed_modrinth_version(props.instance.path, versionId)
|
await update_managed_modrinth_version(props.instance.path, versionId)
|
||||||
inProgress.value = false
|
inProgress.value = false
|
||||||
emit('finish-install')
|
emit('finish-install')
|
||||||
}
|
}
|
||||||
|
|
||||||
const onHide = () => {
|
const onHide = () => {
|
||||||
if (!inProgress.value) {
|
if (!inProgress.value) {
|
||||||
emit('finish-install')
|
emit('finish-install')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper
|
<ModalWrapper
|
||||||
ref="modpackVersionModal"
|
ref="modpackVersionModal"
|
||||||
class="modpack-version-modal"
|
class="modpack-version-modal"
|
||||||
header="Change modpack version"
|
header="Change modpack version"
|
||||||
:on-hide="onHide"
|
:on-hide="onHide"
|
||||||
>
|
>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div v-if="instance.linked_data" class="mod-card">
|
<div v-if="instance.linked_data" class="mod-card">
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table-row with-columns table-head">
|
<div class="table-row with-columns table-head">
|
||||||
<div class="table-cell table-text download-cell" />
|
<div class="table-cell table-text download-cell" />
|
||||||
<div class="name-cell table-cell table-text">Name</div>
|
<div class="name-cell table-cell table-text">Name</div>
|
||||||
<div class="table-cell table-text">Supports</div>
|
<div class="table-cell table-text">Supports</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scrollable">
|
<div class="scrollable">
|
||||||
<div
|
<div
|
||||||
v-for="version in filteredVersions"
|
v-for="version in filteredVersions"
|
||||||
:key="version.id"
|
:key="version.id"
|
||||||
class="table-row with-columns selectable"
|
class="table-row with-columns selectable"
|
||||||
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
||||||
>
|
>
|
||||||
<div class="table-cell table-text">
|
<div class="table-cell table-text">
|
||||||
<Button
|
<ButtonStyled
|
||||||
:color="version.id === installedVersion ? '' : 'primary'"
|
circular
|
||||||
icon-only
|
:color="version.id === installedVersion ? 'standard' : 'brand'"
|
||||||
:disabled="inProgress || installing || version.id === installedVersion"
|
>
|
||||||
@click.stop="() => switchVersion(version.id)"
|
<button
|
||||||
>
|
:disabled="inProgress || installing || version.id === installedVersion"
|
||||||
<SwapIcon v-if="version.id !== installedVersion" />
|
@click.stop="() => switchVersion(version.id)"
|
||||||
<CheckIcon v-else />
|
>
|
||||||
</Button>
|
<SwapIcon v-if="version.id !== installedVersion" />
|
||||||
</div>
|
<CheckIcon v-else />
|
||||||
<div class="name-cell table-cell table-text">
|
</button>
|
||||||
<div class="version-link">
|
</ButtonStyled>
|
||||||
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
</div>
|
||||||
<div class="version-badge">
|
<div class="name-cell table-cell table-text">
|
||||||
<div class="channel-indicator">
|
<div class="version-link">
|
||||||
<Badge
|
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
||||||
:color="releaseColor(version.version_type)"
|
<div class="version-badge">
|
||||||
:type="
|
<div class="channel-indicator">
|
||||||
version.version_type.charAt(0).toUpperCase() +
|
<Badge
|
||||||
version.version_type.slice(1)
|
:color="releaseColor(version.version_type)"
|
||||||
"
|
:type="
|
||||||
/>
|
version.version_type.charAt(0).toUpperCase() +
|
||||||
</div>
|
version.version_type.slice(1)
|
||||||
<div>
|
"
|
||||||
{{ version.version_number }}
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
{{ version.version_number }}
|
||||||
</div>
|
</div>
|
||||||
<div class="table-cell table-text stacked-text">
|
</div>
|
||||||
<span>
|
</div>
|
||||||
{{
|
</div>
|
||||||
version.loaders
|
<div class="table-cell table-text stacked-text">
|
||||||
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
|
<span>
|
||||||
.join(', ')
|
{{
|
||||||
}}
|
version.loaders
|
||||||
</span>
|
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
|
||||||
<span>
|
.join(', ')
|
||||||
{{ version.game_versions.join(', ') }}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span>
|
||||||
</div>
|
{{ version.game_versions.join(', ') }}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.filter-header {
|
.filter-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.with-columns {
|
.with-columns {
|
||||||
grid-template-columns: min-content 1fr 1fr;
|
grid-template-columns: min-content 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 25rem;
|
max-height: 25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-row {
|
.card-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-card {
|
.mod-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-link {
|
.version-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
.version-badge {
|
.version-badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.channel-indicator {
|
.channel-indicator {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stacked-text {
|
.stacked-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-cell {
|
.download-cell {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
border: 1px solid var(--color-bg);
|
border: 1px solid var(--color-bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="typeof to === 'string'"
|
v-if="typeof to === 'string'"
|
||||||
:to="to"
|
:to="to"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:class="{
|
:active-class="isSubpage ? '' : undefined"
|
||||||
'router-link-active': isPrimary && isPrimary(route),
|
:class="{
|
||||||
'subpage-active': isSubpage && isSubpage(route),
|
'router-link-active': isPrimary && isPrimary(route),
|
||||||
}"
|
'subpage-active': isSubpage && isSubpage(route),
|
||||||
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
disabled: disabled,
|
||||||
>
|
}"
|
||||||
<slot />
|
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
</RouterLink>
|
>
|
||||||
<button
|
<slot />
|
||||||
v-else
|
</RouterLink>
|
||||||
v-bind="$attrs"
|
<button
|
||||||
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
v-else
|
||||||
@click="to"
|
v-bind="$attrs"
|
||||||
>
|
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
<slot />
|
:disabled="disabled"
|
||||||
</button>
|
@click="to"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -29,31 +32,37 @@ const route = useRoute()
|
|||||||
|
|
||||||
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
||||||
|
|
||||||
defineProps<{
|
withDefaults(
|
||||||
to: (() => void) | string
|
defineProps<{
|
||||||
isPrimary?: RouteFunction
|
to: (() => void) | string
|
||||||
isSubpage?: RouteFunction
|
isPrimary?: RouteFunction
|
||||||
highlightOverride?: boolean
|
isSubpage?: RouteFunction
|
||||||
}>()
|
highlightOverride?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.router-link-active,
|
.router-link-active,
|
||||||
.subpage-active {
|
.subpage-active {
|
||||||
svg {
|
svg {
|
||||||
filter: drop-shadow(0 0 0.5rem black);
|
filter: drop-shadow(0 0 0.5rem black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-link-active {
|
.router-link-active {
|
||||||
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
|
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
|
||||||
}
|
}
|
||||||
|
|
||||||
.subpage-active {
|
.subpage-active {
|
||||||
@apply text-contrast bg-button-bg;
|
@apply text-contrast bg-button-bg;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
<template>
|
|
||||||
<nav
|
|
||||||
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
|
||||||
>
|
|
||||||
<RouterLink
|
|
||||||
v-for="(link, index) in filteredLinks"
|
|
||||||
v-show="link.shown === undefined ? true : link.shown"
|
|
||||||
:key="index"
|
|
||||||
ref="tabLinkElements"
|
|
||||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
|
||||||
:class="`button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full ${activeIndex === index && !subpageSelected ? 'text-button-textSelected' : activeIndex === index && subpageSelected ? 'text-contrast' : 'text-primary'}`"
|
|
||||||
>
|
|
||||||
<component :is="link.icon" v-if="link.icon" class="size-5" />
|
|
||||||
<span class="text-nowrap">{{ link.label }}</span>
|
|
||||||
</RouterLink>
|
|
||||||
<div
|
|
||||||
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'}`"
|
|
||||||
:style="{
|
|
||||||
left: sliderLeftPx,
|
|
||||||
top: sliderTopPx,
|
|
||||||
right: sliderRightPx,
|
|
||||||
bottom: sliderBottomPx,
|
|
||||||
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
|
|
||||||
}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></div>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
|
||||||
import { useRoute, RouterLink } from 'vue-router'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
interface Tab {
|
|
||||||
label: string
|
|
||||||
href: string | RouteLocationRaw
|
|
||||||
shown?: boolean
|
|
||||||
icon?: unknown
|
|
||||||
subpages?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
links: Tab[]
|
|
||||||
query?: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const sliderLeft = ref(4)
|
|
||||||
const sliderTop = ref(4)
|
|
||||||
const sliderRight = ref(4)
|
|
||||||
const sliderBottom = ref(4)
|
|
||||||
const activeIndex = ref(-1)
|
|
||||||
const oldIndex = ref(-1)
|
|
||||||
const subpageSelected = ref(false)
|
|
||||||
|
|
||||||
const filteredLinks = computed(() =>
|
|
||||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
|
||||||
)
|
|
||||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
|
||||||
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
|
||||||
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
|
||||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
|
||||||
|
|
||||||
function pickLink() {
|
|
||||||
let index = -1
|
|
||||||
subpageSelected.value = false
|
|
||||||
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
|
||||||
const link = filteredLinks.value[i]
|
|
||||||
|
|
||||||
if (route.path === (typeof link.href === 'string' ? link.href : link.href.path)) {
|
|
||||||
index = i
|
|
||||||
break
|
|
||||||
} else if (link.subpages && link.subpages.some((subpage) => route.path.includes(subpage))) {
|
|
||||||
index = i
|
|
||||||
subpageSelected.value = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activeIndex.value = index
|
|
||||||
|
|
||||||
if (activeIndex.value !== -1) {
|
|
||||||
startAnimation()
|
|
||||||
} else {
|
|
||||||
oldIndex.value = -1
|
|
||||||
sliderLeft.value = 0
|
|
||||||
sliderRight.value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabLinkElements = ref()
|
|
||||||
|
|
||||||
function startAnimation() {
|
|
||||||
const el = tabLinkElements.value[activeIndex.value].$el
|
|
||||||
|
|
||||||
if (!el || !el.offsetParent) return
|
|
||||||
|
|
||||||
const newValues = {
|
|
||||||
left: el.offsetLeft,
|
|
||||||
top: el.offsetTop,
|
|
||||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
|
||||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
|
||||||
sliderLeft.value = newValues.left
|
|
||||||
sliderRight.value = newValues.right
|
|
||||||
sliderTop.value = newValues.top
|
|
||||||
sliderBottom.value = newValues.bottom
|
|
||||||
} else {
|
|
||||||
const delay = 200
|
|
||||||
|
|
||||||
if (newValues.left < sliderLeft.value) {
|
|
||||||
sliderLeft.value = newValues.left
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderRight.value = newValues.right
|
|
||||||
}, delay)
|
|
||||||
} else {
|
|
||||||
sliderRight.value = newValues.right
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderLeft.value = newValues.left
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValues.top < sliderTop.value) {
|
|
||||||
sliderTop.value = newValues.top
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderBottom.value = newValues.bottom
|
|
||||||
}, delay)
|
|
||||||
} else {
|
|
||||||
sliderBottom.value = newValues.bottom
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderTop.value = newValues.top
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', pickLink)
|
|
||||||
pickLink()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', pickLink)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(route, () => {
|
|
||||||
pickLink()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.navtabs-transition {
|
|
||||||
/* Delay on opacity is to hide any jankiness as the page loads */
|
|
||||||
transition:
|
|
||||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
|
||||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,33 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-bar__fill" :style="{ width: `${progress}%` }"></div>
|
<div
|
||||||
</div>
|
class="progress-bar__fill"
|
||||||
|
:style="{
|
||||||
|
width: `${progress}%`,
|
||||||
|
'background-color': error ? 'var(--color-red)' : 'var(--color-brand)',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
defineProps({
|
||||||
progress: {
|
progress: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
validator(value) {
|
validator(value) {
|
||||||
return value >= 0 && value <= 100
|
return value >= 0 && value <= 100
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar__fill {
|
.progress-bar__fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--color-brand);
|
transition: width 0.3s ease-out;
|
||||||
transition: width 0.3s;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { Avatar, TagItem } from '@modrinth/ui'
|
|
||||||
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const featuredCategory = computed(() => {
|
|
||||||
if (props.project.categories.includes('optimization')) {
|
|
||||||
return 'optimization'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.project.categories.length > 0) {
|
|
||||||
return props.project.categories[0]
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const toColor = computed(() => {
|
|
||||||
let color = props.project.color
|
|
||||||
|
|
||||||
color >>>= 0
|
|
||||||
const b = color & 0xff
|
|
||||||
const g = (color >>> 8) & 0xff
|
|
||||||
const r = (color >>> 16) & 0xff
|
|
||||||
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
|
||||||
})
|
|
||||||
|
|
||||||
const toTransparent = computed(() => {
|
|
||||||
let color = props.project.color
|
|
||||||
|
|
||||||
color >>>= 0
|
|
||||||
const b = color & 0xff
|
|
||||||
const g = (color >>> 8) & 0xff
|
|
||||||
const r = (color >>> 16) & 0xff
|
|
||||||
return (
|
|
||||||
'linear-gradient(rgba(' +
|
|
||||||
[r, g, b, 0.03].join(',') +
|
|
||||||
'), 65%, rgba(' +
|
|
||||||
[r, g, b, 0.3].join(',') +
|
|
||||||
'))'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
|
|
||||||
@click="router.push(`/project/${project.slug}`)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
|
||||||
:style="{
|
|
||||||
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
|
||||||
'background-image': `url(${
|
|
||||||
project.featured_gallery ??
|
|
||||||
project.gallery[0] ??
|
|
||||||
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
|
||||||
})`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="badges-wrapper"
|
|
||||||
:class="{
|
|
||||||
'no-image': !project.featured_gallery && !project.gallery[0],
|
|
||||||
}"
|
|
||||||
:style="{
|
|
||||||
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center gap-2 px-4 py-3">
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
<Avatar size="48px" :src="project.icon_url" />
|
|
||||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
|
||||||
<span class="line-clamp-2">{{ project.title }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
|
|
||||||
{{ project.description }}
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
{{ formatNumber(project.downloads) }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
|
||||||
>
|
|
||||||
<HeartIcon />
|
|
||||||
{{ formatNumber(project.follows) }}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1 pr-2">
|
|
||||||
<TagIcon />
|
|
||||||
<TagItem>
|
|
||||||
{{ formatCategory(featuredCategory) }}
|
|
||||||
</TagItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -1,73 +1,74 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { list } from '@/helpers/profile'
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
import { handleError } from '@/store/notifications'
|
import { Avatar, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { onUnmounted, ref } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
import { Avatar } from '@modrinth/ui'
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { list } from '@/helpers/profile'
|
||||||
import { SpinnerIcon } from '@modrinth/assets'
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const recentInstances = ref([])
|
const recentInstances = ref([])
|
||||||
const getInstances = async () => {
|
const getInstances = async () => {
|
||||||
const profiles = await list().catch(handleError)
|
const profiles = await list().catch(handleError)
|
||||||
|
|
||||||
recentInstances.value = profiles
|
recentInstances.value = profiles
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const dateACreated = dayjs(a.created)
|
const dateACreated = dayjs(a.created)
|
||||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||||
|
|
||||||
const dateBCreated = dayjs(b.created)
|
const dateBCreated = dayjs(b.created)
|
||||||
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
||||||
|
|
||||||
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
||||||
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
||||||
|
|
||||||
if (dateA.isSame(dateB)) {
|
if (dateA.isSame(dateB)) {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dateB - dateA
|
return dateB - dateA
|
||||||
})
|
})
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
await getInstances()
|
await getInstances()
|
||||||
|
|
||||||
const unlistenProfile = await profile_listener(async (event) => {
|
const unlistenProfile = await profile_listener(async (event) => {
|
||||||
if (event.event !== 'synced') {
|
if (event.event !== 'synced') {
|
||||||
await getInstances()
|
await getInstances()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProfile()
|
unlistenProfile()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NavButton
|
<div v-for="instance in recentInstances" :key="instance.id" v-tooltip.right="instance.name">
|
||||||
v-for="instance in recentInstances"
|
<NavButton :to="`/instance/${encodeURIComponent(instance.path)}`" class="relative">
|
||||||
:key="instance.id"
|
<Avatar
|
||||||
v-tooltip.right="instance.name"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
size="28px"
|
||||||
class="relative"
|
:tint-by="instance.path"
|
||||||
>
|
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||||
<Avatar
|
/>
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
<div
|
||||||
size="28px"
|
v-if="instance.install_stage !== 'installed'"
|
||||||
:tint-by="instance.path"
|
class="absolute inset-0 flex items-center justify-center z-10 pointer-events-none"
|
||||||
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
>
|
||||||
/>
|
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||||
<div
|
</div>
|
||||||
v-if="instance.install_stage !== 'installed'"
|
</NavButton>
|
||||||
class="absolute inset-0 flex items-center justify-center z-10"
|
</div>
|
||||||
>
|
<div
|
||||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
v-if="instances && recentInstances.length > 0"
|
||||||
</div>
|
class="h-px w-6 mx-auto my-2 bg-divider"
|
||||||
</NavButton>
|
></div>
|
||||||
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@@ -1,599 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="action-groups">
|
|
||||||
<ButtonStyled v-if="currentLoadingBars.length > 0" color="brand" type="transparent" circular>
|
|
||||||
<button ref="infoButton" @click="toggleCard()">
|
|
||||||
<DownloadIcon />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<div v-if="offline" class="status">
|
|
||||||
<UnplugIcon />
|
|
||||||
<div class="running-text">
|
|
||||||
<span> Offline </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedProcess" class="status">
|
|
||||||
<span class="circle running" />
|
|
||||||
<div ref="profileButton" class="running-text">
|
|
||||||
<router-link
|
|
||||||
class="text-primary"
|
|
||||||
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
|
||||||
>
|
|
||||||
{{ selectedProcess.profile.name }}
|
|
||||||
</router-link>
|
|
||||||
<div v-if="currentProcesses.length > 1" class="arrow button-base" :class="{ rotate: showProfiles }"
|
|
||||||
@click="toggleProfiles()">
|
|
||||||
<DropdownIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click="stop(selectedProcess)">
|
|
||||||
<StopCircleIcon />
|
|
||||||
</Button>
|
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
|
||||||
<TerminalSquareIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="status">
|
|
||||||
<span class="circle stopped" />
|
|
||||||
<span class="running-text"> No instances running </span>
|
|
||||||
</div>
|
|
||||||
<div v-if="updateState">
|
|
||||||
<a>
|
|
||||||
<Button class="download" :disabled="installState" @click="confirmUpdating(), getRemote(false, false)">
|
|
||||||
<DownloadIcon />
|
|
||||||
{{
|
|
||||||
installState
|
|
||||||
? "Downloading new update..."
|
|
||||||
: "Download new update"
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ModalWrapper ref="confirmUpdate" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="markdown-body">
|
|
||||||
<p>The new version of the AstralRinth launcher is available.</p>
|
|
||||||
<p>Your version is outdated. We recommend that you update to the latest version.</p>
|
|
||||||
<p><strong>⚠️ Warning ⚠️</strong></p>
|
|
||||||
<p>
|
|
||||||
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
|
|
||||||
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
|
|
||||||
your files, so you should always make copies of them and keep them in a safe place.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span>Source • Git Astralium</span>
|
|
||||||
<span>Version on remote server • <p id="releaseData" class="cosmic inline-fix"></p></span>
|
|
||||||
<span>Version on local device •
|
|
||||||
<p class="cosmic inline-fix">v{{ version }}</p>
|
|
||||||
</span>
|
|
||||||
<div class="button-group push-right">
|
|
||||||
<Button class="download-modal" @click="confirmUpdate.hide()">
|
|
||||||
Decline</Button>
|
|
||||||
<Button class="download-modal" @click="approveUpdate()">
|
|
||||||
Accept
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</div>
|
|
||||||
<transition name="download">
|
|
||||||
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
|
||||||
<div v-for="loadingBar in currentLoadingBars" :key="loadingBar.id" class="info-text">
|
|
||||||
<h3 class="info-title">
|
|
||||||
{{ loadingBar.title }}
|
|
||||||
</h3>
|
|
||||||
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
|
||||||
<div class="row">
|
|
||||||
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% {{ loadingBar.message }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</transition>
|
|
||||||
<transition name="download">
|
|
||||||
<Card v-if="showProfiles === true && currentProcesses.length > 0" ref="profiles" class="profile-card">
|
|
||||||
<Button v-for="process in currentProcesses" :key="process.uuid" class="profile-button"
|
|
||||||
@click="selectProcess(process)">
|
|
||||||
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
|
||||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click.stop="stop(process)">
|
|
||||||
<StopCircleIcon />
|
|
||||||
</Button>
|
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click.stop="goToTerminal(process.profile.path)">
|
|
||||||
<TerminalSquareIcon />
|
|
||||||
</Button>
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
DownloadIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
TerminalSquareIcon,
|
|
||||||
DropdownIcon,
|
|
||||||
UnplugIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { Button, ButtonStyled, Card } from '@modrinth/ui'
|
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
||||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
|
||||||
import { loading_listener, process_listener } from '@/helpers/events'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { progress_bars_list } from '@/helpers/state.js'
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { get_many } from '@/helpers/profile.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
|
||||||
|
|
||||||
const version = await getVersion()
|
|
||||||
|
|
||||||
import { installState, getRemote, updateState } from '@/helpers/update.js'
|
|
||||||
import ModalWrapper from './modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
const confirmUpdate = ref(null)
|
|
||||||
|
|
||||||
const confirmUpdating = async () => {
|
|
||||||
confirmUpdate.value.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const approveUpdate = async () => {
|
|
||||||
confirmUpdate.value.hide()
|
|
||||||
await getRemote(true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
await getRemote(true, false)
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const card = ref(null)
|
|
||||||
const profiles = ref(null)
|
|
||||||
const infoButton = ref(null)
|
|
||||||
const profileButton = ref(null)
|
|
||||||
const showCard = ref(false)
|
|
||||||
|
|
||||||
const showProfiles = ref(false)
|
|
||||||
|
|
||||||
const currentProcesses = ref([])
|
|
||||||
const selectedProcess = ref()
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
const processes = await getRunningProcesses().catch(handleError)
|
|
||||||
const profiles = await get_many(processes.map((x) => x.profile_path)).catch(handleError)
|
|
||||||
|
|
||||||
currentProcesses.value = processes.map((x) => ({
|
|
||||||
profile: profiles.find((prof) => x.profile_path === prof.path),
|
|
||||||
...x,
|
|
||||||
}))
|
|
||||||
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
|
|
||||||
selectedProcess.value = currentProcesses.value[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await refresh()
|
|
||||||
|
|
||||||
const offline = ref(!navigator.onLine)
|
|
||||||
window.addEventListener('offline', () => {
|
|
||||||
offline.value = true
|
|
||||||
})
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
offline.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
const unlistenProcess = await process_listener(async () => {
|
|
||||||
await refresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
const stop = async (process) => {
|
|
||||||
try {
|
|
||||||
await killProcess(process.uuid).catch(handleError)
|
|
||||||
|
|
||||||
trackEvent('InstanceStop', {
|
|
||||||
loader: process.profile.loader,
|
|
||||||
game_version: process.profile.game_version,
|
|
||||||
source: 'AppBar',
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToTerminal = (path) => {
|
|
||||||
router.push(`/instance/${encodeURIComponent(path ?? selectedProcess.value.profile.path)}/logs`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentLoadingBars = ref([])
|
|
||||||
|
|
||||||
const refreshInfo = async () => {
|
|
||||||
const currentLoadingBarCount = currentLoadingBars.value.length
|
|
||||||
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError)).map(
|
|
||||||
(x) => {
|
|
||||||
if (x.bar_type.type === 'java_download') {
|
|
||||||
x.title = 'Downloading Java ' + x.bar_type.version
|
|
||||||
}
|
|
||||||
if (x.bar_type.profile_path) {
|
|
||||||
x.title = x.bar_type.profile_path
|
|
||||||
}
|
|
||||||
if (x.bar_type.pack_name) {
|
|
||||||
x.title = x.bar_type.pack_name
|
|
||||||
}
|
|
||||||
|
|
||||||
return x
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
currentLoadingBars.value.sort((a, b) => {
|
|
||||||
if (a.loading_bar_uuid < b.loading_bar_uuid) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (a.loading_bar_uuid > b.loading_bar_uuid) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
if (currentLoadingBars.value.length === 0) {
|
|
||||||
showCard.value = false
|
|
||||||
} else if (currentLoadingBarCount < currentLoadingBars.value.length) {
|
|
||||||
showCard.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await refreshInfo()
|
|
||||||
const unlistenLoading = await loading_listener(async () => {
|
|
||||||
await refreshInfo()
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectProcess = (process) => {
|
|
||||||
selectedProcess.value = process
|
|
||||||
showProfiles.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClickOutsideCard = (event) => {
|
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
|
||||||
if (
|
|
||||||
card.value &&
|
|
||||||
card.value.$el !== event.target &&
|
|
||||||
!elements.includes(card.value.$el) &&
|
|
||||||
infoButton.value &&
|
|
||||||
!infoButton.value.contains(event.target)
|
|
||||||
) {
|
|
||||||
showCard.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClickOutsideProfile = (event) => {
|
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
|
||||||
if (
|
|
||||||
profiles.value &&
|
|
||||||
profiles.value.$el !== event.target &&
|
|
||||||
!elements.includes(profiles.value.$el) &&
|
|
||||||
!profileButton.value.contains(event.target)
|
|
||||||
) {
|
|
||||||
showProfiles.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleCard = async () => {
|
|
||||||
showCard.value = !showCard.value
|
|
||||||
showProfiles.value = false
|
|
||||||
await refreshInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleProfiles = async () => {
|
|
||||||
if (currentProcesses.value.length === 1) return
|
|
||||||
showProfiles.value = !showProfiles.value
|
|
||||||
showCard.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('click', handleClickOutsideCard)
|
|
||||||
window.addEventListener('click', handleClickOutsideProfile)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener('click', handleClickOutsideCard)
|
|
||||||
window.removeEventListener('click', handleClickOutsideProfile)
|
|
||||||
unlistenProcess()
|
|
||||||
unlistenLoading()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.inline-fix {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-top: -2rem;
|
|
||||||
margin-bottom: -2rem;
|
|
||||||
//margin-left: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cosmic {
|
|
||||||
color: #3e8cde;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
:deep(table) {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(hr),
|
|
||||||
:deep(h1),
|
|
||||||
:deep(h2) {
|
|
||||||
max-width: max(60rem, 90%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(ul),
|
|
||||||
:deep(ol) {
|
|
||||||
margin-left: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: var(--gap-lg);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download {
|
|
||||||
color: #3e8cde;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
// padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download:hover,
|
|
||||||
.download:focus,
|
|
||||||
.download:active {
|
|
||||||
color: #10fae5;
|
|
||||||
text-shadow: #26065e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-modal {
|
|
||||||
color: #3e8cde;
|
|
||||||
padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-modal:hover,
|
|
||||||
.download-modal:focus,
|
|
||||||
.download-modal:active {
|
|
||||||
color: #10fae5;
|
|
||||||
text-shadow: #26065e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-groups {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
transition: transform 0.2s ease-in-out;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&.rotate {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.running-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--gap-xs);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
/* Safari */
|
|
||||||
-ms-user-select: none;
|
|
||||||
/* IE 10 and IE 11 */
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&.clickable:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
|
|
||||||
&.running {
|
|
||||||
background-color: var(--color-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stopped {
|
|
||||||
background-color: var(--color-base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
box-shadow: none;
|
|
||||||
width: 1.25rem !important;
|
|
||||||
height: 1.25rem !important;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
min-width: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stop {
|
|
||||||
color: var(--color-red);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card {
|
|
||||||
position: absolute;
|
|
||||||
top: 3.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
z-index: 9;
|
|
||||||
width: 20rem;
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
box-shadow: var(--shadow-raised);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
overflow: auto;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
background-color: var(--color-raised-bg-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-icon {
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
:deep(svg) {
|
|
||||||
left: 1rem;
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-enter-active,
|
|
||||||
.download-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-enter-from,
|
|
||||||
.download-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-title {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-card {
|
|
||||||
position: absolute;
|
|
||||||
top: 3.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
z-index: 9;
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
box-shadow: var(--shadow-raised);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: auto;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
padding: var(--gap-md);
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-text);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('open')
|
|
||||||
$router.push({
|
|
||||||
path: `/project/${project.project_id ?? project.id}`,
|
|
||||||
query: { i: props.instance ? props.instance.path : undefined },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="icon w-[96px] h-[96px] relative">
|
|
||||||
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 overflow-hidden">
|
|
||||||
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
|
||||||
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
|
|
||||||
{{ project.title }}
|
|
||||||
</span>
|
|
||||||
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="m-0 line-clamp-2">
|
|
||||||
{{ project.description }}
|
|
||||||
</div>
|
|
||||||
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
|
||||||
<TagsIcon class="h-4 w-4 shrink-0" />
|
|
||||||
<div
|
|
||||||
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
|
|
||||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
|
||||||
>
|
|
||||||
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
|
||||||
Client or server
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-else-if="
|
|
||||||
(project.client_side === 'optional' || project.client_side === 'required') &&
|
|
||||||
(project.server_side === 'optional' || project.server_side === 'unsupported')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Client
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-else-if="
|
|
||||||
(project.server_side === 'optional' || project.server_side === 'required') &&
|
|
||||||
(project.client_side === 'optional' || project.client_side === 'unsupported')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Server
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-else-if="
|
|
||||||
project.client_side === 'unsupported' && project.server_side === 'unsupported'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Unsupported
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
|
|
||||||
>
|
|
||||||
Client and server
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="tag in categories"
|
|
||||||
:key="tag"
|
|
||||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
|
||||||
>
|
|
||||||
{{ formatCategory(tag.name) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DownloadIcon class="shrink-0" />
|
|
||||||
<span>
|
|
||||||
{{ formatNumber(project.downloads) }}
|
|
||||||
<span class="text-secondary">downloads</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<HeartIcon class="shrink-0" />
|
|
||||||
<span>
|
|
||||||
{{ formatNumber(project.follows ?? project.followers) }}
|
|
||||||
<span class="text-secondary">followers</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-auto relative">
|
|
||||||
<div class="absolute bottom-0 right-0 w-fit">
|
|
||||||
<ButtonStyled color="brand" type="outlined">
|
|
||||||
<button
|
|
||||||
:disabled="installed || installing"
|
|
||||||
class="shrink-0 no-wrap"
|
|
||||||
@click.stop="install()"
|
|
||||||
>
|
|
||||||
<template v-if="!installed">
|
|
||||||
<DownloadIcon v-if="modpack || instance" />
|
|
||||||
<PlusIcon v-else />
|
|
||||||
</template>
|
|
||||||
<CheckIcon v-else />
|
|
||||||
{{
|
|
||||||
installing
|
|
||||||
? 'Installing'
|
|
||||||
: installed
|
|
||||||
? 'Installed'
|
|
||||||
: modpack || instance
|
|
||||||
? 'Install'
|
|
||||||
: 'Add to an instance'
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
|
|
||||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
dayjs.extend(relativeTime)
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
backgroundImage: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
categories: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
instance: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
featured: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
installed: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['open', 'install'])
|
|
||||||
|
|
||||||
const installing = ref(false)
|
|
||||||
|
|
||||||
async function install() {
|
|
||||||
installing.value = true
|
|
||||||
await installVersion(
|
|
||||||
props.project.project_id ?? props.project.id,
|
|
||||||
null,
|
|
||||||
props.instance ? props.instance.path : null,
|
|
||||||
'SearchCard',
|
|
||||||
() => {
|
|
||||||
installing.value = false
|
|
||||||
emit('install', props.project.project_id ?? props.project.id)
|
|
||||||
},
|
|
||||||
(profile) => {
|
|
||||||
router.push(`/instance/${profile}`)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const modpack = computed(() => props.project.project_type === 'modpack')
|
|
||||||
</script>
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,100 +1,114 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Button } from '@modrinth/ui'
|
import { ButtonStyled, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import SearchCard from '@/components/ui/SearchCard.vue'
|
|
||||||
import { get_categories } from '@/helpers/tags.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { get_version, get_project } from '@/helpers/cache.js'
|
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { get_project_v3, get_version } from '@/helpers/cache.js'
|
||||||
|
import { injectContentInstall } from '@/providers/content-install'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
const { install: installVersion } = injectContentInstall()
|
||||||
|
|
||||||
const confirmModal = ref(null)
|
const confirmModal = ref(null)
|
||||||
const project = ref(null)
|
const project = ref(null)
|
||||||
const version = ref(null)
|
const version = ref(null)
|
||||||
const categories = ref(null)
|
|
||||||
const installing = ref(false)
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
async show(event) {
|
async show(event) {
|
||||||
if (event.event === 'InstallVersion') {
|
if (event.event === 'InstallVersion') {
|
||||||
version.value = await get_version(event.id, 'must_revalidate').catch(handleError)
|
version.value = await get_version(event.id, 'must_revalidate').catch(handleError)
|
||||||
project.value = await get_project(version.value.project_id, 'must_revalidate').catch(
|
project.value = await get_project_v3(version.value.project_id, 'must_revalidate').catch(
|
||||||
handleError,
|
handleError,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
project.value = await get_project(event.id, 'must_revalidate').catch(handleError)
|
project.value = await get_project_v3(event.id, 'must_revalidate').catch(handleError)
|
||||||
version.value = await get_version(
|
version.value = await get_version(
|
||||||
project.value.versions[project.value.versions.length - 1],
|
project.value.versions[project.value.versions.length - 1],
|
||||||
'must_revalidate',
|
'must_revalidate',
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
categories.value = (await get_categories().catch(handleError)).filter(
|
confirmModal.value.show()
|
||||||
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
|
},
|
||||||
)
|
|
||||||
confirmModal.value.show()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
confirmModal.value.hide()
|
confirmModal.value.hide()
|
||||||
await installVersion(project.value.id, version.value.id, null, 'URLConfirmModal')
|
await installVersion(
|
||||||
|
project.value.id,
|
||||||
|
version.value.id,
|
||||||
|
null,
|
||||||
|
'URLConfirmModal',
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
|
<ModalWrapper ref="confirmModal" :header="`Install ${project?.name}`">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<SearchCard
|
<ProjectCard
|
||||||
:project="project"
|
:title="project.name"
|
||||||
class="project-card"
|
:link="() => confirmModal.hide()"
|
||||||
:categories="categories"
|
:icon-url="project.icon_url"
|
||||||
@open="confirmModal.hide()"
|
:summary="project.summary"
|
||||||
/>
|
:tags="project.display_categories"
|
||||||
<div class="button-row">
|
:all-tags="project.categories"
|
||||||
<div class="markdown-body">
|
:downloads="project.downloads"
|
||||||
<p>
|
:followers="project.follows"
|
||||||
Installing <code>{{ version.id }}</code> from Modrinth
|
:date-updated="project.date_modified"
|
||||||
</p>
|
:banner="project.featured_gallery ?? undefined"
|
||||||
</div>
|
:color="project.color ?? undefined"
|
||||||
<div class="button-group">
|
layout="list"
|
||||||
<Button :loading="installing" color="primary" @click="install">Install</Button>
|
class="project-card"
|
||||||
</div>
|
/>
|
||||||
</div>
|
<div class="button-row">
|
||||||
</div>
|
<div class="markdown-body">
|
||||||
</ModalWrapper>
|
<p>
|
||||||
|
Installing <code>{{ version.id }}</code> from Modrinth
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="install">Install</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card {
|
.project-card {
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
:deep(.badge) {
|
:deep(.badge) {
|
||||||
border: 1px solid var(--color-raised-bg);
|
border: 1px solid var(--color-raised-bg);
|
||||||
background-color: var(--color-accent-contrast);
|
background-color: var(--color-accent-contrast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<section
|
||||||
|
v-if="showControls"
|
||||||
|
class="flex items-center gap-2 mr-1.5"
|
||||||
|
data-tauri-drag-region-exclude
|
||||||
|
>
|
||||||
|
<ButtonStyled type="transparent" circular>
|
||||||
|
<button class="relative expanded-button" @click="() => getCurrentWindow().minimize()">
|
||||||
|
<MinimizeIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled type="transparent" circular>
|
||||||
|
<button class="relative expanded-button" @click="() => getCurrentWindow().toggleMaximize()">
|
||||||
|
<RestoreIcon v-if="isMaximized" />
|
||||||
|
<MaximizeIcon v-else />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled
|
||||||
|
type="transparent"
|
||||||
|
color="red"
|
||||||
|
color-fill="none"
|
||||||
|
hover-color-fill="background"
|
||||||
|
circular
|
||||||
|
>
|
||||||
|
<button class="relative expanded-button close-button" @click="handleClose">
|
||||||
|
<XIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { MaximizeIcon, MinimizeIcon, RestoreIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { ButtonStyled } from '@modrinth/ui'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import { get as getSettings } from '@/helpers/settings.ts'
|
||||||
|
import { getOS } from '@/helpers/utils.js'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
const nativeDecorations = ref(true)
|
||||||
|
const isMaximized = ref(false)
|
||||||
|
const os = ref('')
|
||||||
|
|
||||||
|
const alwaysShowAppControls = computed(() => themeStore.getFeatureFlag('always_show_app_controls'))
|
||||||
|
|
||||||
|
const showControls = computed(
|
||||||
|
() =>
|
||||||
|
alwaysShowAppControls.value ||
|
||||||
|
(!nativeDecorations.value && (os.value === 'Windows' || os.value === 'Linux')),
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
os.value = await getOS()
|
||||||
|
|
||||||
|
const settings = await getSettings()
|
||||||
|
nativeDecorations.value = settings.native_decorations
|
||||||
|
|
||||||
|
if (os.value !== 'MacOS') {
|
||||||
|
await getCurrentWindow().setDecorations(nativeDecorations.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||||
|
|
||||||
|
const unlisten = await getCurrentWindow().onResized(async () => {
|
||||||
|
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unlisten()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = async () => {
|
||||||
|
await saveWindowState(StateFlags.ALL)
|
||||||
|
await getCurrentWindow().close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.expanded-button::before {
|
||||||
|
inset: -9px -6px;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-button.close-button::before {
|
||||||
|
inset: -9px -9px -9px -6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, Combobox, defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import {
|
||||||
|
downloadLatestRelease,
|
||||||
|
getAvailableInstallers,
|
||||||
|
isUpdateInstalling,
|
||||||
|
LAUNCHER_RELEASES_URL,
|
||||||
|
LAUNCHER_REPOSITORY_URL,
|
||||||
|
latestLauncherReleases,
|
||||||
|
} from '@/helpers/astralrinth/update'
|
||||||
|
|
||||||
|
type ModalHandle = {
|
||||||
|
hide: () => void
|
||||||
|
show: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
version: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const updateModalView = ref<ModalHandle | null>(null)
|
||||||
|
const updateRequestFailView = ref<ModalHandle | null>(null)
|
||||||
|
const selectedInstallerName = ref<string | null>(null)
|
||||||
|
|
||||||
|
const releaseTag = computed(() => latestLauncherReleases.value?.tag_name ?? '')
|
||||||
|
const releaseTitle = computed(() => latestLauncherReleases.value?.name ?? '')
|
||||||
|
const availableInstallers = computed(() => getAvailableInstallers())
|
||||||
|
const selectedInstaller = computed(
|
||||||
|
() =>
|
||||||
|
availableInstallers.value.find((installer) => installer.name === selectedInstallerName.value) ??
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
const selectedInstallerUrl = computed(() => selectedInstaller.value?.browser_download_url ?? null)
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
updateHeader: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.header',
|
||||||
|
defaultMessage: 'AstralRinth launcher update',
|
||||||
|
},
|
||||||
|
updateTitle: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.title',
|
||||||
|
defaultMessage: 'A new version of the AstralRinth launcher is available.',
|
||||||
|
},
|
||||||
|
updateDescription: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.description',
|
||||||
|
defaultMessage:
|
||||||
|
'You are using an older version. We recommend updating now for the latest fixes and improvements.',
|
||||||
|
},
|
||||||
|
updateNoticeTitle: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.notice-title',
|
||||||
|
defaultMessage: '⚠️ Before you continue',
|
||||||
|
},
|
||||||
|
updateNoticeLead: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.notice-lead',
|
||||||
|
defaultMessage:
|
||||||
|
'Save your work, close all running launcher instances, and back up your launcher data before installing the update.',
|
||||||
|
},
|
||||||
|
updateNoticeWindows: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.notice-windows',
|
||||||
|
defaultMessage: 'On Windows, important data may be stored in',
|
||||||
|
},
|
||||||
|
updateNoticeMacos: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.notice-macos',
|
||||||
|
defaultMessage: 'On macOS, important data may be stored in',
|
||||||
|
},
|
||||||
|
updateNoticeOutro: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.notice-outro',
|
||||||
|
defaultMessage: 'To avoid data loss, keep a backup copy in a safe place before continuing.',
|
||||||
|
},
|
||||||
|
installerTitle: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.installer-title',
|
||||||
|
defaultMessage: 'Installer type',
|
||||||
|
},
|
||||||
|
installerDescription: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.installer-description',
|
||||||
|
defaultMessage: 'Choose the installer package you want to continue with.',
|
||||||
|
},
|
||||||
|
selectInstaller: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.select-installer',
|
||||||
|
defaultMessage: 'Select an installer',
|
||||||
|
},
|
||||||
|
latestReleaseTag: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.latest-release-tag',
|
||||||
|
defaultMessage: '☁️ Latest release tag:',
|
||||||
|
},
|
||||||
|
latestReleaseTitle: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.latest-release-title',
|
||||||
|
defaultMessage: '☁️ Latest release title:',
|
||||||
|
},
|
||||||
|
installedVersion: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.installed-version',
|
||||||
|
defaultMessage: '💾 Installed & Running version:',
|
||||||
|
},
|
||||||
|
repositoryLink: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.repository-link',
|
||||||
|
defaultMessage: 'Open the project repository',
|
||||||
|
},
|
||||||
|
cancelAction: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.cancel-action',
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
},
|
||||||
|
downloadAction: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.update.download-action',
|
||||||
|
defaultMessage: 'Download update',
|
||||||
|
},
|
||||||
|
errorHeader: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.error.header',
|
||||||
|
defaultMessage: 'Could not download the update',
|
||||||
|
},
|
||||||
|
errorTitle: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.error.title',
|
||||||
|
defaultMessage: 'Download failed',
|
||||||
|
},
|
||||||
|
errorDescription: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.error.description',
|
||||||
|
defaultMessage: 'AstralRinth could not download the update file from the server.',
|
||||||
|
},
|
||||||
|
errorHelpText: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.error.help-text',
|
||||||
|
defaultMessage: 'You can try downloading it manually from',
|
||||||
|
},
|
||||||
|
errorHelpLink: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.error.help-link',
|
||||||
|
defaultMessage: 'AstralRinth repository releases',
|
||||||
|
},
|
||||||
|
errorHelpSuffix: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.error.help-suffix',
|
||||||
|
defaultMessage: 'if a newer release is available there.',
|
||||||
|
},
|
||||||
|
localVersion: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.error.local-version',
|
||||||
|
defaultMessage: 'Local AstralRinth:',
|
||||||
|
},
|
||||||
|
closeAction: {
|
||||||
|
id: 'astralrinth.app.launcher-update-modal.error.close-action',
|
||||||
|
defaultMessage: 'Close',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
availableInstallers,
|
||||||
|
(installers) => {
|
||||||
|
const hasSelectedInstaller = installers.some(
|
||||||
|
(installer) => installer.name === selectedInstallerName.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasSelectedInstaller) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedInstallerName.value = installers.length === 1 ? installers[0].name : null
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
async function show() {
|
||||||
|
updateModalView.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initDownload() {
|
||||||
|
updateModalView.value?.hide()
|
||||||
|
const result = await downloadLatestRelease(selectedInstaller.value)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
updateRequestFailView.value?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
hide: () => updateModalView.value?.hide(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="updateModalView"
|
||||||
|
:has-to-type="false"
|
||||||
|
:header="formatMessage(messages.updateHeader)"
|
||||||
|
>
|
||||||
|
<div class="space-y-3 pb-16">
|
||||||
|
<div class="space-y-1 rounded-2xl border border-solid border-[rgba(255,255,255,0.12)] p-3">
|
||||||
|
<p class="m-0 text-base">
|
||||||
|
<strong>{{ formatMessage(messages.updateTitle) }}</strong>
|
||||||
|
</p>
|
||||||
|
<p class="m-0 text-secondary">{{ formatMessage(messages.updateDescription) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="space-y-2 rounded-2xl border border-solid border-[rgba(255,255,255,0.12)] bg-[rgba(255,255,255,0.03)] p-3"
|
||||||
|
>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="m-0">
|
||||||
|
<strong class="neon-text">{{ formatMessage(messages.updateNoticeTitle) }}</strong>
|
||||||
|
</p>
|
||||||
|
<p class="m-0 text-secondary text-sm">{{ formatMessage(messages.updateNoticeLead) }}</p>
|
||||||
|
<p class="m-0 text-sm">
|
||||||
|
{{ formatMessage(messages.updateNoticeWindows) }}
|
||||||
|
<code class="neon-text">%appdata%\Roaming\AstralRinthApp</code>
|
||||||
|
</p>
|
||||||
|
<p class="m-0 text-sm">
|
||||||
|
{{ formatMessage(messages.updateNoticeMacos) }}
|
||||||
|
<code class="neon-text">~/Library/Application Support/AstralRinthApp</code>
|
||||||
|
</p>
|
||||||
|
<p class="m-0 text-sm">{{ formatMessage(messages.updateNoticeOutro) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="space-y-2 rounded-2xl border border-solid border-[rgba(255,255,255,0.12)] p-3 text-sm text-secondary"
|
||||||
|
>
|
||||||
|
<p class="m-0">
|
||||||
|
<strong>{{ formatMessage(messages.latestReleaseTag) }}</strong>
|
||||||
|
<span class="neon-text">{{ releaseTag }}</span>
|
||||||
|
<br />
|
||||||
|
<strong>{{ formatMessage(messages.latestReleaseTitle) }}</strong>
|
||||||
|
<span class="neon-text">{{ releaseTitle }}</span>
|
||||||
|
<br />
|
||||||
|
<strong>{{ formatMessage(messages.installedVersion) }}</strong>
|
||||||
|
<span class="neon-text">v{{ props.version }}</span>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
class="inline-flex neon-text"
|
||||||
|
:href="LAUNCHER_REPOSITORY_URL"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.repositoryLink) }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 rounded-2xl border border-solid border-[rgba(255,255,255,0.12)] p-3">
|
||||||
|
<div>
|
||||||
|
<p class="m-0 text-base">
|
||||||
|
<strong>{{ formatMessage(messages.installerTitle) }}</strong>
|
||||||
|
</p>
|
||||||
|
<p class="m-0 text-secondary text-sm">
|
||||||
|
{{ formatMessage(messages.installerDescription) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Combobox
|
||||||
|
v-model="selectedInstallerName"
|
||||||
|
name="AstralRinth launcher installer"
|
||||||
|
:options="
|
||||||
|
availableInstallers.map((installer) => ({
|
||||||
|
value: installer.name,
|
||||||
|
label: installer.name,
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:display-value="selectedInstallerName ?? formatMessage(messages.selectInstaller)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
|
||||||
|
<Button class="bordered" @click="updateModalView?.hide()">
|
||||||
|
{{ formatMessage(messages.cancelAction) }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="bordered"
|
||||||
|
:disabled="isUpdateInstalling || !selectedInstallerUrl"
|
||||||
|
@click="initDownload()"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.downloadAction) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
|
||||||
|
<ModalWrapper
|
||||||
|
ref="updateRequestFailView"
|
||||||
|
:has-to-type="false"
|
||||||
|
:header="formatMessage(messages.errorHeader)"
|
||||||
|
>
|
||||||
|
<div class="space-y-3 pb-16">
|
||||||
|
<div class="space-y-2 rounded-2xl border border-solid border-[rgba(255,255,255,0.12)] p-3">
|
||||||
|
<p>
|
||||||
|
<strong>{{ formatMessage(messages.errorTitle) }}</strong>
|
||||||
|
</p>
|
||||||
|
<p class="m-0 text-secondary">{{ formatMessage(messages.errorDescription) }}</p>
|
||||||
|
<p class="m-0 text-sm">
|
||||||
|
{{ formatMessage(messages.errorHelpText) }}
|
||||||
|
<a
|
||||||
|
class="neon-text"
|
||||||
|
:href="LAUNCHER_RELEASES_URL"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.errorHelpLink) }}
|
||||||
|
</a>
|
||||||
|
{{ formatMessage(messages.errorHelpSuffix) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-solid border-[rgba(255,255,255,0.12)] p-3 text-sm text-secondary"
|
||||||
|
>
|
||||||
|
<p class="m-0">
|
||||||
|
<strong>{{ formatMessage(messages.localVersion) }}</strong>
|
||||||
|
<span class="neon-text">v{{ props.version }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
|
||||||
|
<Button class="bordered" @click="updateRequestFailView?.hide()">
|
||||||
|
{{ formatMessage(messages.closeAction) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../../../../packages/assets/styles/astralrinth/neon-button.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/astralrinth/neon-text.scss';
|
||||||
|
</style>
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
|
type ModalHandle = {
|
||||||
|
hide: () => void
|
||||||
|
show: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
maxOfflinePlayerNameLength: number
|
||||||
|
minOfflinePlayerNameLength: number
|
||||||
|
nameExp: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'retry-elyby'): void
|
||||||
|
(event: 'retry-offline'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const authenticationElyByErrorModal = ref<ModalHandle | null>(null)
|
||||||
|
const inputElyByErrorModal = ref<ModalHandle | null>(null)
|
||||||
|
const inputOfflineErrorModal = ref<ModalHandle | null>(null)
|
||||||
|
const unexpectedErrorModal = ref<ModalHandle | null>(null)
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
authenticationElyByHeader: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.authentication-elyby.header',
|
||||||
|
defaultMessage: 'Error while proceeding authentication event with Ely.by',
|
||||||
|
},
|
||||||
|
authenticationElyByDescription: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.authentication-elyby.description',
|
||||||
|
defaultMessage: 'An error occurred while logging in.',
|
||||||
|
},
|
||||||
|
inputElyByHeader: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-elyby.header',
|
||||||
|
defaultMessage: 'Error while proceeding input event with Ely.by',
|
||||||
|
},
|
||||||
|
inputElyByDescription: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-elyby.description',
|
||||||
|
defaultMessage:
|
||||||
|
'An error occurred while adding the Ely.by account. Please follow the instructions below.',
|
||||||
|
},
|
||||||
|
inputElyByNameOrEmailHint: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-elyby.name-or-email-hint',
|
||||||
|
defaultMessage: 'Check that you have entered the correct player name or email.',
|
||||||
|
},
|
||||||
|
inputElyByPasswordHint: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-elyby.password-hint',
|
||||||
|
defaultMessage: 'Check that you have entered the correct password.',
|
||||||
|
},
|
||||||
|
inputOfflineHeader: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-offline.header',
|
||||||
|
defaultMessage: 'Error while proceeding input event with offline account',
|
||||||
|
},
|
||||||
|
inputOfflineDescription: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-offline.description',
|
||||||
|
defaultMessage:
|
||||||
|
'An error occurred while adding the offline account. Please follow the instructions below.',
|
||||||
|
},
|
||||||
|
inputOfflineNameHint: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-offline.name-hint',
|
||||||
|
defaultMessage: 'Check that you have entered the correct player name.',
|
||||||
|
},
|
||||||
|
inputOfflineLengthHint: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-offline.length-hint',
|
||||||
|
defaultMessage:
|
||||||
|
'Player name must be at least {min} characters long and no more than {max} characters.',
|
||||||
|
},
|
||||||
|
inputOfflineFormatHint: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.input-offline.format-hint',
|
||||||
|
defaultMessage: 'Make sure your name meets the format requirement `{nameExp}`',
|
||||||
|
},
|
||||||
|
unexpectedHeader: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.unexpected.header',
|
||||||
|
defaultMessage: 'Unexpected error occurred',
|
||||||
|
},
|
||||||
|
unexpectedDescription: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.unexpected.description',
|
||||||
|
defaultMessage: 'An unexpected error has occurred. Please try again later.',
|
||||||
|
},
|
||||||
|
retryAction: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.error.retry-action',
|
||||||
|
defaultMessage: 'Try again',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
hideAuthenticationElyByError: () => authenticationElyByErrorModal.value?.hide(),
|
||||||
|
hideInputElyByError: () => inputElyByErrorModal.value?.hide(),
|
||||||
|
hideInputOfflineError: () => inputOfflineErrorModal.value?.hide(),
|
||||||
|
showAuthenticationElyByError: () => authenticationElyByErrorModal.value?.show(),
|
||||||
|
showInputElyByError: () => inputElyByErrorModal.value?.show(),
|
||||||
|
showInputOfflineError: () => inputOfflineErrorModal.value?.show(),
|
||||||
|
showUnexpectedError: () => unexpectedErrorModal.value?.show(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="authenticationElyByErrorModal"
|
||||||
|
class="modal"
|
||||||
|
:header="formatMessage(messages.authenticationElyByHeader)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
{{ formatMessage(messages.authenticationElyByDescription) }}
|
||||||
|
</label>
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" @click="emit('retry-elyby')">
|
||||||
|
{{ formatMessage(messages.retryAction) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="inputElyByErrorModal"
|
||||||
|
class="modal"
|
||||||
|
:header="formatMessage(messages.inputElyByHeader)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
{{ formatMessage(messages.inputElyByDescription) }}
|
||||||
|
</label>
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>{{ formatMessage(messages.inputElyByNameOrEmailHint) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.inputElyByPasswordHint) }}</li>
|
||||||
|
</ul>
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" @click="emit('retry-elyby')">
|
||||||
|
{{ formatMessage(messages.retryAction) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="inputOfflineErrorModal"
|
||||||
|
class="modal"
|
||||||
|
:header="formatMessage(messages.inputOfflineHeader)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
{{ formatMessage(messages.inputOfflineDescription) }}
|
||||||
|
</label>
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>{{ formatMessage(messages.inputOfflineNameHint) }}</li>
|
||||||
|
<li>
|
||||||
|
{{
|
||||||
|
formatMessage(messages.inputOfflineLengthHint, {
|
||||||
|
min: minOfflinePlayerNameLength,
|
||||||
|
max: maxOfflinePlayerNameLength,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ formatMessage(messages.inputOfflineFormatHint, { nameExp }) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" @click="emit('retry-offline')">
|
||||||
|
{{ formatMessage(messages.retryAction) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="unexpectedErrorModal"
|
||||||
|
class="modal"
|
||||||
|
:header="formatMessage(messages.unexpectedHeader)"
|
||||||
|
>
|
||||||
|
<div class="modal-body">
|
||||||
|
<label class="label">{{ formatMessage(messages.unexpectedDescription) }}</label>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.modal {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--gap-lg);
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--gap-xl);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
|
type ModalHandle = {
|
||||||
|
hide: () => void
|
||||||
|
show: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
elyByLoginDisabled: boolean
|
||||||
|
elyByLoginValue: string
|
||||||
|
elyByPassword: string
|
||||||
|
elyByTwoFactorCode: string
|
||||||
|
offlineLoginDisabled: boolean
|
||||||
|
offlinePlayerName: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'submit-elyby'): void
|
||||||
|
(event: 'submit-offline'): void
|
||||||
|
(event: 'update:elyByLoginValue', value: string): void
|
||||||
|
(event: 'update:elyByPassword', value: string): void
|
||||||
|
(event: 'update:elyByTwoFactorCode', value: string): void
|
||||||
|
(event: 'update:offlinePlayerName', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const addOfflineModal = ref<ModalHandle | null>(null)
|
||||||
|
const addElyByModal = ref<ModalHandle | null>(null)
|
||||||
|
const requestElyByTwoFactorCodeModal = ref<ModalHandle | null>(null)
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
addElyByHeader: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.header',
|
||||||
|
defaultMessage: 'Authenticate with Ely.by',
|
||||||
|
},
|
||||||
|
requestTwoFactorHeader: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.two-factor.header',
|
||||||
|
defaultMessage: 'Ely.by requested 2FA code for authentication',
|
||||||
|
},
|
||||||
|
requestTwoFactorLabel: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.two-factor.label',
|
||||||
|
defaultMessage: 'Enter your 2FA code',
|
||||||
|
},
|
||||||
|
requestTwoFactorPlaceholder: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.two-factor.placeholder',
|
||||||
|
defaultMessage: 'Your 2FA code here...',
|
||||||
|
},
|
||||||
|
continueAction: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.two-factor.continue-action',
|
||||||
|
defaultMessage: 'Continue',
|
||||||
|
},
|
||||||
|
elyByLoginLabel: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.login.label',
|
||||||
|
defaultMessage: 'Enter your player name or email (preferred)',
|
||||||
|
},
|
||||||
|
elyByLoginPlaceholder: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.login.placeholder',
|
||||||
|
defaultMessage: 'Your player name or email here...',
|
||||||
|
},
|
||||||
|
elyByPasswordLabel: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.password.label',
|
||||||
|
defaultMessage: 'Enter your password',
|
||||||
|
},
|
||||||
|
elyByPasswordPlaceholder: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.elyby.password.placeholder',
|
||||||
|
defaultMessage: 'Your password here...',
|
||||||
|
},
|
||||||
|
loginAction: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.login-action',
|
||||||
|
defaultMessage: 'Login',
|
||||||
|
},
|
||||||
|
addOfflineHeader: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.offline.header',
|
||||||
|
defaultMessage: 'Add new offline account',
|
||||||
|
},
|
||||||
|
offlineNameLabel: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.offline.name.label',
|
||||||
|
defaultMessage: 'Enter your player name',
|
||||||
|
},
|
||||||
|
offlineNamePlaceholder: {
|
||||||
|
id: 'astralrinth.app.minecraft-account.input.offline.name.placeholder',
|
||||||
|
defaultMessage: 'Your player name here...',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
hideElyBy: () => addElyByModal.value?.hide(),
|
||||||
|
hideElyByTwoFactor: () => requestElyByTwoFactorCodeModal.value?.hide(),
|
||||||
|
hideOffline: () => addOfflineModal.value?.hide(),
|
||||||
|
showElyBy: () => addElyByModal.value?.show(),
|
||||||
|
showElyByTwoFactor: () => requestElyByTwoFactorCodeModal.value?.show(),
|
||||||
|
showOffline: () => addOfflineModal.value?.show(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalWrapper ref="addElyByModal" class="modal" :header="formatMessage(messages.addElyByHeader)">
|
||||||
|
<ModalWrapper
|
||||||
|
ref="requestElyByTwoFactorCodeModal"
|
||||||
|
class="modal"
|
||||||
|
:header="formatMessage(messages.requestTwoFactorHeader)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="label form-label">{{ formatMessage(messages.requestTwoFactorLabel) }}</label>
|
||||||
|
<input
|
||||||
|
:value="props.elyByTwoFactorCode"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.requestTwoFactorPlaceholder)"
|
||||||
|
class="input soft-input"
|
||||||
|
@input="
|
||||||
|
emit('update:elyByTwoFactorCode', ($event.target as HTMLInputElement).value)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" :disabled="props.elyByLoginDisabled" @click="emit('submit-elyby')">
|
||||||
|
{{ formatMessage(messages.continueAction) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="label form-label">{{ formatMessage(messages.elyByLoginLabel) }}</label>
|
||||||
|
<input
|
||||||
|
:value="props.elyByLoginValue"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.elyByLoginPlaceholder)"
|
||||||
|
class="input soft-input"
|
||||||
|
@input="emit('update:elyByLoginValue', ($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
<label class="label form-label">{{ formatMessage(messages.elyByPasswordLabel) }}</label>
|
||||||
|
<input
|
||||||
|
:value="props.elyByPassword"
|
||||||
|
type="password"
|
||||||
|
:placeholder="formatMessage(messages.elyByPasswordPlaceholder)"
|
||||||
|
class="input soft-input"
|
||||||
|
@input="emit('update:elyByPassword', ($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" :disabled="props.elyByLoginDisabled" @click="emit('submit-elyby')">
|
||||||
|
{{ formatMessage(messages.loginAction) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="addOfflineModal"
|
||||||
|
class="modal"
|
||||||
|
:header="formatMessage(messages.addOfflineHeader)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="label form-label">{{ formatMessage(messages.offlineNameLabel) }}</label>
|
||||||
|
<input
|
||||||
|
:value="props.offlinePlayerName"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.offlineNamePlaceholder)"
|
||||||
|
class="input soft-input"
|
||||||
|
@input="emit('update:offlinePlayerName', ($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" :disabled="props.offlineLoginDisabled" @click="emit('submit-offline')">
|
||||||
|
{{ formatMessage(messages.loginAction) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import '../../../../../../../../packages/assets/styles/astralrinth/soft-inputs.scss';
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,361 +1,405 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Avatar, ButtonStyled, OverflowMenu, useRelativeTime } from '@modrinth/ui'
|
import { MailIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
UserPlusIcon,
|
Avatar,
|
||||||
MoreVerticalIcon,
|
ButtonStyled,
|
||||||
MailIcon,
|
defineMessages,
|
||||||
SettingsIcon,
|
injectNotificationManager,
|
||||||
TrashIcon,
|
IntlFormatted,
|
||||||
XIcon,
|
StyledInput,
|
||||||
} from '@modrinth/assets'
|
useRelativeTime,
|
||||||
import { ref, onUnmounted, watch, computed } from 'vue'
|
useVIntl,
|
||||||
import { friend_listener } from '@/helpers/events'
|
} from '@modrinth/ui'
|
||||||
import { friends, friend_statuses, add_friend, remove_friend } from '@/helpers/friends'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
import { get_user_many } from '@/helpers/cache'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import type { Dayjs } from 'dayjs'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
|
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { friend_listener } from '@/helpers/events'
|
||||||
|
import {
|
||||||
|
add_friend,
|
||||||
|
friends,
|
||||||
|
type FriendWithUserData,
|
||||||
|
remove_friend,
|
||||||
|
transformFriends,
|
||||||
|
} from '@/helpers/friends.ts'
|
||||||
|
import type { ModrinthCredentials } from '@/helpers/mr_auth'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
credentials: unknown | null
|
credentials: ModrinthCredentials | null
|
||||||
signIn: () => void
|
signIn: () => void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const userCredentials = computed(() => props.credentials)
|
const userCredentials = computed(() => props.credentials)
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const manageFriendsModal = ref()
|
|
||||||
const friendInvitesModal = ref()
|
const friendInvitesModal = ref()
|
||||||
|
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
const addFriendModal = ref()
|
const addFriendModal = ref()
|
||||||
async function addFriendFromModal() {
|
async function addFriendFromModal() {
|
||||||
addFriendModal.value.hide()
|
addFriendModal.value.hide()
|
||||||
await add_friend(username.value).catch(handleError)
|
await add_friend(username.value).catch(handleError)
|
||||||
username.value = ''
|
username.value = ''
|
||||||
await loadFriends()
|
await loadFriends()
|
||||||
}
|
}
|
||||||
|
|
||||||
const friendOptions = ref()
|
async function addFriend(friend: FriendWithUserData) {
|
||||||
async function handleFriendOptions(args) {
|
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
|
||||||
switch (args.option) {
|
if (id) {
|
||||||
case 'remove-friend':
|
await add_friend(id).catch(handleError)
|
||||||
await removeFriend(args.item)
|
await loadFriends()
|
||||||
break
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addFriend(friend: Friend) {
|
async function removeFriend(friend: FriendWithUserData) {
|
||||||
await add_friend(
|
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
|
||||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
if (id) {
|
||||||
).catch(handleError)
|
await remove_friend(id).catch(handleError)
|
||||||
await loadFriends()
|
await loadFriends()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFriend(friend: Friend) {
|
const userFriends = ref<FriendWithUserData[]>([])
|
||||||
await remove_friend(
|
const sortedFriends = computed<FriendWithUserData[]>(() =>
|
||||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
userFriends.value.slice().sort((a, b) => {
|
||||||
).catch(handleError)
|
if (a.last_updated === null && b.last_updated === null) {
|
||||||
await loadFriends()
|
return 0 // Both are null, equal in sorting
|
||||||
}
|
}
|
||||||
|
if (a.last_updated === null) {
|
||||||
|
return 1 // `a` is null, move it after `b`
|
||||||
|
}
|
||||||
|
if (b.last_updated === null) {
|
||||||
|
return -1 // `b` is null, move it after `a`
|
||||||
|
}
|
||||||
|
// Both are non-null, sort by date
|
||||||
|
return b.last_updated.diff(a.last_updated)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const filteredFriends = computed<FriendWithUserData[]>(() =>
|
||||||
|
sortedFriends.value.filter((x) =>
|
||||||
|
x.username.trim().toLowerCase().includes(search.value.trim().toLowerCase()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
type Friend = {
|
const activeFriends = computed<FriendWithUserData[]>(() =>
|
||||||
id: string
|
filteredFriends.value.filter((x) => !!x.status && x.online && x.accepted),
|
||||||
friend_id: string | null
|
)
|
||||||
status: string | null
|
const onlineFriends = computed<FriendWithUserData[]>(() =>
|
||||||
last_updated: Dayjs | null
|
filteredFriends.value.filter((x) => x.online && !x.status && x.accepted),
|
||||||
created: Dayjs
|
)
|
||||||
username: string
|
const offlineFriends = computed<FriendWithUserData[]>(() =>
|
||||||
accepted: boolean
|
filteredFriends.value.filter((x) => !x.online && x.accepted),
|
||||||
online: boolean
|
|
||||||
avatar: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const userFriends = ref<Friend[]>([])
|
|
||||||
const acceptedFriends = computed(() =>
|
|
||||||
userFriends.value
|
|
||||||
.filter((x) => x.accepted)
|
|
||||||
.toSorted((a, b) => {
|
|
||||||
if (a.last_updated === null && b.last_updated === null) {
|
|
||||||
return 0 // Both are null, equal in sorting
|
|
||||||
}
|
|
||||||
if (a.last_updated === null) {
|
|
||||||
return 1 // `a` is null, move it after `b`
|
|
||||||
}
|
|
||||||
if (b.last_updated === null) {
|
|
||||||
return -1 // `b` is null, move it after `a`
|
|
||||||
}
|
|
||||||
// Both are non-null, sort by date
|
|
||||||
return b.last_updated.diff(a.last_updated)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
const pendingFriends = computed(() =>
|
const pendingFriends = computed(() =>
|
||||||
userFriends.value.filter((x) => !x.accepted).toSorted((a, b) => b.created.diff(a.created)),
|
filteredFriends.value
|
||||||
|
.filter((x) => !x.accepted && x.id !== userCredentials.value?.user_id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.created.diff(a.created)),
|
||||||
|
)
|
||||||
|
const incomingRequests = computed(() =>
|
||||||
|
userFriends.value
|
||||||
|
.filter((x) => !x.accepted && x.id === userCredentials.value?.user_id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.created.diff(a.created)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
async function loadFriends(timeout = false) {
|
async function loadFriends(timeout = false) {
|
||||||
loading.value = timeout
|
loading.value = timeout
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const friendsList = await friends()
|
const friendsList = await friends()
|
||||||
|
userFriends.value = await transformFriends(friendsList, userCredentials.value)
|
||||||
if (friendsList.length === 0) {
|
loading.value = false
|
||||||
userFriends.value = []
|
} catch (e) {
|
||||||
} else {
|
console.error('Error loading friends', e)
|
||||||
const friendStatuses = await friend_statuses()
|
if (timeout) {
|
||||||
const users = await get_user_many(
|
setTimeout(() => loadFriends(), 15 * 1000)
|
||||||
friendsList.map((x) => (x.id === userCredentials.value.user_id ? x.friend_id : x.id)),
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
userFriends.value = friendsList.map((friend) => {
|
|
||||||
const user = users.find((x) => x.id === friend.id || x.id === friend.friend_id)
|
|
||||||
const status = friendStatuses.find(
|
|
||||||
(x) => x.user_id === friend.id || x.user_id === friend.friend_id,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
id: friend.id,
|
|
||||||
friend_id: friend.friend_id,
|
|
||||||
status: status?.profile_name,
|
|
||||||
last_updated: status && status.last_update ? dayjs(status.last_update) : null,
|
|
||||||
created: dayjs(friend.created),
|
|
||||||
avatar: user?.avatar_url,
|
|
||||||
username: user?.username,
|
|
||||||
online: !!status,
|
|
||||||
accepted: friend.accepted,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error loading friends', e)
|
|
||||||
if (timeout) {
|
|
||||||
setTimeout(() => loadFriends(), 15 * 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
userCredentials,
|
userCredentials,
|
||||||
() => {
|
() => {
|
||||||
if (userCredentials.value === undefined) {
|
if (userCredentials.value === undefined) {
|
||||||
userFriends.value = []
|
userFriends.value = []
|
||||||
} else if (userCredentials.value === null) {
|
loading.value = false
|
||||||
userFriends.value = []
|
} else if (userCredentials.value === null) {
|
||||||
loading.value = false
|
userFriends.value = []
|
||||||
} else {
|
loading.value = false
|
||||||
loadFriends(true)
|
} else {
|
||||||
}
|
loadFriends(true)
|
||||||
},
|
}
|
||||||
{ immediate: true },
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const unlisten = await friend_listener(() => loadFriends())
|
const unlisten = await friend_listener(() => loadFriends())
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlisten()
|
unlisten()
|
||||||
|
})
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
addFriend: {
|
||||||
|
id: 'friends.action.add-friend',
|
||||||
|
defaultMessage: 'Add a friend',
|
||||||
|
},
|
||||||
|
addingAFriend: {
|
||||||
|
id: 'friends.add-friend.title',
|
||||||
|
defaultMessage: 'Adding a friend',
|
||||||
|
},
|
||||||
|
usernameTitle: {
|
||||||
|
id: 'friends.add-friend.username.title',
|
||||||
|
defaultMessage: "What's your friend's Modrinth username?",
|
||||||
|
},
|
||||||
|
usernameDescription: {
|
||||||
|
id: 'friends.add-friend.username.description',
|
||||||
|
defaultMessage: 'It may be different from their Minecraft username!',
|
||||||
|
},
|
||||||
|
usernamePlaceholder: {
|
||||||
|
id: 'friends.add-friend.username.placeholder',
|
||||||
|
defaultMessage: 'Enter Modrinth username...',
|
||||||
|
},
|
||||||
|
sendFriendRequest: {
|
||||||
|
id: 'friends.add-friend.submit',
|
||||||
|
defaultMessage: 'Send friend request',
|
||||||
|
},
|
||||||
|
viewFriendRequests: {
|
||||||
|
id: 'friends.action.view-friend-requests',
|
||||||
|
defaultMessage: '{count} friend {count, plural, one {request} other {requests}}',
|
||||||
|
},
|
||||||
|
searchFriends: {
|
||||||
|
id: 'friends.search-friends-placeholder',
|
||||||
|
defaultMessage: 'Search friends...',
|
||||||
|
},
|
||||||
|
friends: {
|
||||||
|
id: 'friends.heading',
|
||||||
|
defaultMessage: 'Friends',
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
id: 'friends.heading.pending',
|
||||||
|
defaultMessage: 'Pending',
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
id: 'friends.heading.active',
|
||||||
|
defaultMessage: 'Active',
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
id: 'friends.heading.online',
|
||||||
|
defaultMessage: 'Online',
|
||||||
|
},
|
||||||
|
offline: {
|
||||||
|
id: 'friends.heading.offline',
|
||||||
|
defaultMessage: 'Offline',
|
||||||
|
},
|
||||||
|
noFriendsMatch: {
|
||||||
|
id: 'friends.no-friends-match',
|
||||||
|
defaultMessage: `No friends matching ''{query}''`,
|
||||||
|
},
|
||||||
|
signInToAddFriends: {
|
||||||
|
id: 'friends.sign-in-to-add-friends',
|
||||||
|
defaultMessage:
|
||||||
|
"<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!",
|
||||||
|
},
|
||||||
|
addFriendsToShare: {
|
||||||
|
id: 'friends.add-friends-to-share',
|
||||||
|
defaultMessage: "<link>Add friends</link> to see what they're playing!",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="manageFriendsModal" header="Manage friends">
|
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
||||||
<p v-if="acceptedFriends.length === 0">Add friends to share what you're playing!</p>
|
<p v-if="incomingRequests.length === 0">You have no pending friend requests :C</p>
|
||||||
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
|
<div v-else class="flex flex-col gap-4 min-w-[40rem]">
|
||||||
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
|
<div v-for="friend in incomingRequests" :key="friend.username" class="flex gap-2">
|
||||||
<div
|
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||||
v-for="friend in acceptedFriends.filter(
|
<div class="grid grid-cols-[1fr_auto] w-full gap-4">
|
||||||
(x) => !search || x.username.toLowerCase().includes(search),
|
<div>
|
||||||
)"
|
<p class="m-0">
|
||||||
:key="friend.username"
|
<template v-if="friend.id === userCredentials?.user_id">
|
||||||
class="flex gap-2 items-center"
|
<span class="text-contrast">{{ friend.username }}</span> sent you a friend request
|
||||||
>
|
</template>
|
||||||
<div class="relative">
|
<template v-else>
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
||||||
<span
|
</template>
|
||||||
v-if="friend.online"
|
</p>
|
||||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
<p class="m-0 text-sm text-secondary">
|
||||||
/>
|
{{ formatRelativeTime(friend.created.toISOString()) }}
|
||||||
</div>
|
</p>
|
||||||
<div>{{ friend.username }}</div>
|
</div>
|
||||||
<div class="ml-auto">
|
<div class="flex gap-2">
|
||||||
<ButtonStyled>
|
<template v-if="friend.id === userCredentials?.user_id">
|
||||||
<button @click="removeFriend(friend)">
|
<ButtonStyled color="brand">
|
||||||
<XIcon />
|
<button @click="addFriend(friend)">
|
||||||
Remove
|
<UserPlusIcon />
|
||||||
</button>
|
Accept
|
||||||
</ButtonStyled>
|
</button>
|
||||||
</div>
|
</ButtonStyled>
|
||||||
</div>
|
<ButtonStyled>
|
||||||
</div>
|
<button @click="removeFriend(friend)">
|
||||||
</ModalWrapper>
|
<XIcon />
|
||||||
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
Ignore
|
||||||
<p v-if="pendingFriends.length === 0">You have no pending friend requests :C</p>
|
</button>
|
||||||
<div v-else class="flex flex-col gap-4">
|
</ButtonStyled>
|
||||||
<div v-for="friend in pendingFriends" :key="friend.username" class="flex gap-2">
|
</template>
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
<template v-else>
|
||||||
<div class="flex flex-col gap-2">
|
<ButtonStyled>
|
||||||
<div>
|
<button @click="removeFriend(friend)">
|
||||||
<p class="m-0">
|
<XIcon />
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
Cancel
|
||||||
<span class="font-bold">{{ friend.username }}</span> sent you a friend request
|
</button>
|
||||||
</template>
|
</ButtonStyled>
|
||||||
<template v-else>
|
</template>
|
||||||
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
<p class="m-0 text-sm text-secondary">
|
</div>
|
||||||
{{ formatRelativeTime(friend.created.toISOString()) }}
|
</ModalWrapper>
|
||||||
</p>
|
<ModalWrapper ref="addFriendModal" :header="formatMessage(messages.addingAFriend)">
|
||||||
</div>
|
<div class="min-w-[30rem]">
|
||||||
<div class="flex gap-2">
|
<h2 class="m-0 text-base font-medium text-primary">
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
{{ formatMessage(messages.usernameTitle) }}
|
||||||
<ButtonStyled color="brand">
|
</h2>
|
||||||
<button @click="addFriend(friend)">
|
<p class="m-0 mt-1 text-sm text-secondary leading-tight">
|
||||||
<UserPlusIcon />
|
{{ formatMessage(messages.usernameDescription) }}
|
||||||
Accept
|
</p>
|
||||||
</button>
|
<div class="flex items-center gap-2 mt-4">
|
||||||
</ButtonStyled>
|
<StyledInput
|
||||||
<ButtonStyled>
|
v-model="username"
|
||||||
<button @click="removeFriend(friend)">
|
:icon="UserIcon"
|
||||||
<XIcon />
|
type="text"
|
||||||
Ignore
|
:placeholder="formatMessage(messages.usernamePlaceholder)"
|
||||||
</button>
|
wrapper-class="flex-1"
|
||||||
</ButtonStyled>
|
@keyup.enter="addFriendFromModal"
|
||||||
</template>
|
/>
|
||||||
<template v-else>
|
<ButtonStyled color="brand">
|
||||||
<ButtonStyled>
|
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||||
<button @click="removeFriend(friend)">
|
<SendIcon />
|
||||||
<XIcon />
|
{{ formatMessage(messages.sendFriendRequest) }}
|
||||||
Cancel
|
</button>
|
||||||
</button>
|
</ButtonStyled>
|
||||||
</ButtonStyled>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</ModalWrapper>
|
||||||
</div>
|
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 -ml-1">
|
||||||
</div>
|
<template v-if="sortedFriends.length > 0">
|
||||||
</div>
|
<ButtonStyled circular type="transparent">
|
||||||
</ModalWrapper>
|
<button
|
||||||
<ModalWrapper ref="addFriendModal" header="Add a friend">
|
v-tooltip="formatMessage(messages.addFriend)"
|
||||||
<div class="mb-4">
|
:aria-label="formatMessage(messages.addFriend)"
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Username</h2>
|
@click="addFriendModal.show"
|
||||||
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
>
|
||||||
<input v-model="username" class="mt-2 w-full" type="text" placeholder="Enter username..." />
|
<UserPlusIcon />
|
||||||
</div>
|
</button>
|
||||||
<ButtonStyled color="brand">
|
</ButtonStyled>
|
||||||
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
<StyledInput
|
||||||
<UserPlusIcon />
|
v-model="search"
|
||||||
Add friend
|
type="text"
|
||||||
</button>
|
:placeholder="formatMessage(messages.searchFriends)"
|
||||||
</ButtonStyled>
|
clearable
|
||||||
</ModalWrapper>
|
variant="outlined"
|
||||||
<div class="flex justify-between items-center">
|
wrapper-class="flex-1"
|
||||||
<h3 class="text-lg m-0">Friends</h3>
|
@keyup.esc="search = ''"
|
||||||
<ButtonStyled v-if="userCredentials" type="transparent" circular>
|
/>
|
||||||
<OverflowMenu
|
</template>
|
||||||
:options="[
|
<h3 v-else class="w-full text-base text-primary font-medium m-0">
|
||||||
{
|
{{ formatMessage(messages.friends) }}
|
||||||
id: 'add-friend',
|
</h3>
|
||||||
action: () => addFriendModal.show(),
|
<ButtonStyled v-if="incomingRequests.length > 0" circular type="transparent">
|
||||||
},
|
<button
|
||||||
{
|
v-tooltip="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||||
id: 'manage-friends',
|
class="relative"
|
||||||
action: () => manageFriendsModal.show(),
|
:aria-label="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||||
shown: acceptedFriends.length > 0,
|
@click="friendInvitesModal.show"
|
||||||
},
|
>
|
||||||
{
|
<MailIcon />
|
||||||
id: 'view-requests',
|
<span
|
||||||
action: () => friendInvitesModal.show(),
|
v-if="incomingRequests.length > 0"
|
||||||
shown: pendingFriends.length > 0,
|
aria-hidden="true"
|
||||||
},
|
class="absolute bg-brand text-brand-inverted text-[8px] top-0.5 px-1 right-0.5 min-w-3 h-3 rounded-full flex items-center justify-center font-bold"
|
||||||
]"
|
>
|
||||||
aria-label="More options"
|
{{ incomingRequests.length }}
|
||||||
>
|
</span>
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
</button>
|
||||||
<template #add-friend>
|
</ButtonStyled>
|
||||||
<UserPlusIcon aria-hidden="true" />
|
</div>
|
||||||
Add friend
|
<div class="flex flex-col gap-3">
|
||||||
</template>
|
<h3 v-if="loading" class="text-base text-primary font-medium m-0">
|
||||||
<template #manage-friends>
|
{{ formatMessage(messages.friends) }}
|
||||||
<SettingsIcon aria-hidden="true" />
|
</h3>
|
||||||
Manage friends
|
<template v-if="loading">
|
||||||
<div
|
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
||||||
v-if="acceptedFriends.length > 0"
|
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
||||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
<div class="flex flex-col w-full">
|
||||||
>
|
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
||||||
{{ acceptedFriends.length }}
|
<div class="h-2.5 bg-button-bg rounded-full w-3/4"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<template #view-requests>
|
</template>
|
||||||
<MailIcon aria-hidden="true" />
|
<template v-else-if="sortedFriends.length === 0">
|
||||||
View friend requests
|
<div class="text-sm">
|
||||||
<div
|
<div v-if="!userCredentials">
|
||||||
v-if="pendingFriends.length > 0"
|
<IntlFormatted :message-id="messages.signInToAddFriends">
|
||||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
<template #link="{ children }">
|
||||||
>
|
<span class="font-semibold text-brand cursor-pointer" @click="signIn">
|
||||||
{{ pendingFriends.length }}
|
<component :is="() => children" />
|
||||||
</div>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
</IntlFormatted>
|
||||||
</ButtonStyled>
|
</div>
|
||||||
</div>
|
<div v-else>
|
||||||
<div class="flex flex-col gap-2 mt-2">
|
<IntlFormatted :message-id="messages.addFriendsToShare">
|
||||||
<template v-if="loading">
|
<template #link="{ children }">
|
||||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
<span class="font-semibold text-brand cursor-pointer" @click="addFriendModal.show">
|
||||||
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
<component :is="() => children" />
|
||||||
<div class="flex flex-col w-full">
|
</span>
|
||||||
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
</template>
|
||||||
<div class="h-2.5 bg-button-bg rounded-full w-3/4"></div>
|
</IntlFormatted>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="acceptedFriends.length === 0">
|
<template v-else>
|
||||||
<div class="text-sm">
|
<FriendsSection
|
||||||
<div v-if="!userCredentials">
|
v-if="activeFriends.length > 0"
|
||||||
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
:is-searching="!!search"
|
||||||
</div>
|
open-by-default
|
||||||
<div v-else>
|
:friends="activeFriends"
|
||||||
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
|
:heading="formatMessage(messages.active)"
|
||||||
to share what you're playing!
|
:remove-friend="removeFriend"
|
||||||
</div>
|
/>
|
||||||
</div>
|
<FriendsSection
|
||||||
</template>
|
v-if="onlineFriends.length > 0"
|
||||||
<template v-else>
|
:is-searching="!!search"
|
||||||
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
open-by-default
|
||||||
<template #remove-friend> <TrashIcon /> Remove friend </template>
|
:friends="onlineFriends"
|
||||||
</ContextMenu>
|
:heading="formatMessage(messages.online)"
|
||||||
<div
|
:remove-friend="removeFriend"
|
||||||
v-for="friend in acceptedFriends.slice(0, 5)"
|
/>
|
||||||
:key="friend.username"
|
<FriendsSection
|
||||||
class="flex gap-2 items-center"
|
v-if="offlineFriends.length > 0"
|
||||||
:class="{ grayscale: !friend.online }"
|
:is-searching="!!search"
|
||||||
@contextmenu.prevent.stop="
|
:open-by-default="activeFriends.length + onlineFriends.length < 3"
|
||||||
(event) =>
|
:friends="offlineFriends"
|
||||||
friendOptions.showMenu(event, friend, [
|
:heading="formatMessage(messages.offline)"
|
||||||
{
|
:remove-friend="removeFriend"
|
||||||
name: 'remove-friend',
|
/>
|
||||||
color: 'danger',
|
<FriendsSection
|
||||||
},
|
v-if="pendingFriends.length > 0"
|
||||||
])
|
:is-searching="!!search"
|
||||||
"
|
:friends="pendingFriends"
|
||||||
>
|
:heading="formatMessage(messages.pending)"
|
||||||
<div class="relative">
|
:remove-friend="removeFriend"
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
/>
|
||||||
<span
|
<p v-if="filteredFriends.length === 0 && search" class="text-sm text-secondary my-1 mx-4">
|
||||||
v-if="friend.online"
|
{{ formatMessage(messages.noFriendsMatch, { query: search }) }}
|
||||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
</p>
|
||||||
/>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-md m-0 font-medium" :class="{ 'text-secondary': !friend.online }">
|
|
||||||
{{ friend.username }}
|
|
||||||
</span>
|
|
||||||
<span v-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { MoreVerticalIcon, TrashIcon, UserIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
defineMessages,
|
||||||
|
OverflowMenu,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import { useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import type { FriendWithUserData } from '@/helpers/friends.ts'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
friends: FriendWithUserData[]
|
||||||
|
heading: string
|
||||||
|
removeFriend: (friend: FriendWithUserData) => Promise<void>
|
||||||
|
isSearching?: boolean
|
||||||
|
openByDefault?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
isSearching: false,
|
||||||
|
openByDefault: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function createContextMenuOptions(friend: FriendWithUserData) {
|
||||||
|
if (friend.accepted) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'view-profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'remove-friend',
|
||||||
|
color: 'danger',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'view-profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancel-request',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openProfile(username: string) {
|
||||||
|
openUrl('https://modrinth.com/user/' + username)
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendOptions = useTemplateRef('friendOptions')
|
||||||
|
async function handleFriendOptions(args: { item: FriendWithUserData; option: string }) {
|
||||||
|
switch (args.option) {
|
||||||
|
case 'remove-friend':
|
||||||
|
case 'cancel-request':
|
||||||
|
await props.removeFriend(args.item)
|
||||||
|
break
|
||||||
|
case 'view-profile':
|
||||||
|
openProfile(args.item.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
removeFriend: {
|
||||||
|
id: 'friends.friend.remove-friend',
|
||||||
|
defaultMessage: 'Remove friend',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
id: 'friends.section.heading',
|
||||||
|
defaultMessage: '{title} - {count}',
|
||||||
|
},
|
||||||
|
friendRequestSent: {
|
||||||
|
id: 'friends.friend.request-sent',
|
||||||
|
defaultMessage: 'Friend request sent',
|
||||||
|
},
|
||||||
|
cancelRequest: {
|
||||||
|
id: 'friends.friend.cancel-request',
|
||||||
|
defaultMessage: 'Cancel request',
|
||||||
|
},
|
||||||
|
viewProfile: {
|
||||||
|
id: 'friends.friend.view-profile',
|
||||||
|
defaultMessage: 'View profile',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
||||||
|
<template #view-profile>
|
||||||
|
<UserIcon />
|
||||||
|
{{ formatMessage(messages.viewProfile) }}
|
||||||
|
</template>
|
||||||
|
<template #remove-friend> <TrashIcon /> {{ formatMessage(messages.removeFriend) }} </template>
|
||||||
|
<template #cancel-request> <XIcon /> {{ formatMessage(messages.cancelRequest) }} </template>
|
||||||
|
</ContextMenu>
|
||||||
|
<Accordion
|
||||||
|
:open-by-default="openByDefault"
|
||||||
|
:force-open="isSearching"
|
||||||
|
:button-class="
|
||||||
|
'flex w-full items-center bg-transparent border-0 p-0' +
|
||||||
|
(isSearching
|
||||||
|
? ''
|
||||||
|
: ' cursor-pointer hover:brightness-[--hover-brightness] active:scale-[0.98] transition-all')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h3 class="text-base text-primary font-medium m-0">
|
||||||
|
{{ formatMessage(messages.heading, { title: heading, count: friends.length }) }}
|
||||||
|
</h3>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div class="pt-3 flex flex-col gap-1">
|
||||||
|
<div
|
||||||
|
v-for="friend in friends"
|
||||||
|
:key="friend.username"
|
||||||
|
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
|
||||||
|
@contextmenu.prevent.stop="
|
||||||
|
(event) => friendOptions?.showMenu(event, friend, createContextMenuOptions(friend))
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<Avatar
|
||||||
|
:src="friend.avatar"
|
||||||
|
:class="{ grayscale: !friend.online && friend.accepted }"
|
||||||
|
class="w-12 h-12 rounded-full"
|
||||||
|
size="32px"
|
||||||
|
circle
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="friend.online"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span
|
||||||
|
class="text-sm m-0"
|
||||||
|
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
|
||||||
|
>
|
||||||
|
{{ friend.username }}
|
||||||
|
</span>
|
||||||
|
<span v-if="!friend.accepted" class="m-0 text-xs">
|
||||||
|
{{ formatMessage(messages.friendRequestSent) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||||
|
</div>
|
||||||
|
<ButtonStyled v-if="friend.accepted" circular type="transparent">
|
||||||
|
<OverflowMenu
|
||||||
|
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'view-profile',
|
||||||
|
action: () => openProfile(friend.username),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-friend',
|
||||||
|
action: () => removeFriend(friend),
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
<template #view-profile>
|
||||||
|
<UserIcon />
|
||||||
|
{{ formatMessage(messages.viewProfile) }}
|
||||||
|
</template>
|
||||||
|
<template #remove-friend>
|
||||||
|
<TrashIcon />
|
||||||
|
{{ formatMessage(messages.removeFriend) }}
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled v-else type="transparent" circular>
|
||||||
|
<button v-tooltip="formatMessage(messages.cancelRequest)" @click="removeFriend(friend)">
|
||||||
|
<XIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Accordion>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CheckIcon, PlusIcon, SearchIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Admonition,
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
injectNotificationManager,
|
||||||
|
StyledInput,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { list } from '@/helpers/profile'
|
||||||
|
import { add_server_to_profile, get_profile_worlds } from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
|
const modal = ref()
|
||||||
|
const searchFilter = ref('')
|
||||||
|
const profiles = ref([])
|
||||||
|
|
||||||
|
const serverName = ref('')
|
||||||
|
const serverAddress = ref('')
|
||||||
|
|
||||||
|
const shownProfiles = computed(() =>
|
||||||
|
profiles.value.filter((profile) => {
|
||||||
|
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show: async (name, address) => {
|
||||||
|
serverName.value = name
|
||||||
|
serverAddress.value = address
|
||||||
|
searchFilter.value = ''
|
||||||
|
|
||||||
|
const profilesVal = await list().catch(handleError)
|
||||||
|
await Promise.allSettled(
|
||||||
|
profilesVal.map(async (profile) => {
|
||||||
|
profile.adding = false
|
||||||
|
profile.added = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const worlds = await get_profile_worlds(profile.path)
|
||||||
|
profile.added = worlds.some(
|
||||||
|
(w) => w.type === 'server' && w.address === serverAddress.value,
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// Ignore - will show as not added
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
profiles.value = profilesVal
|
||||||
|
modal.value.show()
|
||||||
|
|
||||||
|
trackEvent('AddServerToInstanceStart', { source: 'AddServerToInstanceModal' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function addServer(profile) {
|
||||||
|
profile.adding = true
|
||||||
|
try {
|
||||||
|
await add_server_to_profile(profile.path, serverName.value, serverAddress.value, 'prompt')
|
||||||
|
profile.added = true
|
||||||
|
|
||||||
|
trackEvent('AddServerToInstance', {
|
||||||
|
server_name: serverName.value,
|
||||||
|
instance_name: profile.name,
|
||||||
|
source: 'AddServerToInstanceModal',
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err)
|
||||||
|
}
|
||||||
|
profile.adding = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalWrapper ref="modal" header="Add server to instance">
|
||||||
|
<div class="flex flex-col gap-4 min-w-[350px]">
|
||||||
|
<Admonition type="warning" body="This server may not be compatible with all instances." />
|
||||||
|
<StyledInput
|
||||||
|
v-model="searchFilter"
|
||||||
|
:icon="SearchIcon"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search for an instance"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<div class="max-h-[21rem] overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="profile in shownProfiles"
|
||||||
|
:key="profile.path"
|
||||||
|
class="flex w-full items-center justify-between gap-2 bg-bg-raised text-icon shadow-none"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="btn btn-transparent p-2 text-left"
|
||||||
|
:to="`/instance/${encodeURIComponent(profile.path)}`"
|
||||||
|
@click="modal.hide()"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
||||||
|
class="mr-2 [--size:2rem]"
|
||||||
|
/>
|
||||||
|
{{ profile.name }}
|
||||||
|
</router-link>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button :disabled="profile.added || profile.adding" @click="addServer(profile)">
|
||||||
|
<PlusIcon v-if="!profile.added && !profile.adding" />
|
||||||
|
<CheckIcon v-else-if="profile.added" />
|
||||||
|
{{ profile.adding ? 'Adding...' : profile.added ? 'Added' : 'Add' }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group push-right">
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="modal.hide()">Cancel</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>
|
|
||||||
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
|
|
||||||
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
|
|
||||||
installed.
|
|
||||||
</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr class="header">
|
|
||||||
<th>{{ instance?.name }}</th>
|
|
||||||
<th>{{ project.title }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr class="content">
|
|
||||||
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
|
||||||
<td>
|
|
||||||
<multiselect
|
|
||||||
v-if="versions?.length > 1"
|
|
||||||
v-model="selectedVersion"
|
|
||||||
:options="versions"
|
|
||||||
:searchable="true"
|
|
||||||
placeholder="Select version"
|
|
||||||
open-direction="top"
|
|
||||||
:show-labels="false"
|
|
||||||
:custom-label="
|
|
||||||
(version) =>
|
|
||||||
`${version?.name} (${version?.loaders
|
|
||||||
.map((name) => formatCategory(name))
|
|
||||||
.join(', ')} - ${version?.game_versions.join(', ')})`
|
|
||||||
"
|
|
||||||
:max-height="150"
|
|
||||||
/>
|
|
||||||
<span v-else>
|
|
||||||
<span>
|
|
||||||
{{ selectedVersion?.name }} ({{
|
|
||||||
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
|
|
||||||
}}
|
|
||||||
- {{ selectedVersion?.game_versions.join(', ') }})
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="button-group">
|
|
||||||
<Button @click="() => incompatibleModal.hide()"><XIcon />Cancel</Button>
|
|
||||||
<Button color="primary" :disabled="installing" @click="install()">
|
|
||||||
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
|
||||||
import { Button } from '@modrinth/ui'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
|
||||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import Multiselect from 'vue-multiselect'
|
|
||||||
|
|
||||||
const instance = ref(null)
|
|
||||||
const project = ref(null)
|
|
||||||
const versions = ref(null)
|
|
||||||
const selectedVersion = ref(null)
|
|
||||||
const incompatibleModal = ref(null)
|
|
||||||
const installing = ref(false)
|
|
||||||
|
|
||||||
const onInstall = ref(() => {})
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
show: (instanceVal, projectVal, projectVersions, callback) => {
|
|
||||||
instance.value = instanceVal
|
|
||||||
versions.value = projectVersions
|
|
||||||
selectedVersion.value = projectVersions[0]
|
|
||||||
|
|
||||||
project.value = projectVal
|
|
||||||
|
|
||||||
onInstall.value = callback
|
|
||||||
installing.value = false
|
|
||||||
|
|
||||||
incompatibleModal.value.show()
|
|
||||||
|
|
||||||
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const install = async () => {
|
|
||||||
installing.value = true
|
|
||||||
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
|
|
||||||
installing.value = false
|
|
||||||
onInstall.value(selectedVersion.value.id)
|
|
||||||
incompatibleModal.value.hide()
|
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
|
||||||
loader: instance.value.loader,
|
|
||||||
game_version: instance.value.game_version,
|
|
||||||
id: project.value,
|
|
||||||
version_id: selectedVersion.value.id,
|
|
||||||
project_type: project.value.project_type,
|
|
||||||
title: project.value.title,
|
|
||||||
source: 'ProjectIncompatibilityWarningModal',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.data {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border-collapse: collapse;
|
|
||||||
box-shadow: 0 0 0 1px var(--color-button-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: left;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
overflow: hidden;
|
|
||||||
border-bottom: 1px solid var(--color-button-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
th:first-child {
|
|
||||||
border-top-left-radius: var(--radius-lg);
|
|
||||||
border-right: 1px solid var(--color-button-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
th:last-child {
|
|
||||||
border-top-right-radius: var(--radius-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:first-child {
|
|
||||||
border-right: 1px solid var(--color-button-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
:deep(.animated-dropdown .options) {
|
|
||||||
max-height: 13.375rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import { Button } from '@modrinth/ui'
|
|
||||||
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
const versionId = ref()
|
|
||||||
const project = ref()
|
|
||||||
const confirmModal = ref(null)
|
|
||||||
const installing = ref(false)
|
|
||||||
|
|
||||||
const onInstall = ref(() => {})
|
|
||||||
const onCreateInstance = ref(() => {})
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
|
||||||
project.value = projectVal
|
|
||||||
versionId.value = versionIdVal
|
|
||||||
installing.value = false
|
|
||||||
confirmModal.value.show()
|
|
||||||
|
|
||||||
onInstall.value = callback
|
|
||||||
onCreateInstance.value = createInstanceCallback
|
|
||||||
|
|
||||||
trackEvent('PackInstallStart')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function install() {
|
|
||||||
installing.value = true
|
|
||||||
confirmModal.value.hide()
|
|
||||||
|
|
||||||
await pack_install(
|
|
||||||
project.value.id,
|
|
||||||
versionId.value,
|
|
||||||
project.value.title,
|
|
||||||
project.value.icon_url,
|
|
||||||
onCreateInstance.value,
|
|
||||||
).catch(handleError)
|
|
||||||
trackEvent('PackInstall', {
|
|
||||||
id: project.value.id,
|
|
||||||
version_id: versionId.value,
|
|
||||||
title: project.value.title,
|
|
||||||
source: 'ConfirmModal',
|
|
||||||
})
|
|
||||||
|
|
||||||
onInstall.value(versionId.value)
|
|
||||||
installing.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
|
|
||||||
<div class="input-group push-right">
|
|
||||||
<Button @click="() => $refs.confirmModal.hide()"><XIcon />Cancel</Button>
|
|
||||||
<Button color="primary" :disabled="installing" @click="install()"
|
|
||||||
><DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import {
|
|
||||||
DownloadIcon,
|
|
||||||
PlusIcon,
|
|
||||||
UploadIcon,
|
|
||||||
XIcon,
|
|
||||||
RightArrowIcon,
|
|
||||||
CheckIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import {
|
|
||||||
add_project_from_version as installMod,
|
|
||||||
check_installed,
|
|
||||||
get,
|
|
||||||
list,
|
|
||||||
create,
|
|
||||||
} from '@/helpers/profile'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { installVersionDependencies } from '@/store/install.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const versions = ref()
|
|
||||||
const project = ref()
|
|
||||||
|
|
||||||
const installModal = ref()
|
|
||||||
const searchFilter = ref('')
|
|
||||||
|
|
||||||
const showCreation = ref(false)
|
|
||||||
const icon = ref(null)
|
|
||||||
const name = ref(null)
|
|
||||||
const display_icon = ref(null)
|
|
||||||
const loader = ref(null)
|
|
||||||
const gameVersion = ref(null)
|
|
||||||
const creatingInstance = ref(false)
|
|
||||||
|
|
||||||
const profiles = ref([])
|
|
||||||
|
|
||||||
const shownProfiles = computed(() =>
|
|
||||||
profiles.value
|
|
||||||
.filter((profile) => {
|
|
||||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
|
||||||
})
|
|
||||||
.filter((profile) => {
|
|
||||||
const loaders = versions.value.flatMap((v) => v.loaders)
|
|
||||||
|
|
||||||
return (
|
|
||||||
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
|
|
||||||
(project.value.project_type === 'mod'
|
|
||||||
? loaders.includes(profile.loader) || loaders.includes('minecraft')
|
|
||||||
: true)
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const onInstall = ref(() => {})
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
show: async (projectVal, versionsVal, callback) => {
|
|
||||||
project.value = projectVal
|
|
||||||
versions.value = versionsVal
|
|
||||||
searchFilter.value = ''
|
|
||||||
|
|
||||||
showCreation.value = false
|
|
||||||
name.value = null
|
|
||||||
icon.value = null
|
|
||||||
display_icon.value = null
|
|
||||||
gameVersion.value = null
|
|
||||||
loader.value = null
|
|
||||||
|
|
||||||
onInstall.value = callback
|
|
||||||
|
|
||||||
const profilesVal = await list().catch(handleError)
|
|
||||||
for (const profile of profilesVal) {
|
|
||||||
profile.installing = false
|
|
||||||
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
|
|
||||||
handleError,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
profiles.value = profilesVal
|
|
||||||
|
|
||||||
installModal.value.show()
|
|
||||||
|
|
||||||
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function install(instance) {
|
|
||||||
instance.installing = true
|
|
||||||
const version = versions.value.find((v) => {
|
|
||||||
return (
|
|
||||||
v.game_versions.includes(instance.game_version) &&
|
|
||||||
(project.value.project_type === 'mod'
|
|
||||||
? v.loaders.includes(instance.loader) || v.loaders.includes('minecraft')
|
|
||||||
: true)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!version) {
|
|
||||||
instance.installing = false
|
|
||||||
handleError('No compatible version found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await installMod(instance.path, version.id).catch(handleError)
|
|
||||||
await installVersionDependencies(instance, version)
|
|
||||||
|
|
||||||
instance.installedMod = true
|
|
||||||
instance.installing = false
|
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
|
||||||
loader: instance.loader,
|
|
||||||
game_version: instance.game_version,
|
|
||||||
id: project.value.id,
|
|
||||||
version_id: version.id,
|
|
||||||
project_type: project.value.project_type,
|
|
||||||
title: project.value.title,
|
|
||||||
source: 'ProjectInstallModal',
|
|
||||||
})
|
|
||||||
|
|
||||||
onInstall.value(version.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleCreation = () => {
|
|
||||||
showCreation.value = !showCreation.value
|
|
||||||
name.value = null
|
|
||||||
icon.value = null
|
|
||||||
display_icon.value = null
|
|
||||||
gameVersion.value = null
|
|
||||||
loader.value = null
|
|
||||||
|
|
||||||
if (showCreation.value) {
|
|
||||||
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const upload_icon = async () => {
|
|
||||||
const res = await open({
|
|
||||||
multiple: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Image',
|
|
||||||
extensions: ['png', 'jpeg'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
icon.value = res.path ?? res
|
|
||||||
|
|
||||||
if (!icon.value) return
|
|
||||||
display_icon.value = convertFileSrc(icon.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset_icon = () => {
|
|
||||||
icon.value = null
|
|
||||||
display_icon.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const createInstance = async () => {
|
|
||||||
creatingInstance.value = true
|
|
||||||
|
|
||||||
const loader =
|
|
||||||
versions.value[0].loaders[0] !== 'forge' &&
|
|
||||||
versions.value[0].loaders[0] !== 'fabric' &&
|
|
||||||
versions.value[0].loaders[0] !== 'quilt'
|
|
||||||
? 'vanilla'
|
|
||||||
: versions.value[0].loaders[0]
|
|
||||||
|
|
||||||
const id = await create(
|
|
||||||
name.value,
|
|
||||||
versions.value[0].game_versions[0],
|
|
||||||
loader,
|
|
||||||
'latest',
|
|
||||||
icon.value,
|
|
||||||
).catch(handleError)
|
|
||||||
|
|
||||||
await installMod(id, versions.value[0].id).catch(handleError)
|
|
||||||
|
|
||||||
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
|
||||||
|
|
||||||
const instance = await get(id, true)
|
|
||||||
await installVersionDependencies(instance, versions.value[0])
|
|
||||||
|
|
||||||
trackEvent('InstanceCreate', {
|
|
||||||
profile_name: name.value,
|
|
||||||
game_version: versions.value[0].game_versions[0],
|
|
||||||
loader: loader,
|
|
||||||
loader_version: 'latest',
|
|
||||||
has_icon: !!icon.value,
|
|
||||||
source: 'ProjectInstallModal',
|
|
||||||
})
|
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
|
||||||
loader: loader,
|
|
||||||
game_version: versions.value[0].game_versions[0],
|
|
||||||
id: project.value,
|
|
||||||
version_id: versions.value[0].id,
|
|
||||||
project_type: project.value.project_type,
|
|
||||||
title: project.value.title,
|
|
||||||
source: 'ProjectInstallModal',
|
|
||||||
})
|
|
||||||
|
|
||||||
onInstall.value(versions.value[0].id)
|
|
||||||
|
|
||||||
if (installModal.value) installModal.value.hide()
|
|
||||||
creatingInstance.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
|
|
||||||
<div class="modal-body">
|
|
||||||
<input
|
|
||||||
v-model="searchFilter"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
class="search"
|
|
||||||
placeholder="Search for an instance"
|
|
||||||
/>
|
|
||||||
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
|
|
||||||
<div v-for="profile in shownProfiles" :key="profile.name" class="option">
|
|
||||||
<router-link
|
|
||||||
class="btn btn-transparent profile-button"
|
|
||||||
:to="`/instance/${encodeURIComponent(profile.path)}`"
|
|
||||||
@click="installModal.hide()"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
|
||||||
class="profile-image"
|
|
||||||
/>
|
|
||||||
{{ profile.name }}
|
|
||||||
</router-link>
|
|
||||||
<div
|
|
||||||
v-tooltip="
|
|
||||||
profile.linked_data?.locked && !profile.installedMod
|
|
||||||
? 'Unpair or unlock an instance to add mods.'
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
:disabled="profile.installedMod || profile.installing"
|
|
||||||
@click="install(profile)"
|
|
||||||
>
|
|
||||||
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
|
|
||||||
<CheckIcon v-else-if="profile.installedMod" />
|
|
||||||
{{
|
|
||||||
profile.installing
|
|
||||||
? 'Installing...'
|
|
||||||
: profile.installedMod
|
|
||||||
? 'Installed'
|
|
||||||
: 'Install'
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Card v-if="showCreation" class="creation-card">
|
|
||||||
<div class="creation-container">
|
|
||||||
<div class="creation-icon">
|
|
||||||
<Avatar size="md" class="icon" :src="display_icon" />
|
|
||||||
<div class="creation-icon__description">
|
|
||||||
<Button @click="upload_icon()">
|
|
||||||
<UploadIcon />
|
|
||||||
<span class="no-wrap"> Select icon </span>
|
|
||||||
</Button>
|
|
||||||
<Button :disabled="!display_icon" @click="reset_icon()">
|
|
||||||
<XIcon />
|
|
||||||
<span class="no-wrap"> Remove icon </span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="creation-settings">
|
|
||||||
<input
|
|
||||||
v-model="name"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Name"
|
|
||||||
class="creation-input"
|
|
||||||
/>
|
|
||||||
<Button :disabled="creatingInstance === true || !name" @click="createInstance()">
|
|
||||||
<RightArrowIcon />
|
|
||||||
{{ creatingInstance ? 'Creating...' : 'Create' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<div class="input-group push-right">
|
|
||||||
<Button :color="showCreation ? '' : 'primary'" @click="toggleCreation()">
|
|
||||||
<PlusIcon />
|
|
||||||
{{ showCreation ? 'Hide New Instance' : 'Create new instance' }}
|
|
||||||
</Button>
|
|
||||||
<Button @click="installModal.hide()">Cancel</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.creation-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
margin: 0;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.creation-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creation-icon {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
.creation-icon__description {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.creation-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-wrap {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creation-dropdown {
|
|
||||||
width: min-content !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creation-settings {
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
min-width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profiles {
|
|
||||||
max-height: 12rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
&.hide-creation {
|
|
||||||
max-height: 21rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.option {
|
|
||||||
width: calc(100%);
|
|
||||||
background: var(--color-raised-bg);
|
|
||||||
color: var(--color-base);
|
|
||||||
box-shadow: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-button {
|
|
||||||
align-content: start;
|
|
||||||
padding: 0.5rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-image {
|
|
||||||
--size: 2rem !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal ref="modal" :header="formatMessage(messages.header)" :on-hide="reset">
|
||||||
|
<div class="max-w-[31rem] flex flex-col gap-6">
|
||||||
|
<Admonition
|
||||||
|
type="warning"
|
||||||
|
:header="formatMessage(messages.warningTitle)"
|
||||||
|
:body="formatMessage(messages.warningBody)"
|
||||||
|
/>
|
||||||
|
<div v-if="fileName" class="overflow-x-auto whitespace-nowrap text-sm text-secondary">
|
||||||
|
{{ fileName }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mt-0 leading-tight">
|
||||||
|
{{ formatMessage(messages.body) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-orange font-semibold mb-0 leading-tight">
|
||||||
|
{{ formatMessage(messages.malwareStatement) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Checkbox v-model="dontShowAgain" :label="formatMessage(messages.dontShowAgain)" />
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<ButtonStyled type="outlined">
|
||||||
|
<button @click="cancel">
|
||||||
|
<XIcon />
|
||||||
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="orange">
|
||||||
|
<button :disabled="isProceeding" @click="proceed">
|
||||||
|
<SpinnerIcon v-if="isProceeding" class="animate-spin" />
|
||||||
|
<CircleArrowRightIcon v-else />
|
||||||
|
{{ formatMessage(messages.installAnyway) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CircleArrowRightIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Admonition,
|
||||||
|
ButtonStyled,
|
||||||
|
Checkbox,
|
||||||
|
commonMessages,
|
||||||
|
defineMessages,
|
||||||
|
NewModal,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
import { get as getSettings, set as setSettings } from '@/helpers/settings'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
import type { FeatureFlag } from '@/store/theme.ts'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const themeStore = useTheming()
|
||||||
|
const skipUnknownPackWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
|
||||||
|
|
||||||
|
const dontShowAgain = ref(false)
|
||||||
|
const modal = useTemplateRef('modal')
|
||||||
|
const onProceed = ref<() => Promise<void>>()
|
||||||
|
const isProceeding = ref(false)
|
||||||
|
const fileName = ref('')
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
header: {
|
||||||
|
id: 'unknown-pack-warning-modal.header',
|
||||||
|
defaultMessage: 'Confirm installation',
|
||||||
|
},
|
||||||
|
warningTitle: {
|
||||||
|
id: 'unknown-pack-warning-modal.warning.title',
|
||||||
|
defaultMessage: 'Unknown file warning',
|
||||||
|
},
|
||||||
|
warningBody: {
|
||||||
|
id: 'unknown-pack-warning-modal.warning.body',
|
||||||
|
defaultMessage: `We couldn't find this file on Modrinth. We strongly recommend only installing files from sources you trust.`,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
id: 'unknown-pack-warning-modal.body',
|
||||||
|
defaultMessage: `A file is only reviewed if it’s uploaded to Modrinth, regardless of its file format (including .mrpack).`,
|
||||||
|
},
|
||||||
|
malwareStatement: {
|
||||||
|
id: 'unknown-pack-warning-modal.malware-statement',
|
||||||
|
defaultMessage: `Malware is often distributed through modpack files by sharing them on platforms like Discord.`,
|
||||||
|
},
|
||||||
|
dontShowAgain: {
|
||||||
|
id: 'unknown-pack-warning-modal.dont-show-again',
|
||||||
|
defaultMessage: `Don't show this warning again`,
|
||||||
|
},
|
||||||
|
installAnyway: {
|
||||||
|
id: 'unknown-pack-warning-modal.install-anyway',
|
||||||
|
defaultMessage: `Install anyway`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function show(createInstance: () => Promise<void>, selectedFileName = '') {
|
||||||
|
onProceed.value = createInstance
|
||||||
|
fileName.value = selectedFileName
|
||||||
|
dontShowAgain.value = false
|
||||||
|
|
||||||
|
if (themeStore.getFeatureFlag(skipUnknownPackWarningFeatureFlag)) {
|
||||||
|
// noinspection ES6MissingAwait
|
||||||
|
createInstance()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
onProceed.value = undefined
|
||||||
|
fileName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proceed() {
|
||||||
|
if (!onProceed.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dontShowAgain.value) {
|
||||||
|
themeStore.featureFlags[skipUnknownPackWarningFeatureFlag] = true
|
||||||
|
const settings = await getSettings()
|
||||||
|
settings.feature_flags[skipUnknownPackWarningFeatureFlag] = true
|
||||||
|
await setSettings(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createInstance = onProceed.value
|
||||||
|
modal.value?.hide()
|
||||||
|
// noinspection ES6MissingAwait
|
||||||
|
createInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
</script>
|
||||||
@@ -1,326 +1,442 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { CopyIcon, EditIcon, PlusIcon, SpinnerIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
Checkbox,
|
||||||
|
Chips,
|
||||||
|
defineMessages,
|
||||||
|
injectNotificationManager,
|
||||||
|
OverflowMenu,
|
||||||
|
StyledInput,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { SpinnerIcon, TrashIcon, UploadIcon, PlusIcon, EditIcon, CopyIcon } from '@modrinth/assets'
|
|
||||||
import { Avatar, ButtonStyled, OverflowMenu, Checkbox } from '@modrinth/ui'
|
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
|
||||||
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { computed, type Ref, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
import type { InstanceSettingsTabProps, GameInstance } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
|
import type { GameInstance } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const deleteConfirmModal = ref()
|
const deleteConfirmModal = ref()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance } = injectInstanceSettings()
|
||||||
|
type ReleaseChannel = GameInstance['preferred_update_channel']
|
||||||
|
const releaseChannelOptions: ReleaseChannel[] = ['release', 'beta', 'alpha']
|
||||||
|
|
||||||
const title = ref(props.instance.name)
|
const title = ref(instance.value.name)
|
||||||
const icon: Ref<string | undefined> = ref(props.instance.icon_path)
|
const icon: Ref<string | undefined> = ref(instance.value.icon_path)
|
||||||
const groups = ref(props.instance.groups)
|
const groups = ref([...instance.value.groups])
|
||||||
|
const savingReleaseChannel = ref(false)
|
||||||
|
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.preferred_update_channel)
|
||||||
|
const releaseChannelDisabledItems = computed<ReleaseChannel[]>(() =>
|
||||||
|
savingReleaseChannel.value ? [...releaseChannelOptions] : [],
|
||||||
|
)
|
||||||
|
|
||||||
const newCategoryInput = ref('')
|
const newCategoryInput = ref('')
|
||||||
|
|
||||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
const installing = computed(() => instance.value.install_stage !== 'installed')
|
||||||
|
|
||||||
async function duplicateProfile() {
|
async function duplicateProfile() {
|
||||||
await duplicate(props.instance.path).catch(handleError)
|
await duplicate(instance.value.path).catch(handleError)
|
||||||
trackEvent('InstanceDuplicate', {
|
trackEvent('InstanceDuplicate', {
|
||||||
loader: props.instance.loader,
|
loader: instance.value.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: instance.value.game_version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const allInstances = ref((await list()) as GameInstance[])
|
const allInstances = ref((await list()) as GameInstance[])
|
||||||
const availableGroups = computed(() => [
|
const availableGroups = computed(() => [
|
||||||
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
||||||
])
|
])
|
||||||
|
|
||||||
async function resetIcon() {
|
function formatReleaseChannelLabel(channel: ReleaseChannel) {
|
||||||
icon.value = undefined
|
switch (channel) {
|
||||||
await edit_icon(props.instance.path, null).catch(handleError)
|
case 'release':
|
||||||
trackEvent('InstanceRemoveIcon')
|
return formatMessage(messages.updateChannelRelease)
|
||||||
|
case 'beta':
|
||||||
|
return formatMessage(messages.updateChannelBeta)
|
||||||
|
case 'alpha':
|
||||||
|
return formatMessage(messages.updateChannelAlpha)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setIcon() {
|
function formatReleaseChannelDescription(channel: ReleaseChannel) {
|
||||||
const value = await open({
|
switch (channel) {
|
||||||
multiple: false,
|
case 'release':
|
||||||
filters: [
|
return formatMessage(messages.updateChannelReleaseDescription)
|
||||||
{
|
case 'beta':
|
||||||
name: 'Image',
|
return formatMessage(messages.updateChannelBetaDescription)
|
||||||
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
case 'alpha':
|
||||||
},
|
return formatMessage(messages.updateChannelAlphaDescription)
|
||||||
],
|
}
|
||||||
})
|
|
||||||
|
|
||||||
if (!value) return
|
|
||||||
|
|
||||||
icon.value = value
|
|
||||||
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
|
||||||
|
|
||||||
trackEvent('InstanceSetIcon')
|
|
||||||
}
|
|
||||||
|
|
||||||
const editProfileObject = computed(() => ({
|
|
||||||
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
|
||||||
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const toggleGroup = (group: string) => {
|
|
||||||
if (groups.value.includes(group)) {
|
|
||||||
groups.value = groups.value.filter((x) => x !== group)
|
|
||||||
} else {
|
|
||||||
groups.value.push(group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addCategory = () => {
|
|
||||||
const text = newCategoryInput.value.trim()
|
|
||||||
|
|
||||||
if (text.length > 0) {
|
|
||||||
groups.value.push(text.substring(0, 32))
|
|
||||||
newCategoryInput.value = ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[title, groups, groups],
|
() => [instance.value.path, instance.value.preferred_update_channel] as const,
|
||||||
async () => {
|
() => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
if (!savingReleaseChannel.value) {
|
||||||
},
|
selectedReleaseChannel.value = instance.value.preferred_update_channel
|
||||||
{ deep: true },
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(selectedReleaseChannel, async (channel, previousChannel) => {
|
||||||
|
const previousReleaseChannel = previousChannel ?? instance.value.preferred_update_channel
|
||||||
|
if (channel === instance.value.preferred_update_channel) return
|
||||||
|
|
||||||
|
savingReleaseChannel.value = true
|
||||||
|
const profilePath = instance.value.path
|
||||||
|
await edit(profilePath, { preferred_update_channel: channel })
|
||||||
|
.then(() => queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', profilePath] }))
|
||||||
|
.catch((error) => {
|
||||||
|
selectedReleaseChannel.value = previousReleaseChannel
|
||||||
|
handleError(error)
|
||||||
|
})
|
||||||
|
savingReleaseChannel.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
async function resetIcon() {
|
||||||
|
icon.value = undefined
|
||||||
|
await edit_icon(instance.value.path, null).catch(handleError)
|
||||||
|
trackEvent('InstanceRemoveIcon')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setIcon() {
|
||||||
|
const value = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Image',
|
||||||
|
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!value) return
|
||||||
|
|
||||||
|
icon.value = value
|
||||||
|
await edit_icon(instance.value.path, icon.value).catch(handleError)
|
||||||
|
|
||||||
|
trackEvent('InstanceSetIcon')
|
||||||
|
}
|
||||||
|
|
||||||
|
const editProfileObject = computed(() => ({
|
||||||
|
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
||||||
|
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const toggleGroup = (group: string) => {
|
||||||
|
if (groups.value.includes(group)) {
|
||||||
|
groups.value = groups.value.filter((x) => x !== group)
|
||||||
|
} else {
|
||||||
|
groups.value.push(group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCategory = () => {
|
||||||
|
const text = newCategoryInput.value.trim()
|
||||||
|
|
||||||
|
if (text.length > 0) {
|
||||||
|
groups.value.push(text.substring(0, 32))
|
||||||
|
newCategoryInput.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[title, groups, groups],
|
||||||
|
async () => {
|
||||||
|
if (removing.value) return
|
||||||
|
await edit(instance.value.path, editProfileObject.value).catch(handleError)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const removing = ref(false)
|
const removing = ref(false)
|
||||||
async function removeProfile() {
|
async function removeProfile() {
|
||||||
removing.value = true
|
removing.value = true
|
||||||
await remove(props.instance.path).catch(handleError)
|
const path = instance.value.path
|
||||||
removing.value = false
|
|
||||||
|
|
||||||
trackEvent('InstanceRemove', {
|
trackEvent('InstanceRemove', {
|
||||||
loader: props.instance.loader,
|
loader: instance.value.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: instance.value.game_version,
|
||||||
})
|
})
|
||||||
|
|
||||||
await router.push({ path: '/' })
|
await router.push({ path: '/' })
|
||||||
|
await remove(path).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
name: {
|
name: {
|
||||||
id: 'instance.settings.tabs.general.name',
|
id: 'instance.settings.tabs.general.name',
|
||||||
defaultMessage: 'Name',
|
defaultMessage: 'Name',
|
||||||
},
|
},
|
||||||
libraryGroups: {
|
libraryGroups: {
|
||||||
id: 'instance.settings.tabs.general.library-groups',
|
id: 'instance.settings.tabs.general.library-groups',
|
||||||
defaultMessage: 'Library groups',
|
defaultMessage: 'Library groups',
|
||||||
},
|
},
|
||||||
libraryGroupsDescription: {
|
libraryGroupsDescription: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.description',
|
id: 'instance.settings.tabs.general.library-groups.description',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Library groups allow you to organize your instances into different sections in your library.',
|
'Library groups allow you to organize your instances into different sections in your library.',
|
||||||
},
|
},
|
||||||
libraryGroupsEnterName: {
|
libraryGroupsEnterName: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.enter-name',
|
id: 'instance.settings.tabs.general.library-groups.enter-name',
|
||||||
defaultMessage: 'Enter group name',
|
defaultMessage: 'Enter group name',
|
||||||
},
|
},
|
||||||
libraryGroupsCreate: {
|
libraryGroupsCreate: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.create',
|
id: 'instance.settings.tabs.general.library-groups.create',
|
||||||
defaultMessage: 'Create new group',
|
defaultMessage: 'Create new group',
|
||||||
},
|
},
|
||||||
editIcon: {
|
editIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon',
|
id: 'instance.settings.tabs.general.edit-icon',
|
||||||
defaultMessage: 'Edit icon',
|
defaultMessage: 'Edit icon',
|
||||||
},
|
},
|
||||||
selectIcon: {
|
selectIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.select',
|
id: 'instance.settings.tabs.general.edit-icon.select',
|
||||||
defaultMessage: 'Select icon',
|
defaultMessage: 'Select icon',
|
||||||
},
|
},
|
||||||
replaceIcon: {
|
replaceIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.replace',
|
id: 'instance.settings.tabs.general.edit-icon.replace',
|
||||||
defaultMessage: 'Replace icon',
|
defaultMessage: 'Replace icon',
|
||||||
},
|
},
|
||||||
removeIcon: {
|
removeIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.remove',
|
id: 'instance.settings.tabs.general.edit-icon.remove',
|
||||||
defaultMessage: 'Remove icon',
|
defaultMessage: 'Remove icon',
|
||||||
},
|
},
|
||||||
duplicateInstance: {
|
duplicateInstance: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-instance',
|
id: 'instance.settings.tabs.general.duplicate-instance',
|
||||||
defaultMessage: 'Duplicate instance',
|
defaultMessage: 'Duplicate instance',
|
||||||
},
|
},
|
||||||
duplicateInstanceDescription: {
|
duplicateInstanceDescription: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-instance.description',
|
id: 'instance.settings.tabs.general.duplicate-instance.description',
|
||||||
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
|
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
|
||||||
},
|
},
|
||||||
duplicateButtonTooltipInstalling: {
|
duplicateButtonTooltipInstalling: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
|
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
|
||||||
defaultMessage: 'Cannot duplicate while installing.',
|
defaultMessage: 'Cannot duplicate while installing.',
|
||||||
},
|
},
|
||||||
duplicateButton: {
|
duplicateButton: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-button',
|
id: 'instance.settings.tabs.general.duplicate-button',
|
||||||
defaultMessage: 'Duplicate',
|
defaultMessage: 'Duplicate',
|
||||||
},
|
},
|
||||||
deleteInstance: {
|
updateChannel: {
|
||||||
id: 'instance.settings.tabs.general.delete',
|
id: 'instance.settings.tabs.general.update-channel',
|
||||||
defaultMessage: 'Delete instance',
|
defaultMessage: 'Update channel',
|
||||||
},
|
},
|
||||||
deleteInstanceDescription: {
|
updateChannelReleaseDescription: {
|
||||||
id: 'instance.settings.tabs.general.delete.description',
|
id: 'instance.settings.tabs.general.update-channel.release.description',
|
||||||
defaultMessage:
|
defaultMessage: 'Only release versions will be shown as available updates.',
|
||||||
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
|
},
|
||||||
},
|
updateChannelBetaDescription: {
|
||||||
deleteInstanceButton: {
|
id: 'instance.settings.tabs.general.update-channel.beta.description',
|
||||||
id: 'instance.settings.tabs.general.delete.button',
|
defaultMessage: 'Release and beta versions will be shown as available updates.',
|
||||||
defaultMessage: 'Delete instance',
|
},
|
||||||
},
|
updateChannelAlphaDescription: {
|
||||||
deletingInstanceButton: {
|
id: 'instance.settings.tabs.general.update-channel.alpha.description',
|
||||||
id: 'instance.settings.tabs.general.deleting.button',
|
defaultMessage: 'Release, beta, and alpha versions will be shown as available updates.',
|
||||||
defaultMessage: 'Deleting...',
|
},
|
||||||
},
|
updateChannelRelease: {
|
||||||
|
id: 'instance.settings.tabs.general.update-channel.release',
|
||||||
|
defaultMessage: 'Release',
|
||||||
|
},
|
||||||
|
updateChannelBeta: {
|
||||||
|
id: 'instance.settings.tabs.general.update-channel.beta',
|
||||||
|
defaultMessage: 'Beta',
|
||||||
|
},
|
||||||
|
updateChannelAlpha: {
|
||||||
|
id: 'instance.settings.tabs.general.update-channel.alpha',
|
||||||
|
defaultMessage: 'Alpha',
|
||||||
|
},
|
||||||
|
selectUpdateChannelAriaLabel: {
|
||||||
|
id: 'instance.settings.tabs.general.update-channel.select',
|
||||||
|
defaultMessage: 'Select update channel',
|
||||||
|
},
|
||||||
|
deleteInstance: {
|
||||||
|
id: 'instance.settings.tabs.general.delete',
|
||||||
|
defaultMessage: 'Delete instance',
|
||||||
|
},
|
||||||
|
deleteInstanceDescription: {
|
||||||
|
id: 'instance.settings.tabs.general.delete.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
|
||||||
|
},
|
||||||
|
deleteInstanceButton: {
|
||||||
|
id: 'instance.settings.tabs.general.delete.button',
|
||||||
|
defaultMessage: 'Delete instance',
|
||||||
|
},
|
||||||
|
deletingInstanceButton: {
|
||||||
|
id: 'instance.settings.tabs.general.deleting.button',
|
||||||
|
defaultMessage: 'Deleting...',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModalWrapper
|
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="removeProfile" />
|
||||||
ref="deleteConfirmModal"
|
<div class="block">
|
||||||
title="Are you sure you want to delete this instance?"
|
<div class="float-end ml-10 relative group w-fit">
|
||||||
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
|
<div class="flex flex-col gap-1">
|
||||||
:has-to-type="false"
|
<span class="text-lg font-semibold text-contrast">Icon</span>
|
||||||
proceed-label="Delete"
|
<div class="group relative w-fit">
|
||||||
:show-ad-on-close="false"
|
<OverflowMenu
|
||||||
@proceed="removeProfile"
|
v-tooltip="formatMessage(messages.editIcon)"
|
||||||
/>
|
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
||||||
<div class="block">
|
:options="[
|
||||||
<div class="float-end ml-4 relative group">
|
{
|
||||||
<OverflowMenu
|
id: 'select',
|
||||||
v-tooltip="formatMessage(messages.editIcon)"
|
action: () => setIcon(),
|
||||||
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
},
|
||||||
:options="[
|
{
|
||||||
{
|
id: 'remove',
|
||||||
id: 'select',
|
color: 'danger',
|
||||||
action: () => setIcon(),
|
action: () => resetIcon(),
|
||||||
},
|
shown: !!icon,
|
||||||
{
|
},
|
||||||
id: 'remove',
|
]"
|
||||||
color: 'danger',
|
>
|
||||||
action: () => resetIcon(),
|
<Avatar
|
||||||
shown: !!icon,
|
:src="icon ? convertFileSrc(icon) : icon"
|
||||||
},
|
size="108px"
|
||||||
]"
|
class="transition-[filter] group-hover:brightness-75"
|
||||||
>
|
:tint-by="instance.path"
|
||||||
<Avatar
|
no-shadow
|
||||||
:src="icon ? convertFileSrc(icon) : icon"
|
/>
|
||||||
size="108px"
|
<div
|
||||||
class="!border-4 group-hover:brightness-75"
|
class="absolute top-0 h-full w-full flex items-center justify-center opacity-0 transition-all group-hover:opacity-100"
|
||||||
:tint-by="props.instance.path"
|
>
|
||||||
no-shadow
|
<EditIcon aria-hidden="true" class="h-10 w-10 text-primary" />
|
||||||
/>
|
</div>
|
||||||
<div class="absolute top-0 right-0 m-2">
|
<template #select>
|
||||||
<div
|
<UploadIcon />
|
||||||
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
|
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
||||||
>
|
</template>
|
||||||
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
|
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||||
</div>
|
</OverflowMenu>
|
||||||
</div>
|
</div>
|
||||||
<template #select>
|
</div>
|
||||||
<UploadIcon />
|
</div>
|
||||||
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
<label for="instance-name" class="m-0 text-lg font-semibold text-contrast block">
|
||||||
</template>
|
{{ formatMessage(messages.name) }}
|
||||||
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
</label>
|
||||||
</OverflowMenu>
|
<div class="flex">
|
||||||
</div>
|
<StyledInput
|
||||||
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
id="instance-name"
|
||||||
{{ formatMessage(messages.name) }}
|
v-model="title"
|
||||||
</label>
|
autocomplete="off"
|
||||||
<div class="flex">
|
:maxlength="80"
|
||||||
<input
|
wrapper-class="flex-grow"
|
||||||
id="instance-name"
|
/>
|
||||||
v-model="title"
|
</div>
|
||||||
autocomplete="off"
|
<template v-if="instance.install_stage == 'installed'">
|
||||||
maxlength="80"
|
<div class="flex flex-col gap-2.5 mt-6">
|
||||||
class="flex-grow"
|
<h2 id="duplicate-instance-label" class="m-0 text-lg font-semibold text-contrast block">
|
||||||
type="text"
|
{{ formatMessage(messages.duplicateInstance) }}
|
||||||
/>
|
</h2>
|
||||||
</div>
|
<ButtonStyled>
|
||||||
<template v-if="instance.install_stage == 'installed'">
|
<button
|
||||||
<div>
|
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||||
<h2
|
aria-labelledby="duplicate-instance-label"
|
||||||
id="duplicate-instance-label"
|
:disabled="installing"
|
||||||
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
|
class="w-max !shadow-none"
|
||||||
>
|
@click="duplicateProfile"
|
||||||
{{ formatMessage(messages.duplicateInstance) }}
|
>
|
||||||
</h2>
|
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||||
<p class="m-0 mb-2">
|
</button>
|
||||||
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
</ButtonStyled>
|
||||||
</p>
|
<p class="m-0">
|
||||||
</div>
|
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
||||||
<ButtonStyled>
|
</p>
|
||||||
<button
|
</div>
|
||||||
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
</template>
|
||||||
aria-labelledby="duplicate-instance-label"
|
<div class="flex flex-col gap-2.5 mt-6">
|
||||||
:disabled="installing"
|
<h2 class="m-0 text-lg font-semibold text-contrast block">
|
||||||
@click="duplicateProfile"
|
{{ formatMessage(messages.libraryGroups) }}
|
||||||
>
|
</h2>
|
||||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
|
||||||
</button>
|
<div class="flex flex-col gap-1">
|
||||||
</ButtonStyled>
|
<Checkbox
|
||||||
</template>
|
v-for="group in availableGroups"
|
||||||
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
:key="group"
|
||||||
{{ formatMessage(messages.libraryGroups) }}
|
:model-value="groups.includes(group)"
|
||||||
</h2>
|
:label="group"
|
||||||
<p class="m-0 mb-2">
|
@click="toggleGroup(group)"
|
||||||
{{ formatMessage(messages.libraryGroupsDescription) }}
|
/>
|
||||||
</p>
|
<div class="flex gap-2 items-center">
|
||||||
<div class="flex flex-col gap-1">
|
<StyledInput
|
||||||
<Checkbox
|
v-model="newCategoryInput"
|
||||||
v-for="group in availableGroups"
|
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||||
:key="group"
|
class="w-full max-w-[300px]"
|
||||||
:model-value="groups.includes(group)"
|
@submit="() => addCategory"
|
||||||
:label="group"
|
/>
|
||||||
@click="toggleGroup(group)"
|
<ButtonStyled>
|
||||||
/>
|
<button class="w-fit !shadow-none" @click="() => addCategory()">
|
||||||
<div class="flex gap-2 items-center">
|
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||||
<input
|
</button>
|
||||||
v-model="newCategoryInput"
|
</ButtonStyled>
|
||||||
type="text"
|
</div>
|
||||||
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
</div>
|
||||||
@submit="() => addCategory"
|
<p class="m-0">
|
||||||
/>
|
{{ formatMessage(messages.libraryGroupsDescription) }}
|
||||||
<ButtonStyled>
|
</p>
|
||||||
<button class="w-fit" @click="() => addCategory()">
|
</div>
|
||||||
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
|
||||||
</button>
|
<div class="flex flex-col gap-2.5 mt-6">
|
||||||
</ButtonStyled>
|
<h2 class="m-0 text-lg font-semibold text-contrast block">
|
||||||
</div>
|
{{ formatMessage(messages.updateChannel) }}
|
||||||
</div>
|
</h2>
|
||||||
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<Chips
|
||||||
{{ formatMessage(messages.deleteInstance) }}
|
v-model="selectedReleaseChannel"
|
||||||
</h2>
|
:items="releaseChannelOptions"
|
||||||
<p class="m-0 mb-2">
|
:format-label="formatReleaseChannelLabel"
|
||||||
{{ formatMessage(messages.deleteInstanceDescription) }}
|
:capitalize="false"
|
||||||
</p>
|
:disabled-items="releaseChannelDisabledItems"
|
||||||
<ButtonStyled color="red">
|
:aria-label="formatMessage(messages.selectUpdateChannelAriaLabel)"
|
||||||
<button
|
/>
|
||||||
aria-labelledby="delete-instance-label"
|
<p class="m-0">
|
||||||
:disabled="removing"
|
{{ formatReleaseChannelDescription(selectedReleaseChannel) }}
|
||||||
@click="deleteConfirmModal.show()"
|
</p>
|
||||||
>
|
</div>
|
||||||
<SpinnerIcon v-if="removing" class="animate-spin" />
|
|
||||||
<TrashIcon v-else />
|
<div class="flex flex-col gap-2.5 mt-6">
|
||||||
{{
|
<h2 id="delete-instance-label" class="m-0 text-lg font-semibold text-contrast block">
|
||||||
removing
|
{{ formatMessage(messages.deleteInstance) }}
|
||||||
? formatMessage(messages.deletingInstanceButton)
|
</h2>
|
||||||
: formatMessage(messages.deleteInstanceButton)
|
<ButtonStyled color="red">
|
||||||
}}
|
<button
|
||||||
</button>
|
aria-labelledby="delete-instance-label"
|
||||||
</ButtonStyled>
|
:disabled="removing"
|
||||||
</div>
|
class="w-fit !shadow-none"
|
||||||
|
@click="deleteConfirmModal.show()"
|
||||||
|
>
|
||||||
|
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||||
|
<TrashIcon v-else />
|
||||||
|
{{
|
||||||
|
removing
|
||||||
|
? formatMessage(messages.deletingInstanceButton)
|
||||||
|
: formatMessage(messages.deleteInstanceButton)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<p class="m-0">
|
||||||
|
{{ formatMessage(messages.deleteInstanceDescription) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.hovering-icon-shadow {
|
.hovering-icon-shadow {
|
||||||
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,152 +1,157 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox } from '@modrinth/ui'
|
import {
|
||||||
|
Checkbox,
|
||||||
|
defineMessages,
|
||||||
|
injectNotificationManager,
|
||||||
|
StyledInput,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import { edit } from '@/helpers/profile'
|
|
||||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
|
import type { AppSettings, Hooks } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance } = injectInstanceSettings()
|
||||||
|
|
||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
const overrideHooks = ref(
|
const overrideHooks = ref(
|
||||||
!!props.instance.hooks.pre_launch ||
|
!!instance.value.hooks.pre_launch ||
|
||||||
!!props.instance.hooks.wrapper ||
|
!!instance.value.hooks.wrapper ||
|
||||||
!!props.instance.hooks.post_exit,
|
!!instance.value.hooks.post_exit,
|
||||||
)
|
)
|
||||||
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
|
const hooks = ref(instance.value.hooks ?? globalSettings.hooks)
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
const editProfile: {
|
||||||
hooks?: Hooks
|
hooks?: Hooks
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
// When hooks are not overridden per-instance, we want to clear them
|
// When hooks are not overridden per-instance, we want to clear them
|
||||||
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[overrideHooks, hooks],
|
[overrideHooks, hooks],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(instance.value.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
hooks: {
|
hooks: {
|
||||||
id: 'instance.settings.tabs.hooks.title',
|
id: 'instance.settings.tabs.hooks.title',
|
||||||
defaultMessage: 'Game launch hooks',
|
defaultMessage: 'Game launch hooks',
|
||||||
},
|
},
|
||||||
hooksDescription: {
|
hooksDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.description',
|
id: 'instance.settings.tabs.hooks.description',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
||||||
},
|
},
|
||||||
customHooks: {
|
customHooks: {
|
||||||
id: 'instance.settings.tabs.hooks.custom-hooks',
|
id: 'instance.settings.tabs.hooks.custom-hooks',
|
||||||
defaultMessage: 'Custom launch hooks',
|
defaultMessage: 'Custom launch hooks',
|
||||||
},
|
},
|
||||||
preLaunch: {
|
preLaunch: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch',
|
id: 'instance.settings.tabs.hooks.pre-launch',
|
||||||
defaultMessage: 'Pre-launch',
|
defaultMessage: 'Pre-launch',
|
||||||
},
|
},
|
||||||
preLaunchDescription: {
|
preLaunchDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch.description',
|
id: 'instance.settings.tabs.hooks.pre-launch.description',
|
||||||
defaultMessage: 'Ran before the instance is launched.',
|
defaultMessage: 'Ran before the instance is launched.',
|
||||||
},
|
},
|
||||||
preLaunchEnter: {
|
preLaunchEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch.enter',
|
id: 'instance.settings.tabs.hooks.pre-launch.enter',
|
||||||
defaultMessage: 'Enter pre-launch command...',
|
defaultMessage: 'Enter pre-launch command...',
|
||||||
},
|
},
|
||||||
wrapper: {
|
wrapper: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper',
|
id: 'instance.settings.tabs.hooks.wrapper',
|
||||||
defaultMessage: 'Wrapper',
|
defaultMessage: 'Wrapper',
|
||||||
},
|
},
|
||||||
wrapperDescription: {
|
wrapperDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper.description',
|
id: 'instance.settings.tabs.hooks.wrapper.description',
|
||||||
defaultMessage: 'Wrapper command for launching Minecraft.',
|
defaultMessage: 'Wrapper command for launching Minecraft.',
|
||||||
},
|
},
|
||||||
wrapperEnter: {
|
wrapperEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper.enter',
|
id: 'instance.settings.tabs.hooks.wrapper.enter',
|
||||||
defaultMessage: 'Enter wrapper command...',
|
defaultMessage: 'Enter wrapper command...',
|
||||||
},
|
},
|
||||||
postExit: {
|
postExit: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit',
|
id: 'instance.settings.tabs.hooks.post-exit',
|
||||||
defaultMessage: 'Post-exit',
|
defaultMessage: 'Post-exit',
|
||||||
},
|
},
|
||||||
postExitDescription: {
|
postExitDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit.description',
|
id: 'instance.settings.tabs.hooks.post-exit.description',
|
||||||
defaultMessage: 'Ran after the game closes.',
|
defaultMessage: 'Ran after the game closes.',
|
||||||
},
|
},
|
||||||
postExitEnter: {
|
postExitEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit.enter',
|
id: 'instance.settings.tabs.hooks.post-exit.enter',
|
||||||
defaultMessage: 'Enter post-exit command...',
|
defaultMessage: 'Enter post-exit command...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 m-0 text-lg font-semibold text-contrast">
|
||||||
{{ formatMessage(messages.hooks) }}
|
{{ formatMessage(messages.hooks) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="my-2.5" />
|
||||||
{{ formatMessage(messages.hooksDescription) }}
|
<p class="m-0">
|
||||||
</p>
|
{{ formatMessage(messages.hooksDescription) }}
|
||||||
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
|
</p>
|
||||||
|
|
||||||
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||||
{{ formatMessage(messages.preLaunch) }}
|
{{ formatMessage(messages.preLaunch) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<StyledInput
|
||||||
{{ formatMessage(messages.preLaunchDescription) }}
|
id="pre-launch"
|
||||||
</p>
|
v-model="hooks.pre_launch"
|
||||||
<input
|
autocomplete="off"
|
||||||
id="pre-launch"
|
:disabled="!overrideHooks"
|
||||||
v-model="hooks.pre_launch"
|
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||||
autocomplete="off"
|
wrapper-class="w-full my-2.5"
|
||||||
:disabled="!overrideHooks"
|
/>
|
||||||
type="text"
|
<p class="m-0">
|
||||||
:placeholder="formatMessage(messages.preLaunchEnter)"
|
{{ formatMessage(messages.preLaunchDescription) }}
|
||||||
class="w-full mt-2"
|
</p>
|
||||||
/>
|
|
||||||
|
|
||||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||||
{{ formatMessage(messages.wrapper) }}
|
{{ formatMessage(messages.wrapper) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<StyledInput
|
||||||
{{ formatMessage(messages.wrapperDescription) }}
|
id="wrapper"
|
||||||
</p>
|
v-model="hooks.wrapper"
|
||||||
<input
|
autocomplete="off"
|
||||||
id="wrapper"
|
:disabled="!overrideHooks"
|
||||||
v-model="hooks.wrapper"
|
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||||
autocomplete="off"
|
wrapper-class="w-full my-2.5"
|
||||||
:disabled="!overrideHooks"
|
/>
|
||||||
type="text"
|
<p class="m-0">
|
||||||
:placeholder="formatMessage(messages.wrapperEnter)"
|
{{ formatMessage(messages.wrapperDescription) }}
|
||||||
class="w-full mt-2"
|
</p>
|
||||||
/>
|
|
||||||
|
|
||||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||||
{{ formatMessage(messages.postExit) }}
|
{{ formatMessage(messages.postExit) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<StyledInput
|
||||||
{{ formatMessage(messages.postExitDescription) }}
|
id="post-exit"
|
||||||
</p>
|
v-model="hooks.post_exit"
|
||||||
<input
|
autocomplete="off"
|
||||||
id="post-exit"
|
:disabled="!overrideHooks"
|
||||||
v-model="hooks.post_exit"
|
:placeholder="formatMessage(messages.postExitEnter)"
|
||||||
autocomplete="off"
|
wrapper-class="w-full my-2.5"
|
||||||
:disabled="!overrideHooks"
|
/>
|
||||||
type="text"
|
<p class="m-0">
|
||||||
:placeholder="formatMessage(messages.postExitEnter)"
|
{{ formatMessage(messages.postExitDescription) }}
|
||||||
class="w-full mt-2"
|
</p>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,188 +1,322 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox, Slider } from '@modrinth/ui'
|
import {
|
||||||
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
CheckCircleIcon,
|
||||||
|
CoffeeIcon,
|
||||||
|
FolderSearchIcon,
|
||||||
|
RefreshCwIcon,
|
||||||
|
SearchIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
ButtonStyled,
|
||||||
|
Checkbox,
|
||||||
|
defineMessages,
|
||||||
|
injectNotificationManager,
|
||||||
|
Slider,
|
||||||
|
StyledInput,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { computed, readonly, ref, watch } from 'vue'
|
import { computed, readonly, ref, watch } from 'vue'
|
||||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||||
|
import useJavaTest from '@/composables/useJavaTest'
|
||||||
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
|
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
|
import type { AppSettings } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance } = injectInstanceSettings()
|
||||||
|
|
||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
|
||||||
|
|
||||||
const overrideJavaInstall = ref(!!props.instance.java_path)
|
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
|
||||||
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
|
||||||
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
|
||||||
|
|
||||||
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
const overrideJavaInstall = ref(!!instance.value.java_path)
|
||||||
|
const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '')
|
||||||
|
|
||||||
|
const activePath = computed(() =>
|
||||||
|
overrideJavaInstall.value ? javaPath.value : (optimalJava?.path ?? ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(overrideJavaInstall, (enabled) => {
|
||||||
|
if (enabled && !javaPath.value) {
|
||||||
|
javaPath.value = optimalJava?.path ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { testingJava, javaTestResult, testJavaInstallationDebounced, testJavaInstallation } =
|
||||||
|
useJavaTest()
|
||||||
|
|
||||||
|
const hoveringTest = ref(false)
|
||||||
|
let hasInitialized = false
|
||||||
|
|
||||||
|
watch(
|
||||||
|
activePath,
|
||||||
|
(newPath) => {
|
||||||
|
if (newPath && optimalJava?.parsed_version) {
|
||||||
|
if (!hasInitialized) {
|
||||||
|
testJavaInstallation(newPath, optimalJava?.parsed_version, false)
|
||||||
|
hasInitialized = true
|
||||||
|
} else {
|
||||||
|
testJavaInstallationDebounced(newPath, optimalJava?.parsed_version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const javaDetectionModal = ref<{ show: (version: number, current: object) => void } | null>(null)
|
||||||
|
|
||||||
|
async function handleBrowseJava() {
|
||||||
|
const result = await open({ multiple: false })
|
||||||
|
if (result) {
|
||||||
|
javaPath.value = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDetectJava() {
|
||||||
|
javaDetectionModal.value?.show(optimalJava?.parsed_version, { path: javaPath.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const overrideJavaArgs = ref((instance.value.extra_launch_args?.length ?? 0) > 0)
|
||||||
const javaArgs = ref(
|
const javaArgs = ref(
|
||||||
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
(instance.value.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
||||||
)
|
)
|
||||||
|
|
||||||
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
const overrideEnvVars = ref((instance.value.custom_env_vars?.length ?? 0) > 0)
|
||||||
const envVars = ref(
|
const envVars = ref(
|
||||||
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
(instance.value.custom_env_vars ?? globalSettings.custom_env_vars)
|
||||||
.map((x) => x.join('='))
|
.map((x) => x.join('='))
|
||||||
.join(' '),
|
.join(' '),
|
||||||
)
|
)
|
||||||
|
|
||||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
const overrideMemorySettings = ref(!!instance.value.memory)
|
||||||
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
const memory = ref(instance.value.memory ?? globalSettings.memory)
|
||||||
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
|
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||||
|
maxMemory: number
|
||||||
|
snapPoints: number[]
|
||||||
|
}
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
return {
|
||||||
java_path?: string
|
java_path:
|
||||||
extra_launch_args?: string[]
|
overrideJavaInstall.value && javaPath.value
|
||||||
custom_env_vars?: string[][]
|
? javaPath.value.replace('java.exe', 'javaw.exe')
|
||||||
memory?: MemorySettings
|
: null,
|
||||||
} = {}
|
extra_launch_args: overrideJavaArgs.value
|
||||||
|
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||||
if (overrideJavaInstall.value) {
|
: null,
|
||||||
if (javaInstall.value.path !== '') {
|
custom_env_vars: overrideEnvVars.value
|
||||||
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
? envVars.value
|
||||||
}
|
.trim()
|
||||||
}
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
if (overrideJavaArgs.value) {
|
.map((x) => x.split('=').filter(Boolean))
|
||||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
: null,
|
||||||
}
|
memory: overrideMemorySettings.value ? memory.value : null,
|
||||||
|
}
|
||||||
if (overrideEnvVars.value) {
|
|
||||||
editProfile.custom_env_vars = envVars.value
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((x) => x.split('=').filter(Boolean))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideMemorySettings.value) {
|
|
||||||
editProfile.memory = memory.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return editProfile
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
overrideJavaInstall,
|
overrideJavaInstall,
|
||||||
javaInstall,
|
javaPath,
|
||||||
overrideJavaArgs,
|
overrideJavaArgs,
|
||||||
javaArgs,
|
javaArgs,
|
||||||
overrideEnvVars,
|
overrideEnvVars,
|
||||||
envVars,
|
envVars,
|
||||||
overrideMemorySettings,
|
overrideMemorySettings,
|
||||||
memory,
|
memory,
|
||||||
],
|
],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(instance.value.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
javaInstallation: {
|
javaInstallation: {
|
||||||
id: 'instance.settings.tabs.java.java-installation',
|
id: 'instance.settings.tabs.java.java-installation',
|
||||||
defaultMessage: 'Java installation',
|
defaultMessage: 'Java installation',
|
||||||
},
|
},
|
||||||
javaArguments: {
|
customJavaInstallation: {
|
||||||
id: 'instance.settings.tabs.java.java-arguments',
|
id: 'instance.settings.tabs.java.custom-java-installation',
|
||||||
defaultMessage: 'Java arguments',
|
defaultMessage: 'Custom Java installation',
|
||||||
},
|
},
|
||||||
javaEnvironmentVariables: {
|
javaPathPlaceholder: {
|
||||||
id: 'instance.settings.tabs.java.environment-variables',
|
id: 'instance.settings.tabs.java.java-path-placeholder',
|
||||||
defaultMessage: 'Environment variables',
|
defaultMessage: '/path/to/java',
|
||||||
},
|
},
|
||||||
javaMemory: {
|
javaMemory: {
|
||||||
id: 'instance.settings.tabs.java.java-memory',
|
id: 'instance.settings.tabs.java.java-memory',
|
||||||
defaultMessage: 'Memory allocated',
|
defaultMessage: 'Memory allocated',
|
||||||
},
|
},
|
||||||
hooks: {
|
customMemoryAllocation: {
|
||||||
id: 'instance.settings.tabs.java.hooks',
|
id: 'instance.settings.tabs.java.custom-memory-allocation',
|
||||||
defaultMessage: 'Hooks',
|
defaultMessage: 'Custom memory allocation',
|
||||||
},
|
},
|
||||||
|
javaArguments: {
|
||||||
|
id: 'instance.settings.tabs.java.java-arguments',
|
||||||
|
defaultMessage: 'Java arguments',
|
||||||
|
},
|
||||||
|
customJavaArguments: {
|
||||||
|
id: 'instance.settings.tabs.java.custom-java-arguments',
|
||||||
|
defaultMessage: 'Custom Java arguments',
|
||||||
|
},
|
||||||
|
enterJavaArguments: {
|
||||||
|
id: 'instance.settings.tabs.java.enter-java-arguments',
|
||||||
|
defaultMessage: 'Enter Java arguments...',
|
||||||
|
},
|
||||||
|
javaEnvironmentVariables: {
|
||||||
|
id: 'instance.settings.tabs.java.environment-variables',
|
||||||
|
defaultMessage: 'Environment variables',
|
||||||
|
},
|
||||||
|
customEnvironmentVariables: {
|
||||||
|
id: 'instance.settings.tabs.java.custom-environment-variables',
|
||||||
|
defaultMessage: 'Custom environment variables',
|
||||||
|
},
|
||||||
|
enterEnvironmentVariables: {
|
||||||
|
id: 'instance.settings.tabs.java.enter-environment-variables',
|
||||||
|
defaultMessage: 'Enter environmental variables...',
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
id: 'instance.settings.tabs.java.hooks',
|
||||||
|
defaultMessage: 'Hooks',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
<JavaDetectionModal ref="javaDetectionModal" @submit="(val) => (javaPath = val.path)" />
|
||||||
{{ formatMessage(messages.javaInstallation) }}
|
<h2 class="m-0 mb-2 text-lg font-extrabold text-contrast block">
|
||||||
</h2>
|
{{ formatMessage(messages.javaInstallation) }}
|
||||||
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
|
</h2>
|
||||||
<template v-if="!overrideJavaInstall">
|
<Checkbox
|
||||||
<div class="flex my-2 items-center gap-2 font-semibold">
|
v-model="overrideJavaInstall"
|
||||||
<template v-if="javaInstall">
|
:label="formatMessage(messages.customJavaInstallation)"
|
||||||
<CheckCircleIcon class="text-brand-green h-4 w-4" />
|
class="mb-2"
|
||||||
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
|
/>
|
||||||
</template>
|
<div class="flex gap-4 p-4 bg-bg rounded-2xl">
|
||||||
<template v-else-if="optimalJava">
|
<div class="flex gap-3 items-start flex-1 min-w-0">
|
||||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
<div
|
||||||
<span
|
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 mt-1 shrink-0 [&_svg]:h-full [&_svg]:w-full"
|
||||||
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
|
>
|
||||||
one below:</span
|
<CoffeeIcon />
|
||||||
>
|
</div>
|
||||||
</template>
|
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||||
<template v-else>
|
<span class="font-semibold leading-none mt-2"
|
||||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
>Java {{ optimalJava?.parsed_version }}</span
|
||||||
<span
|
>
|
||||||
>Could not automatically determine a Java installation to use. Please set one
|
<div class="flex gap-2 items-center">
|
||||||
below:</span
|
<StyledInput
|
||||||
>
|
:model-value="activePath"
|
||||||
</template>
|
:disabled="!overrideJavaInstall"
|
||||||
</div>
|
autocomplete="off"
|
||||||
<div
|
:placeholder="formatMessage(messages.javaPathPlaceholder)"
|
||||||
v-if="javaInstall && !overrideJavaInstall"
|
wrapper-class="flex-1 min-w-0"
|
||||||
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
|
@update:model-value="(val) => (javaPath = String(val))"
|
||||||
>
|
/>
|
||||||
{{ javaInstall.path }}
|
<ButtonStyled
|
||||||
</div>
|
:color="
|
||||||
</template>
|
!hoveringTest && !testingJava
|
||||||
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
? javaTestResult === true
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
? 'green'
|
||||||
{{ formatMessage(messages.javaMemory) }}
|
: 'red'
|
||||||
</h2>
|
: 'standard'
|
||||||
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
|
"
|
||||||
<Slider
|
color-fill="text"
|
||||||
id="max-memory"
|
>
|
||||||
v-model="memory.maximum"
|
<button
|
||||||
:disabled="!overrideMemorySettings"
|
:disabled="!overrideJavaInstall || testingJava"
|
||||||
:min="512"
|
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
|
||||||
:max="maxMemory"
|
@mouseenter="overrideJavaInstall && (hoveringTest = true)"
|
||||||
:step="64"
|
@mouseleave="hoveringTest = false"
|
||||||
unit="MB"
|
>
|
||||||
/>
|
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<CheckCircleIcon
|
||||||
{{ formatMessage(messages.javaArguments) }}
|
v-else-if="javaTestResult === true && !hoveringTest"
|
||||||
</h2>
|
class="h-4 w-4"
|
||||||
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
/>
|
||||||
<input
|
<XCircleIcon v-else-if="javaTestResult !== true && !hoveringTest" class="h-4 w-4" />
|
||||||
id="java-args"
|
<RefreshCwIcon v-else-if="overrideJavaInstall" class="h-4 w-4" />
|
||||||
v-model="javaArgs"
|
</button>
|
||||||
autocomplete="off"
|
</ButtonStyled>
|
||||||
:disabled="!overrideJavaArgs"
|
</div>
|
||||||
type="text"
|
<div v-if="overrideJavaInstall" class="flex gap-2">
|
||||||
class="w-full"
|
<ButtonStyled>
|
||||||
placeholder="Enter java arguments..."
|
<button @click="handleDetectJava">
|
||||||
/>
|
<SearchIcon />
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
Detect
|
||||||
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
</button>
|
||||||
</h2>
|
</ButtonStyled>
|
||||||
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
<ButtonStyled>
|
||||||
<input
|
<button @click="handleBrowseJava">
|
||||||
id="env-vars"
|
<FolderSearchIcon />
|
||||||
v-model="envVars"
|
Browse
|
||||||
autocomplete="off"
|
</button>
|
||||||
:disabled="!overrideEnvVars"
|
</ButtonStyled>
|
||||||
type="text"
|
</div>
|
||||||
class="w-full"
|
</div>
|
||||||
placeholder="Enter environmental variables..."
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.javaMemory) }}
|
||||||
|
</h2>
|
||||||
|
<Checkbox
|
||||||
|
v-model="overrideMemorySettings"
|
||||||
|
:label="formatMessage(messages.customMemoryAllocation)"
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
id="max-memory"
|
||||||
|
v-model="memory.maximum"
|
||||||
|
:disabled="!overrideMemorySettings"
|
||||||
|
:min="512"
|
||||||
|
:max="maxMemory"
|
||||||
|
:step="64"
|
||||||
|
:snap-points="snapPoints"
|
||||||
|
:snap-range="512"
|
||||||
|
unit="MB"
|
||||||
|
/>
|
||||||
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.javaArguments) }}
|
||||||
|
</h2>
|
||||||
|
<Checkbox
|
||||||
|
v-model="overrideJavaArgs"
|
||||||
|
:label="formatMessage(messages.customJavaArguments)"
|
||||||
|
class="my-2"
|
||||||
|
/>
|
||||||
|
<StyledInput
|
||||||
|
id="java-args"
|
||||||
|
v-model="javaArgs"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideJavaArgs"
|
||||||
|
:placeholder="formatMessage(messages.enterJavaArguments)"
|
||||||
|
wrapper-class="w-full"
|
||||||
|
/>
|
||||||
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
||||||
|
</h2>
|
||||||
|
<Checkbox
|
||||||
|
v-model="overrideEnvVars"
|
||||||
|
:label="formatMessage(messages.customEnvironmentVariables)"
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
|
<StyledInput
|
||||||
|
id="env-vars"
|
||||||
|
v-model="envVars"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="!overrideEnvVars"
|
||||||
|
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
|
||||||
|
wrapper-class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,164 +1,161 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox, Toggle } from '@modrinth/ui'
|
import {
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
Checkbox,
|
||||||
import { handleError } from '@/store/notifications'
|
defineMessages,
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
injectNotificationManager,
|
||||||
import { get } from '@/helpers/settings.ts'
|
StyledInput,
|
||||||
import { edit } from '@/helpers/profile'
|
Toggle,
|
||||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { computed, type Ref, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
|
import type { AppSettings } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const { instance } = injectInstanceSettings()
|
||||||
|
|
||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
const overrideWindowSettings = ref(
|
const overrideWindowSettings = ref(
|
||||||
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
!!instance.value.game_resolution || !!instance.value.force_fullscreen,
|
||||||
)
|
)
|
||||||
const resolution: Ref<[number, number]> = ref(
|
const resolution: Ref<[number, number]> = ref(
|
||||||
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
instance.value.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
||||||
)
|
)
|
||||||
const fullscreenSetting: Ref<boolean> = ref(
|
const fullscreenSetting: Ref<boolean> = ref(
|
||||||
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
instance.value.force_fullscreen ?? globalSettings.force_fullscreen,
|
||||||
)
|
)
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
if (!overrideWindowSettings.value) {
|
||||||
force_fullscreen?: boolean
|
return {
|
||||||
game_resolution?: [number, number]
|
force_fullscreen: null,
|
||||||
} = {}
|
game_resolution: null,
|
||||||
|
}
|
||||||
if (overrideWindowSettings.value) {
|
}
|
||||||
editProfile.force_fullscreen = fullscreenSetting.value
|
return {
|
||||||
|
force_fullscreen: fullscreenSetting.value,
|
||||||
if (!fullscreenSetting.value) {
|
game_resolution: fullscreenSetting.value ? null : resolution.value,
|
||||||
editProfile.game_resolution = resolution.value
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return editProfile
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[overrideWindowSettings, resolution, fullscreenSetting],
|
[overrideWindowSettings, resolution, fullscreenSetting],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(instance.value.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
customWindowSettings: {
|
customWindowSettings: {
|
||||||
id: 'instance.settings.tabs.window.custom-window-settings',
|
id: 'instance.settings.tabs.window.custom-window-settings',
|
||||||
defaultMessage: 'Custom window settings',
|
defaultMessage: 'Custom window settings',
|
||||||
},
|
},
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
id: 'instance.settings.tabs.window.fullscreen',
|
id: 'instance.settings.tabs.window.fullscreen',
|
||||||
defaultMessage: 'Fullscreen',
|
defaultMessage: 'Fullscreen',
|
||||||
},
|
},
|
||||||
fullscreenDescription: {
|
fullscreenDescription: {
|
||||||
id: 'instance.settings.tabs.window.fullscreen.description',
|
id: 'instance.settings.tabs.window.fullscreen.description',
|
||||||
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
|
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
id: 'instance.settings.tabs.window.width',
|
id: 'instance.settings.tabs.window.width',
|
||||||
defaultMessage: 'Width',
|
defaultMessage: 'Width',
|
||||||
},
|
},
|
||||||
widthDescription: {
|
widthDescription: {
|
||||||
id: 'instance.settings.tabs.window.width.description',
|
id: 'instance.settings.tabs.window.width.description',
|
||||||
defaultMessage: 'The width of the game window when launched.',
|
defaultMessage: 'The width of the game window when launched.',
|
||||||
},
|
},
|
||||||
enterWidth: {
|
enterWidth: {
|
||||||
id: 'instance.settings.tabs.window.width.enter',
|
id: 'instance.settings.tabs.window.width.enter',
|
||||||
defaultMessage: 'Enter width...',
|
defaultMessage: 'Enter width...',
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
id: 'instance.settings.tabs.window.height',
|
id: 'instance.settings.tabs.window.height',
|
||||||
defaultMessage: 'Height',
|
defaultMessage: 'Height',
|
||||||
},
|
},
|
||||||
heightDescription: {
|
heightDescription: {
|
||||||
id: 'instance.settings.tabs.window.height.description',
|
id: 'instance.settings.tabs.window.height.description',
|
||||||
defaultMessage: 'The height of the game window when launched.',
|
defaultMessage: 'The height of the game window when launched.',
|
||||||
},
|
},
|
||||||
enterHeight: {
|
enterHeight: {
|
||||||
id: 'instance.settings.tabs.window.height.enter',
|
id: 'instance.settings.tabs.window.height.enter',
|
||||||
defaultMessage: 'Enter height...',
|
defaultMessage: 'Enter height...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex flex-col gap-6">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="overrideWindowSettings"
|
v-model="overrideWindowSettings"
|
||||||
:label="formatMessage(messages.customWindowSettings)"
|
:label="formatMessage(messages.customWindowSettings)"
|
||||||
@update:model-value="
|
/>
|
||||||
(value) => {
|
<div class="flex items-center gap-4 justify-between">
|
||||||
if (!value) {
|
<div class="flex flex-col gap-1">
|
||||||
resolution = globalSettings.game_resolution
|
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||||
fullscreenSetting = globalSettings.force_fullscreen
|
{{ formatMessage(messages.fullscreen) }}
|
||||||
}
|
</h2>
|
||||||
}
|
<p class="m-0">
|
||||||
"
|
{{ formatMessage(messages.fullscreenDescription) }}
|
||||||
/>
|
</p>
|
||||||
<div class="mt-2 flex items-center gap-4 justify-between">
|
</div>
|
||||||
<div>
|
<Toggle
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
id="fullscreen"
|
||||||
{{ formatMessage(messages.fullscreen) }}
|
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
|
||||||
</h2>
|
:disabled="!overrideWindowSettings"
|
||||||
<p class="m-0">
|
@update:model-value="
|
||||||
{{ formatMessage(messages.fullscreenDescription) }}
|
(e) => {
|
||||||
</p>
|
fullscreenSetting = e
|
||||||
</div>
|
}
|
||||||
<Toggle
|
"
|
||||||
id="fullscreen"
|
/>
|
||||||
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
|
</div>
|
||||||
:disabled="!overrideWindowSettings"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
fullscreenSetting = e
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
<div class="flex items-center gap-4 justify-between">
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||||
{{ formatMessage(messages.width) }}
|
{{ formatMessage(messages.width) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.widthDescription) }}
|
{{ formatMessage(messages.widthDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<StyledInput
|
||||||
id="width"
|
id="width"
|
||||||
v-model="resolution[0]"
|
v-model="resolution[0]"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="formatMessage(messages.enterWidth)"
|
:placeholder="formatMessage(messages.enterWidth)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
<div class="flex items-center gap-4 justify-between">
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||||
{{ formatMessage(messages.height) }}
|
{{ formatMessage(messages.height) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.heightDescription) }}
|
{{ formatMessage(messages.heightDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<StyledInput
|
||||||
id="height"
|
id="height"
|
||||||
v-model="resolution[1]"
|
v-model="resolution[1]"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="formatMessage(messages.enterHeight)"
|
:placeholder="formatMessage(messages.enterHeight)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
CopyIcon,
|
||||||
|
DropdownIcon,
|
||||||
|
LogInIcon,
|
||||||
|
MessagesSquareIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Admonition, ButtonStyled, Collapsible, NewModal } from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
|
import { findMinecraftAuthError, type MinecraftAuthError } from './minecraft-auth-errors'
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const rawError = ref<string>('')
|
||||||
|
const matchedError = ref<MinecraftAuthError | null>(null)
|
||||||
|
const debugCollapsed = ref(true)
|
||||||
|
const copied = ref(false)
|
||||||
|
const loadingSignIn = ref(false)
|
||||||
|
|
||||||
|
function show(errorVal: { message?: string }) {
|
||||||
|
rawError.value = errorVal?.message ?? String(errorVal)
|
||||||
|
|
||||||
|
matchedError.value = findMinecraftAuthError(rawError.value)
|
||||||
|
|
||||||
|
debugCollapsed.value = true
|
||||||
|
modal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function signInAgain() {
|
||||||
|
try {
|
||||||
|
loadingSignIn.value = true
|
||||||
|
const loggedIn = await login_flow()
|
||||||
|
if (loggedIn) {
|
||||||
|
await set_default_user(loggedIn.profile.id)
|
||||||
|
}
|
||||||
|
loadingSignIn.value = false
|
||||||
|
modal.value?.hide()
|
||||||
|
} catch (err) {
|
||||||
|
loadingSignIn.value = false
|
||||||
|
handleSevereError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debugInfo = computed(() => rawError.value || 'No error message.')
|
||||||
|
|
||||||
|
async function copyToClipboard(text: string) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
copied.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
copied.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NewModal ref="modal" header="Sign in Failed" :max-width="'548px'">
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<Admonition
|
||||||
|
type="warning"
|
||||||
|
body=" We couldn't sign you into your Microsoft account. This may be due to account restrictions or
|
||||||
|
regional limitations."
|
||||||
|
>
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<!-- Matched error details -->
|
||||||
|
<div class="bg-surface-2 rounded-2xl p-4 px-5 flex flex-col gap-3">
|
||||||
|
<template v-if="matchedError">
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<h3 class="text-base font-bold m-0">What we think happened</h3>
|
||||||
|
<p class="text-sm text-secondary m-0">
|
||||||
|
{{ matchedError.whatHappened }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<h3 class="text-base font-bold m-0">How to fix it</h3>
|
||||||
|
<ol class="list-none flex flex-col gap-2 m-0 pl-0">
|
||||||
|
<li
|
||||||
|
v-for="(step, index) in matchedError.stepsToFix"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-baseline gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center justify-center shrink-0 w-5 h-5 rounded-full bg-surface-4 border border-solid border-surface-5 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}
|
||||||
|
</span>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<span
|
||||||
|
class="text-sm [&_a]:text-info [&_a]:font-medium [&_a]:underline"
|
||||||
|
v-html="step"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<h3 class="text-base font-bold m-0">Unknown error</h3>
|
||||||
|
<p class="text-sm text-secondary m-0">
|
||||||
|
We don’t recognize this error and can’t recommend specific steps to resolve it.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-secondary m-0">
|
||||||
|
Try visiting
|
||||||
|
<a
|
||||||
|
class="text-info font-medium underline hover:underline"
|
||||||
|
href="https://www.minecraft.net/en-us/login"
|
||||||
|
>Minecraft Login</a
|
||||||
|
>
|
||||||
|
and signing in, as it may prompt you with the necessary steps. You can also contact
|
||||||
|
support and we can look into it further.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ButtonStyled>
|
||||||
|
<a href="https://support.modrinth.com" class="!w-full" @click="modal?.hide()">
|
||||||
|
<MessagesSquareIcon /> Contact support
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button :disabled="loadingSignIn" class="!w-full" @click="signInAgain">
|
||||||
|
<LogInIcon /> Sign in again
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="w-full h-[1px] bg-surface-5"></div>
|
||||||
|
|
||||||
|
<!-- Debug info -->
|
||||||
|
<div class="overflow-clip">
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between w-full bg-transparent border-0 py-4 cursor-pointer"
|
||||||
|
@click="debugCollapsed = !debugCollapsed"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-2 text-contrast font-extrabold m-0">
|
||||||
|
<WrenchIcon class="h-4 w-4" />
|
||||||
|
Debug information
|
||||||
|
</span>
|
||||||
|
<DropdownIcon
|
||||||
|
class="h-5 w-5 text-secondary transition-transform"
|
||||||
|
:class="{ 'rotate-180': !debugCollapsed }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<Collapsible :collapsed="debugCollapsed">
|
||||||
|
<div
|
||||||
|
class="p-3 bg-surface-2 rounded-2xl text-xs grid grid-cols-[1fr_auto] max-w-full items-start"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="m-0 p-0 rounded-none bg-transparent text-sm font-mono break-words overflow-auto"
|
||||||
|
>
|
||||||
|
{{ debugInfo }}
|
||||||
|
</div>
|
||||||
|
<ButtonStyled circular>
|
||||||
|
<button
|
||||||
|
v-tooltip="'Copy debug info'"
|
||||||
|
:disabled="copied"
|
||||||
|
@click="copyToClipboard(debugInfo)"
|
||||||
|
>
|
||||||
|
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||||
|
<template v-else> <CopyIcon /> </template>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
export interface MinecraftAuthError {
|
||||||
|
errorCode?: string
|
||||||
|
errorMatchers?: string[]
|
||||||
|
matches?: (message: string) => boolean
|
||||||
|
whatHappened: string
|
||||||
|
stepsToFix: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const minecraftAuthErrors: MinecraftAuthError[] = [
|
||||||
|
{
|
||||||
|
errorMatchers: ['Failed to deserialize response to JSON during step RefreshOAuthToken:'],
|
||||||
|
whatHappened:
|
||||||
|
'Your saved Microsoft sign-in token has expired or was revoked, so Modrinth App cannot refresh your Minecraft session.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Sign out of the affected Minecraft account in Modrinth App',
|
||||||
|
'Sign in to the account again',
|
||||||
|
'Once the new sign-in finishes, try launching Minecraft again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorMatchers: ['Failed to deserialize response to JSON during step SisuAuthenticate:'],
|
||||||
|
whatHappened:
|
||||||
|
'Xbox services rejected the first sign-in response. This is most often caused by your system clock or time zone being out of sync.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Open your system date and time settings',
|
||||||
|
'Turn on automatic time zone and automatic time, if available',
|
||||||
|
'Use the sync option in your system settings to synchronize the clock',
|
||||||
|
'Restart Modrinth App',
|
||||||
|
'Try signing in again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matches: (message) =>
|
||||||
|
message.includes('Failed to deserialize response to JSON during step MinecraftToken:') &&
|
||||||
|
message.includes('429 Too Many Requests'),
|
||||||
|
whatHappened:
|
||||||
|
'Microsoft or Minecraft temporarily blocked the sign-in request because there were too many recent attempts.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Wait about an hour before trying again',
|
||||||
|
'Restart Modrinth App after waiting',
|
||||||
|
'Try signing in once more',
|
||||||
|
'If the same message appears, wait longer before retrying so the temporary limit can clear',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matches: (message) =>
|
||||||
|
message.includes('Failed to deserialize response to JSON during step MinecraftToken:') &&
|
||||||
|
/Status Code: 5\d\d/.test(message),
|
||||||
|
whatHappened:
|
||||||
|
"Minecraft's authentication service is returning a server error, so Modrinth App cannot finish signing you in right now.",
|
||||||
|
stepsToFix: [
|
||||||
|
'Wait a few minutes and try signing in again',
|
||||||
|
'Check <a href="https://support.xbox.com/xbox-live-status">Xbox Status</a> for current service issues',
|
||||||
|
'Try signing in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a> to confirm whether Minecraft sign-in is also affected there',
|
||||||
|
'If the service is healthy and this keeps happening, contact support with the debug information below',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorMatchers: ['Failed to fetch player profile'],
|
||||||
|
whatHappened:
|
||||||
|
'Minecraft services could not return a Java Edition profile for this account. This most often happens when the game was purchased recently, the Java profile has not finished being created, or the wrong Microsoft account is being used.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Sign in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>',
|
||||||
|
'Launch Minecraft: Java Edition once from the official launcher',
|
||||||
|
'Wait up to an hour if the purchase or profile setup was recent',
|
||||||
|
'Make sure you are using the Microsoft account that owns Minecraft. See <a href="https://support.modrinth.com/en/articles/9409136-finding-the-right-xbox-account">Finding the right Xbox account</a> for help',
|
||||||
|
'Try signing in to Modrinth App again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matches: (message) =>
|
||||||
|
message.includes('error sending request for url (') &&
|
||||||
|
[
|
||||||
|
'minecraft.net',
|
||||||
|
'minecraftservices.com',
|
||||||
|
'mojang.com',
|
||||||
|
'xbox.com',
|
||||||
|
'xboxlive.com',
|
||||||
|
'live.com',
|
||||||
|
].some((domain) => message.includes(domain)),
|
||||||
|
whatHappened:
|
||||||
|
'Modrinth App could not connect to a Microsoft, Xbox, or Minecraft service needed for sign-in. This is usually caused by a local network, DNS, proxy, firewall, hosts file, VPN, or antivirus issue.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Restart Modrinth App and try signing in again',
|
||||||
|
'Check that your internet connection is working',
|
||||||
|
'Allow Modrinth App through your firewall, antivirus, proxy, VPN, and hosts file rules',
|
||||||
|
'Try a different network or temporarily disable VPN/proxy software if you use one',
|
||||||
|
'If routing or DNS is the issue, a service like Cloudflare WARP can sometimes help',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916222',
|
||||||
|
whatHappened:
|
||||||
|
'Your Minecraft/Xbox Live account requires age verification to comply with UK regulations. You must complete this before signing in.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Go to the <a href="https://www.minecraft.net/en-us/login">Minecraft Login</a> page and sign in',
|
||||||
|
'Follow the instructions to verify your age',
|
||||||
|
'Once verified, try signing in again',
|
||||||
|
'For additional help, visit <a href="https://support.xbox.com/en-GB/help/family-online-safety/online-safety/UK-age-verification">UK age verification on Xbox</a>',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916233',
|
||||||
|
whatHappened: "This account doesn't have an Xbox profile set up or doesn't own Minecraft.",
|
||||||
|
stepsToFix: [
|
||||||
|
'Make sure Minecraft is purchased on this account',
|
||||||
|
'Visit <a href="https://www.minecraft.net/en-us/login">Minecraft Login</a> and sign in',
|
||||||
|
'Complete Xbox profile setup if prompted',
|
||||||
|
'Once finished, try signing in again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916235',
|
||||||
|
whatHappened: "Xbox Live isn't available in your region, so sign-in is blocked.",
|
||||||
|
stepsToFix: [
|
||||||
|
'Xbox services must be supported in your country before you can sign in',
|
||||||
|
'Check <a href="https://www.xbox.com/en-US/regions">Xbox Availability</a> for supported regions',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916236',
|
||||||
|
whatHappened: 'This account requires adult verification under South Korean regulations.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||||
|
'Complete the identity verification process',
|
||||||
|
'Once finished, try signing in again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916237',
|
||||||
|
whatHappened: 'This account requires adult verification under South Korean regulations.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||||
|
'Complete the identity verification process',
|
||||||
|
'Once finished, try signing in again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916238',
|
||||||
|
whatHappened: 'This account is underage and not linked to a Microsoft family group.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Review the <a href="https://help.minecraft.net/hc/en-us/articles/4408968616077">Family Setup Guide</a>',
|
||||||
|
'Join or create a family group as instructed',
|
||||||
|
'Once finished, try signing in again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916227',
|
||||||
|
whatHappened: 'This account was suspended for violating Xbox Community Standards.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Visit <a href="https://support.xbox.com">Xbox Support</a> and review the enforcement details',
|
||||||
|
'Submit an appeal if one is available',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916229',
|
||||||
|
whatHappened: "This account is restricted and doesn't have permission to play online.",
|
||||||
|
stepsToFix: [
|
||||||
|
'Have a guardian sign in to <a href="https://account.microsoft.com/family/">Microsoft Family</a>',
|
||||||
|
'Update online play permissions',
|
||||||
|
'Once finished, try signing in again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: '2148916234',
|
||||||
|
whatHappened: "This account hasn't accepted Xbox's Terms of Service.",
|
||||||
|
stepsToFix: [
|
||||||
|
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||||
|
'Accept the Terms if prompted',
|
||||||
|
'Once finished, try signing in again',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorMatchers: ['Failed to deserialize response to JSON during step XstsAuthorize:'],
|
||||||
|
whatHappened:
|
||||||
|
'Xbox services rejected the request to authorize this account for Minecraft services, but did not return a specific account restriction that Modrinth App recognizes.',
|
||||||
|
stepsToFix: [
|
||||||
|
'Sign in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>',
|
||||||
|
'Complete any prompts shown by Microsoft, Xbox, or Minecraft',
|
||||||
|
'Try signing in to Modrinth App again',
|
||||||
|
'If the official launcher also fails, follow the error shown there or contact Xbox Support',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function findMinecraftAuthError(message: string): MinecraftAuthError | null {
|
||||||
|
return (
|
||||||
|
minecraftAuthErrors.find((error) => {
|
||||||
|
if (error.errorCode && message.includes(error.errorCode)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.errorMatchers?.some((matcher) => message.includes(matcher))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.matches?.(message) ?? false
|
||||||
|
}) ?? null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,101 +1,125 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ReportIcon,
|
AstralRinthLogo,
|
||||||
AstralRinthLogo,
|
CoffeeIcon,
|
||||||
ShieldIcon,
|
DownloadIcon,
|
||||||
SettingsIcon,
|
GameIcon,
|
||||||
GaugeIcon,
|
GaugeIcon,
|
||||||
PaintbrushIcon,
|
LanguagesIcon,
|
||||||
GameIcon,
|
PaintbrushIcon,
|
||||||
CoffeeIcon,
|
SettingsIcon,
|
||||||
|
ShieldIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
ToggleRightIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { TabbedModal } from '@modrinth/ui'
|
import {
|
||||||
import { computed, ref, watch } from 'vue'
|
commonMessages,
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
commonSettingsMessages,
|
||||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
defineMessage,
|
||||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
defineMessages,
|
||||||
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
ProgressBar,
|
||||||
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
TabbedModal,
|
||||||
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/plugin-os'
|
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
||||||
import { useTheming } from '@/store/state'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import LauncherUpdateModal from '@/components/ui/astralrinth/LauncherUpdateModal.vue'
|
||||||
|
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||||
|
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
||||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||||
|
import LanguageSettings from '@/components/ui/settings/LanguageSettings.vue'
|
||||||
|
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
||||||
|
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
import { isUpdateInstalling, isUpdateAvailable } from '@/helpers/astralrinth/update'
|
||||||
|
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const devModeCounter = ref(0)
|
const devModeCounter = ref(0)
|
||||||
|
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||||
|
const launcherUpdateModal = ref<InstanceType<typeof LauncherUpdateModal> | null>(null)
|
||||||
|
|
||||||
const developerModeEnabled = defineMessage({
|
const developerModeEnabled = defineMessage({
|
||||||
id: 'app.settings.developer-mode-enabled',
|
id: 'app.settings.developer-mode-enabled',
|
||||||
defaultMessage: 'Developer mode enabled.',
|
defaultMessage: 'Developer mode enabled.',
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.appearance',
|
id: 'app.settings.tabs.appearance',
|
||||||
defaultMessage: 'Appearance',
|
defaultMessage: 'Appearance',
|
||||||
}),
|
}),
|
||||||
icon: PaintbrushIcon,
|
icon: PaintbrushIcon,
|
||||||
content: AppearanceSettings,
|
content: AppearanceSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.privacy',
|
id: 'app.settings.tabs.language',
|
||||||
defaultMessage: 'Privacy',
|
defaultMessage: 'Language',
|
||||||
}),
|
}),
|
||||||
icon: ShieldIcon,
|
icon: LanguagesIcon,
|
||||||
content: PrivacySettings,
|
content: LanguageSettings,
|
||||||
},
|
badge: commonMessages.beta,
|
||||||
{
|
},
|
||||||
name: defineMessage({
|
{
|
||||||
id: 'app.settings.tabs.java-installations',
|
name: defineMessage({
|
||||||
defaultMessage: 'Java installations',
|
id: 'app.settings.tabs.privacy',
|
||||||
}),
|
defaultMessage: 'Privacy',
|
||||||
icon: CoffeeIcon,
|
}),
|
||||||
content: JavaSettings,
|
icon: ShieldIcon,
|
||||||
},
|
content: PrivacySettings,
|
||||||
{
|
},
|
||||||
name: defineMessage({
|
{
|
||||||
id: 'app.settings.tabs.default-instance-options',
|
name: defineMessage({
|
||||||
defaultMessage: 'Default instance options',
|
id: 'app.settings.tabs.java-installations',
|
||||||
}),
|
defaultMessage: 'Java installations',
|
||||||
icon: GameIcon,
|
}),
|
||||||
content: DefaultInstanceSettings,
|
icon: CoffeeIcon,
|
||||||
},
|
content: JavaSettings,
|
||||||
{
|
},
|
||||||
name: defineMessage({
|
{
|
||||||
id: 'app.settings.tabs.resource-management',
|
name: defineMessage({
|
||||||
defaultMessage: 'Resource management',
|
id: 'app.settings.tabs.default-instance-options',
|
||||||
}),
|
defaultMessage: 'Default instance options',
|
||||||
icon: GaugeIcon,
|
}),
|
||||||
content: ResourceManagementSettings,
|
icon: GameIcon,
|
||||||
},
|
content: DefaultInstanceSettings,
|
||||||
{
|
},
|
||||||
name: defineMessage({
|
{
|
||||||
id: 'app.settings.tabs.feature-flags',
|
name: defineMessage({
|
||||||
defaultMessage: 'Feature flags',
|
id: 'app.settings.tabs.resource-management',
|
||||||
}),
|
defaultMessage: 'Resource management',
|
||||||
icon: ReportIcon,
|
}),
|
||||||
content: FeatureFlagSettings,
|
icon: GaugeIcon,
|
||||||
developerOnly: true,
|
content: ResourceManagementSettings,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: commonSettingsMessages.featureFlags,
|
||||||
|
icon: ToggleRightIcon,
|
||||||
|
content: FeatureFlagSettings,
|
||||||
|
developerOnly: true,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const modal = ref()
|
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
modal.value.show()
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOpen = computed(() => modal.value?.isOpen)
|
function showUpdateModal() {
|
||||||
|
modal.value?.show()
|
||||||
|
void launcherUpdateModal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ show, isOpen })
|
defineExpose({ show, showUpdateModal })
|
||||||
|
|
||||||
|
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
|
||||||
|
|
||||||
const version = await getVersion()
|
const version = await getVersion()
|
||||||
const osPlatform = getOsPlatform()
|
const osPlatform = getOsPlatform()
|
||||||
@@ -103,59 +127,107 @@ const osVersion = getOsVersion()
|
|||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
await set(settings.value)
|
await set(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
function devModeCount() {
|
function devModeCount() {
|
||||||
devModeCounter.value++
|
devModeCounter.value++
|
||||||
if (devModeCounter.value > 5) {
|
if (devModeCounter.value > 5) {
|
||||||
themeStore.devMode = !themeStore.devMode
|
themeStore.devMode = !themeStore.devMode
|
||||||
settings.value.developer_mode = !!themeStore.devMode
|
settings.value.developer_mode = !!themeStore.devMode
|
||||||
devModeCounter.value = 0
|
devModeCounter.value = 0
|
||||||
|
|
||||||
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
|
if (!themeStore.devMode && tabs[modal.value!.selectedTab].developerOnly) {
|
||||||
modal.value.setTab(0)
|
modal.value!.setTab(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<ModalWrapper ref="modal">
|
|
||||||
<template #title>
|
|
||||||
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
|
||||||
<SettingsIcon /> Settings
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
const messages = defineMessages({
|
||||||
<template #footer>
|
downloading: {
|
||||||
<div class="mt-auto text-secondary text-sm">
|
id: 'app.settings.downloading',
|
||||||
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
defaultMessage: 'Downloading v{version}',
|
||||||
{{ formatMessage(developerModeEnabled) }}
|
},
|
||||||
</p>
|
updateInstalling: {
|
||||||
<div class="flex items-center gap-3">
|
id: 'astralrinth.app.settings.update-installing',
|
||||||
<button
|
defaultMessage: 'Installing update...',
|
||||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
},
|
||||||
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
viewUpdateInfo: {
|
||||||
@click="devModeCount"
|
id: 'astralrinth.app.settings.view-update-info',
|
||||||
>
|
defaultMessage: 'View update info',
|
||||||
<AstralRinthLogo class="w-6 h-6" />
|
},
|
||||||
</button>
|
})
|
||||||
<div>
|
</script>
|
||||||
<p class="m-0">AstralRinth App {{ version }}</p>
|
|
||||||
<p class="m-0">
|
<template>
|
||||||
<span v-if="osPlatform === 'macos'">MacOS</span>
|
<TabbedModal ref="modal" :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
||||||
<span v-else class="capitalize">{{ osPlatform }}</span>
|
<template #title>
|
||||||
{{ osVersion }}
|
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
</p>
|
<SettingsIcon /> Settings
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<template #footer>
|
||||||
</template>
|
<div class="mt-auto text-secondary text-sm">
|
||||||
</TabbedModal>
|
<div class="mb-3">
|
||||||
</ModalWrapper>
|
<template v-if="progress > 0 && progress < 1">
|
||||||
|
<p class="m-0 mb-2">
|
||||||
|
{{ formatMessage(messages.downloading, { version: downloadingVersion }) }}
|
||||||
|
</p>
|
||||||
|
<ProgressBar :progress="progress" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||||
|
{{ formatMessage(developerModeEnabled) }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
|
:class="{
|
||||||
|
'text-brand': themeStore.devMode,
|
||||||
|
'text-secondary': !themeStore.devMode,
|
||||||
|
}"
|
||||||
|
@click="devModeCount"
|
||||||
|
>
|
||||||
|
<AstralRinthLogo class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<div class="max-w-[200px]">
|
||||||
|
<p class="m-0">AstralRinth App {{ version }}</p>
|
||||||
|
<p class="m-0">
|
||||||
|
<span v-if="osPlatform === 'macos'">macOS</span>
|
||||||
|
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||||
|
{{ osVersion }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isUpdateAvailable"
|
||||||
|
class="w-8 h-8 cursor-pointer hover:brightness-75 neon-icon pulse shrink-0"
|
||||||
|
>
|
||||||
|
<template v-if="isUpdateInstalling">
|
||||||
|
<SpinnerIcon
|
||||||
|
class="size-6 animate-spin"
|
||||||
|
v-tooltip.bottom="formatMessage(messages.updateInstalling)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<DownloadIcon
|
||||||
|
class="size-6"
|
||||||
|
v-tooltip.bottom="formatMessage(messages.viewUpdateInfo)"
|
||||||
|
@click="showUpdateModal()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TabbedModal>
|
||||||
|
|
||||||
|
<LauncherUpdateModal ref="launcherUpdateModal" :version="version" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../../../../packages/assets/styles/astralrinth/neon-icon.scss';
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
onFlowCancel: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return async () => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const modal = ref()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show, hide })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ModalWrapper ref="modal" @hide="onFlowCancel">
|
||||||
|
<template #title>
|
||||||
|
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
|
<LogInIcon /> Sign in
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-2">
|
||||||
|
<SpinnerIcon class="w-12 h-12 animate-spin" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Please sign in at the browser window that just opened to continue.
|
||||||
|
</p>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user