Merge branch 'master' into vpm-spec

This commit is contained in:
anatawa12 2024-06-19 17:29:09 +09:00 committed by GitHub
commit 5c1dc06ff2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 5082 additions and 2043 deletions

View file

@ -84,6 +84,11 @@ jobs:
cp vrc-get-gui/Cargo.toml vrc-get-gui/Cargo.toml.bak
sed -E "/^version/s/\"$/+$(git rev-parse --short HEAD)\"/" < vrc-get-gui/Cargo.toml.bak > vrc-get-gui/Cargo.toml
- name: Enable Devtools Feature
shell: bash
run: |
cargo add --package vrc-get-gui tauri --features devtools
- uses: tauri-apps/tauri-action@v0
with:
projectPath: vrc-get-gui

View file

@ -8,17 +8,40 @@ The format is based on [Keep a Changelog].
## [Unreleased]
### Added
- De-duplicating duplicated projects or Unity in VCC project list `#1081`
- Show package description on hovering package name / id `#1118`
### Changed
- Refine Dark Theme `#1083`
- Show package display name on uninstalled toast `#1086`
### Deprecated
### Removed
### Fixed
- Unity from Unity Hub will be registered as manually registered Unity `#1081`
- Unity selector radio button does not work well `#1082`
- `vcc:` link install button does not work well on linux `#1117`
### Security
## [0.1.7] - 2024-05-30
### Added
- Support for dark theme [`#1028`](https://github.com/vrc-get/vrc-get/pull/1028) [`#1044`](https://github.com/vrc-get/vrc-get/pull/1044)
### Changed
- Changed component library to shadcn [`#1028`](https://github.com/vrc-get/vrc-get/pull/1028)
- Informational message will be shown if you've installed fake latest because of the Unity version [`#1046`](https://github.com/vrc-get/vrc-get/pull/1046) [`#1061`](https://github.com/vrc-get/vrc-get/pull/1061)
- Show newly installed packages and reinstalling packages separately [`#1052`](https://github.com/vrc-get/vrc-get/pull/1052)
- Prevents opening multiple unity instances [`#1055`](https://github.com/vrc-get/vrc-get/pull/1055) [`#1062`](https://github.com/vrc-get/vrc-get/pull/1062)
- Migration will be prevented if the project is opened in Unity [`#1055`](https://github.com/vrc-get/vrc-get/pull/1055) [`#1062`](https://github.com/vrc-get/vrc-get/pull/1062)
### Fixed
- Clicking '+' of the incompatible package will do nothing [`#1046`](https://github.com/vrc-get/vrc-get/pull/1046)
- Opening Manage Project page will cause VCC to crash [`#1048`](https://github.com/vrc-get/vrc-get/pull/1048)
- Fails to load installed unity versions if Unity 2018 is installed again [`#1051`](https://github.com/vrc-get/vrc-get/pull/1051)
## [0.1.6] - 2024-05-21
### Fixed
- Fails to load installed unity versions if Unity 2018 is installed [`#1024`](https://github.com/vrc-get/vrc-get/pull/1024)
@ -300,7 +323,8 @@ The format is based on [Keep a Changelog].
- Apple code signing [`#422`](https://github.com/anatawa12/vrc-get/pull/422)
- Migrate vpm 2019 project to 2022 [`#435`](https://github.com/anatawa12/vrc-get/pull/435)
[Unreleased]: https://github.com/vrc-get/vrc-get/compare/gui-v0.1.6...HEAD
[Unreleased]: https://github.com/vrc-get/vrc-get/compare/gui-v0.1.7...HEAD
[0.1.7]: https://github.com/vrc-get/vrc-get/compare/gui-v0.1.6...gui-v0.1.7
[0.1.6]: https://github.com/vrc-get/vrc-get/compare/gui-v0.1.5...gui-v0.1.6
[0.1.5]: https://github.com/vrc-get/vrc-get/compare/gui-v0.1.4...gui-v0.1.5
[0.1.4]: https://github.com/vrc-get/vrc-get/compare/gui-v0.1.3...gui-v0.1.4

View file

@ -10,6 +10,9 @@ The format is based on [Keep a Changelog].
### Added
- Per-package `headers` field support `#718`
- Since this is adding support for missing features, I treat this as a bugfix and not bump minor version.
- De-duplicating duplicated projects or Unity in VCC project list `#1081`
- Customizing Command Line Arguments for Unity `#1127`
- Preserve Unity if multiple instance of the same unity version are installed `#1127`
### Changed
@ -18,6 +21,7 @@ The format is based on [Keep a Changelog].
### Removed
### Fixed
- Unity from Unity Hub will be registered as manually registered Unity `#1081`
### Security

425
Cargo.lock generated
View file

@ -507,9 +507,9 @@ dependencies = [
[[package]]
name = "bson"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2"
checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
dependencies = [
"ahash",
"base64 0.13.1",
@ -540,7 +540,7 @@ dependencies = [
name = "build-check-static-link"
version = "0.1.0"
dependencies = [
"object 0.35.0",
"object 0.36.0",
]
[[package]]
@ -616,9 +616,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695"
[[package]]
name = "cesu8"
@ -662,6 +662,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.38"
@ -679,9 +685,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.4"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
dependencies = [
"clap_builder",
"clap_derive",
@ -689,9 +695,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.2"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
dependencies = [
"anstream",
"anstyle",
@ -701,18 +707,18 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.2"
version = "4.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e"
checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.5.4"
version = "4.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -1077,6 +1083,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "displaydoc"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]]
name = "document-features"
version = "0.2.8"
@ -2141,6 +2158,124 @@ dependencies = [
"png",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -2149,12 +2284,14 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.5.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"icu_normalizer",
"icu_properties",
"smallvec",
"utf8_iter",
]
[[package]]
@ -2425,6 +2562,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "litrs"
version = "0.4.1"
@ -2643,6 +2786,18 @@ dependencies = [
"memoffset 0.7.1",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nodrop"
version = "0.1.14"
@ -2765,9 +2920,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.35.0"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
dependencies = [
"memchr",
]
@ -2790,9 +2945,9 @@ dependencies = [
[[package]]
name = "open"
version = "5.1.3"
version = "5.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb49fbd5616580e9974662cb96a3463da4476e649a7e4b258df0de065db0657"
checksum = "b5ca541f22b1c46d4bb9801014f234758ab4297e7870b904b6a8415b980a7388"
dependencies = [
"is-wsl",
"libc",
@ -3796,9 +3951,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.202"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
@ -3824,9 +3979,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.202"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
@ -4189,6 +4344,17 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]]
name = "sys-locale"
version = "0.2.4"
@ -4324,9 +4490,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.40"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb"
checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909"
dependencies = [
"filetime",
"libc",
@ -4341,9 +4507,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "tauri"
version = "1.6.7"
version = "1.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c7177b6be45bbb875aa239578f5adc982a1b3d5ea5b315ccd100aeb0043374"
checksum = "77567d2b3b74de4588d544147142d02297f3eaa171a25a065252141d8597a516"
dependencies = [
"anyhow",
"base64 0.21.7",
@ -4460,7 +4626,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-single-instance"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#0d649843c6b49b7a69200816c8d5195f9953a4fb"
dependencies = [
"log",
"serde",
@ -4668,25 +4834,20 @@ dependencies = [
]
[[package]]
name = "tinyvec"
version = "1.6.0"
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"tinyvec_macros",
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
@ -4702,9 +4863,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
@ -4943,27 +5104,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
@ -4978,9 +5124,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.0"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56"
dependencies = [
"form_urlencoded",
"idna",
@ -4994,6 +5140,18 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.1"
@ -5062,7 +5220,7 @@ dependencies = [
[[package]]
name = "vrc-get-gui"
version = "0.1.7-beta.0"
version = "0.1.8-beta.0"
dependencies = [
"arc-swap",
"async-stream",
@ -5076,8 +5234,9 @@ dependencies = [
"indexmap 2.2.6",
"log",
"log-panics",
"nix 0.29.0",
"objc",
"open 5.1.3",
"open 5.1.4",
"reqwest 0.12.4",
"ringbuffer",
"serde",
@ -5094,20 +5253,21 @@ dependencies = [
"url",
"uuid",
"vrc-get-vpm",
"windows 0.57.0",
"winreg 0.52.0",
]
[[package]]
name = "vrc-get-litedb"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6d3b22367458ac41c3d3182b9272ac4093614c0226f334d5b9b36079778a582"
checksum = "895989c08cb4529ec35808cc7bb1153f76e6557e57db178bf52cd016c91ce130"
dependencies = [
"ar",
"bson",
"cc",
"hex",
"object 0.35.0",
"object 0.36.0",
"once_cell",
"rand 0.8.5",
"serde",
@ -5115,13 +5275,13 @@ dependencies = [
[[package]]
name = "vrc-get-litedb"
version = "0.2.1-beta.0"
version = "0.2.2-beta.0"
dependencies = [
"ar",
"bson",
"cc",
"hex",
"object 0.35.0",
"object 0.36.0",
"once_cell",
"rand 0.8.5",
"serde",
@ -5155,7 +5315,7 @@ dependencies = [
"tokio-util",
"url",
"uuid",
"vrc-get-litedb 0.2.0",
"vrc-get-litedb 0.2.1",
"winreg 0.52.0",
]
@ -5467,6 +5627,16 @@ dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets 0.52.5",
]
[[package]]
name = "windows-bindgen"
version = "0.39.0"
@ -5493,7 +5663,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
dependencies = [
"windows-implement 0.56.0",
"windows-interface",
"windows-interface 0.56.0",
"windows-result",
"windows-targets 0.52.5",
]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result",
"windows-targets 0.52.5",
]
@ -5519,6 +5701,17 @@ dependencies = [
"syn 2.0.65",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]]
name = "windows-interface"
version = "0.56.0"
@ -5530,6 +5723,17 @@ dependencies = [
"syn 2.0.65",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]]
name = "windows-metadata"
version = "0.39.0"
@ -5878,6 +6082,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
version = "0.24.10"
@ -5967,6 +6183,30 @@ dependencies = [
"winapi",
]
[[package]]
name = "yoke"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"synstructure",
]
[[package]]
name = "zbus"
version = "3.15.2"
@ -5991,7 +6231,7 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
"nix",
"nix 0.26.4",
"once_cell",
"ordered-stream",
"rand 0.8.5",
@ -6053,12 +6293,55 @@ dependencies = [
"syn 2.0.65",
]
[[package]]
name = "zerofrom"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
[[package]]
name = "zerovec"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]]
name = "zip"
version = "0.6.6"

View file

@ -13,7 +13,7 @@ readme.workspace = true
[dependencies]
[dependencies.object]
version = "0.35.0"
version = "0.36.0"
default-features = false
features = [
"read_core",

View file

@ -13,5 +13,5 @@ readme.workspace = true
[dependencies]
chrono = { version = "0.4.38", default-features = false, features = ["now", "serde"] }
indexmap = { version = "2.2.6", features = ["serde"] }
serde = { version = "1.0.200", features = ["derive"] }
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.116"

View file

@ -1,6 +1,6 @@
[package]
name = "vrc-get-gui"
version = "0.1.7-beta.0"
version = "0.1.8-beta.0"
description = "A Tauri App"
homepage.workspace = true
@ -11,27 +11,27 @@ edition.workspace = true
[build-dependencies]
flate2 = "1.0.30"
tar = "0.4.40"
tar = "0.4.41"
tauri-build = { version = "1.5.1", features = [ "config-toml" ] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.6.7", features = [ "os-all", "updater", "shell-open", "config-toml", "dialog" ] }
tauri = { version = "1.6.8", features = [ "os-all", "updater", "shell-open", "config-toml", "dialog" ] }
vrc-get-vpm = { path = "../vrc-get-vpm", features = ["experimental-project-management", "experimental-unity-management", "tokio"] }
reqwest = "0.12.4"
specta = { version = "1.0.5", features = [ "chrono" ] }
tauri-specta = { version = "1.0.2", features = ["typescript"] }
open = "5.1.3"
open = "5.1.4"
arc-swap = "1.7.1"
log = { version = "0.4.21", features = [ "std" ] }
chrono = { version = "0.4.38", features = [ "serde" ] }
ringbuffer = "0.15.0"
tokio = { version = "1.37.0", features = ["process"] }
tokio = { version = "1.38.0", features = ["process"] }
fs_extra = "1.3.0"
indexmap = "2.2.6"
futures = "0.3.30"
tar = "0.4.40"
tar = "0.4.41"
flate2 = "1.0.30"
uuid = { version = "1.8.0", features = ["v4"] }
trash = "4.1.1"
@ -40,16 +40,20 @@ async-stream = "0.3.5"
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
sys-locale = "0.3.1"
log-panics = { version = "2", features = ["with-backtrace"] }
url = "2.5.0"
url = "2.5.1"
dirs-next = "2.0.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.57.0", features = ["Win32_Storage_FileSystem", "Win32_System_IO"] }
winreg = "0.52.0"
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
cocoa = "0.24"
[target.'cfg(unix)'.dependencies]
nix = { version = "0.29.0", features = ["fs"] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.

View file

@ -2,6 +2,8 @@
This folder contains the experimental GUI version of vrc-get, ALCOM.
[Homepage (Help Wanted)](https://vrc-get.anatawa12.com/alcom/)
## Installation
The recommended way to install ALCOM is download from [GitHub Releases][alcom-releases].

View file

@ -7,6 +7,7 @@ accepted = [
"MPL-2.0",
"OpenSSL",
"Unicode-DFS-2016",
"Unicode-3.0",
]
workarounds = [

View file

@ -2,19 +2,88 @@
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
@layer base {
:root {
--background: 0 0% 100%;
--background-start: 190 7.89% 85.1%;
--background-end: 0, 0%, 100%;
--foreground: 240 10% 20%;
--card: 0 0% 100%;
--card-foreground: 240 10% 35%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 20%;
--primary: 240 5.9% 20%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 30%;
--info: 207 90% 54%;
--info-foreground: 210 40% 98%;
--success: 122 39% 49%;
--success-foreground: 210 40% 98%;
--warning: 52.15, 100%, 46.47%;
--warning-foreground: 210 40% 98%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.75rem;
}
.dark {
--bg-color: 240 10% 13%;
--fg-color: 240 10% 85%;
--secondary-bg: 240 3.7% 19%;
--primary-fg: 240 5.9% 15%;
--background: var(--bg-color);
--background-start: 0 0% 3%;
--background-end: 10 8% 15%;
--foreground: var(--fg-color);
--card: var(--bg-color);
--card-foreground: var(--fg-color);
--popover: var(--bg-color);
--popover-foreground: var(--fg-color);
--primary: var(--fg-color);
--primary-foreground: var(--primary-fg);
--secondary: var(--secondary-bg);
--secondary-foreground: var(--fg-color);
--muted: var(--secondary-bg);
--muted-foreground: 240 5% 74%;
--accent: var(--secondary-bg);
--accent-foreground: var(--fg-color);
--info: 207 90% 54%;
--info-foreground: 210 40% 90%;
--success: 122 39% 49%;
--success-foreground: 210 40% 90%;
--warning: 52.15, 100%, 46.47%;
--warning-foreground: 210 40% 90%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: var(--fg-color);
--border: var(--secondary-bg);
--input: var(--secondary-bg);
--ring: 240 4.9% 83.9%;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
) rgb(var(--background-start-rgb));
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
color: hsl(var(--foreground));
background: linear-gradient(
to bottom,
transparent,
hsl(var(--background-end))
) hsl(var(--background-start));
}
}
@layer utilities {
@ -56,3 +125,34 @@ h6 {
code {
@apply font-mono;
}
/*
this is a ad-hoc way to apply toastify variables.
We could not find way to correctly order the toastify css and this css so put in body to get higher specificity
*/
body {
--toastify-color-light: hsl(var(--background));
/*--toastify-color-info: #3498db;*/
--toastify-color-success: hsl(var(--success));
/*--toastify-color-warning: #f1c40f;*/
--toastify-color-error: hsl(var(--destructive));
/*--toastify-color-transparent: rgba(255, 255, 255, 0.7);*/
--toastify-icon-color-info: var(--toastify-color-info);
--toastify-icon-color-success: var(--toastify-color-success);
--toastify-icon-color-warning: var(--toastify-color-warning);
--toastify-icon-color-error: var(--toastify-color-error);
/* size and fonts are not customized */
--toastify-text-color-light: hsl(var(--foreground));
--toastify-color-progress-info: var(--toastify-color-info);
--toastify-color-progress-success: var(--toastify-color-success);
--toastify-color-progress-warning: var(--toastify-color-warning);
--toastify-color-progress-error: var(--toastify-color-error);
.Toastify__toast {
box-shadow: 0 4px 12px hsl(var(--primary) / 0.05);
}
}

View file

@ -1,6 +1,6 @@
"use client";
import {Card, Typography} from "@material-tailwind/react";
import {Card} from "@/components/ui/card";
import {HNavBar, VStack} from "@/components/layout";
import React, {useCallback, useEffect} from "react";
import {LogEntry, utilGetLogEntries} from "@/lib/bindings";
@ -25,12 +25,12 @@ export default function Page() {
return (
<VStack className={"m-4"}>
<HNavBar className={"flex-shrink-0"}>
<Typography className="cursor-pointer py-1.5 font-bold flex-grow-0">
<p className="cursor-pointer py-1.5 font-bold flex-grow-0">
{tc("logs")}
</Typography>
</p>
</HNavBar>
<main className="flex-shrink overflow-hidden flex flex-grow">
<Card className={`w-full overflow-x-auto overflow-y-scroll p-2 whitespace-pre font-mono shadow-none`}>
<Card className={`w-full overflow-x-auto overflow-y-scroll p-2 whitespace-pre font-mono shadow-none text-muted-foreground`}>
{logEntries.map((entry) => logEntryToText(entry)).join("\n")}
</Card>
</main>
@ -41,4 +41,3 @@ export default function Page() {
function logEntryToText(entry: LogEntry) {
return `${entry.time} [${entry.level.padStart(5, ' ')}] ${entry.target}: ${entry.message}`;
}

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
import React, {Fragment, useState} from "react";
import {Button, Dialog, DialogBody, DialogFooter, DialogHeader, Radio, Typography} from "@material-tailwind/react";
import {nop} from "@/lib/nop";
import {Button} from "@/components/ui/button";
import {DialogDescription, DialogFooter, DialogOpen, DialogTitle} from "@/components/ui/dialog";
import {tc, tt} from "@/lib/i18n";
import {toastError, toastSuccess, toastThrownError} from "@/lib/toast";
import {
environmentCopyProjectForMigration,
projectCallUnityForMigration,
environmentCopyProjectForMigration, environmentUnityVersions,
projectCallUnityForMigration, projectIsUnityLaunching,
projectMigrateProjectTo2022, TauriUnityVersions
} from "@/lib/bindings";
import {callAsyncCommand} from "@/lib/call-async-command";
@ -23,17 +23,14 @@ function findRecommendedUnity(unityVersions?: TauriUnityVersions): UnityInstalla
export function useUnity2022Migration(
{
projectPath,
unityVersions,
refresh,
}: {
projectPath: string,
unityVersions?: TauriUnityVersions,
refresh?: () => void,
}
): Result {
return useMigrationInternal({
projectPath,
unityVersions,
updateProjectPreUnityLaunch: async (project) => await projectMigrateProjectTo2022(project),
refresh,
ConfirmComponent: MigrationConfirmMigrationDialog,
@ -43,16 +40,16 @@ export function useUnity2022Migration(
function MigrationConfirmMigrationDialog({cancel, doMigrate}: ConfirmProps) {
return (
<>
<DialogBody>
<Typography className={"text-red-700"}>
<DialogDescription>
<p className={"text-destructive"}>
{tc("projects:dialog:vpm migrate description")}
</Typography>
</DialogBody>
</p>
</DialogDescription>
<DialogFooter>
<Button onClick={cancel} className="mr-1">{tc("general:button:cancel")}</Button>
<Button onClick={() => doMigrate(false)} color={"red"}
<Button onClick={() => doMigrate(false)} variant={"destructive"}
className="mr-1">{tc("projects:button:migrate copy")}</Button>
<Button onClick={() => doMigrate(true)} color={"red"}>{tc("projects:button:migrate in-place")}</Button>
<Button onClick={() => doMigrate(true)} variant={"destructive"}>{tc("projects:button:migrate in-place")}</Button>
</DialogFooter>
</>
);
@ -61,17 +58,14 @@ function MigrationConfirmMigrationDialog({cancel, doMigrate}: ConfirmProps) {
export function useUnity2022PatchMigration(
{
projectPath,
unityVersions,
refresh,
}: {
projectPath: string,
unityVersions?: TauriUnityVersions,
refresh?: () => void,
}
): Result {
return useMigrationInternal({
projectPath,
unityVersions,
updateProjectPreUnityLaunch: async () => {
}, // nothing pre-launch
refresh,
@ -92,14 +86,14 @@ function MigrationConfirmMigrationPatchDialog(
}) {
return (
<>
<DialogBody>
<Typography className={"text-red-700"}>
<DialogDescription>
<p className={"text-destructive"}>
{tc("projects:dialog:migrate unity2022 patch description", {unity})}
</Typography>
</DialogBody>
</p>
</DialogDescription>
<DialogFooter>
<Button onClick={cancel} className="mr-1">{tc("general:button:cancel")}</Button>
<Button onClick={() => doMigrate(true)} color={"red"}>{tc("projects:button:migrate in-place")}</Button>
<Button onClick={() => doMigrate(true)} variant={"destructive"}>{tc("projects:button:migrate in-place")}</Button>
</DialogFooter>
</>
);
@ -109,8 +103,11 @@ type StateInternal = {
state: "normal";
} | {
state: "confirm";
unityVersions: TauriUnityVersions;
unityFound: UnityInstallation[];
} | {
state: "noExactUnity2022";
unityVersions: TauriUnityVersions;
} | {
state: "copyingProject";
} | {
@ -134,14 +131,12 @@ type ConfirmProps = {
function useMigrationInternal(
{
projectPath,
unityVersions,
updateProjectPreUnityLaunch,
refresh,
ConfirmComponent,
}: {
projectPath: string,
unityVersions?: TauriUnityVersions,
updateProjectPreUnityLaunch: (projectPath: string) => Promise<unknown>,
refresh?: () => void,
@ -154,20 +149,23 @@ function useMigrationInternal(
const [installStatus, setInstallStatus] = React.useState<StateInternal>({state: "normal"});
const request = async () => {
if (await projectIsUnityLaunching(projectPath)) {
toastError(tt("projects:toast:close unity before migration"));
return;
}
const unityVersions = await environmentUnityVersions();
const unityFound = findRecommendedUnity(unityVersions);
if (unityFound.length == 0)
setInstallStatus({state: "noExactUnity2022"});
setInstallStatus({state: "noExactUnity2022", unityVersions});
else
setInstallStatus({state: "confirm"});
setInstallStatus({state: "confirm", unityVersions, unityFound});
}
const startMigrateProjectTo2022 = async (inPlace: boolean) => {
const startMigrateProjectTo2022 = async (inPlace: boolean, unityFound: UnityInstallation[]) => {
try {
const unityFound = findRecommendedUnity(unityVersions);
switch (unityFound.length) {
case 0:
setInstallStatus({state: "noExactUnity2022"});
break;
throw new Error("unreachable");
case 1:
// noinspection ES6MissingAwait
continueMigrateProjectTo2022(inPlace, unityFound[0][0]);
@ -177,8 +175,7 @@ function useMigrationInternal(
if (selected == null)
setInstallStatus({state: "normal"});
else
// noinspection ES6MissingAwait
continueMigrateProjectTo2022(inPlace, selected);
void continueMigrateProjectTo2022(inPlace, selected.unityPath);
break;
}
} catch (e) {
@ -254,9 +251,9 @@ function useMigrationInternal(
break;
case "confirm":
dialogBodyForState = <ConfirmComponent
unity={unityVersions!.recommended_version}
unity={installStatus.unityVersions!.recommended_version}
cancel={cancelMigrateProjectTo2022}
doMigrate={startMigrateProjectTo2022}
doMigrate={(inPlace) => startMigrateProjectTo2022(inPlace, installStatus.unityFound)}
/>;
break;
case "copyingProject":
@ -267,8 +264,8 @@ function useMigrationInternal(
break;
case "noExactUnity2022":
dialogBodyForState = <NoExactUnity2022Dialog
expectedVersion={unityVersions!.recommended_version}
installWithUnityHubLink={unityVersions!.install_recommended_version_link}
expectedVersion={installStatus.unityVersions!.recommended_version}
installWithUnityHubLink={installStatus.unityVersions!.install_recommended_version_link}
close={cancelMigrateProjectTo2022}
/>;
break;
@ -283,35 +280,35 @@ function useMigrationInternal(
dialog: <>
{unitySelector.dialog}
{dialogBodyForState == null ? null :
<Dialog open handler={nop} className={"whitespace-normal"}>
<DialogHeader>{tc("projects:manage:dialog:unity migrate header")}</DialogHeader>
<DialogOpen className={"whitespace-normal leading-relaxed"}>
<DialogTitle>{tc("projects:manage:dialog:unity migrate header")}</DialogTitle>
{dialogBodyForState}
</Dialog>}
</DialogOpen>}
</>,
request,
};
}
function MigrationCopyingDialog() {
return <DialogBody>
<Typography>
return <DialogDescription>
<p>
{tc("projects:pre-migrate copying...")}
</Typography>
<Typography>
</p>
<p>
{tc("projects:manage:dialog:do not close")}
</Typography>
</DialogBody>;
</p>
</DialogDescription>;
}
function MigrationMigratingDialog() {
return <DialogBody>
<Typography>
return <DialogDescription>
<p>
{tc("projects:migrating...")}
</Typography>
<Typography>
</p>
<p>
{tc("projects:manage:dialog:do not close")}
</Typography>
</DialogBody>;
</p>
</DialogDescription>;
}
function MigrationCallingUnityForMigrationDialog(
@ -327,18 +324,18 @@ function MigrationCallingUnityForMigrationDialog(
ref.current?.scrollIntoView({behavior: "auto"});
}, [lines]);
return <DialogBody>
<Typography>
return <DialogDescription>
<p>
{tc("projects:manage:dialog:unity migrate finalizing...")}
</Typography>
<Typography>
</p>
<p>
{tc("projects:manage:dialog:do not close")}
</Typography>
<pre className={"overflow-y-auto h-[50vh] bg-gray-900 text-white text-sm"}>
</p>
<pre className={"overflow-y-auto h-[50vh] bg-secondary text-secondary-foreground text-sm"}>
{lines.map(([lineNumber, line]) => <Fragment key={lineNumber}>{line}{"\n"}</Fragment>)}
<div ref={ref}/>
</pre>
</DialogBody>;
</DialogDescription>;
}
function NoExactUnity2022Dialog(
@ -357,11 +354,11 @@ function NoExactUnity2022Dialog(
}
return <>
<DialogBody>
<Typography>
<DialogDescription>
<p>
{tc("projects:manage:dialog:exact version unity not found for patch migration description", {unity: expectedVersion})}
</Typography>
</DialogBody>
</p>
</DialogDescription>
<DialogFooter className={"gap-2"}>
<Button onClick={openUnityHub}>{tc("projects:manage:dialog:open unity hub")}</Button>
<Button onClick={close} className="mr-1">{tc("general:button:close")}</Button>

View file

@ -1,24 +1,25 @@
"use client"
import {Button} from "@/components/ui/button";
import {Card, CardHeader} from "@/components/ui/card";
import {Checkbox} from "@/components/ui/checkbox";
import {DialogDescription, DialogFooter, DialogOpen, DialogTitle} from "@/components/ui/dialog";
import {
Button,
ButtonGroup,
Card,
Checkbox,
Dialog,
DialogBody,
DialogFooter,
DialogHeader,
IconButton,
Input,
Menu,
MenuHandler,
MenuItem,
MenuList,
Spinner,
Tooltip,
Typography
} from "@material-tailwind/react";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {Input} from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {Tooltip, TooltipContent, TooltipPortal, TooltipTrigger} from "@/components/ui/tooltip";
import React, {forwardRef, Fragment, useEffect, useMemo, useState} from "react";
import {
ArrowPathIcon,
@ -42,7 +43,7 @@ import {
environmentProjects,
environmentSetFavoriteProject,
environmentSetProjectSorting,
environmentUnityVersions,
environmentUnityVersions, projectIsUnityLaunching,
projectMigrateProjectToVpm,
TauriProject,
TauriProjectDirCheckResult,
@ -55,7 +56,6 @@ import {useRouter} from "next/navigation";
import {SearchBox} from "@/components/SearchBox";
import {nop} from "@/lib/nop";
import {useDebounce} from "@uidotdev/usehooks";
import {VGOption, VGSelect} from "@/components/select";
import {toastError, toastSuccess, toastThrownError} from "@/lib/toast";
import {useRemoveProjectModal} from "@/lib/remove-project";
import {tc, tt} from "@/lib/i18n";
@ -85,15 +85,10 @@ export default function Page() {
queryKey: ["projects"],
queryFn: environmentProjects,
});
const unityVersionsResult = useQuery({
queryKey: ["unityVersions"],
queryFn: () => environmentUnityVersions(),
});
const [search, setSearch] = useState("");
const [loadingOther, setLoadingOther] = useState(false);
const [createProjectState, setCreateProjectState] = useState<'normal' | 'creating'>('normal');
const openUnity = useOpenUnity(unityVersionsResult?.data);
const openUnity = useOpenUnity();
const startCreateProject = () => setCreateProjectState('creating');
@ -108,19 +103,21 @@ export default function Page() {
search={search} setSearch={setSearch}/>
<main className="flex-shrink overflow-hidden flex">
<Card className="w-full overflow-x-auto overflow-y-auto shadow-none">
{
result.status == "pending" ? <Card className={"p-4"}>{tc("general:loading...")}</Card> :
result.status == "error" ?
<Card className={"p-4"}>{tc("projects:error:load error", {msg: result.error.message})}</Card> :
<ProjectsTable
projects={result.data}
search={search}
loading={loading}
openUnity={openUnity.openUnity}
refresh={() => result.refetch()}
onRemoved={() => result.refetch()}
/>
}
<CardHeader>
{
result.status == "pending" ? <Card className={"p-4"}>{tc("general:loading...")}</Card> :
result.status == "error" ?
<Card className={"p-4"}>{tc("projects:error:load error", {msg: result.error.message})}</Card> :
<ProjectsTable
projects={result.data}
search={search}
loading={loading}
openUnity={openUnity.openUnity}
refresh={() => result.refetch()}
onRemoved={() => result.refetch()}
/>
}
</CardHeader>
</Card>
{createProjectState === "creating" &&
<CreateProject close={() => setCreateProjectState("normal")} refetch={() => result.refetch()}/>}
@ -234,7 +231,7 @@ function ProjectsTable(
return searched;
}, [projects, sorting, search]);
const thClass = `sticky top-0 z-10 border-b border-blue-gray-100 p-2.5`;
const thClass = `sticky top-0 z-10 border-b border-primary p-2.5`;
const iconClass = `size-3 invisible project-table-header-chevron-up-down`;
const setSorting = async (simpleSorting: SimpleSorting) => {
@ -256,7 +253,7 @@ function ProjectsTable(
}
}
const headerBg = (target: SimpleSorting) => sorting === target || sorting === `${target}Reversed` ? "bg-blue-100" : "bg-blue-gray-50";
const headerBg = (target: SimpleSorting) => sorting === target || sorting === `${target}Reversed` ? "bg-primary text-primary-foreground" : "bg-secondary text-secondary-foreground";
const icon = (target: SimpleSorting) =>
sorting === target ? <ChevronDownIcon className={"size-3"}/>
: sorting === `${target}Reversed` ? <ChevronUpIcon className={"size-3"}/>
@ -266,7 +263,7 @@ function ProjectsTable(
<table className="relative table-auto text-left">
<thead>
<tr>
<th className={`${thClass} bg-blue-gray-50`}>
<th className={`${thClass} bg-secondary text-secondary-foreground`}>
<StarIcon className={"size-4"}/>
</th>
<th
@ -274,31 +271,31 @@ function ProjectsTable(
<button className={"flex w-full project-table-button"}
onClick={() => setSorting("name")}>
{icon("name")}
<Typography variant="small" className="font-normal leading-none">{tc("general:name")}</Typography>
<small className="font-normal leading-none">{tc("general:name")}</small>
</button>
</th>
<th
className={`${thClass} ${headerBg('type')}`}>
<button className={"flex w-full project-table-button"} onClick={() => setSorting("type")}>
{icon("type")}
<Typography variant="small" className="font-normal leading-none">{tc("projects:type")}</Typography>
<small className="font-normal leading-none">{tc("projects:type")}</small>
</button>
</th>
<th
className={`${thClass} ${headerBg('unity')}`}>
<button className={"flex w-full project-table-button"} onClick={() => setSorting("unity")}>
{icon("unity")}
<Typography variant="small" className="font-normal leading-none">{tc("projects:unity")}</Typography>
<small className="font-normal leading-none">{tc("projects:unity")}</small>
</button>
</th>
<th
className={`${thClass} ${headerBg('lastModified')}`}>
<button className={"flex w-full project-table-button"} onClick={() => setSorting("lastModified")}>
{icon("lastModified")}
<Typography variant="small" className="font-normal leading-none">{tc("projects:last modified")}</Typography>
<small className="font-normal leading-none">{tc("projects:last modified")}</small>
</button>
</th>
<th className={`${thClass} bg-blue-gray-50`}></th>
<th className={`${thClass} bg-secondary text-secondary-foreground`}></th>
</tr>
</thead>
<tbody>
@ -392,7 +389,13 @@ function ProjectRow(
const openProjectFolder = () => utilOpen(project.path);
const startMigrateVpm = () => setDialogStatus({type: 'migrateVpm:confirm'});
const startMigrateVpm = async () => {
if (await projectIsUnityLaunching(project.path)) {
toastError(tt("projects:toast:close unity before migration"));
return;
}
setDialogStatus({type: 'migrateVpm:confirm'})
};
const doMigrateVpm = async (inPlace: boolean) => {
setDialogStatus({type: 'normal'});
try {
@ -428,14 +431,21 @@ function ProjectRow(
const removed = !project.is_exists;
const MayTooltip = removed ? Tooltip : Fragment;
const MayTooltipRev = !removed ? Tooltip : Fragment;
const MayTooltip = removed ? TooltipTrigger : Fragment;
const MayTooltipRev = !removed ? TooltipTrigger : Fragment;
const RowButton = forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(function RowButton(props, ref) {
if (removed) {
return <Tooltip content={tt("projects:tooltip:no directory")}>
<Button {...props} className={`disabled:pointer-events-auto ${props.className}`} disabled ref={ref}/>
</Tooltip>
return (
<Tooltip>
<TooltipTrigger asChild>
<Button {...props} className={`disabled:pointer-events-auto ${props.className}`} disabled ref={ref}/>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>{tt("projects:tooltip:no directory")}</TooltipContent>
</TooltipPortal>
</Tooltip>
)
} else {
return (
<Button {...props} className={`disabled:pointer-events-auto ${props.className}`}
@ -449,25 +459,32 @@ function ProjectRow(
switch (project.project_type) {
case "LegacySdk2":
manageButton =
<Tooltip content={tc("projects:tooltip:sdk2 migration hint")}>
<RowButton color={"light-green"} disabled>
{tc("projects:button:migrate")}
</RowButton>
<Tooltip>
<TooltipTrigger asChild>
<RowButton variant={"success"} disabled>
{tc("projects:button:migrate")}
</RowButton>
</TooltipTrigger>
<TooltipContent>{tc("projects:tooltip:sdk2 migration hint")}</TooltipContent>
</Tooltip>
break;
case "LegacyWorlds":
case "LegacyAvatars":
manageButton =
<RowButton color={"light-green"} onClick={startMigrateVpm}>{tc("projects:button:migrate")}</RowButton>
<RowButton variant={"success"} onClick={startMigrateVpm}>{tc("projects:button:migrate")}</RowButton>
break;
case "UpmWorlds":
case "UpmAvatars":
case "UpmStarter":
manageButton = <Tooltip content={tc("projects:tooltip:git-vcc not supported")}>
<RowButton color={"blue"} disabled>
{tc("projects:button:manage")}
</RowButton>
</Tooltip>
manageButton =
<Tooltip>
<TooltipTrigger asChild>
<RowButton variant={"info"} disabled>
{tc("projects:button:manage")}
</RowButton>
</TooltipTrigger>
<TooltipContent>{tc("projects:tooltip:git-vcc not supported")}</TooltipContent>
</Tooltip>
break;
case "Unknown":
case "Worlds":
@ -475,7 +492,7 @@ function ProjectRow(
case "VpmStarter":
manageButton = <RowButton
onClick={() => router.push(`/projects/manage?${new URLSearchParams({projectPath: project.path})}`)}
color={"blue"}>
variant={"info"}>
{tc("projects:button:manage")}
</RowButton>
break;
@ -485,74 +502,88 @@ function ProjectRow(
switch (dialogStatus.type) {
case "migrateVpm:confirm":
dialogContent = (
<Dialog open handler={nop} className={"whitespace-normal"}>
<DialogHeader>{tc("projects:dialog:vpm migrate header")}</DialogHeader>
<DialogBody>
<Typography className={"text-red-700"}>
<DialogOpen className={"whitespace-normal"}>
<DialogTitle>{tc("projects:dialog:vpm migrate header")}</DialogTitle>
<DialogDescription>
<p className={"text-destructive"}>
{tc("projects:dialog:vpm migrate description")}
</Typography>
</DialogBody>
</p>
</DialogDescription>
<DialogFooter>
<Button onClick={() => setDialogStatus({type: "normal"})}
className="mr-1">{tc("general:button:cancel")}</Button>
<Button onClick={() => doMigrateVpm(false)} color={"red"}
className="mr-1">{tc("projects:button:migrate copy")}</Button>
<Button onClick={() => doMigrateVpm(true)} color={"red"}>{tc("projects:button:migrate in-place")}</Button>
className="mr-1">{tc("general:button:cancel")}</Button>
<Button onClick={() => doMigrateVpm(false)} variant={"destructive"}
className="mr-1">{tc("projects:button:migrate copy")}</Button>
<Button onClick={() => doMigrateVpm(true)} variant={"destructive"}>{tc("projects:button:migrate in-place")}</Button>
</DialogFooter>
</Dialog>
</DialogOpen>
);
break;
case "migrateVpm:copyingProject":
dialogContent = (
<Dialog open handler={nop} className={"whitespace-normal"}>
<DialogHeader>{tc("projects:dialog:vpm migrate header")}</DialogHeader>
<DialogBody>
<Typography>
<DialogOpen className={"whitespace-normal"}>
<DialogTitle>{tc("projects:dialog:vpm migrate header")}</DialogTitle>
<DialogDescription>
<p>
{tc("projects:pre-migrate copying...")}
</Typography>
</DialogBody>
</Dialog>
</p>
</DialogDescription>
</DialogOpen>
);
break;
case "migrateVpm:updating":
dialogContent = (
<Dialog open handler={nop} className={"whitespace-normal"}>
<DialogHeader>{tc("projects:dialog:vpm migrate header")}</DialogHeader>
<DialogBody>
<Typography>
<DialogOpen className={"whitespace-normal"}>
<DialogTitle>{tc("projects:dialog:vpm migrate header")}</DialogTitle>
<DialogDescription>
<p>
{tc("projects:migrating...")}
</Typography>
</DialogBody>
</Dialog>
</p>
</DialogDescription>
</DialogOpen>
);
break;
}
return (
<tr className={`even:bg-blue-gray-50/50 ${(removed || loading) ? 'opacity-50' : ''}`}>
<tr className={`even:bg-secondary/30 ${(removed || loading) ? 'opacity-50' : ''}`}>
<td className={`${cellClass} w-3`}>
<Checkbox ripple={false} containerProps={{className: "p-0 rounded-none"}}
checked={project.favorite}
onChange={onToggleFavorite}
disabled={removed || loading}
icon={<StarIcon className={"size-3"}/>}
className="hover:before:content-none before:transition-none border-none"/>
<div className={"relative inline-flex"}>
<Checkbox checked={project.favorite}
onCheckedChange={onToggleFavorite}
disabled={removed || loading}
className="hover:before:content-none before:transition-none border-none !text-primary peer"/>
<span className={"text-background opacity-0 peer-data-[state=checked]:opacity-100 pointer-events-none absolute top-2/4 left-2/4 -translate-y-2/4 -translate-x-2/4"}>
<StarIcon className={"size-3"} />
</span>
</div>
</td>
<td className={`${cellClass} max-w-64 overflow-hidden`}>
<MayTooltip content={tc("projects:tooltip:no directory")}>
<div className="flex flex-col">
<MayTooltipRev content={project.name}>
<Typography className="font-normal whitespace-pre">
{project.name}
</Typography>
</MayTooltipRev>
<MayTooltipRev content={project.path}>
<Typography className="font-normal opacity-50 text-sm whitespace-pre">
{project.path}
</Typography>
</MayTooltipRev>
</div>
</MayTooltip>
<Tooltip>
<MayTooltip className={"text-left select-text cursor-auto w-full"}>
<div className="flex flex-col">
<Tooltip>
<MayTooltipRev className={"text-left select-text cursor-auto w-full"}>
<p className="font-normal whitespace-pre">
{project.name}
</p>
</MayTooltipRev>
<TooltipContent>{project.name}</TooltipContent>
</Tooltip>
<Tooltip>
<MayTooltipRev className={"text-left select-text cursor-auto w-full"}>
<p className="font-normal opacity-50 text-sm whitespace-pre">
{project.path}
</p>
</MayTooltipRev>
<TooltipContent>{project.path}</TooltipContent>
</Tooltip>
</div>
</MayTooltip>
<TooltipPortal>
<TooltipContent>{tc("projects:tooltip:no directory")}</TooltipContent>
</TooltipPortal>
</Tooltip>
</td>
<td className={`${cellClass} w-[8em] min-w-[8em]`}>
<div className="flex flex-row gap-2">
@ -562,27 +593,32 @@ function ProjectRow(
<QuestionMarkCircleIcon className={typeIconClass}/>}
</div>
<div className="flex flex-col justify-center">
<Typography className="font-normal">
<p className="font-normal">
{displayType}
</Typography>
</p>
{isLegacy &&
<Typography
className="font-normal opacity-50 text-sm text-red-700">{tc("projects:type:legacy")}</Typography>}
<p
className="font-normal opacity-50 dark:opacity-80 text-sm text-destructive">{tc("projects:type:legacy")}</p>}
</div>
</div>
</td>
<td className={noGrowCellClass}>
<Typography className="font-normal">
<p className="font-normal">
{project.unity}
</Typography>
</p>
</td>
<td className={noGrowCellClass}>
<Tooltip content={lastModifiedHumanReadable}>
<time dateTime={lastModified.toISOString()}>
<Typography as={"time"} className="font-normal">
{formatDateOffset(project.last_modified)}
</Typography>
</time>
<Tooltip>
<TooltipTrigger>
<time dateTime={lastModified.toISOString()}>
<time className="font-normal">
{formatDateOffset(project.last_modified)}
</time>
</time>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>{lastModifiedHumanReadable}</TooltipContent>
</TooltipPortal>
</Tooltip>
</td>
<td className={noGrowCellClass}>
@ -591,21 +627,21 @@ function ProjectRow(
onClick={() => openUnity(project.path, project.unity, project.unity_revision)}>{tc("projects:button:open unity")}</RowButton>
{manageButton}
<RowButton onClick={() => backupProjectModal.startBackup(project)}
color={"green"}>{tc("projects:backup")}</RowButton>
<Menu>
<MenuHandler>
<IconButton variant="text" color={"blue"}><EllipsisHorizontalIcon
className={"size-5"}/></IconButton>
</MenuHandler>
<MenuList>
<MenuItem onClick={openProjectFolder}
disabled={removed || loading}>{tc("projects:menuitem:open directory")}</MenuItem>
<MenuItem onClick={() => removeProjectModal.startRemove(project)} disabled={loading}
className={'text-red-700 focus:text-red-700'}>
variant={"success"}>{tc("projects:backup")}</RowButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size={"icon"} className={"hover:bg-primary/10 text-primary hover:text-primary"}><EllipsisHorizontalIcon
className={"size-5"}/></Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={openProjectFolder}
disabled={removed || loading}>{tc("projects:menuitem:open directory")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => removeProjectModal.startRemove(project)} disabled={loading}
className={'text-destructive focus:text-destructive'}>
{tc("projects:remove project")}
</MenuItem>
</MenuList>
</Menu>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{dialogContent}
{removeProjectModal.dialog}
@ -652,32 +688,35 @@ function ProjectViewHeader({className, refresh, startCreateProject, isLoading, s
};
return (
<HNavBar className={className}>
<Typography className="cursor-pointer py-1.5 font-bold flex-grow-0">
<HNavBar className={`${className}`}>
<p className="cursor-pointer py-1.5 font-bold flex-grow-0">
{tc("projects")}
</Typography>
</p>
<Tooltip content={tc("projects:tooltip:refresh")}>
<IconButton variant={"text"} onClick={() => refresh?.()} disabled={isLoading}>
{isLoading ? <Spinner className="w-5 h-5"/> : <ArrowPathIcon className={"w-5 h-5"}/>}
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={"ghost"} size={"icon"} onClick={() => refresh?.()} disabled={isLoading}>
{isLoading ? <ArrowPathIcon className="w-5 h-5 animate-spin"/> : <ArrowPathIcon className={"w-5 h-5"}/>}
</Button>
</TooltipTrigger>
<TooltipContent>{tc("projects:tooltip:refresh")}</TooltipContent>
</Tooltip>
<SearchBox className={"w-max flex-grow"} value={search} onChange={(e) => setSearch(e.target.value)}/>
<Menu>
<ButtonGroup>
<Button className={"pl-4 pr-3"} onClick={startCreateProject}>{tc("projects:create new project")}</Button>
<MenuHandler className={"pl-2 pr-2"}>
<DropdownMenu>
<div className={"flex divide-x"}>
<Button className={"rounded-r-none pl-4 pr-3"} onClick={startCreateProject}>{tc("projects:create new project")}</Button>
<DropdownMenuTrigger asChild className={"rounded-l-none pl-2 pr-2"}>
<Button>
<ChevronDownIcon className={"w-4 h-4"}/>
</Button>
</MenuHandler>
</ButtonGroup>
<MenuList>
<MenuItem onClick={addProject}>{tc("projects:add existing project")}</MenuItem>
</MenuList>
</Menu>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent>
<DropdownMenuItem onClick={addProject}>{tc("projects:add existing project")}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{dialog}
</HNavBar>
@ -715,6 +754,14 @@ function CreateProject(
const [unityVersion, setUnityVersion] = useState<(typeof templateUnityVersions)[number]>(latestUnityVersion);
const [customTemplate, setCustomTemplate] = useState<CustomTemplate>();
function onCustomTemplateChange(value: string) {
let newCustomTemplate: CustomTemplate = {
type: "Custom",
name: value,
}
setCustomTemplate(newCustomTemplate);
}
const [projectNameRaw, setProjectName] = useState("New Project");
const projectName = projectNameRaw.trim();
const [projectLocation, setProjectLocation] = useState("");
@ -836,7 +883,7 @@ function CreateProject(
projectNameState = "err";
break;
case "checking":
projectNameCheck = <Spinner/>;
projectNameCheck = <ArrowPathIcon className={"w-5 h-5 animate-spin"} />;
projectNameState = "Ok";
break;
default:
@ -847,27 +894,27 @@ function CreateProject(
let projectNameStateClass;
switch (projectNameState) {
case "Ok":
projectNameStateClass = "text-green-700";
projectNameStateClass = "text-success";
break;
case "warn":
projectNameStateClass = "text-yellow-900";
projectNameStateClass = "text-warning";
break;
case "err":
projectNameStateClass = "text-red-900";
projectNameStateClass = "text-destructive";
}
if (checking) projectNameCheck = <Spinner/>
if (checking) projectNameCheck = <ArrowPathIcon className={"w-5 h-5 animate-spin"} />
let dialogBody;
switch (state) {
case "loadingInitialInformation":
dialogBody = <Spinner/>;
dialogBody = <ArrowPathIcon className={"w-5 h-5 animate-spin"} />;
break;
case "enteringInformation":
const renderUnityVersion = (unityVersion: string) => {
if (unityVersion === latestUnityVersion) {
return <>{unityVersion} <span className={"text-green-700"}>{tc("projects:latest")}</span></>
return <>{unityVersion} <span className={"text-success"}>{tc("projects:latest")}</span></>
} else {
return unityVersion;
}
@ -876,77 +923,91 @@ function CreateProject(
<VStack>
<div className={"flex gap-1"}>
<div className={"flex items-center"}>
<Typography as={"label"}>{tc("projects:template:type")}</Typography>
<label>{tc("projects:template:type")}</label>
</div>
<VGSelect menuClassName={"z-[19999]"} value={tc(`projects:type:${templateType}`)}
onChange={value => setTemplateType(value)}>
<VGOption value={"avatars"}>{tc("projects:type:avatars")}</VGOption>
<VGOption value={"worlds"}>{tc("projects:type:worlds")}</VGOption>
<VGOption value={"custom"} disabled={customTemplates.length == 0}>{tc("projects:type:custom")}</VGOption>
</VGSelect>
<Select defaultValue={templateType} onValueChange={value => setTemplateType(value as any)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={"avatars"}>{tc("projects:type:avatars")}</SelectItem>
<SelectItem value={"worlds"}>{tc("projects:type:worlds")}</SelectItem>
<SelectItem value={"custom"} disabled={customTemplates.length == 0}>{tc("projects:type:custom")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
{templateType !== "custom" ? (
<div className={"flex gap-1"}>
<div className={"flex items-center"}>
<Typography as={"label"}>{tc("projects:template:unity version")}</Typography>
<label>{tc("projects:template:unity version")}</label>
</div>
<VGSelect menuClassName={"z-[19999]"} value={renderUnityVersion(unityVersion)}
onChange={value => setUnityVersion(value)}>
{templateUnityVersions.map(unityVersion =>
<VGOption value={unityVersion} key={unityVersion}>{renderUnityVersion(unityVersion)}</VGOption>)}
</VGSelect>
<Select defaultValue={unityVersion} onValueChange={value => setUnityVersion(value as any)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{templateUnityVersions.map(unityVersion =>
<SelectItem value={unityVersion} key={unityVersion}>{renderUnityVersion(unityVersion)}</SelectItem>)}
</SelectContent>
</Select>
</div>
) : (
<div className={"flex gap-1"}>
<div className={"flex items-center"}>
<Typography as={"label"}>{tc("projects:template")}</Typography>
<label>{tc("projects:template")}</label>
</div>
<VGSelect menuClassName={"z-[19999]"} value={customTemplate?.name}
onChange={value => setCustomTemplate(value)}>
{customTemplates.map(template =>
<VGOption value={template} key={template.name}>{template.name}</VGOption>)}
</VGSelect>
<Select value={customTemplate?.name} onValueChange={onCustomTemplateChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{customTemplates.map(template =>
<SelectItem value={template.name} key={template.name}>{template.name}</SelectItem>)}
</SelectGroup>
</SelectContent>
</Select>
</div>
)}
<Input label={"Project Name"} value={projectNameRaw} onChange={(e) => setProjectName(e.target.value)}/>
<div className={"flex gap-1"}>
<Input className="flex-auto" label={"Project Location"} value={projectLocation} disabled/>
<Input value={projectNameRaw} onChange={(e) => setProjectName(e.target.value)}/>
<div className={"flex gap-1 items-center"}>
<Input className="flex-auto" value={projectLocation} disabled/>
<Button className="flex-none px-4"
onClick={selectProjectDefaultFolder}>{tc("general:button:select")}</Button>
</div>
<Typography variant={"small"} className={"whitespace-normal"}>
<small className={"whitespace-normal"}>
{tc("projects:hint:path of creating project", {path: `${projectLocation}${pathSeparator()}${projectName}`}, {
components: {
path: <span className={"p-0.5 font-path whitespace-pre bg-gray-100"}/>
path: <span className={"p-0.5 font-path whitespace-pre bg-secondary text-secondary-foreground"}/>
}
})}
</Typography>
<Typography variant={"small"} className={`whitespace-normal ${projectNameStateClass}`}>
</small>
<small className={`whitespace-normal ${projectNameStateClass}`}>
{projectNameCheck}
</Typography>
</small>
</VStack>
</>;
break;
case "creating":
dialogBody = <>
<Spinner/>
<Typography>{tc("projects:creating project...")}</Typography>
<ArrowPathIcon className={"w-5 h-5 animate-spin"} />
<p>{tc("projects:creating project...")}</p>
</>;
break;
}
return <Dialog handler={nop} open>
<DialogHeader>{tc("projects:create new project")}</DialogHeader>
<DialogBody>
return <DialogOpen>
<DialogTitle>{tc("projects:create new project")}</DialogTitle>
<DialogDescription>
{dialogBody}
</DialogBody>
<DialogFooter>
<div className={"flex gap-2"}>
<Button onClick={close} disabled={state == "creating"}>{tc("general:button:cancel")}</Button>
<Button onClick={createProject}
disabled={state == "creating" || checking || projectNameState == "err"}>{tc("projects:button:create")}</Button>
</div>
</DialogDescription>
<DialogFooter className={"gap-2"}>
<Button onClick={close} disabled={state == "creating"}>{tc("general:button:cancel")}</Button>
<Button onClick={createProject}
disabled={state == "creating" || checking || projectNameState == "err"}>{tc("projects:button:create")}</Button>
</DialogFooter>
{dialog}
</Dialog>;
</DialogOpen>;
}

View file

@ -1,23 +1,14 @@
"use client"
import {
Button,
Card,
Checkbox,
Dialog,
DialogBody,
DialogFooter,
DialogHeader,
IconButton,
Input,
List,
ListItem,
Tooltip,
Typography
} from "@material-tailwind/react";
import {Button} from "@/components/ui/button";
import {Card, CardHeader} from "@/components/ui/card";
import {Checkbox} from "@/components/ui/checkbox";
import {DialogDescription, DialogFooter, DialogOpen, DialogTitle} from "@/components/ui/dialog";
import {Input} from "@/components/ui/input";
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
import {useQuery} from "@tanstack/react-query";
import {
deepLinkHasAddRepository, deepLinkTakeAddRepository,
deepLinkTakeAddRepository,
environmentAddRepository,
environmentDownloadRepository,
environmentHideRepository,
@ -28,12 +19,10 @@ import {
TauriUserRepository
} from "@/lib/bindings";
import {HNavBar, VStack} from "@/components/layout";
import React, {Suspense, useCallback, useEffect, useMemo, useState} from "react";
import React, {Suspense, useCallback, useEffect, useId, useMemo, useState} from "react";
import {MinusCircleIcon, PlusCircleIcon, XCircleIcon} from "@heroicons/react/24/outline";
import {nop} from "@/lib/nop";
import {toastError, toastNormal, toastSuccess, toastThrownError} from "@/lib/toast";
import {tc, tt} from "@/lib/i18n";
import {InputNoLabel} from "@/components/InputNoLabel";
import {loadManifestWithRetries} from "next/dist/server/load-components";
import {useTauriListen} from "@/lib/use-tauri-listen";
@ -161,27 +150,30 @@ function PageBody() {
const _exhaustiveCheck: never = state;
}
const dialog = dialogBody ?
<Dialog handler={nop} open><DialogHeader>{tc("vpm repositories:button:add repository")}</DialogHeader>{dialogBody}
</Dialog> : null;
<DialogOpen>
<DialogTitle>{tc("vpm repositories:button:add repository")}</DialogTitle>{dialogBody}
</DialogOpen> : null;
return (
<VStack className={"p-4 overflow-y-auto"}>
<HNavBar className={"flex-shrink-0"}>
<Typography className="cursor-pointer py-1.5 font-bold flex-grow-0">
<p className="cursor-pointer py-1.5 font-bold flex-grow-0">
{tc("vpm repositories:community repositories")}
</Typography>
</p>
<Button
onClick={() => setState({type: 'enteringRepositoryInfo'})}>{tc("vpm repositories:button:add repository")}</Button>
</HNavBar>
<main className="flex-shrink flex-grow overflow-hidden flex">
<Card className="w-full overflow-x-auto overflow-y-scroll shadow-none">
<RepositoryTable
userRepos={result.data?.user_repositories || []}
hiddenUserRepos={hiddenUserRepos}
removeRepository={removeRepository}
refetch={() => result.refetch()}
/>
{dialog}
<CardHeader>
<RepositoryTable
userRepos={result.data?.user_repositories || []}
hiddenUserRepos={hiddenUserRepos}
removeRepository={removeRepository}
refetch={() => result.refetch()}
/>
{dialog}
</CardHeader>
</Card>
</main>
</VStack>
@ -214,8 +206,8 @@ function RepositoryTable(
<tr>
{TABLE_HEAD.map((head, index) => (
<th key={index}
className={`sticky top-0 z-10 border-b border-blue-gray-100 bg-blue-gray-50 p-2.5`}>
<Typography variant="small" className="font-normal leading-none">{tc(head)}</Typography>
className={`sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5`}>
<small className="font-normal leading-none">{tc(head)}</small>
</th>
))}
</tr>
@ -250,7 +242,7 @@ function RepositoryRow(
}
) {
const cellClass = "p-2.5";
const id = `repository-${repo.id}`;
const id = useId();
const [removeDialogOpen, setRemoveDialogOpen] = useState(false);
@ -265,12 +257,13 @@ function RepositoryRow(
let dialog;
if (removeDialogOpen) {
dialog = <Dialog handler={nop} open>
<DialogHeader>{tc("vpm repositories:remove repository")}</DialogHeader>
<DialogBody>
<Typography
className={"whitespace-normal font-normal"}>{tc("vpm repositories:dialog:confirm remove description", {name: repo.display_name})}</Typography>
</DialogBody>
dialog = <DialogOpen>
<DialogTitle>{tc("vpm repositories:remove repository")}</DialogTitle>
<DialogDescription>
<p className={"whitespace-normal font-normal"}>
{tc("vpm repositories:dialog:confirm remove description", {name: repo.display_name})}
</p>
</DialogDescription>
<DialogFooter>
<Button onClick={() => setRemoveDialogOpen(false)}>{tc("general:button:cancel")}</Button>
<Button onClick={() => {
@ -278,32 +271,35 @@ function RepositoryRow(
setRemoveDialogOpen(false);
}} className={"ml-2"}>{tc("vpm repositories:remove repository")}</Button>
</DialogFooter>
</Dialog>;
</DialogOpen>;
}
return (
<tr className="even:bg-blue-gray-50/50">
<tr className="even:bg-secondary/30">
<td className={cellClass}>
<Checkbox ripple={false} containerProps={{className: "p-0 rounded-none"}} id={id}
checked={selected} onChange={onChange}/>
<Checkbox id={id}
checked={selected} onCheckedChange={onChange}/>
</td>
<td className={cellClass}>
<label htmlFor={id}>
<Typography className="font-normal">
<p className="font-normal">
{repo.display_name}
</Typography>
</p>
</label>
</td>
<td className={cellClass}>
<Typography className="font-normal">
<p className="font-normal">
{repo.url}
</Typography>
</p>
</td>
<td className={`${cellClass} w-0`}>
<Tooltip content={tc("vpm repositories:remove repository")}>
<IconButton onClick={() => setRemoveDialogOpen(true)} variant={"text"}>
<XCircleIcon className={"size-5 text-red-700"}/>
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={() => setRemoveDialogOpen(true)} variant={"ghost"} size={"icon"}>
<XCircleIcon className={"size-5 text-destructive"}/>
</Button>
</TooltipTrigger>
<TooltipContent>{tc("vpm repositories:remove repository")}</TooltipContent>
</Tooltip>
</td>
{dialog}
@ -408,21 +404,21 @@ function EnteringRepositoryInfo(
return (
<>
<DialogBody>
<Typography className={'font-normal'}>
<DialogDescription>
<p className={'font-normal'}>
{tc("vpm repositories:dialog:enter repository info")}
</Typography>
<Input type={"vpm repositories:url"} label={"URL"} value={url} onChange={e => setUrl(e.target.value)}
placeholder={"https://vpm.anatawa12.com/vpm.json"}></Input>
</p>
<Input className={"w-full"} type={"vpm repositories:url"} value={url} onChange={e => setUrl(e.target.value)}
placeholder={"https://vpm.anatawa12.com/vpm.json"}></Input>
<details>
<summary className={"font-bold"}>{tc("vpm repositories:dialog:headers")}</summary>
<div className={"w-full max-h-[50vh] overflow-y-auto"}>
<table className={"w-full"}>
<thead>
<tr>
<th className={"sticky top-0 z-10 bg-white"}>{tc("vpm repositories:dialog:header name")}</th>
<th className={"sticky top-0 z-10 bg-white"}>{tc("vpm repositories:dialog:header value")}</th>
<th className={"sticky top-0 z-10 bg-white"}></th>
<th className={"sticky top-0 z-10"}>{tc("vpm repositories:dialog:header name")}</th>
<th className={"sticky top-0 z-10"}>{tc("vpm repositories:dialog:header value")}</th>
<th className={"sticky top-0 z-10"}></th>
</tr>
</thead>
<tbody>
@ -430,7 +426,7 @@ function EnteringRepositoryInfo(
headerArray.map(({name, value, id}, idx) => (
<tr key={id}>
<td>
<InputNoLabel
<Input
type={"text"}
value={name}
className={"w-96"}
@ -445,7 +441,7 @@ function EnteringRepositoryInfo(
/>
</td>
<td>
<InputNoLabel
<Input
type={"text"}
value={value}
onChange={e => {
@ -459,15 +455,21 @@ function EnteringRepositoryInfo(
/>
</td>
<td className={"w-20"}>
<Tooltip content={tc("vpm repositories:tooltip:add header")} className={"z-[19999]"}>
<IconButton variant={"text"} onClick={addHeader}>
<PlusCircleIcon color={"green"} className={"size-5"}/>
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={"ghost"} size={"icon"} onClick={addHeader}>
<PlusCircleIcon color={"green"} className={"size-5"}/>
</Button>
</TooltipTrigger>
<TooltipContent className={"z-[19999]"}>{tc("vpm repositories:tooltip:add header")}</TooltipContent>
</Tooltip>
<Tooltip content={tc("vpm repositories:tooltip:remove header")} className={"z-[19999]"}>
<IconButton variant={"text"} onClick={() => removeHeader(idx)}>
<MinusCircleIcon color={"red"} className={"size-5"}/>
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={"ghost"} size={"icon"} onClick={() => removeHeader(idx)}>
<MinusCircleIcon color={"red"} className={"size-5"}/>
</Button>
</TooltipTrigger>
<TooltipContent className={"z-[19999]"}>{tc("vpm repositories:tooltip:remove header")}</TooltipContent>
</Tooltip>
</td>
</tr>
@ -478,12 +480,12 @@ function EnteringRepositoryInfo(
</div>
</details>
{foundHeaderNameError &&
<Typography className={"text-red-700"}>{tc("vpm repositories:hint:invalid header names")}</Typography>}
<p className={"text-destructive"}>{tc("vpm repositories:hint:invalid header names")}</p>}
{foundHeaderValueError &&
<Typography className={"text-red-700"}>{tc("vpm repositories:hint:invalid header values")}</Typography>}
<p className={"text-destructive"}>{tc("vpm repositories:hint:invalid header values")}</p>}
{foundDuplicateHeader &&
<Typography className={"text-red-700"}>{tc("vpm repositories:hint:duplicate headers")}</Typography>}
</DialogBody>
<p className={"text-destructive"}>{tc("vpm repositories:hint:duplicate headers")}</p>}
</DialogDescription>
<DialogFooter>
<Button onClick={cancel}>{tc("general:button:cancel")}</Button>
<Button onClick={onAddRepository} className={"ml-2"}
@ -502,11 +504,11 @@ function LoadingRepository(
) {
return (
<>
<DialogBody>
<Typography>
<DialogDescription>
<p>
{tc("vpm repositories:dialog:downloading...")}
</Typography>
</DialogBody>
</p>
</DialogDescription>
<DialogFooter>
<Button onClick={cancel}>{tc("general:button:cancel")}</Button>
</DialogFooter>
@ -523,11 +525,11 @@ function Duplicated(
) {
return (
<>
<DialogBody>
<Typography>
<DialogDescription>
<p>
{tc("vpm repositories:dialog:already added")}
</Typography>
</DialogBody>
</p>
</DialogDescription>
<DialogFooter>
<Button onClick={cancel}>{tc("general:button:ok")}</Button>
</DialogFooter>
@ -550,13 +552,13 @@ function Confirming(
) {
return (
<>
<DialogBody className={"max-h-[50vh] overflow-y-auto font-normal"}>
<Typography
className={"font-normal"}>{tc("vpm repositories:dialog:name", {name: repo.display_name})}</Typography>
<Typography className={"font-normal"}>{tc("vpm repositories:dialog:url", {url: repo.url})}</Typography>
<DialogDescription className={"max-h-[50vh] overflow-y-auto font-normal"}>
<p
className={"font-normal"}>{tc("vpm repositories:dialog:name", {name: repo.display_name})}</p>
<p className={"font-normal"}>{tc("vpm repositories:dialog:url", {url: repo.url})}</p>
{Object.keys(headers).length > 0 && (
<>
<Typography className={"font-normal"}>{tc("vpm repositories:dialog:headers")}</Typography>
<p className={"font-normal"}>{tc("vpm repositories:dialog:headers")}</p>
<ul className={"list-disc pl-6"}>
{
Object.entries(headers).map(([key, value], idx) => (
@ -566,7 +568,7 @@ function Confirming(
</ul>
</>
)}
<Typography className={"font-normal"}>{tc("vpm repositories:dialog:packages")}</Typography>
<p className={"font-normal"}>{tc("vpm repositories:dialog:packages")}</p>
<ul className={"list-disc pl-6"}>
{
repo.packages.map((info, idx) => (
@ -574,7 +576,7 @@ function Confirming(
))
}
</ul>
</DialogBody>
</DialogDescription>
<DialogFooter>
<Button onClick={cancel}>{tc("general:button:cancel")}</Button>
<Button onClick={add} className={"ml-2"}>{tc("vpm repositories:button:add repository")}</Button>

View file

@ -1,6 +1,6 @@
"use client";
import {Card, Typography} from "@material-tailwind/react";
import {Card} from "@/components/ui/card";
import Link from "next/link";
import {Licenses} from "@/lib/licenses";
import {shellOpen} from "@/lib/shellOpen";
@ -9,7 +9,7 @@ export default function RenderPage({licenses}: { licenses: Licenses | null }) {
if (licenses === null) {
return (
<div className={"p-4 whitespace-normal"}>
<Typography>Failed to load licenses.</Typography>
<p>Failed to load licenses.</p>
</div>
);
}
@ -17,18 +17,18 @@ export default function RenderPage({licenses}: { licenses: Licenses | null }) {
return (
<div className={"overflow-y-scroll"}>
<Card className={"m-4 p-4"}>
<Typography>
<p>
This project is built on top of many open-source projects.<br/>
Here are the licenses of the projects used in this project:
</Typography>
</p>
<ul>
</ul>
</Card>
{licenses.map((license, idx) => (
<Card className={"m-4 p-4"} key={idx}>
<Typography as={'h3'}>{license.name}</Typography>
<Typography as={'h4'}>Used by:</Typography>
<h3>{license.name}</h3>
<h4>Used by:</h4>
<ul className={"ml-2"}>
{license.packages.map(pkg => (
<li key={`${pkg.name}@${pkg.version}`}><a

View file

@ -1,6 +1,17 @@
"use client"
import {Button, Card, Checkbox, Input, Typography} from "@material-tailwind/react";
import {Button} from "@/components/ui/button";
import {Card, CardHeader} from "@/components/ui/card";
import {Checkbox} from "@/components/ui/checkbox";
import {Input} from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import Link from "next/link";
import {useQuery} from "@tanstack/react-query";
import {
@ -12,7 +23,10 @@ import {
environmentPickUnityHub,
environmentSetBackupFormat,
environmentSetLanguage,
environmentSetTheme,
environmentSetShowPrereleasePackages,
environmentLanguage,
environmentTheme,
TauriEnvironmentSettings,
utilGetVersion,
} from "@/lib/bindings";
@ -20,7 +34,6 @@ import {HNavBar, VStack} from "@/components/layout";
import React from "react";
import {toastError, toastSuccess, toastThrownError} from "@/lib/toast";
import i18next, {languages, tc, tt} from "@/lib/i18n";
import {VGOption, VGSelect} from "@/components/select";
import {useFilePickerFunction} from "@/lib/use-file-picker-dialog";
import {emit} from "@tauri-apps/api/event";
import {shellOpen} from "@/lib/shellOpen";
@ -51,9 +64,9 @@ export default function Page() {
return (
<VStack className={"p-4"}>
<HNavBar className={"flex-shrink-0"}>
<Typography className="cursor-pointer py-1.5 font-bold flex-grow-0">
<p className="cursor-pointer py-1.5 font-bold flex-grow-0">
{tc("settings")}
</Typography>
</p>
</HNavBar>
{body}
</VStack>
@ -188,9 +201,9 @@ function Settings(
}
}
const toggleShowPrereleasePackages = async (e: React.ChangeEvent<HTMLInputElement>) => {
const toggleShowPrereleasePackages = async (e: "indeterminate" | boolean) => {
try {
await environmentSetShowPrereleasePackages(e.target.checked)
await environmentSetShowPrereleasePackages(e===true)
refetch()
} catch (e) {
console.error(e);
@ -198,13 +211,38 @@ function Settings(
}
}
const {data: lang, refetch: refetchLang} = useQuery({
queryKey: ["environmentLanguage"],
queryFn: environmentLanguage
})
const changeLanguage = async (value: string) => {
await Promise.all([
i18next.changeLanguage(value),
environmentSetLanguage(value),
refetchLang(),
])
};
const [theme, setTheme] = React.useState<string | null>(null);
React.useEffect(() => {
(async () => {
const theme = await environmentTheme();
setTheme(theme);
})();
}, [])
const changeTheme = async (theme: string) => {
await environmentSetTheme(theme);
setTheme(theme);
if (theme === "system") {
const {appWindow} = await import("@tauri-apps/api/window");
theme = await appWindow.theme() ?? "light";
}
document.documentElement.setAttribute("class", theme);
};
const reportIssue = async () => {
const url = new URL("https://github.com/vrc-get/vrc-get/issues/new")
url.searchParams.append("labels", "bug,vrc-get-gui")
@ -233,11 +271,11 @@ function Settings(
<main className="flex flex-col gap-2 flex-shrink overflow-y-auto flex-grow">
<Card className={"flex-shrink-0 p-4"}>
<h2 className={"pb-2"}>{tc("settings:unity hub path")}</h2>
<div className={"flex gap-1"}>
<div className={"flex gap-1 items-center"}>
{
settings.unity_hub
? <Input className="flex-auto" value={settings.unity_hub} disabled/>
: <Input value={"Unity Hub Not Found"} disabled className={"flex-auto text-red-900"}/>
: <Input value={"Unity Hub Not Found"} disabled className={"flex-auto text-destructive"}/>
}
<Button className={"flex-none px-4"} onClick={selectUnityHub}>{tc("general:button:select")}</Button>
</div>
@ -250,15 +288,17 @@ function Settings(
<Button onClick={addUnity} size={"sm"} className={"m-1"}>{tc("settings:button:add unity")}</Button>
</div>
<Card className="w-full overflow-x-auto overflow-y-scroll min-h-[20vh]">
<UnityTable unityPaths={settings.unity_paths}/>
<CardHeader>
<UnityTable unityPaths={settings.unity_paths}/>
</CardHeader>
</Card>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<h2>{tc("settings:default project path")}</h2>
<Typography className={"whitespace-normal"}>
<p className={"whitespace-normal"}>
{tc("settings:default project path description")}
</Typography>
<div className={"flex gap-1"}>
</p>
<div className={"flex gap-1 items-center"}>
<Input className="flex-auto" value={settings.default_project_path} disabled/>
<Button className={"flex-none px-4"}
onClick={selectProjectDefaultFolder}>{tc("general:button:select")}</Button>
@ -268,10 +308,10 @@ function Settings(
<h2>{tc("projects:backup")}</h2>
<div className="mt-2">
<h3>{tc("settings:backup:path")}</h3>
<Typography className={"whitespace-normal"}>
<p className={"whitespace-normal"}>
{tc("settings:backup:path description")}
</Typography>
<div className={"flex gap-1"}>
</p>
<div className={"flex gap-1 items-center"}>
<Input className="flex-auto" value={settings.project_backup_path} disabled/>
<Button className={"flex-none px-4"}
onClick={selectProjectBackupFolder}>{tc("general:button:select")}</Button>
@ -280,40 +320,77 @@ function Settings(
<div className="mt-2">
<label className={"flex items-center"}>
<h3>{tc("settings:backup:format")}</h3>
<VGSelect value={tc("settings:backup:format:" + settings.backup_format)} onChange={setBackupFormat}>
<VGOption value={"default"}>{tc("settings:backup:format:default")}</VGOption>
<VGOption value={"zip-store"}>{tc("settings:backup:format:zip-store")}</VGOption>
<VGOption value={"zip-fast"}>{tc("settings:backup:format:zip-fast")}</VGOption>
<VGOption value={"zip-best"}>{tc("settings:backup:format:zip-best")}</VGOption>
</VGSelect>
<Select defaultValue={settings.backup_format} onValueChange={setBackupFormat}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={"default"}>{tc("settings:backup:format:default")}</SelectItem>
<SelectItem value={"zip-store"}>{tc("settings:backup:format:zip-store")}</SelectItem>
<SelectItem value={"zip-fast"}>{tc("settings:backup:format:zip-fast")}</SelectItem>
<SelectItem value={"zip-best"}>{tc("settings:backup:format:zip-best")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</label>
</div>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<Typography className={"whitespace-normal"}>
<p className={"whitespace-normal"}>
{tc("settings:show prerelease description")}
</Typography>
</p>
<label className={"flex items-center"}>
<Checkbox checked={settings.show_prerelease_packages} onChange={toggleShowPrereleasePackages}/>
<div className={"p-3"}>
<Checkbox checked={settings.show_prerelease_packages} onCheckedChange={(e) => toggleShowPrereleasePackages(e)}/>
</div>
{tc("settings:show prerelease")}
</label>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<label className={"flex items-center"}>
<h2>{tc("settings:language")}: </h2>
<VGSelect value={tc("settings:langName")} onChange={changeLanguage} menuClassName={"w-96"}>
{
languages.map((lang) => (
<VGOption key={lang} value={lang}>{tc("settings:langName", {lng: lang})}</VGOption>
))
}
</VGSelect>
{lang && (
<Select defaultValue={lang} onValueChange={changeLanguage}>
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{
languages.map((lang) => (
<SelectItem key={lang} value={lang}>{tc("settings:langName", {lng: lang})}</SelectItem>
))
}
</SelectGroup>
</SelectContent>
</Select>
)}
</label>
</Card>
{unityDialog}
{unityHubDialog}
{projectDefaultDialog}
{projectBackupDialog}
<Card className={"flex-shrink-0 p-4"}>
<label className={"flex items-center"}>
<h2>{tc("settings:theme")}: </h2>
{theme && (
<Select defaultValue={theme} onValueChange={changeTheme}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={"system"}>{tc("settings:theme:system")}</SelectItem>
<SelectItem value={"light"}>{tc("settings:theme:light")}</SelectItem>
<SelectItem value={"dark"}>{tc("settings:theme:dark")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
)}
</label>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<h2>{tc("settings:check update")}</h2>
<div>
@ -322,9 +399,9 @@ function Settings(
</Card>
{osType != "Darwin" && <Card className={"flex-shrink-0 p-4"}>
<h2>{tc("settings:vcc scheme")}</h2>
<Typography className={"whitespace-normal"}>
<p className={"whitespace-normal"}>
{tc("settings:vcc scheme description")}
</Typography>
</p>
<div>
<Button onClick={installVccProtocol}>{tc("settings:register vcc scheme")}</Button>
</div>
@ -337,11 +414,11 @@ function Settings(
</Card>
<Card className={"flex-shrink-0 p-4"}>
<h2>{tc("settings:licenses")}</h2>
<Typography className={"whitespace-normal"}>
<p className={"whitespace-normal"}>
{tc("settings:licenses description", {}, {
components: {l: <Link href={"/settings/licenses"} className={"underline"}/>}
})}
</Typography>
</p>
</Card>
</main>
)
@ -361,8 +438,8 @@ function UnityTable(
<tr>
{UNITY_TABLE_HEAD.map((head, index) => (
<th key={index}
className={`sticky top-0 z-10 border-b border-blue-gray-100 bg-blue-gray-50 p-2.5`}>
<Typography variant="small" className="font-normal leading-none">{tc(head)}</Typography>
className={`sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5`}>
<small className="font-normal leading-none">{tc(head)}</small>
</th>
))}
</tr>
@ -370,7 +447,7 @@ function UnityTable(
<tbody>
{
unityPaths.map(([path, version, isFromHub]) => (
<tr key={path}>
<tr key={path} className="even:bg-secondary/30">
<td className={"p-2.5"}>{version}</td>
<td className={"p-2.5"}>{path}</td>
<td className={"p-2.5"}>

View file

@ -0,0 +1,140 @@
"use client"
import {Button} from "@/components/ui/button";
import {Card, CardHeader} from "@/components/ui/card";
import {Checkbox} from "@/components/ui/checkbox";
import {Input} from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem, SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {HNavBar, VStack} from "@/components/layout";
import {toastError, toastInfo, toastNormal, toastSuccess} from "@/lib/toast";
export default function Page() {
return (
<VStack className={"p-4"}>
<HNavBar className={"flex-shrink-0"}>
<p className="cursor-pointer py-1.5 font-bold flex-grow-0">
UI Palette (dev only)
</p>
</HNavBar>
<main className="flex flex-col gap-2 flex-shrink overflow-y-auto flex-grow">
<Card className={"flex-shrink-0 p-4"}>
<h2 className={"pb-2"}>File Selector</h2>
<div className={"flex gap-1 items-center"}>
<Input className="flex-auto" value={"/some/path/field"} disabled/>
<Button className={"flex-none px-4"}>Select</Button>
</div>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<div className={"pb-2 flex align-middle"}>
<div className={"flex-grow flex items-center"}>
<h2>Table</h2>
</div>
<Button size={"sm"} className={"m-1"}>Add Unity</Button>
</div>
<Card className="w-full overflow-x-auto overflow-y-scroll min-h-[20vh]">
<CardHeader>
<UnityTable/>
</CardHeader>
</Card>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<h2>Dropdown Selector</h2>
<div className="mt-2">
<label className={"flex items-center"}>
<h3>Selector</h3>
<Select>
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={"default"}>Option 0</SelectItem>
<SelectItem value={"zip-store"}>Option 1</SelectItem>
<SelectLabel>Select Label</SelectLabel>
<SelectItem value={"zip-fast"}>Option 2</SelectItem>
<SelectItem value={"zip-best"}>Option3</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</label>
</div>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<p className={"whitespace-normal"}>
Some Description Here
</p>
<label className={"flex items-center"}>
<div className={"p-3"}>
<Checkbox/>
</div>
Checkbox
</label>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<h2 className={"pb-2"}>Buttons</h2>
<div className={"flex gap-2 items-center"}>
<Button>Normal</Button>
<Button variant={"destructive"}>Destructive</Button>
<Button variant={"success"}>Success</Button>
<Button variant={"info"}>Info</Button>
<Button variant={"outline-success"}>Outline Success</Button>
<Button variant={"ghost"}>Ghost</Button>
<Button variant={"ghost-destructive"}>Ghost Destructive</Button>
</div>
</Card>
<Card className={"flex-shrink-0 p-4"}>
<h2 className={"pb-2"}>Toasts</h2>
<div className={"flex gap-2 items-center"}>
<Button onClick={() => toastNormal("Normal Toast Body")}>Normal</Button>
<Button variant={"destructive"} onClick={() => toastError("Error Toast Body")}>Error</Button>
<Button variant={"success"} onClick={() => toastSuccess("Success Toast Body")}>Success</Button>
<Button variant={"info"} onClick={() => toastInfo("Info Toast Body")}>Info</Button>
</div>
</Card>
</main>
</VStack>
)
}
function UnityTable() {
const unityPaths: [path: string, version: string, fromHub: boolean][] = [
["/Applications/Unity/Hub/Editor/2019.4.31f1/Unity.app/Contents/MacOS/Unity", "2019.4.31f1", true],
["/Applications/Unity/Hub/Editor/2022.3.22f1/Unity.app/Contents/MacOS/Unity", "2022.3.22f1", true],
];
const UNITY_TABLE_HEAD = ["Version", "Path", "Source"];
return (
<table className="relative table-auto text-left">
<thead>
<tr>
{UNITY_TABLE_HEAD.map((head, index) => (
<th key={index}
className={`sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5`}>
<small className="font-normal leading-none">{head}</small>
</th>
))}
</tr>
</thead>
<tbody>
{
unityPaths.map(([path, version, isFromHub]) => (
<tr key={path} className="even:bg-secondary/30">
<td className={"p-2.5"}>{version}</td>
<td className={"p-2.5"}>{path}</td>
<td className={"p-2.5"}>
Unity Hub
</td>
</tr>
))
}
</tbody>
</table>
)
}

View file

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View file

@ -1,21 +0,0 @@
import {Input} from "@material-tailwind/react";
import React, {ComponentProps} from "react";
export function InputNoLabel(
props: ComponentProps<typeof Input>
) {
return (
<Input
{...props}
containerProps={{
...props.containerProps,
className: `min-w-[100px] ${props.containerProps?.className}`,
}}
className={`!border-t-blue-gray-300 placeholder:text-blue-gray-300 focus:!border-blue-gray-300 ${props.className}`}
labelProps={{
...props.labelProps,
className: `before:content-none after:content-none ${props.labelProps?.className}`
}}
/>
)
}

View file

@ -1,7 +1,7 @@
import {MagnifyingGlassIcon} from "@heroicons/react/24/solid";
import React from "react";
import {useTranslation} from "react-i18next";
import {InputNoLabel} from "@/components/InputNoLabel";
import {Input} from "@/components/ui/input";
export function SearchBox({className, value, onChange}: {
className?: string,
@ -13,14 +13,14 @@ export function SearchBox({className, value, onChange}: {
return (
<div className={`relative flex gap-2 ${className}`}>
{/* The search box */}
<InputNoLabel
<Input
type="search"
placeholder={t("search:placeholder")}
className={"pl-9 placeholder:opacity-100"}
className={"w-full placeholder:text-primary focus:border-primary pl-9 placeholder:opacity-100"}
value={value}
onChange={onChange}
/>
<MagnifyingGlassIcon className="!absolute left-3 top-[13px]" width={13} height={14}/>
<MagnifyingGlassIcon className="!absolute left-4 top-[17px]" width={13} height={14}/>
</div>
)
}

View file

@ -1,7 +1,8 @@
"use client";
import {Card, List, ListItem, ListItemPrefix} from "@material-tailwind/react";
import {CloudIcon, Cog6ToothIcon, ListBulletIcon} from "@heroicons/react/24/solid";
import {Button} from "@/components/ui/button";
import {Card} from "@/components/ui/card";
import {CloudIcon, Cog6ToothIcon, ListBulletIcon, SwatchIcon} from "@heroicons/react/24/solid";
import React from "react";
import {Bars4Icon} from "@heroicons/react/24/outline";
import {useQuery} from "@tanstack/react-query";
@ -32,18 +33,21 @@ export function SideBar({className}: { className?: string }) {
toastNormal(t("sidebar:toast:version copied"));
}
};
const isDev = process.env.NODE_ENV == 'development';
return (
<Card
className={`${className} w-auto max-w-[20rem] p-2 shadow-xl shadow-blue-gray-900/5 ml-4 my-4 shrink-0`}>
<List className="min-w-[10rem] flex-grow">
className={`${className} flex w-auto max-w-[20rem] p-2 shadow-xl shadow-primary/5 ml-4 my-4 shrink-0`}>
<div className="flex flex-col gap-1 p-2 min-w-[10rem] flex-grow">
<SideBarItem href={"/projects"} text={t("projects")} icon={ListBulletIcon}/>
<SideBarItem href={"/repositories"} text={t("vpm repositories")} icon={CloudIcon}/>
<SideBarItem href={"/settings"} text={t("settings")} icon={Cog6ToothIcon}/>
<SideBarItem href={"/log"} text={t("logs")} icon={Bars4Icon}/>
{isDev && <SideBarItem href={"/settings/palette"} text={"UI Palette (dev only)"} icon={SwatchIcon}/>}
<div className={'flex-grow'}/>
<ListItem className={"text-sm"} onClick={copyVersionName}>v{currentVersion}</ListItem>
</List>
<Button variant={"ghost"} className={"text-sm justify-start hover:bg-card hover:text-card-foreground"}
onClick={copyVersionName}>v{currentVersion}</Button>
</div>
</Card>
);
}
@ -54,11 +58,11 @@ function SideBarItem(
const router = useRouter();
const IconElenment = icon;
return (
<ListItem onClick={() => router.push(href)}>
<ListItemPrefix>
<Button variant={"ghost"} className={"justify-start"} onClick={() => router.push(href)}>
<div className={"mr-4"}>
<IconElenment className="h-5 w-5"/>
</ListItemPrefix>
</div>
{text}
</ListItem>
</Button>
);
}

View file

@ -1,7 +1,7 @@
"use client"
import React from "react";
import {Navbar} from "@material-tailwind/react";
import {Card} from "@/components/ui/card";
export function VStack({className, children}: { className?: string, children: React.ReactNode }) {
return (
@ -13,10 +13,10 @@ export function VStack({className, children}: { className?: string, children: Re
export function HNavBar({className, children}: { className?: string, children: React.ReactNode }) {
return (
<Navbar className={`${className} mx-auto px-4 py-2`}>
<div className="container mx-auto flex flex-wrap items-center justify-between text-blue-gray-900 gap-2">
<Card className={`${className} mx-auto px-4 py-2 w-full`}>
<div className="mx-auto flex flex-wrap items-center justify-between text-primary gap-2">
{children}
</div>
</Navbar>
</Card>
)
}

View file

@ -3,13 +3,13 @@
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {ToastContainer} from 'react-toastify';
import {useCallback, useEffect, useState} from "react";
import {deepLinkHasAddRepository, environmentLanguage, LogEntry} from "@/lib/bindings";
import {deepLinkHasAddRepository, environmentLanguage, environmentTheme, LogEntry} from "@/lib/bindings";
import i18next from "@/lib/i18n";
import {I18nextProvider} from "react-i18next";
import {toastError, toastNormal} from "@/lib/toast";
import {ThemeProvider} from "@material-tailwind/react";
import {useTauriListen} from "@/lib/use-tauri-listen";
import {usePathname, useRouter} from "next/navigation";
import {TooltipProvider} from "@/components/ui/tooltip";
const queryClient = new QueryClient();
@ -59,6 +59,32 @@ export function Providers({children}: { children: React.ReactNode }) {
return () => i18next.off("languageChanged", changeLanguage);
}, []);
useEffect(() => {
// initially set theme based on query parameter for early feedback
if ('location' in globalThis) {
const search = new URLSearchParams(location.search);
let theme = search.get('theme');
if (theme) {
if (theme === "system") {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
theme = isDark ? "dark" : "light";
}
document.documentElement.setAttribute("class", theme);
}
}
(async () => {
// then, load theme from environment
// the theme can be different from the query parameter if the user has changed it in the settings
let theme = await environmentTheme();
if (theme === "system") {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
theme = isDark ? "dark" : "light";
}
document.documentElement.setAttribute("class", theme);
})();
}, [])
return (
<>
<ToastContainer
@ -76,17 +102,11 @@ export function Providers({children}: { children: React.ReactNode }) {
/>
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18next}>
<ThemeProvider value={{
Typography: {
styles: {
font: 'normal'
}
}
}}>
{<div lang={language} className="contents">
<TooltipProvider>
<div lang={language} className="contents">
{children}
</div> as any}
</ThemeProvider>
</div>
</TooltipProvider>
</I18nextProvider>
</QueryClientProvider>
</>

View file

@ -1,118 +0,0 @@
// based on https://github.com/creativetimofficial/material-tailwind/blob/main/packages/material-tailwind-react/src/components/Select/index.tsx#L298
import React, {createContext, useContext, useState} from "react";
import {Menu, MenuHandler, MenuItem, MenuList, useTheme} from "@material-tailwind/react";
import findMatch from "@material-tailwind/react/utils/findMatch";
import objectsToString from "@material-tailwind/react/utils/objectsToString";
import {twMerge} from "tailwind-merge";
import classnames from "classnames";
import {ChevronDownIcon} from "@heroicons/react/24/solid";
interface SelectContext {
onClick(value: any): void;
}
export const SelectContext = createContext<SelectContext | undefined>(undefined);
export function VGSelect(
{
children,
disabled,
value,
className,
menuClassName,
onChange,
}: {
children: React.ReactNode,
disabled?: boolean,
value?: React.ReactNode,
className?: string,
menuClassName?: string,
onChange?: (value: any) => void,
}
) {
const [state, setState] = useState<string>("close");
const [open, setOpen] = React.useState(false);
const {select} = useTheme();
const {defaultProps, valid, styles} = select;
const {base, variants} = styles;
const contextValue: SelectContext = {
onClick(value: any) {
onChange?.(value);
setOpen(false);
}
}
const size = defaultProps.size;
const selectVariant = variants.outlined;
const selectSize = selectVariant.sizes[findMatch(valid.sizes, size, "md")];
const stateClasses = selectVariant.states[state];
const containerClasses = classnames(
objectsToString(base.container),
objectsToString(selectSize.container),
);
const selectClasses = twMerge(
classnames(
objectsToString(base.select),
objectsToString(selectVariant.base.select),
objectsToString(stateClasses.select),
objectsToString(selectSize.select),
),
className,
);
const arrowClasses = classnames(objectsToString(base.arrow.initial), {
[objectsToString(base.arrow.active)]: open,
});
const buttonContentClasses = "absolute top-2/4 -translate-y-2/4 left-3 pt-0.5";
React.useEffect(() => {
if (open) {
setState("open");
} else {
setState("close");
}
}, [open, value]);
return (
<SelectContext.Provider value={contextValue}>
<Menu open={open} handler={() => setOpen(!open)}>
<div className={containerClasses}>
<MenuHandler>
<button className={selectClasses} disabled={disabled}>
<span className={buttonContentClasses}>{value}</span>
<div className={arrowClasses}>
<ChevronDownIcon className="size-3"/>
</div>
</button>
</MenuHandler>
</div>
<MenuList className={`max-h-96 overflow-y-scroll ${menuClassName}`}>
{children}
</MenuList>
</Menu>
</SelectContext.Provider>
)
}
export const VGOption = React.forwardRef(VGOptionImpl)
function VGOptionImpl(
{
children,
value,
disabled,
}: {
children: React.ReactNode,
value: any,
disabled?: boolean,
},
ref: React.Ref<HTMLButtonElement>
) {
const contextValue = useContext(SelectContext);
return (
<MenuItem ref={ref} disabled={disabled} onClick={() => contextValue?.onClick(value)}>{children}</MenuItem>
)
}

View file

@ -0,0 +1,58 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-primary/50 hover:shadow-primary/50 shadow hover:shadow-md transition-shadow uppercase",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-destructive/50 hover:shadow-destructive/50 shadow hover:shadow-md transition-shadow uppercase",
"outline-success": "border border-input hover:text-accent-foreground border-success hover:border-success/70 text-success hover:text-success/70",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-secondary/50 hover:shadow-secondary/50 shadow hover:shadow-md transition-shadow uppercase",
ghost: "hover:bg-accent text-accent-foreground hover:text-accent-foreground",
"ghost-destructive": "hover:bg-destructive/10 text-destructive hover:text-destructive",
link: "text-primary underline-offset-4 hover:underline",
info: "bg-info text-info-foreground hover:bg-info/90 shadow-info/50 hover:shadow-info/50 shadow hover:shadow-md transition-shadow uppercase",
success: "bg-success text-success-foreground hover:bg-success/90 shadow-success/50 hover:shadow-success/50 shadow hover:shadow-md transition-shadow uppercase",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View file

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View file

@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-5 w-5 shrink-0 rounded border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View file

@ -0,0 +1,126 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"text-foreground fixed left-[50%] top-[50%] z-50 flex flex-col w-full max-w-3xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-4 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogOpen = (props: ComponentProps<typeof DialogContent>) => {
return (
<Dialog open>
<DialogContent {...props} />
</Dialog>
)
}
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-2xl pb-4 font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogOpen,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,198 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View file

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex m-1 h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View file

@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -0,0 +1,42 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View file

@ -0,0 +1,158 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex m-1 h-10 w-full min-w-[12.5rem] items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View file

@ -0,0 +1,36 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider(props: TooltipPrimitive.TooltipProviderProps) {
return (
<TooltipPrimitive.TooltipProvider delayDuration={300} {...props}>
{props.children}
</TooltipPrimitive.TooltipProvider>
)
}
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
const TooltipPortal = TooltipPrimitive.Portal;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipPortal, TooltipProvider }

View file

@ -1,5 +1,6 @@
import React, {ReactNode, useState} from "react";
import {Button, Dialog, DialogBody, DialogFooter, DialogHeader} from "@material-tailwind/react";
import {Button} from "@/components/ui/button";
import {DialogDescription, DialogFooter, DialogOpen, DialogTitle} from "@/components/ui/dialog";
import {projectCreateBackup, TauriProject} from "@/lib/bindings";
import {toastNormal, toastSuccess, toastThrownError} from "@/lib/toast";
import {tc, tt} from "@/lib/i18n";
@ -54,15 +55,15 @@ export function useBackupProjectModal(_: Params = {}): Result {
break;
case "backing-up":
dialog = (
<Dialog open handler={nop} className={'whitespace-normal'}>
<DialogHeader>{tc("projects:dialog:backup header")}</DialogHeader>
<DialogBody>
<DialogOpen className={"whitespace-normal"}>
<DialogTitle>{tc("projects:dialog:backup header")}</DialogTitle>
<DialogDescription>
{tc("projects:dialog:creating backup...")}
</DialogBody>
</DialogDescription>
<DialogFooter>
<Button className="mr-1" onClick={state.cancel}>{tc("general:button:cancel")}</Button>
</DialogFooter>
</Dialog>
</DialogOpen>
);
break;
default:

View file

@ -18,6 +18,14 @@ export function environmentSetLanguage(language: string) {
return invoke()<null>("environment_set_language", { language })
}
export function environmentTheme() {
return invoke()<string>("environment_theme")
}
export function environmentSetTheme(theme: string) {
return invoke()<null>("environment_set_theme", { theme })
}
export function environmentProjects() {
return invoke()<TauriProject[]>("environment_projects")
}
@ -171,13 +179,33 @@ export function projectMigrateProjectToVpm(projectPath: string) {
}
export function projectOpenUnity(projectPath: string, unityPath: string) {
return invoke()<null>("project_open_unity", { projectPath,unityPath })
return invoke()<boolean>("project_open_unity", { projectPath,unityPath })
}
export function projectIsUnityLaunching(projectPath: string) {
return invoke()<boolean>("project_is_unity_launching", { projectPath })
}
export function projectCreateBackup(channel: string, projectPath: string) {
return invoke()<AsyncCallResult<null, null>>("project_create_backup", { channel,projectPath })
}
export function projectGetCustomUnityArgs(projectPath: string) {
return invoke()<string[] | null>("project_get_custom_unity_args", { projectPath })
}
export function projectSetCustomUnityArgs(projectPath: string, args: string[] | null) {
return invoke()<boolean>("project_set_custom_unity_args", { projectPath,args })
}
export function projectGetUnityPath(projectPath: string) {
return invoke()<string | null>("project_get_unity_path", { projectPath })
}
export function projectSetUnityPath(projectPath: string, unityPath: string | null) {
return invoke()<boolean>("project_set_unity_path", { projectPath,unityPath })
}
export function utilOpen(path: string) {
return invoke()<null>("util_open", { path })
}
@ -202,35 +230,35 @@ export function deepLinkInstallVcc() {
return invoke()<null>("deep_link_install_vcc")
}
export type TauriPendingProjectChanges = { changes_version: number; package_changes: ([string, TauriPackageChange])[]; remove_legacy_files: string[]; remove_legacy_folders: string[]; conflicts: ([string, TauriConflictInfo])[] }
export type TauriBasePackageInfo = { name: string; display_name: string | null; aliases: string[]; version: TauriVersion; unity: [number, number] | null; changelog_url: string | null; vpm_dependencies: string[]; legacy_packages: string[]; is_yanked: boolean }
export type TauriProjectType = "Unknown" | "LegacySdk2" | "LegacyWorlds" | "LegacyAvatars" | "UpmWorlds" | "UpmAvatars" | "UpmStarter" | "Worlds" | "Avatars" | "VpmStarter"
export type TauriPackageChange = { InstallNew: TauriBasePackageInfo } | { Remove: TauriRemoveReason }
export type TauriRemoteRepositoryInfo = { display_name: string; id: string; url: string; packages: TauriBasePackageInfo[] }
export type TauriUserRepository = { id: string; url: string | null; display_name: string }
export type TauriCallUnityForMigrationResult = { type: "ExistsWithNonZero"; status: string } | { type: "FinishedSuccessfully" }
export type TauriPickProjectDefaultPathResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful"; new_path: string }
export type TauriPickUnityResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful"
export type TauriProjectCreationInformation = { templates: TauriProjectTemplate[]; default_path: string }
export type TauriRepositoriesInfo = { user_repositories: TauriUserRepository[]; hidden_user_repositories: string[]; hide_local_user_packages: boolean; show_prerelease_packages: boolean }
export type TauriAddProjectWithPickerResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful"
export type TauriRemoveReason = "Requested" | "Legacy" | "Unused"
export type LogEntry = { time: string; level: LogLevel; target: string; message: string }
export type TauriConflictInfo = { packages: string[]; unity_conflict: boolean }
export type TauriUnityVersions = { unity_paths: ([string, string, boolean])[]; recommended_version: string; install_recommended_version_link: string }
export type LogLevel = "Error" | "Warn" | "Info" | "Debug" | "Trace"
export type TauriCreateProjectResult = "AlreadyExists" | "TemplateNotFound" | "Successful"
export type TauriPackageSource = "LocalUser" | { Remote: { id: string; display_name: string } }
export type AddRepositoryInfo = { url: string; headers: { [key: string]: string } }
export type TauriProjectTemplate = { type: "Builtin"; id: string; name: string } | { type: "Custom"; name: string }
export type TauriProjectDetails = { unity: [number, number] | null; unity_str: string | null; unity_revision: string | null; installed_packages: ([string, TauriBasePackageInfo])[]; should_resolve: boolean }
export type TauriPickProjectBackupPathResult = "NoFolderSelected" | "InvalidSelection" | "Successful"
export type AsyncCallResult<P, R> = { type: "Result"; value: R } | { type: "Started" } | { type: "UnusedProgress"; progress: P }
export type TauriVersion = { major: number; minor: number; patch: number; pre: string; build: string }
export type TauriPackage = ({ name: string; display_name: string | null; aliases: string[]; version: TauriVersion; unity: [number, number] | null; changelog_url: string | null; vpm_dependencies: string[]; legacy_packages: string[]; is_yanked: boolean }) & { env_version: number; index: number; source: TauriPackageSource }
export type TauriEnvironmentSettings = { default_project_path: string; project_backup_path: string; unity_hub: string; unity_paths: ([string, string, boolean])[]; show_prerelease_packages: boolean; backup_format: string }
export type TauriBasePackageInfo = { name: string; display_name: string | null; description: string | null; aliases: string[]; version: TauriVersion; unity: [number, number] | null; changelog_url: string | null; vpm_dependencies: string[]; legacy_packages: string[]; is_yanked: boolean }
export type TauriPackageSource = "LocalUser" | { Remote: { id: string; display_name: string } }
export type TauriProjectDirCheckResult = "InvalidNameForFolderName" | "MayCompatibilityProblem" | "WideChar" | "AlreadyExists" | "Ok"
export type TauriUnityVersions = { unity_paths: ([string, string, boolean])[]; recommended_version: string; install_recommended_version_link: string }
export type TauriRemoteRepositoryInfo = { display_name: string; id: string; url: string; packages: TauriBasePackageInfo[] }
export type TauriPendingProjectChanges = { changes_version: number; package_changes: ([string, TauriPackageChange])[]; remove_legacy_files: string[]; remove_legacy_folders: string[]; conflicts: ([string, TauriConflictInfo])[] }
export type TauriProjectDetails = { unity: [number, number] | null; unity_str: string | null; unity_revision: string | null; installed_packages: ([string, TauriBasePackageInfo])[]; should_resolve: boolean }
export type TauriAddRepositoryResult = "BadUrl" | "Success"
export type TauriPickUnityHubResult = "NoFolderSelected" | "InvalidSelection" | "Successful"
export type TauriPickProjectBackupPathResult = "NoFolderSelected" | "InvalidSelection" | "Successful"
export type TauriEnvironmentSettings = { default_project_path: string; project_backup_path: string; unity_hub: string; unity_paths: ([string, string, boolean])[]; show_prerelease_packages: boolean; backup_format: string }
export type TauriRepositoriesInfo = { user_repositories: TauriUserRepository[]; hidden_user_repositories: string[]; hide_local_user_packages: boolean; show_prerelease_packages: boolean }
export type LogEntry = { time: string; level: LogLevel; target: string; message: string }
export type TauriDownloadRepository = { type: "BadUrl" } | { type: "Duplicated" } | { type: "DownloadError"; message: string } | { type: "Success"; value: TauriRemoteRepositoryInfo }
export type TauriPickUnityHubResult = "NoFolderSelected" | "InvalidSelection" | "Successful"
export type TauriRemoveReason = "Requested" | "Legacy" | "Unused"
export type LogLevel = "Error" | "Warn" | "Info" | "Debug" | "Trace"
export type TauriCallUnityForMigrationResult = { type: "ExistsWithNonZero"; status: string } | { type: "FinishedSuccessfully" }
export type TauriPackage = ({ name: string; display_name: string | null; description: string | null; aliases: string[]; version: TauriVersion; unity: [number, number] | null; changelog_url: string | null; vpm_dependencies: string[]; legacy_packages: string[]; is_yanked: boolean }) & { env_version: number; index: number; source: TauriPackageSource }
export type AddRepositoryInfo = { url: string; headers: { [key: string]: string } }
export type TauriPickUnityResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful"
export type TauriConflictInfo = { packages: string[]; unity_conflict: boolean }
export type TauriAddProjectWithPickerResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful"
export type TauriPackageChange = { InstallNew: TauriBasePackageInfo } | { Remove: TauriRemoveReason }
export type TauriProjectTemplate = { type: "Builtin"; id: string; name: string } | { type: "Custom"; name: string }
export type TauriProjectType = "Unknown" | "LegacySdk2" | "LegacyWorlds" | "LegacyAvatars" | "UpmWorlds" | "UpmAvatars" | "UpmStarter" | "Worlds" | "Avatars" | "VpmStarter"
export type AsyncCallResult<P, R> = { type: "Result"; value: R } | { type: "Started" } | { type: "UnusedProgress"; progress: P }
export type TauriCreateProjectResult = "AlreadyExists" | "TemplateNotFound" | "Successful"
export type TauriProjectCreationInformation = { templates: TauriProjectTemplate[]; default_path: string }
export type TauriProject = { list_version: number; index: number; name: string; path: string; project_type: TauriProjectType; unity: string; unity_revision: string | null; last_modified: number; created_at: number; favorite: boolean; is_exists: boolean }
export type TauriUserRepository = { id: string; url: string | null; display_name: string }
export type TauriPickProjectDefaultPathResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful"; new_path: string }

View file

@ -38,6 +38,15 @@ i18next
}
})
if ('location' in globalThis) {
const search = new URLSearchParams(location.search);
const lang = search.get('lang');
if (lang) {
// noinspection JSIgnoredPromiseFromCall
i18next.changeLanguage(lang)
}
}
export default i18next;
export const languages = Object.keys(languageResources);

View file

@ -1,6 +1,6 @@
import React, {ReactNode, useState} from "react";
import {Button, Dialog, DialogBody, DialogFooter, DialogHeader, Typography} from "@material-tailwind/react";
import {nop} from "@/lib/nop";
import {Button} from "@/components/ui/button";
import {DialogDescription, DialogFooter, DialogOpen, DialogTitle} from "@/components/ui/dialog";
import {environmentRemoveProject, environmentRemoveProjectByPath, TauriProject} from "@/lib/bindings";
import {toastSuccess} from "@/lib/toast";
import {tc, tt} from "@/lib/i18n";
@ -64,43 +64,43 @@ export function useRemoveProjectModal({onRemoved}: Params): Result {
}
dialog = (
<Dialog open handler={nop} className={'whitespace-normal'}>
<DialogHeader>{tc("projects:remove project")}</DialogHeader>
<DialogBody>
<Typography className={"font-normal"}>
<DialogOpen className={'whitespace-normal'}>
<DialogTitle>{tc("projects:remove project")}</DialogTitle>
<DialogDescription>
<p className={"font-normal"}>
{tc("projects:dialog:warn removing project", {name: project.name})}
</Typography>
</DialogBody>
</p>
</DialogDescription>
<DialogFooter>
<Button onClick={cancel} className="mr-1">{tc("general:button:cancel")}</Button>
<Button onClick={() => removeProjectButton(false)} className="mr-1 px-2">
{tc("projects:button:remove from list")}
</Button>
<Button onClick={() => removeProjectButton(true)} color={"red"} className="px-2"
<Button onClick={() => removeProjectButton(true)} variant={"destructive"} className="px-2"
disabled={!project.is_exists}>
{tc("projects:button:remove directory")}
</Button>
</DialogFooter>
</Dialog>
</DialogOpen>
);
break;
case "removing":
dialog = (
<Dialog open handler={nop} className={'whitespace-normal'}>
<DialogHeader>{tc("projects:remove project")}</DialogHeader>
<DialogBody>
<DialogOpen className={'whitespace-normal'}>
<DialogTitle>{tc("projects:remove project")}</DialogTitle>
<DialogDescription>
{tc("projects:dialog:removing...")}
</DialogBody>
</DialogDescription>
<DialogFooter>
<Button className="mr-1" disabled>{tc("general:button:cancel")}</Button>
<Button className="mr-1 px-2" disabled>
{tc("projects:button:remove from list")}
</Button>
<Button color={"red"} className="px-2" disabled>
<Button variant={"destructive"} className="px-2" disabled>
{tc("projects:button:remove directory")}
</Button>
</DialogFooter>
</Dialog>
</DialogOpen>
);
break;
default:

View file

@ -6,6 +6,12 @@ export function toastNormal(message: ToastContent) {
});
}
export function toastInfo(message: ToastContent) {
toast.info(message, {
pauseOnFocusLoss: false,
});
}
export function toastSuccess(message: ToastContent) {
toast.success(message, {
pauseOnFocusLoss: false,

View file

@ -1,6 +1,5 @@
import {ReactNode, useCallback, useState} from "react";
import {Dialog, DialogBody, DialogHeader} from "@material-tailwind/react";
import {nop} from "@/lib/nop";
import {Dialog, DialogContent, DialogDescription, DialogTitle} from "@/components/ui/dialog";
import {tc} from "@/lib/i18n";
export function useFilePickerFunction<A extends unknown[], R>(
@ -16,9 +15,11 @@ export function useFilePickerFunction<A extends unknown[], R>(
}
}, [setIsPicking, f]);
let dialog = <Dialog open={isPicking} handler={nop}>
<DialogHeader>{tc("general:dialog:select file or directory header")}</DialogHeader>
<DialogBody>{tc("general:dialog:select file or directory")}</DialogBody>
let dialog = <Dialog open={isPicking}>
<DialogContent>
<DialogTitle>{tc("general:dialog:select file or directory header")}</DialogTitle>
<DialogDescription>{tc("general:dialog:select file or directory")}</DialogDescription>
</DialogContent>
</Dialog>;
return [result, dialog];

View file

@ -1,11 +1,17 @@
import {projectOpenUnity, TauriUnityVersions} from "@/lib/bindings";
import {
environmentUnityVersions,
projectGetUnityPath,
projectOpenUnity,
projectSetUnityPath,
TauriUnityVersions
} from "@/lib/bindings";
import i18next, {tc} from "@/lib/i18n";
import {toastError, toastNormal} from "@/lib/toast";
import {useUnitySelectorDialog} from "@/lib/use-unity-selector-dialog";
import {shellOpen} from "@/lib/shellOpen";
import {Button, Dialog, DialogBody, DialogFooter, DialogHeader, Typography} from "@material-tailwind/react";
import {Button} from "@/components/ui/button";
import {DialogDescription, DialogFooter, DialogOpen, DialogTitle} from "@/components/ui/dialog";
import React from "react";
import {nop} from "@/lib/nop";
export type OpenUnityFunction = (projectPath: string, unityVersion: string | null, unityRevision?: string | null) => void;
@ -22,7 +28,7 @@ type StateInternal = {
unityHubLink: string;
}
export function useOpenUnity(unityVersions: TauriUnityVersions | undefined): Result {
export function useOpenUnity(): Result {
const unitySelector = useUnitySelectorDialog();
const [installStatus, setInstallStatus] = React.useState<StateInternal>({state: "normal"});
@ -31,6 +37,10 @@ export function useOpenUnity(unityVersions: TauriUnityVersions | undefined): Res
toastError(i18next.t("projects:toast:invalid project unity version"));
return;
}
const [unityVersions, selectedPath] = await Promise.all([
environmentUnityVersions(),
projectGetUnityPath(projectPath),
]);
if (unityVersions == null) {
toastError(i18next.t("projects:toast:match version unity not found", {unity: unityVersion}));
return;
@ -50,15 +60,43 @@ export function useOpenUnity(unityVersions: TauriUnityVersions | undefined): Res
toastError(i18next.t("projects:toast:match version unity not found", {unity: unityVersion}));
}
return;
case 1:
toastNormal(i18next.t("projects:toast:opening unity..."));
await projectOpenUnity(projectPath, foundVersions[0][0]);
case 1: {
if (selectedPath) {
if (foundVersions[0][0] != selectedPath) {
// if only unity is not
void projectSetUnityPath(projectPath, null);
}
}
const result = await projectOpenUnity(projectPath, foundVersions[0][0]);
if (result)
toastNormal(i18next.t("projects:toast:opening unity..."));
else
toastError(i18next.t("projects:toast:unity already running"));
}
return;
default:
const selected = await unitySelector.select(foundVersions);
default: {
if (selectedPath) {
const found = foundVersions.find(([p, _v, _i]) => p === selectedPath);
if (found) {
const result = await projectOpenUnity(projectPath, selectedPath);
if (result)
toastNormal(i18next.t("projects:toast:opening unity..."));
else
toastError(i18next.t("projects:toast:unity already running"));
return;
}
}
const selected = await unitySelector.select(foundVersions, true);
if (selected == null) return;
toastNormal(i18next.t("projects:toast:opening unity..."));
await projectOpenUnity(projectPath, selected);
if (selected.keepUsingThisVersion) {
void projectSetUnityPath(projectPath, selected.unityPath);
}
const result = await projectOpenUnity(projectPath, selected.unityPath);
if (result)
toastNormal(i18next.t("projects:toast:opening unity..."));
else
toastError("Unity already running");
}
}
}
@ -92,19 +130,19 @@ function UnityInstallWindow(
await shellOpen(installWithUnityHubLink);
}
return <Dialog open handler={nop}>
<DialogHeader>
return <DialogOpen>
<DialogTitle>
{tc("projects:manage:dialog:unity not found")}
</DialogHeader>
<DialogBody>
<Typography>
</DialogTitle>
<DialogDescription>
<p>
{tc("projects:manage:dialog:unity version of the project not found", {unity: expectedVersion})}
</Typography>
</DialogBody>
</p>
</DialogDescription>
<DialogFooter className={"gap-2"}>
<Button onClick={openUnityHub}>{tc("projects:manage:dialog:open unity hub")}</Button>
<Button onClick={close} className="mr-1">{tc("general:button:close")}</Button>
</DialogFooter>
</Dialog>;
</DialogOpen>;
}

View file

@ -1,7 +1,10 @@
import React, {useState} from "react";
import {Button, Dialog, DialogBody, DialogFooter, DialogHeader, Radio, Typography} from "@material-tailwind/react";
import {nop} from "@/lib/nop";
import React, {useId, useState} from "react";
import {Button} from "@/components/ui/button";
import {DialogDescription, DialogFooter, DialogOpen, DialogTitle} from "@/components/ui/dialog";
import {Label} from "@/components/ui/label";
import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group";
import {tc} from "@/lib/i18n";
import {Checkbox} from "@/components/ui/checkbox";
type UnityInstallation = [path: string, version: string, fromHub: boolean];
@ -10,20 +13,35 @@ type StateUnitySelector = {
} | {
state: "selecting";
unityVersions: UnityInstallation[];
resolve: (unityPath: string | null) => void;
supportKeepUsing: boolean; // if true, show the option to keep using this unity in the future
resolve: (unityInfo: SelectResult | null) => void;
}
type SelectResult = SelectResultWithoutInTheFuture | SelectResultWithInTheFuture
type SelectResultWithoutInTheFuture = {
unityPath: string,
}
type SelectResultWithInTheFuture = {
unityPath: string,
keepUsingThisVersion: boolean,
}
type ResultUnitySelector = {
dialog: React.ReactNode;
select: (unityList: [path: string, version: string, fromHub: boolean][]) => Promise<string | null>;
select(unityList: UnityInstallation[]): Promise<SelectResultWithoutInTheFuture | null>
select(unityList: UnityInstallation[], supportKeepUsing: true): Promise<SelectResultWithInTheFuture | null>
}
export function useUnitySelectorDialog(): ResultUnitySelector {
const [installStatus, setInstallStatus] = React.useState<StateUnitySelector>({state: "normal"});
const select = (unityVersions: UnityInstallation[]) => {
return new Promise<string | null>((resolve) => {
setInstallStatus({state: "selecting", unityVersions, resolve});
function select(unityVersions: UnityInstallation[]): Promise<SelectResultWithoutInTheFuture | null>
function select(unityVersions: UnityInstallation[], supportKeepUsing: boolean): Promise<SelectResultWithInTheFuture | null>
function select(unityVersions: UnityInstallation[], supportKeepUsing?: boolean) {
return new Promise<SelectResult | null>((resolve) => {
setInstallStatus({state: "selecting", unityVersions, resolve, supportKeepUsing: supportKeepUsing ?? false});
});
}
let dialog: React.ReactNode = null;
@ -32,18 +50,23 @@ export function useUnitySelectorDialog(): ResultUnitySelector {
case "normal":
break;
case "selecting":
const resolveWrapper = (unityPath: string | null) => {
const cancel = () => {
setInstallStatus({state: "normal"});
installStatus.resolve(unityPath);
installStatus.resolve(null);
}
const resolveWrapper = (unityPath: string, keepUsingThisVersion: boolean) => {
setInstallStatus({state: "normal"});
installStatus.resolve(installStatus.supportKeepUsing ? {unityPath, keepUsingThisVersion} : {unityPath})
};
dialog = <Dialog open handler={nop} className={"whitespace-normal"}>
<DialogHeader>{tc("projects:manage:dialog:select unity header")}</DialogHeader>
dialog = <DialogOpen className={"whitespace-normal"}>
<DialogTitle>{tc("projects:manage:dialog:select unity header")}</DialogTitle>
<SelectUnityVersionDialog
unityVersions={installStatus.unityVersions}
cancel={() => resolveWrapper(null)}
onSelect={(unityPath) => resolveWrapper(unityPath)}
cancel={cancel}
withKeepUsing={installStatus.supportKeepUsing}
onSelect={resolveWrapper}
/>
</Dialog>;
</DialogOpen>;
break;
default:
const _: never = installStatus;
@ -56,33 +79,52 @@ function SelectUnityVersionDialog(
{
unityVersions,
cancel,
withKeepUsing,
onSelect,
}: {
unityVersions: UnityInstallation[],
cancel: () => void,
onSelect: (unityPath: string) => void,
withKeepUsing: boolean,
onSelect: (unityPath: string, keepUsingThisVersion: boolean) => void,
}) {
const name = useState(() => `select-unity-version-${Math.random().toString(36).slice(2)}-radio`)[0];
const id = useId();
const [selectedUnityPath, setSelectedUnityPath] = useState<string | null>(null);
const [keepUsingThisVersion, setKeepUsingThisVersion] = useState(false);
return (
<>
<DialogBody>
<Typography>
<DialogDescription>
<p>
{tc("projects:manage:dialog:multiple unity found")}
</Typography>
{unityVersions.map(([path, version, _]) =>
<Radio
key={path} name={name} label={`${version} (${path})`}
checked={selectedUnityPath == path}
onChange={() => setSelectedUnityPath(path)}
/>)}
</DialogBody>
</p>
{withKeepUsing && <div>
<label className={"flex cursor-pointer items-center gap-2 p-2 whitespace-normal"}>
<Checkbox checked={keepUsingThisVersion}
onCheckedChange={(e) => setKeepUsingThisVersion(e == true)}
className="hover:before:content-none"/>
{"Keep Using This Version"}
</label>
</div>}
<RadioGroup
onValueChange={(path) => setSelectedUnityPath(path)}
value={selectedUnityPath ?? undefined}
>
{unityVersions.map(([path, version, _]) =>
<div
key={path}
className={"flex items-center space-x-2"}
>
<RadioGroupItem value={path} id={`${id}:${path}`}/>
<Label htmlFor={`${id}:${path}`}>{`${version} (${path})`}</Label>
</div>
)}
</RadioGroup>
</DialogDescription>
<DialogFooter>
<Button onClick={cancel} className="mr-1">{tc("general:button:cancel")}</Button>
<Button
onClick={() => onSelect(selectedUnityPath!)}
onClick={() => onSelect(selectedUnityPath!, keepUsingThisVersion)}
disabled={selectedUnityPath == null}
>{tc("projects:manage:button:continue")}</Button>
</DialogFooter>

6
vrc-get-gui/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View file

@ -34,7 +34,7 @@
"projects:type:custom": "Eigene Vorlagen",
"general:loading...": "Laden...",
"projects:error:load error": "Fehler beim Laden der Projekte: {{msg}}",
"projects:toast:unity exits with non-zero": "Unity wurde mit Fehlercode non-zero geschlossen.",
"projects:toast:unity exits with non-zero": "Unity wurde unerwartet geschlossen.",
"projects:toast:project migrated": "Projekt erfolgreich migriert",
"projects:tooltip:no directory": "Projekt Ordner existiert nicht",
"projects:tooltip:sdk2 migration hint": "SDK2 Projekte können nicht automatisch migriert werden.<br>Bitte migriere das Projekt manuell zu SDK3.",
@ -62,16 +62,16 @@
"general:toast:invalid directory": "Ungültiger Ordner wurde ausgewählt",
"projects:toast:project added": "Projekt erfolgreich hinzugefügt",
"projects:toast:project already exists": "Das Projekt wurde bereits hinzugefügt.",
"projects:hint:create project ready": "Name verfügbar, bereit Projekt zu erstellen",
"projects:hint:invalid project name": "Ungültiger Projektname",
"projects:hint:warn symbol in project name": "Symbole wie diese könnten Probleme verursachen",
"projects:hint:warn multibyte char in project name": "Multibyte Symbole wie z.B. Umlaute könnten Probleme verursachen",
"projects:hint:project already exists": "Es existiert bereits ein Projekt mit diesem Namen",
"projects:hint:create project ready": "Projektname verfügbar!",
"projects:hint:invalid project name": "Ungültiger Projektname!",
"projects:hint:warn symbol in project name": "Symbole wie diese könnten Probleme verursachen.",
"projects:hint:warn multibyte char in project name": "Multibyte Symbole wie z.B. Umlaute könnten Probleme verursachen.",
"projects:hint:project already exists": "Es existiert bereits ein Projekt mit diesem Namen!",
"projects:template": "Vorlage:",
"projects:template:type": "Typ:",
"projects:latest": "Neueste",
"projects:template:unity version": "Unity Version:",
"projects:hint:path of creating project": "Das Projekt wird sich hier befinden: <path>{{path}}</path>",
"projects:hint:path of creating project": "Projektpfad: <path>{{path}}</path>",
"projects:creating project...": "Erstelle Projekt...",
"projects:button:create": "Erstellen",
"projects:toast:project created": "Projekt erfolgreich erstellt",
@ -82,9 +82,15 @@
"projects:manage:toast:no upgradable": "Keine aktualisierbaren Pakete",
"projects:manage:toast:package installed": "Installiert {{name}} Version {{version}}",
"projects:manage:toast:package removed": "Entferne {{name}}",
"projects:manage:toast:all packages upgraded": "Upgrade alle Pakete",
"projects:manage:toast:resolved": "Abhängigkeiten gelöst.",
"projects:manage:toast:all packages reinstalled": "Alle Pakete wurden erfolgreich neu installiert.",
"projects:manage:toast:all packages upgraded": "Alle Pakete wurden aktualisiert.",
"projects:manage:toast:selected packages installed": "Ausgewählte Pakete wurden installiert.",
"projects:manage:toast:selected packages removed": "Ausgewählte Pakete wurden entfernt.",
// V used in single operation
"projects:manage:toast:the package has newer latest with incompatible unity": "Das Paket hat eine neuere Version, welche für die genutzte Unity Version nicht verfügbar ist.",
// V used in bulk operation
"projects:manage:toast:some package has newer latest with incompatible unity": "Einige Pakete haben eine neuere Version, welche für die genutzte Unity Version nicht verfügbar sind.",
"projects:manage:button:unity migrate": "Migriere Projekt",
"projects:toast:unity migrate failed by unity not found": "Fehler beim migrieren: Unity 2022 nicht gefunden",
"projects:toast:unity migration finalize failed by unity not found": "Fehler beim abschluss der Migration: Unity 2022 nicht gefunden",
@ -122,7 +128,8 @@
"projects:manage:button:see changelog": "Changelog anzeigen",
"projects:manage:button:apply changes": "Änderungen anwenden",
"projects:manage:dialog:install package": "Installiere <b>{{name}}</b> Version {{version}}",
"projects:manage:dialog:uninstall package as requested": "Entferne <b>{{name}}</b> wie angefordert",
"projects:manage:dialog:reinstall package": "Installiere <b>{{name}}</b> version {{version}} erneut",
"projects:manage:dialog:uninstall package as requested": "Entferne <b>{{name}}</b>",
"projects:manage:dialog:uninstall package as legacy": "Entferne <b>{{name}}</b>, da es veraltet ist",
"projects:manage:dialog:uninstall package as unused": "Entferne <b>{{name}}</b>, da es nicht verwendet wird",
"projects:manage:dialog:package version conflicts_one": "Es gibt einen Versionskonflikt",
@ -135,12 +142,13 @@
"projects:manage:dialog:files and directories are removed as legacy": "Die folgenden veralteten Daten und Ordner werden entfernt",
"projects:manage:button:apply": "Anwenden",
"vpm repositories:source:local": "Benutzerdefiniert",
"projects:manage:incompatible packages": "Inkompatible",
"projects:manage:incompatible packages": "Nicht unterstützt",
"projects:manage:source not selected": "Inaktive Quelle",
"projects:manage:none": "keine",
"projects:manage:multiple sources": "Mehrere Quellen",
"projects:manage:tooltip:remove packages": "Paket entfernen",
"projects:manage:tooltip:add package": "Paket hinzufügen",
"projects:manage:tooltip:incompatible with unity": "Nicht unterstützt",
"projects:manage:tooltip:upgrade package": "Paket upgraden",
"projects:manage:yanked": "projects:manage:yanked",
"projects:manage:tooltip:back to projects": "Zurück zu Projekte",
@ -190,31 +198,37 @@
"settings:backup:format": "Backup-Format:",
"settings:backup:format:default": "Standard (Unkomprimierte zip)",
"settings:backup:format:zip-store": "Unkomprimierte zip (Schnell)",
"settings:backup:format:zip-fast": "Leicht komprimierte zip (Langsam)",
"settings:backup:format:zip-best": "Stark komprimierte zip (Am langsamsten)",
"settings:backup:format:zip-fast": "Leicht komprimierte zip (Moderat)",
"settings:backup:format:zip-best": "Stark komprimierte zip (Langsam)",
"settings:show prerelease description": "Aktiviere Vorschau-Pakete, um diese in der Paketliste anzuzeigen. Sie werden auch bei der Auflösung von Abhängigkeiten verwendet.",
"settings:show prerelease": "Vorschau-Pakete aktivieren",
"settings:show prerelease": "Vorschau-Pakete anzeigen",
"settings:unity:version": "Version",
"settings:unity:path": "Pfad",
"settings:unity:source:manual": "Benutzerdefiniert",
"settings:unity:source:unity hub": "Unity Hub",
"settings:vcc scheme": "<code>vcc:</code> Linktyp Zuweisung",
"settings:vcc scheme description": "Der <code>vcc:</code> Linktyp wird verwendet, um VCC-Paketlisten direkt vom Browser aus hinzufügen zu können.<br>Als alternative zu VCC kann ALCOM diese Anfragen annehmen.<br>Unter MacOS geschieht dies automatisch. Andere Betriebssysteme müssen dies manuell festlegen.",
"settings:register vcc scheme": "ALCOM zum öffnen von <code>vcc:</code> Linktypen registrieren",
"settings:toast:vcc scheme installed": "ALCOM wurde als Programm für <code>vcc:</code> Linktypen registriert.",
"settings:vcc scheme": "<code>vcc:</code> Protokoll",
"settings:vcc scheme description": "Das <code>vcc:</code> Protokoll wird verwendet, um VCC-Paketlisten direkt vom Browser aus hinzufügen zu können.<br>Als alternative zu VCC kann ALCOM diese Anfragen annehmen.<br>Unter MacOS geschieht das automatisch. Andere Betriebssysteme müssen dies manuell festlegen.",
"settings:register vcc scheme": "<code>vcc:</code> Protokoll registrieren",
"settings:toast:vcc scheme installed": "ALCOM wurde als Programm für <code>vcc:</code> Links registriert.",
"general:toast:not supported": "{{name}} wird noch nicht unterstützt",
"general:not implemented": "Nicht implementiert",
"projects:toast:invalid project unity version": "Wir konnten keine gültige Unity Installation finden",
"projects:toast:match version unity not found": "Unity Version {{unity}} nicht gefunden. Bitte installiere diese oder füge sie in den Einstellungen hinzu.",
"projects:toast:opening unity...": "Öffne Unity...",
"projects:toast:unity already running": "Unity wird bereits ausgeführt.",
"projects:toast:close unity before migration": "Unity muss vor der Migration geschlossen werden.",
"general:dialog:select file or directory header": "Datei oder Ordner auswählen",
"general:dialog:select file or directory": "Bitte Datei oder Ordner auswählen",
"projects:dialog:backup header": "Backup erstellen",
"projects:dialog:creating backup...": "Erstelle Backup...",
"projects:dialog:creating backup...": "Erstelle Backup, bitte warten...",
"projects:toast:backup canceled": "Backup abgebrochen",
"projects:toast:backup succeeded": "Backup erfolgreich erstellt",
"settings:language": "Sprache",
"settings:report issue": "Problembehandlung",
"settings:button:open issue": "Fehler melden",
"settings:theme": "Design",
"settings:theme:system": "System",
"settings:theme:light": "Hell",
"settings:theme:dark": "Dunkel",
},
}

View file

@ -44,6 +44,9 @@
"projects:remove project": "Remove Project",
"projects:dialog:warn removing project": "You're about to remove the project <strong>{{name}}</strong>. Are you sure?",
"general:button:cancel": "Cancel",
"general:button:save": "Save",
"general:button:add": "Add",
"general:button:reset": "Reset",
"projects:button:remove from list": "Remove from the List",
"projects:button:remove directory": "Remove the Directory",
"projects:dialog:removing...": "Removing the project...",
@ -58,7 +61,14 @@
"projects:button:open unity": "Open Unity",
"projects:backup": "Backup",
"projects:menuitem:backup": "Make Backup",
"projects:menuitem:change launch options": "Change Launch Options",
"projects:menuitem:open directory": "Open Project Directory",
"projects:menuitem:forget unity path": "Forget Unity for This Project",
"projects:dialog:launch options": "Launch Options",
"projects:dialog:command-line arguments": "Command-line Arguments",
"projects:dialog:use default command line arguments": "Use Default Command-line Arguments",
"projects:dialog:customize command line arguments": "Customize Command-line Arguments",
"projects:hint:some arguments are empty": "Some arguments are empty.",
"general:toast:invalid directory": "Invalid directory was selected.",
"projects:toast:project added": "Project was addded successfully.",
"projects:toast:project already exists": "The project was already added.",
@ -82,9 +92,15 @@
"projects:manage:toast:no upgradable": "No upgradable package",
"projects:manage:toast:package installed": "{{name}} version {{version}} was installed successfully.",
"projects:manage:toast:package removed": "{{name}} was removed successfully.",
"projects:manage:toast:resolved": "Resolved dependencies.",
"projects:manage:toast:all packages reinstalled": "All packages were reinstalled successfully.",
"projects:manage:toast:all packages upgraded": "All packages were upgraded successfully.",
"projects:manage:toast:selected packages installed": "Selected packages were installed successfully.",
"projects:manage:toast:selected packages removed": "Selected packages were removed successfully.",
// V used in single operation
"projects:manage:toast:the package has newer latest with incompatible unity": "The package has a newer version that is incompatible with the Unity version.",
// V used in bulk operation
"projects:manage:toast:some package has newer latest with incompatible unity": "Some packages have newer versions that are incompatible with the Unity version.",
"projects:manage:button:unity migrate": "Migrate Project",
"projects:toast:unity migrate failed by unity not found": "Failed to migrate project: Unity 2022 not found",
"projects:toast:unity migration finalize failed by unity not found": "Failed to finalize the migration: Unity 2022 not found",
@ -122,6 +138,7 @@
"projects:manage:button:see changelog": "See Changelog",
"projects:manage:button:apply changes": "Apply Changes",
"projects:manage:dialog:install package": "Install <b>{{name}}</b> version {{version}}",
"projects:manage:dialog:reinstall package": "Reinstall <b>{{name}}</b> version {{version}}",
"projects:manage:dialog:uninstall package as requested": "Remove <b>{{name}}</b> as you requested",
"projects:manage:dialog:uninstall package as legacy": "Remove <b>{{name}}</b> since it's a legacy package",
"projects:manage:dialog:uninstall package as unused": "Remove <b>{{name}}</b> which is unused",
@ -141,8 +158,9 @@
"projects:manage:multiple sources": "Multiple sources",
"projects:manage:tooltip:remove packages": "Remove Package",
"projects:manage:tooltip:add package": "Add Package",
"projects:manage:tooltip:incompatible with unity": "Incompatible with Unity",
"projects:manage:tooltip:upgrade package": "Upgrade Package",
"projects:manage:yanked": "projects:manage:yanked",
"projects:manage:yanked": "(Yanked)",
"projects:manage:tooltip:back to projects": "Back to projects",
"vpm repositories:toast:invalid url": "The URL is invalid.",
"vpm repositories:toast:load failed": "Failed to download the repository: {{message}}",
@ -207,6 +225,8 @@
"projects:toast:invalid project unity version": "We couldn't detect suitable Unity installations.",
"projects:toast:match version unity not found": "Unity {{unity}} is not found. Please install Unity in your computer or add a Unity version in the ALCOM settings.",
"projects:toast:opening unity...": "Opening Unity...",
"projects:toast:unity already running": "Unity is already running.",
"projects:toast:close unity before migration": "Unity must be closed before migration.",
"general:dialog:select file or directory header": "Selecting file or directory.",
"general:dialog:select file or directory": "Please select a file or directory.",
"projects:dialog:backup header": "Backup Project",
@ -216,5 +236,9 @@
"settings:language": "Language",
"settings:report issue": "Report an Issue",
"settings:button:open issue": "Open an Issue",
"settings:theme": "Theme",
"settings:theme:system": "System",
"settings:theme:light": "Light",
"settings:theme:dark": "Dark",
},
}

View file

@ -1,207 +1,230 @@
{
"translation": {
"settings:langName": "Français",
"projects": "Projets",
"settings": "Réglages",
"vpm repositories": "Dépots VPM",
"logs": "Logs",
"sidebar:toast:version copied": "Nom de version copié.",
"search:placeholder": "Recherche...",
"projects:tooltip:refresh": "Rafraichir les projets",
"projects:create new project": "Créer un nouveau projet",
"projects:add existing project": "Ajouter un nouveau projet",
"general:name": "Nom",
"general:button:close": "Fermer",
"general:not implemented": "Pas encore implémenté",
"projects:type": "Type",
"projects:unity": "Unity",
"projects:last modified": "Dernière modification",
"projects:last modified:moments": "Il y a quelques instants",
"projects:last modified:minutes_one": "Il y a {{count}} minute",
"projects:last modified:minutes_other": "Il y a {{count}} minutes",
"projects:last modified:hours_one": "Il y a {{count}} heure",
"projects:last modified:hours_other": "Il y a {{count}} heures",
"projects:last modified:days_one": "Il y a {{count}} jour",
"projects:last modified:days_other": "Il y a {{count}} jours",
"projects:last modified:weeks_one": "Il y a {{count}} semaine",
"projects:last modified:weeks_other": "Il y a {{count}} weeks ago",
"projects:last modified:months_one": "Il y a {{count}} month ago",
"projects:last modified:months_other": "Il y a {{count}} months ago",
"projects:last modified:years_one": "Il y a {{count}} year ago",
"projects:last modified:years_other": "Il y a {{count}} years ago",
"projects:type:unknown": "Iconnu",
"projects:type:sdk2": "SDK2",
"projects:type:worlds": "Monde",
"projects:type:avatars": "Avatar",
"general:loading...": "Chargement...",
"projects:error:load error": "Erreur lors du chargement des projets: {{msg}}",
"projects:toast:project migrated": "Le projet a été migré avec succès.",
"projects:tooltip:no directory": "Le dossier projet n'existe pas.",
"projects:tooltip:sdk2 migration hint": "Le projet déprécié en SDK2 n'a pas pu être migré automatiquement.<br>Migrez en SDK3 dans un premier temps.",
"projects:button:migrate": "Migrer",
"projects:tooltip:git-vcc not supported": "Les projets git-VCC ne sont pas supportés.",
"projects:button:manage": "Configurer",
"projects:remove project": "Supprimer le projet",
"projects:dialog:warn removing project": "Vous vous apprêtez a supprimer le projet <strong>{{name}}</strong>. Êtes vous sur ?",
"general:button:cancel": "Annuler",
"projects:button:remove from list": "Supprimer de la liste",
"projects:button:remove directory": "Supprimer du dossier",
"projects:dialog:removing...": "Suppression du projet...",
"projects:toast:project removed": "Projet supprimmé avec succès.",
"projects:dialog:vpm migrate header": "Migrer le projet déprécié",
"projects:dialog:vpm migrate description": "La migration de projet est une fonctionalité expérimentale dans vrc-get.<br>Pensez a faire un backup de votre projet avant la migration.",
"general:button:close": "Fermer",
"general:button:ok": "OK",
"general:button:select": "Sélectionner",
"general:dialog:select file or directory header": "Sélection d'un fichier ou d'un dossier",
"general:dialog:select file or directory": "Sélectionnez un fichier ou un dossier.",
"general:loading...": "Chargement...",
"general:name": "Nom",
"general:not implemented": "Pas encore implémenté",
"general:source": "Source",
"general:toast:invalid directory": "Le dossier projet sélectionné est invalide.",
"general:toast:not supported": "{{name}} n'est pas encore supporté.",
"logs": "Journal",
"projects": "Projets",
"projects:add existing project": "Ajouter un nouveau projet",
"projects:backup": "Backup",
"projects:button:create": "Créer",
"projects:button:manage": "Configurer",
"projects:button:migrate copy": "Migrer une copie",
"projects:button:migrate in-place": "Migrer le projet même",
"projects:pre-migrate copying...": "Copie du projet en cours pour la migration...",
"projects:migrating...": "Migration du projet...",
"projects:type:legacy": "Déprécié",
"projects:button:migrate": "Migrer",
"projects:button:open unity": "Ouvrir Unity",
"projects:backup": "Backup",
"projects:menuitem:backup": "Faire un backup",
"projects:menuitem:open directory": "Ouvrir le dossier projet",
"general:toast:invalid directory": "Le dossier projet sélectionné est invalide.",
"projects:toast:project added": "Projet ajouté avec succès.",
"projects:toast:project already exists": "Le projet a déja été ajouté.",
"projects:button:remove directory": "Supprimer du dossier",
"projects:button:remove from list": "Supprimer de la liste",
"projects:create new project": "Créer un nouveau projet",
"projects:creating project...": "Création du projet...",
"projects:dialog:backup header": "Backup de projet",
"projects:dialog:creating backup...": "Création d'un backup...",
"projects:dialog:migrate unity2022 patch description": "Votre projet sera migré vers Unity {{unity}}.",
"projects:dialog:removing...": "Suppression du projet...",
"projects:dialog:vpm migrate description": "La migration de projet est une fonctionalité expérimentale dans vrc-get.<br>Pensez a faire un backup de votre projet avant la migration.",
"projects:dialog:vpm migrate header": "Migrer le projet déprécié",
"projects:dialog:warn removing project": "Vous vous apprêtez a supprimer le projet <strong>{{name}}</strong>. Êtes vous sur ?",
"projects:error:load error": "Erreur lors du chargement des projets: {{msg}}",
"projects:hint:create project ready": "Prêt a créer un projet !",
"projects:hint:invalid project name": "Nom de projet invalide.",
"projects:hint:warn symbol in project name": "L'utilisation de certains symboles peut causer des problèmes.",
"projects:hint:warn multibyte char in project name": "L'utilisation de certains caracthères multibytes peu causer des problèmes.",
"projects:hint:project already exists": "Le dossier existe déja.",
"projects:template": "Modèle:",
"projects:hint:path of creating project": "Le nouveau projet sera dans le dossier <path>{{path}}</path>",
"projects:creating project...": "Création du projet...",
"projects:button:create": "Créer",
"projects:toast:project created": "Projet créer avec succès.",
"projects:manage:package": "Package",
"projects:hint:project already exists": "Le dossier existe déja.",
"projects:hint:warn multibyte char in project name": "L'utilisation de certains caracthères multibytes peu causer des problèmes.",
"projects:hint:warn symbol in project name": "L'utilisation de certains symboles peut causer des problèmes.",
"projects:last modified": "Dernière modification",
"projects:last modified:days_one": "Il y a {{count}} jour",
"projects:last modified:days_other": "Il y a {{count}} jours",
"projects:last modified:hours_one": "Il y a {{count}} heure",
"projects:last modified:hours_other": "Il y a {{count}} heures",
"projects:last modified:minutes_one": "Il y a {{count}} minute",
"projects:last modified:minutes_other": "Il y a {{count}} minutes",
"projects:last modified:moments": "Il y a quelques instants",
"projects:last modified:months_one": "Il y a {{count}} month ago",
"projects:last modified:months_other": "Il y a {{count}} months ago",
"projects:last modified:weeks_one": "Il y a {{count}} semaine",
"projects:last modified:weeks_other": "Il y a {{count}} weeks ago",
"projects:last modified:years_one": "Il y a {{count}} year ago",
"projects:last modified:years_other": "Il y a {{count}} years ago",
"projects:latest": "Dernière version",
"projects:manage:button:apply changes": "Appliquer les changements",
"projects:manage:button:apply": "Appliquer",
"projects:manage:button:clear selection": "Nettoyer la sélection",
"projects:manage:button:continue": "Continuer",
"projects:manage:button:install selected": "Installer l'option sélectionnée",
"projects:manage:button:reinstall all": "Tout réinstaller",
"projects:manage:button:resolve": "Installer les packets",
"projects:manage:button:see changelog": "Voir la liste des changements",
"projects:manage:button:select repositories": "Sélectionner un dépot",
"projects:manage:button:uninstall selected": "Supression de l'option sélectionnée",
"projects:manage:button:unity migrate": "Migrer le projet",
"projects:manage:button:upgrade all": "Tout mettre a jour",
"projects:manage:button:upgrade selected": "Mettre a jour l'option sélectionnée",
"projects:manage:dialog:confirm changes description": "Vous appliquer les changements a la section en cours.",
"projects:manage:dialog:conflicts with": "Le package <b>{{pkg}}</b> est en conflit avec le package <b>{{other}}</b>.",
"projects:manage:dialog:do not close": "Ne fermez pas cette fenêttre.",
"projects:manage:dialog:exact version unity not found for patch migration description": "Nous n'avons pas trouvé la version recommandée pour Unity 2022 dédié au patch de migration.<br>Installez Unity {{unity}} avec le Unity Hub et redémarrez ALCOM.",
"projects:manage:dialog:files and directories are removed as legacy": "Ces fichiers et dossiers dépréciés seront supprimés.",
"projects:manage:dialog:install package": "Installez le package <b>{{name}}</b> dans la version {{version}}",
"projects:manage:dialog:multiple unity found": "Plusieurs installations identiques de cette version de Unity ont été trouvés.",
"projects:manage:dialog:open unity hub": "Ouvrir Unity Hub",
"projects:manage:dialog:package not supported your unity": "Le package <b>{{pkg}}</b> n'est pas supporté sur votre version de Unity.",
"projects:manage:dialog:package version conflicts_one": "Il y a un conflit de version.",
"projects:manage:dialog:package version conflicts_other": "Il y a des conflits de version.",
"projects:manage:dialog:reinstall package": "Réinstaller <b>{{name}}</b> en version {{version}}",
"projects:manage:dialog:select unity header": "Sélectionner Unity",
"projects:manage:dialog:uninstall package as legacy": "Supprimez le package <b>{{name}}</b> comme il s'agit d'un package déprécié",
"projects:manage:dialog:uninstall package as requested": "Supprimez <b>{{name}}</b> as you requested",
"projects:manage:dialog:uninstall package as unused": "Supprimez le package <b>{{name}}</b> comme il n'est pas utilisé",
"projects:manage:dialog:unity migrate finalizing...": "Lancement de Unity 2022 en arrière plan pour finaliser la migration...",
"projects:manage:dialog:unity migrate header": "Migration Unity",
"projects:manage:dialog:unity not found": "Unity introuvable",
"projects:manage:dialog:unity version conflicts_one": "Il y a un conflit avec la version de Unity.",
"projects:manage:dialog:unity version conflicts_other": "Il y a des conflits avec la version de Unity.",
"projects:manage:dialog:unity version of the project not found": "Ce projet utilise Unity {{unity}} mais il est introuvable.<br>Installez Unity {{unity}} avec le Unity Hub et redémarrez ALCOM.",
"projects:manage:incompatible packets": "Incompatibles",
"projects:manage:installed": "Installé",
"projects:manage:latest": "Dernière version",
"general:source": "Source",
"projects:manage:manage packets": "Gestion des packets",
"projects:manage:multiple sources": "Sources multiples",
"projects:manage:none": "Aucun",
"projects:manage:package": "Package",
"projects:manage:project location": "Emplacement: <path>{{path}}</path>",
"projects:manage:source not selected": "Non sélectionné",
"projects:manage:suggest resolve": "Des packets obligatoires pour ce projet ne sont pas installés.<br>Il est fortement recommandé d'installer ces packets.",
"projects:manage:suggest unity migration": "Votre projet utilise Unity 2019 qui n'est plus supporté par le SDK de VRChat. Il est recommandé de migrer votre projet vers Unity 2022.",
"projects:manage:suggest unity patch migration": "Votre projet utilise une vieille version de Unity 2022. VRChat vous recommande de migrer votre projet vers la version la plus récente de Unity 2022.",
"projects:manage:toast:all packets reinstalled": "Tous les packets ont été réinstallés avec succès.",
"projects:manage:toast:all packets upgraded": "Tous les packets ont été mis a jour avec succès.",
"projects:manage:toast:no upgradable": "Aucune mise a jour de package disponible.",
"projects:manage:toast:package installed": "Le package {{name}} en version {{version}} a été installé avec succès.",
"projects:manage:toast:package removed": "Package {{name}} supprimé avec succès.",
"projects:manage:toast:all packages upgraded": "Tout les packages ont été mis a jour avec succès.",
"projects:manage:toast:selected packages installed": "Les packages sélectionnés ont été installés avec succès.",
"projects:manage:toast:selected packages removed": "Les packages sélectionnés ont été supprimés avec succès.",
"projects:manage:button:unity migrate": "Migrer le projet",
"projects:toast:unity migrate failed by unity not found": "Echec de migration du projet: Unity 2022 non trouvé",
"projects:toast:unity migration finalize failed by unity not found": "Echec de l'initialization de la migration: Unity 2022 non trouvé",
"projects:toast:unity migrated": "Le projet a été migré en Unity 2022",
"projects:manage:project location": "Emplacement: <path>{{path}}</path>",
"projects:manage:unity version": "Version de Unity: ",
"projects:manage:manage packages": "Gestion des packages",
"projects:manage:tooltip:refresh packages": "Rafraichir les packages",
"projects:manage:button:upgrade all": "Tout mettre a jour",
"projects:manage:button:reinstall all": "Tout réinstaller",
"projects:manage:button:select repositories": "Sélectionner un dépot",
"vpm repositories:source:official": "Officiel",
"vpm repositories:source:curated": "Epurée",
"projects:manage:suggest unity migration": "Votre projet utilise Unity 2019 qui n'est plus supporté par le SDK de VRChat. Il est recommandé de migrer votre projet vers Unity 2022.",
"projects:manage:dialog:unity migrate header": "Migration Unity",
"projects:manage:button:continue": "Continuer",
"projects:manage:dialog:do not close": "Ne fermez pas cette fenêttre.",
"projects:manage:dialog:unity migrate finalizing...": "Lancement de Unity 2022 en arrière plan pour finaliser la migration...",
"projects:manage:button:install selected": "Installer l'option sélectionnée",
"projects:manage:button:upgrade selected": "Mettre a jour l'option sélectionnée",
"projects:manage:button:uninstall selected": "Supression de l'option sélectionnée",
"projects:manage:button:clear selection": "Nettoyer la sélection",
"projects:manage:dialog:confirm changes description": "Vous appliquer les changements a la section en cours.",
"projects:manage:button:see changelog": "Voir la liste des changements",
"projects:manage:button:apply changes": "Appliquer les changements",
"projects:manage:dialog:install package": "Installez le package <b>{{name}}</b> dans la version {{version}}",
"projects:manage:dialog:uninstall package as requested": "Supprimez <b>{{name}}</b> as you requested",
"projects:manage:dialog:uninstall package as legacy": "Supprimez le package <b>{{name}}</b> comme il s'agit d'un package déprécié",
"projects:manage:dialog:uninstall package as unused": "Supprimez le package <b>{{name}}</b> comme il n'est pas utilisé",
"projects:manage:dialog:package version conflicts_one": "Il y a un conflit de version.",
"projects:manage:dialog:package version conflicts_other": "Il y a des conflits de version.",
"projects:manage:dialog:conflicts with": "Le package <b>{{pkg}}</b> est en conflit avec le package <b>{{other}}</b>.",
"projects:manage:dialog:unity version conflicts_one": "Il y a un conflit avec la version de Unity.",
"projects:manage:dialog:unity version conflicts_other": "Il y a des conflits avec la version de Unity.",
"projects:manage:dialog:package not supported your unity": "Le package <b>{{pkg}}</b> n'est pas supporté sur votre version de Unity.",
"projects:manage:dialog:files and directories are removed as legacy": "Ces fichiers et dossiers dépréciés seront supprimés.",
"projects:manage:button:apply": "Appliquer",
"vpm repositories:source:local": "Utilisateur Local",
"projects:manage:incompatible packages": "Incompatibles",
"projects:manage:source not selected": "Non sélectionné",
"projects:manage:none": "Aucun",
"projects:manage:multiple sources": "Sources multiples",
"projects:manage:tooltip:remove packages": "Supprimer un package",
"projects:manage:toast:resolved": "Dépendances résolues.",
"projects:manage:toast:selected packets installed": "Les packets sélectionnés ont été installés avec succès.",
"projects:manage:toast:selected packets removed": "Les packets sélectionnés ont été supprimés avec succès.",
"projects:manage:toast:some package has newer latest with incompatible unity": "Certains packets ont une version plus récente disponible qui n'est pas compatible avec la version de Unity.",
"projects:manage:toast:the package has newer latest with incompatible unity": "Le packet a une version plus récente disponible qui n'est pas compatible avec la version de Unity.",
"projects:manage:tooltip:add package": "Ajouter un package",
"projects:manage:tooltip:upgrade package": "Mettre a jour un package",
"projects:manage:yanked": "projects:manage:yanked",
"projects:manage:tooltip:back to projects": "Retour aux projets",
"vpm repositories:toast:invalid url": "L'URL est invalide",
"vpm repositories:toast:load failed": "Echech de téléchargement du dépot: {{message}}",
"vpm repositories:toast:repository added": "Le dépot a été ajouté avec succès.",
"vpm repositories:community repositories": "Dépots communautaires",
"vpm repositories:button:add repository": "Ajouter un dépot",
"vpm repositories:url": "URL",
"vpm repositories:remove repository": "Supprimer un dépot",
"vpm repositories:dialog:confirm remove description": "Voullez vous supprimer le dépot <b>{{name}}</b> ?",
"vpm repositories:dialog:enter repository info": "Entez les informations du dépot",
"vpm repositories:dialog:headers": "Headers",
"vpm repositories:dialog:header name": "Nom du header",
"vpm repositories:dialog:header value": "Valeur du header",
"vpm repositories:tooltip:add header": "Ajouter un header",
"vpm repositories:tooltip:remove header": "Supprimer un header",
"vpm repositories:hint:invalid header names": "Le nom du header est invalide.",
"vpm repositories:hint:invalid header values": "La valeur du header est invalide.",
"vpm repositories:hint:duplicate headers": "Le nom du header dupliqué.",
"vpm repositories:dialog:headers": "Headers:",
"vpm repositories:dialog:downloading...": "Téléchargement du dépot...",
"vpm repositories:dialog:already added": "Ce dépot a déja été ajouté.",
"general:button:ok": "OK",
"vpm repositories:dialog:name": "Nom: <b>{{name}}</b>",
"vpm repositories:dialog:url": "URL: {{url}}",
"vpm repositories:dialog:packages": "Packages:",
"settings:error:load error": "Erreur de chargement des réglages.",
"settings:check update": "Vérifier les mises a jour",
"settings:licenses": "Licenses",
"settings:licenses description": "Cliquez <l>ici</l> pour voir les licenses du projet utilisés par ALCOM",
"settings:toast:not unity hub": "Le fichier sélectionné n'est pas Unity Hub.",
"settings:toast:unity hub path updated": "Unity Hub mis a jour avec success !",
"settings:toast:not unity": "Le fichier sélectionné n'est pas Unity",
"settings:toast:unity already added": "Le Unity sélectionné a déja été ajouté",
"settings:toast:unity added": "Unity ajouté avec succès !",
"settings:toast:default project path updated": "Le dossier projet par défaut a été changé avec succès !",
"settings:toast:backup path updated": "Le dossier de backup de projet a été changé avec succès.",
"settings:unity hub": "Unity Hub",
"general:button:select": "Sélectionner",
"settings:unity installations": "Versions installées de Unity",
"settings:button:add unity": "Ajouter Unity",
"settings:default project path": "Dossier projet par défault",
"settings:default project path description": "Le dossier projet par défaut est le dossier ou seront créés tous vos nouveaux projets.",
"settings:backup:path": "Dossier de backup",
"settings:backup:path description": "Le dossier de backup est le dossier ou ALCOM ira créer vos backup de projets au format zip.",
"settings:backup:format": "Format de l'archve de backup:",
"settings:backup:format:default": "Défaut (Zip non compressé)",
"settings:backup:format:zip-store": "Zip non compressé (Fast)",
"settings:backup:format:zip-fast": "Compression Zip basse (Lent)",
"settings:backup:format:zip-best": "Haute compression Zip (Le plus lent)",
"settings:show prerelease description": "Activer les packages expérimentaux vous affichera tous les packages en cours de stabilisation en provenance de la liste de packages. De plus, ces packages expérimentaux seront disponibles pour la résolution de dépendances.",
"settings:show prerelease": "Afficher les packages expérimentaux",
"settings:unity:version": "Version de Unity",
"settings:unity:path": "Dossier de Unity",
"settings:unity:source:manual": "Manuel",
"settings:language": "Language",
"settings:report issue": "Rapporter un problème",
"settings:button:open issue": "Ouvrir une réclamation",
"general:toast:not supported": "{{name}} n'est pas encore supporté.",
"projects:manage:tooltip:incompatible with unity": "Incompatible avec Unity",
"projects:manage:tooltip:refresh packets": "Rafraichir les packets",
"projects:manage:tooltip:remove packets": "Supprimer un package",
"projects:manage:tooltip:upgrade package": "Mettre a jour un package",
"projects:manage:unity version": "Version de Unity: ",
"projects:manage:yanked": "projects:manage:yanked",
"projects:menuitem:backup": "Faire un backup",
"projects:menuitem:open directory": "Ouvrir le dossier projet",
"projects:migrating...": "Migration du projet...",
"projects:pre-migrate copying...": "Copie du projet en cours pour la migration...",
"projects:remove project": "Supprimer le projet",
"projects:template": "Modèle:",
"projects:template:type": "Type:",
"projects:template:unity version": "Version de Unity:",
"projects:toast:backup canceled": "Le backup a été annulé",
"projects:toast:backup succeeded": "Le backup a été crée avec succès.",
"projects:toast:close unity before migration": "Unity dois être fermé avant la migration.",
"projects:toast:invalid project unity version": "Nous n'avons pu détecter aucune version de Unity utilisable.",
"projects:toast:match version unity not found": "Aucune version compatible de Unity trouvé. Installez Unity dans un premier temps ou ajouter une version de Unity manuellement dans les réglages de ALCOM",
"projects:toast:opening unity...": "Ouverture de Unity...",
"general:dialog:select file or directory header": "Sélection d'un fichier ou d'un dossier",
"general:dialog:select file or directory": "Sélectionnez un fichier ou un dossier.",
"projects:dialog:backup header": "Backup de projet",
"projects:dialog:creating backup...": "Création d'un backup...",
"projects:toast:backup canceled": "Le backup a été annulé",
"projects:toast:backup succeeded": "Le backup a été crée avec succès.",
"projects:toast:project added": "Projet ajouté avec succès.",
"projects:toast:project already exists": "Le projet a déja été ajouté.",
"projects:toast:project created": "Projet créer avec succès.",
"projects:toast:project migrated": "Le projet a été migré avec succès.",
"projects:toast:project removed": "Projet supprimmé avec succès.",
"projects:toast:unity already running": "Unity est déja en cours d'exécution.",
"projects:toast:unity exits with non-zero": "Unity s'est fermé avec un code d'erreur différent de zéro.",
"projects:manage:suggest unity patch migration": "Votre projet utilise une vieille version de Unity 2022. VRChat vous recommande de migrer votre projet vers la version la plus récente de Unity 2022.",
"projects:manage:dialog:multiple unity found": "Plusieurs installations identiques de cette version de Unity ont été trouvés.",
"projects:manage:dialog:multiple unity found": "Multiple Unity installations of the version were found.",
"projects:manage:dialog:select unity header": "Sélectionner Unity",
"projects:dialog:migrate unity2022 patch description": "Votre projet sera migré vers Unity {{unity}}.",
"projects:manage:dialog:exact version unity not found for patch migration description": "Nous n'avons pas trouvé la version recommandée pour Unity 2022 dédié au patch de migration.<br>Installez Unity {{unity}} avec le Unity Hub et redémarrez ALCOM.",
"projects:manage:dialog:open unity hub": "Ouvrir Unity Hub",
},
"projects:toast:unity migrate failed by unity not found": "Echec de migration du projet: Unity 2022 non trouvé",
"projects:toast:unity migrated": "Le projet a été migré en Unity 2022",
"projects:toast:unity migration finalize failed by unity not found": "Echec de l'initialization de la migration: Unity 2022 non trouvé",
"projects:tooltip:git-vcc not supported": "Les projets git-VCC ne sont pas supportés.",
"projects:tooltip:no directory": "Le dossier projet n'existe pas.",
"projects:tooltip:refresh": "Rafraichir les projets",
"projects:tooltip:sdk2 migration hint": "Le projet déprécié en SDK2 n'a pas pu être migré automatiquement.<br>Migrez en SDK3 dans un premier temps.",
"projects:type": "Type",
"projects:type:avatars": "Avatar",
"projects:type:custom": "Modèle Custom",
"projects:type:legacy": "Déprécié",
"projects:type:sdk2": "SDK2",
"projects:type:unknown": "Iconnu",
"projects:type:worlds": "Monde",
"projects:unity": "Unity",
"search:placeholder": "Recherche...",
"settings": "Réglages",
"settings:backup:format": "Format de l'archive de backup:",
"settings:backup:format:default": "Défaut (Zip non compressé)",
"settings:backup:format:zip-best": "Zip Haute compression (Le plus lent)",
"settings:backup:format:zip-fast": "Zip Basse Compression (Lent)",
"settings:backup:format:zip-store": "Zip non compressé (Rapide)",
"settings:backup:path description": "Le dossier de backup est le dossier où ALCOM ira créer vos backup de projets au format zip.",
"settings:backup:path": "Dossier de backup",
"settings:button:add unity": "Ajouter Unity",
"settings:button:open issue": "Ouvrir une réclamation",
"settings:check update": "Vérifier les mises a jour",
"settings:default project path description": "Le dossier projet par défaut est le dossier où seront créés tous vos nouveaux projets.",
"settings:default project path": "Dossier projet par défault",
"settings:error:load error": "Erreur de chargement des réglages.",
"settings:langName": "Français",
"settings:language": "Language",
"settings:licenses description": "Cliquez <l>ici</l> pour voir les licenses du projet utilisés par ALCOM",
"settings:licenses": "Licenses",
"settings:register vcc scheme": "Enregistrer ALCOM en temps que gestion de schéma d'URL <code>vcc:</code>",
"settings:report issue": "Rapporter un problème",
"settings:show prerelease description": "Activer les packets expérimentaux vous affichera tous les packets en cours de stabilisation en provenance de la liste de packets. De plus, ces packets expérimentaux seront disponibles pour la résolution de dépendances.",
"settings:show prerelease": "Afficher les packets expérimentaux",
"settings:theme": "Thême",
"settings:theme:dark": "Sombre",
"settings:theme:light": "Lumineux",
"settings:theme:system": "Système",
"settings:toast:backup path updated": "Le dossier de backup de projet a été changé avec succès.",
"settings:toast:default project path updated": "Le dossier projet par défaut a été changé avec succès !",
"settings:toast:not unity hub": "Le fichier sélectionné n'est pas Unity Hub.",
"settings:toast:not unity": "Le fichier sélectionné n'est pas Unity",
"settings:toast:unity added": "Unity ajouté avec succès !",
"settings:toast:unity already added": "Le Unity sélectionné a déja été ajouté",
"settings:toast:unity hub path updated": "Unity Hub mis a jour avec success !",
"settings:toast:vcc scheme installed": "ALCOM a été enregistré en temps que gestion de schéma d'URL <code>vcc:</code>.",
"settings:unity hub path": "Dossier Unity Hub",
"settings:unity installations": "Versions installées de Unity",
"settings:unity:path": "Dossier de Unity",
"settings:unity:source:manual": "Manuelle",
"settings:unity:source:unity hub": "Unity Hub",
"settings:unity:version": "Version de Unity",
"settings:vcc scheme description": "Le schéma d'URL <code>vcc:</code> est utilisé afin d'ouvrir VCC pour ajouter un dépot depuis un navigateur web.<br>En tant qu'alternative à VCC, Vous pouvez utiliser ALCOM en temps qu'application pour ajouter ces dépots depuis un navigateur web.<br>Il est automatiquement enregistré pour macOS, mais pour les autres plateformes, vous devez l'enregistrer manuellement.",
"settings:vcc scheme": "<code>vcc:</code> Gestion du schéma d'URL",
"sidebar:toast:version copied": "Nom de version copié.",
"vpm repositories": "Dépots VPM",
"vpm repositories:button:add repository": "Ajouter un dépot",
"vpm repositories:community repositories": "Dépots communautaires",
"vpm repositories:dialog:already added": "Ce dépot a déja été ajouté.",
"vpm repositories:dialog:confirm remove description": "Voullez vous supprimer le dépot <b>{{name}}</b> ?",
"vpm repositories:dialog:downloading...": "Téléchargement du dépot...",
"vpm repositories:dialog:enter repository info": "Entez les informations du dépot",
"vpm repositories:dialog:header name": "Nom du header",
"vpm repositories:dialog:header value": "Valeur du header",
"vpm repositories:dialog:headers": "Headers",
"vpm repositories:dialog:name": "Nom: <b>{{name}}</b>",
"vpm repositories:dialog:packets": "Packets:",
"vpm repositories:dialog:url": "URL: {{url}}",
"vpm repositories:hint:duplicate headers": "Le nom du header dupliqué.",
"vpm repositories:hint:invalid header names": "Le nom du header est invalide.",
"vpm repositories:hint:invalid header values": "La valeur du header est invalide.",
"vpm repositories:remove repository": "Supprimer un dépot",
"vpm repositories:source:curated": "Epurée",
"vpm repositories:source:local": "Utilisateur Local",
"vpm repositories:source:official": "Officiel",
"vpm repositories:toast:invalid url": "L'URL est invalide",
"vpm repositories:toast:load failed": "Echech de téléchargement du dépot: {{message}}",
"vpm repositories:toast:repository added": "Le dépot a été ajouté avec succès.",
"vpm repositories:tooltip:add header": "Ajouter un header",
"vpm repositories:tooltip:remove header": "Supprimer un header",
"vpm repositories:url": "URL"
}
}

View file

@ -30,7 +30,7 @@
"projects:error:load error": "プロジェクトリストの読込中にエラーが発生しました: {{msg}}",
"projects:toast:unity exits with non-zero": "Unityが0以外の終了コードで終了しました。",
"projects:toast:project migrated": "プロジェクト移行に成功しました。",
"projects:tooltip:no directory": "プロジェクトが見つかりませんでした。",
"projects:tooltip:no directory": "プロジェクトが見つかりません。",
"projects:tooltip:sdk2 migration hint": "SDK2のプロジェクトを自動で移行することは出来ません。<br>先に手動でSDK3へ移行してください。",
"projects:button:migrate": "移行",
"projects:tooltip:git-vcc not supported": "git-VCCのプロジェクトはサポートされていません。",
@ -76,9 +76,15 @@
"projects:manage:toast:no upgradable": "更新可能なパッケージはありません。",
"projects:manage:toast:package installed": "{{name}} バージョン {{version}} をインストールしました。",
"projects:manage:toast:package removed": "{{name}} をアンインストールしました。",
"projects:manage:toast:resolved": "不足しているパッケージをインストールしました。",
"projects:manage:toast:all packages reinstalled": "すべてのパッケージを入れ直しました。",
"projects:manage:toast:all packages upgraded": "すべてのパッケージを更新しました。",
"projects:manage:toast:selected packages installed": "選択されたパッケージをインストールしました。",
"projects:manage:toast:selected packages removed": "選択されたパッケージをアンイストールしました。",
// V used in single operation
"projects:manage:toast:the package has newer latest with incompatible unity": "このパッケージには、使用中のUnityバージョンと互換性のない新しいバージョンが公開されています。",
// V used in bulk operation
"projects:manage:toast:some package has newer latest with incompatible unity": "使用中のUnityバージョンと互換性のない、新しいバージョンが公開されているパッケージがあります。",
"projects:manage:button:unity migrate": "プロジェクトを移行",
"projects:toast:unity migrate failed by unity not found": "プロジェクト移行に失敗しました: Unity 2022がありません",
"projects:toast:unity migration finalize failed by unity not found": "プロジェクト移行の完了処理を行えませんでした: Unity 2022がありません",
@ -116,6 +122,7 @@
"projects:manage:button:see changelog": "更新履歴を見る",
"projects:manage:button:apply changes": "変更を適用",
"projects:manage:dialog:install package": "<b>{{name}}</b> バージョン {{version}} をインストール",
"projects:manage:dialog:reinstall package": "<b>{{name}}</b> バージョン {{version}} を入れ直す",
"projects:manage:dialog:uninstall package as requested": "<b>{{name}}</b> をアンインストール",
"projects:manage:dialog:uninstall package as legacy": "レガシーパッケージとして <b>{{name}}</b> をアンインストール",
"projects:manage:dialog:uninstall package as unused": "使用されていない <b>{{name}}</b> をアンインストール",
@ -132,6 +139,7 @@
"projects:manage:multiple sources": "複数ソース",
"projects:manage:tooltip:remove packages": "パッケージを除去",
"projects:manage:tooltip:add package": "パッケージを追加",
"projects:manage:tooltip:incompatible with unity": "使用中のUnityと互換性がありません。",
"projects:manage:tooltip:upgrade package": "パッケージを更新",
"projects:manage:yanked": "(取り下げ済み)",
"projects:manage:tooltip:back to projects": "プロジェクトリストに戻る",
@ -198,6 +206,8 @@
"projects:toast:invalid project unity version": "プロジェクトのUnityバージョン指定が不正です。",
"projects:toast:match version unity not found": "Unity {{unity}}が見つかりませんでした。該当するUnityをインストールするか、ALCOMに登録してください。",
"projects:toast:opening unity...": "Unityを起動中...",
"projects:toast:unity already running": "Unityは既に起動しています。",
"projects:toast:close unity before migration": "プロジェクト移行を始める前に、Unityを閉じてください。",
"general:dialog:select file or directory header": "ファイルまたはディレクトリを選択",
"general:dialog:select file or directory": "ファイルまたはディレクトリを選択してください。",
"projects:dialog:backup header": "バックアップを作成",
@ -207,5 +217,9 @@
"settings:language": "言語設定(Language)",
"settings:report issue": "問題を報告する",
"settings:button:open issue": "Issueを作成",
"settings:theme": "UIのテーマ",
"settings:theme:system": "システム設定",
"settings:theme:light": "ライト",
"settings:theme:dark": "ダーク",
},
}

View file

@ -78,13 +78,17 @@
"projects:manage:package": "软件包",
"projects:manage:installed": "已安装版本",
"projects:manage:latest": "最新版本",
"general:source": "源",
"general:source": "源",
"projects:manage:toast:no upgradable": "没有可升级的软件包",
"projects:manage:toast:package installed": "已安装 {{name}} 版本 {{version}}",
"projects:manage:toast:package removed": "已删除 {{name}}",
"projects:manage:toast:resolved": "已解决软件包依赖性问题",
"projects:manage:toast:all packages reinstalled": "所有软件包重装成功",
"projects:manage:toast:all packages upgraded": "所有软件包升级成功",
"projects:manage:toast:selected packages installed": "所选软件包安装成功",
"projects:manage:toast:selected packages removed": "所选软件包删除成功",
"projects:manage:toast:the package has newer latest with incompatible unity": "软件包的更新版本与 Unity 版本不兼容。",
"projects:manage:toast:some package has newer latest with incompatible unity": "有些软件包的更新版本与 Unity 版本不兼容。",
"projects:manage:button:unity migrate": "迁移项目",
"projects:toast:unity migrate failed by unity not found": "迁移项目失败: 未找到 Unity 2022",
"projects:toast:unity migration finalize failed by unity not found": "未能完成迁移: 未找到 Unity 2022",
@ -122,6 +126,7 @@
"projects:manage:button:see changelog": "查看更新日志",
"projects:manage:button:apply changes": "应用更改",
"projects:manage:dialog:install package": "安装 <b>{{name}}</b> 版本 {{version}}",
"projects:manage:dialog:reinstall package": "重新安装 <b>{{name}}</b> 版本 {{version}}",
"projects:manage:dialog:uninstall package as requested": "根据您的要求,删除 <b>{{name}}</b>",
"projects:manage:dialog:uninstall package as legacy": "删除 <b>{{name}}</b>,因为它是一个旧版软件包",
"projects:manage:dialog:uninstall package as unused": "删除 <b>{{name}}</b> 因为它未被使用",
@ -140,6 +145,7 @@
"projects:manage:multiple sources": "多个源",
"projects:manage:tooltip:remove packages": "删除软件包",
"projects:manage:tooltip:add package": "添加软件包",
"projects:manage:tooltip:incompatible with unity": "与 Unity 不兼容",
"projects:manage:tooltip:upgrade package": "升级软件包",
"projects:manage:yanked": "(已撤回)",
"projects:manage:tooltip:back to projects": "返回项目",
@ -206,6 +212,8 @@
"projects:toast:invalid project unity version": "我们没有检测到合适的 Unity 安装 ",
"projects:toast:match version unity not found": "未找到匹配的 Unity 版本。请在 ALCOM 设置中安装或添加 Unity 版本",
"projects:toast:opening unity...": "正在打开 Unity...",
"projects:toast:unity already running": "Unity 已经在运行了",
"projects:toast:close unity before migration": "请在迁移前关闭 Unity",
"general:dialog:select file or directory header": "选择文件或文件夹",
"general:dialog:select file or directory": "请选择文件或文件夹",
"projects:dialog:backup header": "备份项目",
@ -215,5 +223,9 @@
"settings:language": "界面语言(Language)",
"settings:report issue": "报告问题",
"settings:button:open issue": "在 GitHub 上创建问题",
"settings:theme": "主题",
"settings:theme:system": "跟随系统",
"settings:theme:light": "明亮",
"settings:theme:dark": "暗黑",
},
}

File diff suppressed because it is too large Load diff

View file

@ -13,31 +13,43 @@
},
"dependencies": {
"@heroicons/react": "^2.1.3",
"@material-tailwind/react": "^2.1.9",
"@tanstack/react-query": "^5.37.1",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.45.1",
"@tauri-apps/api": "^1.5.6",
"@uidotdev/usehooks": "^2.4.1",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"i18next": "^23.11.4",
"next": "14.2.3",
"clsx": "^2.1.1",
"i18next": "^23.11.5",
"lucide-react": "^0.395.0",
"next": "14.2.4",
"react": "^18",
"react-dom": "^18",
"react-i18next": "^14.1.1",
"react-toastify": "^10.0.5"
"react-i18next": "^14.1.2",
"react-toastify": "^10.0.5",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
},
"__comment": "we use react 18.2.19 due to bug. see https://github.com/creativetimofficial/material-tailwind/issues/528",
"devDependencies": {
"@tauri-apps/cli": "^1.5.14",
"@types/node": "^20",
"@types/react": "18.2.19",
"@types/react": "18.3.3",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.19",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"eslint-config-next": "14.2.4",
"json5": "^2.2.3",
"license-checker": "^25.0.1",
"postcss": "^8",
"tailwindcss": "^3.4.3",
"tailwindcss": "^3.4.4",
"typescript": "^5"
}
}

View file

@ -144,10 +144,12 @@ licenseNames.set("ISC", "ISC License");
licenseNames.set("Apache-2.0", "Apache License 2.0");
licenseNames.set("MPL-2.0", "Mozilla Public License 2.0");
licenseNames.set("OFL-1.1", "SIL Open Font License 1.1");
licenseNames.set("BlueOak-1.0.0", "Blue Oak Model License 1.0.0");
licenseNames.set("OpenSSL", "OpenSSL License");
licenseNames.set("CC-BY-4.0", "Creative Commons Attribution 4.0");
licenseNames.set("Unicode-DFS-2016", "Unicode License Agreement - Data Files and Software (2016)");
licenseNames.set("Unicode-3.0", "Unicode License v3")
licenseNames.set("0BSD", "BSD Zero Clause License");
licenseNames.set("BSD-2-Clause", "BSD 2-Clause License");

View file

@ -1,7 +0,0 @@
use std::ffi::OsStr;
use tokio::process::Command;
pub(crate) async fn start_command(_: &OsStr, path: &OsStr, args: &[&OsStr]) -> std::io::Result<()> {
Command::new(path).args(args).spawn()?;
Ok(())
}

View file

@ -1,151 +0,0 @@
//! This module is for creating `cmd.exe /d /c start "Name"
//! "path/to/executable" args` command correctly.
//!
//! Since the `cmd.exe` has a unique escape sequence behavior,
//! It's necessary to escape the path and arguments correctly.
//!
//! I wrote this module based on [research by Y.m Ryota][research-zenn].
//!
//! [research-zenn]: https://zenn.dev/tryjsky/articles/0610b2f32453e7
use std::ffi::{OsStr, OsString};
use std::os::windows::prelude::*;
use tokio::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
pub(crate) async fn start_command(
name: &OsStr,
path: &OsStr,
args: &[&OsStr],
) -> std::io::Result<()> {
// prepare
let percent_env_name = "PERCENT".encode_utf16().collect::<Vec<_>>();
let mut cmd_args = Vec::new();
cmd_args.extend("/d /c start /b \"".encode_utf16());
cmd_args.extend(name.encode_wide());
cmd_args.push(b'"' as u16);
cmd_args.push(b' ' as u16);
// since pathname cannot have '"' in it, we don't need to escape it
cmd_args.push('"' as u16);
append_cmd_no_caret_escape(
&mut cmd_args,
path.encode_wide().collect::<Vec<_>>().as_slice(),
&percent_env_name,
);
cmd_args.push('"' as u16);
let mut buffer = Vec::new();
for arg in args {
cmd_args.push(b' ' as u16);
let arg = arg.encode_wide().collect::<Vec<_>>();
buffer.clear();
append_cpp_escaped(&mut buffer, &arg);
append_cmd_escaped(&mut cmd_args, &buffer, &percent_env_name);
}
// execute
let status = Command::new("cmd.exe")
.raw_arg(OsString::from_wide(&cmd_args))
.creation_flags(CREATE_NO_WINDOW)
.status()
.await?;
if !status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("cmd.exe /d /c start /d failed with status: {}", status),
));
} else {
Ok(())
}
}
fn append_cpp_escaped(args: &mut Vec<u16>, arg: &[u16]) {
let need_quote = arg.iter().any(|&c| c == b' ' as u16 || c == b'\t' as u16);
if need_quote {
args.push(b'"' as u16);
}
let mut backslashes = 0;
for &x in arg {
if x == b'\\' as u16 {
backslashes += 1;
} else {
if x == b'"' as u16 {
// n + 1 backslashes makes n * 2 + 1 backslashes
args.extend(std::iter::repeat(b'\\' as u16).take(backslashes + 1));
}
backslashes = 0;
}
args.push(x);
}
if need_quote {
// n backslashes makes n * 2 backslashes
args.extend(std::iter::repeat(b'\\' as u16).take(backslashes));
args.push(b'"' as u16);
}
}
// ' ' (whitespace), '=', ';', ',', '<', '>', '|', '&', '^', '(', ')', '!', '"', '@'
// We need another escape for '%'
const ESCAPE_CHARS: &[u16] = &[
0x20, 0x3d, 0x3b, 0x2c, 0x3c, 0x3e, 0x7c, 0x26, 0x5e, 0x28, 0x29, 0x21, 0x22, 0x40,
];
fn append_cmd_escaped(args: &mut Vec<u16>, arg: &[u16], percent_env_var_name: &[u16]) {
if arg.first().copied() == Some('"' as u16) && arg.last().copied() == Some('"' as u16) {
// it's "-quoted, so we don't need to escape if there is no '"' inside
let contains_quote = arg.iter().filter(|&&x| x == '"' as u16).count() > 2;
if contains_quote {
append_cmd_caret_escaped(args, arg, percent_env_var_name);
} else {
append_cmd_no_caret_escape(args, arg, percent_env_var_name);
}
} else if arg.iter().any(|x| ESCAPE_CHARS.contains(x)) {
if !arg.iter().any(|&x| x == '"' as u16) {
// if contains escape chars but not ", we can use "-quoting
args.push(b'"' as u16);
append_cmd_no_caret_escape(args, arg, percent_env_var_name);
args.push(b'"' as u16);
} else {
// if contains ", we have to use caret-escaping
append_cmd_caret_escaped(args, arg, percent_env_var_name);
}
} else {
// no escape is needed
append_cmd_no_caret_escape(args, arg, percent_env_var_name);
}
}
fn append_cmd_no_caret_escape(args: &mut Vec<u16>, arg: &[u16], percent_env_var_name: &[u16]) {
// even without ^-escaping, we need to escape '%' since env var expansion is proceeded even
// inside '"'-quoted string
for &x in arg {
if x == b'%' as u16 {
args.push(b'%' as u16);
args.extend_from_slice(percent_env_var_name);
args.push(b'%' as u16);
} else {
args.push(x);
}
}
}
fn append_cmd_caret_escaped(args: &mut Vec<u16>, arg: &[u16], percent_env_var_name: &[u16]) {
for &x in arg {
if x == b'%' as u16 {
args.push(b'%' as u16);
args.extend_from_slice(percent_env_var_name);
args.push(b'%' as u16);
} else if ESCAPE_CHARS.contains(&x) {
args.push(b'^' as u16);
args.push(x);
} else {
args.push(x);
}
}
}

View file

@ -22,6 +22,7 @@ use tokio::fs::read_dir;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use crate::commands::async_command::immediate;
use async_command::{async_command, AsyncCallResult, AsyncCommandContext, With};
use vrc_get_vpm::environment::UserProject;
use vrc_get_vpm::io::{DefaultEnvironmentIo, DefaultProjectIo, DirEntry, EnvironmentIo, IoTrait};
@ -46,6 +47,8 @@ pub(crate) fn handlers() -> impl Fn(Invoke) + Send + Sync + 'static {
generate_handler![
environment_language,
environment_set_language,
environment_theme,
environment_set_theme,
environment_projects,
environment_add_project_with_picker,
environment_remove_project,
@ -85,7 +88,12 @@ pub(crate) fn handlers() -> impl Fn(Invoke) + Send + Sync + 'static {
project_call_unity_for_migration,
project_migrate_project_to_vpm,
project_open_unity,
project_is_unity_launching,
project_create_backup,
project_get_custom_unity_args,
project_set_custom_unity_args,
project_get_unity_path,
project_set_unity_path,
util_open,
util_get_log_entries,
util_get_version,
@ -101,6 +109,8 @@ pub(crate) fn export_ts() {
specta::collect_types![
environment_language,
environment_set_language,
environment_theme,
environment_set_theme,
environment_projects,
environment_add_project_with_picker,
environment_remove_project,
@ -140,7 +150,12 @@ pub(crate) fn export_ts() {
project_call_unity_for_migration,
project_migrate_project_to_vpm,
project_open_unity,
project_is_unity_launching,
project_create_backup,
project_get_custom_unity_args,
project_set_custom_unity_args,
project_get_unity_path,
project_set_unity_path,
util_open,
util_get_log_entries,
util_get_version,
@ -224,13 +239,17 @@ pub(crate) fn startup(app: &mut App) {
async fn open_main(app: AppHandle) -> tauri::Result<()> {
let state: State<'_, Mutex<EnvironmentState>> = app.state();
let (size, fullscreen) =
with_config!(state, |config| (config.window_size, config.fullscreen));
let config = with_config!(state, |config| config.clone());
let query = url::form_urlencoded::Serializer::new(String::new())
.append_pair("lang", &config.language)
.append_pair("theme", &config.theme)
.finish();
let window = tauri::WindowBuilder::new(
&app,
"main", /* the unique window label */
tauri::WindowUrl::App("/projects".into()),
tauri::WindowUrl::App(format!("/projects/?{query}").into()),
)
.title("ALCOM")
.resizable(true)
@ -246,14 +265,14 @@ pub(crate) fn startup(app: &mut App) {
.build()?;
// keep original size if it's too small
if size.width > 100 && size.height > 100 {
if config.window_size.width > 100 && config.window_size.height > 100 {
window.set_size(LogicalSize {
width: size.width,
height: size.height,
width: config.window_size.width,
height: config.window_size.height,
})?;
}
window.set_fullscreen(fullscreen)?;
window.set_fullscreen(config.fullscreen)?;
let cloned = window.clone();
@ -618,6 +637,25 @@ async fn environment_set_language(
})
}
#[tauri::command]
#[specta::specta]
async fn environment_theme(state: State<'_, Mutex<EnvironmentState>>) -> Result<String, RustError> {
with_config!(state, |config| Ok(config.theme.clone()))
}
#[tauri::command]
#[specta::specta]
async fn environment_set_theme(
state: State<'_, Mutex<EnvironmentState>>,
theme: String,
) -> Result<(), RustError> {
with_config!(state, |mut config| {
config.theme = theme;
config.save().await?;
Ok(())
})
}
#[tauri::command]
#[specta::specta]
async fn environment_projects(
@ -635,6 +673,7 @@ async fn environment_projects(
environment.migrate_from_settings_json().await?;
info!("syncing information with real projects");
environment.sync_with_real_projects(true).await?;
environment.dedup_projects()?;
environment.save().await?;
info!("fetching projects");
@ -927,6 +966,7 @@ impl From<&Version> for TauriVersion {
struct TauriBasePackageInfo {
name: String,
display_name: Option<String>,
description: Option<String>,
aliases: Vec<String>,
version: TauriVersion,
unity: Option<(u16, u8)>,
@ -941,6 +981,7 @@ impl TauriBasePackageInfo {
Self {
name: package.name().to_string(),
display_name: package.display_name().map(|v| v.to_string()),
description: package.description().map(|v| v.to_string()),
aliases: package.aliases().iter().map(|v| v.to_string()).collect(),
version: package.version().into(),
unity: package.unity().map(|v| (v.major(), v.minor())),
@ -1139,18 +1180,22 @@ async fn environment_unity_versions(
with_environment!(&state, |environment| {
environment.find_unity_hub().await.ok();
let unity_paths = environment
.get_unity_installations()?
.iter()
.filter_map(|unity| {
Some((
unity.path().to_string(),
unity.version()?.to_string(),
unity.loaded_from_hub(),
))
})
.collect();
environment.disconnect_litedb();
Ok(TauriUnityVersions {
unity_paths: environment
.get_unity_installations()?
.iter()
.filter_map(|unity| {
Some((
unity.path().to_string(),
unity.version()?.to_string(),
unity.loaded_from_hub(),
))
})
.collect(),
unity_paths,
recommended_version: VRCHAT_RECOMMENDED_2022_UNITY.to_string(),
install_recommended_version_link: VRCHAT_RECOMMENDED_2022_UNITY_HUB_LINK.to_string(),
})
@ -2337,25 +2382,52 @@ async fn project_migrate_project_to_vpm(
Ok(())
}
fn is_unity_running(project_path: impl AsRef<Path>) -> bool {
crate::os::is_locked(&project_path.as_ref().join("Temp/UnityLockFile")).unwrap_or(false)
}
#[tauri::command]
#[specta::specta]
async fn project_open_unity(
state: State<'_, Mutex<EnvironmentState>>,
project_path: String,
unity_path: String,
) -> Result<(), RustError> {
) -> Result<bool, RustError> {
if is_unity_running(&project_path) {
// it looks unity is running. returning false
return Ok(false);
}
let mut custom_args: Option<Vec<String>> = None;
with_environment!(&state, |environment| {
if let Some(project) = environment.find_project(project_path.as_ref())? {
custom_args = project
.custom_unity_args()
.map(|x| Vec::from_iter(x.iter().map(ToOwned::to_owned)));
}
update_project_last_modified(environment, project_path.as_ref()).await;
});
crate::cmd_start::start_command(
"Unity".as_ref(),
unity_path.as_ref(),
&["-projectPath".as_ref(), OsStr::new(project_path.as_str())],
)
.await?;
let mut args = vec!["-projectPath".as_ref(), OsStr::new(project_path.as_str())];
Ok(())
if let Some(custom_args) = &custom_args {
args.extend(custom_args.iter().map(OsStr::new));
} else {
// TODO: configurable default options?
// Note: remember to change similar in typescript
args.push(OsStr::new("-debugCodeOptimization"));
}
crate::os::start_command("Unity".as_ref(), unity_path.as_ref(), &args).await?;
Ok(true)
}
#[tauri::command]
#[specta::specta]
fn project_is_unity_launching(project_path: String) -> bool {
return is_unity_running(&project_path);
}
fn folder_stream(
@ -2580,6 +2652,92 @@ async fn project_create_backup(
.await
}
#[tauri::command]
#[specta::specta]
async fn project_get_custom_unity_args(
state: State<'_, Mutex<EnvironmentState>>,
project_path: String,
) -> Result<Option<Vec<String>>, RustError> {
with_environment!(&state, |environment| {
let result;
if let Some(project) = environment.find_project(project_path.as_ref())? {
result = project
.custom_unity_args()
.map(|x| x.iter().map(ToOwned::to_owned).collect());
} else {
result = None;
}
environment.disconnect_litedb();
Ok(result)
})
}
#[tauri::command]
#[specta::specta]
async fn project_set_custom_unity_args(
state: State<'_, Mutex<EnvironmentState>>,
project_path: String,
args: Option<Vec<String>>,
) -> Result<bool, RustError> {
with_environment!(&state, |environment| {
if let Some(mut project) = environment.find_project(project_path.as_ref())? {
if let Some(args) = args {
project.set_custom_unity_args(args);
} else {
project.clear_custom_unity_args();
}
environment.update_project(&project)?;
environment.disconnect_litedb();
Ok(true)
} else {
environment.disconnect_litedb();
Ok(false)
}
})
}
#[tauri::command]
#[specta::specta]
async fn project_get_unity_path(
state: State<'_, Mutex<EnvironmentState>>,
project_path: String,
) -> Result<Option<String>, RustError> {
with_environment!(&state, |environment| {
let result;
if let Some(project) = environment.find_project(project_path.as_ref())? {
result = project.unity_path().map(ToOwned::to_owned);
} else {
result = None;
}
environment.disconnect_litedb();
Ok(result)
})
}
#[tauri::command]
#[specta::specta]
async fn project_set_unity_path(
state: State<'_, Mutex<EnvironmentState>>,
project_path: String,
unity_path: Option<String>,
) -> Result<bool, RustError> {
with_environment!(&state, |environment| {
if let Some(mut project) = environment.find_project(project_path.as_ref())? {
if let Some(unity_path) = unity_path {
project.set_unity_path(unity_path);
} else {
project.clear_unity_path();
}
environment.update_project(&project)?;
environment.disconnect_litedb();
Ok(true)
} else {
environment.disconnect_litedb();
Ok(false)
}
})
}
#[tauri::command]
#[specta::specta]
async fn util_open(path: String) -> Result<(), RustError> {

View file

@ -6,7 +6,7 @@ use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use vrc_get_vpm::io::{DefaultEnvironmentIo, EnvironmentIo, IoTrait};
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GuiConfig {
#[serde(default)]
@ -19,6 +19,8 @@ pub struct GuiConfig {
pub fullscreen: bool,
#[serde(default = "language_default")]
pub language: String,
#[serde(default = "theme_default")]
pub theme: String,
#[serde(default = "backup_default")]
pub backup_format: String,
#[serde(default = "project_sorting_default")]
@ -33,6 +35,7 @@ impl Default for GuiConfig {
window_size: WindowSize::default(),
fullscreen: false,
language: language_default(),
theme: theme_default(),
backup_format: backup_default(),
project_sorting: project_sorting_default(),
}
@ -75,6 +78,10 @@ fn language_default() -> String {
"en".to_string()
}
fn theme_default() -> String {
"system".to_string()
}
fn backup_default() -> String {
"default".to_string()
}

View file

@ -177,6 +177,7 @@ Categories=Utility;
log::info!("Desktop file created: {}", desktop_file.display());
if let Err(e) = tokio::process::Command::new("update-desktop-database")
.arg(applications_dir)
.status()
.await
{

View file

@ -3,10 +3,6 @@
use tauri::Manager;
#[cfg_attr(windows, path = "cmd_start_win.rs")]
#[cfg_attr(not(windows), path = "cmd_start_basic.rs")]
mod cmd_start;
mod commands;
mod config;
mod deep_link_support;
@ -14,6 +10,10 @@ mod logging;
mod specta;
mod templates;
#[cfg_attr(windows, path = "os_windows.rs")]
#[cfg_attr(not(windows), path = "os_posix.rs")]
mod os;
// for clippy compatibility
#[cfg(not(clippy))]
fn tauri_context() -> tauri::Context<impl tauri::Assets> {

View file

@ -0,0 +1,30 @@
//! OS-specific functionality.
use std::ffi::OsStr;
use std::fs::OpenOptions;
use std::io;
use std::os::fd::AsRawFd;
use std::path::Path;
use nix::libc::{c_short, flock, F_UNLCK};
use tokio::process::Command;
pub(crate) async fn start_command(_: &OsStr, path: &OsStr, args: &[&OsStr]) -> std::io::Result<()> {
Command::new(path).args(args).spawn()?;
Ok(())
}
pub(crate) fn is_locked(path: &Path) -> io::Result<bool> {
let mut lock = flock {
l_start: 0,
l_len: 0,
l_pid: 0,
l_type: F_UNLCK as c_short, // macOS denies l_type: 0
l_whence: 0,
};
let file = OpenOptions::new().read(true).open(path)?;
nix::fcntl::fcntl(file.as_raw_fd(), nix::fcntl::F_GETLK(&mut lock))?;
return Ok(lock.l_type != F_UNLCK as c_short);
}

View file

@ -0,0 +1,174 @@
//! OS-specific functionality.
//! This module is for creating `cmd.exe /d /c start "Name"
//! "path/to/executable" args` command correctly.
//!
//! Since the `cmd.exe` has a unique escape sequence behavior,
//! It's necessary to escape the path and arguments correctly.
//!
//! I wrote this module based on [BatBadBut] article.
//!
//! [BatBadBut]: https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/#as-a-developer
use std::ffi::{OsStr, OsString};
use std::fs::OpenOptions;
use std::mem::MaybeUninit;
use std::os::windows::ffi::EncodeWide;
use std::os::windows::prelude::*;
use std::path::Path;
use std::{io, result};
use tokio::process::Command;
use windows::Win32::Foundation::{ERROR_LOCK_VIOLATION, HANDLE};
use windows::Win32::Storage::FileSystem::{
LockFileEx, UnlockFileEx, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LOCK_FILE_FLAGS,
};
use windows::Win32::System::IO::OVERLAPPED;
const CREATE_NO_WINDOW: u32 = 0x08000000;
pub(crate) async fn start_command(
name: &OsStr,
path: &OsStr,
args: &[&OsStr],
) -> std::io::Result<()> {
// prepare
let mut cmd_args = Vec::new();
cmd_args.extend("/E:ON /V:OFF /d /c start /b ".encode_utf16());
append_cmd_escaped(&mut cmd_args, name.encode_wide());
cmd_args.push(b' ' as u16);
append_cmd_escaped(&mut cmd_args, path.encode_wide());
let mut buffer = Vec::new();
for arg in args {
cmd_args.push(b' ' as u16);
let arg = arg.encode_wide().collect::<Vec<_>>();
buffer.clear();
append_cpp_escaped(&mut buffer, &arg);
append_cmd_escaped(&mut cmd_args, buffer.iter().copied());
}
// execute
let status = Command::new("cmd.exe")
.raw_arg(OsString::from_wide(&cmd_args))
.creation_flags(CREATE_NO_WINDOW)
.status()
.await?;
if !status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"cmd.exe /E:ON /V:OFF /d /c start /d failed with status: {}",
status
),
));
} else {
Ok(())
}
}
/*
/d /c /E:ON /V:OFF start /b "Unity" "C:\Program Files\Unity\Hub\Editor\2022.3.22f1\Editor\Unity.exe" "-projectPath" """D:\VRC\新しいフォルダー (3)\world 2""" "-debugCodeOptimization"
*/
fn append_cpp_escaped(args: &mut Vec<u16>, arg: &[u16]) {
let need_quote = arg.iter().any(|&c| c == b' ' as u16 || c == b'\t' as u16);
if need_quote {
args.push(b'"' as u16);
}
let mut backslashes = 0;
for &x in arg {
if x == b'\\' as u16 {
backslashes += 1;
} else {
if x == b'"' as u16 {
// n + 1 backslashes makes n * 2 + 1 backslashes
args.extend(std::iter::repeat(b'\\' as u16).take(backslashes + 1));
}
backslashes = 0;
}
args.push(x);
}
if need_quote {
// n backslashes makes n * 2 backslashes
args.extend(std::iter::repeat(b'\\' as u16).take(backslashes));
args.push(b'"' as u16);
}
}
// %%cd:~,%
const PERCENT_ESCAPED: &[u16] = &[0x25, 0x25, 0x63, 0x64, 0x3a, 0x7e, 0x2c, 0x25];
// based on https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/#as-a-developer
fn append_cmd_escaped(args: &mut Vec<u16>, arg: impl Iterator<Item = u16>) {
// Enclose the argument with double quotes (").
args.push('"' as u16);
let mut backslash = 0;
for x in arg {
if x == b'%' as u16 {
args.extend_from_slice(PERCENT_ESCAPED);
} else if x == b'"' as u16 {
// Replace the backslash (\) in front of the double quote (") with two backslashes (\\).
// To implement that, append the backslashes again
args.extend(std::iter::repeat(b'\\' as u16).take(backslash));
// Replace the double quote (") with two double quotes ("").
args.push(b'"' as u16);
args.push(b'"' as u16);
} else if x == '\n' as u16 {
// Remove newline characters (\n).
} else {
args.push(x);
}
// count b'\\'
if x == b'\\' as u16 {
backslash += 1;
} else {
backslash = 0;
}
}
// Enclose the argument with double quotes (").
args.push('"' as u16);
}
pub(crate) fn is_locked(path: &Path) -> io::Result<bool> {
let file = OpenOptions::new().read(true).open(path)?;
unsafe {
let mut overlapped: OVERLAPPED = MaybeUninit::zeroed().assume_init();
overlapped.Anonymous.Anonymous.Offset = 0;
overlapped.Anonymous.Anonymous.OffsetHigh = 0;
match LockFileEx(
HANDLE(file.as_raw_handle() as isize),
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
0,
0,
&mut overlapped,
) {
Err(ref e) if e.code() == ERROR_LOCK_VIOLATION.into() => {
// ERROR_LOCK_VIOLATION means it's already locked
return Ok(false);
}
// other error
Err(e) => return Err(e.into()),
Ok(()) => {}
}
// lock successful; it's not locked so unlock and return true
let mut overlapped: OVERLAPPED = MaybeUninit::zeroed().assume_init();
overlapped.Anonymous.Anonymous.Offset = 0;
overlapped.Anonymous.Anonymous.OffsetHigh = 0;
UnlockFileEx(
HANDLE(file.as_raw_handle() as isize),
0,
!0,
!0,
&mut overlapped,
)?;
return Ok(true);
}
}

View file

@ -1,13 +1,21 @@
import type {Config} from "tailwindcss";
import withMT from "@material-tailwind/react/utils/withMT";
const config: Config = {
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
@ -18,9 +26,76 @@ const config: Config = {
sans: ["system-ui"],
path: ["system-ui"],
mono: ["consolas", "monospace"],
}
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
info: {
DEFAULT: "hsl(var(--info))",
foreground: "hsl(var(--info-foreground))",
},
success: {
DEFAULT: "hsl(var(--success))",
foreground: "hsl(var(--success-foreground))",
},
warning: {
DEFAULT: "hsl(var(--warning))",
foreground: "hsl(var(--warning-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [],
};
export default withMT(config);
plugins: [require("tailwindcss-animate")],
} satisfies Config;
export default config;

View file

@ -1,6 +1,6 @@
[package]
name = "vrc-get-litedb"
version = "0.2.1-beta.0"
version = "0.2.2-beta.0"
edition.workspace = true
license.workspace = true
authors.workspace = true
@ -28,13 +28,13 @@ include = [
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bson = "2.10.0"
bson = "2.11.0"
hex = "0.4.3"
once_cell = "1.19.0"
rand = "0.8.5"
serde = "1.0.202"
serde = "1.0.203"
[build-dependencies]
ar = "0.9.0"
cc = "1.0.98"
object = { version = "0.35.0", default-features = false, features = ["macho"] }
cc = "1.0.99"
object = { version = "0.36.0", default-features = false, features = ["macho"] }

View file

@ -87,8 +87,8 @@ fn main() {
let common_libs: &[&str] = &[
//"static=Runtime.ServerGC",
"static=Runtime.WorkstationGC",
"static=eventpipe-disabled",
"static:-bundle=Runtime.WorkstationGC",
"static:-bundle=eventpipe-disabled",
];
for lib in common_libs {

View file

@ -46,7 +46,10 @@ impl TargetInformation {
Self {
dotnet_runtime_id: rid,
output_file_name: "vrc-get-litedb.a",
link_libraries: vec!["static=System.Native", "static=stdc++compat"],
link_libraries: vec![
"static:-bundle=System.Native",
"static:-bundle=stdc++compat",
],
bootstrapper: "libbootstrapperdll.o",
patch_mach_o: false,
family: TargetFamily::Linux,

@ -1 +1 @@
Subproject commit 7db4e9111ceffae4f3a45f2e91536e97ed8dbee1
Subproject commit 34de31e7ed6f1996ea991a06cab2e167d88760c9

View file

@ -26,20 +26,20 @@ itertools = "0.13.0"
log = "0.4.21"
pin-project-lite = "0.2.14"
reqwest = { version = "0.12.4", features = ["stream"], default-features = false }
serde = { version = "1.0.200", features = ["derive", "rc"] }
serde = { version = "1.0.203", features = ["derive", "rc"] }
serde_json = { version = "1.0.116", features = ["preserve_order"] }
sha2 = "0.10.8"
tokio-util = "0.7.11"
url = { version = "2.5.0", features = ["serde"] }
url = { version = "2.5.1", features = ["serde"] }
uuid = { version = "1.8.0", features = ["v4"] }
lazy_static = "1.4.0"
enum-map = "2.7.3"
vrc-get-litedb = { version = "0.2.0", optional = true }
tokio = { version = "1.37.0", features = ["fs", "process"], optional = true }
vrc-get-litedb = { version = "0.2.1", optional = true }
tokio = { version = "1.38.0", features = ["fs", "process"], optional = true }
serde_path_to_error = "0.1.16"
serde-value = "0.7.0"
bson = "2.10.0"
bson = "2.11.0"
serde_repr = "0.1.19"
[target."cfg(windows)".dependencies]

View file

@ -17,7 +17,7 @@ use crate::repository::local::LocalCachedRepository;
use crate::repository::RemoteRepository;
use crate::structs::setting::UserRepoSetting;
use crate::traits::{EnvironmentIoHolder, HttpClient, PackageCollection, RemotePackageDownloader};
use crate::utils::{to_vec_pretty_os_eol, Sha256AsyncWrite};
use crate::utils::{normalize_path, to_vec_pretty_os_eol, Sha256AsyncWrite};
use crate::{PackageInfo, PackageManifest, VersionSelector};
use futures::future::{join_all, try_join};
use futures::prelude::*;
@ -392,6 +392,8 @@ impl<T: HttpClient, IO: EnvironmentIo> Environment<T, IO> {
path: &Path,
name: Option<&str>,
) -> Result<(), AddRepositoryErr> {
let path = normalize_path(path);
if self.get_user_repos().iter().any(|x| x.local_path() == path) {
return Err(AddRepositoryErr::AlreadyAdded);
}

View file

@ -1,5 +1,5 @@
use crate::io::{EnvironmentIo, FileSystemProjectIo, ProjectIo};
use crate::utils::PathBufExt;
use crate::utils::{check_absolute_path, normalize_path, PathBufExt};
use crate::version::UnityVersion;
use crate::{io, Environment, HttpClient, ProjectType, UnityProject};
use bson::oid::ObjectId;
@ -7,7 +7,7 @@ use bson::DateTime;
use futures::future::join_all;
use log::error;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::path::{Component, Path, PathBuf};
pub(crate) static COLLECTION: &str = "projects";
@ -120,6 +120,13 @@ impl<T: HttpClient, IO: EnvironmentIo> Environment<T, IO> {
return Ok(None);
}
let normalized = normalize_path(path);
let normalized = if normalized != path {
Some(normalized)
} else {
None
};
let mut changed = false;
let loaded_project = UnityProject::load(io.new_project_io(path)).await?;
@ -146,36 +153,89 @@ impl<T: HttpClient, IO: EnvironmentIo> Environment<T, IO> {
project.project_type = project_type;
}
if let Some(normalized) = normalized {
changed = true;
project.path = normalized.to_str().unwrap().into();
}
Ok(if changed { Some(project) } else { None })
}
Ok(())
}
pub fn dedup_projects(&mut self) -> io::Result<()> {
let db = self.get_db()?; // ensure the database connection is initialized
let projects = db.get_values::<UserProject>(COLLECTION)?;
let mut projects_by_path = HashMap::<_, Vec<_>>::new();
for project in &projects {
projects_by_path
.entry(project.path())
.or_default()
.push(project);
}
for (_, mut values) in projects_by_path {
if values.len() == 1 {
continue;
}
// update favorite and last modified
let favorite = values.iter().any(|x| x.favorite());
let last_modified = values.iter().map(|x| x.last_modified()).max().unwrap();
let mut project = values[0].clone();
let mut changed = false;
if project.favorite() != favorite {
project.set_favorite(favorite);
changed = true;
}
if project.last_modified() != last_modified {
project.last_modified = last_modified;
changed = true;
}
if changed {
db.update(COLLECTION, &project)?;
}
// remove rest
for project in values.iter().skip(1) {
db.delete(COLLECTION, project.id)?;
}
}
Ok(())
}
// TODO: return wrapper type instead?
pub fn get_projects(&self) -> io::Result<Vec<UserProject>> {
Ok(self.get_db()?.get_values(COLLECTION)?)
}
pub fn update_project_last_modified(&mut self, project_path: &Path) -> io::Result<()> {
pub fn find_project(&self, project_path: &Path) -> io::Result<Option<UserProject>> {
check_absolute_path(project_path)?;
let db = self.get_db()?;
let project_path = if project_path.is_absolute() {
normalize_path(project_path)
} else {
normalize_path(&std::env::current_dir().unwrap().joined(project_path))
};
let project_path = normalize_path(project_path);
let mut project = db.get_values::<UserProject>(COLLECTION)?;
let Some(project) = project
.iter_mut()
.find(|x| Path::new(x.path()) == project_path)
else {
Ok(project
.into_iter()
.find(|x| Path::new(x.path()) == project_path))
}
pub fn update_project_last_modified(&mut self, project_path: &Path) -> io::Result<()> {
check_absolute_path(project_path)?;
let Some(mut project) = self.find_project(project_path)? else {
return Ok(());
};
project.last_modified = DateTime::now();
db.update(COLLECTION, project)?;
self.update_project(&project)?;
Ok(())
}
@ -196,12 +256,8 @@ impl<T: HttpClient, IO: EnvironmentIo> Environment<T, IO> {
&mut self,
project: &UnityProject<ProjectIO>,
) -> io::Result<()> {
let path = project.project_dir();
let path = if path.is_absolute() {
normalize_path(path)
} else {
normalize_path(&std::env::current_dir().unwrap().joined(path))
};
check_absolute_path(project.project_dir())?;
let path = normalize_path(project.project_dir());
let path = path.to_str().ok_or(io::Error::new(
io::ErrorKind::InvalidData,
"project path is not utf8",
@ -227,25 +283,7 @@ impl<T: HttpClient, IO: EnvironmentIo> Environment<T, IO> {
}
}
fn normalize_path(input: &Path) -> PathBuf {
let mut result = PathBuf::with_capacity(input.as_os_str().len());
for component in input.components() {
match component {
Component::Prefix(prefix) => result.push(prefix.as_os_str()),
Component::RootDir => result.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
result.pop();
}
Component::Normal(_) => result.push(component.as_os_str()),
}
}
result
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub struct UserProject {
#[serde(rename = "_id")]
id: ObjectId,
@ -265,12 +303,14 @@ pub struct UserProject {
vrc_get: Option<VrcGetMeta>,
}
#[derive(Serialize, Deserialize, Default)]
#[derive(Serialize, Deserialize, Default, Clone)]
struct VrcGetMeta {
#[serde(default)]
cached_unity_version: Option<UnityVersion>,
#[serde(default)]
unity_revision: Option<String>,
custom_unity_args: Option<Vec<String>>,
unity_path: Option<String>,
}
impl UserProject {
@ -345,4 +385,32 @@ impl UserProject {
.filter(|x| x.cached_unity_version == self.unity_version)
.and_then(|x| x.unity_revision.as_deref())
}
pub fn custom_unity_args(&self) -> Option<&[String]> {
self.vrc_get
.as_ref()
.and_then(|x| x.custom_unity_args.as_deref())
}
pub fn set_custom_unity_args(&mut self, custom_unity_args: Vec<String>) {
self.vrc_get
.get_or_insert_with(Default::default)
.custom_unity_args = Some(custom_unity_args);
}
pub fn clear_custom_unity_args(&mut self) {
self.vrc_get.as_mut().map(|x| x.custom_unity_args = None);
}
pub fn unity_path(&self) -> Option<&str> {
self.vrc_get.as_ref().and_then(|x| x.unity_path.as_deref())
}
pub fn set_unity_path(&mut self, unity_path: String) {
self.vrc_get.get_or_insert_with(Default::default).unity_path = Some(unity_path);
}
pub fn clear_unity_path(&mut self) {
self.vrc_get.as_mut().map(|x| x.unity_path = None);
}
}

View file

@ -1,9 +1,10 @@
use crate::io::EnvironmentIo;
use crate::utils::{check_absolute_path, normalize_path};
use crate::version::UnityVersion;
use crate::{io, Environment, HttpClient};
use bson::oid::ObjectId;
use log::info;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
@ -18,10 +19,23 @@ impl<T: HttpClient, IO: EnvironmentIo> Environment<T, IO> {
&mut self,
path: &str,
version: UnityVersion,
) -> io::Result<()> {
check_absolute_path(path)?;
self.add_unity_installation_internal(path, version, false)
.await
}
async fn add_unity_installation_internal(
&mut self,
path: &str,
version: UnityVersion,
is_from_hub: bool,
) -> io::Result<()> {
let db = self.get_db()?;
let installation = UnityInstallation::new(path.into(), Some(version), false);
let mut installation = UnityInstallation::new(path.into(), Some(version), false);
installation.loaded_from_hub = is_from_hub;
db.insert(COLLECTION, &installation)?;
@ -151,19 +165,39 @@ impl<T: HttpClient, IO: EnvironmentIo> Environment<T, IO> {
let mut installed = HashSet::new();
for mut in_db in db.get_values::<UnityInstallation>(COLLECTION)? {
if !self.io.is_file(in_db.path().as_ref()).await {
let path = Path::new(in_db.path());
if !self.io.is_file(path).await {
// if the unity editor not found, remove it from the db
info!("Removed Unity that is not exists: {}", in_db.path());
db.delete(COLLECTION, in_db.id)?;
continue;
}
installed.insert(PathBuf::from(in_db.path()));
if installed.contains(path) {
// if the unity editor is already installed, remove it from the db
info!("Removed duplicated Unity: {}", in_db.path());
db.delete(COLLECTION, in_db.id)?;
continue;
}
let exists_in_hub = paths_from_hub.contains(Path::new(in_db.path()));
installed.insert(PathBuf::from(path));
let normalized = normalize_path(path).into_os_string().into_string().unwrap();
let exists_in_hub = paths_from_hub.contains(path);
let mut update = false;
if exists_in_hub != in_db.loaded_from_hub() {
in_db.loaded_from_hub = exists_in_hub;
update = true;
}
if &normalized != in_db.path() {
in_db.path = normalized.into();
update = true;
}
if update {
db.update(COLLECTION, &in_db)?;
}
}
@ -178,7 +212,7 @@ impl<T: HttpClient, IO: EnvironmentIo> Environment<T, IO> {
continue;
}
info!("Adding Unity from Unity Hub: {}", path.display());
self.add_unity_installation(&path.to_string_lossy(), version)
self.add_unity_installation_internal(&path.to_string_lossy(), version, true)
.await?;
}
}
@ -195,7 +229,7 @@ pub struct UnityInstallation {
#[serde(rename = "Path")]
path: Box<str>,
#[serde(rename = "Version")]
#[serde(deserialize_with = "parse_loose_unity_version")]
#[serde(deserialize_with = "default_if_err")]
version: Option<UnityVersion>,
#[serde(rename = "LoadedFromHub")]
loaded_from_hub: bool,
@ -225,19 +259,15 @@ impl UnityInstallation {
}
// for unity 2018.x or older, VCC will parse version as "2018.4.0" instead of "2018.4.0f1"
// so we need to use loose version parser
fn parse_loose_unity_version<'de, D>(deserializer: D) -> Result<Option<UnityVersion>, D::Error>
// and 2018.4.31f1 as "2018.4" instead of "2018.4.31f1"
// Therefore, we need skip parsing such a version string.
fn default_if_err<'de, D, T>(de: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
D: Deserializer<'de>,
T: Deserialize<'de> + Default,
{
let s = String::deserialize(deserializer)?;
if let Some(parsed) = UnityVersion::parse(&s) {
return Ok(Some(parsed));
match T::deserialize(de) {
Ok(v) => Ok(v),
Err(_) => Ok(T::default()),
}
if let Some(parsed) = UnityVersion::parse_no_type_increment(&s) {
return Ok(Some(parsed));
}
Err(serde::de::Error::custom("Invalid Unity Version"))
}

View file

@ -14,7 +14,7 @@ use futures::stream::FuturesUnordered;
use pin_project_lite::pin_project;
use serde_json::error::Category;
use serde_json::{Map, Value};
use std::path::{Path, PathBuf};
use std::path::{Component, Path, PathBuf};
use std::pin::Pin;
use std::task::{ready, Context, Poll};
@ -290,3 +290,31 @@ where
Err(e) => Err(e),
}
}
pub(crate) fn normalize_path(input: &Path) -> PathBuf {
let mut result = PathBuf::with_capacity(input.as_os_str().len());
for component in input.components() {
match component {
Component::Prefix(prefix) => result.push(prefix.as_os_str()),
Component::RootDir => result.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
result.pop();
}
Component::Normal(_) => result.push(component.as_os_str()),
}
}
result
}
pub(crate) fn check_absolute_path(path: impl AsRef<Path>) -> io::Result<()> {
if !path.as_ref().is_absolute() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"project path must be absolute",
));
}
Ok(())
}

View file

@ -16,17 +16,17 @@ categories = ["command-line-utilities"]
[dependencies]
anstyle = "1.0.7"
clap = { version = "4.5.4", features = ["derive"] }
clap_complete = "4.5.1"
clap = { version = "4.5.7", features = ["derive"] }
clap_complete = "4.5.5"
color-print = "0.3.6"
env_logger = "0.11.3"
indexmap = { version = "2.2.6", features = ["serde"] }
itertools = "0.13.0"
log = "0.4.21"
reqwest = { version = "0.12.4", default-features = false }
serde = { version = "1.0.200", features = ["derive", "rc"] }
serde = { version = "1.0.203", features = ["derive", "rc"] }
serde_json = { version = "1.0.116", features = ["preserve_order"] }
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros", "fs"] }
tokio = { version = "1.38.0", features = ["rt-multi-thread", "macros", "fs"] }
[dependencies.vrc-get-vpm]
version = "0.0.13-beta.0"

View file

@ -109,10 +109,24 @@ async fn load_unity(path: Option<Box<Path>>) -> UnityProject {
.exit_context("loading unity project")
}
fn absolute_path(path: impl AsRef<Path>) -> PathBuf {
fn impl_(path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_owned()
} else {
env::current_dir()
.exit_context("getting current directory")
.join(path)
}
}
impl_(path.as_ref())
}
#[cfg(feature = "experimental-vcc")]
async fn update_project_last_modified(env: Environment, project_dir: &Path) {
async fn inner(mut env: Environment, project_dir: &Path) -> Result<(), std::io::Error> {
env.update_project_last_modified(project_dir)?;
env.update_project_last_modified(&absolute_path(project_dir))?;
env.save().await?;
Ok(())
}
@ -1020,13 +1034,11 @@ impl RepoAdd {
.await
.exit_context("adding repository")
} else {
let cwd = env::current_dir().exit_context("getting current directory");
let joined = cwd.join(&self.path_or_url);
let normalized = normalize_path(&joined);
let normalized = absolute_path(&self.path_or_url);
if !normalized.exists() {
exit_with!("path not found: {}", normalized.display());
}
env.add_local_repo(normalized.as_ref(), self.name.as_deref())
env.add_local_repo(&normalized, self.name.as_deref())
.exit_context("adding repository")
}
@ -1034,24 +1046,6 @@ impl RepoAdd {
}
}
fn normalize_path(input: &Path) -> PathBuf {
let mut result = PathBuf::with_capacity(input.as_os_str().len());
for component in input.components() {
match component {
Component::Prefix(prefix) => result.push(prefix.as_os_str()),
Component::RootDir => result.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
result.pop();
}
Component::Normal(_) => result.push(component.as_os_str()),
}
}
result
}
/// Remove repository with specified url, path or name
#[derive(Parser)]
#[command(author, version)]

View file

@ -1,4 +1,4 @@
use crate::commands::{load_env, ResultExt};
use crate::commands::{absolute_path, load_env, ResultExt};
use clap::{Parser, Subcommand};
use log::warn;
use std::cmp::Reverse;
@ -56,6 +56,8 @@ impl ProjectList {
.await
.exit_context("syncing with real projects");
env.dedup_projects().exit_context("deduplicating projects");
let mut projects = env.get_projects().exit_context("getting projects");
projects.sort_by_key(|x| Reverse(x.last_modified().timestamp_millis()));
@ -91,10 +93,11 @@ impl ProjectAdd {
pub async fn run(self) {
let mut env = load_env(&self.env_args).await;
let project =
UnityProject::load(DefaultProjectIo::new(Path::new(self.path.as_ref()).into()))
.await
.exit_context("loading specified project");
let project_path = absolute_path(Path::new(self.path.as_ref()));
let project_io = DefaultProjectIo::new(project_path.into());
let project = UnityProject::load(project_io)
.await
.exit_context("loading specified project");
if !project.is_valid().await {
return eprintln!("Invalid project at {}", self.path);