mirror of
https://github.com/vrc-get/vrc-get.git
synced 2026-06-21 09:58:08 +00:00
Compare commits
382 commits
release-na
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a1044df51 |
||
|
|
e71c48141b |
||
|
|
467434903a | ||
|
|
ba8606d312 | ||
|
|
ed42cdfca4 | ||
|
|
dbd479fbc9 |
||
|
|
db54f0c8ae |
||
|
|
da419a0374 |
||
|
|
8351270552 |
||
|
|
50fccc9a50 |
||
|
|
cf7075a6d5 |
||
|
|
5e4709209f |
||
|
|
269cb8aeda |
||
|
|
ba9ad1f59c |
||
|
|
23b1bb2515 |
||
|
|
2c36473e6e | ||
|
|
791e50d94f | ||
|
|
a77ab4a0a3 |
||
|
|
aa14673468 |
||
|
|
36aa59b304 |
||
|
|
b61c44c64a |
||
|
|
0a5ab490b8 |
||
|
|
cfc1b627eb |
||
|
|
1b2065afe0 |
||
|
|
4634c42410 |
||
|
|
fd8b735ac9 |
||
|
|
580c8da047 |
||
|
|
0ebfb25263 |
||
|
|
9eaf99b3d1 |
||
|
|
87753ceffa |
||
|
|
0fa954cef0 |
||
|
|
41e25d4e86 |
||
|
|
3c499f9d6a |
||
|
|
65e37ff7af |
||
|
|
48e1866ff3 |
||
|
|
968ae63a4c |
||
|
|
805be35ac6 |
||
|
|
901368c9bf |
||
|
|
09c8190634 | ||
|
|
5eed047fa4 |
||
|
|
056073834c |
||
|
|
c4117c7457 |
||
|
|
3bf6b96b93 |
||
|
|
00fb4d8546 |
||
|
|
4d4c0c27d3 | ||
|
|
a31c947c1a |
||
|
|
f6e677909c | ||
|
|
8b58270235 | ||
|
|
c1b297e02a | ||
|
|
a0ec779fab |
||
|
|
800278a0c3 | ||
|
|
334492e5c6 | ||
|
|
1a947b7a6c | ||
|
|
514f52a419 | ||
|
|
3e42f0de4e | ||
|
|
6dfdd9c5f3 | ||
|
|
21a0e19722 | ||
|
|
6ab9c49c94 | ||
|
|
04adf285b5 | ||
|
|
665513d0fc |
||
|
|
6f8cd53159 |
||
|
|
ec6454b0d4 |
||
|
|
533a40b1eb | ||
|
|
90c7e249cd | ||
|
|
e057838795 | ||
|
|
437a63bcc6 | ||
|
|
836bf82351 |
||
|
|
316c73a6b6 | ||
|
|
ec04492af8 | ||
|
|
ebd117afd2 | ||
|
|
253a0266e6 | ||
|
|
0cf635e892 | ||
|
|
f8923dce1b |
||
|
|
9fc228eed0 |
||
|
|
54113144a8 | ||
|
|
0ab6c9434b | ||
|
|
9d94d8b929 | ||
|
|
d1887eb467 | ||
|
|
45ed34f573 | ||
|
|
930715960e | ||
|
|
8db24b7b7c | ||
|
|
44ed683c23 | ||
|
|
c248b0ed55 | ||
|
|
9d11a5e816 |
||
|
|
81d66143c9 | ||
|
|
6f143ac5e3 |
||
|
|
f44633a2ad |
||
|
|
3fb8281a47 |
||
|
|
244946c234 |
||
|
|
ccd252f8aa |
||
|
|
c2b2b9b491 |
||
|
|
9decfe400c |
||
|
|
35e7537a90 |
||
|
|
1800951c12 | ||
|
|
76b0f05651 |
||
|
|
f45ee6514e |
||
|
|
d421ee04b2 |
||
|
|
bd92649248 |
||
|
|
68be14aa90 |
||
|
|
dd9673c2cb |
||
|
|
73cfa96f45 |
||
|
|
fa22fc3611 |
||
|
|
8b61a0c806 |
||
|
|
c37758d4c1 |
||
|
|
37688599ae | ||
|
|
fcb0c5263d |
||
|
|
a7e19a6479 | ||
|
|
f0a7cbd050 |
||
|
|
a9322f4d64 |
||
|
|
0c895bf4a5 |
||
|
|
07e7279c31 |
||
|
|
a9e135b940 |
||
|
|
d2aca1554c |
||
|
|
5be2c13984 |
||
|
|
1b1a0eefae |
||
|
|
3e94514b13 |
||
|
|
56a32bad04 |
||
|
|
fb6a6538d7 |
||
|
|
ab64f403a5 |
||
|
|
96d8ced3bb |
||
|
|
bde10abd3b |
||
|
|
e3313f0327 |
||
|
|
bb0b8cda55 |
||
|
|
11b6b1cad4 |
||
|
|
92cff628c2 |
||
|
|
3f3465d74b |
||
|
|
d1c28f6479 |
||
|
|
089427cbfa |
||
|
|
375894c6c3 |
||
|
|
eed54886b8 |
||
|
|
11c1b7048b |
||
|
|
b613f834ea |
||
|
|
5d9c6ce802 |
||
|
|
f15e489515 |
||
|
|
687904918d |
||
|
|
c5d38a1f6e |
||
|
|
55635bbe9c |
||
|
|
ca30b889f1 |
||
|
|
0a17c9a1cd |
||
|
|
50aba0530e |
||
|
|
33be110c7a |
||
|
|
2aad318387 |
||
|
|
43ca6755ff |
||
|
|
92bc6cf38e |
||
|
|
8d293e5e69 |
||
|
|
1e9a100d9c |
||
|
|
8624950ca5 |
||
|
|
57c9a54d37 |
||
|
|
6470169cf1 |
||
|
|
795699baf0 |
||
|
|
7fbf8c60d1 |
||
|
|
a5c5d6e128 |
||
|
|
a3aa50256f |
||
|
|
498eedc1d6 |
||
|
|
c863bc4461 |
||
|
|
8fd964fa83 |
||
|
|
c7012c5e81 |
||
|
|
cf6c82adbd |
||
|
|
e0931be79a | ||
|
|
e0dce679d7 |
||
|
|
f9ca613378 |
||
|
|
0fa0c7ca00 |
||
|
|
9c1465868e |
||
|
|
874492f3fc |
||
|
|
8588a7276c |
||
|
|
0a5b9c38eb |
||
|
|
17a28fa609 |
||
|
|
099883a33d |
||
|
|
5cb60c4002 |
||
|
|
cdb1b8294d |
||
|
|
0de963736f | ||
|
|
0026f5029c |
||
|
|
987357354e |
||
|
|
f13add3409 |
||
|
|
2d7fefb34c |
||
|
|
e625630856 |
||
|
|
4943a671e0 |
||
|
|
6e63cdba25 |
||
|
|
e04d1ee919 |
||
|
|
a4992e3973 |
||
|
|
185b7aa8b7 |
||
|
|
24ae2b33de |
||
|
|
1b928b150b |
||
|
|
2d859d993e |
||
|
|
4018f718a7 |
||
|
|
de73063b98 |
||
|
|
d6ea9b3079 |
||
|
|
eaf09ecb6a |
||
|
|
d74c7b16b8 |
||
|
|
6c46983cae |
||
|
|
36b76933fc |
||
|
|
ad7efa259e |
||
|
|
97042ebc6a |
||
|
|
e135092745 |
||
|
|
5607daf7b5 | ||
|
|
2861fcaa68 | ||
|
|
8a71e426d6 |
||
|
|
cd242e389b |
||
|
|
95dc46666b |
||
|
|
f0c4de7d78 |
||
|
|
e9a994a932 |
||
|
|
803fbf3395 |
||
|
|
20d24b496b |
||
|
|
a1ff52fed4 |
||
|
|
4a99cfe4c9 |
||
|
|
8b13244b74 | ||
|
|
875a989d52 |
||
|
|
973c91c9c0 |
||
|
|
3d2dd2e058 |
||
|
|
16d8a787ec |
||
|
|
4ccc7d7bc8 |
||
|
|
8c8315f57d |
||
|
|
4cac964713 |
||
|
|
fb32bd97cd |
||
|
|
1a70d2db82 |
||
|
|
cacb9a5e9a |
||
|
|
4700bfa2c0 |
||
|
|
223be38368 |
||
|
|
523ede22ea |
||
|
|
38a7649fbd |
||
|
|
d61b46ded8 |
||
|
|
4756f6c115 |
||
|
|
e14806c72c |
||
|
|
8324a4ec04 |
||
|
|
a9f2a9713f |
||
|
|
40cd4487d5 |
||
|
|
56a2139df6 |
||
|
|
175008956c |
||
|
|
00a928e58e |
||
|
|
7037a758bc |
||
|
|
f457ffcc69 |
||
|
|
9291853c19 |
||
|
|
0e6d011ee5 |
||
|
|
be5c9aa765 |
||
|
|
b090c440ac |
||
|
|
3c51a2de31 |
||
|
|
04fa1f8244 |
||
|
|
35b26d9322 |
||
|
|
478c25b03a |
||
|
|
20e5836985 |
||
|
|
ce8de700a7 |
||
|
|
3076713ea6 |
||
|
|
75974f4ad9 |
||
|
|
4eadf2bdcd |
||
|
|
c43ed4a828 |
||
|
|
f675d29173 |
||
|
|
c45e4e399f |
||
|
|
f5f7ac794e |
||
|
|
88fd6a06a7 |
||
|
|
c3020225f3 |
||
|
|
d2f9a39b67 |
||
|
|
776bd4b4e8 |
||
|
|
4ae3507f96 |
||
|
|
7b105639f9 |
||
|
|
fc177add7c |
||
|
|
4063021e2a |
||
|
|
463b4c5df7 |
||
|
|
bbbbd19926 |
||
|
|
3d7b2b8e22 |
||
|
|
b24bcf1c38 |
||
|
|
19bde29c6b |
||
|
|
f18bc14777 |
||
|
|
07a5eb27b3 |
||
|
|
6a5c60a79e |
||
|
|
2b04760299 |
||
|
|
1845e8083d |
||
|
|
faf432fd61 |
||
|
|
45942829df | ||
|
|
b64e09dc55 |
||
|
|
dee9a2fb78 |
||
|
|
508d62b3e6 |
||
|
|
f119b203f9 |
||
|
|
a64cb3af9b |
||
|
|
70f7f8c78e |
||
|
|
b64058cab0 |
||
|
|
3e8d5cc9d9 |
||
|
|
2809d94996 |
||
|
|
dccd85be31 |
||
|
|
726ca0c068 |
||
|
|
5887c5631c |
||
|
|
a57de0d7f1 |
||
|
|
9bc7d26fff |
||
|
|
20c20a49b0 |
||
|
|
6a4648ddbb |
||
|
|
4783761910 |
||
|
|
1fdb97d04a |
||
|
|
b3430d926b |
||
|
|
4edda28776 |
||
|
|
b8903da155 |
||
|
|
38ac981d22 |
||
|
|
9fa4d25637 |
||
|
|
490190606c |
||
|
|
fd6eb27b09 |
||
|
|
5fd5109f3b |
||
|
|
496a1c91c1 |
||
|
|
42f1835360 |
||
|
|
4b89aec67a |
||
|
|
422b4b1b8d |
||
|
|
98cde02985 |
||
|
|
b6cb3f51f7 |
||
|
|
50bc72d662 |
||
|
|
9655865ba9 |
||
|
|
827042c827 |
||
|
|
e265b8fcdb |
||
|
|
3394e59e49 |
||
|
|
dccfbc2867 |
||
|
|
1cbc34bb30 |
||
|
|
61832aa154 |
||
|
|
b696c7cc31 |
||
|
|
847d76cb5c | ||
|
|
a29a9bc9e2 |
||
|
|
72103a67a5 |
||
|
|
7f1be1818e |
||
|
|
6a7d60d89c |
||
|
|
7df14594d1 |
||
|
|
001a6a330e |
||
|
|
9ff7c8e1e7 |
||
|
|
17f315a44a |
||
|
|
d2cdc980e2 |
||
|
|
9c9e8b8f9c |
||
|
|
b7dc45c2c3 |
||
|
|
ebb17f10e3 |
||
|
|
da26f04917 |
||
|
|
930c79d649 |
||
|
|
f19b9d0084 |
||
|
|
c771de380a |
||
|
|
bb26d062de |
||
|
|
41b1514f66 |
||
|
|
09aebbcb8b |
||
|
|
df8174341e |
||
|
|
93012d8bd1 |
||
|
|
9a739177bc |
||
|
|
34ef49a629 |
||
|
|
647799180a |
||
|
|
e00162180f |
||
|
|
1567b9dec2 |
||
|
|
2e5962422a |
||
|
|
fb292da426 |
||
|
|
98b8df7bb2 |
||
|
|
a5355dd287 |
||
|
|
8915177909 |
||
|
|
ad232e3506 |
||
|
|
a8aed0a011 |
||
|
|
79b7776e40 |
||
|
|
d96ae23a60 |
||
|
|
c44bbe2584 |
||
|
|
fa05c2bc30 |
||
|
|
9b26a5fdb3 |
||
|
|
c8b0cf89c8 |
||
|
|
911c0a5e51 |
||
|
|
8773f3d3e6 |
||
|
|
974a195a3c |
||
|
|
9b63d1919f |
||
|
|
04b65168b8 |
||
|
|
f2ce2829eb |
||
|
|
d3d127efb8 |
||
|
|
98a9bf6938 |
||
|
|
56f586974e | ||
|
|
2f6e52032e |
||
|
|
09d60c804f |
||
|
|
8c210c248f | ||
|
|
3f6ba916c0 | ||
|
|
3213f9288a |
||
|
|
2411f7482b |
||
|
|
7ac0b41800 |
||
|
|
03f1140d71 |
||
|
|
3e58fc6d1a |
||
|
|
fe0f711d62 |
||
|
|
dbd5fbbca8 |
||
|
|
da12ba3cad |
||
|
|
1178e151ef |
||
|
|
0cbbaacd12 |
||
|
|
bd7f84196b |
||
|
|
40d0786b19 |
||
|
|
7fdd1192e1 |
||
|
|
d09244291f |
||
|
|
670642d8cb | ||
|
|
d2cabbdc0b | ||
|
|
257a479f3e | ||
|
|
add44e04a9 | ||
|
|
c1eb9ffc16 | ||
|
|
6104fc017d |
117 changed files with 5570 additions and 4071 deletions
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github: [anatawa12]
|
||||
custom: [https://booth.pm/ja/items/6448396]
|
||||
8
.github/copilot-instructions.md
vendored
Normal file
8
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
If you're writing code:
|
||||
- Please don't make localization for locales other than en / ja. I cannot review those locales.
|
||||
- Run cargo clippy for lints and cargo fmt for format before commit.
|
||||
- After completing the code and commit, please add a changelog entry. Please note that the numbers in the changelog file are pull request numbers, not issue numbers.
|
||||
- Please add it to the bottom of the change list.
|
||||
- Please use the proper section for each change. "Fix" should be used only for bug fixes. UX improvements typically belong under "Change", and new features typically under "Add". These are not strict rules, so use them flexibly.
|
||||
- You should use Conventional Commits (chore:, fix:, dev:, build:, docs:, style:, lint:, and others).
|
||||
- Please split commits for implementation and changelog updates.
|
||||
195
.github/workflows/ci-gui.yml
vendored
195
.github/workflows/ci-gui.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
include:
|
||||
- triple: x86_64-unknown-linux-gnu
|
||||
on: ubuntu-22.04
|
||||
bundles: appimage,appimage-updater,deb,rpm
|
||||
bundles: appimage,appimage-updater
|
||||
setup: |
|
||||
sudo apt update && sudo apt install -y lld
|
||||
ld.lld --version
|
||||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
|
||||
- triple: x86_64-pc-windows-msvc
|
||||
on: windows-latest
|
||||
bundles: setup-exe,exe-updater
|
||||
bundles: setup-exe,setup-exe-zip,exe-updater
|
||||
|
||||
- triple: universal-apple-darwin
|
||||
on: macos-14
|
||||
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
|
||||
steps:
|
||||
- uses: samypr100/setup-dev-drive@v3
|
||||
- uses: samypr100/setup-dev-drive@v4
|
||||
with:
|
||||
drive-size: 12GB # github actions grantees 14 GB of disk space. we have few GB for action environment
|
||||
drive-path: "/dev_drive.vhdx"
|
||||
|
|
@ -122,6 +122,195 @@ jobs:
|
|||
target/${{ matrix.triple }}/release/bundle/*/ALCOM*
|
||||
target/${{ matrix.triple }}/release/bundle/*/alcom*
|
||||
|
||||
build-rpm:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- install_rust: false
|
||||
- no_dist: false
|
||||
- mock-env: fedora-40-x86_64
|
||||
install_rust: true
|
||||
no_dist: true
|
||||
mock-env:
|
||||
- fedora-40-x86_64
|
||||
- fedora-rawhide-x86_64
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'fedora:latest'
|
||||
options: --privileged
|
||||
env:
|
||||
MOCK_ENV: ${{ matrix.mock-env }}
|
||||
RPMBUILD_OPTS: ${{ case(matrix.no_dist, '-D "dist %{nil}"', '') }} ${{ case(matrix.install_rust, '-D "install_rust 1"', '') }}
|
||||
steps:
|
||||
- name: Install CI dependencies
|
||||
run: dnf install -y git tar curl
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
# https://github.com/actions/checkout/issues/1169
|
||||
- run: git config --system --add safe.directory $GITHUB_WORKSPACE
|
||||
- name: install dependencies
|
||||
run: dnf install -y mock rpmbuild
|
||||
- name: prepare rpm build environment
|
||||
run: mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
- name: update version name in spec file
|
||||
run: |
|
||||
COMMIT_HASH="$(git rev-parse HEAD)"
|
||||
SHORT_HASH="$(git rev-parse --short HEAD)"
|
||||
PKG_VERSION="$(<vrc-get-gui/Cargo.toml sed -En -e '/^version/{s/.*"(.*)".*/\1/p;}')+${SHORT_HASH}"
|
||||
|
||||
echo "PKG_VERSION=$PKG_VERSION" >> $GITHUB_ENV
|
||||
|
||||
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
|
||||
rm vrc-get-gui/Cargo.toml.bak
|
||||
git add vrc-get-gui/Cargo.toml
|
||||
|
||||
sed -i vrc-get-gui/bundle/alcom.spec -e "/^Version:/c\Version: ${PKG_VERSION//-/\~}"
|
||||
- name: build source rpm package
|
||||
run: |
|
||||
git archive --format=tar --prefix=vrc-get-gui-v$PKG_VERSION/ $(git write-tree) | gzip > ~/rpmbuild/SOURCES/gui-v$PKG_VERSION.tar.gz
|
||||
eval "rpmbuild -bs vrc-get-gui/bundle/alcom.spec $RPMBUILD_OPTS"
|
||||
- name: build rpm package
|
||||
run: eval "mock -v -r '$(ls -1 /etc/mock{/eol,}/$MOCK_ENV.cfg 2>/dev/null)' --enable-network $RPMBUILD_OPTS rebuild ~/rpmbuild/SRPMS/alcom-${PKG_VERSION//-/\~}-1*.src.rpm"
|
||||
- name: copy built binaries
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp ~/rpmbuild/SRPMS/alcom-${PKG_VERSION//-/\~}-1*.src.rpm artifacts/
|
||||
cp /var/lib/mock/$MOCK_ENV/result/alcom-${PKG_VERSION//-/\~}-1*.${MOCK_ENV##*-}.rpm artifacts/
|
||||
|
||||
- name: Upload built binary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rpm-${{ matrix.mock-env }}
|
||||
path: artifacts/*
|
||||
|
||||
build-deb:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- install_rust: false
|
||||
- install_nodejs: false
|
||||
- apt-components: main
|
||||
- apt-with-updates: false
|
||||
|
||||
# Old distributions have older tools than we need. Download tools in build process
|
||||
- pbuilder-distribution: bookworm
|
||||
install_rust: true
|
||||
install_nodejs: true
|
||||
- pbuilder-distribution: jammy
|
||||
install_rust: true
|
||||
install_nodejs: true
|
||||
|
||||
# Debian uses mirror from debian-archive.trafficmanager.net which is managed by microsoft on azure
|
||||
- pbuilder-distribution: bookworm
|
||||
mirror: http://debian-archive.trafficmanager.net/debian/
|
||||
keyring: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
- pbuilder-distribution: sid
|
||||
mirror: http://debian-archive.trafficmanager.net/debian/
|
||||
keyring: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
# Ubuntu legacy release
|
||||
- pbuilder-distribution: jammy
|
||||
mirror: http://archive.ubuntu.com/ubuntu/
|
||||
apt-components: main universe
|
||||
apt-with-updates: true
|
||||
keyring: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
# For note,
|
||||
# bookworm: libc6@2.36
|
||||
# sid: libc6@2.39 as of 2026/06/14
|
||||
# jammy: libc6@2.35
|
||||
pbuilder-distribution:
|
||||
# debian distribution
|
||||
- bookworm
|
||||
- sid
|
||||
# ubuntu distribution
|
||||
- jammy # jammy is the oldest ubuntu release with libwebkit2gtk-4.1 >= 2.41 (but requires -updates and universe)
|
||||
target-arch:
|
||||
- amd64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.target-arch }}
|
||||
PBUILDER_DISTRIBUTION: ${{ matrix.pbuilder-distribution }}
|
||||
PBUILDER_MIRROR: ${{ matrix.mirror }}
|
||||
PBUILDER_KEYRING: ${{ matrix.keyring }}
|
||||
PBUILDER_COMPONENTS: ${{ matrix.apt-components }}
|
||||
PBUILDER_OTHERMIRROR: ${{ case(matrix.apt-with-updates, format('deb {0} {1}-updates {2}', matrix.mirror, matrix.pbuilder-distribution, matrix.apt-components), '') }}
|
||||
INSTALL_RUST: ${{ case(matrix.install_rust, '1', '0') }}
|
||||
INSTALL_NODEJS: ${{ case(matrix.install_nodejs, '1', '0') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
path: vrc-get
|
||||
submodules: recursive
|
||||
- name: install dependencies
|
||||
run: sudo apt update && sudo apt install -y pbuilder debian-archive-keyring debhelper-compat=13
|
||||
- name: prepare deb build environment
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
cp -r vrc-get-gui/bundle/debian debian
|
||||
sudo pbuilder create \
|
||||
--architecture "$TARGET_ARCH" \
|
||||
--keyring "$PBUILDER_KEYRING" \
|
||||
--mirror "$PBUILDER_MIRROR" \
|
||||
--distribution "$PBUILDER_DISTRIBUTION" \
|
||||
--components "$PBUILDER_COMPONENTS" \
|
||||
--othermirror "$PBUILDER_OTHERMIRROR"
|
||||
- name: update changelog
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
COMMIT_HASH="$(git rev-parse HEAD)"
|
||||
SHORT_HASH="$(git rev-parse --short HEAD)"
|
||||
PKG_VERSION="$(<vrc-get-gui/Cargo.toml sed -En -e '/^version/{s/.*"(.*)".*/\1/p;}')+${SHORT_HASH}"
|
||||
|
||||
echo "PKG_VERSION=$PKG_VERSION" >> $GITHUB_ENV
|
||||
|
||||
cp debian/changelog debian/changelog.bak
|
||||
cat - debian/changelog.bak <<CHANGELOG > debian/changelog
|
||||
alcom (${PKG_VERSION//-/\~}-1) experimental;
|
||||
|
||||
* Upgraded version to ${PKG_VERSION}
|
||||
|
||||
-- anatawa12 <i@anatawa12.com> $(date -u +"%a, %d %b %Y %H:%M:%S +0000")
|
||||
|
||||
CHANGELOG
|
||||
rm debian/changelog.bak
|
||||
|
||||
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
|
||||
rm vrc-get-gui/Cargo.toml.bak
|
||||
git add vrc-get-gui/Cargo.toml
|
||||
|
||||
echo cat debian/changelog
|
||||
cat debian/changelog
|
||||
dpkg-parsechangelog
|
||||
- name: build source deb package
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
git archive --format=tar $(git write-tree) | xz > ../alcom_${PKG_VERSION//-/\~}.orig.tar.xz
|
||||
dpkg-buildpackage -d -S
|
||||
- name: build deb package
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
sudo --preserve-env=INSTALL_RUST,INSTALL_NODEJS pbuilder build --use-network yes ../alcom_${PKG_VERSION//-/\~}-1.dsc
|
||||
- name: copy built binaries
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp vrc-get/debian/changelog artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}* artifacts/
|
||||
ls artifacts
|
||||
- name: Print information about built package
|
||||
run: dpkg-deb -I artifacts/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.deb
|
||||
|
||||
- name: Upload built binary
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deb-${{ matrix.pbuilder-distribution }}-${{ matrix.target-arch }}
|
||||
path: artifacts/*
|
||||
|
||||
conclude-gui:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
|
|
|
|||
189
.github/workflows/publish-gui.yml
vendored
189
.github/workflows/publish-gui.yml
vendored
|
|
@ -67,6 +67,21 @@ jobs:
|
|||
;;
|
||||
esac
|
||||
|
||||
GUI_VERSION="$(get-version -t gui)"
|
||||
# update debian package
|
||||
cp vrc-get-gui/bundle/debian/changelog vrc-get-gui/bundle/debian/changelog.bak
|
||||
cat - vrc-get-gui/bundle/debian/changelog.bak <<CHANGELOG > vrc-get-gui/bundle/debian/changelog
|
||||
alcom (${GUI_VERSION//-/\~}-1) stable;
|
||||
|
||||
* Upgraded version to ${GUI_VERSION}
|
||||
|
||||
-- anatawa12 <i@anatawa12.com> $(date -u +"%a, %d %b %Y %H:%M:%S +0000")
|
||||
|
||||
CHANGELOG
|
||||
rm vrc-get-gui/bundle/debian/changelog.bak
|
||||
# update rpm package
|
||||
sed -i vrc-get-gui/bundle/alcom.spec -e "/^Version:/c\Version: ${GUI_VERSION//-/\~}"
|
||||
|
||||
case "$GITHUB_REF_NAME" in
|
||||
master | master-* )
|
||||
echo "head is master or master-*"
|
||||
|
|
@ -81,7 +96,7 @@ jobs:
|
|||
;;
|
||||
esac
|
||||
|
||||
gh-export-variable GUI_VERSION "$(get-version -t gui)"
|
||||
gh-export-variable GUI_VERSION "${GUI_VERSION}"
|
||||
env:
|
||||
RELEASE_KIND_IN: ${{ inputs.release_kind }}
|
||||
DRY_RUN: ${{ inputs.dry-run }}
|
||||
|
|
@ -152,29 +167,15 @@ jobs:
|
|||
bundle/appimage/ALCOM_${GUI_VERSION}_x86_64.AppImage.tar.gz:alcom-${GUI_VERSION}-x86_64.AppImage.tar.gz
|
||||
bundle/appimage/ALCOM_${GUI_VERSION}_x86_64.AppImage.tar.gz.sig:alcom-${GUI_VERSION}-x86_64.AppImage.tar.gz.sig
|
||||
|
||||
- name: x86_64-linux-package
|
||||
triple: x86_64-unknown-linux-gnu
|
||||
on: ubuntu-22.04
|
||||
setup: |
|
||||
sudo apt update && sudo apt install -y lld
|
||||
ld.lld --version
|
||||
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
rustflags: "-C link-arg=-fuse-ld=lld"
|
||||
alcom-build-options: --no-self-updater --updater-instruction-message "en=You can download newer package from official website."
|
||||
last-bundles: deb,rpm
|
||||
dist-path: |
|
||||
bundle/deb/alcom_${GUI_VERSION}-1_amd64.deb:alcom_${GUI_VERSION}-1_amd64.deb
|
||||
bundle/rpm/alcom-${GUI_VERSION}-1.x86_64.rpm:alcom-${GUI_VERSION}-1.x86_64.rpm
|
||||
|
||||
- name: x86_64-windows-all
|
||||
triple: x86_64-pc-windows-msvc
|
||||
on: windows-2022
|
||||
last-bundles: exe-updater
|
||||
last-bundles: setup-exe-zip,exe-updater
|
||||
updater-bundle: bundle/setup/alcom-updater.exe
|
||||
dist-path: |
|
||||
ALCOM.exe:ALCOM-${GUI_VERSION}-x86_64.exe
|
||||
bundle/setup/alcom-setup.exe:ALCOM-${GUI_VERSION}-x86_64-setup.exe
|
||||
bundle/setup/alcom-setup.exe.zip:ALCOM-${GUI_VERSION}-x86_64-setup.exe.zip
|
||||
bundle/setup/alcom-updater.exe:ALCOM-${GUI_VERSION}-x86_64-updater.exe
|
||||
bundle/setup/alcom-updater.exe.sig:ALCOM-${GUI_VERSION}-x86_64-updater.exe.sig
|
||||
|
||||
|
|
@ -195,8 +196,6 @@ jobs:
|
|||
name:
|
||||
- x86_64-linux-appimage
|
||||
#- aarch64-unknown-linux-musl
|
||||
- x86_64-linux-package
|
||||
#- aarch64-linux-package
|
||||
- x86_64-windows-all
|
||||
#- aarch64-windows-all
|
||||
- universal-macos-all
|
||||
|
|
@ -273,11 +272,6 @@ jobs:
|
|||
- name: Bundle ALCOM (${{ matrix.last-bundles }})
|
||||
run: cargo xtask bundle-alcom --target ${{ matrix.triple }} --release --bundles ${{ matrix.last-bundles }}
|
||||
|
||||
- name: Bundle ALCOM Updater (Windows)
|
||||
if: ${{ contains(matrix.name, 'windows') }}
|
||||
shell: bash
|
||||
run: cargo xtask bundle-alcom --target ${{ matrix.triple }} --release --bundles exe-updater
|
||||
|
||||
- name: Sign updater artifacts (All Platforms)
|
||||
shell: bash
|
||||
if: ${{ matrix.updater-bundle }}
|
||||
|
|
@ -291,6 +285,7 @@ jobs:
|
|||
cargo xtask sign-alcom-updater "target/${{ matrix.triple }}/release/${UPDATER_BUNDLE}"
|
||||
|
||||
- name: Move artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
env:
|
||||
GUI_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
|
|
@ -309,10 +304,154 @@ jobs:
|
|||
done
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: artifacts-${{ matrix.name }}
|
||||
path: artifacts/*
|
||||
|
||||
build-rpm:
|
||||
needs: [ pre-build ]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- install_rust: false
|
||||
- no_dist: false
|
||||
- mock-env: fedora-40-x86_64
|
||||
install_rust: true
|
||||
no_dist: true
|
||||
mock-env:
|
||||
- fedora-40-x86_64
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'fedora:latest'
|
||||
options: --privileged
|
||||
env:
|
||||
MOCK_ENV: ${{ matrix.mock-env }}
|
||||
RPMBUILD_OPTS: ${{ case(matrix.no_dist, '-D "dist %{nil}"', '') }} ${{ case(matrix.install_rust, '-D "install_rust 1"', '') }}
|
||||
|
||||
PKG_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
steps:
|
||||
- name: Install CI dependencies
|
||||
run: dnf install -y git tar curl
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
submodules: recursive
|
||||
# https://github.com/actions/checkout/issues/1169
|
||||
- run: git config --system --add safe.directory $GITHUB_WORKSPACE
|
||||
- name: install dependencies
|
||||
run: dnf install -y mock rpmbuild
|
||||
- name: prepare rpm build environment
|
||||
run: mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
- name: build source rpm package
|
||||
run: |
|
||||
git archive --format=tar --prefix=vrc-get-gui-v$PKG_VERSION/ $(git write-tree) | gzip > ~/rpmbuild/SOURCES/gui-v$PKG_VERSION.tar.gz
|
||||
eval "rpmbuild -bs vrc-get-gui/bundle/alcom.spec $RPMBUILD_OPTS"
|
||||
- name: build rpm package
|
||||
run: eval "mock -v -r '$(ls -1 /etc/mock{/eol,}/$MOCK_ENV.cfg 2>/dev/null)' --enable-network $RPMBUILD_OPTS rebuild ~/rpmbuild/SRPMS/alcom-${PKG_VERSION//-/\~}-1*.src.rpm"
|
||||
- name: copy built binaries
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp ~/rpmbuild/SRPMS/alcom-${PKG_VERSION//-/\~}-1*.src.rpm artifacts/
|
||||
cp /var/lib/mock/$MOCK_ENV/result/alcom-${PKG_VERSION//-/\~}-1*.${MOCK_ENV##*-}.rpm artifacts/
|
||||
|
||||
- name: Upload built binary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-rpm-${{ matrix.mock-env }}
|
||||
path: artifacts/*
|
||||
|
||||
build-deb:
|
||||
needs: [ pre-build ]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- install_rust: false
|
||||
- install_nodejs: false
|
||||
- apt-components: main
|
||||
- apt-with-updates: false
|
||||
|
||||
# Old distributions have older tools than we need. Download tools in build process
|
||||
- pbuilder-distribution: jammy
|
||||
install_rust: true
|
||||
install_nodejs: true
|
||||
|
||||
# Debian uses mirror from debian-archive.trafficmanager.net which is managed by microsoft on azure
|
||||
- pbuilder-distribution: jammy
|
||||
mirror: http://archive.ubuntu.com/ubuntu/
|
||||
apt-components: main universe
|
||||
apt-with-updates: true
|
||||
keyring: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
# We build on jammy since it's the distribution with a) libwebkit2gtk-4.1 >= 2.41 and 2) oldest libc version required.
|
||||
# bookworm: libc6@2.36
|
||||
# sid: libc6@2.39 as of 2026/06/14
|
||||
# jammy: libc6@2.35
|
||||
pbuilder-distribution:
|
||||
- jammy # jammy is the oldest ubuntu release with libwebkit2gtk-4.1 >= 2.41 (but requires -updates and universe)
|
||||
target-arch:
|
||||
- amd64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.target-arch }}
|
||||
PBUILDER_DISTRIBUTION: ${{ matrix.pbuilder-distribution }}
|
||||
PBUILDER_MIRROR: ${{ matrix.mirror }}
|
||||
PBUILDER_KEYRING: ${{ matrix.keyring }}
|
||||
PBUILDER_COMPONENTS: ${{ matrix.apt-components }}
|
||||
PBUILDER_OTHERMIRROR: ${{ case(matrix.apt-with-updates, format('deb {0} {1}-updates {2}', matrix.mirror, matrix.pbuilder-distribution, matrix.apt-components), '') }}
|
||||
INSTALL_RUST: ${{ case(matrix.install_rust, '1', '0') }}
|
||||
INSTALL_NODEJS: ${{ case(matrix.install_nodejs, '1', '0') }}
|
||||
|
||||
PKG_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
path: vrc-get
|
||||
submodules: recursive
|
||||
- name: install dependencies
|
||||
run: sudo apt update && sudo apt install -y pbuilder debian-archive-keyring debhelper-compat=13
|
||||
- name: prepare deb build environment
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
( mkdir debian && cd debian && ln -s ../vrc-get-gui/bundle/debian/* . )
|
||||
sudo pbuilder create \
|
||||
--architecture "$TARGET_ARCH" \
|
||||
--keyring "$PBUILDER_KEYRING" \
|
||||
--mirror "$PBUILDER_MIRROR" \
|
||||
--distribution "$PBUILDER_DISTRIBUTION" \
|
||||
--components "$PBUILDER_COMPONENTS" \
|
||||
--othermirror "$PBUILDER_OTHERMIRROR"
|
||||
- name: build source deb package
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
git archive --format=tar HEAD | xz > ../alcom_${PKG_VERSION//-/\~}.orig.tar.xz
|
||||
dpkg-buildpackage -d -S
|
||||
- name: build deb package
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
sudo --preserve-env=INSTALL_RUST,INSTALL_NODEJS pbuilder build --use-network yes ../alcom_${PKG_VERSION//-/\~}-1.dsc
|
||||
- name: copy built binaries
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.deb artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.buildinfo artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.changes artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1.debian.tar.xz artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1.dsc artifacts/
|
||||
ls artifacts
|
||||
- name: Print information about built package
|
||||
run: dpkg-deb -I artifacts/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.deb
|
||||
|
||||
- name: Upload built binary
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-deb-${{ matrix.pbuilder-distribution }}-${{ matrix.target-arch }}
|
||||
path: artifacts/*
|
||||
|
||||
build-updater-json:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre-build, build-rust ]
|
||||
|
|
@ -349,7 +488,7 @@ jobs:
|
|||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre-build, build-rust, build-updater-json ]
|
||||
needs: [ pre-build, build-rust, build-rpm, build-deb, build-updater-json ]
|
||||
env:
|
||||
GUI_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
steps:
|
||||
|
|
|
|||
24
.github/workflows/publish.yml
vendored
24
.github/workflows/publish.yml
vendored
|
|
@ -12,6 +12,11 @@ on:
|
|||
- prerelease
|
||||
- start-rc
|
||||
- stable
|
||||
dry-run:
|
||||
type: boolean
|
||||
description: Dry Run, If true, do not publish release to GitHub.
|
||||
default: true
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: releasing
|
||||
|
|
@ -70,8 +75,12 @@ jobs:
|
|||
echo "head is master, master-*, or hotfix-*"
|
||||
;;
|
||||
* )
|
||||
echo "invalid release kind: $RELEASE_KIND_IN is not allowd for $GITHUB_REF_NAME"
|
||||
exit 255
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "head is not master, but DRY_RUN is true"
|
||||
else
|
||||
echo "head is not master, but DRY_RUN is false"
|
||||
exit 255
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
|
|
@ -79,6 +88,7 @@ jobs:
|
|||
gh-export-variable VPM_VERSION "$(get-version -t vpm)"
|
||||
env:
|
||||
RELEASE_KIND_IN: ${{ github.event.inputs.release_kind }}
|
||||
DRY_RUN: ${{ inputs.dry-run }}
|
||||
|
||||
# region changelog
|
||||
- name: Create Changelog
|
||||
|
|
@ -189,10 +199,13 @@ jobs:
|
|||
run: cargo build --target ${{ matrix.triple }} --release --verbose
|
||||
- name: Check binary is statically linked
|
||||
shell: bash
|
||||
env:
|
||||
RUSTFLAGS: ''
|
||||
run: |
|
||||
cargo xtask check-static-link target/${{ matrix.triple }}/release/vrc-get${WINDIR:+.exe}
|
||||
|
||||
- name: Move artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
run: |-
|
||||
mkdir artifacts
|
||||
|
|
@ -203,12 +216,14 @@ jobs:
|
|||
popd
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: artifacts-${{ matrix.triple }}
|
||||
path: artifacts/*
|
||||
|
||||
publish-crates-io:
|
||||
name: Publish to crates.io
|
||||
if: ${{ !inputs.dry-run }}
|
||||
environment:
|
||||
name: crates.io
|
||||
url: https://crates.io/crates/vrc-get
|
||||
|
|
@ -233,6 +248,7 @@ jobs:
|
|||
|
||||
publish-to-github:
|
||||
name: Publish to GitHub
|
||||
if: ${{ !inputs.dry-run }}
|
||||
environment:
|
||||
name: actions-github-app
|
||||
url: https://github.com/anatawa12/vrc-get/releases/v${{ needs.pre-build.outputs.cli-version }}
|
||||
|
|
@ -337,7 +353,7 @@ jobs:
|
|||
publish-to-homebrew:
|
||||
name: Publish to homebrew
|
||||
# vrc-get is on autobump list https://github.com/Homebrew/homebrew-core/blame/master/.github/autobump.txt
|
||||
if: false # ${{ !needs.pre-build.outputs.prerelease }}
|
||||
if: false # ${{ !inputs.dry-run && !needs.pre-build.outputs.prerelease }}
|
||||
environment:
|
||||
name: homebrew-core
|
||||
url: https://github.com/homebrew/homebrew-core
|
||||
|
|
@ -351,7 +367,7 @@ jobs:
|
|||
|
||||
publish-to-winget:
|
||||
name: Publish to winget
|
||||
if: ${{ !needs.pre-build.outputs.prerelease }}
|
||||
if: ${{ !inputs.dry-run && !needs.pre-build.outputs.prerelease }}
|
||||
needs: [ pre-build, publish-to-github ]
|
||||
|
||||
uses: vrc-get/vrc-get/.github/workflows/publish-cli-winget.yml@master
|
||||
|
|
|
|||
|
|
@ -8,30 +8,79 @@ The format is based on [Keep a Changelog].
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Build-time option to disable auto updater `#2759`
|
||||
- Please read README for new build instruction.
|
||||
- Implement project sorting by creation date `#2941`
|
||||
|
||||
### Changed
|
||||
- File filled with '\0' or whitespace will be treated as empty file `#2710`
|
||||
- This should prevent `syntax error loading settings.json: expected value at line 1 column 1` if settings.json is broken
|
||||
- Completely changed how do we build ALCOM and how do we self-update ALCOM `#2759`
|
||||
- This fixes few problems relates to auto update
|
||||
- Please read README for new build instruction.
|
||||
- Improved backup speed by parallelizing the process [`#2746`](https://github.com/vrc-get/vrc-get/pull/2746)
|
||||
- Along with this change, the default compression level has been changed to `zip-fast`
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
- Fixed an issue where the progress bar flickered and did not display correct progress in environments using WebKit as the renderer. [`#2641`](https://github.com/vrc-get/vrc-get/pull/2641)
|
||||
- Fails to import UnityPackages with files in `Packages` directory `#2679`
|
||||
- null as vpmDependencies value is not allowed `#2709`
|
||||
- It's not recommended, but we allow null for `vpmDependencies` as a alias of `{}`
|
||||
|
||||
### Security
|
||||
|
||||
## [1.1.6] - 2026-06-02
|
||||
### Added
|
||||
- The package list can show hidden packages. [`#2731`](https://github.com/vrc-get/vrc-get/pull/2731)
|
||||
- Build-time option to disable auto updater [`#2759`](https://github.com/vrc-get/vrc-get/pull/2759)
|
||||
- Please read README for new build instruction.
|
||||
- User repositories can now be reordered by drag and drop [`#2935`](https://github.com/vrc-get/vrc-get/pull/2935)
|
||||
|
||||
### Changed
|
||||
- The "Clear Selection" button in the package management screen is now red (destructive style) to distinguish it from the "Install Selected" button [`#2803`](https://github.com/vrc-get/vrc-get/pull/2803)
|
||||
- File filled with '\0' or whitespace will be treated as empty file [`#2710`](https://github.com/vrc-get/vrc-get/pull/2710)
|
||||
- This should prevent `syntax error loading settings.json: expected value at line 1 column 1` if settings.json is broken
|
||||
- We also added a backup file to recover from settings.json corruption [`#2933`](https://github.com/vrc-get/vrc-get/pull/2933)
|
||||
- Completely changed how do we build ALCOM and how do we self-update ALCOM [`#2759`](https://github.com/vrc-get/vrc-get/pull/2759) [`#2828`](https://github.com/vrc-get/vrc-get/pull/2828) [`#2881`](https://github.com/vrc-get/vrc-get/pull/2881) [`#2882`](https://github.com/vrc-get/vrc-get/pull/2882) [`#2885`](https://github.com/vrc-get/vrc-get/pull/2885)
|
||||
- This fixes few problems relates to auto update
|
||||
- Please read README for new build instruction.
|
||||
- Improved backup speed by parallelizing the process [`#2746`](https://github.com/vrc-get/vrc-get/pull/2746)
|
||||
- Along with this change, the default compression level has been changed to `zip-fast`
|
||||
- We added dialog on enabling "Show Prerelease Packages" [`#2795`](https://github.com/vrc-get/vrc-get/pull/2795)
|
||||
- I hope this prevents users unexpectedly adding prerelease packages
|
||||
- Path for unitypackage on Template Editor now can be reselected [`#2635`](https://github.com/vrc-get/vrc-get/pull/2635)
|
||||
- ALCOM now refuses launching project if project is on noexec mount points [`#2814`](https://github.com/vrc-get/vrc-get/pull/2814)
|
||||
- This would cause problems with several native plugins
|
||||
- Already-added packages are now excluded from the package name suggestions in the Template Editor [`#2828`](https://github.com/vrc-get/vrc-get/pull/2828)
|
||||
- Extended some timeouts to 1 minute [`#2826`](https://github.com/vrc-get/vrc-get/pull/2826)
|
||||
- Prevents timeouts in slow DNS environments
|
||||
- Improved robustness for package installation errors [`#2844`](https://github.com/vrc-get/vrc-get/pull/2844)
|
||||
- It is now unlikely that vrc-get will leave the project directory corrupted if an I/O error occurs while installing a package
|
||||
- Backslashes in path in zip file are now treated as path separator on unix [`#2845`](https://github.com/vrc-get/vrc-get/pull/2845)
|
||||
- This fixes problem with Gesture Manager 3.9.7
|
||||
- Empty string for `documentationUrl` and `changelogUrl` are now allowed and ignored [`#2930`](https://github.com/vrc-get/vrc-get/pull/2930)
|
||||
- They are formerly rejected as invalid url
|
||||
|
||||
### Fixed
|
||||
- Fixed an issue where the progress bar flickered and did not display correct progress in environments using WebKit as the renderer. [`#2641`](https://github.com/vrc-get/vrc-get/pull/2641)
|
||||
- Fails to import UnityPackages with files in `Packages` directory [`#2679`](https://github.com/vrc-get/vrc-get/pull/2679)
|
||||
- null as vpmDependencies value is not allowed [`#2709`](https://github.com/vrc-get/vrc-get/pull/2709)
|
||||
- It's not recommended, but we allow null for `vpmDependencies` as a alias of `{}`
|
||||
- ALCOM cannot detect per-user flatpak installation of unity hub [`#2812`](https://github.com/vrc-get/vrc-get/pull/2812)
|
||||
- Unabled to import some untypackages [`#2821`](https://github.com/vrc-get/vrc-get/pull/2821)
|
||||
- It's hard to say but some older unitypackages ware unsupported.
|
||||
- Panic when resolving projects where dependency packages depend on newer versions of locked packages [`#2822`](https://github.com/vrc-get/vrc-get/pull/2822)
|
||||
- Missing glibc and libgcc_s dependency notation in .deb / .rpm distributon [`#2828`](https://github.com/vrc-get/vrc-get/pull/2828)
|
||||
- Unclear error message for invalid version name or version range [`#2842`](https://github.com/vrc-get/vrc-get/pull/2842)
|
||||
- Default file names in save dialogs now include the appropriate file extension [`#2846`](https://github.com/vrc-get/vrc-get/pull/2846)
|
||||
- Template export now defaults to `{template name}.alcomtemplate`
|
||||
- Repository list export now defaults to `repositories.txt`
|
||||
- Uninformative `[object Object]` appearing as an error message [`#2848`](https://github.com/vrc-get/vrc-get/pull/2848)
|
||||
- New Unity Hub loading method may not load manually added Unity Editors [`#2850`](https://github.com/vrc-get/vrc-get/pull/2850)
|
||||
- New Unity Hub loading method does load unity hub configuration on Linux [`#2850`](https://github.com/vrc-get/vrc-get/pull/2850)
|
||||
- Too many open files when copying project `#2867
|
||||
- Added workaround for VRCDefaultWorldScene generation issue in SDK 3.10.2 or later [`#2916`](https://github.com/vrc-get/vrc-get/pull/2916)
|
||||
- See [this][default-scene-canny] canny for bug in VRCSDK and issue [#2913][issue-2913] for our decision.
|
||||
|
||||
### Security
|
||||
- Package hash checks are now enforced when installing packages [`#2849`](https://github.com/vrc-get/vrc-get/pull/2849)
|
||||
- It has been about two years since the error message for package hash mismatches was introduced.
|
||||
- It is now enforced for security.
|
||||
|
||||
[default-scene-canny]: https://feedback.vrchat.com/sdk-bug-reports/p/3102-3103-vrcscenetemplateinitializer-does-not-create-sample-scene-if-udon-prepr
|
||||
[issue-2913]: https://github.com/vrc-get/vrc-get/issues/2913
|
||||
|
||||
## [1.1.5] - 2025-11-16
|
||||
- Fix package version selector dropdown exceeding window height [`#2589`](https://github.com/vrc-get/vrc-get/pull/2589)
|
||||
- The dropdown list now has a maximum height of 50% of the viewport or 24rem, whichever is smaller
|
||||
|
|
@ -654,7 +703,8 @@ Release pipeline fixes
|
|||
- 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-v1.1.5...HEAD
|
||||
[Unreleased]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.6...HEAD
|
||||
[1.1.6]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.5...gui-v1.1.6
|
||||
[1.1.5]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.4...gui-v1.1.5
|
||||
[1.1.4]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.3...gui-v1.1.4
|
||||
[1.1.3]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.2...gui-v1.1.3
|
||||
|
|
|
|||
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -15,6 +15,10 @@ The format is based on [Keep a Changelog].
|
|||
- This should reduce losing settings after crashing ALCOM or PC.
|
||||
- null as vpmDependencies value is not allowed `#2709`
|
||||
- It's not recommended, but we allow null for `vpmDependencies` as a alias of `{}`
|
||||
- Improved robustness for package installation errors `#2844`
|
||||
- It is now unlikely that vrc-get will leave the project directory corrupted if an I/O error occurs while installing a package
|
||||
- Backslashes in path in zip file are now treated as path separator on unix `#2845`
|
||||
- This fixes problem with Gesture Manager 3.9.7
|
||||
|
||||
### Deprecated
|
||||
|
||||
|
|
@ -22,8 +26,16 @@ The format is based on [Keep a Changelog].
|
|||
|
||||
### Fixed
|
||||
- Fix 'Detected Loop' panic with valid database file `#2607`
|
||||
- Panic when resolving projects where dependency packages depend on newer versions of locked packages `#2822`
|
||||
- Warning for backup/project path in AppData folder not shown when path is in Roaming or LocalLow [`#2827`](https://github.com/vrc-get/vrc-get/pull/2827)
|
||||
- Unclear error message for invalid version name or version range `#2842`
|
||||
- Empty string for `documentationUrl` and `changelogUrl` are now allowed and ignored `#2930`
|
||||
- They are formerly rejected as invalid url
|
||||
|
||||
### Security
|
||||
- Package hash checks are now enforced when installing packages `#2849`
|
||||
- It has been about two years since the error message for package hash mismatches was introduced.
|
||||
- It is now enforced for security.
|
||||
|
||||
## [1.9.1] - 2025-07-28
|
||||
### Changed
|
||||
|
|
|
|||
1540
Cargo.lock
generated
1540
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vrc-get-gui"
|
||||
version = "1.1.6-beta.1"
|
||||
version = "1.1.7-beta.0"
|
||||
description = "A fast open-source alternative of VRChat Creator Companion"
|
||||
|
||||
homepage.workspace = true
|
||||
|
|
@ -22,12 +22,12 @@ tauri-build = { version = "2", features = [ "config-toml" ] }
|
|||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_with = { version = "3", features = ["base64"] }
|
||||
tauri = { version = "=2.10.3", features = [ "config-toml" ] } # = for sync version between npm and cargo
|
||||
tauri = { version = "=2.11.2", features = [ "config-toml" ] } # = for sync version between npm and cargo
|
||||
vrc-get-vpm = { path = "../vrc-get-vpm", features = ["experimental-project-management", "experimental-unity-management"] }
|
||||
reqwest = { version = "0.12", features = ["gzip", "brotli", "json"] }
|
||||
specta = { version = "2.0.0-rc.20", features = [ "chrono", "url", "indexmap" ] }
|
||||
tauri-specta = { version = "2.0.0-rc.20", features = ["typescript"] }
|
||||
specta-typescript = "0.0.7"
|
||||
reqwest = { version = "0.13", features = ["gzip", "brotli", "json"] }
|
||||
specta = { version = "2.0.0-rc.24", features = [ "chrono", "url", "indexmap" ] }
|
||||
tauri-specta = { version = "2.0.0-rc.24", features = ["typescript"] }
|
||||
specta-typescript = "0.0.11"
|
||||
open = "5"
|
||||
arc-swap = "1"
|
||||
log = { version = "0.4", features = [ "std", "kv" ] }
|
||||
|
|
@ -61,11 +61,11 @@ yoke = { version = "0.8", features = ["derive"] }
|
|||
atomicbox = "0.4"
|
||||
stable_deref_trait = "1"
|
||||
itertools = "0.14"
|
||||
sysinfo = "0.38.4"
|
||||
sysinfo = "0.39.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_Storage_FileSystem", "Win32_System_IO", "Win32_NetworkManagement_IpHelper", "Wdk_System_SystemServices", "Win32_System_SystemInformation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
winreg = "0.55"
|
||||
winreg = "0.56"
|
||||
wmi = "0.18"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
|
@ -78,7 +78,7 @@ dispatch2 = "0.3.0"
|
|||
rlimit = "0.11.0"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.31", features = ["fs"] }
|
||||
nix = { version = "0.31", features = ["fs", "mount"] }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
|
|
|
|||
|
|
@ -6,15 +6,20 @@ import globalInfo from "@/lib/global-info";
|
|||
export default function ErrorPage({
|
||||
error,
|
||||
}: {
|
||||
error: Error;
|
||||
error: object;
|
||||
reset?: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
const errorMessage = `${error}`;
|
||||
const errorStack = `${error.stack}`;
|
||||
// When there is overridden toString, use it. if not, use stringify
|
||||
const errorMessage =
|
||||
error.toString === Object.prototype.toString
|
||||
? JSON.stringify(error)
|
||||
: error.toString();
|
||||
const errorStack =
|
||||
"stack" in error ? `${error.stack}` : "No stacktrace provided";
|
||||
|
||||
const openIssue = () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastError, toastInfo, toastNormal, toastSuccess } from "@/lib/toast";
|
||||
import {
|
||||
toastError,
|
||||
toastInfo,
|
||||
toastNormal,
|
||||
toastSuccess,
|
||||
toastWarning,
|
||||
} from "@/lib/toast";
|
||||
|
||||
export const Route = createFileRoute("/_main/dev-palette/")({
|
||||
component: Page,
|
||||
|
|
@ -113,6 +119,12 @@ function Page() {
|
|||
>
|
||||
Error
|
||||
</Button>
|
||||
<Button
|
||||
variant={"warning"}
|
||||
onClick={() => toastWarning("Warning Toast Body")}
|
||||
>
|
||||
Warning
|
||||
</Button>
|
||||
<Button
|
||||
variant={"success"}
|
||||
onClick={() => toastSuccess("Success Toast Body")}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,26 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
type CollisionDetection,
|
||||
closestCenter,
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
DragOverlay,
|
||||
type DragStartEvent,
|
||||
defaultDropAnimation,
|
||||
defaultDropAnimationSideEffects,
|
||||
type Modifier,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
|
|
@ -7,8 +28,16 @@ import {
|
|||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ChevronDown, CircleX } from "lucide-react";
|
||||
import { Suspense, useCallback, useEffect, useId, useMemo } from "react";
|
||||
import { ChevronDown, CircleX, GripVertical } from "lucide-react";
|
||||
import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -41,6 +70,8 @@ export const Route = createFileRoute("/_main/packages/repositories/")({
|
|||
component: Page,
|
||||
});
|
||||
|
||||
type UserRepoWithListId = TauriUserRepository & { listId: string };
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
|
|
@ -49,11 +80,99 @@ function Page() {
|
|||
);
|
||||
}
|
||||
|
||||
const restrictToVerticalAxis: Modifier = ({ transform }) => ({
|
||||
...transform,
|
||||
x: 0,
|
||||
});
|
||||
|
||||
const DRAG_OVERLAY_MODIFIERS = [restrictToVerticalAxis];
|
||||
|
||||
const customDropAnimation: typeof defaultDropAnimation = {
|
||||
...defaultDropAnimation,
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
styles: {
|
||||
active: { opacity: "0" },
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const TABLE_HEAD = [
|
||||
"", // checkbox
|
||||
"general:name",
|
||||
"vpm repositories:url",
|
||||
"", // actions
|
||||
"", // grip handle
|
||||
] as const;
|
||||
|
||||
const environmentRepositoriesInfo = queryOptions({
|
||||
queryKey: ["environmentRepositoriesInfo"],
|
||||
queryFn: commands.environmentRepositoriesInfo,
|
||||
});
|
||||
|
||||
// Scrolls the given viewport element when the pointer is near the top or bottom
|
||||
// edge during drag. dnd-kit's built-in autoscroll is disabled because it causes
|
||||
// jitter with Radix UI ScrollArea (wrong container detection + double-smoothing).
|
||||
function useDragAutoScroll(
|
||||
viewportRef: React.RefObject<HTMLElement | null>,
|
||||
isActive: boolean,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!isActive) return;
|
||||
|
||||
const THRESHOLD = 80; // px from edge to begin scrolling
|
||||
const MAX_SPEED = 15; // px/frame at the very edge
|
||||
|
||||
let pointerY = 0;
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
pointerY = e.clientY;
|
||||
};
|
||||
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
||||
|
||||
let rafId: number;
|
||||
const tick = () => {
|
||||
const viewport = viewportRef.current;
|
||||
if (viewport) {
|
||||
const { top, bottom } = viewport.getBoundingClientRect();
|
||||
const distFromTop = pointerY - top;
|
||||
const distFromBottom = bottom - pointerY;
|
||||
|
||||
let delta = 0;
|
||||
if (distFromTop >= 0 && distFromTop < THRESHOLD) {
|
||||
delta = -MAX_SPEED * (1 - distFromTop / THRESHOLD);
|
||||
} else if (distFromBottom >= 0 && distFromBottom < THRESHOLD) {
|
||||
delta = MAX_SPEED * (1 - distFromBottom / THRESHOLD);
|
||||
}
|
||||
|
||||
if (delta !== 0) {
|
||||
viewport.scrollTo({
|
||||
top: viewport.scrollTop + delta,
|
||||
behavior: "instant",
|
||||
});
|
||||
}
|
||||
}
|
||||
rafId = requestAnimationFrame(tick);
|
||||
};
|
||||
rafId = requestAnimationFrame(tick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointermove", onPointerMove);
|
||||
cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [isActive, viewportRef]);
|
||||
}
|
||||
|
||||
function computeSlotKey(repo: TauriUserRepository, used: Set<string>): string {
|
||||
const base = `${repo.id} ${repo.url ?? ""}`;
|
||||
let key = base;
|
||||
let counter = 0;
|
||||
while (used.has(key)) {
|
||||
counter++;
|
||||
key = `${base} ${counter}`;
|
||||
}
|
||||
used.add(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
function PageBody() {
|
||||
const result = useQuery(environmentRepositoriesInfo);
|
||||
|
||||
|
|
@ -95,140 +214,110 @@ function PageBody() {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const bodyAnimation = usePrevPathName().startsWith("/packages")
|
||||
? "slide-right"
|
||||
: "";
|
||||
const guiAnimation = useQuery({
|
||||
queryKey: ["environmentGuiAnimation"],
|
||||
queryFn: commands.environmentGuiAnimation,
|
||||
initialData: true,
|
||||
}).data;
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<HNavBar
|
||||
className="shrink-0"
|
||||
leading={<HeadingPageName pageType={"/packages/repositories"} />}
|
||||
trailing={
|
||||
<DropdownMenu>
|
||||
<div className={"flex divide-x"}>
|
||||
<Button
|
||||
className={"rounded-r-none compact:h-10"}
|
||||
onClick={() => openAddRepositoryDialog()}
|
||||
>
|
||||
{tc("vpm repositories:button:add repository")}
|
||||
</Button>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={"rounded-l-none pl-2 pr-2 compact:h-10"}
|
||||
>
|
||||
<Button>
|
||||
<ChevronDown className={"w-4 h-4"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => importRepositoriesMutation.mutate()}
|
||||
>
|
||||
{tc("vpm repositories:button:import repositories")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => exportRepositories.mutate()}>
|
||||
{tc("vpm repositories:button:export repositories")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
<main
|
||||
className={`shrink overflow-hidden flex w-full h-full ${bodyAnimation}`}
|
||||
>
|
||||
<ScrollableCardTable className={"h-full w-full"}>
|
||||
<RepositoryTableBody
|
||||
userRepos={result.data?.user_repositories || []}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
/>
|
||||
</ScrollableCardTable>
|
||||
</main>
|
||||
</VStack>
|
||||
const userRepos = result.data?.user_repositories;
|
||||
|
||||
const listIdMapRef = useRef<Map<string, string>>(new Map());
|
||||
|
||||
const augmentedUserRepos = useMemo<UserRepoWithListId[]>(() => {
|
||||
if (!userRepos) {
|
||||
listIdMapRef.current = new Map();
|
||||
return [];
|
||||
}
|
||||
const prev = listIdMapRef.current;
|
||||
const next = new Map<string, string>();
|
||||
const usedKeys = new Set<string>();
|
||||
const result: UserRepoWithListId[] = [];
|
||||
|
||||
for (const r of userRepos) {
|
||||
const key = computeSlotKey(r, usedKeys);
|
||||
const listId = prev.get(key) ?? crypto.randomUUID();
|
||||
next.set(key, listId);
|
||||
result.push({ ...r, listId });
|
||||
}
|
||||
|
||||
listIdMapRef.current = next;
|
||||
return result;
|
||||
}, [userRepos]);
|
||||
|
||||
const [orderedListIds, setOrderedListIds] = useState<string[]>(() =>
|
||||
augmentedUserRepos.map((r) => r.listId),
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryTableBody({
|
||||
userRepos,
|
||||
hiddenUserRepos,
|
||||
}: {
|
||||
userRepos: TauriUserRepository[];
|
||||
hiddenUserRepos: Set<string>;
|
||||
}) {
|
||||
const TABLE_HEAD = [
|
||||
"", // checkbox
|
||||
"general:name",
|
||||
"vpm repositories:url",
|
||||
"", // actions
|
||||
];
|
||||
useEffect(() => {
|
||||
setOrderedListIds(augmentedUserRepos.map((r) => r.listId));
|
||||
}, [augmentedUserRepos]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<thead>
|
||||
<tr>
|
||||
{TABLE_HEAD.map((head, index) => (
|
||||
<th
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static array
|
||||
key={index}
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground px-2.5 py-1.5"
|
||||
}
|
||||
>
|
||||
<small className="font-normal leading-none">{tc(head)}</small>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<RepositoryRow
|
||||
repoId={"com.vrchat.repos.official"}
|
||||
url={"https://packages.vrchat.com/official?download"}
|
||||
displayName={tt("vpm repositories:source:official")}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
canRemove={false}
|
||||
/>
|
||||
<RepositoryRow
|
||||
repoId={"com.vrchat.repos.curated"}
|
||||
url={"https://packages.vrchat.com/curated?download"}
|
||||
displayName={tt("vpm repositories:source:curated")}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
className={"border-b border-primary/10"}
|
||||
canRemove={false}
|
||||
/>
|
||||
{userRepos.map((repo) => (
|
||||
<RepositoryRow
|
||||
key={repo.id}
|
||||
repoId={repo.id}
|
||||
displayName={repo.display_name}
|
||||
url={repo.url}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
const userRepoByListId = useMemo(
|
||||
() => new Map(augmentedUserRepos.map((r) => [r.listId, r])),
|
||||
[augmentedUserRepos],
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryRow({
|
||||
repoId,
|
||||
displayName,
|
||||
url,
|
||||
hiddenUserRepos,
|
||||
className,
|
||||
canRemove = true,
|
||||
}: {
|
||||
repoId: TauriUserRepository["id"];
|
||||
displayName: TauriUserRepository["display_name"];
|
||||
url: TauriUserRepository["url"];
|
||||
hiddenUserRepos: Set<string>;
|
||||
className?: string;
|
||||
canRemove?: boolean;
|
||||
}) {
|
||||
const cellClass = "p-2.5 compact:py-1";
|
||||
const id = useId();
|
||||
const userRepoByListIdRef =
|
||||
useRef<Map<string, UserRepoWithListId>>(userRepoByListId);
|
||||
useEffect(() => {
|
||||
userRepoByListIdRef.current = userRepoByListId;
|
||||
}, [userRepoByListId]);
|
||||
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [overId, setOverId] = useState<string | null>(null);
|
||||
const [columnWidths, setColumnWidths] = useState<number[]>([]);
|
||||
const theadRowRef = useRef<HTMLTableRowElement>(null);
|
||||
const scrollViewportRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const orderedListIdsSet = useMemo(
|
||||
() => new Set(orderedListIds),
|
||||
[orderedListIds],
|
||||
);
|
||||
|
||||
const collisionDetection = useCallback<CollisionDetection>(
|
||||
(args) =>
|
||||
closestCenter({
|
||||
...args,
|
||||
droppableContainers: args.droppableContainers.filter((c) =>
|
||||
orderedListIdsSet.has(c.id as string),
|
||||
),
|
||||
}),
|
||||
[orderedListIdsSet],
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const reorderMutation = useMutation({
|
||||
mutationFn: (listIds: string[]) => {
|
||||
const repos = listIds
|
||||
.map((lid) => userRepoByListId.get(lid))
|
||||
.filter((r): r is UserRepoWithListId => r !== undefined)
|
||||
.map((r) => ({ index: r.index, id: r.id }));
|
||||
return commands.environmentReorderRepositories(repos);
|
||||
},
|
||||
// Pin listIds to the new positions so duplicate-keyed rows don't swap their listIds on refetch.
|
||||
onMutate: (newListIds: string[]) => {
|
||||
const prevMap = new Map(listIdMapRef.current);
|
||||
const rebuilt = new Map<string, string>();
|
||||
const usedKeys = new Set<string>();
|
||||
for (const lid of newListIds) {
|
||||
const repo = userRepoByListIdRef.current.get(lid);
|
||||
if (!repo) continue;
|
||||
const key = computeSlotKey(repo, usedKeys);
|
||||
rebuilt.set(key, lid);
|
||||
}
|
||||
listIdMapRef.current = rebuilt;
|
||||
return { prevMap };
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries(environmentRepositoriesInfo),
|
||||
onError: (e, _newListIds, ctx) => {
|
||||
if (ctx?.prevMap) listIdMapRef.current = ctx.prevMap;
|
||||
toastThrownError(e);
|
||||
},
|
||||
});
|
||||
|
||||
const setHideRepository = useMutation({
|
||||
mutationFn: async ({ id, shown }: { id: string; shown: boolean }) => {
|
||||
if (shown) {
|
||||
|
|
@ -273,72 +362,483 @@ function RepositoryRow({
|
|||
},
|
||||
});
|
||||
|
||||
const activeVisualIndex = useMemo(() => {
|
||||
if (!activeId) return 0;
|
||||
const effectiveId = overId ?? activeId;
|
||||
return orderedListIds.indexOf(effectiveId) + 2; // +2 for the 2 fixed rows
|
||||
}, [activeId, overId, orderedListIds]);
|
||||
|
||||
function handleDragStart(event: DragStartEvent) {
|
||||
setActiveId(event.active.id as string);
|
||||
if (theadRowRef.current) {
|
||||
const widths = Array.from(
|
||||
theadRowRef.current.querySelectorAll("th"),
|
||||
(th) => th.getBoundingClientRect().width,
|
||||
);
|
||||
setColumnWidths(widths);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver(event: DragOverEvent) {
|
||||
setOverId((event.over?.id as string | null) ?? null);
|
||||
}
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
setActiveId(null);
|
||||
setOverId(null);
|
||||
const { active, over } = event;
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = orderedListIds.indexOf(active.id as string);
|
||||
const newIndex = orderedListIds.indexOf(over.id as string);
|
||||
const newListIds = arrayMove(orderedListIds, oldIndex, newIndex);
|
||||
setOrderedListIds(newListIds);
|
||||
reorderMutation.mutate(newListIds);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragCancel() {
|
||||
setActiveId(null);
|
||||
setOverId(null);
|
||||
}
|
||||
|
||||
useDragAutoScroll(scrollViewportRef, activeId !== null);
|
||||
|
||||
const bodyAnimation = usePrevPathName().startsWith("/packages")
|
||||
? "slide-right"
|
||||
: "";
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={collisionDetection}
|
||||
autoScroll={false}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<VStack>
|
||||
<div style={activeId !== null ? { pointerEvents: "none" } : undefined}>
|
||||
<HNavBar
|
||||
className="shrink-0"
|
||||
leading={<HeadingPageName pageType={"/packages/repositories"} />}
|
||||
trailing={
|
||||
<DropdownMenu>
|
||||
<div className={"flex divide-x"}>
|
||||
<Button
|
||||
className={"rounded-r-none compact:h-10"}
|
||||
onClick={() => openAddRepositoryDialog()}
|
||||
>
|
||||
{tc("vpm repositories:button:add repository")}
|
||||
</Button>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={"rounded-l-none pl-2 pr-2 compact:h-10"}
|
||||
>
|
||||
<Button>
|
||||
<ChevronDown className={"w-4 h-4"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => importRepositoriesMutation.mutate()}
|
||||
>
|
||||
{tc("vpm repositories:button:import repositories")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => exportRepositories.mutate()}>
|
||||
{tc("vpm repositories:button:export repositories")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<main
|
||||
className={`shrink overflow-hidden flex w-full h-full ${bodyAnimation}`}
|
||||
>
|
||||
<ScrollableCardTable
|
||||
className={"h-full w-full"}
|
||||
viewportRef={scrollViewportRef}
|
||||
>
|
||||
<RepositoryTableBody
|
||||
orderedListIds={orderedListIds}
|
||||
userRepoByListId={userRepoByListId}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
theadRowRef={theadRowRef}
|
||||
guiAnimation={guiAnimation}
|
||||
onToggleVisibility={(id, shown) =>
|
||||
setHideRepository.mutate({ id, shown })
|
||||
}
|
||||
isDragActive={activeId !== null}
|
||||
/>
|
||||
</ScrollableCardTable>
|
||||
</main>
|
||||
</VStack>
|
||||
<DragOverlay
|
||||
modifiers={DRAG_OVERLAY_MODIFIERS}
|
||||
dropAnimation={guiAnimation ? customDropAnimation : null}
|
||||
>
|
||||
{activeId ? (
|
||||
<RepositoryDragOverlay
|
||||
repo={userRepoByListId.get(activeId)}
|
||||
selected={
|
||||
!hiddenUserRepos.has(userRepoByListId.get(activeId)?.id ?? "")
|
||||
}
|
||||
columnWidths={columnWidths}
|
||||
visualIndex={activeVisualIndex}
|
||||
guiAnimation={guiAnimation}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryTableBody({
|
||||
orderedListIds,
|
||||
userRepoByListId,
|
||||
hiddenUserRepos,
|
||||
theadRowRef,
|
||||
guiAnimation,
|
||||
onToggleVisibility,
|
||||
isDragActive,
|
||||
}: {
|
||||
orderedListIds: string[];
|
||||
userRepoByListId: Map<string, UserRepoWithListId>;
|
||||
hiddenUserRepos: Set<string>;
|
||||
theadRowRef: React.RefObject<HTMLTableRowElement | null>;
|
||||
guiAnimation: boolean;
|
||||
onToggleVisibility: (id: string, shown: boolean) => void;
|
||||
isDragActive: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<thead>
|
||||
<tr ref={theadRowRef}>
|
||||
{TABLE_HEAD.map((head, index) => (
|
||||
<th
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static array
|
||||
key={index}
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground px-2.5 py-1.5"
|
||||
}
|
||||
>
|
||||
<small className="font-normal leading-none">{tc(head)}</small>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<RepositoryRow
|
||||
repoId={"com.vrchat.repos.official"}
|
||||
url={"https://packages.vrchat.com/official?download"}
|
||||
displayName={tt("vpm repositories:source:official")}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
canRemove={false}
|
||||
rowIndex={0}
|
||||
guiAnimation={guiAnimation}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
<RepositoryRow
|
||||
repoId={"com.vrchat.repos.curated"}
|
||||
url={"https://packages.vrchat.com/curated?download"}
|
||||
displayName={tt("vpm repositories:source:curated")}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
className={"border-b border-primary/10"}
|
||||
canRemove={false}
|
||||
rowIndex={1}
|
||||
guiAnimation={guiAnimation}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
<SortableContext
|
||||
items={orderedListIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{orderedListIds.map((listId, index) => {
|
||||
const repo = userRepoByListId.get(listId);
|
||||
if (!repo) return null;
|
||||
return (
|
||||
<RepositoryRow
|
||||
key={listId}
|
||||
listId={listId}
|
||||
repoId={repo.id}
|
||||
repoIndex={repo.index}
|
||||
displayName={repo.display_name}
|
||||
url={repo.url}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
rowIndex={2 + index}
|
||||
guiAnimation={guiAnimation}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const CELL_CLASS = "p-2.5 compact:py-1 align-middle";
|
||||
|
||||
function RepositoryRowCells({
|
||||
labelId,
|
||||
displayName,
|
||||
url,
|
||||
canRemove,
|
||||
selected,
|
||||
onCheckedChange,
|
||||
onRemove,
|
||||
dragListeners,
|
||||
dragAttributes,
|
||||
}: {
|
||||
labelId?: string;
|
||||
displayName: string;
|
||||
url: string | null | undefined;
|
||||
canRemove: boolean;
|
||||
selected: boolean;
|
||||
onCheckedChange?: (shown: boolean) => void;
|
||||
onRemove?: () => void;
|
||||
dragListeners?: ReturnType<typeof useSortable>["listeners"];
|
||||
dragAttributes?: ReturnType<typeof useSortable>["attributes"];
|
||||
}) {
|
||||
const interactive = onCheckedChange !== undefined;
|
||||
return (
|
||||
<>
|
||||
<td className={CELL_CLASS}>
|
||||
{interactive ? (
|
||||
<div className="flex">
|
||||
<Checkbox
|
||||
id={labelId}
|
||||
checked={selected}
|
||||
onCheckedChange={(x) => onCheckedChange(x === true)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pointer-events-none flex">
|
||||
<Checkbox checked={selected} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className={CELL_CLASS}>
|
||||
{interactive ? (
|
||||
<label htmlFor={labelId}>
|
||||
<p className="font-normal">{displayName}</p>
|
||||
</label>
|
||||
) : (
|
||||
<p className="font-normal">{displayName}</p>
|
||||
)}
|
||||
</td>
|
||||
<td className={CELL_CLASS}>
|
||||
<p className="font-normal">{url}</p>
|
||||
</td>
|
||||
<td className={`${CELL_CLASS} w-0`}>
|
||||
{interactive ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={canRemove}>
|
||||
<Button
|
||||
disabled={!canRemove}
|
||||
onClick={onRemove}
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
>
|
||||
<CircleX className={"size-5 text-destructive"} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{canRemove
|
||||
? tc("vpm repositories:remove repository")
|
||||
: tc(
|
||||
"vpm repositories:tooltip:remove curated or official repository",
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button variant={"ghost"} size={"icon"} disabled>
|
||||
<CircleX className={"size-5 text-destructive"} />
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
className={cn(
|
||||
CELL_CLASS,
|
||||
"w-0",
|
||||
canRemove ? "cursor-move" : "cursor-not-allowed",
|
||||
)}
|
||||
{...(canRemove ? dragListeners : undefined)}
|
||||
{...(canRemove ? dragAttributes : undefined)}
|
||||
>
|
||||
<GripVertical
|
||||
className={cn(
|
||||
"size-5 text-muted-foreground",
|
||||
!canRemove && "opacity-50",
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryRow({
|
||||
listId,
|
||||
repoId,
|
||||
repoIndex,
|
||||
displayName,
|
||||
url,
|
||||
hiddenUserRepos,
|
||||
className,
|
||||
canRemove = true,
|
||||
rowIndex,
|
||||
guiAnimation,
|
||||
onToggleVisibility,
|
||||
isDragActive,
|
||||
}: {
|
||||
listId?: string;
|
||||
repoId: TauriUserRepository["id"];
|
||||
repoIndex?: number;
|
||||
displayName: TauriUserRepository["display_name"];
|
||||
url: TauriUserRepository["url"];
|
||||
hiddenUserRepos: Set<string>;
|
||||
className?: string;
|
||||
canRemove?: boolean;
|
||||
rowIndex: number;
|
||||
guiAnimation: boolean;
|
||||
onToggleVisibility: (id: string, shown: boolean) => void;
|
||||
isDragActive: boolean;
|
||||
}) {
|
||||
const labelId = useId();
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: listId ?? repoId, disabled: !canRemove });
|
||||
|
||||
const visualIndex = useMemo(() => {
|
||||
if (isDragging) return rowIndex;
|
||||
const dy = transform?.y ?? 0;
|
||||
if (dy < 0) return rowIndex - 1;
|
||||
if (dy > 0) return rowIndex + 1;
|
||||
return rowIndex;
|
||||
}, [rowIndex, transform?.y, isDragging]);
|
||||
|
||||
const dragStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
transform: transform ? `translateY(${transform.y}px)` : undefined,
|
||||
transition: guiAnimation
|
||||
? [transition, isDragActive ? undefined : "background-color 200ms ease"]
|
||||
.filter(Boolean)
|
||||
.join(", ") || undefined
|
||||
: undefined,
|
||||
opacity: isDragging ? 0 : 1,
|
||||
position: "relative",
|
||||
}),
|
||||
[transform, transition, isDragging, guiAnimation, isDragActive],
|
||||
);
|
||||
|
||||
const selected = !hiddenUserRepos.has(repoId);
|
||||
|
||||
return (
|
||||
<tr className={cn("even:bg-secondary/30", className)}>
|
||||
<td className={cellClass}>
|
||||
<Checkbox
|
||||
id={id}
|
||||
checked={selected}
|
||||
onCheckedChange={(x) =>
|
||||
setHideRepository.mutate({ id: repoId, shown: x === true })
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className={cellClass}>
|
||||
<label htmlFor={id}>
|
||||
<p className="font-normal">{displayName}</p>
|
||||
</label>
|
||||
</td>
|
||||
<td className={cellClass}>
|
||||
<p className="font-normal">{url}</p>
|
||||
</td>
|
||||
<td className={`${cellClass} w-0`}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={canRemove}>
|
||||
<Button
|
||||
disabled={!canRemove}
|
||||
onClick={() => {
|
||||
void openSingleDialog(RemoveRepositoryDialog, {
|
||||
displayName,
|
||||
id: repoId,
|
||||
});
|
||||
}}
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
>
|
||||
<CircleX className={"size-5 text-destructive"} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{canRemove
|
||||
? tc("vpm repositories:remove repository")
|
||||
: tc(
|
||||
"vpm repositories:tooltip:remove curated or official repository",
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<tr
|
||||
ref={setNodeRef}
|
||||
style={dragStyle}
|
||||
className={cn(visualIndex % 2 === 1 ? "bg-secondary/30" : "", className)}
|
||||
>
|
||||
<RepositoryRowCells
|
||||
labelId={labelId}
|
||||
displayName={displayName}
|
||||
url={url}
|
||||
canRemove={canRemove}
|
||||
selected={selected}
|
||||
onCheckedChange={(shown) => onToggleVisibility(repoId, shown)}
|
||||
onRemove={() =>
|
||||
void openSingleDialog(RemoveRepositoryDialog, {
|
||||
displayName,
|
||||
index: repoIndex ?? 0,
|
||||
id: repoId,
|
||||
})
|
||||
}
|
||||
dragListeners={listeners}
|
||||
dragAttributes={attributes}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryDragOverlay({
|
||||
repo,
|
||||
selected,
|
||||
columnWidths,
|
||||
visualIndex,
|
||||
guiAnimation,
|
||||
}: {
|
||||
repo: TauriUserRepository | undefined;
|
||||
selected: boolean;
|
||||
columnWidths: number[];
|
||||
visualIndex: number;
|
||||
guiAnimation: boolean;
|
||||
}) {
|
||||
const style = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
transition: guiAnimation ? "background-color 200ms ease" : undefined,
|
||||
}),
|
||||
[guiAnimation],
|
||||
);
|
||||
|
||||
if (!repo) return null;
|
||||
return (
|
||||
<table
|
||||
className={cn(
|
||||
"w-full table-fixed text-left",
|
||||
visualIndex % 2 === 1 ? "bg-secondary/30" : "",
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{columnWidths.length > 0 && (
|
||||
<colgroup>
|
||||
{columnWidths.map((w, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: fixed column order
|
||||
<col key={i} style={{ width: w }} />
|
||||
))}
|
||||
</colgroup>
|
||||
)}
|
||||
<tbody>
|
||||
<tr>
|
||||
<RepositoryRowCells
|
||||
displayName={repo.display_name}
|
||||
url={repo.url}
|
||||
canRemove={true}
|
||||
selected={selected}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function RemoveRepositoryDialog({
|
||||
dialog,
|
||||
displayName,
|
||||
index,
|
||||
id,
|
||||
}: {
|
||||
dialog: DialogContext<void>;
|
||||
displayName: string;
|
||||
index: number;
|
||||
id: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const removeRepository = useMutation({
|
||||
mutationFn: async (id: string) =>
|
||||
await commands.environmentRemoveRepository(id),
|
||||
onMutate: async (id) => {
|
||||
mutationFn: async (args: { index: number; id: string }) =>
|
||||
await commands.environmentRemoveRepository(args.index, args.id),
|
||||
onMutate: async ({ index }) => {
|
||||
await queryClient.cancelQueries(environmentRepositoriesInfo);
|
||||
const data = queryClient.getQueryData(
|
||||
environmentRepositoriesInfo.queryKey,
|
||||
|
|
@ -346,10 +846,18 @@ function RemoveRepositoryDialog({
|
|||
if (data !== undefined) {
|
||||
queryClient.setQueryData(environmentRepositoriesInfo.queryKey, {
|
||||
...data,
|
||||
user_repositories: data.user_repositories.filter((x) => x.id !== id),
|
||||
user_repositories: data.user_repositories.filter(
|
||||
(x) => x.index !== index,
|
||||
),
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
onError: (e, _args, ctx) => {
|
||||
queryClient.setQueryData(environmentRepositoriesInfo.queryKey, ctx);
|
||||
toastThrownError(e);
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries(environmentRepositoriesInfo),
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -369,7 +877,7 @@ function RemoveRepositoryDialog({
|
|||
<Button
|
||||
onClick={() => {
|
||||
dialog.close();
|
||||
removeRepository.mutate(id);
|
||||
removeRepository.mutate({ index, id });
|
||||
}}
|
||||
className={"ml-2"}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ import type React from "react";
|
|||
import { Suspense, useId, useMemo, useState } from "react";
|
||||
import { HeadingPageName } from "@/app/_main/packages/-tab-selector";
|
||||
import Loading from "@/app/-loading";
|
||||
import { FilePathRow } from "@/components/common-setting-parts";
|
||||
import { FavoriteStarToggleButton } from "@/components/FavoriteStarButton";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { Overlay } from "@/components/Overlay";
|
||||
import {
|
||||
ReorderableList,
|
||||
type ReorderableListId,
|
||||
useReorderableList,
|
||||
} from "@/components/ReorderableList";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
|
|
@ -38,6 +40,7 @@ import {
|
|||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import {
|
||||
commands,
|
||||
type TauriAlcomTemplate,
|
||||
|
|
@ -46,7 +49,7 @@ import {
|
|||
} from "@/lib/bindings";
|
||||
import { dateToString, formatDateOffset } from "@/lib/dateToString";
|
||||
import { type DialogContext, openSingleDialog } from "@/lib/dialog";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { processResult } from "@/lib/import-templates";
|
||||
import { usePrevPathName } from "@/lib/prev-page";
|
||||
import {
|
||||
|
|
@ -55,7 +58,7 @@ import {
|
|||
projectTemplateDisplayId,
|
||||
projectTemplateName,
|
||||
} from "@/lib/project-template";
|
||||
import { toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { compareVersion } from "@/lib/version";
|
||||
|
||||
|
|
@ -738,6 +741,11 @@ function TemplateEditor({
|
|||
reorderable: false,
|
||||
});
|
||||
|
||||
const addedPackageNames = useMemo(
|
||||
() => new Set(packagesListContext.value.map((p) => p.name)),
|
||||
[packagesListContext.value],
|
||||
);
|
||||
|
||||
const unityPackagesListContext = useReorderableList<string>({
|
||||
defaultValue: "",
|
||||
defaultArray: template?.unity_packages ?? [],
|
||||
|
|
@ -748,7 +756,7 @@ function TemplateEditor({
|
|||
|
||||
const addUnityPackages = async () => {
|
||||
try {
|
||||
const packages = await commands.environmentPickUnityPackage();
|
||||
const packages = await commands.environmentPickUnityPackages();
|
||||
for (const pkg of packages) {
|
||||
unityPackagesListContext.add(pkg);
|
||||
}
|
||||
|
|
@ -758,6 +766,31 @@ function TemplateEditor({
|
|||
}
|
||||
};
|
||||
|
||||
const pickUnityPackage = async (
|
||||
currentValue: string,
|
||||
currentId: ReorderableListId,
|
||||
) => {
|
||||
try {
|
||||
const result = await commands.environmentPickUnityPackage(currentValue);
|
||||
switch (result.type) {
|
||||
case "NoFolderSelected":
|
||||
// no-op
|
||||
break;
|
||||
case "InvalidSelection":
|
||||
toastError(tt("general:toast:invalid file"));
|
||||
break;
|
||||
case "Successful":
|
||||
unityPackagesListContext.update(currentId, result.new_path);
|
||||
break;
|
||||
default:
|
||||
assertNever(result);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toastThrownError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const saveTemplate = async () => {
|
||||
try {
|
||||
|
|
@ -882,7 +915,11 @@ function TemplateEditor({
|
|||
<Autocomplete
|
||||
value={value.name}
|
||||
className={"grow"}
|
||||
options={packageCandidates}
|
||||
options={packageCandidates.filter(
|
||||
(c) =>
|
||||
c.value === value.name ||
|
||||
!addedPackageNames.has(c.value),
|
||||
)}
|
||||
onChange={(value) =>
|
||||
packagesListContext.update(id, (old) => ({
|
||||
...old,
|
||||
|
|
@ -941,16 +978,13 @@ function TemplateEditor({
|
|||
{tc("templates:dialog:no unitypackages")}
|
||||
</td>
|
||||
)}
|
||||
renderItem={(value) => (
|
||||
renderItem={(value, id) => (
|
||||
<td>
|
||||
<div className={"flex"}>
|
||||
<Input
|
||||
type={"text"}
|
||||
value={value}
|
||||
className={"grow"}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<FilePathRow
|
||||
path={value}
|
||||
pick={() => pickUnityPackage(value, id).finally()}
|
||||
withOpen={false}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -316,8 +316,10 @@ function UnityVersion({
|
|||
function CreatingProject() {
|
||||
return (
|
||||
<DialogBase>
|
||||
<RefreshCw className={"w-5 h-5 animate-spin"} />
|
||||
<p>{tc("projects:creating project...")}</p>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<RefreshCw className={"w-5 h-5 animate-spin"} />
|
||||
<p>{tc("projects:creating project...")}</p>
|
||||
</div>
|
||||
</DialogBase>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@ import {
|
|||
} from "@/components/ui/tooltip";
|
||||
import type { TauriProject } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { dateToString, formatDateOffset } from "@/lib/dateToString";
|
||||
import {
|
||||
dateToString,
|
||||
dayToString,
|
||||
formatDateOffset,
|
||||
} from "@/lib/dateToString";
|
||||
import { openSingleDialog } from "@/lib/dialog";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastThrownError } from "@/lib/toast";
|
||||
|
|
@ -45,7 +49,7 @@ export function ProjectGridItem({
|
|||
|
||||
const typeIconClass = "w-5 h-5";
|
||||
|
||||
const { projectTypeKind, displayType, isLegacy, lastModified } =
|
||||
const { projectTypeKind, displayType, isLegacy, createdAt, lastModified } =
|
||||
getProjectDisplayInfo(project);
|
||||
|
||||
const removed = !project.is_exists;
|
||||
|
|
@ -165,22 +169,44 @@ export function ProjectGridItem({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{tc("general:last modified")}:{" "}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<time dateTime={lastModified.toISOString()}>
|
||||
<time className="font-normal">
|
||||
{formatDateOffset(project.last_modified)}
|
||||
<div className="flex flex-row gap-1">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{tc("general:created at")}:{" "}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<time dateTime={createdAt.toISOString()}>
|
||||
<time className="font-normal">
|
||||
{dayToString(project.created_at)}
|
||||
</time>
|
||||
</time>
|
||||
</time>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.last_modified)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.created_at)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">/</p>
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{tc("general:last modified")}:{" "}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<time dateTime={lastModified.toISOString()}>
|
||||
<time className="font-normal">
|
||||
{formatDateOffset(project.last_modified)}
|
||||
</time>
|
||||
</time>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.last_modified)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex flex-wrap gap-2 justify-end compact:gap-1">
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@ import {
|
|||
import { assertNever } from "@/lib/assert-never";
|
||||
import type { TauriProject, TauriProjectType } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { dateToString, formatDateOffset } from "@/lib/dateToString";
|
||||
import {
|
||||
dateToString,
|
||||
dayToString,
|
||||
formatDateOffset,
|
||||
} from "@/lib/dateToString";
|
||||
import { type DialogContext, openSingleDialog, showDialog } from "@/lib/dialog";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { router } from "@/lib/main";
|
||||
|
|
@ -78,7 +82,7 @@ export function ProjectRow({
|
|||
const noGrowCellClass = `${cellClass} w-1`;
|
||||
const typeIconClass = "w-5 h-5";
|
||||
|
||||
const { projectTypeKind, displayType, isLegacy, lastModified } =
|
||||
const { projectTypeKind, displayType, isLegacy, createdAt, lastModified } =
|
||||
getProjectDisplayInfo(project);
|
||||
|
||||
const openProjectFolder = () =>
|
||||
|
|
@ -105,7 +109,7 @@ export function ProjectRow({
|
|||
<tr
|
||||
className={`group even:bg-secondary/30 ${removed || loading || !(project.is_valid ?? true) ? "opacity-50" : ""}`}
|
||||
>
|
||||
<td className={`${cellClass} w-3`}>
|
||||
<td className={noGrowCellClass}>
|
||||
<div className={"relative flex"}>
|
||||
<FavoriteStarToggleButton
|
||||
favorite={project.favorite}
|
||||
|
|
@ -147,7 +151,7 @@ export function ProjectRow({
|
|||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className={`${cellClass} w-[8em] min-w-[8em]`}>
|
||||
<td className={noGrowCellClass}>
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="flex items-center">
|
||||
{projectTypeKind === "avatars" ? (
|
||||
|
|
@ -171,6 +175,22 @@ export function ProjectRow({
|
|||
<td className={noGrowCellClass}>
|
||||
<p className="font-normal">{project.unity}</p>
|
||||
</td>
|
||||
<td className={noGrowCellClass}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<time dateTime={createdAt.toISOString()}>
|
||||
<time className="font-normal">
|
||||
{dayToString(project.created_at)}
|
||||
</time>
|
||||
</time>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.created_at)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className={noGrowCellClass}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
|
|
@ -501,12 +521,14 @@ export function getProjectDisplayInfo(project: TauriProject) {
|
|||
const projectTypeKind = ProjectDisplayType[project.project_type] ?? "unknown";
|
||||
const displayType = tc(`projects:type:${projectTypeKind}`);
|
||||
const isLegacy = LegacyProjectTypes.includes(project.project_type);
|
||||
const createdAt = new Date(project.created_at);
|
||||
const lastModified = new Date(project.last_modified);
|
||||
|
||||
return {
|
||||
projectTypeKind,
|
||||
displayType,
|
||||
isLegacy,
|
||||
createdAt,
|
||||
lastModified,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const sortingOptions: { key: SimpleSorting; label: string }[] = [
|
|||
{ key: "name", label: "general:name" },
|
||||
{ key: "type", label: "projects:type" },
|
||||
{ key: "unity", label: "projects:unity" },
|
||||
{ key: "createdAt", label: "general:created at" },
|
||||
{ key: "lastModified", label: "general:last modified" },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@ import { toastThrownError } from "@/lib/toast";
|
|||
import { compareUnityVersionString } from "@/lib/version";
|
||||
import { ProjectRow } from "./-project-row";
|
||||
|
||||
export const sortings = ["lastModified", "name", "unity", "type"] as const;
|
||||
export const sortings = [
|
||||
"createdAt",
|
||||
"lastModified",
|
||||
"name",
|
||||
"unity",
|
||||
"type",
|
||||
] as const;
|
||||
|
||||
type SimpleSorting = (typeof sortings)[number];
|
||||
type Sorting = SimpleSorting | `${SimpleSorting}Reversed`;
|
||||
|
|
@ -158,6 +164,18 @@ export function ProjectsTableCard({
|
|||
</small>
|
||||
</button>
|
||||
</th>
|
||||
<th className={`${thClass} ${headerBg("createdAt")}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={"flex w-full project-table-button"}
|
||||
onClick={() => setSorting("createdAt")}
|
||||
>
|
||||
{icon("createdAt")}
|
||||
<small className="font-normal leading-none">
|
||||
{tc("general:created at")}
|
||||
</small>
|
||||
</button>
|
||||
</th>
|
||||
<th className={`${thClass} ${headerBg("lastModified")}`}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -194,6 +212,12 @@ export function sortSearchProjects(
|
|||
searched.sort((a, b) => b.last_modified - a.last_modified);
|
||||
|
||||
switch (sorting) {
|
||||
case "createdAt":
|
||||
searched.sort((a, b) => b.created_at - a.created_at);
|
||||
break;
|
||||
case "createdAtReversed":
|
||||
searched.sort((a, b) => a.created_at - b.created_at);
|
||||
break;
|
||||
case "lastModified":
|
||||
searched.sort((a, b) => b.last_modified - a.last_modified);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export interface PackageRowInfo {
|
|||
unityIncompatible: Map<string, TauriPackage>;
|
||||
sources: Set<string>;
|
||||
isThereSource: boolean; // this will be true even if all sources are hidden
|
||||
visibleSources: Set<string>;
|
||||
installed: null | {
|
||||
version: TauriVersion;
|
||||
yanked: boolean;
|
||||
|
|
@ -92,7 +93,9 @@ export function combinePackagesAndProjectDetails(
|
|||
const yankedVersions = new Set<`${string}:${string}`>();
|
||||
const knownPackages = new Set<string>();
|
||||
const packagesPerRepository = new Map<string, TauriPackage[]>();
|
||||
const hiddenPackagesPerRepository = new Map<string, TauriPackage[]>();
|
||||
const userPackages: TauriPackage[] = [];
|
||||
const hiddenUserPackages: TauriPackage[] = [];
|
||||
|
||||
for (const pkg of packages) {
|
||||
if (!showPrereleasePackages && pkg.version.pre) continue;
|
||||
|
|
@ -107,13 +110,19 @@ export function combinePackagesAndProjectDetails(
|
|||
let packages: TauriPackage[];
|
||||
// check the repository is visible
|
||||
if (pkg.source === "LocalUser") {
|
||||
if (hideLocalUserPackages) continue;
|
||||
packages = userPackages;
|
||||
if (hideLocalUserPackages) {
|
||||
packages = hiddenUserPackages;
|
||||
} else {
|
||||
packages = userPackages;
|
||||
}
|
||||
} else if ("Remote" in pkg.source) {
|
||||
if (hiddenRepositoriesSet.has(pkg.source.Remote.id)) continue;
|
||||
|
||||
packages = packagesPerRepository.get(pkg.source.Remote.id) ?? [];
|
||||
packagesPerRepository.set(pkg.source.Remote.id, packages);
|
||||
if (hiddenRepositoriesSet.has(pkg.source.Remote.id)) {
|
||||
packages = hiddenPackagesPerRepository.get(pkg.source.Remote.id) ?? [];
|
||||
hiddenPackagesPerRepository.set(pkg.source.Remote.id, packages);
|
||||
} else {
|
||||
packages = packagesPerRepository.get(pkg.source.Remote.id) ?? [];
|
||||
packagesPerRepository.set(pkg.source.Remote.id, packages);
|
||||
}
|
||||
} else {
|
||||
assertNever(pkg.source);
|
||||
}
|
||||
|
|
@ -138,6 +147,7 @@ export function combinePackagesAndProjectDetails(
|
|||
unityIncompatible: new Map(),
|
||||
sources: new Set(),
|
||||
isThereSource: false,
|
||||
visibleSources: new Set(),
|
||||
installed: null,
|
||||
latest: { status: "none" },
|
||||
stableLatest: { status: "none" },
|
||||
|
|
@ -178,8 +188,14 @@ export function combinePackagesAndProjectDetails(
|
|||
|
||||
if (pkg.source === "LocalUser") {
|
||||
packageRowInfo.sources.add("User");
|
||||
if (!hideLocalUserPackages) {
|
||||
packageRowInfo.visibleSources.add("User");
|
||||
}
|
||||
} else if ("Remote" in pkg.source) {
|
||||
packageRowInfo.sources.add(pkg.source.Remote.display_name);
|
||||
if (!hiddenRepositoriesSet.has(pkg.source.Remote.id)) {
|
||||
packageRowInfo.visibleSources.add(pkg.source.Remote.display_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +203,13 @@ export function combinePackagesAndProjectDetails(
|
|||
packagesPerRepository.get("com.vrchat.repos.official")?.forEach(addPackage);
|
||||
packagesPerRepository.get("com.vrchat.repos.curated")?.forEach(addPackage);
|
||||
userPackages.forEach(addPackage);
|
||||
hiddenUserPackages.forEach((pkg) => {
|
||||
const packageRowInfo = getRowInfo(pkg);
|
||||
packageRowInfo.isThereSource = true;
|
||||
if (pkg.source === "LocalUser") {
|
||||
packageRowInfo.sources.add("User");
|
||||
}
|
||||
});
|
||||
packagesPerRepository.delete("com.vrchat.repos.official");
|
||||
packagesPerRepository.delete("com.vrchat.repos.curated");
|
||||
|
||||
|
|
@ -201,6 +224,17 @@ export function combinePackagesAndProjectDetails(
|
|||
packages.forEach(addPackage);
|
||||
}
|
||||
|
||||
// process hidden repositories - only add to sources, not to version calculations
|
||||
for (const packages of hiddenPackagesPerRepository.values()) {
|
||||
packages.forEach((pkg) => {
|
||||
const packageRowInfo = getRowInfo(pkg);
|
||||
packageRowInfo.isThereSource = true;
|
||||
if (pkg.source !== "LocalUser") {
|
||||
packageRowInfo.sources.add(pkg.source.Remote.display_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// sort versions
|
||||
for (const value of packagesTable.values()) {
|
||||
value.unityCompatible = new Map(
|
||||
|
|
@ -299,7 +333,7 @@ export function combinePackagesAndProjectDetails(
|
|||
pkg.is_yanked ||
|
||||
yankedVersions.has(`${pkg.name}:${toVersionString(pkg.version)}`),
|
||||
};
|
||||
packageRowInfo.isThereSource = knownPackages.has(pkg.name);
|
||||
packageRowInfo.isThereSource = true;
|
||||
|
||||
// if we have the latest version, check if it's upgradable
|
||||
if (packageRowInfo.latest.status !== "none") {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleArrowUp,
|
||||
CircleMinus,
|
||||
CirclePlus,
|
||||
|
|
@ -28,6 +30,7 @@ import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
|||
import { SearchBox } from "@/components/SearchBox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
|
|
@ -55,6 +58,7 @@ import {
|
|||
import { assertNever } from "@/lib/assert-never";
|
||||
import type { TauriPackage, TauriRepositoriesInfo } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { type DialogContext, openSingleDialog } from "@/lib/dialog";
|
||||
import { isFindKey, useDocumentEvent } from "@/lib/events";
|
||||
import { usePackageUpdateInProgress } from "@/lib/global-events";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
|
|
@ -89,6 +93,7 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
const [bulkUpdatePackageIdsRaw, setBulkUpdatePackageIds] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [showHiddenPackages, setShowHiddenPackages] = useState(false);
|
||||
|
||||
const bulkUpdatePackageIds = useMemo(() => {
|
||||
const packageIds = new Set(packageRowsData.map((p) => p.id));
|
||||
|
|
@ -132,6 +137,22 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
);
|
||||
}, [packageRowsData, search]);
|
||||
|
||||
const hiddenPackages = useMemo(() => {
|
||||
return packageRowsData.filter(
|
||||
(pkg) =>
|
||||
pkg.visibleSources.size === 0 && pkg.isThereSource && !pkg.installed,
|
||||
);
|
||||
}, [packageRowsData]);
|
||||
|
||||
const visibleHiddenPackagesCount = useMemo(() => {
|
||||
return hiddenPackages.filter((pkg) => filteredPackageIds.has(pkg.id))
|
||||
.length;
|
||||
}, [hiddenPackages, filteredPackageIds]);
|
||||
|
||||
const toggleShowHiddenPackages = useCallback(() => {
|
||||
setShowHiddenPackages((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const hiddenUserRepositories = useMemo(
|
||||
() => new Set(repositoriesInfo?.hidden_user_repositories ?? []),
|
||||
[repositoriesInfo],
|
||||
|
|
@ -224,26 +245,79 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{packageRowsData.map((row) => (
|
||||
<tr
|
||||
className="even:bg-secondary/30 anchor-none"
|
||||
hidden={!filteredPackageIds.has(row.id)}
|
||||
key={row.id}
|
||||
>
|
||||
<PackageRow
|
||||
pkg={row}
|
||||
bulkUpdateSelected={bulkUpdatePackageIds.some(
|
||||
(id) => id === row.id,
|
||||
)}
|
||||
bulkUpdateAvailable={canBulkUpdate(
|
||||
bulkUpdateMode,
|
||||
bulkUpdateModeForPackage(row),
|
||||
)}
|
||||
addBulkUpdatePackage={addBulkUpdatePackage}
|
||||
removeBulkUpdatePackage={removeBulkUpdatePackage}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
{packageRowsData.map((row) => {
|
||||
if (
|
||||
row.visibleSources.size === 0 &&
|
||||
row.isThereSource &&
|
||||
!row.installed
|
||||
)
|
||||
return null;
|
||||
return (
|
||||
<tr
|
||||
className="even:bg-secondary/30 anchor-none"
|
||||
hidden={!filteredPackageIds.has(row.id)}
|
||||
key={row.id}
|
||||
>
|
||||
<PackageRow
|
||||
pkg={row}
|
||||
bulkUpdateSelected={bulkUpdatePackageIds.some(
|
||||
(id) => id === row.id,
|
||||
)}
|
||||
bulkUpdateAvailable={canBulkUpdate(
|
||||
bulkUpdateMode,
|
||||
bulkUpdateModeForPackage(row),
|
||||
)}
|
||||
addBulkUpdatePackage={addBulkUpdatePackage}
|
||||
removeBulkUpdatePackage={removeBulkUpdatePackage}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{/* Hidden packages section */}
|
||||
{hiddenPackages.length > 0 && (
|
||||
<>
|
||||
<tr
|
||||
className="bg-secondary/50 hover:bg-secondary/70 cursor-pointer"
|
||||
onClick={toggleShowHiddenPackages}
|
||||
>
|
||||
<td className="p-3.5 compact:py-1 w-1">
|
||||
{showHiddenPackages ? (
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
colSpan={TABLE_HEAD.length + 1}
|
||||
className="p-3.5 compact:py-1 font-medium text-sm text-muted-foreground"
|
||||
>
|
||||
{tc("projects:manage:hidden packages")} (
|
||||
{visibleHiddenPackagesCount})
|
||||
</td>
|
||||
</tr>
|
||||
{showHiddenPackages &&
|
||||
hiddenPackages.map((row) => (
|
||||
<tr
|
||||
className="even:bg-secondary/30 anchor-none"
|
||||
hidden={!filteredPackageIds.has(row.id)}
|
||||
key={row.id}
|
||||
>
|
||||
<PackageRow
|
||||
pkg={row}
|
||||
bulkUpdateSelected={bulkUpdatePackageIds.some(
|
||||
(id) => id === row.id,
|
||||
)}
|
||||
bulkUpdateAvailable={canBulkUpdate(
|
||||
bulkUpdateMode,
|
||||
bulkUpdateModeForPackage(row),
|
||||
)}
|
||||
addBulkUpdatePackage={addBulkUpdatePackage}
|
||||
removeBulkUpdatePackage={removeBulkUpdatePackage}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</ScrollableCardTable>
|
||||
</CardContent>
|
||||
|
|
@ -252,6 +326,29 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
);
|
||||
});
|
||||
|
||||
function ShowPrereleaseConfirmDialog({
|
||||
dialog,
|
||||
}: {
|
||||
dialog: DialogContext<boolean>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>
|
||||
{tc("settings:dialog:show prerelease packages")}
|
||||
</DialogTitle>
|
||||
<div>{tc("settings:dialog:show prerelease packages description")}</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(false)}>
|
||||
{tc("general:button:cancel")}
|
||||
</Button>
|
||||
<Button variant="warning" onClick={() => dialog.close(true)}>
|
||||
{tc("settings:dialog:enable show prerelease packages")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ManagePackagesHeading({
|
||||
packageRowsData,
|
||||
hiddenUserRepositories,
|
||||
|
|
@ -467,9 +564,21 @@ function ManagePackagesHeading({
|
|||
checked={repositoriesInfo?.show_prerelease_packages}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowPrereleasePackages.mutate(
|
||||
!repositoriesInfo?.show_prerelease_packages,
|
||||
);
|
||||
const newValue = !repositoriesInfo?.show_prerelease_packages;
|
||||
if (newValue) {
|
||||
void openSingleDialog(ShowPrereleaseConfirmDialog, {})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
setShowPrereleasePackages.mutate(true);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toastThrownError(e);
|
||||
});
|
||||
} else {
|
||||
setShowPrereleasePackages.mutate(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tc("settings:show prerelease")}
|
||||
|
|
@ -624,7 +733,7 @@ function BulkUpdateCard({
|
|||
return (
|
||||
<Card
|
||||
className={
|
||||
"shrink-0 p-2 flex flex-row gap-2 bg-secondary text-secondary-foreground flex-wrap"
|
||||
"shrink-0 p-2 compact:p-1 flex flex-row gap-2 compact:gap-1 bg-secondary text-secondary-foreground flex-wrap"
|
||||
}
|
||||
>
|
||||
{bulkUpdateMode.canInstallOrUpgrade && (
|
||||
|
|
@ -654,7 +763,7 @@ function BulkUpdateCard({
|
|||
{tc("projects:manage:button:uninstall selected")}
|
||||
</ButtonDisabledIfLoading>
|
||||
)}
|
||||
<ButtonDisabledIfLoading onClick={cancel}>
|
||||
<ButtonDisabledIfLoading onClick={cancel} variant={"warning"}>
|
||||
{tc("projects:manage:button:clear selection")}
|
||||
{" ("}
|
||||
{tc("projects:manage:n packages selected", { count })}
|
||||
|
|
@ -846,7 +955,7 @@ const PackageRow = memo(function PackageRow({
|
|||
return (
|
||||
<>
|
||||
<td className={`${cellClass} w-1 compact:px-2`}>
|
||||
<div className={"flex content-center aspect-square"}>
|
||||
<div className={"flex items-center justify-center aspect-square"}>
|
||||
<CheckboxDisabledIfLoading
|
||||
checked={bulkUpdateSelected}
|
||||
onCheckedChange={onClickBulkUpdate}
|
||||
|
|
@ -884,27 +993,29 @@ const PackageRow = memo(function PackageRow({
|
|||
<LatestPackageInfo info={pkg.latest} />
|
||||
</td>
|
||||
<td className={`${noGrowCellClass} max-w-32 overflow-hidden`}>
|
||||
{pkg.sources.size === 0 ? (
|
||||
{pkg.visibleSources.size === 0 ? (
|
||||
pkg.isThereSource ? (
|
||||
<p>{tc("projects:manage:source not selected")}</p>
|
||||
) : (
|
||||
<p>{tc("projects:manage:none")}</p>
|
||||
)
|
||||
) : pkg.sources.size === 1 ? (
|
||||
) : pkg.visibleSources.size === 1 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p className="overflow-hidden text-ellipsis">
|
||||
{[...pkg.sources][0]}
|
||||
{[...pkg.visibleSources][0]}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{[...pkg.sources][0]}</TooltipContent>
|
||||
<TooltipContent>{[...pkg.visibleSources][0]}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p>{tc("projects:manage:multiple sources")}</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{[...pkg.sources].join(", ")}</TooltipContent>
|
||||
<TooltipContent>
|
||||
{[...pkg.visibleSources].join(", ")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
|
|
@ -941,9 +1052,24 @@ const PackageRow = memo(function PackageRow({
|
|||
</ButtonDisabledIfLoading>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{!latestVersion
|
||||
? tc("projects:manage:tooltip:incompatible with unity")
|
||||
: tc("projects:manage:tooltip:add package")}
|
||||
{pkg.visibleSources.size === 0 && pkg.isThereSource ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p>
|
||||
{tc(
|
||||
"projects:manage:tooltip:select repository to install",
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs opacity-75">
|
||||
{[...pkg.sources]
|
||||
.filter((source) => !pkg.visibleSources.has(source))
|
||||
.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
) : !latestVersion ? (
|
||||
tc("projects:manage:tooltip:incompatible with unity")
|
||||
) : (
|
||||
tc("projects:manage:tooltip:add package")
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ function categorizeChange(
|
|||
change: TauriPackageChange,
|
||||
installedPackages: Map<string, TauriBasePackageInfo>,
|
||||
): PackageChangeDisplayInformation {
|
||||
if ("InstallNew" in change) {
|
||||
if (change.InstallNew !== undefined) {
|
||||
const name = change.InstallNew.display_name ?? change.InstallNew.name;
|
||||
|
||||
const installed = installedPackages.get(pkgId);
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
body {
|
||||
--toastify-font-family: var(--font-sans);
|
||||
--toastify-color-light: var(--background);
|
||||
/*--toastify-color-info: #3498db;*/
|
||||
|
|
@ -303,6 +303,17 @@ html {
|
|||
@apply pe-2.5;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add padding end for the content area of scrollable card if vertical scroll bar is visible
|
||||
* This prevents the table / items from being hidden behind the vertical scroll bar
|
||||
*/
|
||||
.vrc-get-scrollable-card:has(
|
||||
> .vrc-get-scrollable-card-vertical-bar
|
||||
) > div[data-radix-scroll-area-viewport]
|
||||
> div {
|
||||
@apply pe-2.5;
|
||||
}
|
||||
|
||||
.vrc-get-sidebar-hostname-warning-container {
|
||||
contain-intrinsic-size: 0 7em;
|
||||
contain: size;
|
||||
|
|
|
|||
70
vrc-get-gui/bundle/alcom.spec
Normal file
70
vrc-get-gui/bundle/alcom.spec
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
Name: alcom
|
||||
Version: 1.1.6
|
||||
Release: 1%{?dist}
|
||||
Summary: A short description of my custom application
|
||||
|
||||
%global git_version %(echo "%{version}" | tr '~' '-')
|
||||
|
||||
License: MIT
|
||||
URL: https://vrc-get.anatawa12.com/alcom/
|
||||
Source0: https://github.com/vrc-get/vrc-get/archive/gui-v%{git_version}.tar.gz
|
||||
|
||||
BuildRequires: gcc
|
||||
BuildRequires: nodejs
|
||||
BuildRequires: npm
|
||||
BuildRequires: pkgconfig(gtk+-3.0)
|
||||
BuildRequires: pkgconfig(webkit2gtk-4.1)
|
||||
BuildRequires: pkgconfig(openssl)
|
||||
|
||||
# we download rust toolchain manually when building inside mock container
|
||||
%if ! 0%{?install_rust:1}
|
||||
BuildRequires: cargo
|
||||
%endif
|
||||
|
||||
# disable stripping symbols.
|
||||
%global __os_install_post %{nil}
|
||||
%global debug_package %{nil}
|
||||
|
||||
%description
|
||||
ALCOM - Alternative Creator Companion
|
||||
ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri.
|
||||
|
||||
%prep
|
||||
%setup -q -n vrc-get-gui-v%{git_version}
|
||||
|
||||
%if 0%{?install_rust:1}
|
||||
echo "=== Mock environment detected. Installing isolated Rust toolchain ==="
|
||||
|
||||
export RUSTUP_HOME="$(pwd)/.rustup"
|
||||
export CARGO_HOME="$(pwd)/.cargo"
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
|
||||
|
||||
cat << EOF > ./load_rust_env.sh
|
||||
export RUSTUP_HOME="${RUSTUP_HOME}"
|
||||
export CARGO_HOME="${CARGO_HOME}"
|
||||
source "${CARGO_HOME}/env"
|
||||
EOF
|
||||
%endif
|
||||
|
||||
# marker: ci inserts version update here
|
||||
|
||||
%build
|
||||
%{?install_rust: source ./load_rust_env.sh}
|
||||
cargo xtask build-alcom --release
|
||||
|
||||
%install
|
||||
%{?install_rust: source ./load_rust_env.sh}
|
||||
rm -rf %{buildroot}
|
||||
cargo xtask bundle-alcom --release --bundles buildroot --buildroot=%{buildroot}
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
# %doc vrc-get-gui/README.md
|
||||
#%doc vrc-get-gui/CHANGELOG.md
|
||||
%{_bindir}/alcom
|
||||
%{_datadir}/applications/alcom.desktop
|
||||
%{_datadir}/icons/hicolor/*/apps/alcom.png
|
||||
|
||||
%changelog
|
||||
* Migrated to native rpm build pipeline with spec file
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
Package: alcom
|
||||
Version: {{version}}-1
|
||||
Architecture: {{arch}}
|
||||
Installed-Size: {{estimated_size}}
|
||||
Maintainer: anatawa12 <i@anatawa12.com>
|
||||
Priority: optional
|
||||
Homepage: https://vrc-get.anatawa12.com/alcom/
|
||||
Depends: libwebkit2gtk-4.1-0, libgtk-3-0
|
||||
Description: ALCOM - Alternative Creator Companion
|
||||
ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri.
|
||||
7
vrc-get-gui/bundle/debian/.gitignore
vendored
Normal file
7
vrc-get-gui/bundle/debian/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/*-build-stamp
|
||||
/*.substvars
|
||||
/.debhelper
|
||||
/files
|
||||
/cargo_home
|
||||
/npm_cache
|
||||
alcom/
|
||||
5
vrc-get-gui/bundle/debian/README.Debian
Normal file
5
vrc-get-gui/bundle/debian/README.Debian
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
alcom for Debian
|
||||
|
||||
ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri.
|
||||
|
||||
-- anatawa12 <i@anatawa12.com> Thu, 11 Jun 2026 14:34:57 +0000
|
||||
20
vrc-get-gui/bundle/debian/README.source
Normal file
20
vrc-get-gui/bundle/debian/README.source
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
alcom for Debian
|
||||
|
||||
This is README for alcom Debian package.
|
||||
|
||||
This Debian package is made to provide one of official distribution of alcom, formaly known as vrc-get-gui.
|
||||
Starting with 1.1.7 or later, alcom maintainer switched from custom .deb toolchain to official .deb toolchain.
|
||||
This directory contains source code of debian package as non-native package.
|
||||
|
||||
This directory is placed at vrc-get-gui/bundles/debian on original source tree, but before building debian package,
|
||||
you must copy vrc-get-gui/bundles/debian to debian.
|
||||
|
||||
Violating best practice of debian package, this package requires network access, to download cargo dependencies on build.
|
||||
In addition, by declaring `INSTALL_RUST` environment variable to `1` with `--set-envvar=INSTALL_RUST=1`, you can let
|
||||
build process to install newest available rust to build on older distribution does not provide new enough rust version.
|
||||
You also install NODEJS on build by declaring `INSTALL_NODEJS=1`.
|
||||
Those options are helpful when building inside sandbox like pbuilder.
|
||||
|
||||
This Debian package is based on package generated by debmake Version 4.5.1.
|
||||
|
||||
-- anatawa12 <i@anatawa12.com> Thu, 11 Jun 2026 14:34:57 +0000
|
||||
5
vrc-get-gui/bundle/debian/changelog
Normal file
5
vrc-get-gui/bundle/debian/changelog
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
alcom (1.1.6-1) UNRELEASED; urgency=low
|
||||
|
||||
* No changelog are provided for this package
|
||||
|
||||
-- root <i@anatawa12.com> Thu, 11 Jun 2026 14:34:57 +0000
|
||||
8
vrc-get-gui/bundle/debian/clean
Normal file
8
vrc-get-gui/bundle/debian/clean
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
target/
|
||||
vrc-get-gui/node_modules/
|
||||
vrc-get-gui/out/
|
||||
vrc-get-gui/gen/
|
||||
debian/cargo_home/
|
||||
debian/npm_cache/
|
||||
debian/rustup_home/
|
||||
debian/nodejs_installed/
|
||||
29
vrc-get-gui/bundle/debian/control
Normal file
29
vrc-get-gui/bundle/debian/control
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
Source: alcom
|
||||
Priority: optional
|
||||
Maintainer: anatawa12 <i@anatawa12.com>
|
||||
Build-Depends:
|
||||
debhelper-compat (= 13),
|
||||
libssl-dev,
|
||||
pkg-config,
|
||||
cargo,
|
||||
nodejs,
|
||||
npm,
|
||||
# curl for rust-install build only
|
||||
curl,
|
||||
libgtk-3-dev,
|
||||
libwebkit2gtk-4.1-dev (>= 2.41),
|
||||
Standards-Version: 4.7.0
|
||||
Homepage: https://vrc-get.anatawa12.com/alcom/
|
||||
Rules-Requires-Root: no
|
||||
|
||||
Package: alcom
|
||||
Architecture: any
|
||||
Multi-Arch: foreign
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
${shlibs:Depends},
|
||||
Description: ALCOM - Alternative Creator Companion
|
||||
ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri.
|
||||
.
|
||||
This package is one of official distribution of ALCOM, released as a part of the updates from ALCOM.
|
||||
No packaging only updates will be provided.
|
||||
41
vrc-get-gui/bundle/debian/copyright
Normal file
41
vrc-get-gui/bundle/debian/copyright
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: alcom
|
||||
Upstream-Contact: <preferred name and address to reach the upstream project>
|
||||
Source: <url://example.com>
|
||||
|
||||
Files: *
|
||||
Copyright: Copyright (c) 2023 anatawa12 and other contribcmeutors
|
||||
License: MIT
|
||||
MIT License
|
||||
.
|
||||
Copyright (c) 2023 anatawa12 and other contributors
|
||||
.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
.
|
||||
|
||||
Files: vrc-get-gui/third-party/Anton-Regular.ttf
|
||||
vrc-get-gui/third-party/NotoSans-Italic-VariableFont_wdth,wght.ttf
|
||||
vrc-get-gui/third-party/NotoSans-VariableFont_wdth,wght.ttf
|
||||
Copyright: 2020 The Anton Project Authors (https://github.com/googlefonts/AntonFont.git)
|
||||
2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||
License: SIL Open Font License 1.1
|
||||
|
||||
Files: vrc-get-gui/icons/*
|
||||
Copyright: 2024 lilxyzw, anatawa12 and other contributors
|
||||
License: CC-BY-4.0
|
||||
39
vrc-get-gui/bundle/debian/rules
Executable file
39
vrc-get-gui/bundle/debian/rules
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
export DEB_BUILD_OPTIONS += nostrip
|
||||
|
||||
export CARGO_HOME = $(CURDIR)/debian/cargo_home
|
||||
export NPM_CONFIG_CACHE = $(CURDIR)/debian/npm_cache
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
# install rust in configure phase when requested
|
||||
ifeq ($(INSTALL_RUST),1)
|
||||
export RUSTUP_HOME = $(CURDIR)/debian/rustup_home
|
||||
export PATH := $(CURDIR)/debian/cargo_home/bin:$(PATH)
|
||||
|
||||
override_dh_auto_configure::
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
|
||||
endif
|
||||
|
||||
ifeq ($(INSTALL_NODEJS),1)
|
||||
export RUSTUP_HOME = $(CURDIR)/debian/rustup_home
|
||||
export PATH := $(CURDIR)/debian/nodejs_installed/bin:$(PATH)
|
||||
|
||||
ifeq ($(DEB_HOST_ARCH),amd64)
|
||||
NODE_ARCH := x64
|
||||
else ifeq ($(DEB_HOST_ARCH),arm64)
|
||||
NODE_ARCH := arm64
|
||||
endif
|
||||
|
||||
override_dh_auto_configure::
|
||||
mkdir -p $(CURDIR)/debian/nodejs_installed
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://nodejs.org/dist/v26.3.0/node-v26.3.0-linux-$(NODE_ARCH).tar.gz | gunzip | tar x --strip-components 1 -C $(CURDIR)/debian/nodejs_installed
|
||||
endif
|
||||
|
||||
override_dh_auto_build:
|
||||
cargo xtask build-alcom --release
|
||||
|
||||
override_dh_auto_install:
|
||||
cargo xtask bundle-alcom --release --bundles buildroot --buildroot=$(CURDIR)/debian/alcom
|
||||
1
vrc-get-gui/bundle/debian/source/format
Normal file
1
vrc-get-gui/bundle/debian/source/format
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
||||
24
vrc-get-gui/bundle/debian/upstream/metadata
Normal file
24
vrc-get-gui/bundle/debian/upstream/metadata
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Metadata about the upstream project.
|
||||
# See https://wiki.debian.org/UpstreamMetadata
|
||||
|
||||
Bug-Database: https://github.com/vrc-get/vrc-get/issues
|
||||
Bug-Submit: https://github.com/vrc-get/vrc-get/issues/new
|
||||
Changelog: https://github.com/vrc-get/vrc-get/blob/master/vrc-get-gui/CHANGELOG.md
|
||||
Documentation: https://vrc-get.anatawa12.com/alcom/
|
||||
Repository-Browse: https://github.com/vrc-get/vrc-get
|
||||
Repository: https://github.com/vrc-get/vrc-get.git
|
||||
#FAQ: https://github.com/<user>/<project>/blob/main/FAQ.md
|
||||
Donation: https://github.com/sponsors/anatawa12
|
||||
#Registration: https://github.com/signup
|
||||
#Archive: PyPI # or CPAN, boost, etc.
|
||||
|
||||
# Uncomment these and fill them out to help users evaluate upstream:
|
||||
#Gallery: https://github.com/<user>/<project>/wiki/pictures-made-with-this-program
|
||||
#Screenshots: # pictures *of* the program, as opposed to pictures *made with* the program
|
||||
# - https://github.com/<user>/<project>/wiki/login-screen.png
|
||||
# - https://github.com/<user>/<project>/wiki/help-menu.png
|
||||
# - ...
|
||||
|
||||
# Uncomment these and fill them out to help resolve security issues:
|
||||
#Security-Contact: <how to send security-related messages>
|
||||
#CPE: <space-separated Common Platform Enumerator values - see https://wiki.debian.org/CPEtagPackagesDep>
|
||||
10
vrc-get-gui/bundle/debian/watch
Normal file
10
vrc-get-gui/bundle/debian/watch
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# You must remove unused comment lines for the released package.
|
||||
# Compulsory line, this is a version 4 file
|
||||
version=4
|
||||
opts=\
|
||||
filenamemangle=s%.*/@ANY_VERSION@%@PACKAGE@-$1.tar.gz%,\
|
||||
downloadurlmangle=s%(api.github.com/repos/[^/]+/[^/]+)/git/refs/%$1/tarball/refs/%g,\
|
||||
uversionmangle=s/(\d)[-]?((RC|rc|pre|dev|beta|alpha)\.\d*)$/$1~$2/,\
|
||||
searchmode=plain \
|
||||
https://api.github.com/repos/vrc-get/vrc-get/git/matching-refs/tags/gui- \
|
||||
https://api.github.com/repos/[^/]+/[^/]+/git/refs/tags/gui-@ANY_VERSION@
|
||||
|
|
@ -13,7 +13,7 @@ import { assertNever } from "@/lib/assert-never";
|
|||
const internalSymbol: unique symbol = Symbol("ReorderableListContextInternal");
|
||||
const idSymbol: unique symbol = Symbol("IdSymbol");
|
||||
|
||||
type Id = { [idSymbol]: number; toString: () => string };
|
||||
export type ReorderableListId = { [idSymbol]: number; toString: () => string };
|
||||
|
||||
type NonFunction =
|
||||
| string
|
||||
|
|
@ -25,9 +25,9 @@ type NonFunction =
|
|||
| bigint
|
||||
| object;
|
||||
|
||||
type AddOptions = { after: Id } | { before: Id };
|
||||
type AddOptions = { after: ReorderableListId } | { before: ReorderableListId };
|
||||
|
||||
type ReordeableListValue<T> = { id: Id; value: T };
|
||||
type ReordeableListValue<T> = { id: ReorderableListId; value: T };
|
||||
|
||||
type ReorderableListContextInternal<T> = {
|
||||
backedList: ReordeableListValue<T>[];
|
||||
|
|
@ -40,8 +40,8 @@ type ReorderableListContextInternal<T> = {
|
|||
export type ReorderableListContext<T> = {
|
||||
setList: Dispatch<SetStateAction<T[]>>;
|
||||
add: (value: T, options?: AddOptions) => void;
|
||||
remove: (id: Id) => void;
|
||||
update: (id: Id, action: SetStateAction<T>) => void;
|
||||
remove: (id: ReorderableListId) => void;
|
||||
update: (id: ReorderableListId, action: SetStateAction<T>) => void;
|
||||
get value(): T[];
|
||||
[internalSymbol]: ReorderableListContextInternal<T>;
|
||||
};
|
||||
|
|
@ -127,7 +127,7 @@ export function useReorderableList<T extends NonFunction>({
|
|||
}, []);
|
||||
|
||||
const remove = useCallback(
|
||||
(id: Id) => {
|
||||
(id: ReorderableListId) => {
|
||||
setBackedList((old) => {
|
||||
let list = old.filter(({ id: _id }) => _id !== id);
|
||||
if (list.length === 0 && !allowEmpty) list = [makeValue(defaultValue)];
|
||||
|
|
@ -137,26 +137,29 @@ export function useReorderableList<T extends NonFunction>({
|
|||
[allowEmpty, defaultValue],
|
||||
);
|
||||
|
||||
const update = useCallback((id: Id, action: SetStateAction<T>) => {
|
||||
if (typeof action === "function") {
|
||||
setBackedList((old) => {
|
||||
const idx = old.findIndex(({ id: _id }) => _id === id);
|
||||
if (idx === -1) return old;
|
||||
const newValue = action(old[idx].value);
|
||||
const newArray = [...old];
|
||||
newArray[idx] = { id, value: newValue };
|
||||
return newArray;
|
||||
});
|
||||
} else {
|
||||
setBackedList((old) => {
|
||||
const idx = old.findIndex(({ id: _id }) => _id === id);
|
||||
if (idx === -1) return old;
|
||||
const newArray = [...old];
|
||||
newArray[idx] = { id, value: action };
|
||||
return newArray;
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const update = useCallback(
|
||||
(id: ReorderableListId, action: SetStateAction<T>) => {
|
||||
if (typeof action === "function") {
|
||||
setBackedList((old) => {
|
||||
const idx = old.findIndex(({ id: _id }) => _id === id);
|
||||
if (idx === -1) return old;
|
||||
const newValue = action(old[idx].value);
|
||||
const newArray = [...old];
|
||||
newArray[idx] = { id, value: newValue };
|
||||
return newArray;
|
||||
});
|
||||
} else {
|
||||
setBackedList((old) => {
|
||||
const idx = old.findIndex(({ id: _id }) => _id === id);
|
||||
if (idx === -1) return old;
|
||||
const newArray = [...old];
|
||||
newArray[idx] = { id, value: action };
|
||||
return newArray;
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const swap = useCallback((index1: number, index2: number) => {
|
||||
setBackedList((old) => {
|
||||
|
|
@ -168,14 +171,15 @@ export function useReorderableList<T extends NonFunction>({
|
|||
});
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
return useMemo(() => {
|
||||
let valueCache: T[] | undefined;
|
||||
return {
|
||||
setList,
|
||||
add,
|
||||
update,
|
||||
remove,
|
||||
get value() {
|
||||
return backedList.map(({ value }) => value);
|
||||
return (valueCache ??= backedList.map(({ value }) => value));
|
||||
},
|
||||
[internalSymbol]: {
|
||||
backedList,
|
||||
|
|
@ -184,19 +188,18 @@ export function useReorderableList<T extends NonFunction>({
|
|||
reorderable,
|
||||
addable,
|
||||
},
|
||||
}),
|
||||
[
|
||||
setList,
|
||||
add,
|
||||
update,
|
||||
remove,
|
||||
backedList,
|
||||
defaultValue,
|
||||
swap,
|
||||
reorderable,
|
||||
addable,
|
||||
],
|
||||
);
|
||||
};
|
||||
}, [
|
||||
setList,
|
||||
add,
|
||||
update,
|
||||
remove,
|
||||
backedList,
|
||||
defaultValue,
|
||||
swap,
|
||||
reorderable,
|
||||
addable,
|
||||
]);
|
||||
}
|
||||
|
||||
export function ReorderableList<T>({
|
||||
|
|
@ -206,7 +209,7 @@ export function ReorderableList<T>({
|
|||
disabled,
|
||||
}: {
|
||||
context: ReorderableListContext<T>;
|
||||
renderItem: (value: T, id: Id) => React.ReactNode;
|
||||
renderItem: (value: T, id: ReorderableListId) => React.ReactNode;
|
||||
ifEmpty?: () => React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -286,10 +286,10 @@ export function ProjectPathWarnings({ projectPath }: { projectPath: string }) {
|
|||
const isWindows = globalInfo.osType === "WindowsNT";
|
||||
const hasNonAscii = isWindows && projectPath.match(/[^\x20-\x7F]/);
|
||||
const hasWhitespace = projectPath.includes(" ");
|
||||
const inLocalAppData = !!(
|
||||
const inAppData = !!(
|
||||
isWindows &&
|
||||
globalInfo.localAppData &&
|
||||
projectPath.includes(globalInfo.localAppData)
|
||||
globalInfo.appData &&
|
||||
projectPath.startsWith(globalInfo.appData)
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -300,10 +300,8 @@ export function ProjectPathWarnings({ projectPath }: { projectPath: string }) {
|
|||
{hasNonAscii && (
|
||||
<WarningMessage>{tc("settings:warning:non-ascii")}</WarningMessage>
|
||||
)}
|
||||
{inLocalAppData && (
|
||||
<WarningMessage>
|
||||
{tc("settings:warning:in-local-app-data")}
|
||||
</WarningMessage>
|
||||
{inAppData && (
|
||||
<WarningMessage>{tc("settings:warning:in-app-data")}</WarningMessage>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -312,18 +310,16 @@ export function ProjectPathWarnings({ projectPath }: { projectPath: string }) {
|
|||
export function BackupPathWarnings({ backupPath }: { backupPath: string }) {
|
||||
const globalInfo = useGlobalInfo();
|
||||
const isWindows = globalInfo.osType === "WindowsNT";
|
||||
const inLocalAppData = !!(
|
||||
const inAppData = !!(
|
||||
isWindows &&
|
||||
globalInfo.localAppData &&
|
||||
backupPath.includes(globalInfo.localAppData)
|
||||
globalInfo.appData &&
|
||||
backupPath.startsWith(globalInfo.appData)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{inLocalAppData && (
|
||||
<WarningMessage>
|
||||
{tc("settings:warning:in-local-app-data")}
|
||||
</WarningMessage>
|
||||
{inAppData && (
|
||||
<WarningMessage>{tc("settings:warning:in-app-data")}</WarningMessage>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import type React from "react";
|
||||
import { Suspense, useCallback, useEffect } from "react";
|
||||
import { Suspense, useCallback, useEffect, useEffectEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import Loading from "@/app/-loading";
|
||||
|
|
@ -16,19 +16,37 @@ import { isFindKey, useDocumentEvent } from "@/lib/events";
|
|||
import { tc } from "@/lib/i18n";
|
||||
import { processResult } from "@/lib/import-templates";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import {
|
||||
toastError,
|
||||
toastSuccess,
|
||||
toastThrownError,
|
||||
toastWarning,
|
||||
} from "@/lib/toast";
|
||||
import { useTauriListen } from "@/lib/use-tauri-listen";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useTauriListen<LogEntry>("log", (event) => {
|
||||
const entry = event.payload as LogEntry;
|
||||
if (entry.level === "Error" && entry.gui_toast) {
|
||||
const showToastForLog = useEffectEvent((entry: LogEntry) => {
|
||||
if (entry.level === "Error" && (entry.gui_toast ?? true)) {
|
||||
toastError(entry.message);
|
||||
} else if (entry.level === "Warn" && (entry.gui_toast ?? false)) {
|
||||
toastWarning(entry.message);
|
||||
}
|
||||
});
|
||||
|
||||
useTauriListen<LogEntry>("log", (event) => {
|
||||
showToastForLog(event.payload);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
commands.utilGetLogEntries().then((value) => {
|
||||
for (const entry of value) {
|
||||
showToastForLog(entry);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const moveToRepositories = useCallback(() => {
|
||||
if (location.pathname !== "/packages/repositories") {
|
||||
navigate({ to: "/packages/repositories" });
|
||||
|
|
|
|||
|
|
@ -49,4 +49,4 @@ const AccordionContent = ({
|
|||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ CardFooter.displayName = "CardFooter";
|
|||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -168,11 +168,11 @@ function CommandShortcut({
|
|||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -103,14 +103,14 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogOpen,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOpen,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -188,18 +188,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
|||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,4 +43,4 @@ function PopoverAnchor({
|
|||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
|
||||
|
|
|
|||
|
|
@ -142,13 +142,13 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ const TooltipPortal = TooltipPrimitive.Portal;
|
|||
|
||||
export {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipPortal,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,465 +1,464 @@
|
|||
// This file has been generated by Tauri Specta. Do not edit this file manually.
|
||||
|
||||
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||
|
||||
/** user-defined commands **/
|
||||
|
||||
import { invoke as __TAURI_INVOKE } from "@tauri-apps/api/core";
|
||||
|
||||
/** Commands */
|
||||
export const commands = {
|
||||
async environmentLanguage() : Promise<string> {
|
||||
return await TAURI_INVOKE("environment_language");
|
||||
},
|
||||
async environmentSetLanguage(language: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_language", { language });
|
||||
},
|
||||
async environmentTheme() : Promise<string> {
|
||||
return await TAURI_INVOKE("environment_theme");
|
||||
},
|
||||
async environmentSetTheme(theme: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_theme", { theme });
|
||||
},
|
||||
async environmentGetProjectSorting() : Promise<string> {
|
||||
return await TAURI_INVOKE("environment_get_project_sorting");
|
||||
},
|
||||
async environmentSetProjectSorting(sorting: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_project_sorting", { sorting });
|
||||
},
|
||||
async environmentGetFinishedSetupPages() : Promise<SetupPages[]> {
|
||||
return await TAURI_INVOKE("environment_get_finished_setup_pages");
|
||||
},
|
||||
async environmentFinishedSetupPage(page: SetupPages) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_finished_setup_page", { page });
|
||||
},
|
||||
async environmentClearSetupProcess() : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_clear_setup_process");
|
||||
},
|
||||
async environmentLogsLevel() : Promise<LogLevel[]> {
|
||||
return await TAURI_INVOKE("environment_logs_level");
|
||||
},
|
||||
async environmentSetLogsLevel(logsLevel: LogLevel[]) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_logs_level", { logsLevel });
|
||||
},
|
||||
async environmentGuiAnimation() : Promise<boolean> {
|
||||
return await TAURI_INVOKE("environment_gui_animation");
|
||||
},
|
||||
async environmentSetGuiAnimation(guiAnimation: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_gui_animation", { guiAnimation });
|
||||
},
|
||||
async environmentGuiCompact() : Promise<boolean> {
|
||||
return await TAURI_INVOKE("environment_gui_compact");
|
||||
},
|
||||
async environmentSetGuiCompact(guiCompact: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_gui_compact", { guiCompact });
|
||||
},
|
||||
async environmentProjectViewMode() : Promise<string> {
|
||||
return await TAURI_INVOKE("environment_project_view_mode");
|
||||
},
|
||||
async environmentSetProjectViewMode(projectViewMode: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_project_view_mode", { projectViewMode });
|
||||
},
|
||||
async environmentSetUnityHubAccessMethod(unityHubAccessMethod: UnityHubAccessMethod) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_unity_hub_access_method", { unityHubAccessMethod });
|
||||
},
|
||||
async environmentSetTemplateFavorite(templateId: string, favorite: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_template_favorite", { templateId, favorite });
|
||||
},
|
||||
async environmentProjects() : Promise<TauriProject[]> {
|
||||
return await TAURI_INVOKE("environment_projects");
|
||||
},
|
||||
async environmentAddProjectWithPicker() : Promise<TauriAddProjectWithPickerResult> {
|
||||
return await TAURI_INVOKE("environment_add_project_with_picker");
|
||||
},
|
||||
async environmentRemoveProjectByPath(projectPath: string, directory: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_remove_project_by_path", { projectPath, directory });
|
||||
},
|
||||
async environmentCopyProjectForMigration(channel: string, sourcePath: string) : Promise<AsyncCallResult<TauriCopyProjectProgress, string>> {
|
||||
return await TAURI_INVOKE("environment_copy_project_for_migration", { channel, sourcePath });
|
||||
},
|
||||
async environmentCopyProject(channel: string, sourcePath: string, newPath: string) : Promise<AsyncCallResult<TauriCopyProjectProgress, string>> {
|
||||
return await TAURI_INVOKE("environment_copy_project", { channel, sourcePath, newPath });
|
||||
},
|
||||
async environmentSetFavoriteProject(projectPath: string, favorite: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_favorite_project", { projectPath, favorite });
|
||||
},
|
||||
async environmentProjectCreationInformation() : Promise<TauriProjectCreationInformation> {
|
||||
return await TAURI_INVOKE("environment_project_creation_information");
|
||||
},
|
||||
async environmentCheckProjectName(basePath: string, projectName: string) : Promise<TauriProjectDirCheckResult> {
|
||||
return await TAURI_INVOKE("environment_check_project_name", { basePath, projectName });
|
||||
},
|
||||
async environmentCreateProject(basePath: string, projectName: string, templateId: string, templateVersion: number, unityVersion: string) : Promise<TauriCreateProjectResult> {
|
||||
return await TAURI_INVOKE("environment_create_project", { basePath, projectName, templateId, templateVersion, unityVersion });
|
||||
},
|
||||
async environmentRefetchPackages() : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_refetch_packages");
|
||||
},
|
||||
async environmentPackages() : Promise<TauriPackage[]> {
|
||||
return await TAURI_INVOKE("environment_packages");
|
||||
},
|
||||
async environmentRepositoriesInfo() : Promise<TauriRepositoriesInfo> {
|
||||
return await TAURI_INVOKE("environment_repositories_info");
|
||||
},
|
||||
async environmentHideRepository(repository: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_hide_repository", { repository });
|
||||
},
|
||||
async environmentShowRepository(repository: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_show_repository", { repository });
|
||||
},
|
||||
async environmentSetHideLocalUserPackages(value: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_hide_local_user_packages", { value });
|
||||
},
|
||||
async environmentDownloadRepository(url: string, headers: { [key in string]: string }) : Promise<TauriDownloadRepository> {
|
||||
return await TAURI_INVOKE("environment_download_repository", { url, headers });
|
||||
},
|
||||
async environmentAddRepository(url: string, headers: { [key in string]: string }) : Promise<TauriAddRepositoryResult> {
|
||||
return await TAURI_INVOKE("environment_add_repository", { url, headers });
|
||||
},
|
||||
async environmentRemoveRepository(id: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_remove_repository", { id });
|
||||
},
|
||||
async environmentImportRepositoryPick() : Promise<TauriImportRepositoryPickResult> {
|
||||
return await TAURI_INVOKE("environment_import_repository_pick");
|
||||
},
|
||||
async environmentImportDownloadRepositories(channel: string, repositories: TauriRepositoryDescriptor[]) : Promise<AsyncCallResult<number, ([TauriRepositoryDescriptor, TauriDownloadRepository])[]>> {
|
||||
return await TAURI_INVOKE("environment_import_download_repositories", { channel, repositories });
|
||||
},
|
||||
async environmentImportAddRepositories(repositories: TauriRepositoryDescriptor[]) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_import_add_repositories", { repositories });
|
||||
},
|
||||
async environmentExportRepositories() : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_export_repositories");
|
||||
},
|
||||
async environmentClearPackageCache() : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_clear_package_cache");
|
||||
},
|
||||
async environmentGetUserPackages() : Promise<TauriUserPackage[]> {
|
||||
return await TAURI_INVOKE("environment_get_user_packages");
|
||||
},
|
||||
async environmentAddUserPackageWithPicker() : Promise<TauriAddUserPackageWithPickerResult> {
|
||||
return await TAURI_INVOKE("environment_add_user_package_with_picker");
|
||||
},
|
||||
async environmentRemoveUserPackages(path: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_remove_user_packages", { path });
|
||||
},
|
||||
async environmentUnityVersions() : Promise<TauriUnityVersions> {
|
||||
return await TAURI_INVOKE("environment_unity_versions");
|
||||
},
|
||||
async environmentGetSettings() : Promise<TauriEnvironmentSettings> {
|
||||
return await TAURI_INVOKE("environment_get_settings");
|
||||
},
|
||||
async environmentPickUnityHub() : Promise<TauriPickUnityHubResult> {
|
||||
return await TAURI_INVOKE("environment_pick_unity_hub");
|
||||
},
|
||||
async environmentPickUnity() : Promise<TauriPickUnityResult> {
|
||||
return await TAURI_INVOKE("environment_pick_unity");
|
||||
},
|
||||
async environmentPickProjectDefaultPath() : Promise<TauriPickProjectDefaultPathResult> {
|
||||
return await TAURI_INVOKE("environment_pick_project_default_path");
|
||||
},
|
||||
async environmentPickProjectBackupPath() : Promise<TauriPickProjectBackupPathResult> {
|
||||
return await TAURI_INVOKE("environment_pick_project_backup_path");
|
||||
},
|
||||
async environmentSetShowPrereleasePackages(value: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_show_prerelease_packages", { value });
|
||||
},
|
||||
async environmentSetBackupFormat(backupFormat: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_backup_format", { backupFormat });
|
||||
},
|
||||
async environmentSetExcludeVpmPackagesFromBackup(excludeVpmPackagesFromBackup: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_exclude_vpm_packages_from_backup", { excludeVpmPackagesFromBackup });
|
||||
},
|
||||
async environmentSetReleaseChannel(releaseChannel: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_release_channel", { releaseChannel });
|
||||
},
|
||||
async environmentSetUseAlcomForVccProtocol(useAlcomForVccProtocol: boolean) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_use_alcom_for_vcc_protocol", { useAlcomForVccProtocol });
|
||||
},
|
||||
async environmentGetDefaultUnityArguments() : Promise<string[]> {
|
||||
return await TAURI_INVOKE("environment_get_default_unity_arguments");
|
||||
},
|
||||
async environmentSetDefaultUnityArguments(defaultUnityArguments: string[] | null) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_set_default_unity_arguments", { defaultUnityArguments });
|
||||
},
|
||||
async environmentExportTemplate(id: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_export_template", { id });
|
||||
},
|
||||
async environmentGetAlcomTemplate(id: string) : Promise<TauriAlcomTemplate> {
|
||||
return await TAURI_INVOKE("environment_get_alcom_template", { id });
|
||||
},
|
||||
async environmentPickUnityPackage() : Promise<string[]> {
|
||||
return await TAURI_INVOKE("environment_pick_unity_package");
|
||||
},
|
||||
async environmentSaveTemplate(id: string | null, base: string, name: string, unityRange: string, vpmPackages: ([string, string])[], unityPackages: string[]) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_save_template", { id, base, name, unityRange, vpmPackages, unityPackages });
|
||||
},
|
||||
async environmentRemoveTemplate(id: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("environment_remove_template", { id });
|
||||
},
|
||||
async environmentImportTemplate() : Promise<TauriImportTemplateResult> {
|
||||
return await TAURI_INVOKE("environment_import_template");
|
||||
},
|
||||
async environmentImportTemplateOverride(importOverride: TauriImportDuplicated[]) : Promise<number> {
|
||||
return await TAURI_INVOKE("environment_import_template_override", { importOverride });
|
||||
},
|
||||
async environmentUpdateUnityPathsFromUnityHub() : Promise<boolean> {
|
||||
return await TAURI_INVOKE("environment_update_unity_paths_from_unity_hub");
|
||||
},
|
||||
async environmentIsLoadingFromUnityHubInProgress() : Promise<boolean> {
|
||||
return await TAURI_INVOKE("environment_is_loading_from_unity_hub_in_progress");
|
||||
},
|
||||
async environmentWaitForUnityHubUpdate() : Promise<void> {
|
||||
await TAURI_INVOKE("environment_wait_for_unity_hub_update");
|
||||
},
|
||||
async projectDetails(projectPath: string) : Promise<TauriProjectDetails> {
|
||||
return await TAURI_INVOKE("project_details", { projectPath });
|
||||
},
|
||||
async projectInstallPackages(projectPath: string, installs: ([string, string])[]) : Promise<TauriPendingProjectChanges> {
|
||||
return await TAURI_INVOKE("project_install_packages", { projectPath, installs });
|
||||
},
|
||||
async projectReinstallPackages(projectPath: string, packageIds: string[]) : Promise<TauriPendingProjectChanges> {
|
||||
return await TAURI_INVOKE("project_reinstall_packages", { projectPath, packageIds });
|
||||
},
|
||||
async projectResolve(projectPath: string) : Promise<TauriPendingProjectChanges> {
|
||||
return await TAURI_INVOKE("project_resolve", { projectPath });
|
||||
},
|
||||
async projectRemovePackages(projectPath: string, names: string[]) : Promise<TauriPendingProjectChanges> {
|
||||
return await TAURI_INVOKE("project_remove_packages", { projectPath, names });
|
||||
},
|
||||
async projectApplyPendingChanges(projectPath: string, changesVersion: number) : Promise<null> {
|
||||
return await TAURI_INVOKE("project_apply_pending_changes", { projectPath, changesVersion });
|
||||
},
|
||||
async projectClearPendingChanges() : Promise<null> {
|
||||
return await TAURI_INVOKE("project_clear_pending_changes");
|
||||
},
|
||||
async projectMigrateProjectTo2022(projectPath: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("project_migrate_project_to_2022", { projectPath });
|
||||
},
|
||||
async projectCallUnityForMigration(channel: string, projectPath: string, unityPath: string) : Promise<AsyncCallResult<string, TauriCallUnityForMigrationResult>> {
|
||||
return await TAURI_INVOKE("project_call_unity_for_migration", { channel, projectPath, unityPath });
|
||||
},
|
||||
async projectMigrateProjectToVpm(projectPath: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("project_migrate_project_to_vpm", { projectPath });
|
||||
},
|
||||
async projectOpenUnity(projectPath: string, unityPath: string) : Promise<boolean> {
|
||||
return await TAURI_INVOKE("project_open_unity", { projectPath, unityPath });
|
||||
},
|
||||
async projectIsUnityLaunching(projectPath: string) : Promise<boolean> {
|
||||
return await TAURI_INVOKE("project_is_unity_launching", { projectPath });
|
||||
},
|
||||
async projectCreateBackup(channel: string, projectPath: string) : Promise<AsyncCallResult<TauriCreateBackupProgress, null>> {
|
||||
return await TAURI_INVOKE("project_create_backup", { channel, projectPath });
|
||||
},
|
||||
async projectGetCustomUnityArgs(projectPath: string) : Promise<string[] | null> {
|
||||
return await TAURI_INVOKE("project_get_custom_unity_args", { projectPath });
|
||||
},
|
||||
async projectSetCustomUnityArgs(projectPath: string, args: string[] | null) : Promise<boolean> {
|
||||
return await TAURI_INVOKE("project_set_custom_unity_args", { projectPath, args });
|
||||
},
|
||||
async projectGetUnityPath(projectPath: string) : Promise<string | null> {
|
||||
return await TAURI_INVOKE("project_get_unity_path", { projectPath });
|
||||
},
|
||||
async projectSetUnityPath(projectPath: string, unityPath: string | null) : Promise<boolean> {
|
||||
return await TAURI_INVOKE("project_set_unity_path", { projectPath, unityPath });
|
||||
},
|
||||
async utilOpen(path: string, ifNotExists: OpenOptions) : Promise<null> {
|
||||
return await TAURI_INVOKE("util_open", { path, ifNotExists });
|
||||
},
|
||||
async utilOpenUrl(url: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("util_open_url", { url });
|
||||
},
|
||||
async utilGetLogEntries() : Promise<LogEntry[]> {
|
||||
return await TAURI_INVOKE("util_get_log_entries");
|
||||
},
|
||||
async utilGetVersion() : Promise<string> {
|
||||
return await TAURI_INVOKE("util_get_version");
|
||||
},
|
||||
async utilCheckForUpdate() : Promise<CheckForUpdateResponse | null> {
|
||||
return await TAURI_INVOKE("util_check_for_update");
|
||||
},
|
||||
async utilInstallAndUpgrade(channel: string, version: number) : Promise<AsyncCallResult<InstallUpgradeProgress, null>> {
|
||||
return await TAURI_INVOKE("util_install_and_upgrade", { channel, version });
|
||||
},
|
||||
async utilIsBadHostname() : Promise<boolean> {
|
||||
return await TAURI_INVOKE("util_is_bad_hostname");
|
||||
},
|
||||
async utilPickDirectory(current: string) : Promise<TauriPickProjectDefaultPathResult> {
|
||||
return await TAURI_INVOKE("util_pick_directory", { current });
|
||||
},
|
||||
async deepLinkHasAddRepository() : Promise<boolean> {
|
||||
return await TAURI_INVOKE("deep_link_has_add_repository");
|
||||
},
|
||||
async deepLinkTakeAddRepository() : Promise<AddRepositoryInfo | null> {
|
||||
return await TAURI_INVOKE("deep_link_take_add_repository");
|
||||
},
|
||||
async deepLinkInstallVcc() : Promise<void> {
|
||||
await TAURI_INVOKE("deep_link_install_vcc");
|
||||
},
|
||||
async deepLinkImportedClearNonToastedCount() : Promise<number> {
|
||||
return await TAURI_INVOKE("deep_link_imported_clear_non_toasted_count");
|
||||
},
|
||||
async deepLinkReduceImportedClearNonToastedCount(reduce: number) : Promise<void> {
|
||||
await TAURI_INVOKE("deep_link_reduce_imported_clear_non_toasted_count", { reduce });
|
||||
}
|
||||
}
|
||||
environmentLanguage: () => __TAURI_INVOKE<string>("environment_language"),
|
||||
environmentSetLanguage: (language: string) => __TAURI_INVOKE<null>("environment_set_language", { language }),
|
||||
environmentTheme: () => __TAURI_INVOKE<string>("environment_theme"),
|
||||
environmentSetTheme: (theme: string) => __TAURI_INVOKE<null>("environment_set_theme", { theme }),
|
||||
environmentGetProjectSorting: () => __TAURI_INVOKE<string>("environment_get_project_sorting"),
|
||||
environmentSetProjectSorting: (sorting: string) => __TAURI_INVOKE<null>("environment_set_project_sorting", { sorting }),
|
||||
environmentGetFinishedSetupPages: () => __TAURI_INVOKE<SetupPages[]>("environment_get_finished_setup_pages"),
|
||||
environmentFinishedSetupPage: (page: SetupPages) => __TAURI_INVOKE<null>("environment_finished_setup_page", { page }),
|
||||
environmentClearSetupProcess: () => __TAURI_INVOKE<null>("environment_clear_setup_process"),
|
||||
environmentLogsLevel: () => __TAURI_INVOKE<LogLevel[]>("environment_logs_level"),
|
||||
environmentSetLogsLevel: (logsLevel: LogLevel[]) => __TAURI_INVOKE<null>("environment_set_logs_level", { logsLevel }),
|
||||
environmentGuiAnimation: () => __TAURI_INVOKE<boolean>("environment_gui_animation"),
|
||||
environmentSetGuiAnimation: (guiAnimation: boolean) => __TAURI_INVOKE<null>("environment_set_gui_animation", { guiAnimation }),
|
||||
environmentGuiCompact: () => __TAURI_INVOKE<boolean>("environment_gui_compact"),
|
||||
environmentSetGuiCompact: (guiCompact: boolean) => __TAURI_INVOKE<null>("environment_set_gui_compact", { guiCompact }),
|
||||
environmentProjectViewMode: () => __TAURI_INVOKE<string>("environment_project_view_mode"),
|
||||
environmentSetProjectViewMode: (projectViewMode: string) => __TAURI_INVOKE<null>("environment_set_project_view_mode", { projectViewMode }),
|
||||
environmentSetUnityHubAccessMethod: (unityHubAccessMethod: UnityHubAccessMethod) => __TAURI_INVOKE<null>("environment_set_unity_hub_access_method", { unityHubAccessMethod }),
|
||||
environmentSetTemplateFavorite: (templateId: string, favorite: boolean) => __TAURI_INVOKE<null>("environment_set_template_favorite", { templateId, favorite }),
|
||||
environmentProjects: () => __TAURI_INVOKE<TauriProject[]>("environment_projects"),
|
||||
environmentAddProjectWithPicker: () => __TAURI_INVOKE<TauriAddProjectWithPickerResult>("environment_add_project_with_picker"),
|
||||
environmentRemoveProjectByPath: (projectPath: string, directory: boolean) => __TAURI_INVOKE<null>("environment_remove_project_by_path", { projectPath, directory }),
|
||||
environmentCopyProjectForMigration: (channel: string, sourcePath: string) => __TAURI_INVOKE<AsyncCallResult<TauriCopyProjectProgress, string>>("environment_copy_project_for_migration", { channel, sourcePath }),
|
||||
environmentCopyProject: (channel: string, sourcePath: string, newPath: string) => __TAURI_INVOKE<AsyncCallResult<TauriCopyProjectProgress, string>>("environment_copy_project", { channel, sourcePath, newPath }),
|
||||
environmentSetFavoriteProject: (projectPath: string, favorite: boolean) => __TAURI_INVOKE<null>("environment_set_favorite_project", { projectPath, favorite }),
|
||||
environmentProjectCreationInformation: () => __TAURI_INVOKE<TauriProjectCreationInformation>("environment_project_creation_information"),
|
||||
environmentCheckProjectName: (basePath: string, projectName: string) => __TAURI_INVOKE<TauriProjectDirCheckResult>("environment_check_project_name", { basePath, projectName }),
|
||||
environmentCreateProject: (basePath: string, projectName: string, templateId: string, templateVersion: number, unityVersion: string) => __TAURI_INVOKE<TauriCreateProjectResult>("environment_create_project", { basePath, projectName, templateId, templateVersion, unityVersion }),
|
||||
environmentRefetchPackages: () => __TAURI_INVOKE<null>("environment_refetch_packages"),
|
||||
environmentPackages: () => __TAURI_INVOKE<TauriPackage[]>("environment_packages"),
|
||||
environmentRepositoriesInfo: () => __TAURI_INVOKE<TauriRepositoriesInfo>("environment_repositories_info"),
|
||||
environmentHideRepository: (repository: string) => __TAURI_INVOKE<null>("environment_hide_repository", { repository }),
|
||||
environmentShowRepository: (repository: string) => __TAURI_INVOKE<null>("environment_show_repository", { repository }),
|
||||
environmentSetHideLocalUserPackages: (value: boolean) => __TAURI_INVOKE<null>("environment_set_hide_local_user_packages", { value }),
|
||||
environmentDownloadRepository: (url: string, headers: { [key in string]: string }) => __TAURI_INVOKE<TauriDownloadRepository>("environment_download_repository", { url, headers }),
|
||||
environmentAddRepository: (url: string, headers: { [key in string]: string }) => __TAURI_INVOKE<TauriAddRepositoryResult>("environment_add_repository", { url, headers }),
|
||||
environmentRemoveRepository: (index: number, expectedId: string) => __TAURI_INVOKE<null>("environment_remove_repository", { index, expectedId }),
|
||||
environmentReorderRepositories: (repos: TauriUserRepositoryRef[]) => __TAURI_INVOKE<null>("environment_reorder_repositories", { repos }),
|
||||
environmentImportRepositoryPick: () => __TAURI_INVOKE<TauriImportRepositoryPickResult>("environment_import_repository_pick"),
|
||||
environmentImportDownloadRepositories: (channel: string, repositories: TauriRepositoryDescriptor[]) => __TAURI_INVOKE<AsyncCallResult<number, ([TauriRepositoryDescriptor, TauriDownloadRepository])[]>>("environment_import_download_repositories", { channel, repositories }),
|
||||
environmentImportAddRepositories: (repositories: TauriRepositoryDescriptor[]) => __TAURI_INVOKE<null>("environment_import_add_repositories", { repositories }),
|
||||
environmentExportRepositories: () => __TAURI_INVOKE<null>("environment_export_repositories"),
|
||||
environmentClearPackageCache: () => __TAURI_INVOKE<null>("environment_clear_package_cache"),
|
||||
environmentGetUserPackages: () => __TAURI_INVOKE<TauriUserPackage[]>("environment_get_user_packages"),
|
||||
environmentAddUserPackageWithPicker: () => __TAURI_INVOKE<TauriAddUserPackageWithPickerResult>("environment_add_user_package_with_picker"),
|
||||
environmentRemoveUserPackages: (path: string) => __TAURI_INVOKE<null>("environment_remove_user_packages", { path }),
|
||||
environmentUnityVersions: () => __TAURI_INVOKE<TauriUnityVersions>("environment_unity_versions"),
|
||||
environmentGetSettings: () => __TAURI_INVOKE<TauriEnvironmentSettings>("environment_get_settings"),
|
||||
environmentPickUnityHub: () => __TAURI_INVOKE<TauriPickUnityHubResult>("environment_pick_unity_hub"),
|
||||
environmentPickUnity: () => __TAURI_INVOKE<TauriPickUnityResult>("environment_pick_unity"),
|
||||
environmentPickProjectDefaultPath: () => __TAURI_INVOKE<TauriPickProjectDefaultPathResult>("environment_pick_project_default_path"),
|
||||
environmentPickProjectBackupPath: () => __TAURI_INVOKE<TauriPickProjectBackupPathResult>("environment_pick_project_backup_path"),
|
||||
environmentSetShowPrereleasePackages: (value: boolean) => __TAURI_INVOKE<null>("environment_set_show_prerelease_packages", { value }),
|
||||
environmentSetBackupFormat: (backupFormat: string) => __TAURI_INVOKE<null>("environment_set_backup_format", { backupFormat }),
|
||||
environmentSetExcludeVpmPackagesFromBackup: (excludeVpmPackagesFromBackup: boolean) => __TAURI_INVOKE<null>("environment_set_exclude_vpm_packages_from_backup", { excludeVpmPackagesFromBackup }),
|
||||
environmentSetReleaseChannel: (releaseChannel: string) => __TAURI_INVOKE<null>("environment_set_release_channel", { releaseChannel }),
|
||||
environmentSetUseAlcomForVccProtocol: (useAlcomForVccProtocol: boolean) => __TAURI_INVOKE<null>("environment_set_use_alcom_for_vcc_protocol", { useAlcomForVccProtocol }),
|
||||
environmentGetDefaultUnityArguments: () => __TAURI_INVOKE<string[]>("environment_get_default_unity_arguments"),
|
||||
environmentSetDefaultUnityArguments: (defaultUnityArguments: string[] | null) => __TAURI_INVOKE<null>("environment_set_default_unity_arguments", { defaultUnityArguments }),
|
||||
environmentExportTemplate: (id: string) => __TAURI_INVOKE<null>("environment_export_template", { id }),
|
||||
environmentGetAlcomTemplate: (id: string) => __TAURI_INVOKE<TauriAlcomTemplate>("environment_get_alcom_template", { id }),
|
||||
environmentPickUnityPackages: () => __TAURI_INVOKE<string[]>("environment_pick_unity_packages"),
|
||||
environmentPickUnityPackage: (current: string) => __TAURI_INVOKE<TauriPickUnityPackageResult>("environment_pick_unity_package", { current }),
|
||||
environmentSaveTemplate: (id: string | null, base: string, name: string, unityRange: string, vpmPackages: ([string, string])[], unityPackages: string[]) => __TAURI_INVOKE<null>("environment_save_template", { id, base, name, unityRange, vpmPackages, unityPackages }),
|
||||
environmentRemoveTemplate: (id: string) => __TAURI_INVOKE<null>("environment_remove_template", { id }),
|
||||
environmentImportTemplate: () => __TAURI_INVOKE<TauriImportTemplateResult_Serialize>("environment_import_template"),
|
||||
environmentImportTemplateOverride: (importOverride: TauriImportDuplicated_Deserialize[]) => __TAURI_INVOKE<number>("environment_import_template_override", { importOverride }),
|
||||
environmentUpdateUnityPathsFromUnityHub: () => __TAURI_INVOKE<boolean>("environment_update_unity_paths_from_unity_hub"),
|
||||
environmentIsLoadingFromUnityHubInProgress: () => __TAURI_INVOKE<boolean>("environment_is_loading_from_unity_hub_in_progress"),
|
||||
environmentWaitForUnityHubUpdate: () => __TAURI_INVOKE<void>("environment_wait_for_unity_hub_update"),
|
||||
projectDetails: (projectPath: string) => __TAURI_INVOKE<TauriProjectDetails>("project_details", { projectPath }),
|
||||
projectInstallPackages: (projectPath: string, installs: ([string, string])[]) => __TAURI_INVOKE<TauriPendingProjectChanges>("project_install_packages", { projectPath, installs }),
|
||||
projectReinstallPackages: (projectPath: string, packageIds: string[]) => __TAURI_INVOKE<TauriPendingProjectChanges>("project_reinstall_packages", { projectPath, packageIds }),
|
||||
projectResolve: (projectPath: string) => __TAURI_INVOKE<TauriPendingProjectChanges>("project_resolve", { projectPath }),
|
||||
projectRemovePackages: (projectPath: string, names: string[]) => __TAURI_INVOKE<TauriPendingProjectChanges>("project_remove_packages", { projectPath, names }),
|
||||
projectApplyPendingChanges: (projectPath: string, changesVersion: number) => __TAURI_INVOKE<null>("project_apply_pending_changes", { projectPath, changesVersion }),
|
||||
projectClearPendingChanges: () => __TAURI_INVOKE<null>("project_clear_pending_changes"),
|
||||
projectMigrateProjectTo2022: (projectPath: string) => __TAURI_INVOKE<null>("project_migrate_project_to_2022", { projectPath }),
|
||||
projectCallUnityForMigration: (channel: string, projectPath: string, unityPath: string) => __TAURI_INVOKE<AsyncCallResult<string, TauriCallUnityForMigrationResult>>("project_call_unity_for_migration", { channel, projectPath, unityPath }),
|
||||
projectMigrateProjectToVpm: (projectPath: string) => __TAURI_INVOKE<null>("project_migrate_project_to_vpm", { projectPath }),
|
||||
projectOpenUnity: (projectPath: string, unityPath: string) => __TAURI_INVOKE<boolean>("project_open_unity", { projectPath, unityPath }),
|
||||
projectIsUnityLaunching: (projectPath: string) => __TAURI_INVOKE<boolean>("project_is_unity_launching", { projectPath }),
|
||||
projectCreateBackup: (channel: string, projectPath: string) => __TAURI_INVOKE<AsyncCallResult<TauriCreateBackupProgress, null>>("project_create_backup", { channel, projectPath }),
|
||||
projectGetCustomUnityArgs: (projectPath: string) => __TAURI_INVOKE<string[] | null>("project_get_custom_unity_args", { projectPath }),
|
||||
projectSetCustomUnityArgs: (projectPath: string, args: string[] | null) => __TAURI_INVOKE<boolean>("project_set_custom_unity_args", { projectPath, args }),
|
||||
projectGetUnityPath: (projectPath: string) => __TAURI_INVOKE<string | null>("project_get_unity_path", { projectPath }),
|
||||
projectSetUnityPath: (projectPath: string, unityPath: string | null) => __TAURI_INVOKE<boolean>("project_set_unity_path", { projectPath, unityPath }),
|
||||
utilOpen: (path: string, ifNotExists: OpenOptions) => __TAURI_INVOKE<null>("util_open", { path, ifNotExists }),
|
||||
utilOpenUrl: (url: string) => __TAURI_INVOKE<null>("util_open_url", { url }),
|
||||
utilOpenUrlNocheck: (url: string) => __TAURI_INVOKE<null>("util_open_url_nocheck", { url }),
|
||||
utilGetLogEntries: () => __TAURI_INVOKE<LogEntry_Serialize[]>("util_get_log_entries"),
|
||||
utilGetVersion: () => __TAURI_INVOKE<string>("util_get_version"),
|
||||
utilCheckForUpdate: () => __TAURI_INVOKE<{
|
||||
version: number,
|
||||
current_version: string,
|
||||
latest_version: string,
|
||||
updater_status: UpdaterStatus,
|
||||
update_description: string | null,
|
||||
updater_disabled_messages: { [key in string]: string } | null,
|
||||
} | null>("util_check_for_update"),
|
||||
utilInstallAndUpgrade: (channel: string, version: number) => __TAURI_INVOKE<AsyncCallResult<InstallUpgradeProgress, null>>("util_install_and_upgrade", { channel, version }),
|
||||
utilIsBadHostname: () => __TAURI_INVOKE<boolean>("util_is_bad_hostname"),
|
||||
utilPickDirectory: (current: string) => __TAURI_INVOKE<TauriPickProjectDefaultPathResult>("util_pick_directory", { current }),
|
||||
deepLinkHasAddRepository: () => __TAURI_INVOKE<boolean>("deep_link_has_add_repository"),
|
||||
deepLinkTakeAddRepository: () => __TAURI_INVOKE<{
|
||||
url: string,
|
||||
headers: { [key in string]: string },
|
||||
} | null>("deep_link_take_add_repository"),
|
||||
deepLinkInstallVcc: () => __TAURI_INVOKE<void>("deep_link_install_vcc"),
|
||||
deepLinkImportedClearNonToastedCount: () => __TAURI_INVOKE<number>("deep_link_imported_clear_non_toasted_count"),
|
||||
deepLinkReduceImportedClearNonToastedCount: (reduce: number) => __TAURI_INVOKE<void>("deep_link_reduce_imported_clear_non_toasted_count", { reduce }),
|
||||
};
|
||||
|
||||
/** user-defined events **/
|
||||
/* Types */
|
||||
export type AddRepositoryInfo = {
|
||||
url: string,
|
||||
headers: { [key in string]: string },
|
||||
};
|
||||
|
||||
export type AsyncCallResult<P, R> = { type: "Result"; value: R } | { type: "Started" } | { type: "UnusedProgress"; progress: P };
|
||||
|
||||
export type CheckForUpdateResponse = {
|
||||
version: number,
|
||||
current_version: string,
|
||||
latest_version: string,
|
||||
updater_status: UpdaterStatus,
|
||||
update_description: string | null,
|
||||
updater_disabled_messages: { [key in string]: string } | null,
|
||||
};
|
||||
|
||||
/** user-defined constants **/
|
||||
export type GlobalInfo = {
|
||||
language: string,
|
||||
theme: string,
|
||||
version: string | null,
|
||||
commitHash: string | null,
|
||||
osType: string,
|
||||
arch: string,
|
||||
osInfo: string,
|
||||
webviewVersion: string,
|
||||
appData: string,
|
||||
defaultUnityArguments: string[],
|
||||
vpmHomeFolder: string,
|
||||
checkForUpdates: boolean,
|
||||
shouldInstallDeepLink: boolean,
|
||||
};
|
||||
|
||||
// Errors that is expected to be handled on the GUI side
|
||||
export type HandleableRustError = { type: "MissingDependencies"; dependencies: ([string, string])[] };
|
||||
|
||||
export type InstallUpgradeProgress = { type: "DownloadProgress"; received: number; total: number | null } | { type: "DownloadComplete" };
|
||||
|
||||
/** user-defined types **/
|
||||
export type LocalizableRustError = {
|
||||
id: string,
|
||||
args: { [key in string]: string },
|
||||
};
|
||||
|
||||
export type LogEntry = LogEntry_Serialize | LogEntry_Deserialize;
|
||||
|
||||
export type LogEntry_Deserialize = {
|
||||
time: string,
|
||||
level: LogLevel,
|
||||
target: string,
|
||||
message: string,
|
||||
gui_toast: boolean | null,
|
||||
};
|
||||
|
||||
export type LogEntry_Serialize = {
|
||||
time: string,
|
||||
level: LogLevel,
|
||||
target: string,
|
||||
message: string,
|
||||
gui_toast: boolean | null,
|
||||
};
|
||||
|
||||
export type LogLevel = "Error" | "Warn" | "Info" | "Debug" | "Trace";
|
||||
|
||||
export type OpenOptions = "ErrorIfNotExists" | "CreateFolderIfNotExists" | "OpenParentIfNotExists";
|
||||
|
||||
export type RustError = { type: "Unrecoverable"; message: string } | { type: "Localizable" } & (LocalizableRustError) | { type: "Handleable"; message: string; body: HandleableRustError };
|
||||
|
||||
export type SetupPages = "Appearance" | "UnityHub" | "ProjectPath" | "Backups" | "SystemSetting";
|
||||
|
||||
export type TauriAddProjectWithPickerResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful";
|
||||
|
||||
export type TauriAddRepositoryResult = "BadUrl" | "Success";
|
||||
|
||||
export type TauriAddUserPackageWithPickerResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful";
|
||||
|
||||
export type TauriAlcomTemplate = {
|
||||
display_name: string,
|
||||
base: string,
|
||||
unity_version: string | null,
|
||||
vpm_dependencies: { [key in string]: string },
|
||||
unity_packages: string[],
|
||||
};
|
||||
|
||||
export type TauriBasePackageInfo = {
|
||||
name: string,
|
||||
display_name: string | null,
|
||||
description: string | null,
|
||||
keywords: string[],
|
||||
version: TauriVersion,
|
||||
unity: [number, number] | null,
|
||||
changelog_url: string | null,
|
||||
documentation_url: string | null,
|
||||
vpm_dependencies: string[],
|
||||
legacy_packages: string[],
|
||||
is_yanked: boolean,
|
||||
};
|
||||
|
||||
export type TauriCallUnityForMigrationResult = { type: "ExistsWithNonZero"; status: string } | { type: "FinishedSuccessfully" };
|
||||
|
||||
export type TauriConflictInfo = {
|
||||
packages: string[],
|
||||
unity_conflict: boolean,
|
||||
unlocked_names: string[],
|
||||
};
|
||||
|
||||
export type TauriCopyProjectProgress = {
|
||||
total: number,
|
||||
proceed: number,
|
||||
last_proceed: string,
|
||||
};
|
||||
|
||||
export type TauriCreateBackupProgress = {
|
||||
total: number,
|
||||
proceed: number,
|
||||
last_proceed: string,
|
||||
};
|
||||
|
||||
export type TauriCreateProjectResult = "AlreadyExists" | "TemplateNotFound" | "Successful";
|
||||
|
||||
export type TauriDownloadRepository = { type: "BadUrl" } | { type: "Duplicated"; reason: TauriDuplicatedReason; duplicated_name: string } | { type: "DownloadError"; message: string } | { type: "Success"; value: TauriRemoteRepositoryInfo };
|
||||
|
||||
export type TauriDuplicatedReason = "URLDuplicated" | "IDDuplicated";
|
||||
|
||||
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,
|
||||
release_channel: string,
|
||||
use_alcom_for_vcc_protocol: boolean,
|
||||
default_unity_arguments: string[] | null,
|
||||
gui_animation: boolean,
|
||||
gui_compact: boolean,
|
||||
unity_hub_access_method: UnityHubAccessMethod,
|
||||
exclude_vpm_packages_from_backup: boolean,
|
||||
};
|
||||
|
||||
export type TauriImportDuplicated = TauriImportDuplicated_Serialize | TauriImportDuplicated_Deserialize;
|
||||
|
||||
export type TauriImportDuplicated_Deserialize = {
|
||||
id: string,
|
||||
existing_path: string,
|
||||
existing_name: string,
|
||||
existing_update_date: string | null,
|
||||
importing_name: string,
|
||||
importing_update_date: string | null,
|
||||
data: string,
|
||||
};
|
||||
|
||||
export type TauriImportDuplicated_Serialize = {
|
||||
id: string,
|
||||
existing_path: string,
|
||||
existing_name: string,
|
||||
existing_update_date: string | null,
|
||||
importing_name: string,
|
||||
importing_update_date: string | null,
|
||||
data: string,
|
||||
};
|
||||
|
||||
export type TauriImportRepositoryPickResult = { type: "NoFilePicked" } | { type: "ParsedRepositories"; repositories: TauriRepositoryDescriptor[]; unparsable_lines: string[] };
|
||||
|
||||
export type TauriImportTemplateResult = TauriImportTemplateResult_Serialize | TauriImportTemplateResult_Deserialize;
|
||||
|
||||
export type TauriImportTemplateResult_Deserialize = {
|
||||
imported: number,
|
||||
duplicates: TauriImportDuplicated_Deserialize[],
|
||||
};
|
||||
|
||||
export type TauriImportTemplateResult_Serialize = {
|
||||
imported: number,
|
||||
duplicates: TauriImportDuplicated_Serialize[],
|
||||
};
|
||||
|
||||
export type TauriPackage = {
|
||||
source: TauriPackageSource,
|
||||
} & (TauriBasePackageInfo);
|
||||
|
||||
export type TauriPackageChange = ({ InstallNew: TauriBasePackageInfo }) & { Remove?: never } | ({ Remove: TauriRemoveReason }) & { InstallNew?: never };
|
||||
|
||||
export type TauriPackageSource = "LocalUser" | { Remote: {
|
||||
id: string,
|
||||
display_name: string,
|
||||
} };
|
||||
|
||||
export type TauriPendingProjectChanges = {
|
||||
changes_version: number,
|
||||
package_changes: ([string, TauriPackageChange])[],
|
||||
remove_legacy_files: string[],
|
||||
remove_legacy_folders: string[],
|
||||
conflicts: ([string, TauriConflictInfo])[],
|
||||
};
|
||||
|
||||
export type TauriPickProjectBackupPathResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful" };
|
||||
|
||||
export type TauriPickProjectDefaultPathResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful"; new_path: string };
|
||||
|
||||
export type TauriPickUnityHubResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful" };
|
||||
|
||||
export type TauriPickUnityPackageResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful"; new_path: string };
|
||||
|
||||
export type TauriPickUnityResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful";
|
||||
|
||||
export type TauriProject = {
|
||||
name: string,
|
||||
path: string,
|
||||
project_type: TauriProjectType,
|
||||
unity: string,
|
||||
unity_revision: string | null,
|
||||
last_modified: number,
|
||||
created_at: number,
|
||||
favorite: boolean,
|
||||
is_exists: boolean,
|
||||
is_valid: boolean | null,
|
||||
};
|
||||
|
||||
export type TauriProjectCreationInformation = {
|
||||
templates: TauriProjectTemplateInfo[],
|
||||
recent_project_locations: string[],
|
||||
favorite_templates: string[],
|
||||
last_used_template: string | null,
|
||||
templates_version: number,
|
||||
default_path: string,
|
||||
};
|
||||
|
||||
export type TauriProjectDetails = {
|
||||
unity: [number, number],
|
||||
unity_str: string,
|
||||
unity_revision: string | null,
|
||||
installed_packages: ([string, TauriBasePackageInfo])[],
|
||||
should_resolve: boolean,
|
||||
};
|
||||
|
||||
export type TauriProjectDirCheckResult = "InvalidNameForFolderName" | "MayCompatibilityProblem" | "WideChar" | "AlreadyExists" | "Ok";
|
||||
|
||||
export type TauriProjectTemplateInfo = {
|
||||
display_name: string,
|
||||
id: string,
|
||||
unity_versions: string[],
|
||||
update_date: string | null,
|
||||
has_unitypackage: boolean,
|
||||
source_path: string | null,
|
||||
available: boolean,
|
||||
};
|
||||
|
||||
export type TauriProjectType = "Unknown" | "LegacySdk2" | "LegacyWorlds" | "LegacyAvatars" | "UpmWorlds" | "UpmAvatars" | "UpmStarter" | "Worlds" | "Avatars" | "VpmStarter";
|
||||
|
||||
export type TauriRemoteRepositoryInfo = {
|
||||
display_name: string,
|
||||
id: string,
|
||||
url: string,
|
||||
packages: TauriBasePackageInfo[],
|
||||
};
|
||||
|
||||
export type TauriRemoveReason = "Requested" | "Legacy" | "Unused";
|
||||
|
||||
export type TauriRepositoriesInfo = {
|
||||
user_repositories: TauriUserRepository[],
|
||||
hidden_user_repositories: string[],
|
||||
hide_local_user_packages: boolean,
|
||||
show_prerelease_packages: boolean,
|
||||
};
|
||||
|
||||
export type TauriRepositoryDescriptor = {
|
||||
url: string,
|
||||
headers: { [key in string]: string },
|
||||
};
|
||||
|
||||
export type TauriUnityVersions = {
|
||||
unity_paths: ([string, string, boolean])[],
|
||||
recommended_version: string,
|
||||
install_recommended_version_link: string,
|
||||
};
|
||||
|
||||
export type TauriUpdatedRealProjectInfo = {
|
||||
path: string,
|
||||
is_valid: boolean,
|
||||
project_type: TauriProjectType,
|
||||
unity: string,
|
||||
unity_revision: string | null,
|
||||
};
|
||||
|
||||
export type TauriUserPackage = {
|
||||
path: string,
|
||||
package: TauriBasePackageInfo,
|
||||
};
|
||||
|
||||
export type TauriUserRepository = {
|
||||
index: number,
|
||||
id: string,
|
||||
url: string | null,
|
||||
display_name: string,
|
||||
};
|
||||
|
||||
export type TauriUserRepositoryRef = {
|
||||
index: number,
|
||||
id: string,
|
||||
};
|
||||
|
||||
export type TauriVersion = {
|
||||
major: number,
|
||||
minor: number,
|
||||
patch: number,
|
||||
pre: string,
|
||||
build: string,
|
||||
};
|
||||
|
||||
export type AddRepositoryInfo = { url: string; headers: { [key in string]: string } }
|
||||
export type AsyncCallResult<P, R> = { type: "Result"; value: R } | { type: "Started" } | { type: "UnusedProgress"; progress: P }
|
||||
export type CheckForUpdateResponse = { version: number; current_version: string; latest_version: string; updater_status: UpdaterStatus; update_description: string | null; updater_disabled_messages: { [key in string]: string } | null }
|
||||
/**
|
||||
* Errors that is expected to be handled on the GUI side
|
||||
*/
|
||||
export type HandleableRustError = { type: "MissingDependencies"; dependencies: ([string, string])[] }
|
||||
export type InstallUpgradeProgress = { type: "DownloadProgress"; received: number; total: number | null } | { type: "DownloadComplete" }
|
||||
export type LocalizableRustError = { id: string; args: { [key in string]: string } }
|
||||
export type LogEntry = { time: string; level: LogLevel; target: string; message: string; gui_toast: boolean }
|
||||
export type LogLevel = "Error" | "Warn" | "Info" | "Debug" | "Trace"
|
||||
export type OpenOptions = "ErrorIfNotExists" | "CreateFolderIfNotExists" | "OpenParentIfNotExists"
|
||||
export type RustError = { type: "Unrecoverable"; message: string } | ({ type: "Localizable" } & LocalizableRustError) | { type: "Handleable"; message: string; body: HandleableRustError }
|
||||
export type SetupPages = "Appearance" | "UnityHub" | "ProjectPath" | "Backups" | "SystemSetting"
|
||||
export type TauriAddProjectWithPickerResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful"
|
||||
export type TauriAddRepositoryResult = "BadUrl" | "Success"
|
||||
export type TauriAddUserPackageWithPickerResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful"
|
||||
export type TauriAlcomTemplate = { display_name: string; base: string; unity_version: string | null; vpm_dependencies: { [key in string]: string }; unity_packages: string[] }
|
||||
export type TauriBasePackageInfo = { name: string; display_name: string | null; description: string | null; keywords: string[]; version: TauriVersion; unity: [number, number] | null; changelog_url: string | null; documentation_url: string | null; vpm_dependencies: string[]; legacy_packages: string[]; is_yanked: boolean }
|
||||
export type TauriCallUnityForMigrationResult = { type: "ExistsWithNonZero"; status: string } | { type: "FinishedSuccessfully" }
|
||||
export type TauriConflictInfo = { packages: string[]; unity_conflict: boolean; unlocked_names: string[] }
|
||||
export type TauriCopyProjectProgress = { total: number; proceed: number; last_proceed: string }
|
||||
export type TauriCreateBackupProgress = { total: number; proceed: number; last_proceed: string }
|
||||
export type TauriCreateProjectResult = "AlreadyExists" | "TemplateNotFound" | "Successful"
|
||||
export type TauriDownloadRepository = { type: "BadUrl" } | { type: "Duplicated"; reason: TauriDuplicatedReason; duplicated_name: string } | { type: "DownloadError"; message: string } | { type: "Success"; value: TauriRemoteRepositoryInfo }
|
||||
export type TauriDuplicatedReason = "URLDuplicated" | "IDDuplicated"
|
||||
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; release_channel: string; use_alcom_for_vcc_protocol: boolean; default_unity_arguments: string[] | null; gui_animation: boolean; gui_compact: boolean; unity_hub_access_method: UnityHubAccessMethod; exclude_vpm_packages_from_backup: boolean }
|
||||
export type TauriImportDuplicated = { id: string; existing_path: string; existing_name: string; existing_update_date: string | null; importing_name: string; importing_update_date: string | null; data: number[] }
|
||||
export type TauriImportRepositoryPickResult = { type: "NoFilePicked" } | { type: "ParsedRepositories"; repositories: TauriRepositoryDescriptor[]; unparsable_lines: string[] }
|
||||
export type TauriImportTemplateResult = { imported: number; duplicates: TauriImportDuplicated[] }
|
||||
export type TauriPackage = ({ name: string; display_name: string | null; description: string | null; keywords: string[]; version: TauriVersion; unity: [number, number] | null; changelog_url: string | null; documentation_url: string | null; vpm_dependencies: string[]; legacy_packages: string[]; is_yanked: boolean }) & { source: TauriPackageSource }
|
||||
export type TauriPackageChange = { InstallNew: TauriBasePackageInfo } | { Remove: TauriRemoveReason }
|
||||
export type TauriPackageSource = "LocalUser" | { Remote: { id: string; display_name: string } }
|
||||
export type TauriPendingProjectChanges = { changes_version: number; package_changes: ([string, TauriPackageChange])[]; remove_legacy_files: string[]; remove_legacy_folders: string[]; conflicts: ([string, TauriConflictInfo])[] }
|
||||
export type TauriPickProjectBackupPathResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful" }
|
||||
export type TauriPickProjectDefaultPathResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful"; new_path: string }
|
||||
export type TauriPickUnityHubResult = { type: "NoFolderSelected" } | { type: "InvalidSelection" } | { type: "Successful" }
|
||||
export type TauriPickUnityResult = "NoFolderSelected" | "InvalidSelection" | "AlreadyAdded" | "Successful"
|
||||
export type TauriProject = { name: string; path: string; project_type: TauriProjectType; unity: string; unity_revision: string | null; last_modified: number; created_at: number; favorite: boolean; is_exists: boolean; is_valid: boolean | null }
|
||||
export type TauriProjectCreationInformation = { templates: TauriProjectTemplateInfo[]; recent_project_locations: string[]; favorite_templates: string[]; last_used_template: string | null; templates_version: number; default_path: string }
|
||||
export type TauriProjectDetails = { unity: [number, number]; unity_str: string; unity_revision: string | null; installed_packages: ([string, TauriBasePackageInfo])[]; should_resolve: boolean }
|
||||
export type TauriProjectDirCheckResult = "InvalidNameForFolderName" | "MayCompatibilityProblem" | "WideChar" | "AlreadyExists" | "Ok"
|
||||
export type TauriProjectTemplateInfo = { display_name: string; id: string; unity_versions: string[]; update_date: string | null; has_unitypackage: boolean; source_path: string | null; available: boolean }
|
||||
export type TauriProjectType = "Unknown" | "LegacySdk2" | "LegacyWorlds" | "LegacyAvatars" | "UpmWorlds" | "UpmAvatars" | "UpmStarter" | "Worlds" | "Avatars" | "VpmStarter"
|
||||
export type TauriRemoteRepositoryInfo = { display_name: string; id: string; url: string; packages: TauriBasePackageInfo[] }
|
||||
export type TauriRemoveReason = "Requested" | "Legacy" | "Unused"
|
||||
export type TauriRepositoriesInfo = { user_repositories: TauriUserRepository[]; hidden_user_repositories: string[]; hide_local_user_packages: boolean; show_prerelease_packages: boolean }
|
||||
export type TauriRepositoryDescriptor = { url: string; headers: { [key in string]: string } }
|
||||
export type TauriUnityVersions = { unity_paths: ([string, string, boolean])[]; recommended_version: string; install_recommended_version_link: string }
|
||||
export type TauriUpdatedRealProjectInfo = { path: string; is_valid: boolean; project_type: TauriProjectType; unity: string; unity_revision: string | null }
|
||||
export type TauriUserPackage = { path: string; package: TauriBasePackageInfo }
|
||||
export type TauriUserRepository = { id: string; url: string | null; display_name: string }
|
||||
export type TauriVersion = { major: number; minor: number; patch: number; pre: string; build: string }
|
||||
export type UnityHubAccessMethod =
|
||||
/**
|
||||
* Reads config files of Unity Hub
|
||||
*/
|
||||
// Reads config files of Unity Hub
|
||||
"ReadConfig" |
|
||||
/**
|
||||
* Launches headless Unity Hub in background
|
||||
*/
|
||||
"CallHub"
|
||||
// Launches headless Unity Hub in background
|
||||
"CallHub";
|
||||
|
||||
export type UpdaterStatus =
|
||||
/**
|
||||
* Update is found and can be updated automatically. UpdaterInformation is available
|
||||
* Update is found and can be updated automatically. UpdaterInformation is available
|
||||
*
|
||||
* User will proceed update.
|
||||
* User will proceed update.
|
||||
*/
|
||||
"Updatable" |
|
||||
/**
|
||||
* Update is found, but installer or package for current architecture does not found.
|
||||
* This can happen if platform support is removed.
|
||||
* x86_64 macOS will become this state in near future, but other platforms may if new arch is expanded enough.
|
||||
* Update is found, but installer or package for current architecture does not found.
|
||||
* This can happen if platform support is removed.
|
||||
* x86_64 macOS will become this state in near future, but other platforms may if new arch is expanded enough.
|
||||
*
|
||||
* Inform only
|
||||
* Inform only
|
||||
*/
|
||||
"NoPlatform" |
|
||||
/**
|
||||
* Update is found and installer is found, but current installation is different from
|
||||
* the previous (detected) installation, or we failed to detect current installation path.
|
||||
* Update is found and installer is found, but current installation is different from
|
||||
* the previous (detected) installation, or we failed to detect current installation path.
|
||||
*
|
||||
* Inform user to install update manually to prevent problem.
|
||||
* Inform user to install update manually to prevent problem.
|
||||
*/
|
||||
"NotUpdatable" |
|
||||
/**
|
||||
* Updater is disabled at build time. generally the installation is managed by package manager.
|
||||
* Updater is disabled at build time. generally the installation is managed by package manager.
|
||||
*
|
||||
* Inform user to upgrade through package manager.
|
||||
* Packager may customize information message by defining
|
||||
* `VRC_GET_GUI_UPDATER_UPDATE_SUGGESTION_MESSAGE` environment variable at build time.
|
||||
* Inform user to upgrade through package manager.
|
||||
* Packager may customize information message by defining
|
||||
* `VRC_GET_GUI_UPDATER_UPDATE_SUGGESTION_MESSAGE` environment variable at build time.
|
||||
*/
|
||||
"UpdaterDisabled"
|
||||
"UpdaterDisabled";
|
||||
|
||||
/** tauri-specta globals **/
|
||||
|
||||
import {
|
||||
invoke as TAURI_INVOKE,
|
||||
Channel as TAURI_CHANNEL,
|
||||
} from "@tauri-apps/api/core";
|
||||
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
|
||||
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
type __EventObj__<T> = {
|
||||
listen: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
once: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
||||
emit: null extends T
|
||||
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
||||
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||
};
|
||||
|
||||
export type Result<T, E> =
|
||||
| { status: "ok"; data: T }
|
||||
| { status: "error"; error: E };
|
||||
|
||||
function __makeEvents__<T extends Record<string, any>>(
|
||||
mappings: Record<keyof T, string>,
|
||||
) {
|
||||
return new Proxy(
|
||||
{} as unknown as {
|
||||
[K in keyof T]: __EventObj__<T[K]> & {
|
||||
(handle: __WebviewWindow__): __EventObj__<T[K]>;
|
||||
};
|
||||
},
|
||||
{
|
||||
get: (_, event) => {
|
||||
const name = mappings[event as keyof T];
|
||||
|
||||
return new Proxy((() => {}) as any, {
|
||||
apply: (_, __, [window]: [__WebviewWindow__]) => ({
|
||||
listen: (arg: any) => window.listen(name, arg),
|
||||
once: (arg: any) => window.once(name, arg),
|
||||
emit: (arg: any) => window.emit(name, arg),
|
||||
}),
|
||||
get: (_, command: keyof __EventObj__<any>) => {
|
||||
switch (command) {
|
||||
case "listen":
|
||||
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
|
||||
case "once":
|
||||
return (arg: any) => TAURI_API_EVENT.once(name, arg);
|
||||
case "emit":
|
||||
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import type React from "react";
|
||||
import { tc } from "@/lib/i18n";
|
||||
|
||||
export function dayToString(dateIn: Date | number | string) {
|
||||
const date = typeof dateIn !== "object" ? new Date(dateIn) : dateIn;
|
||||
|
||||
const year = date.getFullYear().toString().padStart(4, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function dateToString(dateIn: Date | number | string) {
|
||||
const date = typeof dateIn !== "object" ? new Date(dateIn) : dateIn;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
import type { GlobalInfo as GlobalInfoBinding } from "./bindings.js";
|
||||
|
||||
type OsType = "Linux" | "Darwin" | "WindowsNT";
|
||||
type Arch = "x86_64" | "aarch64";
|
||||
|
||||
interface GlobalInfo {
|
||||
language: string;
|
||||
theme: string;
|
||||
version: string | null;
|
||||
commitHash: string | null;
|
||||
type GlobalInfo = GlobalInfoBinding & {
|
||||
osType: OsType;
|
||||
arch: Arch;
|
||||
osInfo: string;
|
||||
webviewVersion: string;
|
||||
localAppData: string; // empty string for non-windows
|
||||
defaultUnityArguments: string[];
|
||||
vpmHomeFolder: string;
|
||||
checkForUpdates: boolean;
|
||||
shouldInstallDeepLink: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackGlobalInfo: Readonly<GlobalInfo> = {
|
||||
language: "en",
|
||||
|
|
@ -26,7 +17,7 @@ const fallbackGlobalInfo: Readonly<GlobalInfo> = {
|
|||
arch: "x86_64",
|
||||
osInfo: "unknown OS",
|
||||
webviewVersion: "unknown",
|
||||
localAppData: "",
|
||||
appData: "",
|
||||
defaultUnityArguments: [],
|
||||
vpmHomeFolder: "",
|
||||
checkForUpdates: false,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { UnitySelectorDialog } from "@/components/unity-selector-dialog";
|
|||
import { commands, type TauriUnityVersions } from "@/lib/bindings";
|
||||
import { type DialogContext, openSingleDialog } from "@/lib/dialog";
|
||||
import i18next, { tc } from "@/lib/i18n";
|
||||
import { toastError, toastNormal } from "@/lib/toast";
|
||||
import { toastError, toastNormal, toastThrownError } from "@/lib/toast";
|
||||
import { parseUnityVersion } from "@/lib/version";
|
||||
|
||||
export async function openUnity(
|
||||
|
|
@ -92,46 +92,50 @@ async function openUnityWith(
|
|||
selectedPath: string | null,
|
||||
projectPath: string,
|
||||
) {
|
||||
if (foundVersions.length === 1) {
|
||||
if (selectedPath) {
|
||||
if (foundVersions[0][0] !== selectedPath) {
|
||||
// if only unity is not
|
||||
void commands.projectSetUnityPath(projectPath, null);
|
||||
try {
|
||||
if (foundVersions.length === 1) {
|
||||
if (selectedPath) {
|
||||
if (foundVersions[0][0] !== selectedPath) {
|
||||
// if only unity is not
|
||||
void commands.projectSetUnityPath(projectPath, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await commands.projectOpenUnity(
|
||||
projectPath,
|
||||
foundVersions[0][0],
|
||||
);
|
||||
if (result) toastNormal(i18next.t("projects:toast:opening unity..."));
|
||||
else toastError(i18next.t("projects:toast:unity already running"));
|
||||
} else {
|
||||
if (selectedPath) {
|
||||
const found = foundVersions.find(([p, _v, _i]) => p === selectedPath);
|
||||
if (found) {
|
||||
const result = await commands.projectOpenUnity(
|
||||
projectPath,
|
||||
selectedPath,
|
||||
);
|
||||
if (result) toastNormal(i18next.t("projects:toast:opening unity..."));
|
||||
else toastError(i18next.t("projects:toast:unity already running"));
|
||||
return;
|
||||
const result = await commands.projectOpenUnity(
|
||||
projectPath,
|
||||
foundVersions[0][0],
|
||||
);
|
||||
if (result) toastNormal(i18next.t("projects:toast:opening unity..."));
|
||||
else toastError(i18next.t("projects:toast:unity already running"));
|
||||
} else {
|
||||
if (selectedPath) {
|
||||
const found = foundVersions.find(([p, _v, _i]) => p === selectedPath);
|
||||
if (found) {
|
||||
const result = await commands.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 openSingleDialog(UnitySelectorDialog, {
|
||||
unityVersions: foundVersions,
|
||||
supportKeepUsing: true,
|
||||
});
|
||||
if (selected == null) return;
|
||||
if (selected.keepUsingThisVersion) {
|
||||
void commands.projectSetUnityPath(projectPath, selected.unityPath);
|
||||
}
|
||||
const result = await commands.projectOpenUnity(
|
||||
projectPath,
|
||||
selected.unityPath,
|
||||
);
|
||||
if (result) toastNormal(i18next.t("projects:toast:opening unity..."));
|
||||
else toastError(i18next.t("projects:toast:unity already running"));
|
||||
}
|
||||
const selected = await openSingleDialog(UnitySelectorDialog, {
|
||||
unityVersions: foundVersions,
|
||||
supportKeepUsing: true,
|
||||
});
|
||||
if (selected == null) return;
|
||||
if (selected.keepUsingThisVersion) {
|
||||
void commands.projectSetUnityPath(projectPath, selected.unityPath);
|
||||
}
|
||||
const result = await commands.projectOpenUnity(
|
||||
projectPath,
|
||||
selected.unityPath,
|
||||
);
|
||||
if (result) toastNormal(i18next.t("projects:toast:opening unity..."));
|
||||
else toastError("Unity already running");
|
||||
} catch (e) {
|
||||
toastThrownError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +149,7 @@ function UnityInstallWindow({
|
|||
dialog: DialogContext<void>;
|
||||
}) {
|
||||
const openUnityHub = async () => {
|
||||
await commands.utilOpenUrl(installWithUnityHubLink);
|
||||
await commands.utilOpenUrlNocheck(installWithUnityHubLink);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ export function toastNormal(message: ToastContent) {
|
|||
});
|
||||
}
|
||||
|
||||
export function toastWarning(message: ToastContent) {
|
||||
toast.warning(wrapWithDiv(message), {
|
||||
pauseOnFocusLoss: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function toastInfo(message: ToastContent) {
|
||||
toast.info(wrapWithDiv(message), {
|
||||
pauseOnFocusLoss: false,
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@
|
|||
"general:unknown date": "Unbekanntes Datum",
|
||||
|
||||
"general:dialog:select file or directory header": "Datei oder Ordner auswählen",
|
||||
"general:dialog:select file or directory": "Bitte Datei oder Ordner auswählen",
|
||||
"general:toast:invalid directory": "Ungültiger Ordner wurde ausgewählt",
|
||||
"general:dialog:select file or directory": "Bitte Datei oder Ordner auswählen.",
|
||||
"general:toast:invalid directory": "Ungültiger Ordner wurde ausgewählt.",
|
||||
"general:toast:invalid file": "Ungültige Datei wurde ausgewählt.",
|
||||
|
||||
"general:toast:not supported": "{{name}} wird noch nicht unterstützt",
|
||||
"general:toast:not supported": "{{name}} wird noch nicht unterstützt.",
|
||||
"general:not implemented": "Nicht implementiert",
|
||||
|
||||
"search:placeholder": "Suchen...",
|
||||
|
|
@ -113,6 +114,7 @@
|
|||
"projects:grid view": "Gitteransicht",
|
||||
"projects:sort by": "Sortierreihenfolge:",
|
||||
"projects:error:load error": "Fehler beim Laden der Projekte: {{msg}}",
|
||||
"projects:error:noexec filesystem": "Das Projekt befindet sich auf einem Dateisystem dass das 'noexec' flag gesetzt hat. Dies hindert Unity daran native Plugins zu laden, und führt zu einem korrupten Zustand. Bitte verschiebe das Projekt auf ein Dateisystem ohne 'noexec' flag.",
|
||||
"projects:toast:project added": "Projekt erfolgreich hinzugefügt",
|
||||
"projects:toast:project already exists": "Das Projekt wurde bereits hinzugefügt.",
|
||||
|
||||
|
|
@ -216,6 +218,7 @@
|
|||
"projects:manage:button:unity migrate": "Migriere Projekt",
|
||||
|
||||
"projects:manage:manage packages": "Pakete verwalten",
|
||||
"projects:manage:hidden packages": "Versteckte Pakete",
|
||||
"projects:manage:tooltip:refresh packages": "Pakete aktualisieren",
|
||||
"projects:manage:button:upgrade all": "Alle Aktualisieren",
|
||||
"projects:manage:button:upgrade all stable": "Alle Aktualisieren (Stabil)",
|
||||
|
|
@ -232,6 +235,7 @@
|
|||
"projects:manage:source not selected": "Inaktive Quelle",
|
||||
"projects:manage:multiple sources": "Mehrere Quellen",
|
||||
"projects:manage:tooltip:add package": "Paket hinzufügen",
|
||||
"projects:manage:tooltip:select repository to install": "Paketquelle für Installation auswählen",
|
||||
"projects:manage:tooltip:upgrade package": "Paket upgraden",
|
||||
"projects:manage:tooltip:remove packages": "Paket entfernen",
|
||||
"projects:manage:tooltip:incompatible with unity": "Nicht unterstützt",
|
||||
|
|
@ -440,7 +444,7 @@
|
|||
|
||||
// Reset your PC is a feature in Windows, so please check your translation is consistent with Windows terminology.
|
||||
// It's at Setting app > System > Recovery > Reset this PC
|
||||
"settings:warning:in-local-app-data": "Gewählter Pfad ist im LocalAppData Verzeichnis, welcher über \"Diesen PC zurücksetzen\" mit \"Eigene Dateien behalten\" gelöscht wird.\nBitte wähle einen anderen Pfad.",
|
||||
"settings:warning:in-app-data": "Gewählter Pfad ist im LocalAppData Verzeichnis, welcher über \"Diesen PC zurücksetzen\" mit \"Eigene Dateien behalten\" gelöscht wird.\nBitte wähle einen anderen Pfad.",
|
||||
"settings:warning:whitespace": "Der Pfad enthält Leerzeichen. Dies kann zu Problemen mit Unity und anderen Tools führen. Bitte wähle einen anderen Pfad.",
|
||||
"settings:warning:non-ascii": "Der Pfad enthält nicht-ASCII Zeichen. Dies kann zu Problemen mit Unity und anderen Tools führen. Bitte wähle einen anderen Pfad.",
|
||||
|
||||
|
|
@ -490,6 +494,10 @@
|
|||
"settings:show prerelease": "Vorschau-Pakete anzeigen",
|
||||
"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:dialog:show prerelease packages": "Zeige Vorschau-Pakete",
|
||||
"settings:dialog:show prerelease packages description": "Vorschau-Pakete sind unfertige und ungetestete Versionen.<br>Die Wahrscheinlichkeit auf Fehler zu stoßen ist höher.<br>Möchtest du Vorschau-Pakete anzeigen?",
|
||||
"settings:dialog:enable show prerelease packages": "Anzeigen",
|
||||
|
||||
"settings:gui animation": "Aktiviere Animationen",
|
||||
"settings:gui animation description": "Ist diese Option aktiviert, werden einige Seitenwechsel animiert. Dies funktioniert auf Systemen mit älteren Webview Versionen evtl. nicht richtig.",
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"general:dialog:select file or directory header": "Selecting file or directory",
|
||||
"general:dialog:select file or directory": "Please select a file or directory.",
|
||||
"general:toast:invalid directory": "Invalid directory was selected.",
|
||||
"general:toast:invalid file": "Invalid file was selected.",
|
||||
|
||||
"general:toast:not supported": "{{name}} is not supported yet.",
|
||||
"general:not implemented": "Not Implemented",
|
||||
|
|
@ -36,6 +37,8 @@
|
|||
"general:error:failed to create dir": "Failed to create directory (missing permission?): {{err}}",
|
||||
"general:error:failed to create dir missing drive": "Failed to create directory. You may forget to connect the drive.",
|
||||
|
||||
"general:created at": "Added",
|
||||
|
||||
"general:last modified": "Last Modified",
|
||||
"general:last modified:moments": "Moments ago",
|
||||
"general:last modified:minutes_one": "{{count}} minute ago",
|
||||
|
|
@ -113,6 +116,7 @@
|
|||
"projects:grid view": "Grid View",
|
||||
"projects:sort by": "Sort by:",
|
||||
"projects:error:load error": "Error loading projects: {{msg}}",
|
||||
"projects:error:noexec filesystem": "The project is located on a filesystem mounted with the 'noexec' flag. This prevents Unity from loading native plugins, resulting in a broken state or corrupted assets. Please move the project to a filesystem without the 'noexec' flag.",
|
||||
"projects:toast:project added": "Project was addded successfully.",
|
||||
"projects:toast:project already exists": "The project was already added.",
|
||||
|
||||
|
|
@ -216,6 +220,7 @@
|
|||
"projects:manage:button:unity migrate": "Migrate Project",
|
||||
|
||||
"projects:manage:manage packages": "Manage Packages",
|
||||
"projects:manage:hidden packages": "Hidden Packages",
|
||||
"projects:manage:tooltip:refresh packages": "Refresh Packages",
|
||||
"projects:manage:button:upgrade all": "Upgrade All (Latest)",
|
||||
"projects:manage:button:upgrade all stable": "Upgrade All (Stable)",
|
||||
|
|
@ -232,6 +237,7 @@
|
|||
"projects:manage:source not selected": "Not selected",
|
||||
"projects:manage:multiple sources": "Multiple sources",
|
||||
"projects:manage:tooltip:add package": "Add Package",
|
||||
"projects:manage:tooltip:select repository to install": "Select repository to install package",
|
||||
"projects:manage:tooltip:upgrade package": "Upgrade Package",
|
||||
"projects:manage:tooltip:remove packages": "Remove Package",
|
||||
"projects:manage:tooltip:incompatible with unity": "Incompatible with Unity",
|
||||
|
|
@ -295,13 +301,13 @@
|
|||
"projects:toast:unity migrated": "The project was migrated to Unity 2022",
|
||||
|
||||
"projects:manage:dialog:confirm changes description": "You're applying the following changes to the project.",
|
||||
"projects:manage:dialog:note breaking changes": "There are some breaking changes among packages. <br/>Your assets, configuration, tools in your project can be incompatible. <br/>Check for compatibility information and upgrade with care.",
|
||||
"projects:manage:dialog:note breaking changes": "There are changes that may affect compatibility. <br/>Some assets, configurations, or tools might not work as expected. Check compatibility information and proceed with care.<br/>If you encounter any issues after the update, try reverting to the previous package version.",
|
||||
"projects:manage:dialog:note incompatibility": "There are some incompatibility with some other packages. <br/>Those tools may report error when loading project or while using. <br/>Check for compatibility information and upgrade related packages at once, or install with care.",
|
||||
"projects:manage:dialog:upgrade package": "Upgrade <b>{{name}}</b> version {{previousVersion}} to <b>{{version}}</b>",
|
||||
"projects:manage:dialog:downgrade package": "Downgrade <b>{{name}}</b> version {{previousVersion}} to <b>{{version}}</b>",
|
||||
"projects:manage:dialog:install package": "Install <b>{{name}}</b> version <b>{{version}}</b>",
|
||||
"projects:manage:dialog:reinstall package": "Reinstall <b>{{name}}</b> version <b>{{version}}</b>",
|
||||
"projects:manage:dialog:breaking changes": "Includes breaking changes",
|
||||
"projects:manage:dialog:breaking changes": "May affect compatibility",
|
||||
"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",
|
||||
|
|
@ -420,7 +426,7 @@
|
|||
"templates:dialog:unitypackages": "UnityPackages",
|
||||
"templates:dialog:no unitypackages": "No UnityPackages",
|
||||
"templates:dialog:remove template": "Remove Template",
|
||||
"templates:dialog:confirm remove template": "Are you sure remove template {{displayName}}?",
|
||||
"templates:dialog:confirm remove template": "Are you sure you want to remove template {{displayName}}?",
|
||||
"templates:button:save template": "Save Template",
|
||||
"templates:button:import template": "Import Template",
|
||||
"templates:toast:imported n templates_one": "Imported {{count}} template",
|
||||
|
|
@ -440,7 +446,7 @@
|
|||
|
||||
// Reset your PC is a feature in Windows, so please check your translation is consistent with Windows terminology.
|
||||
// It's at Setting app > System > Recovery > Reset this PC
|
||||
"settings:warning:in-local-app-data": "The location is in LocalAppData directory, which will be deleted with \"Reset this PC\" with \"Keep my files\".<br>It's recommended to save your data in a different location.",
|
||||
"settings:warning:in-app-data": "The location is in AppData directory, which will be deleted with \"Reset this PC\" with \"Keep my files\".<br>It's recommended to save your data in a different location.",
|
||||
"settings:warning:whitespace": "The location contains whitespace. Whitespace in may cause problems with Unity and other tools. Please consider use another location.",
|
||||
"settings:warning:non-ascii": "The location contains non-ASCII characters. Non-ASCII characters in the location may cause problems with Unity. Please consider use another location.",
|
||||
|
||||
|
|
@ -488,7 +494,11 @@
|
|||
"settings:toast:package cache cleared": "Downloaded package cache was cleared.",
|
||||
|
||||
"settings:show prerelease": "Show Prerelease Packages",
|
||||
"settings:show prerelease description": "Enabling Show Prerelease Packages will show prerelease packages in the package list. In addition, prerelease packages will be used when resolving dependencies.",
|
||||
"settings:show prerelease description": "Enabling Show Prerelease Packages will show prerelease packages in the package list, and they will be used when resolving dependencies.<br>Prerelease packages may have more bugs than stable packages.",
|
||||
|
||||
"settings:dialog:show prerelease packages": "Show Prerelease Packages",
|
||||
"settings:dialog:show prerelease packages description": "Prerelease packages may have more bugs than stable packages.<br>Are you sure you want to show prerelease packages?",
|
||||
"settings:dialog:enable show prerelease packages": "Show",
|
||||
|
||||
"settings:gui animation": "Enable GUI Animation",
|
||||
"settings:gui animation description": "Page transitions are animated when GUI animation is enabled. But it may not work on lower webview versions.",
|
||||
|
|
@ -519,7 +529,7 @@
|
|||
"check update:dialog:title": "ALCOM Updates",
|
||||
"check update:dialog:new version description": "A new version of ALCOM is available.",
|
||||
// note: we may remove x86_64 macos later. This is preparation for this removal
|
||||
"check update:dialog:new version no platform description": "A new version of ALCOM is available, but your platform is no longer supported.<br>\nIf your system supports another platform build, please download it instead.",
|
||||
"check update:dialog:new version no platform description": "A new version of ALCOM is available, but your platform is no longer supported.<br>If your system supports another platform build, please download it instead.",
|
||||
"check update:dialog:new version not updatable description": "A new version of ALCOM is available.<br>However, this installation cannot be updated automatically.<br>Please install the new version manually.",
|
||||
"check update:dialog:new version updater disabled base description": "A new version of ALCOM is available.",
|
||||
"check update:dialog:new version updater how to upgrade fallback": "Automatic updates are disabled. Please upgrade using your package manager.",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
'check update:dialog:downloading...': 'Téléchargement de la mise à jour...',
|
||||
'check update:dialog:latest version': 'Dernière version :',
|
||||
'check update:dialog:new version description': "Une nouvelle version d'ALCOM est disponible.",
|
||||
'check update:dialog:new version no platform description': "Une nouvelle version d'ALCOM est disponible, mais votre plateforme n'est plus supportée.<br>Si votre système supporte une autre type de plateforme, s'il vous plait téléchargez cette dernière à la place.",
|
||||
'check update:dialog:new version not updatable description': "Une nouvelle version d'ALCOM est disponible.<br>Cependant, cette installation ne peut être mise à jour automatiquement.<br>S'il vous plait installez la nouvelle version manuellement.",
|
||||
'check update:dialog:new version updater disabled base description': "Une nouvelle version d'ALCOM est disponible.",
|
||||
'check update:dialog:new version updater how to upgrade fallback': "Les mises a jour automatiques sont désactivées. S'il vous plait mettez a jour l'application via votre gestionnaire de packets.",
|
||||
'check update:dialog:open download page': 'Ouvrir la page de téléchargement',
|
||||
'check update:dialog:relaunching...': "Redémarrage d'ALCOM...",
|
||||
'check update:dialog:title': "Mises à jour d'ALCOM",
|
||||
'check update:dialog:update': 'Téléchargement et Mises a jour !',
|
||||
|
|
@ -45,6 +50,7 @@
|
|||
'general:packages': 'Packages',
|
||||
'general:source': 'Source',
|
||||
'general:toast:invalid directory': 'Le dossier projet sélectionné est invalide.',
|
||||
'general:toast:invalid file': 'Fichier invalide sélectionné.',
|
||||
'general:toast:not supported': "{{name}} n'est pas encore supporté.",
|
||||
'general:unknown date': 'Date inconnue',
|
||||
'general:version': 'Version',
|
||||
|
|
@ -101,6 +107,7 @@
|
|||
'projects:dialog:warn removing project': 'Vous vous apprêtez a supprimer le projet <b>{{name}}</b>. Êtes vous sur ?',
|
||||
'projects:do not close': 'Ne fermez pas cette fenêttre.',
|
||||
'projects:error:load error': 'Erreur lors du chargement des projets: {{msg}}',
|
||||
'projects:error:noexec filesystem': "Le projet se trouve sur un système de fichier (FS) monté avec le tag 'noexec'. Cela empèche Unity de charger correctement les plugins natifs. Cela peut provoquer des états incompatibles ou corrompre certains objets. S'il vous plait, déplacez le projet vers un système de fichier sans le tag 'noexec'.",
|
||||
'projects:grid view': 'Vue en Grille',
|
||||
'projects:hint:create project ready': 'Prêt a créer un projet !',
|
||||
'projects:hint:invalid project name': 'Nom de projet invalide.',
|
||||
|
|
@ -159,6 +166,7 @@
|
|||
'projects:manage:dialog:upgrade minor': "Vous êtes en train d'upgrader la version de Unity. Cela pourrais provoquer des changements dans vos scripts et votre Library pourrais devoir être reconstruite.<br/>Faites un backup de votre projet avant de poursuivre.<br/>Voullez vous continuer ?",
|
||||
'projects:manage:dialog:upgrade minor vrchat unsupported': "Vous êtes en train d'upgrader la version de Unity. Cela pourrais provoquer des changements dans vos scripts et votre Library pourrais devoir être reconstruite.<br/>Faites un backup de votre projet avant de poursuivre.<br/>De plus vous utiliserez une version de Unity non supportée par VRChat, vous empèchant d'uploader du contenu après l'upgrade.<br/>Voullez vous continuer ?",
|
||||
'projects:manage:dialog:upgrade package': 'Mise a jour de <b>{{name}}</b> de la version {{previousVersion}} vers <b>{{version}}</b>',
|
||||
'projects:manage:hidden packages': 'Packets cachés',
|
||||
'projects:manage:incompatible packages': 'Incompatibles',
|
||||
'projects:manage:installed': 'Installé',
|
||||
'projects:manage:latest': 'Dernière version',
|
||||
|
|
@ -195,13 +203,14 @@
|
|||
'projects:manage:tooltip:incompatible with unity': 'Incompatible avec Unity',
|
||||
'projects:manage:tooltip:refresh packages': 'Rafraichir les packets',
|
||||
'projects:manage:tooltip:remove packages': 'Supprimer le packet',
|
||||
'projects:manage:tooltip:select repository to install': 'Sélectionnez le dépot pour installer le packet',
|
||||
'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:change launch options': 'Changer les options de démarrage',
|
||||
'projects:menuitem:copy project': 'Copier le projet',
|
||||
'projects:menuitem:forget unity path': '===Oublier Unity pour ce projet',
|
||||
'projects:menuitem:forget unity path': 'Oublier Unity pour ce projet',
|
||||
'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...',
|
||||
|
|
@ -219,7 +228,7 @@
|
|||
'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:forgot unity path': '===Unity oublié pour ce projet.',
|
||||
'projects:toast:forgot unity path': 'Unity oublié pour ce projet.',
|
||||
'projects:toast:invalid project unity version': "Nous n'avons pu détecter aucune version de Unity utilisable.",
|
||||
'projects:toast:loading unity from unity hub': 'Chargement de Unity depuis le Unity Hub...',
|
||||
'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',
|
||||
|
|
@ -282,6 +291,9 @@
|
|||
'settings:default unity arguments': 'Arguments CLI de Unity',
|
||||
'settings:default unity arguments description': 'Ces arguments CLI seront utilisés lorsque vous ouvrirez Unity via ALCOM.',
|
||||
'settings:dialog:default launch arguments': 'Arguments de Untiy par défaut',
|
||||
'settings:dialog:enable show prerelease packages': 'Afficher',
|
||||
'settings:dialog:show prerelease packages': 'Afficher les packets expérimentaux',
|
||||
'settings:dialog:show prerelease packages description': 'Les packets expérimentaux peuvent avoir plus de bugs que les packets stables.<br>Êtes vous sur de vouloir afficher les packets expérimentaux ?',
|
||||
'settings:error:load error': 'Erreur de chargement des réglages.',
|
||||
'settings:files and directories': 'Fichiers et Dossiers',
|
||||
'settings:files and directories:description': 'Accédez rapidement aux fichiers et dossiers relatifs à ALCOM.',
|
||||
|
|
@ -324,7 +336,7 @@
|
|||
'settings:use legacy unity hub loading': 'Utiliser une ancienne version du chargement du Unity Hub',
|
||||
'settings:use legacy unity hub loading description': "L'ancien mode de chargement du Unity Hub est plus fiable pour charger Unity depuis le Unity Hub en comparaison de la nouvelle méthode, cependant l'ancien mode prends plus de temps.<br/>Cet ancien mode sera supprimé dans le futur donc si vous avez des problèmes de chargement avec cette nouvelle méthode, n'hésitez pas a ouvrir une réclamation au développeur dans la section appropriée.",
|
||||
'settings:use vcc scheme description': "Le schéma d'URL <code>vcc:</code> est utilisé pour ouvrir VCC afin d'ajouter un dépot depuis un navigateur WEB.<br>En tant qu'alternative à VCC, vous pouvez utiliser ALCOM en tant qu'application principale.<br>Si cette option est activée, ALCOM enregistrera le schéma d'URL <code>vcc:</code> au démarrage.<br>Après une (ré)installation/mise a jour de VCC, vous devrez redémarrer ALCOM ou manuellement ré-enregistrer VCC avec le bouton d'enregistrement dédié.",
|
||||
'settings:warning:in-local-app-data': 'Ce chemin existe au sein du dossier LocalAppData, il sera supprimé en utilisant "Réinitialiser cet ordinateur" avec l\'option "Conserver mes fichiers".<br>Il est recommandé de stocker vos données dans un autre emplacement.',
|
||||
'settings:warning:in-app-data': 'Ce chemin existe au sein du dossier AppData, il sera supprimé en utilisant "Réinitialiser cet ordinateur" avec l\'option "Conserver mes fichiers".<br>Il est recommandé de stocker vos données dans un autre emplacement.',
|
||||
'settings:warning:non-ascii': 'Cet emplacement contient des caracthères non-ASCII. les caracthères non-ASCII peuvent provoquer des erreurs avec Unity ou des outils tierces. Préférez un autre emplacement si possible.',
|
||||
'settings:warning:whitespace': 'Cet emplacement contient des espaces. Les espaces peuvent provoquer des erreurs avec Unity ou des outils tierces. Préférez un autre emplacement si possible.',
|
||||
'settings:webview version': 'Version de la Webview',
|
||||
|
|
@ -456,4 +468,4 @@
|
|||
'vpm repositories:tooltip:remove curated or official repository': 'Vous ne pouvez pas supprimer les dépots officiels ou épurés par VRChat',
|
||||
'vpm repositories:url': 'URL',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
"general:dialog:select file or directory header": "ファイルまたはディレクトリを選択",
|
||||
"general:dialog:select file or directory": "ファイルまたはディレクトリを選択してください。",
|
||||
"general:toast:invalid directory": "不正なディレクトリが選択されました。",
|
||||
"general:toast:invalid file": "不正なファイルが選択されました。",
|
||||
|
||||
"general:toast:not supported": "{{name}}はまだ実装されていません。",
|
||||
"general:not implemented": "未実装です",
|
||||
|
|
@ -36,6 +37,8 @@
|
|||
"general:error:failed to create dir": "ディレクトリの作成に失敗しました: {{err}}",
|
||||
"general:error:failed to create dir missing drive": "ディレクトリの作成に失敗しました。保存先のドライブの接続を忘れているかもしれません。",
|
||||
|
||||
"general:created at": "追加日",
|
||||
|
||||
"general:last modified": "最終更新日",
|
||||
"general:last modified:moments": "たった今",
|
||||
"general:last modified:minutes": "{{count}}分前",
|
||||
|
|
@ -107,6 +110,7 @@
|
|||
"projects:grid view": "グリッド表示",
|
||||
"projects:sort by": "表示順:",
|
||||
"projects:error:load error": "プロジェクトリストの読み込み中にエラーが発生しました: {{msg}}",
|
||||
"projects:error:noexec filesystem": "プロジェクトが'noexec'フラグ付きでマウントされたファイルシステム上にあります。これにより、Unityのネイティブプラグインの読み込みが妨げられ、破損したアセットが生成されるなどの不具合が発生します。'noexec'フラグが付いていないファイルシステム上にプロジェクトを移動してください。",
|
||||
"projects:toast:project added": "プロジェクトを追加しました。",
|
||||
"projects:toast:project already exists": "このプロジェクトは既に追加されています。",
|
||||
|
||||
|
|
@ -210,6 +214,7 @@
|
|||
"projects:manage:button:unity migrate": "プロジェクトを移行",
|
||||
|
||||
"projects:manage:manage packages": "パッケージ管理",
|
||||
"projects:manage:hidden packages": "非表示のパッケージ",
|
||||
"projects:manage:tooltip:refresh packages": "パッケージリストを再読み込み",
|
||||
"projects:manage:button:upgrade all": "すべて更新(最新)",
|
||||
"projects:manage:button:upgrade all stable": "すべて更新(安定版)",
|
||||
|
|
@ -226,6 +231,7 @@
|
|||
"projects:manage:source not selected": "未選択",
|
||||
"projects:manage:multiple sources": "複数ソース",
|
||||
"projects:manage:tooltip:add package": "パッケージを追加",
|
||||
"projects:manage:tooltip:select repository to install": "パッケージを提供しているリポジトリを選択してください。",
|
||||
"projects:manage:tooltip:upgrade package": "パッケージを更新",
|
||||
"projects:manage:tooltip:remove packages": "パッケージを除去",
|
||||
"projects:manage:tooltip:incompatible with unity": "使用中のUnityと互換性がありません。",
|
||||
|
|
@ -289,12 +295,12 @@
|
|||
"projects:toast:unity migrated": "プロジェクトをUnity 2022に移行しました。",
|
||||
|
||||
"projects:manage:dialog:confirm changes description": "このプロジェクトに以下の変更を加えようとしています。",
|
||||
"projects:manage:dialog:note breaking changes": "破壊的な変更のあるパッケージが含まれています。<br>アセットや設定、その他のツールなどに互換性がない可能性があります。<br>互換性に関する情報を確認し、気を付けて更新してください。",
|
||||
"projects:manage:dialog:note breaking changes": "互換性に影響を与える変更が含まれています。<br>アセットや設定、その他のツールなどの動作が変化する可能性があるため、更新の際は、当該パッケージの互換性に関する情報にご注意ください。<br>更新後に何らかの問題が発生した場合は、パッケージのバージョンを元に戻してみてください。",
|
||||
"projects:manage:dialog:note incompatibility": "他のパッケージと互換性のないパッケージが含まれているため、Unity上でエラーに遭遇する可能性があります。<br>互換性に関する情報を確認し、まとめて更新するか、バックアップを取るなど、気を付けて更新してください。",
|
||||
"projects:manage:dialog:upgrade package": "<b>{{name}}</b> バージョン {{previousVersion}} から <b>{{version}}</b> へアップデート",
|
||||
"projects:manage:dialog:downgrade package": "<b>{{name}}</b> バージョン {{previousVersion}} から <b>{{version}}</b> へダウングレード",
|
||||
"projects:manage:dialog:install package": "<b>{{name}}</b> バージョン {{version}} をインストール",
|
||||
"projects:manage:dialog:breaking changes": "破壊的な変更が含まれます",
|
||||
"projects:manage:dialog:breaking changes": "互換性に影響を与える変更あり",
|
||||
"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> をアンインストール",
|
||||
|
|
@ -429,7 +435,7 @@
|
|||
|
||||
// Reset your PC is a feature in Windows, so please check your translation is consistent with Windows terminology.
|
||||
// It's at Setting app > System > Recovery > Reset this PC
|
||||
"settings:warning:in-local-app-data": "保存先がAppDataフォルダの中にあります。<br>このフォルダは「このPCを初期状態に戻す」でWindowsの初期化を行う際に、「個人用ファイルを保持する」を選択した場合でも削除されてしまいます。<br>そのため、他の場所を保存先にすることを強くお勧めします。",
|
||||
"settings:warning:in-app-data": "保存先がAppDataフォルダの中にあります。<br>このフォルダは「このPCを初期状態に戻す」でWindowsの初期化を行う際に、「個人用ファイルを保持する」を選択した場合でも削除されてしまいます。<br>そのため、他の場所を保存先にすることを強くお勧めします。",
|
||||
"settings:warning:whitespace": "保存先までのパスに空白文字が含まれています。これはUnityやその他のツールで問題を引き起こす場合があります。",
|
||||
"settings:warning:non-ascii": "保存先までのパスに非ASCII文字(半角英数字や半角記号等ではない文字)が含まれています。これはUnityで問題を引き起こす場合があります。",
|
||||
|
||||
|
|
@ -477,7 +483,11 @@
|
|||
"settings:toast:package cache cleared": "パッケージキャッシュを削除しました。",
|
||||
|
||||
"settings:show prerelease": "プレリリース版のパッケージを表示する",
|
||||
"settings:show prerelease description": "プレリリース版のパッケージは、正式リリース前の動作確認等に用いられるパッケージです。<br>この設定を有効にすると、パッケージのバージョンリストにプレリリース版のパッケージも表示されるようになります。<br>また、パッケージの依存関係を解決する際に、プレリリース版のパッケージも含まれるようになります。",
|
||||
"settings:show prerelease description": "この設定を有効にすると、パッケージのバージョン一覧にプレリリース版のものが表示されるようになり、パッケージの依存関係を解決する際にもそれらが使用されるようになります。<br>プレリリース版のパッケージは正式リリース前の動作確認等に用いられるものであり、安定版よりバグに遭遇しやすくなります。",
|
||||
|
||||
"settings:dialog:show prerelease packages": "プレリリース版のパッケージを表示する",
|
||||
"settings:dialog:show prerelease packages description": "プレリリース版のパッケージを使用すると、安定版よりバグに遭遇しやすくなります。<br>プレリリース版のパッケージを表示してもよろしいですか?",
|
||||
"settings:dialog:enable show prerelease packages": "表示する",
|
||||
|
||||
"settings:gui animation": "UIのアニメーションを有効にする",
|
||||
"settings:gui animation description": "ページを移動する際に遷移アニメーションが再生されます。Webviewのバージョンが低い場合は機能しないかもしれません。",
|
||||
|
|
@ -507,10 +517,16 @@
|
|||
|
||||
"check update:dialog:title": "ALCOMのアップデート",
|
||||
"check update:dialog:new version description": "新しいバージョンのALCOMが使用できます!",
|
||||
// note: we may remove x86_64 macos later. This is preparation for this removal
|
||||
"check update:dialog:new version no platform description": "新しいバージョンのALCOMが公開されていますが、ご利用のプラットフォーム向けのビルドが提供されていません。<br>他のプラットフォーム向けのビルドが使用できる場合は、そちらをご利用ください。",
|
||||
"check update:dialog:new version not updatable description": "新しいバージョンのALCOMが公開されていますが、ご利用のインストール方法は自動更新に対応していません。<br>手動での更新をお願いします。",
|
||||
"check update:dialog:new version updater disabled base description": "新しいバージョンのALCOMが公開されています。",
|
||||
"check update:dialog:new version updater how to upgrade fallback": "自動更新が無効化されているため、ご利用のパッケージマネージャーを通して更新してください。",
|
||||
"check update:dialog:current version": "使用中のバージョン:",
|
||||
"check update:dialog:latest version": "最新のバージョン:",
|
||||
"check update:dialog:changelog": "更新履歴",
|
||||
"check update:dialog:dismiss": "今はやめておく",
|
||||
"check update:dialog:open download page": "ダウンロードページを開く",
|
||||
"check update:dialog:update": "アップデート",
|
||||
"check update:dialog:downloading...": "アップデートをダウンロード中...",
|
||||
"check update:dialog:relaunching...": "ALCOMを再起動しています...",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"general:dialog:select file or directory header": "파일 또는 디렉토리 선택",
|
||||
"general:dialog:select file or directory": "파일 또는 디렉토리를 선택해주세요.",
|
||||
"general:toast:invalid directory": "잘못된 디렉토리가 선택되었습니다.",
|
||||
"general:toast:invalid file": "잘못된 파일이 선택되었습니다.",
|
||||
|
||||
"general:toast:not supported": "{{name}}은(는) 아직 지원되지 않습니다.",
|
||||
"general:not implemented": "미구현",
|
||||
|
|
@ -107,6 +108,7 @@
|
|||
"projects:grid view": "그리드 뷰",
|
||||
"projects:sort by": "정렬:",
|
||||
"projects:error:load error": "프로젝트 목록을 불러오는 중 오류가 발생했습니다: {{msg}}",
|
||||
"projects:error:noexec filesystem": "프로젝트가 'noexec' 플래그로 마운트된 파일 시스템에 위치하고 있습니다. 이로 인해 Unity가 네이티브 플러그인을 로드하지 못하여, 에셋이 손상되거나 정상적으로 동작하지 않을 수 있습니다. 'noexec' 플래그가 없는 파일 시스템으로 프로젝트를 이동해 주세요.",
|
||||
"projects:toast:project added": "프로젝트를 추가했습니다.",
|
||||
"projects:toast:project already exists": "이 프로젝트는 이미 추가되어 있습니다.",
|
||||
|
||||
|
|
@ -213,6 +215,7 @@
|
|||
"projects:manage:button:unity migrate": "프로젝트 마이그레이션",
|
||||
|
||||
"projects:manage:manage packages": "패키지 관리",
|
||||
"projects:manage:hidden packages": "숨겨진 패키지",
|
||||
"projects:manage:tooltip:refresh packages": "패키지 목록 새로고침",
|
||||
"projects:manage:button:upgrade all": "모두 업데이트 (최신 버전)",
|
||||
"projects:manage:button:upgrade all stable": "모두 업데이트 (안정 버전)",
|
||||
|
|
@ -229,6 +232,7 @@
|
|||
"projects:manage:source not selected": "미선택",
|
||||
"projects:manage:multiple sources": "여러 소스",
|
||||
"projects:manage:tooltip:add package": "패키지 추가",
|
||||
"projects:manage:tooltip:select repository to install": "설치하려면 패키지를 제공하는 저장소를 선택해주세요.",
|
||||
"projects:manage:tooltip:upgrade package": "패키지 업데이트",
|
||||
"projects:manage:tooltip:remove packages": "패키지 제거",
|
||||
"projects:manage:tooltip:incompatible with unity": "현재 사용중인 Unity와 호환되지 않습니다.",
|
||||
|
|
@ -433,7 +437,7 @@
|
|||
|
||||
// Reset your PC is a feature in Windows, so please check your translation is consistent with Windows terminology.
|
||||
// It's at Setting app > System > Recovery > Reset this PC
|
||||
"settings:warning:in-local-app-data": "경로가 LocalAppData 디렉토리 내에 있습니다.<br>이 디렉토리는 Windows에서 \"이 PC 초기화\"를 실행하고 \"내 파일 유지\" 옵션을 선택해도 삭제되므로 다른 경로로 변경할 것을 권장합니다.",
|
||||
"settings:warning:in-app-data": "경로가 AppData 디렉토리 내에 있습니다.<br>이 디렉토리는 Windows에서 \"이 PC 초기화\"를 실행하고 \"내 파일 유지\" 옵션을 선택해도 삭제되므로 다른 경로로 변경할 것을 권장합니다.",
|
||||
"settings:warning:whitespace": "경로에 공백 문자가 포함되어 있습니다. 일부 Unity 도구에서 문제를 발생시킬 수 있습니다.",
|
||||
"settings:warning:non-ascii": "경로에 비ASCII 문자(영문, 숫자, 일부 특수문자가 아닌 문자)가 포함되어 있습니다. 이는 Unity에서 문제를 발생시킬 수 있습니다.",
|
||||
|
||||
|
|
@ -483,6 +487,10 @@
|
|||
"settings:show prerelease": "프리릴리즈 패키지 표시",
|
||||
"settings:show prerelease description": "프리릴리즈 패키지는 정식 릴리즈 이전에 동작 확인 등을 위해 사용되는 패키지입니다.<br>이 옵션을 활성화하면 패키지 목록에 프리릴리즈 패키지가 표시되고, 패키지를 설치할 때 프리릴리즈 패키지가 설치될 수 있습니다.",
|
||||
|
||||
"settings:dialog:show prerelease packages": "프리릴리즈 패키지 표시",
|
||||
"settings:dialog:show prerelease packages description": "프리릴리즈 패키지는 안정 버전보다 버그가 더 많을 수 있습니다.<br>프리릴리즈 패키지를 표시하시겠습니까?",
|
||||
"settings:dialog:enable show prerelease packages": "표시",
|
||||
|
||||
"settings:gui animation": "UI 애니메이션 활성화",
|
||||
"settings:gui animation description": "페이지 전환 시 애니메이션이 재생됩니다. Webview 버전이 낮은 경우 제대로 동작하지 않을 수 있습니다.",
|
||||
|
||||
|
|
@ -508,10 +516,15 @@
|
|||
|
||||
"check update:dialog:title": "ALCOM 업데이트",
|
||||
"check update:dialog:new version description": "ALCOM의 최신 버전을 사용할 수 있습니다.",
|
||||
"check update:dialog:new version no platform description": "ALCOM의 최신 버전을 사용할 수 있지만, 현재 사용중인 플랫폼은 더 이상 지원되지 않습니다.<br>시스템이 다른 플랫폼 빌드를 지원하는 경우 해당 빌드를 다운로드해 주세요.",
|
||||
"check update:dialog:new version not updatable description": "ALCOM의 최신 버전을 사용할 수 있습니다.<br>그러나 현재 설치 방식에서는 자동 업데이트가 지원되지 않습니다.<br>수동으로 새 버전을 설치해 주세요.",
|
||||
"check update:dialog:new version updater disabled base description": "ALCOM의 최신 버전을 사용할 수 있습니다.",
|
||||
"check update:dialog:new version updater how to upgrade fallback": "자동 업데이트가 비활성화되어 있습니다. 패키지 관리자를 통해 업데이트해 주세요.",
|
||||
"check update:dialog:current version": "사용중인 버전:",
|
||||
"check update:dialog:latest version": "최신 버전:",
|
||||
"check update:dialog:changelog": "변경 사항",
|
||||
"check update:dialog:dismiss": "무시",
|
||||
"check update:dialog:open download page": "다운로드 페이지 열기",
|
||||
"check update:dialog:update": "업데이트",
|
||||
"check update:dialog:downloading...": "업데이트 다운로드 중...",
|
||||
"check update:dialog:relaunching...": "ALCOM을 다시 시작하고 있습니다...",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"general:dialog:select file or directory header": "选择文件或目录",
|
||||
"general:dialog:select file or directory": "请选择文件或目录。",
|
||||
"general:toast:invalid directory": "选择的路径无效。",
|
||||
"general:toast:invalid file": "选择的文件无效。",
|
||||
|
||||
"general:toast:not supported": "{{name}} 尚未支持",
|
||||
"general:not implemented": "该功能未实现",
|
||||
|
|
@ -107,6 +108,7 @@
|
|||
"projects:grid view": "网格视图",
|
||||
"projects:sort by": "排序条件:",
|
||||
"projects:error:load error": "加载项目时出错: {{msg}}",
|
||||
"projects:error:noexec filesystem": "该项目位于一个使用 'noexec' 标志挂载的文件系统。这会阻止 Unity 加载原生插件,导致资产无法正常加载或损坏。请将项目移动到没有 'noexec' 标志的文件系统。",
|
||||
"projects:toast:project added": "添加项目成功。",
|
||||
"projects:toast:project already exists": "该项目已经添加过了。",
|
||||
|
||||
|
|
@ -210,6 +212,7 @@
|
|||
"projects:manage:button:unity migrate": "迁移项目",
|
||||
|
||||
"projects:manage:manage packages": "管理软件包",
|
||||
"projects:manage:hidden packages": "隐藏的软件包",
|
||||
"projects:manage:tooltip:refresh packages": "刷新软件包",
|
||||
"projects:manage:button:upgrade all": "升级所有软件包(最新)",
|
||||
"projects:manage:button:upgrade all stable": "升级所有软件包(稳定)",
|
||||
|
|
@ -226,6 +229,7 @@
|
|||
"projects:manage:source not selected": "未选择",
|
||||
"projects:manage:multiple sources": "多个源",
|
||||
"projects:manage:tooltip:add package": "安装软件包",
|
||||
"projects:manage:tooltip:select repository to install": "选择存储库以安装软件包",
|
||||
"projects:manage:tooltip:upgrade package": "升级软件包",
|
||||
"projects:manage:tooltip:remove packages": "删除软件包",
|
||||
"projects:manage:tooltip:incompatible with unity": "与 Unity 不兼容",
|
||||
|
|
@ -390,15 +394,15 @@
|
|||
"templates:category:builtin": "内置",
|
||||
"templates:tooltip:category:builtin": "内置模板是由 ALCOM 创建并与 ALCOM 一起发布的。",
|
||||
"templates:category:alcom": "自定义",
|
||||
"templates:tooltip:category:alcom": "自定义模板是由你使用 ALCOM 创建和编辑的。",
|
||||
"templates:tooltip:category:alcom": "自定义模板是由您使用 ALCOM 创建和编辑的。",
|
||||
"templates:category:vcc": "VCC",
|
||||
"templates:tooltip:category:vcc": "VCC 模板是由你为 VCC 手动创建的。",
|
||||
"templates:tooltip:category:vcc": "VCC 模板是由您为 VCC 手动创建的。",
|
||||
"templates:tooltip:remove template": "删除模板",
|
||||
"templates:tooltip:remove builtin template": "你不能删除内置模板。",
|
||||
"templates:tooltip:remove vcc template": "你不能在这里删除 VCC 模板。你应该直接删除该模板的目录。",
|
||||
"templates:tooltip:remove builtin template": "您不能删除内置模板。",
|
||||
"templates:tooltip:remove vcc template": "您不能在这里删除 VCC 模板。您应该直接删除该模板的目录。",
|
||||
"templates:menuitem:edit template": "编辑模板",
|
||||
"templates:menuitem:export template": "导出模板",
|
||||
"templates:tooltip:export template with unitypackage": "该模板使用了不可移植的 Unity 资源包(.unitypackage),所以你不可以导出它。",
|
||||
"templates:tooltip:export template with unitypackage": "该模板使用了不可移植的 Unity 资源包(.unitypackage),所以您不可以导出它。",
|
||||
"templates:menuitem:open vcc template": "打开模板目录",
|
||||
"templates:dialog:edit template": "编辑模板",
|
||||
"templates:dialog:create template": "创建模板",
|
||||
|
|
@ -411,7 +415,7 @@
|
|||
"templates:dialog:unitypackages": "Unity 资源包(.unitypackage)",
|
||||
"templates:dialog:no unitypackages": "没有 Unity 资源包",
|
||||
"templates:dialog:remove template": "删除模板",
|
||||
"templates:dialog:confirm remove template": "你确定要删除模板 {{displayName}} 吗?",
|
||||
"templates:dialog:confirm remove template": "您确定要删除模板 {{displayName}} 吗?",
|
||||
"templates:button:save template": "保存模板",
|
||||
"templates:button:import template": "导入模板",
|
||||
"templates:toast:imported n templates": "已导入 {{count}} 个模板",
|
||||
|
|
@ -430,7 +434,7 @@
|
|||
|
||||
// Reset your PC is a feature in Windows, so please check your translation is consistent with Windows terminology.
|
||||
// It's at Setting app > System > Recovery > Reset this PC
|
||||
"settings:warning:in-local-app-data": "该位置位于 LocalAppData 文件夹中,使用\"保留我的文件\"选项的\"重置此电脑\"功能重置系统后,该文件夹将被删除。\n建议选择其他位置。",
|
||||
"settings:warning:in-app-data": "该位置位于 AppData 文件夹中,使用\"保留我的文件\"选项的\"重置此电脑\"功能重置系统后,该文件夹将被删除。\n建议选择其他位置。",
|
||||
"settings:warning:whitespace": "该位置包含空格,可能导致 Unity 或其他工具出现问题。请考虑使用其他位置。",
|
||||
"settings:warning:non-ascii": "该位置包含非 ASCII 字符,可能导致 Unity 出现问题。请考虑使用其他位置。",
|
||||
|
||||
|
|
@ -480,6 +484,10 @@
|
|||
"settings:show prerelease": "显示预发布软件包",
|
||||
"settings:show prerelease description": "启用 \"显示预发布软件包\" 会在软件包列表中显示预发布软件包。此外,在解析依赖关系时也将使用预发布软件包。",
|
||||
|
||||
"settings:dialog:show prerelease packages": "显示预发布软件包",
|
||||
"settings:dialog:show prerelease packages description": "预发布软件包可能不稳定。<br>您确定要显示预发布软件包吗?",
|
||||
"settings:dialog:enable show prerelease packages": "显示",
|
||||
|
||||
"settings:gui animation": "启用 GUI 动画",
|
||||
"settings:gui animation description": "当启用 GUI 动画时,切换页面将会带有动画效果。但系统 WebView 版本过低时可能不起作用。",
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"general:dialog:select file or directory header": "選擇檔案或資料夾",
|
||||
"general:dialog:select file or directory": "請選擇一個檔案或資料夾。",
|
||||
"general:toast:invalid directory": "選擇了無效路徑。",
|
||||
"general:toast:invalid file": "選擇了無效檔案。",
|
||||
|
||||
"general:toast:not supported": "尚未支援 {{name}}。",
|
||||
"general:not implemented": "未實現",
|
||||
|
|
@ -113,6 +114,7 @@
|
|||
"projects:grid view": "網格視圖",
|
||||
"projects:sort by": "排序方式:",
|
||||
"projects:error:load error": "加載專案時發生錯誤: {{msg}}",
|
||||
"projects:error:noexec filesystem": "此專案目前位於一個啟用了 noexec 掛載設定的檔案系統中。這個設定會阻止 Unity 載入原生外掛,可能導致專案無法正常運作,甚至造成素材或資源損毀。請把專案移到沒有啟用 noexec 的磁碟或資料夾中。",
|
||||
"projects:toast:project added": "專案已添加成功。",
|
||||
"projects:toast:project already exists": "已添加過此專案。",
|
||||
|
||||
|
|
@ -216,6 +218,7 @@
|
|||
"projects:manage:button:unity migrate": "遷移專案",
|
||||
|
||||
"projects:manage:manage packages": "管理套件",
|
||||
"projects:manage:hidden packages": "隱藏套件",
|
||||
"projects:manage:tooltip:refresh packages": "刷新套件",
|
||||
"projects:manage:button:upgrade all": "升級全部(最新版)",
|
||||
"projects:manage:button:upgrade all stable": "升級全部(穩定版)",
|
||||
|
|
@ -232,6 +235,7 @@
|
|||
"projects:manage:source not selected": "未選擇",
|
||||
"projects:manage:multiple sources": "多個來源",
|
||||
"projects:manage:tooltip:add package": "添加套件",
|
||||
"projects:manage:tooltip:select repository to install": "選擇儲存庫以安裝套件",
|
||||
"projects:manage:tooltip:upgrade package": "升級套件",
|
||||
"projects:manage:tooltip:remove packages": "移除套件",
|
||||
"projects:manage:tooltip:incompatible with unity": "與 Unity 不相容",
|
||||
|
|
@ -386,7 +390,7 @@
|
|||
"user packages:dialog:remove package": "移除套件",
|
||||
"user packages:dialog:confirm remove description": "你要移除位於 {{path}} 的套件 <b>{{name}}</b> 嗎?",
|
||||
"user packages:dialog:button:remove package": "移除套件",
|
||||
"user packages:toast:invalid selection": "選定的套件無效。",
|
||||
"user packages:toast:invalid selection": "所選套件無效。",
|
||||
"user packages:toast:package already added": "已添加過此套件。",
|
||||
"user packages:toast:package added": "套件添加成功",
|
||||
"user packages:toast:package removed": "套件移除成功。",
|
||||
|
|
@ -440,7 +444,7 @@
|
|||
|
||||
// Reset your PC is a feature in Windows, so please check your translation is consistent with Windows terminology.
|
||||
// It's at Setting app > System > Recovery > Reset this PC
|
||||
"settings:warning:in-local-app-data": "這個位置在資料夾 LocalAppData 中,如果在電腦的設定中執行「重設此電腦」並選擇「保留我的檔案」,此資料夾將被刪除。<br>建議你將資料保存到其他位置。",
|
||||
"settings:warning:in-app-data": "此路徑位於 AppData 資料夾中,即使在「重設此電腦」時選擇「保留我的檔案」,該資料夾仍會被刪除。<br>建議你將資料保存到其他位置。",
|
||||
"settings:warning:whitespace": "此位置含有空格。這可能導致 Unity 和其他工具出現問題。請考慮使用其他位置。",
|
||||
"settings:warning:non-ascii": "此位置含有非 ASCII 字元。這可能導致 Unity 出現問題。請考慮使用其他位置。(不要使用中文或全形字元)",
|
||||
|
||||
|
|
@ -480,7 +484,7 @@
|
|||
"settings:backup:format:zip-store": "未壓縮的 zip(快)",
|
||||
"settings:backup:format:zip-fast": "低壓縮率的 zip(慢)",
|
||||
"settings:backup:format:zip-best": "高壓縮率的 zip(最慢)",
|
||||
"settings:backup:exclude vpm packages from backup": "備份裡不含有 VPM 套件",
|
||||
"settings:backup:exclude vpm packages from backup": "備份裡不含 VPM 套件",
|
||||
"settings:backup:exclude vpm packages from backup description": "這樣可以減少備份的大小,但若套件作者違反建議,從他們的儲存庫中移除了某套件,那在還原備份時你就必須改用其他版本的該套件。",
|
||||
|
||||
"settings:packages": "套件",
|
||||
|
|
@ -490,6 +494,10 @@
|
|||
"settings:show prerelease": "顯示預先發行版套件",
|
||||
"settings:show prerelease description": "啟用「顯示預先發行版套件」將在套件清單中顯示預先發行版的套件。此外,在解析依賴關係時也將使用預先發行版的套件。",
|
||||
|
||||
"settings:dialog:show prerelease packages": "顯示預先發行版套件",
|
||||
"settings:dialog:show prerelease packages description": "預先發行版套件可能比穩定版套件更容易出現問題。<br>確定要顯示預先發行版套件嗎?",
|
||||
"settings:dialog:enable show prerelease packages": "顯示",
|
||||
|
||||
"settings:gui animation": "啟用 GUI 動畫",
|
||||
"settings:gui animation description": "啟用 GUI 動畫時,頁面切換將帶有動畫效果。但 Webview 版本過舊時可能無效。",
|
||||
|
||||
|
|
@ -517,11 +525,17 @@
|
|||
"check update:toast:no updates": "無可用更新。",
|
||||
|
||||
"check update:dialog:title": "ALCOM 更新",
|
||||
"check update:dialog:new version description": "有可用的新版 ALCOM",
|
||||
"check update:dialog:new version description": "ALCOM 有新版本可用",
|
||||
// note: we may remove x86_64 macos later. This is preparation for this removal
|
||||
"check update:dialog:new version no platform description": "ALCOM 有新版本可用,但已不再支援你的平台。<br>如果你的系統支援其他平台版本,請下載對應版本。",
|
||||
"check update:dialog:new version not updatable description": "ALCOM 有新版本可用。<br>但你目前使用的安裝方式無法自動更新。<br>請手動安裝新版本。",
|
||||
"check update:dialog:new version updater disabled base description": "ALCOM 有新版本可用。",
|
||||
"check update:dialog:new version updater how to upgrade fallback": "Automatic updates are disabled. Please upgrade using your package manager.",
|
||||
"check update:dialog:current version": "目前版本:",
|
||||
"check update:dialog:latest version": "最新版本:",
|
||||
"check update:dialog:changelog": "變更日誌",
|
||||
"check update:dialog:dismiss": "忽略",
|
||||
"check update:dialog:open download page": "開啟下載頁面",
|
||||
"check update:dialog:update": "下載並更新!",
|
||||
"check update:dialog:downloading...": "下載更新中...",
|
||||
"check update:dialog:relaunching...": "重新啟動 ALCOM 中...",
|
||||
|
|
|
|||
2648
vrc-get-gui/package-lock.json
generated
2648
vrc-get-gui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,9 @@
|
|||
"lint": "tsc && biome lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@radix-ui/react-accordion": "^1",
|
||||
"@radix-ui/react-checkbox": "^1",
|
||||
"@radix-ui/react-dialog": "^1",
|
||||
|
|
@ -28,35 +31,35 @@
|
|||
"@radix-ui/react-slot": "^1",
|
||||
"@radix-ui/react-tooltip": "^1",
|
||||
"@tanstack/react-query": "^5",
|
||||
"@tanstack/react-router": "^1.168.3",
|
||||
"@tanstack/router-devtools": "^1.166.11",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tanstack/react-router": "^1.170.10",
|
||||
"@tanstack/router-devtools": "^1.167.0",
|
||||
"@tauri-apps/api": "2.11.0",
|
||||
"@uidotdev/usehooks": "^2",
|
||||
"class-variance-authority": "^0.7",
|
||||
"classnames": "^2",
|
||||
"clsx": "^2",
|
||||
"cmdk": "^1",
|
||||
"i18next": "^25",
|
||||
"i18next": "^26",
|
||||
"lucide-react": "^1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-i18next": "^16",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
"react-i18next": "^17",
|
||||
"react-toastify": "^11",
|
||||
"tailwind-merge": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/router-plugin": "^1.167.4",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@rollup/pluginutils": "^5.4.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@tanstack/router-plugin": "^1.168.13",
|
||||
"@tauri-apps/cli": "2.11.2",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react": "19.2.16",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"json5": "^2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-json5": "^1.2.0"
|
||||
"typescript": "~6.0",
|
||||
"vite": "^8.0.16"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -797,7 +797,7 @@ PlayerSettings:
|
|||
webGLMemoryGeometricGrowthCap: 96
|
||||
webGLPowerPreference: 2
|
||||
scriptingDefineSymbols:
|
||||
Android: VRC_SDK_VRCSDK3;UDON;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
Android: VRC_SDK_VRCSDK3;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
EmbeddedLinux: UNITY_POST_PROCESSING_STACK_V2
|
||||
GameCoreXboxOne: UNITY_POST_PROCESSING_STACK_V2
|
||||
Nintendo Switch: UNITY_POST_PROCESSING_STACK_V2
|
||||
|
|
@ -805,11 +805,11 @@ PlayerSettings:
|
|||
PS5: UNITY_POST_PROCESSING_STACK_V2
|
||||
QNX: UNITY_POST_PROCESSING_STACK_V2
|
||||
Stadia: UNITY_POST_PROCESSING_STACK_V2
|
||||
Standalone: VRC_SDK_VRCSDK3;UDON;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
Standalone: VRC_SDK_VRCSDK3;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
VisionOS: UNITY_POST_PROCESSING_STACK_V2
|
||||
WebGL: UNITY_POST_PROCESSING_STACK_V2
|
||||
XboxOne: UNITY_POST_PROCESSING_STACK_V2
|
||||
iPhone: VRC_SDK_VRCSDK3;UDON;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
iPhone: VRC_SDK_VRCSDK3;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
tvOS: UNITY_POST_PROCESSING_STACK_V2
|
||||
additionalCompilerArguments: {}
|
||||
platformArchitecture: {}
|
||||
|
|
|
|||
|
|
@ -791,7 +791,7 @@ PlayerSettings:
|
|||
webGLMemoryGeometricGrowthCap: 96
|
||||
webGLPowerPreference: 2
|
||||
scriptingDefineSymbols:
|
||||
Android: VRC_SDK_VRCSDK3;UDON;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
Android: VRC_SDK_VRCSDK3;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
EmbeddedLinux: UNITY_POST_PROCESSING_STACK_V2
|
||||
GameCoreXboxOne: UNITY_POST_PROCESSING_STACK_V2
|
||||
Nintendo Switch: UNITY_POST_PROCESSING_STACK_V2
|
||||
|
|
@ -799,11 +799,11 @@ PlayerSettings:
|
|||
PS5: UNITY_POST_PROCESSING_STACK_V2
|
||||
QNX: UNITY_POST_PROCESSING_STACK_V2
|
||||
Stadia: UNITY_POST_PROCESSING_STACK_V2
|
||||
Standalone: VRC_SDK_VRCSDK3;UDON;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
Standalone: VRC_SDK_VRCSDK3;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
VisionOS: UNITY_POST_PROCESSING_STACK_V2
|
||||
WebGL: UNITY_POST_PROCESSING_STACK_V2
|
||||
XboxOne: UNITY_POST_PROCESSING_STACK_V2
|
||||
iPhone: VRC_SDK_VRCSDK3;UDON;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
iPhone: VRC_SDK_VRCSDK3;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
tvOS: UNITY_POST_PROCESSING_STACK_V2
|
||||
additionalCompilerArguments: {}
|
||||
platformArchitecture: {}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { exec as execCallback } from "node:child_process";
|
|||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { LoadResult, ResolveIdResult } from "rollup";
|
||||
import type { LoadResult, ResolveIdResult } from "rolldown";
|
||||
import { normalizePath, type Plugin } from "vite";
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
|
@ -97,6 +96,7 @@ pub(crate) fn handlers() -> impl Fn(Invoke) -> bool + Send + Sync + 'static {
|
|||
environment::packages::environment_download_repository,
|
||||
environment::packages::environment_add_repository,
|
||||
environment::packages::environment_remove_repository,
|
||||
environment::packages::environment_reorder_repositories,
|
||||
environment::packages::environment_import_repository_pick,
|
||||
environment::packages::environment_import_download_repositories,
|
||||
environment::packages::environment_import_add_repositories,
|
||||
|
|
@ -120,6 +120,7 @@ pub(crate) fn handlers() -> impl Fn(Invoke) -> bool + Send + Sync + 'static {
|
|||
environment::settings::environment_set_default_unity_arguments,
|
||||
environment::templates::environment_export_template,
|
||||
environment::templates::environment_get_alcom_template,
|
||||
environment::templates::environment_pick_unity_packages,
|
||||
environment::templates::environment_pick_unity_package,
|
||||
environment::templates::environment_save_template,
|
||||
environment::templates::environment_remove_template,
|
||||
|
|
@ -147,6 +148,7 @@ pub(crate) fn handlers() -> impl Fn(Invoke) -> bool + Send + Sync + 'static {
|
|||
project::project_set_unity_path,
|
||||
util::util_open,
|
||||
util::util_open_url,
|
||||
util::util_open_url_nocheck,
|
||||
util::util_get_log_entries,
|
||||
util::util_get_version,
|
||||
util::util_check_for_update,
|
||||
|
|
@ -204,6 +206,7 @@ pub(crate) fn export_ts() {
|
|||
environment::packages::environment_download_repository,
|
||||
environment::packages::environment_add_repository,
|
||||
environment::packages::environment_remove_repository,
|
||||
environment::packages::environment_reorder_repositories,
|
||||
environment::packages::environment_import_repository_pick,
|
||||
environment::packages::environment_import_download_repositories,
|
||||
environment::packages::environment_import_add_repositories,
|
||||
|
|
@ -227,6 +230,7 @@ pub(crate) fn export_ts() {
|
|||
environment::settings::environment_set_default_unity_arguments,
|
||||
environment::templates::environment_export_template,
|
||||
environment::templates::environment_get_alcom_template,
|
||||
environment::templates::environment_pick_unity_packages,
|
||||
environment::templates::environment_pick_unity_package,
|
||||
environment::templates::environment_save_template,
|
||||
environment::templates::environment_remove_template,
|
||||
|
|
@ -254,6 +258,7 @@ pub(crate) fn export_ts() {
|
|||
project::project_set_unity_path,
|
||||
util::util_open,
|
||||
util::util_open_url,
|
||||
util::util_open_url_nocheck,
|
||||
util::util_get_log_entries,
|
||||
util::util_get_version,
|
||||
util::util_check_for_update,
|
||||
|
|
@ -266,13 +271,9 @@ pub(crate) fn export_ts() {
|
|||
crate::deep_link_support::deep_link_imported_clear_non_toasted_count,
|
||||
crate::deep_link_support::deep_link_reduce_imported_clear_non_toasted_count,
|
||||
])
|
||||
//.typ::<uri_custom_scheme::GlobalInfo>() // https://github.com/specta-rs/specta/issues/281
|
||||
.typ::<uri_custom_scheme::GlobalInfo>()
|
||||
.typ::<environment::projects::TauriUpdatedRealProjectInfo>()
|
||||
.export(
|
||||
specta_typescript::Typescript::default()
|
||||
.bigint(specta_typescript::BigIntExportBehavior::Number),
|
||||
export_path,
|
||||
)
|
||||
.export(specta_typescript::Typescript::default(), export_path)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -290,13 +291,14 @@ async fn update_project_last_modified(io: &DefaultEnvironmentIo, project_dir: &P
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, specta::Type)]
|
||||
#[specta(export)]
|
||||
#[specta(collect)]
|
||||
#[serde(tag = "type")]
|
||||
enum RustError {
|
||||
Unrecoverable {
|
||||
message: String,
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
#[specta(type = LocalizableRustError)]
|
||||
Localizable(Box<LocalizableRustError>),
|
||||
Handleable {
|
||||
message: String,
|
||||
|
|
@ -320,11 +322,16 @@ enum HandleableRustError {
|
|||
}
|
||||
|
||||
impl RustError {
|
||||
fn unrecoverable<T: Display>(value: T) -> Self {
|
||||
error!("{value}");
|
||||
Self::Unrecoverable {
|
||||
message: value.to_string(),
|
||||
}
|
||||
fn unrecoverable<T: std::error::Error>(value: T) -> Self {
|
||||
let message = Self::display_error(&value);
|
||||
error!("{message}");
|
||||
Self::Unrecoverable { message }
|
||||
}
|
||||
|
||||
fn unrecoverable_str<T: Into<String>>(value: T) -> Self {
|
||||
let message = value.into();
|
||||
error!("{message}");
|
||||
Self::Unrecoverable { message }
|
||||
}
|
||||
|
||||
fn handleable(message: String, body: HandleableRustError) -> Self {
|
||||
|
|
@ -332,6 +339,25 @@ impl RustError {
|
|||
Self::Handleable { message, body }
|
||||
}
|
||||
|
||||
// formats the error but with inner error message included
|
||||
fn display_error<T: std::error::Error>(e: T) -> String {
|
||||
let mut message = format!("{e}");
|
||||
|
||||
let mut cur = e.source();
|
||||
while let Some(src) = cur {
|
||||
let src_msg = format!("{src}");
|
||||
|
||||
if !message.contains(&src_msg) {
|
||||
message.push_str(": ");
|
||||
message.push_str(src_msg.as_str());
|
||||
}
|
||||
|
||||
cur = src.source();
|
||||
}
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
fn handleable_missing_dependencies(
|
||||
message: String,
|
||||
dependencies: Vec<(Box<str>, VersionRange)>,
|
||||
|
|
@ -362,13 +388,18 @@ macro_rules! impl_from_error {
|
|||
|
||||
impl_from_error!(
|
||||
io::Error,
|
||||
String,
|
||||
async_zip::error::ZipError,
|
||||
vrc_get_vpm::environment::AddRepositoryErr,
|
||||
vrc_get_vpm::unity_project::RemovePackageErr,
|
||||
fs_extra::error::Error,
|
||||
);
|
||||
|
||||
impl From<String> for RustError {
|
||||
fn from(value: String) -> Self {
|
||||
RustError::unrecoverable_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::compressor::CompressError> for RustError {
|
||||
fn from(value: crate::compressor::CompressError) -> Self {
|
||||
match value {
|
||||
|
|
@ -383,7 +414,7 @@ impl From<crate::compressor::CompressError> for RustError {
|
|||
impl From<crate::updater::Error> for RustError {
|
||||
fn from(value: crate::updater::Error) -> Self {
|
||||
log::error!(gui_toast = false; "updater error: {value}");
|
||||
Self::unrecoverable("failed to load the latest release")
|
||||
Self::unrecoverable_str("failed to load the latest release")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +443,7 @@ impl From<ReinstalPackagesError> for RustError {
|
|||
ReinstalPackagesError::DependenciesNotFound { dependencies } => {
|
||||
RustError::handleable_missing_dependencies(message, dependencies)
|
||||
}
|
||||
_ => RustError::unrecoverable(message),
|
||||
_ => RustError::unrecoverable(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -424,7 +455,7 @@ impl From<AddPackageErr> for RustError {
|
|||
AddPackageErr::DependenciesNotFound { dependencies } => {
|
||||
RustError::handleable_missing_dependencies(message, dependencies)
|
||||
}
|
||||
_ => RustError::unrecoverable(message),
|
||||
_ => RustError::unrecoverable(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -436,7 +467,7 @@ impl From<ResolvePackageErr> for RustError {
|
|||
ResolvePackageErr::DependenciesNotFound { dependencies } => {
|
||||
RustError::handleable_missing_dependencies(message, dependencies)
|
||||
}
|
||||
_ => RustError::unrecoverable(message),
|
||||
_ => RustError::unrecoverable(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -477,6 +508,10 @@ struct TauriBasePackageInfo {
|
|||
is_yanked: bool,
|
||||
}
|
||||
|
||||
fn safe_url(url: &url::Url) -> bool {
|
||||
matches!(url.scheme(), "http" | "https")
|
||||
}
|
||||
|
||||
impl TauriBasePackageInfo {
|
||||
fn new(package: &PackageManifest) -> Self {
|
||||
Self {
|
||||
|
|
@ -488,8 +523,14 @@ impl TauriBasePackageInfo {
|
|||
.collect(),
|
||||
version: package.version().into(),
|
||||
unity: package.unity().map(|v| (v.major(), v.minor())),
|
||||
changelog_url: package.changelog_url().map(|v| v.to_string()),
|
||||
documentation_url: package.documentation_url().map(|v| v.to_string()),
|
||||
changelog_url: package
|
||||
.changelog_url()
|
||||
.take_if(|x| safe_url(x))
|
||||
.map(|v| v.to_string()),
|
||||
documentation_url: package
|
||||
.documentation_url()
|
||||
.take_if(|x| safe_url(x))
|
||||
.map(|v| v.to_string()),
|
||||
vpm_dependencies: package
|
||||
.vpm_dependencies()
|
||||
.keys()
|
||||
|
|
@ -551,7 +592,7 @@ impl IntoPathBuf for tauri_plugin_dialog::FilePath {
|
|||
match self {
|
||||
Self::Url(url) => url
|
||||
.to_file_path()
|
||||
.map_err(|_| RustError::unrecoverable("internal error: bad file url")),
|
||||
.map_err(|_| RustError::unrecoverable_str("internal error: bad file url")),
|
||||
Self::Path(p) => Ok(p),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::commands::async_command::{AsyncCallResult, With, async_command};
|
||||
use crate::commands::prelude::*;
|
||||
use futures::future::{join_all, try_join_all};
|
||||
use futures::future::try_join_all;
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use log::info;
|
||||
|
|
@ -18,7 +18,7 @@ use vrc_get_vpm::environment::{
|
|||
use vrc_get_vpm::io::{DefaultEnvironmentIo, IoTrait};
|
||||
use vrc_get_vpm::repositories_file::RepositoriesFile;
|
||||
use vrc_get_vpm::repository::RemoteRepository;
|
||||
use vrc_get_vpm::{HttpClient, VersionSelector};
|
||||
use vrc_get_vpm::{HttpClient, UserRepoSetting, VersionSelector};
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
|
@ -58,6 +58,7 @@ pub async fn environment_packages(
|
|||
|
||||
#[derive(Serialize, specta::Type)]
|
||||
struct TauriUserRepository {
|
||||
index: usize,
|
||||
id: String,
|
||||
url: Option<String>,
|
||||
display_name: String,
|
||||
|
|
@ -87,9 +88,11 @@ pub async fn environment_repositories_info(
|
|||
let user_repositories = settings
|
||||
.get_user_repos()
|
||||
.iter()
|
||||
.map(|x| {
|
||||
.enumerate()
|
||||
.map(|(index, x)| {
|
||||
let id = x.id().or(x.url().map(Url::as_str)).unwrap();
|
||||
TauriUserRepository {
|
||||
index,
|
||||
id: id.to_string(),
|
||||
url: x.url().map(|x| x.to_string()),
|
||||
display_name: x.name().unwrap_or(id).to_string(),
|
||||
|
|
@ -345,24 +348,48 @@ pub async fn environment_add_repository(
|
|||
Ok(TauriAddRepositoryResult::Success)
|
||||
}
|
||||
|
||||
// Verifies that the repo at `index` in the freshly-loaded settings still has
|
||||
// the `expected_id` the frontend last saw. Guards against silent corruption
|
||||
// from external writes to settings.json between fetch and mutation.
|
||||
fn verify_repo_at_index(
|
||||
repos: &[UserRepoSetting],
|
||||
index: usize,
|
||||
expected_id: &str,
|
||||
) -> Result<(), RustError> {
|
||||
let Some(repo) = repos.get(index) else {
|
||||
return Err(RustError::unrecoverable_str(format!(
|
||||
"Repository index {index} out of range (expected id {expected_id}). \
|
||||
settings.json was likely modified externally; please refresh."
|
||||
)));
|
||||
};
|
||||
let actual = repo.id().or(repo.url().map(Url::as_str));
|
||||
if actual != Some(expected_id) {
|
||||
return Err(RustError::unrecoverable_str(format!(
|
||||
"Repository at index {index} changed (expected id {expected_id}, found {actual:?}). \
|
||||
settings.json was likely modified externally; please refresh."
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn environment_remove_repository(
|
||||
settings: State<'_, SettingsState>,
|
||||
packages: State<'_, PackagesState>,
|
||||
io: State<'_, DefaultEnvironmentIo>,
|
||||
id: String,
|
||||
index: usize,
|
||||
expected_id: String,
|
||||
) -> Result<(), RustError> {
|
||||
let mut settings = settings.load_mut(io.inner()).await?;
|
||||
|
||||
let removed = settings.remove_repo(|r| r.id() == Some(id.as_str()));
|
||||
verify_repo_at_index(settings.get_user_repos(), index, &expected_id)?;
|
||||
|
||||
join_all(
|
||||
removed
|
||||
.iter()
|
||||
.map(|x| async { io.remove_file(x.local_path()).await.ok() }),
|
||||
)
|
||||
.await;
|
||||
let removed = settings.remove_repo_at_index(index);
|
||||
|
||||
if let Some(repo) = &removed {
|
||||
io.remove_file(repo.local_path()).await.ok();
|
||||
}
|
||||
|
||||
settings.save().await?;
|
||||
|
||||
|
|
@ -390,6 +417,35 @@ pub struct TauriRepositoryDescriptor {
|
|||
pub headers: Headers,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, specta::Type)]
|
||||
pub struct TauriUserRepositoryRef {
|
||||
pub index: usize,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn environment_reorder_repositories(
|
||||
settings: State<'_, SettingsState>,
|
||||
packages: State<'_, PackagesState>,
|
||||
io: State<'_, DefaultEnvironmentIo>,
|
||||
repos: Vec<TauriUserRepositoryRef>,
|
||||
) -> Result<(), RustError> {
|
||||
let mut settings = settings.load_mut(io.inner()).await?;
|
||||
log::debug!("reorder user repositories: {} entries", repos.len());
|
||||
|
||||
let user_repos = settings.get_user_repos();
|
||||
for r in &repos {
|
||||
verify_repo_at_index(user_repos, r.index, &r.id)?;
|
||||
}
|
||||
|
||||
let indices: Vec<usize> = repos.into_iter().map(|r| r.index).collect();
|
||||
settings.reorder_user_repos_by_indices(&indices);
|
||||
settings.save().await?;
|
||||
packages.clear_cache();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn environment_import_repository_pick(
|
||||
|
|
@ -537,7 +593,7 @@ pub async fn environment_export_repositories(
|
|||
.file()
|
||||
.set_parent(&window)
|
||||
.add_filter("Text", &["txt"])
|
||||
.set_file_name("repositories")
|
||||
.set_file_name("repositories.txt")
|
||||
.blocking_save_file()
|
||||
.map(|x| x.into_path_buf())
|
||||
.transpose()?
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use std::sync::atomic::AtomicUsize;
|
|||
use std::time::Instant;
|
||||
use tauri::{AppHandle, Emitter, Manager, State, Window};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use tokio::sync::Semaphore;
|
||||
use vrc_get_vpm::ProjectType;
|
||||
use vrc_get_vpm::environment::{
|
||||
InvalidRealProjectInformation, PackageInstaller, RealProjectInformation, Settings, UserProject,
|
||||
|
|
@ -349,7 +350,7 @@ pub async fn environment_remove_project_by_path(
|
|||
let mut connection = VccDatabaseConnection::connect(io.inner()).await?;
|
||||
migrate_sanitize_projects(&mut connection, io.inner(), &settings).await?;
|
||||
let Some(project) = connection.find_project(&project_path).unwrap() else {
|
||||
return Err(RustError::unrecoverable("project not found"));
|
||||
return Err(RustError::unrecoverable_str("project not found"));
|
||||
};
|
||||
connection.remove_project(&project);
|
||||
connection.save(io.inner()).await?;
|
||||
|
|
@ -441,7 +442,7 @@ where
|
|||
let source_path = Path::new(&source_path_str);
|
||||
|
||||
let Some(new_path) = create_folder(source_path.into()).await else {
|
||||
return Err(RustError::unrecoverable(
|
||||
return Err(RustError::unrecoverable_str(
|
||||
"failed to create a new folder for migration",
|
||||
));
|
||||
};
|
||||
|
|
@ -464,6 +465,7 @@ where
|
|||
proceed: AtomicUsize,
|
||||
total_files: usize,
|
||||
new_path: &'a Path,
|
||||
semaphore: Semaphore,
|
||||
ctx: &'a AsyncCommandContext<TauriCopyProjectProgress>,
|
||||
}
|
||||
|
||||
|
|
@ -487,15 +489,19 @@ where
|
|||
let new_entry = self.new_path.join(entry.relative_path());
|
||||
|
||||
if entry.is_dir() {
|
||||
let permission = self.semaphore.acquire().await.unwrap();
|
||||
if let Err(e) = tokio::fs::create_dir(&new_entry).await
|
||||
&& e.kind() != io::ErrorKind::AlreadyExists
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
drop(permission);
|
||||
|
||||
try_join_all(entry.iter().map(|x| self.process(x))).await?;
|
||||
} else {
|
||||
let permission = self.semaphore.acquire().await.unwrap();
|
||||
tokio::fs::copy(entry.absolute_path(), new_entry).await?;
|
||||
drop(permission);
|
||||
|
||||
self.on_finish(entry);
|
||||
}
|
||||
|
|
@ -504,10 +510,17 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let parallelism = std::thread::available_parallelism()
|
||||
.map(|x| x.get() * 2)
|
||||
.unwrap_or(4);
|
||||
|
||||
info!("Copying project with parallelism: {parallelism}");
|
||||
|
||||
CopyFileContext {
|
||||
proceed: AtomicUsize::new(0),
|
||||
total_files,
|
||||
new_path,
|
||||
semaphore: Semaphore::new(parallelism),
|
||||
ctx: &ctx,
|
||||
}
|
||||
.process(&file_tree)
|
||||
|
|
@ -545,7 +558,7 @@ pub async fn environment_set_favorite_project(
|
|||
) -> Result<(), RustError> {
|
||||
let mut connection = VccDatabaseConnection::connect(io.inner()).await?;
|
||||
let Some(mut project) = connection.find_project(&project_path).unwrap() else {
|
||||
return Err(RustError::unrecoverable("project not found"));
|
||||
return Err(RustError::unrecoverable_str("project not found"));
|
||||
};
|
||||
project.set_favorite(favorite);
|
||||
connection.update_project(&project);
|
||||
|
|
@ -719,10 +732,10 @@ pub async fn environment_create_project(
|
|||
|
||||
let templates = templates
|
||||
.get_versioned(template_version)
|
||||
.ok_or_else(|| RustError::unrecoverable("Templates info version mismatch (bug)"))?;
|
||||
.ok_or_else(|| RustError::unrecoverable_str("Templates info version mismatch (bug)"))?;
|
||||
|
||||
let unity_version = UnityVersion::parse(&unity_version)
|
||||
.ok_or_else(|| RustError::unrecoverable("Bad Unity Version (unparsable)"))?;
|
||||
.ok_or_else(|| RustError::unrecoverable_str("Bad Unity Version (unparsable)"))?;
|
||||
|
||||
let base_path = Path::new(&base_path);
|
||||
let base_path = {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crate::templates;
|
|||
use crate::templates::{
|
||||
AlcomTemplate, new_user_template_id, parse_alcom_template, serialize_alcom_template,
|
||||
};
|
||||
use crate::utils::trash_delete;
|
||||
use crate::utils::{find_existing_parent_dir_or_home, trash_delete};
|
||||
use futures::AsyncWriteExt;
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
|
|
@ -33,7 +33,7 @@ pub async fn environment_export_template(
|
|||
.and_then(|x| x.iter().find(|x| x.id == id))
|
||||
.take_if(|x| x.source_path.is_some())
|
||||
else {
|
||||
return Err(RustError::unrecoverable(
|
||||
return Err(RustError::unrecoverable_str(
|
||||
"Template with such id not found (this is bug)",
|
||||
));
|
||||
};
|
||||
|
|
@ -41,7 +41,7 @@ pub async fn environment_export_template(
|
|||
.dialog()
|
||||
.file()
|
||||
.set_parent(&window)
|
||||
.set_file_name(&template.display_name)
|
||||
.set_file_name(format!("{}.alcomtemplate", &template.display_name))
|
||||
.add_filter("ALCOM Project Template", &["alcomtemplate"])
|
||||
.blocking_save_file()
|
||||
.map(|x| x.into_path_buf())
|
||||
|
|
@ -98,7 +98,7 @@ pub async fn environment_get_alcom_template(
|
|||
.and_then(|x| x.iter().find(|x| x.id == id))
|
||||
.and_then(|x| x.alcom_template.as_ref())
|
||||
{
|
||||
None => Err(RustError::unrecoverable(
|
||||
None => Err(RustError::unrecoverable_str(
|
||||
"Template with such id not found (this is bug)",
|
||||
)),
|
||||
Some(template) => Ok(template.into()),
|
||||
|
|
@ -107,7 +107,7 @@ pub async fn environment_get_alcom_template(
|
|||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn environment_pick_unity_package(window: Window) -> Result<Vec<String>, RustError> {
|
||||
pub async fn environment_pick_unity_packages(window: Window) -> Result<Vec<String>, RustError> {
|
||||
window
|
||||
.dialog()
|
||||
.file()
|
||||
|
|
@ -121,6 +121,40 @@ pub async fn environment_pick_unity_package(window: Window) -> Result<Vec<String
|
|||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
#[derive(Serialize, specta::Type)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum TauriPickUnityPackageResult {
|
||||
NoFolderSelected,
|
||||
InvalidSelection,
|
||||
Successful { new_path: String },
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn environment_pick_unity_package(
|
||||
window: Window,
|
||||
current: String,
|
||||
) -> Result<TauriPickUnityPackageResult, RustError> {
|
||||
let Some(path) = window
|
||||
.dialog()
|
||||
.file()
|
||||
.set_parent(&window)
|
||||
.set_directory(find_existing_parent_dir_or_home(current.as_ref()))
|
||||
.add_filter("Unity Package", &["unitypackage"])
|
||||
.blocking_pick_file()
|
||||
.map(|x| x.into_path_buf())
|
||||
.transpose()?
|
||||
else {
|
||||
return Ok(TauriPickUnityPackageResult::NoFolderSelected);
|
||||
};
|
||||
|
||||
let Ok(path) = path.into_os_string().into_string() else {
|
||||
return Ok(TauriPickUnityPackageResult::InvalidSelection);
|
||||
};
|
||||
|
||||
Ok(TauriPickUnityPackageResult::Successful { new_path: path })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
|
@ -140,7 +174,7 @@ pub async fn environment_save_template(
|
|||
id: Some(id.clone().unwrap_or_else(new_user_template_id)),
|
||||
base,
|
||||
unity_version: Some(VersionRange::from_str(&unity_range).map_err(|x| {
|
||||
RustError::unrecoverable(format!("Bad Unity Version Range ({unity_range}): {x}"))
|
||||
RustError::unrecoverable_str(format!("Bad Unity Version Range ({unity_range}): {x}"))
|
||||
})?),
|
||||
vpm_dependencies: vpm_packages
|
||||
.into_iter()
|
||||
|
|
@ -148,7 +182,7 @@ pub async fn environment_save_template(
|
|||
Ok::<_, RustError>((
|
||||
pkg,
|
||||
VersionRange::from_str(&range).map_err(|x| {
|
||||
RustError::unrecoverable(format!("Bad Version Range ({range}): {x}"))
|
||||
RustError::unrecoverable_str(format!("Bad Version Range ({range}): {x}"))
|
||||
})?,
|
||||
))
|
||||
})
|
||||
|
|
@ -157,7 +191,7 @@ pub async fn environment_save_template(
|
|||
};
|
||||
|
||||
let template = serialize_alcom_template(template)
|
||||
.map_err(|x| RustError::unrecoverable(format!("Failed to serialize template: {x}")))?;
|
||||
.map_err(|x| RustError::unrecoverable_str(format!("Failed to serialize template: {x}")))?;
|
||||
|
||||
if let Some(id) = id {
|
||||
// There is id; overwrite existing one
|
||||
|
|
@ -167,7 +201,7 @@ pub async fn environment_save_template(
|
|||
.and_then(|x| x.iter().find(|x| x.id == id))
|
||||
.and_then(|x| x.source_path.as_ref())
|
||||
else {
|
||||
return Err(RustError::unrecoverable(
|
||||
return Err(RustError::unrecoverable_str(
|
||||
"Template with such id not found (this is bug)",
|
||||
));
|
||||
};
|
||||
|
|
@ -263,7 +297,7 @@ pub async fn environment_remove_template(
|
|||
.take_if(|x| x.alcom_template.is_some())
|
||||
.take_if(|x| x.source_path.is_some())
|
||||
{
|
||||
None => Err(RustError::unrecoverable(
|
||||
None => Err(RustError::unrecoverable_str(
|
||||
"Template with such id not found (this is bug)",
|
||||
)),
|
||||
Some(template) => {
|
||||
|
|
@ -356,6 +390,7 @@ pub struct TauriImportDuplicated {
|
|||
importing_name: String,
|
||||
importing_update_date: Option<chrono::DateTime<chrono::offset::Utc>>,
|
||||
#[serde_as(as = "serde_with::base64::Base64")]
|
||||
#[specta(type = &str)]
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ pub async fn project_install_packages(
|
|||
) -> Result<TauriPendingProjectChanges, RustError> {
|
||||
let settings = settings.load(io.inner()).await?;
|
||||
let Some(packages) = packages.get() else {
|
||||
return Err(RustError::unrecoverable(
|
||||
return Err(RustError::unrecoverable_str(
|
||||
"Internal Error: environment version mismatch",
|
||||
));
|
||||
};
|
||||
|
|
@ -194,7 +194,7 @@ pub async fn project_install_packages(
|
|||
.map(|(id, v)| Some((id, Version::from_str(&v).ok()?)))
|
||||
.collect::<Option<Vec<_>>>()
|
||||
else {
|
||||
return Err(RustError::unrecoverable("bad version file"));
|
||||
return Err(RustError::unrecoverable_str("bad version file"));
|
||||
};
|
||||
|
||||
changes!(packages, changes, |collection, packages| {
|
||||
|
|
@ -210,7 +210,7 @@ pub async fn project_install_packages(
|
|||
})
|
||||
.collect::<Option<Vec<_>>>()
|
||||
else {
|
||||
return Err(RustError::unrecoverable("some packages not found"));
|
||||
return Err(RustError::unrecoverable_str("some packages not found"));
|
||||
};
|
||||
|
||||
let unity_project = load_project(project_path).await?;
|
||||
|
|
@ -301,7 +301,7 @@ pub async fn project_apply_pending_changes(
|
|||
changes_version: u32,
|
||||
) -> Result<(), RustError> {
|
||||
let Some(mut changes) = changes.get_versioned(changes_version) else {
|
||||
return Err(RustError::unrecoverable("changes version mismatch"));
|
||||
return Err(RustError::unrecoverable_str("changes version mismatch"));
|
||||
};
|
||||
|
||||
let changes = changes.take_changes();
|
||||
|
|
@ -480,6 +480,16 @@ pub async fn project_open_unity(
|
|||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check if the project is on a noexec filesystem (Linux/macOS only)
|
||||
// This causes shader compilation failures, resulting in non-stereoscopic rendering
|
||||
let project_path_ref = Path::new(&project_path);
|
||||
for subdir in &["Assets", "Packages", "Library"] {
|
||||
let dir = project_path_ref.join(subdir);
|
||||
if crate::os::is_noexec(&dir) {
|
||||
return Err(localizable_error!("projects:error:noexec filesystem"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut custom_args: Option<Vec<String>> = None;
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ pub struct GlobalInfo<'a> {
|
|||
arch: &'a str,
|
||||
os_info: &'a str,
|
||||
webview_version: &'a str,
|
||||
local_app_data: &'a str,
|
||||
app_data: &'a str,
|
||||
default_unity_arguments: &'a [&'a str],
|
||||
vpm_home_folder: &'a std::path::Path,
|
||||
check_for_updates: bool,
|
||||
|
|
@ -78,9 +78,9 @@ pub fn global_info_json(app: &AppHandle) -> Response<Cow<'static, [u8]>> {
|
|||
let check_for_updates = app.env().appimage.is_some();
|
||||
|
||||
#[cfg(windows)]
|
||||
let local_app_data = crate::os::local_app_data();
|
||||
let app_data = crate::os::app_data();
|
||||
#[cfg(not(windows))]
|
||||
let local_app_data = "";
|
||||
let app_data = "";
|
||||
|
||||
let should_install_deep_link = crate::deep_link_support::should_install_deep_link(app);
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ pub fn global_info_json(app: &AppHandle) -> Response<Cow<'static, [u8]>> {
|
|||
arch,
|
||||
os_info,
|
||||
webview_version,
|
||||
local_app_data,
|
||||
app_data,
|
||||
default_unity_arguments: DEFAULT_UNITY_ARGUMENTS,
|
||||
vpm_home_folder: &vpm_home_folder,
|
||||
check_for_updates,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::path::Path;
|
|||
use crate::commands::async_command::{AsyncCallResult, With, async_command};
|
||||
use crate::commands::environment::settings::TauriPickProjectDefaultPathResult;
|
||||
use crate::commands::prelude::*;
|
||||
use crate::commands::safe_url;
|
||||
use crate::logging::LogEntry;
|
||||
use crate::os::open_that;
|
||||
use crate::updater::{self, Update};
|
||||
|
|
@ -26,7 +27,7 @@ pub async fn util_open(path: String, if_not_exists: OpenOptions) -> Result<(), R
|
|||
if !path.exists() {
|
||||
match if_not_exists {
|
||||
OpenOptions::ErrorIfNotExists => {
|
||||
return Err(RustError::unrecoverable("Path does not exist"));
|
||||
return Err(RustError::unrecoverable_str("Path does not exist"));
|
||||
}
|
||||
OpenOptions::CreateFolderIfNotExists => {
|
||||
super::create_dir_all_with_err(&path).await?;
|
||||
|
|
@ -42,9 +43,19 @@ pub async fn util_open(path: String, if_not_exists: OpenOptions) -> Result<(), R
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn util_open_url_nocheck(url: String) -> Result<(), RustError> {
|
||||
open_that(url)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn util_open_url(url: String) -> Result<(), RustError> {
|
||||
if !Url::parse(&url).is_ok_and(|x| safe_url(&x)) {
|
||||
return Err(RustError::unrecoverable_str("Bad URL or bad scheme"));
|
||||
}
|
||||
open_that(url)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -65,7 +76,11 @@ pub async fn check_for_update(
|
|||
app_handle: AppHandle,
|
||||
stable: bool,
|
||||
) -> updater::Result<Option<Update>> {
|
||||
let endpoint = if stable {
|
||||
let endpoint = if let Ok(env) =
|
||||
std::env::var("___ALCOM_UPDATER_URL_OVERRIDE_DEBUG_ONLY_FEATURE_YOU_SHOULD_NOT_USE_THIS___")
|
||||
{
|
||||
Url::parse(&env).unwrap()
|
||||
} else if stable {
|
||||
Url::parse("https://vrc-get.anatawa12.com/api/gui/tauri-updater.json").unwrap()
|
||||
} else {
|
||||
Url::parse("https://vrc-get.anatawa12.com/api/gui/tauri-updater-beta.json").unwrap()
|
||||
|
|
@ -133,11 +148,11 @@ pub async fn util_install_and_upgrade(
|
|||
) -> Result<AsyncCallResult<InstallUpgradeProgress, ()>, RustError> {
|
||||
async_command(channel, window, async move {
|
||||
let Some(response) = updater_state.take() else {
|
||||
return Err(RustError::unrecoverable("No update response found"));
|
||||
return Err(RustError::unrecoverable_str("No update response found"));
|
||||
};
|
||||
|
||||
if response.version() != version {
|
||||
return Err(RustError::unrecoverable("Update data version mismatch"));
|
||||
return Err(RustError::unrecoverable_str("Update data version mismatch"));
|
||||
}
|
||||
|
||||
With::<InstallUpgradeProgress>::continue_async(move |ctx| async move {
|
||||
|
|
|
|||
|
|
@ -241,11 +241,12 @@ impl From<log::Level> for LogLevel {
|
|||
#[derive(Serialize, specta::Type, Clone)]
|
||||
pub(crate) struct LogEntry {
|
||||
#[serde(serialize_with = "to_rfc3339_micros")]
|
||||
#[specta(type = &str)]
|
||||
time: chrono::DateTime<chrono::Local>,
|
||||
level: LogLevel,
|
||||
target: String,
|
||||
message: String,
|
||||
gui_toast: bool,
|
||||
gui_toast: Option<bool>,
|
||||
}
|
||||
|
||||
fn to_rfc3339_micros<S>(
|
||||
|
|
@ -264,8 +265,7 @@ impl LogEntry {
|
|||
let gui_toast = record
|
||||
.key_values()
|
||||
.get("gui_toast".into())
|
||||
.and_then(|x| x.to_bool())
|
||||
.unwrap_or(true);
|
||||
.and_then(|x| x.to_bool());
|
||||
LogEntry {
|
||||
time: chrono::Local::now(),
|
||||
level: record.level().into(),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::collections::HashSet;
|
|||
use std::ffi::{OsStr, OsString};
|
||||
use std::io;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -175,3 +176,10 @@ pub(super) fn fix_env_variables(command: &mut Command) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_noexec(path: &Path) -> bool {
|
||||
use nix::sys::statvfs::FsFlags;
|
||||
nix::sys::statvfs::statvfs(path)
|
||||
.map(|s| s.flags().contains(FsFlags::ST_NOEXEC))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,3 +198,10 @@ pub fn initialize(_: tauri::AppHandle) {
|
|||
pub(crate) fn fix_env_variables(_: &mut Command) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
pub fn is_noexec(path: &Path) -> bool {
|
||||
use nix::mount::MntFlags;
|
||||
nix::sys::statfs::statfs(path)
|
||||
.map(|s| s.flags().contains(MntFlags::MNT_NOEXEC))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,4 +51,5 @@ pub fn os_info() -> &'static str {
|
|||
}
|
||||
|
||||
pub use os_more::initialize;
|
||||
pub use os_more::is_noexec;
|
||||
pub use os_more::open_that;
|
||||
|
|
|
|||
|
|
@ -234,8 +234,24 @@ pub fn local_app_data() -> &'static str {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn app_data() -> &'static str {
|
||||
static APP_DATA: OnceLock<String> = OnceLock::new();
|
||||
|
||||
APP_DATA.get_or_init(|| {
|
||||
// AppData is the parent directory of LocalAppData (AppData\Local)
|
||||
std::path::Path::new(local_app_data())
|
||||
.parent()
|
||||
.map(|x| x.to_string_lossy().into_owned())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
pub use open::that as open_that;
|
||||
|
||||
pub fn initialize(_: tauri::AppHandle) {
|
||||
// nothing to initialize
|
||||
}
|
||||
|
||||
pub fn is_noexec(_path: &Path) -> bool {
|
||||
false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ pub fn new_http_client() -> reqwest::Client {
|
|||
env!("CARGO_PKG_HOMEPAGE"),
|
||||
")"
|
||||
))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.read_timeout(std::time::Duration::from_secs(10))
|
||||
// https://github.com/vrc-get/vrc-get/issues/2653
|
||||
// IDK why but it might take over 10 sec to connect / read
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.read_timeout(std::time::Duration::from_secs(60))
|
||||
.timeout(std::time::Duration::from_secs(10 * 60)) // 10 minutes
|
||||
.build()
|
||||
.expect("building client")
|
||||
|
|
|
|||
|
|
@ -674,8 +674,22 @@ async fn import_unitypackage_impl(
|
|||
}
|
||||
// ignoring paths for non-Assets / Packages
|
||||
if !pathname.starts_with("Assets/") && !pathname.starts_with("Packages/") {
|
||||
warn!(
|
||||
"asset is not under Assets or Packages: {guid}: {pathname:?}",
|
||||
guid = std::str::from_utf8(&guid).unwrap()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// https://github.com/vrc-get/vrc-get/issues/2634
|
||||
// https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-132869
|
||||
// if there is '\n' in their pathname, remove after last '\n'
|
||||
let pathname = if let Some(index) = pathname.rfind('\n') {
|
||||
let mut pathname = pathname;
|
||||
pathname.replace_range(index.., "");
|
||||
pathname
|
||||
} else {
|
||||
pathname
|
||||
};
|
||||
package_entry.pathname = pathname;
|
||||
}
|
||||
_ => continue, // non unitypackage entry
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use crate::templates::{RESERVED_TEMPLATE_PREFIX, UNNAMED_TEMPLATE_PREFIX, VCC_TE
|
|||
use indexmap::IndexMap;
|
||||
use serde::de::{Error, Unexpected};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::Formatter;
|
||||
use std::path::PathBuf;
|
||||
use vrc_get_vpm::version::VersionRange;
|
||||
|
||||
|
|
@ -38,18 +39,51 @@ impl<'de> Deserialize<'de> for TemplateId {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let id = String::deserialize(deserializer)?;
|
||||
if id.is_empty()
|
||||
|| id
|
||||
.chars()
|
||||
.any(|c| !matches!(c, '0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '-'))
|
||||
{
|
||||
return Err(D::Error::invalid_value(
|
||||
Unexpected::Str(&id),
|
||||
&"a valid alcom template id",
|
||||
));
|
||||
struct Visitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = TemplateId;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a valid alcom template id")
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
if v.is_empty()
|
||||
|| v.chars()
|
||||
.any(|c| !matches!(c, '0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '-'))
|
||||
{
|
||||
return Err(E::invalid_value(
|
||||
Unexpected::Str(&v),
|
||||
&"a valid alcom template id",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(TemplateId(v))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
if v.is_empty()
|
||||
|| v.chars()
|
||||
.any(|c| !matches!(c, '0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '-'))
|
||||
{
|
||||
return Err(E::invalid_value(
|
||||
Unexpected::Str(v),
|
||||
&"a valid alcom template id",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(TemplateId(v.to_string()))
|
||||
}
|
||||
}
|
||||
Ok(Self(id))
|
||||
|
||||
deserializer.deserialize_str(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,6 +115,14 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<bool> {
|
||||
if std::env::var(
|
||||
"___ALCOM_UPDATER_DISABLE_SIGNATURE_VERIFICATION_DEBUG_ONLY_FEATURE_DO_NOT_USE_THIS_OR_YOU_WILL_BE_HACKED___",
|
||||
)
|
||||
.as_deref()
|
||||
== Ok("YES_I_WANT_TO_BE_HACKED")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
let pub_key_decoded = base64_to_string(pub_key)?;
|
||||
let public_key =
|
||||
PublicKey::decode(&pub_key_decoded).map_err(|e| Error::Signature(e.to_string()))?;
|
||||
|
|
@ -167,8 +175,24 @@ fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Err
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Version::from_str(s.trim_start_matches('v')).map_err(DeError::custom)
|
||||
struct Visitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = Version;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a semver version")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Version::from_str(v.trim_start_matches('v'))
|
||||
.map_err(|_| DeError::invalid_value(serde::de::Unexpected::Str(v), &self))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Visitor)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -246,6 +270,18 @@ pub async fn check_for_update<R: Runtime>(
|
|||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
|
||||
headers.insert(
|
||||
"X-Alcom-Version",
|
||||
HeaderValue::from_static(env!("CARGO_PKG_VERSION")),
|
||||
);
|
||||
headers.insert(
|
||||
"X-Alcom-OS",
|
||||
HeaderValue::from_static(updater_os().unwrap_or("unknown")),
|
||||
);
|
||||
headers.insert(
|
||||
"X-Alcom-Arch",
|
||||
HeaderValue::from_static(updater_arch().unwrap_or("unknown")),
|
||||
);
|
||||
|
||||
let client = app.state::<reqwest::Client>();
|
||||
|
||||
|
|
@ -777,7 +813,8 @@ mod windows {
|
|||
let params = build_updater_args(&self.platform.args, self.current_install);
|
||||
|
||||
tempfile.disable_cleanup(true);
|
||||
start_installer(op, file, params);
|
||||
drop(tempfile);
|
||||
start_installer(op, file, params)?;
|
||||
|
||||
// For windows install, we need to quit app immediately.
|
||||
std::process::exit(0);
|
||||
|
|
@ -884,14 +921,14 @@ mod windows {
|
|||
|
||||
// os specific call
|
||||
#[cfg(windows)]
|
||||
fn start_installer(op: Vec<u16>, file: Vec<u16>, params: Vec<u16>) {
|
||||
fn start_installer(op: Vec<u16>, file: Vec<u16>, params: Vec<u16>) -> Result<()> {
|
||||
use ::windows::Win32::UI::Shell::ShellExecuteW;
|
||||
use ::windows::Win32::UI::WindowsAndMessaging::SW_SHOW;
|
||||
use ::windows::core::PCWSTR;
|
||||
|
||||
unsafe {
|
||||
// SAFETY: all pointers remain valid for the duration of the call, since owned vec is passed
|
||||
ShellExecuteW(
|
||||
let response = ShellExecuteW(
|
||||
None,
|
||||
PCWSTR(op.as_ptr()),
|
||||
PCWSTR(file.as_ptr()),
|
||||
|
|
@ -899,11 +936,19 @@ mod windows {
|
|||
PCWSTR(std::ptr::null()),
|
||||
SW_SHOW,
|
||||
);
|
||||
|
||||
let response = response.0 as u32;
|
||||
if response > 32 {
|
||||
Ok(())
|
||||
} else {
|
||||
// Map the error code (<= 32) to an IO Error
|
||||
Err(std::io::Error::from_raw_os_error(response as i32).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn start_installer(_op: Vec<u16>, _file: Vec<u16>, _params: Vec<u16>) {
|
||||
fn start_installer(_op: Vec<u16>, _file: Vec<u16>, _params: Vec<u16>) -> Result<()> {
|
||||
unreachable!("install_windows_impl called on a non-Windows platform")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,35 @@
|
|||
import path from "node:path";
|
||||
import { dataToEsm } from "@rollup/pluginutils";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { defineConfig } from "vite";
|
||||
import json5Plugin from "vite-plugin-json5";
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import JSON5 from "json5";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import viteBuildLicenseJson from "./scripts/vite-build-license-json";
|
||||
|
||||
export function json5Plugin(): Plugin {
|
||||
const json5ExtRE = /\.json5$/;
|
||||
|
||||
return {
|
||||
name: "vite:json5",
|
||||
transform(json, id) {
|
||||
if (!json5ExtRE.test(id)) return null;
|
||||
try {
|
||||
// Parse the JSON5
|
||||
const parsed = JSON5.parse(json);
|
||||
// Convert the parsed JSON5 data to an ES module export
|
||||
return {
|
||||
code: dataToEsm(parsed, {}),
|
||||
map: { mappings: "" },
|
||||
};
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
this.error(error.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
|
@ -14,7 +38,7 @@ export default defineConfig({
|
|||
viteBuildLicenseJson({
|
||||
rootDir: __dirname,
|
||||
}),
|
||||
TanStackRouterVite({
|
||||
tanstackRouter({
|
||||
target: "react",
|
||||
autoCodeSplitting: true,
|
||||
routesDirectory: "app",
|
||||
|
|
@ -31,6 +55,9 @@ export default defineConfig({
|
|||
"@/build": path.join(__dirname, "./build"),
|
||||
},
|
||||
},
|
||||
oxc: {
|
||||
target: "es2025",
|
||||
},
|
||||
build: {
|
||||
outDir: "out",
|
||||
chunkSizeWarningLimit: Number.POSITIVE_INFINITY,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ windows-sys = {
|
|||
"Win32_System_Threading",
|
||||
"Win32_Security",
|
||||
"Win32_System_IO",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_System_Console",
|
||||
],
|
||||
}
|
||||
[build-dependencies]
|
||||
embed-manifest = "1.5.0"
|
||||
|
|
|
|||
12
vrc-get-gui/windows-installer-wrapper/build.rs
Normal file
12
vrc-get-gui/windows-installer-wrapper/build.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use embed_manifest::manifest::ExecutionLevel;
|
||||
use embed_manifest::{embed_manifest, new_manifest};
|
||||
|
||||
fn main() {
|
||||
if std::env::var_os("CARGO_CFG_WINDOWS").is_some() {
|
||||
embed_manifest(
|
||||
new_manifest("alcom-updater").requested_execution_level(ExecutionLevel::AsInvoker),
|
||||
)
|
||||
.expect("unable to embed manifest file");
|
||||
}
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ use windows_sys::Win32::System::Console::{GetStdHandle, STD_ERROR_HANDLE, WriteC
|
|||
use windows_sys::Win32::System::Environment::*;
|
||||
use windows_sys::Win32::System::Memory::*;
|
||||
use windows_sys::Win32::System::Threading::*;
|
||||
use windows_sys::Win32::UI::Shell::PathRenameExtensionW;
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
||||
|
||||
static INSTALLER: &[u8] = include_bytes!(env!("INSTALLER_EXE"));
|
||||
|
|
@ -98,6 +99,9 @@ unsafe fn create_temp_file() -> Option<StackPath> {
|
|||
return None;
|
||||
}
|
||||
|
||||
// replace extension
|
||||
PathRenameExtensionW(temp.as_mut_ptr(), w!(".exe"));
|
||||
|
||||
Some(name)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "vrc-get-vpm"
|
||||
|
||||
# discreate versioning since this library will not have stable versions
|
||||
version = "0.0.16-beta.0"
|
||||
version = "0.0.16-rc.0"
|
||||
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
|
@ -25,7 +25,7 @@ indexmap = { version = "2", features = ["serde"] }
|
|||
itertools = "0.14"
|
||||
log = { version = "0.4", features = ['kv'] }
|
||||
pin-project-lite = "0.2"
|
||||
reqwest = { version = "0.12", features = ["stream"], default-features = false }
|
||||
reqwest = { version = "0.13", features = ["stream"], default-features = false }
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
sha2 = "0.11"
|
||||
|
|
@ -42,11 +42,11 @@ tokio = { version = "1", features = ["fs", "process"] }
|
|||
serde_path_to_error = "0.1"
|
||||
serde-value = "0.7"
|
||||
serde_repr = "0.1"
|
||||
sha1 = "0.10"
|
||||
sha1 = "0.11"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
dirs-sys = "0.5"
|
||||
winreg = { version = "0.55", optional = true }
|
||||
winreg = { version = "0.56", optional = true }
|
||||
windows = { version = "0.62", features = ["Win32_System_Threading", "Win32_Security"] }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
|
|
@ -58,7 +58,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
|||
[features]
|
||||
default = ["rustls"]
|
||||
native-tls = ["reqwest/native-tls-vendored"]
|
||||
rustls = ["reqwest/rustls-tls-native-roots"]
|
||||
rustls = ["reqwest/rustls"]
|
||||
|
||||
unity-hub = ["dep:plist", "windows/Win32_Storage_FileSystem"]
|
||||
unity = []
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::{HttpClient, PackageInfo, PackageManifest, io};
|
|||
use futures::prelude::*;
|
||||
use hex::FromHex;
|
||||
use indexmap::IndexMap;
|
||||
use log::{debug, error};
|
||||
use log::debug;
|
||||
use std::io::SeekFrom;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::pin;
|
||||
|
|
@ -29,12 +29,16 @@ impl<T: HttpClient> crate::PackageInstaller for PackageInstaller<'_, T> {
|
|||
&self,
|
||||
io: &DefaultProjectIo,
|
||||
package: PackageInfo<'_>,
|
||||
dest_dir: &Path,
|
||||
abort: &AbortCheck,
|
||||
) -> io::Result<()> {
|
||||
abort.check()?;
|
||||
use crate::PackageInfoInner;
|
||||
log::debug!("adding package {}", package.name());
|
||||
let dest_folder = PathBuf::from(format!("Packages/{}", package.name()));
|
||||
log::debug!(
|
||||
"extracting package {} to {}",
|
||||
package.name(),
|
||||
dest_dir.display()
|
||||
);
|
||||
match package.inner {
|
||||
PackageInfoInner::Remote(package, user_repo) => {
|
||||
let zip_file = get_package(self.io, self.http, user_repo, package).await?;
|
||||
|
|
@ -50,14 +54,14 @@ impl<T: HttpClient> crate::PackageInstaller for PackageInstaller<'_, T> {
|
|||
package.version()
|
||||
);
|
||||
// remove dest folder before extract if exists
|
||||
if let Err(e) = crate::utils::extract_zip(zip_file, io, &dest_folder).await {
|
||||
if let Err(e) = crate::utils::extract_zip(zip_file, io, dest_dir).await {
|
||||
// if an error occurs, try to remove the dest folder
|
||||
log::debug!(
|
||||
"Error occurred while extracting zip file for {}@{}: {e}",
|
||||
package.name(),
|
||||
package.version(),
|
||||
);
|
||||
let _ = io.remove_dir_all(&dest_folder).await;
|
||||
let _ = io.remove_dir_all(dest_dir).await;
|
||||
return Err(e);
|
||||
}
|
||||
debug!(
|
||||
|
|
@ -69,7 +73,7 @@ impl<T: HttpClient> crate::PackageInstaller for PackageInstaller<'_, T> {
|
|||
Ok(())
|
||||
}
|
||||
PackageInfoInner::Local(_, path) => {
|
||||
crate::utils::copy_recursive(self.io, path.into(), io, dest_folder).await?;
|
||||
crate::utils::copy_recursive(self.io, path.into(), io, dest_dir.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -132,12 +136,16 @@ async fn get_package<T: HttpClient>(
|
|||
.and_then(|x| <[u8; 256 / 8] as FromHex>::from_hex(x).ok())
|
||||
&& repo_hash != zip_hash
|
||||
{
|
||||
error!(
|
||||
"Package hash mismatched! This will be hard error in the future!: {} v{}",
|
||||
package.name(),
|
||||
package.version()
|
||||
);
|
||||
//return None;
|
||||
drop(zip_file);
|
||||
io.remove_file(&zip_path).await.ok();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Downloaded file for {}@{} has an unexpected SHA256 hash. This may be the repository owner's fault, or the repository or package may be compromised.",
|
||||
package.name(),
|
||||
package.version()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(zip_file)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,18 @@ pub struct Settings {
|
|||
|
||||
impl Settings {
|
||||
pub async fn load(io: &DefaultEnvironmentIo) -> io::Result<Self> {
|
||||
let settings = VpmSettings::load(io).await?;
|
||||
let settings = if let Some(settings) = VpmSettings::load(io).await? {
|
||||
settings
|
||||
} else if let Some(settings) = VpmSettings::load_alt(io).await? {
|
||||
log::warn!(
|
||||
gui_toast = true;
|
||||
"Recovered settings from a vrc-get backup because the VCC configuration file was missing or corrupted. Some changes made in VCC may have been lost."
|
||||
);
|
||||
settings
|
||||
} else {
|
||||
VpmSettings::default()
|
||||
};
|
||||
|
||||
let vrc_get_settings = VrcGetSettings::load(io).await?;
|
||||
|
||||
Ok(Self {
|
||||
|
|
@ -258,6 +269,14 @@ impl Settings {
|
|||
self.vpm.retain_user_repos(|x| !condition(x))
|
||||
}
|
||||
|
||||
pub fn remove_repo_at_index(&mut self, index: usize) -> Option<UserRepoSetting> {
|
||||
self.vpm.remove_user_repo_at_index(index)
|
||||
}
|
||||
|
||||
pub fn reorder_user_repos_by_indices(&mut self, indices: &[usize]) {
|
||||
self.vpm.reorder_user_repos_by_indices(indices);
|
||||
}
|
||||
|
||||
// auto configurations
|
||||
|
||||
/// Removes id-duplicated repositories
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ fn default_unity_hub_path() -> &'static [&'static str] {
|
|||
};
|
||||
static ref FLATPAK_USER_INSTALLATION: String = {
|
||||
let data_home = std::env::var("XDG_DATA_HOME")
|
||||
.unwrap_or_else(|_| std::env::var("HOME").expect("HOME not set"));
|
||||
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").expect("HOME not set")));
|
||||
format!("{data_home}/flatpak/exports/bin/com.unity.UnityHub")
|
||||
};
|
||||
static ref INSTALLATIONS: [&'static str; 5] =
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ use crate::UserRepoSetting;
|
|||
use crate::environment::PackageCollection;
|
||||
use crate::io;
|
||||
use crate::io::DefaultEnvironmentIo;
|
||||
use crate::utils::{load_json_or_default, save_json};
|
||||
use crate::utils::{save_json, try_load_json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
type JsonObject = Map<String, Value>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AsJson {
|
||||
#[serde(default)]
|
||||
|
|
@ -78,45 +78,39 @@ struct AsJson {
|
|||
rest: JsonObject,
|
||||
}
|
||||
|
||||
impl Default for AsJson {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path_to_unity_exe: Default::default(),
|
||||
path_to_unity_hub: Default::default(),
|
||||
user_projects: Some(vec![]),
|
||||
unity_editors: Default::default(),
|
||||
preferred_unity_editors: Default::default(),
|
||||
default_project_path: Default::default(),
|
||||
last_ui_state: Default::default(),
|
||||
skip_unity_auto_find: Default::default(),
|
||||
user_package_folders: Default::default(),
|
||||
window_size_data: Default::default(),
|
||||
skip_requirements: Default::default(),
|
||||
last_news_update: Default::default(),
|
||||
allow_pii: Default::default(),
|
||||
project_backup_path: Default::default(),
|
||||
show_prerelease_packages: Default::default(),
|
||||
track_community_repos: Default::default(),
|
||||
selected_providers: Default::default(),
|
||||
last_selected_project: Default::default(),
|
||||
user_repos: Default::default(),
|
||||
rest: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub(crate) struct VpmSettings {
|
||||
parsed: AsJson,
|
||||
}
|
||||
|
||||
const JSON_PATH: &str = "settings.json";
|
||||
const ALT_JSON_PATH: &str = "vrc-get/vcc-settings-backup.json";
|
||||
|
||||
impl VpmSettings {
|
||||
pub async fn load(io: &DefaultEnvironmentIo) -> io::Result<Self> {
|
||||
let parsed: AsJson = load_json_or_default(io, JSON_PATH.as_ref()).await?;
|
||||
pub async fn load(io: &DefaultEnvironmentIo) -> io::Result<Option<Self>> {
|
||||
Self::load_inner(io, JSON_PATH).await
|
||||
}
|
||||
|
||||
Ok(Self { parsed })
|
||||
pub async fn load_alt(io: &DefaultEnvironmentIo) -> io::Result<Option<Self>> {
|
||||
let mut settings = Self::load_inner(io, ALT_JSON_PATH).await?;
|
||||
|
||||
// We use data from vcc.litedb for the source of the projecs list since it's much reliable source.
|
||||
if let Some(ref mut settings) = settings {
|
||||
settings.parsed.user_projects = None;
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
async fn load_inner(io: &DefaultEnvironmentIo, path: &str) -> io::Result<Option<Self>> {
|
||||
let Some(parsed): Option<AsJson> = try_load_json(io, path.as_ref()).await? else {
|
||||
log::debug!("VpmSettings Configuration file not found at {path}");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
log::debug!("Parsed VpmSettings at {path}");
|
||||
|
||||
Ok(Some(Self { parsed }))
|
||||
}
|
||||
|
||||
pub(crate) fn user_repos(&self) -> &[UserRepoSetting] {
|
||||
|
|
@ -161,6 +155,32 @@ impl VpmSettings {
|
|||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn remove_user_repo_at_index(&mut self, index: usize) -> Option<UserRepoSetting> {
|
||||
let repos = &mut self.parsed.user_repos;
|
||||
if index < repos.len() {
|
||||
Some(repos.remove(index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reorder_user_repos_by_indices(&mut self, indices: &[usize]) {
|
||||
let mut pool: Vec<Option<UserRepoSetting>> = std::mem::take(&mut self.parsed.user_repos)
|
||||
.into_iter()
|
||||
.map(Some)
|
||||
.collect();
|
||||
let mut result = Vec::with_capacity(pool.len());
|
||||
for &idx in indices {
|
||||
if let Some(slot) = pool.get_mut(idx)
|
||||
&& let Some(repo) = slot.take()
|
||||
{
|
||||
result.push(repo);
|
||||
}
|
||||
}
|
||||
result.extend(pool.into_iter().flatten());
|
||||
self.parsed.user_repos = result;
|
||||
}
|
||||
|
||||
pub(crate) fn add_user_repo(&mut self, repo: UserRepoSetting) {
|
||||
self.parsed.user_repos.push(repo);
|
||||
}
|
||||
|
|
@ -198,7 +218,8 @@ impl VpmSettings {
|
|||
}
|
||||
|
||||
pub async fn save(&self, io: &DefaultEnvironmentIo) -> io::Result<()> {
|
||||
save_json(io, JSON_PATH.as_ref(), &self.parsed).await
|
||||
save_json(io, JSON_PATH.as_ref(), &self.parsed).await?;
|
||||
save_json(io, ALT_JSON_PATH.as_ref(), &self.parsed).await
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ macro_rules! package_json_struct {
|
|||
$(#[$meta:meta])*
|
||||
$vis:vis struct $name: ident {
|
||||
$optional_vis:vis optional$(: #[$optional: meta])?;
|
||||
optional_url$(: #[$optional_url: meta])?;
|
||||
$required_vis:vis required$(: #[$required: meta])?;
|
||||
}
|
||||
$(#[$vr_get_meta:meta])*
|
||||
|
|
@ -84,9 +85,9 @@ macro_rules! package_json_struct {
|
|||
$(#[$optional])?
|
||||
$optional_vis headers: indexmap::IndexMap<Box<str>, Box<str>>,
|
||||
|
||||
$(#[$optional])?
|
||||
$(#[$optional_url])?
|
||||
$optional_vis changelog_url: Option<Url>,
|
||||
$(#[$optional])?
|
||||
$(#[$optional_url])?
|
||||
$optional_vis documentation_url: Option<Url>,
|
||||
|
||||
$(#[$optional])?
|
||||
|
|
@ -119,10 +120,22 @@ where
|
|||
<Option<T>>::deserialize(de).map(|x| x.unwrap_or_default())
|
||||
}
|
||||
|
||||
fn none_if_none_or_empty<'de, D>(de: D) -> Result<Option<Url>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let str = <Option<String>>::deserialize(de)?;
|
||||
let Some(url) = str.filter(|s| !s.trim().is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(Url::parse(&url).map_err(serde::de::Error::custom)?))
|
||||
}
|
||||
|
||||
package_json_struct! {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PackageManifest {
|
||||
optional: #[serde(default, deserialize_with = "default_if_none")];
|
||||
optional_url: #[serde(default, deserialize_with = "none_if_none_or_empty")];
|
||||
required;
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
@ -258,6 +271,7 @@ impl<'de> Deserialize<'de> for LooseManifest {
|
|||
package_json_struct! {
|
||||
pub(super) struct LooseManifest {
|
||||
pub(super) optional: #[serde(default, deserialize_with = "default_if_err")];
|
||||
optional_url: #[serde(default, deserialize_with = "default_if_err")];
|
||||
pub(super) required;
|
||||
}
|
||||
#[derive(Default)]
|
||||
|
|
@ -334,3 +348,31 @@ fn deserialize_null_on_dependencies() {
|
|||
//assert!(package_json.dependencies().is_empty());
|
||||
assert!(package_json.vpm_dependencies().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_empty_documentation() {
|
||||
let json = r##"{
|
||||
"name": "net.yarukizero.vrchat.shizuku",
|
||||
"displayName": "Shizuku",
|
||||
"version": "0.0.0",
|
||||
"unity": "2022.3",
|
||||
"description": "スクリプトでいい感じに定義したい",
|
||||
"vpmDependencies": {
|
||||
"nadena.dev.modular-avatar": ">=1.9.10"
|
||||
},
|
||||
"changelogUrl": " ",
|
||||
"author": {
|
||||
"name": "azumyar",
|
||||
"url": "https://github.com/azumyar"
|
||||
},
|
||||
"documentationUrl": "",
|
||||
"license": "MIT",
|
||||
"zipSHA256": "22a143ed75c429a471ffd784102d2fb577c56b010b49439b5930cbb2df820f8b",
|
||||
"url": "https://github.com/azumyar/vrchat-shizuku/releases/download/0.0.0/net.yarukizero.vrchat.shizuku-0.0.0.zip"
|
||||
}"##;
|
||||
let package_json: PackageManifest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(package_json.name(), "net.yarukizero.vrchat.shizuku");
|
||||
assert_eq!(package_json.version(), &Version::new(0, 0, 0));
|
||||
assert_eq!(package_json.documentation_url(), None);
|
||||
assert_eq!(package_json.changelog_url(), None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use serde::de::Error;
|
||||
use serde::de::{Error, Unexpected};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -19,14 +19,38 @@ impl<'de> Deserialize<'de> for PartialUnityVersion {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if let Some((maj, min)) = s.split_once('.') {
|
||||
let major = maj.trim().parse::<u16>().map_err(Error::custom)?;
|
||||
let minor = min.trim().parse::<u8>().map_err(Error::custom)?;
|
||||
Ok(Self(major, minor))
|
||||
} else {
|
||||
let major = s.trim().parse::<u16>().map_err(Error::custom)?;
|
||||
Ok(Self(major, 0))
|
||||
struct Visitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = PartialUnityVersion;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("unity version (major or major.minor)")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
if let Some((maj, min)) = v.split_once('.') {
|
||||
let major = maj
|
||||
.trim()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| Error::invalid_value(Unexpected::Str(v), &self))?;
|
||||
let minor = min
|
||||
.trim()
|
||||
.parse::<u8>()
|
||||
.map_err(|_| Error::invalid_value(Unexpected::Str(v), &self))?;
|
||||
Ok(PartialUnityVersion(major, minor))
|
||||
} else {
|
||||
let major = v
|
||||
.trim()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| Error::invalid_value(Unexpected::Str(v), &self))?;
|
||||
Ok(PartialUnityVersion(major, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Visitor)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ pub trait PackageInstaller {
|
|||
&self,
|
||||
io: &DefaultProjectIo,
|
||||
package: PackageInfo<'_>,
|
||||
dest_dir: &std::path::Path,
|
||||
abort: &AbortCheck,
|
||||
) -> impl Future<Output = io::Result<()>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ use futures::FutureExt;
|
|||
use futures::future::{join_all, try_join3};
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::result;
|
||||
|
||||
type Result<T> = result::Result<T, std::io::Error>;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ChipArchitecture {
|
||||
X86_64,
|
||||
ARM64,
|
||||
|
|
@ -47,7 +48,27 @@ pub async fn load_unity_by_loading_unity_hub_files() -> Result<Vec<UnityEditorIn
|
|||
)
|
||||
.await?;
|
||||
|
||||
Ok(a.into_iter().chain(b).chain(c).collect())
|
||||
// Disk-scanned entries take priority over editors-v2.json entries for the same
|
||||
// version+architecture, matching Unity Hub's merge behavior.
|
||||
let disk_scanned: Vec<UnityEditorInHub> = a.into_iter().chain(b).collect();
|
||||
let disk_scanned_keys: HashSet<(UnityVersion, ChipArchitecture)> = disk_scanned
|
||||
.iter()
|
||||
.map(|e| (e.version, arch_for_dedup(e.architecture)))
|
||||
.collect();
|
||||
let located_only: Vec<UnityEditorInHub> = c
|
||||
.into_iter()
|
||||
.filter(|located| {
|
||||
!disk_scanned_keys.contains(&(located.version, arch_for_dedup(located.architecture)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(disk_scanned.into_iter().chain(located_only).collect())
|
||||
}
|
||||
|
||||
// Unity Hub defaults null/unknown architecture to X86_64 when building unique identifiers.
|
||||
// We match that behavior here so deduplication between disk-scanned and located editors is correct.
|
||||
fn arch_for_dedup(arch: Option<ChipArchitecture>) -> ChipArchitecture {
|
||||
arch.unwrap_or(ChipArchitecture::X86_64)
|
||||
}
|
||||
|
||||
async fn get_custom_install_location(local_settings: &LocalSettings) -> Option<PathBuf> {
|
||||
|
|
@ -171,7 +192,9 @@ async fn load_located_editors(local_settings: &LocalSettings) -> Vec<UnityEditor
|
|||
#[serde(with = "either::serde_untagged")]
|
||||
location: Either<String, Vec<String>>,
|
||||
version: String,
|
||||
architecture: String,
|
||||
// architecture may be null or absent in older editors-v2.json entries
|
||||
#[serde(default)]
|
||||
architecture: Option<String>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct EditorsV2 {
|
||||
|
|
@ -192,9 +215,9 @@ async fn load_located_editors(local_settings: &LocalSettings) -> Vec<UnityEditor
|
|||
let Some(version) = UnityVersion::parse(&editor.version) else {
|
||||
continue;
|
||||
};
|
||||
let architecture = match editor.architecture.as_str() {
|
||||
"x86_64" => Some(ChipArchitecture::X86_64),
|
||||
"arm64" => Some(ChipArchitecture::ARM64),
|
||||
let architecture = match editor.architecture.as_deref() {
|
||||
Some("x86_64") | None => Some(ChipArchitecture::X86_64),
|
||||
Some("arm64") => Some(ChipArchitecture::ARM64),
|
||||
_ => None,
|
||||
};
|
||||
match editor.location {
|
||||
|
|
|
|||
|
|
@ -108,12 +108,12 @@ mod linux {
|
|||
.take_if(|x| !x.is_empty())
|
||||
.map(PathBuf::from)
|
||||
{
|
||||
config_home.joined("UnityHub")
|
||||
config_home.joined("unityhub")
|
||||
} else {
|
||||
std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.expect("HOME environment variable is not set")
|
||||
.joined(".config/UnityHub")
|
||||
.joined(".config/unityhub")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
use crate::io::{DefaultProjectIo, DirEntry, IoTrait};
|
||||
use crate::io::{DefaultProjectIo, IoTrait};
|
||||
use crate::traits::AbortCheck;
|
||||
use crate::unity_project::find_legacy_assets::collect_legacy_assets;
|
||||
use crate::utils::{PathBufExt, walk_dir_relative};
|
||||
use crate::version::DependencyRange;
|
||||
use crate::{PackageInfo, UnityProject, unity_compatible};
|
||||
use crate::{PackageInstaller, io};
|
||||
use either::Either;
|
||||
use futures::future::{join, join_all};
|
||||
use futures::prelude::*;
|
||||
use indexmap::IndexSet;
|
||||
use log::debug;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::future::ready;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::pin;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Represents Packages to be added and folders / packages to be removed
|
||||
|
|
@ -133,17 +130,17 @@ impl<'env> Builder<'env> {
|
|||
}
|
||||
|
||||
pub fn add_to_dependencies(&mut self, name: Box<str>, version: DependencyRange) -> &mut Self {
|
||||
match self.package_changes.entry(name) {
|
||||
match self.package_changes.entry(name.clone()) {
|
||||
Entry::Occupied(mut e) => match e.get_mut() {
|
||||
PackageChange::Install(e) => {
|
||||
if e.to_dependencies.is_none() {
|
||||
e.to_dependencies = Some(version);
|
||||
} else {
|
||||
panic!("INTERNAL ERROR: already add_to_dependencies");
|
||||
panic!("INTERNAL ERROR: already add_to_dependencies: {}", name);
|
||||
}
|
||||
}
|
||||
PackageChange::Remove(_) => {
|
||||
panic!("INTERNAL ERROR: add_to_dependencies for removed");
|
||||
panic!("INTERNAL ERROR: add_to_dependencies for removed: {}", name);
|
||||
}
|
||||
},
|
||||
Entry::Vacant(e) => {
|
||||
|
|
@ -159,50 +156,60 @@ impl<'env> Builder<'env> {
|
|||
}
|
||||
|
||||
pub fn install_to_locked(&mut self, info: PackageInfo<'env>) -> &mut Self {
|
||||
match self.package_changes.entry(info.name().into()) {
|
||||
Entry::Occupied(mut e) => match e.get_mut() {
|
||||
PackageChange::Install(e) => {
|
||||
if e.package.is_none() {
|
||||
e.package = Some(info);
|
||||
e.add_to_locked = true;
|
||||
} else {
|
||||
panic!("INTERNAL ERROR: already install");
|
||||
}
|
||||
}
|
||||
PackageChange::Remove(_) => {
|
||||
panic!("INTERNAL ERROR: install for removed");
|
||||
}
|
||||
},
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(PackageChange::Install(Install {
|
||||
package: Some(info),
|
||||
add_to_locked: true,
|
||||
to_dependencies: None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
self
|
||||
self.install_to_locked_impl(info, true, false)
|
||||
}
|
||||
|
||||
// replacing is necessary for resolve since dependencies or unlocked package may require
|
||||
// newer versions of locked packages.
|
||||
pub fn install_to_locked_replacing(&mut self, info: PackageInfo<'env>) -> &mut Self {
|
||||
self.install_to_locked_impl(info, true, true)
|
||||
}
|
||||
|
||||
pub fn install_already_locked(&mut self, info: PackageInfo<'env>) -> &mut Self {
|
||||
self.install_to_locked_impl(info, false, false)
|
||||
}
|
||||
|
||||
fn install_to_locked_impl(
|
||||
&mut self,
|
||||
info: PackageInfo<'env>,
|
||||
add_to_locked: bool,
|
||||
allow_replace: bool,
|
||||
) -> &mut Self {
|
||||
match self.package_changes.entry(info.name().into()) {
|
||||
Entry::Occupied(mut e) => match e.get_mut() {
|
||||
PackageChange::Install(e) => {
|
||||
if e.package.is_none() {
|
||||
e.package = Some(info);
|
||||
e.add_to_locked = false;
|
||||
if let Some(installed) = e.package
|
||||
&& !allow_replace
|
||||
{
|
||||
panic!(
|
||||
"INTERNAL ERROR: already install: {} ({} ({}) => {} ({}))",
|
||||
info.name(),
|
||||
installed.version(),
|
||||
if e.add_to_locked {
|
||||
"to locked"
|
||||
} else {
|
||||
"existing"
|
||||
},
|
||||
info.version(),
|
||||
if add_to_locked {
|
||||
"to locked"
|
||||
} else {
|
||||
"existing"
|
||||
},
|
||||
);
|
||||
} else {
|
||||
panic!("INTERNAL ERROR: already install");
|
||||
e.package = Some(info);
|
||||
e.add_to_locked = add_to_locked;
|
||||
}
|
||||
}
|
||||
PackageChange::Remove(_) => {
|
||||
panic!("INTERNAL ERROR: install for removed");
|
||||
panic!("INTERNAL ERROR: install for removed: {}", info.name());
|
||||
}
|
||||
},
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(PackageChange::Install(Install {
|
||||
package: Some(info),
|
||||
add_to_locked: false,
|
||||
add_to_locked,
|
||||
to_dependencies: None,
|
||||
}));
|
||||
}
|
||||
|
|
@ -251,14 +258,14 @@ impl<'env> Builder<'env> {
|
|||
}
|
||||
|
||||
pub fn remove(&mut self, name: Box<str>, reason: RemoveReason) -> &mut Self {
|
||||
match self.package_changes.entry(name) {
|
||||
match self.package_changes.entry(name.clone()) {
|
||||
Entry::Occupied(mut e) => match e.get_mut() {
|
||||
PackageChange::Install(_) => {
|
||||
panic!("INTERNAL ERROR: remove for installed");
|
||||
panic!("INTERNAL ERROR: remove for installed: {name}");
|
||||
}
|
||||
PackageChange::Remove(e) => {
|
||||
if e.reason != reason {
|
||||
panic!("INTERNAL ERROR: already remove");
|
||||
panic!("INTERNAL ERROR: already remove: {name}");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -273,10 +280,10 @@ impl<'env> Builder<'env> {
|
|||
}
|
||||
|
||||
fn remove_unused(&mut self, name: Box<str>) -> &mut Self {
|
||||
match self.package_changes.entry(name) {
|
||||
match self.package_changes.entry(name.clone()) {
|
||||
Entry::Occupied(mut e) => match e.get_mut() {
|
||||
PackageChange::Install(_) => {
|
||||
panic!("INTERNAL ERROR: remove_unused for installed");
|
||||
panic!("INTERNAL ERROR: remove_unused for installed: {name}");
|
||||
}
|
||||
PackageChange::Remove(_) => {
|
||||
// already removed, do nothing
|
||||
|
|
@ -536,35 +543,18 @@ impl UnityProject {
|
|||
) -> io::Result<()> {
|
||||
/*
|
||||
Apply pending changes consists of following steps:
|
||||
- Move packages to temp directory (remove packages)
|
||||
- Apply changes to manifest (add packages)
|
||||
- Install packages
|
||||
- Extract new packages to temp directory
|
||||
- Actually install packages
|
||||
- Move old packages to temp directory (remove packages)
|
||||
- Move new packages to Package directory (add packages)
|
||||
- Apply changes to manifest
|
||||
- Remove legacy assets
|
||||
|
||||
This function will do those steps in the order above.
|
||||
There are several things to consider:
|
||||
- We remove package before applying changes to manifest because:
|
||||
- If we update manifest before removing packages,
|
||||
failing to remove packages will leave previously installed packages as unlocked packages.
|
||||
- If we remove packages before updating manifest,
|
||||
failing to install packages will leave packages as uninstalled locked packages,
|
||||
which is easy to fix with Resolve command.
|
||||
- We install packages after applying changes to manifest because:
|
||||
- If we install packages before updating manifest,
|
||||
failing to update manifest will leave packages as unlocked packages.
|
||||
- If we update manifest before installing packages,
|
||||
failing to install packages will leave packages as uninstalled locked packages,
|
||||
which is easy to fix with Resolve command.
|
||||
- We remove legacy assets after installing packages because:
|
||||
- If we remove legacy assets before installing packages,
|
||||
failing to install package will leave legacy assets removed.
|
||||
- If we install packages before removing legacy assets,
|
||||
failing to remove legacy assets will duplicate legacy assets.
|
||||
- Both cases are not desirable, but the latter is less harmful.
|
||||
- Cleanup temp directory as possible (errors ignored)
|
||||
*/
|
||||
|
||||
let mut installs = Vec::new();
|
||||
let mut remove_names = Vec::new();
|
||||
let mut uninstall_packages = Vec::new();
|
||||
let mut remove_unlocked_names = Vec::new();
|
||||
|
||||
for (name, change) in &request.package_changes {
|
||||
|
|
@ -572,10 +562,12 @@ impl UnityProject {
|
|||
PackageChange::Install(change) => {
|
||||
if let Some(package) = change.package {
|
||||
installs.push(package);
|
||||
uninstall_packages.push(name.as_ref());
|
||||
}
|
||||
}
|
||||
PackageChange::Remove(_) => {
|
||||
remove_names.push(name.as_ref());
|
||||
uninstall_packages.push(name.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -583,28 +575,30 @@ impl UnityProject {
|
|||
for info in request.conflicts.values() {
|
||||
for x in &info.unlocked_names {
|
||||
remove_unlocked_names.push(x.as_ref());
|
||||
uninstall_packages.push(x.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
// remove packages
|
||||
let remove_temp_dir = format!("{PKG_TEMP_DIR}/{}", uuid::Uuid::new_v4());
|
||||
let remove_temp_dir = Path::new(&remove_temp_dir);
|
||||
let temp_dir_base = PathBuf::from(format!("{PKG_TEMP_DIR}/{}", uuid::Uuid::new_v4()));
|
||||
let mut context = InstallPackageContext::new(&temp_dir_base);
|
||||
|
||||
self.io.create_dir_all(remove_temp_dir).await?;
|
||||
// Before all, prepare temp dir
|
||||
self.io.create_dir_all(&context.remove_temp_dir).await?;
|
||||
self.io.create_dir_all(&context.install_temp_dir).await?;
|
||||
|
||||
move_packages_to_temp(
|
||||
&self.io,
|
||||
(remove_names.iter().copied())
|
||||
.chain(installs.iter().map(|x| x.name()))
|
||||
.chain(remove_unlocked_names.iter().copied()),
|
||||
remove_temp_dir,
|
||||
)
|
||||
.await?;
|
||||
let mut r: io::Result<()> = async {
|
||||
// Firstly, extract packages
|
||||
extract_packages(&self.io, env, &context.install_temp_dir, &installs).await?;
|
||||
|
||||
// apply changes to manifest
|
||||
for (name, change) in &request.package_changes {
|
||||
match change {
|
||||
PackageChange::Install(change) => {
|
||||
// Then, update packages directory
|
||||
context
|
||||
.move_uninstall_packages(&self.io, &uninstall_packages)
|
||||
.await?;
|
||||
context.move_install_packages(&self.io, &installs).await?;
|
||||
|
||||
// apply changes to manifest
|
||||
for (name, change) in &request.package_changes {
|
||||
if let PackageChange::Install(change) = change {
|
||||
if let Some(package) = change.package
|
||||
&& change.add_to_locked
|
||||
{
|
||||
|
|
@ -619,183 +613,102 @@ impl UnityProject {
|
|||
self.manifest.add_dependency(name, version.clone());
|
||||
}
|
||||
}
|
||||
PackageChange::Remove(_) => {}
|
||||
}
|
||||
self.manifest.remove_packages(remove_names.iter().copied());
|
||||
self.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
self.manifest.remove_packages(remove_names.iter().copied());
|
||||
|
||||
// save manifest
|
||||
|
||||
self.save().await?;
|
||||
|
||||
// add packages
|
||||
|
||||
install_packages(&self.io, env, &installs).await?;
|
||||
|
||||
self.io.remove_dir_all(remove_temp_dir).await.ok();
|
||||
self.io.remove_dir_all(PKG_TEMP_DIR.as_ref()).await.ok();
|
||||
// remove temp dir also if it's empty
|
||||
self.io.remove_dir(TEMP_DIR.as_ref()).await.ok();
|
||||
|
||||
// remove legacy assets
|
||||
|
||||
remove_assets(
|
||||
&self.io,
|
||||
request.remove_legacy_files.iter().map(|(p, _)| p.as_ref()),
|
||||
request
|
||||
.remove_legacy_folders
|
||||
.iter()
|
||||
.map(|(p, _)| p.as_ref()),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
if let Err(mut e) = r {
|
||||
// When error occurs in installation process, we try rolling back package changes.
|
||||
// Since project manifest file change is the last operation of the project change,
|
||||
// we don't need to rollback manifest changes.
|
||||
if let Err(rollback_error) = context.rollback_changes(&self.io).await {
|
||||
// Rolling back packages failed. this is very unlikely but this state is very
|
||||
// fatal and project is in completely broken state so we provide much information
|
||||
// as possible though error mesasge.
|
||||
#[derive(Debug)]
|
||||
struct RollingBackError {
|
||||
original_error: io::Error,
|
||||
rollback_error: io::Error,
|
||||
remaining_installed_packages: Vec<Box<str>>,
|
||||
remaining_removed_packages: Vec<Box<str>>,
|
||||
}
|
||||
|
||||
static REMOVED_FILE_PREFIX: &str = ".__removed_";
|
||||
impl std::error::Error for RollingBackError {}
|
||||
|
||||
async fn move_packages_to_temp<'a>(
|
||||
io: &DefaultProjectIo,
|
||||
names: impl Iterator<Item = &'a str>,
|
||||
temp_dir: &Path,
|
||||
) -> io::Result<Vec<&'a str>> {
|
||||
// it's expected to cheap to rename (link) packages to temp dir,
|
||||
// so we do it sequentially for simplicity
|
||||
|
||||
let mut moved = IndexSet::new();
|
||||
|
||||
for name in names {
|
||||
if moved.contains(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match move_package(io, name, temp_dir).await {
|
||||
Ok(true) => {
|
||||
moved.insert(name);
|
||||
}
|
||||
Ok(false) => {
|
||||
// package not found, do nothing
|
||||
}
|
||||
Err(err) => {
|
||||
// restore moved packages as possible
|
||||
// our package can also be partially moved so insert to moved
|
||||
moved.insert(name);
|
||||
restore_remove(io, temp_dir, moved.iter().copied()).await;
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(moved.into_iter().collect());
|
||||
|
||||
async fn move_package(io: &DefaultProjectIo, name: &str, temp_dir: &Path) -> io::Result<bool> {
|
||||
let package_dir = format!("Packages/{name}");
|
||||
let package_dir = Path::new(&package_dir);
|
||||
let copied_dir = temp_dir.join(name);
|
||||
|
||||
io.create_dir_all(&copied_dir).await?;
|
||||
let mut iterator = pin!(walk_dir_relative(io, vec![package_dir.into()]));
|
||||
while let Some((original, entry)) = iterator.next().await {
|
||||
let relative = original.strip_prefix(package_dir).unwrap();
|
||||
let mut moved = copied_dir.join(relative);
|
||||
if entry.file_type().await?.is_dir() {
|
||||
match io.create_dir_all(&moved).await {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::error!(gui_toast = false; "error creating directory {}: {e}", moved.display());
|
||||
return Err(e);
|
||||
impl std::fmt::Display for RollingBackError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Error rolling back packages: {rollback_error}\n\
|
||||
original error:{original_error}\n\
|
||||
falsy installed packages: {installed_packages:?}\n\
|
||||
falsy removed packages: {removed_packages:?}",
|
||||
rollback_error = self.rollback_error,
|
||||
original_error = self.original_error,
|
||||
installed_packages = self.remaining_installed_packages,
|
||||
removed_packages = self.remaining_removed_packages,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
e = io::Error::new(
|
||||
e.kind(),
|
||||
RollingBackError {
|
||||
original_error: e,
|
||||
rollback_error,
|
||||
remaining_installed_packages: (context.installed.into_iter())
|
||||
.map(Box::from)
|
||||
.collect(),
|
||||
remaining_removed_packages: (context.removed.into_iter())
|
||||
.map(Box::from)
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
|
||||
// If user is familiar with vrc-get internals, user might recover packages
|
||||
// from temp directory so we don't clean temp directory.
|
||||
} else {
|
||||
if let Some(name) = original.file_name().unwrap().to_str() {
|
||||
moved.pop();
|
||||
moved.push(format!("{REMOVED_FILE_PREFIX}{name}"));
|
||||
}
|
||||
log::trace!("move {} to {}", original.display(), moved.display());
|
||||
|
||||
match io.rename(&original, &moved).await {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
// ignore error
|
||||
log::error!(gui_toast = false; "error moving {} to {}: {e}", original.display(), moved.display());
|
||||
}
|
||||
}
|
||||
// cleanup temp directory when rollback successfully finished
|
||||
cleanup_temp_dir(&self.io).await;
|
||||
}
|
||||
}
|
||||
|
||||
match io.remove_dir_all(package_dir).await {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
return Ok(false);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_remove(io: &DefaultProjectIo, temp_dir: &Path, names: impl Iterator<Item = &str>) {
|
||||
for name in names {
|
||||
let package_dir = format!("Packages/{name}");
|
||||
let package_dir = Path::new(&package_dir);
|
||||
let temp_package_dir = temp_dir.join(name);
|
||||
if io.metadata(&temp_package_dir).await.is_err() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if io.metadata(package_dir).await.is_ok() {
|
||||
// Process partially moved case
|
||||
let mut iterator = pin!(walk_dir_relative(io, vec![temp_package_dir.clone()]));
|
||||
while let Some((original, entry)) = iterator.next().await {
|
||||
if entry
|
||||
.file_type()
|
||||
.await
|
||||
.map(|x| !x.is_dir())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let relative = original.strip_prefix(&temp_package_dir).unwrap();
|
||||
if let Some(name) = original.file_name().unwrap().to_str() {
|
||||
let name = name.strip_prefix(REMOVED_FILE_PREFIX).unwrap_or(name);
|
||||
let moved = package_dir.join(relative.parent().unwrap()).joined(name);
|
||||
io.create_dir_all(moved.parent().unwrap()).await.ok();
|
||||
io.rename(&original, &moved).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
r = Err(e);
|
||||
} else {
|
||||
// Process fully moved case
|
||||
io.rename(&temp_package_dir, package_dir).await.ok();
|
||||
// installation process finished successfully
|
||||
|
||||
let mut iterator = pin!(walk_dir_relative(io, vec![package_dir.into()]));
|
||||
while let Some((original, entry)) = iterator.next().await {
|
||||
if entry
|
||||
.file_type()
|
||||
.await
|
||||
.map(|x| !x.is_dir())
|
||||
.unwrap_or(false)
|
||||
&& let Some(name) = original.file_name().unwrap().to_str()
|
||||
&& let Some(stripped) = name.strip_prefix(REMOVED_FILE_PREFIX)
|
||||
{
|
||||
let moved = original.parent().unwrap().join(stripped);
|
||||
io.rename(&original, &moved).await.ok();
|
||||
}
|
||||
}
|
||||
// cleanup
|
||||
cleanup_temp_dir(&self.io).await;
|
||||
|
||||
// remove legacy assets
|
||||
remove_assets(
|
||||
&self.io,
|
||||
request.remove_legacy_files.iter().map(|(p, _)| p.as_ref()),
|
||||
request
|
||||
.remove_legacy_folders
|
||||
.iter()
|
||||
.map(|(p, _)| p.as_ref()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn cleanup_temp_dir(io: &DefaultProjectIo) {
|
||||
// cleanup temp directory
|
||||
// we ignore error since some file might be locked by unity.
|
||||
io.remove_dir_all(PKG_TEMP_DIR.as_ref()).await.ok();
|
||||
// remove temp dir also if it's empty
|
||||
io.remove_dir(TEMP_DIR.as_ref()).await.ok();
|
||||
}
|
||||
|
||||
r
|
||||
}
|
||||
io.remove_dir(temp_dir).await.ok();
|
||||
io.remove_dir(PKG_TEMP_DIR.as_ref()).await.ok();
|
||||
io.remove_dir(TEMP_DIR.as_ref()).await.ok();
|
||||
}
|
||||
|
||||
async fn install_packages<Env: PackageInstaller>(
|
||||
async fn extract_packages<Env: PackageInstaller>(
|
||||
io: &DefaultProjectIo,
|
||||
env: &Env,
|
||||
extract_dir: &Path,
|
||||
packages: &[PackageInfo<'_>],
|
||||
) -> io::Result<()> {
|
||||
let abort = AbortCheck::new();
|
||||
|
|
@ -803,7 +716,11 @@ async fn install_packages<Env: PackageInstaller>(
|
|||
|
||||
// resolve all packages
|
||||
join_all(packages.iter().map(|package| {
|
||||
env.install_package(io, *package, &abort).then(|x| {
|
||||
async {
|
||||
env.install_package(io, *package, &extract_dir.join(package.name()), &abort)
|
||||
.await
|
||||
}
|
||||
.then(|x| {
|
||||
if let Err(e) = x {
|
||||
error_store.set(e).ok();
|
||||
abort.abort();
|
||||
|
|
@ -820,6 +737,99 @@ async fn install_packages<Env: PackageInstaller>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// The context that holds information to restore changes to packages directory when recoverable error occurs
|
||||
struct InstallPackageContext<'a> {
|
||||
removed: HashSet<&'a str>,
|
||||
installed: HashSet<&'a str>,
|
||||
|
||||
remove_temp_dir: PathBuf,
|
||||
install_temp_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl<'a> InstallPackageContext<'a> {
|
||||
fn new(temp_dir_base: &Path) -> Self {
|
||||
Self {
|
||||
removed: HashSet::new(),
|
||||
installed: HashSet::new(),
|
||||
|
||||
remove_temp_dir: temp_dir_base.join("remove"),
|
||||
install_temp_dir: temp_dir_base.join("install"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn move_uninstall_packages(
|
||||
&mut self,
|
||||
io: &DefaultProjectIo,
|
||||
packages: &[&'a str],
|
||||
) -> io::Result<()> {
|
||||
// it's expected to cheap to rename (link) packages to temp dir,
|
||||
// so we do it sequentially for simplicity
|
||||
let packages_dir = Path::new("Packages");
|
||||
|
||||
for package in packages {
|
||||
if self.removed.contains(package) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let package_dir = packages_dir.join(package);
|
||||
let copied_dir = self.remove_temp_dir.join(package);
|
||||
|
||||
match io.rename(&package_dir, &copied_dir).await {
|
||||
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
// the package does not exist, ignore this
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
Ok(()) => {
|
||||
self.removed.insert(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn move_install_packages(
|
||||
&mut self,
|
||||
io: &DefaultProjectIo,
|
||||
packages: &[PackageInfo<'a>],
|
||||
) -> io::Result<()> {
|
||||
let packages_dir = Path::new("Packages");
|
||||
|
||||
for package in packages.iter() {
|
||||
io.rename(
|
||||
&self.install_temp_dir.join(package.name()),
|
||||
&packages_dir.join(package.name()),
|
||||
)
|
||||
.await?;
|
||||
self.installed.insert(package.name());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rollback_changes(&mut self, io: &DefaultProjectIo) -> io::Result<()> {
|
||||
let packages_dir = Path::new("Packages");
|
||||
|
||||
for installed in self.installed.drain() {
|
||||
io.rename(
|
||||
&packages_dir.join(installed),
|
||||
&self.install_temp_dir.join(installed),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
for removed in self.removed.drain() {
|
||||
io.rename(
|
||||
&self.remove_temp_dir.join(removed),
|
||||
&packages_dir.join(removed),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_assets(
|
||||
io: &DefaultProjectIo,
|
||||
legacy_files: impl Iterator<Item = &Path>,
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ impl UnityProject {
|
|||
);
|
||||
|
||||
for x in result.new_packages {
|
||||
changes.install_to_locked(x);
|
||||
changes.install_to_locked_replacing(x);
|
||||
if install_names.contains(x.name()) {
|
||||
changes.add_to_dependencies(
|
||||
x.name().into(),
|
||||
|
|
@ -315,7 +315,7 @@ impl UnityProject {
|
|||
);
|
||||
|
||||
for x in result.new_packages {
|
||||
changes.install_to_locked(x);
|
||||
changes.install_to_locked_replacing(x);
|
||||
}
|
||||
|
||||
for (package, conflicts_with) in result.conflicts {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use crate::io::{DefaultProjectIo, IoTrait};
|
|||
use crate::utils::MapResultExt;
|
||||
use async_zip::base::read::seek::ZipFileReader;
|
||||
use futures::prelude::*;
|
||||
use log::trace;
|
||||
use std::path::{Component, Path};
|
||||
|
||||
pub(crate) async fn extract_zip(
|
||||
|
|
@ -23,6 +24,8 @@ pub(crate) async fn extract_zip(
|
|||
"path in zip file is not utf8".to_string(),
|
||||
));
|
||||
};
|
||||
let filename = fix_path_separator(filename);
|
||||
let filename = filename.as_ref();
|
||||
if !is_complete_relative(filename.as_ref()) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
|
|
@ -46,6 +49,20 @@ pub(crate) async fn extract_zip(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn fix_path_separator(p: &str) -> std::borrow::Cow<'_, str> {
|
||||
if cfg!(windows) {
|
||||
// On windows Path struct accepts both '/' and '\' as separator so we don't need to convert separator
|
||||
std::borrow::Cow::Borrowed(p)
|
||||
} else if !p.contains('\\') {
|
||||
// If the path does not contain '\\' we don't need to replace path separators
|
||||
std::borrow::Cow::Borrowed(p)
|
||||
} else {
|
||||
// The path contains '\\', we should replace with '/'
|
||||
trace!("fixing '\\' with '/' in path {p:?}");
|
||||
std::borrow::Cow::Owned(p.replace('\\', "/"))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_relative(path: &Path) -> bool {
|
||||
for x in path.components() {
|
||||
match x {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ macro_rules! deserialize_from_str {
|
|||
where
|
||||
E: ::serde::de::Error,
|
||||
{
|
||||
std::str::FromStr::from_str(v).map_err(E::custom)
|
||||
std::str::FromStr::from_str(v)
|
||||
.map_err(|_| E::invalid_value(::serde::de::Unexpected::Str(v), &self))
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_str(Visitor)
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ impl VersionRange {
|
|||
}
|
||||
|
||||
serialize_to_string!(VersionRange);
|
||||
deserialize_from_str!(VersionRange, "version range");
|
||||
deserialize_from_str!(VersionRange, "valid version range");
|
||||
|
||||
impl Display for VersionRange {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ use std::num::NonZeroU8;
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::version::Version;
|
||||
use serde::de::Error as _;
|
||||
use serde::de::Unexpected;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct UnityVersion {
|
||||
// major version such as 2019, 2022, and 6
|
||||
// note: 5 < 2017 < 2023 < 6 < 7 ...
|
||||
|
|
@ -197,8 +197,24 @@ impl<'de> Deserialize<'de> for UnityVersion {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
UnityVersion::parse(&String::deserialize(deserializer)?)
|
||||
.ok_or_else(|| D::Error::custom("invalid unity version"))
|
||||
struct Visitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for Visitor {
|
||||
type Value = UnityVersion;
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a unity version")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
UnityVersion::parse(v)
|
||||
.ok_or_else(|| E::invalid_value(Unexpected::Str(v), &"invalid unity version"))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +263,20 @@ impl PartialEq for ReleaseType {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for ReleaseType {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
// Normal and China compare equal, so they must hash identically
|
||||
let discriminant: u8 = match self {
|
||||
Self::Alpha => 0,
|
||||
Self::Beta => 1,
|
||||
Self::Normal | Self::China => 2,
|
||||
Self::Patch => 3,
|
||||
Self::Experimental => 4,
|
||||
};
|
||||
discriminant.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ReleaseType {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(Ord::cmp(self, other))
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub struct Version {
|
|||
|
||||
from_str_impl!(Version);
|
||||
serialize_to_string!(Version);
|
||||
deserialize_from_str!(Version, "version");
|
||||
deserialize_from_str!(Version, "valid version");
|
||||
|
||||
impl Display for Version {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue