mirror of
https://github.com/vrc-get/vrc-get.git
synced 2026-06-21 09:58:08 +00:00
Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05622d84f6 |
||
|
|
9cbec36e3d |
||
|
|
87affed3d3 |
||
|
|
133b03467e |
||
|
|
5c1dc06ff2 |
||
|
|
1db57a5ec1 |
||
|
|
7ed5dfa64b |
||
|
|
4244e198a1 |
||
|
|
2a790884e0 |
||
|
|
bcdab33b63 |
||
|
|
4dee20fc59 |
||
|
|
366e91b4d3 |
||
|
|
a644b0705d |
81 changed files with 2109 additions and 3027 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,2 +0,0 @@
|
|||
github: [anatawa12]
|
||||
custom: [https://booth.pm/ja/items/6448396]
|
||||
193
.github/workflows/ci-gui.yml
vendored
193
.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
|
||||
bundles: appimage,appimage-updater,deb,rpm
|
||||
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,setup-exe-zip,exe-updater
|
||||
bundles: setup-exe,exe-updater
|
||||
|
||||
- triple: universal-apple-darwin
|
||||
on: macos-14
|
||||
|
|
@ -122,195 +122,6 @@ 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,21 +67,6 @@ 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-*"
|
||||
|
|
@ -96,7 +81,7 @@ jobs:
|
|||
;;
|
||||
esac
|
||||
|
||||
gh-export-variable GUI_VERSION "${GUI_VERSION}"
|
||||
gh-export-variable GUI_VERSION "$(get-version -t gui)"
|
||||
env:
|
||||
RELEASE_KIND_IN: ${{ inputs.release_kind }}
|
||||
DRY_RUN: ${{ inputs.dry-run }}
|
||||
|
|
@ -167,15 +152,29 @@ 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: setup-exe-zip,exe-updater
|
||||
last-bundles: 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
|
||||
|
||||
|
|
@ -196,6 +195,8 @@ 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
|
||||
|
|
@ -272,6 +273,11 @@ 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 }}
|
||||
|
|
@ -285,7 +291,6 @@ 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 }}
|
||||
|
|
@ -304,154 +309,10 @@ 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 ]
|
||||
|
|
@ -488,7 +349,7 @@ jobs:
|
|||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre-build, build-rust, build-rpm, build-deb, build-updater-json ]
|
||||
needs: [ pre-build, build-rust, 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,11 +12,6 @@ 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
|
||||
|
|
@ -75,12 +70,8 @@ jobs:
|
|||
echo "head is master, master-*, or hotfix-*"
|
||||
;;
|
||||
* )
|
||||
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
|
||||
echo "invalid release kind: $RELEASE_KIND_IN is not allowd for $GITHUB_REF_NAME"
|
||||
exit 255
|
||||
;;
|
||||
esac
|
||||
|
||||
|
|
@ -88,7 +79,6 @@ 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
|
||||
|
|
@ -199,13 +189,10 @@ 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
|
||||
|
|
@ -216,14 +203,12 @@ 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
|
||||
|
|
@ -248,7 +233,6 @@ 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 }}
|
||||
|
|
@ -353,7 +337,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 # ${{ !inputs.dry-run && !needs.pre-build.outputs.prerelease }}
|
||||
if: false # ${{ !needs.pre-build.outputs.prerelease }}
|
||||
environment:
|
||||
name: homebrew-core
|
||||
url: https://github.com/homebrew/homebrew-core
|
||||
|
|
@ -367,7 +351,7 @@ jobs:
|
|||
|
||||
publish-to-winget:
|
||||
name: Publish to winget
|
||||
if: ${{ !inputs.dry-run && !needs.pre-build.outputs.prerelease }}
|
||||
if: ${{ !needs.pre-build.outputs.prerelease }}
|
||||
needs: [ pre-build, publish-to-github ]
|
||||
|
||||
uses: vrc-get/vrc-get/.github/workflows/publish-cli-winget.yml@master
|
||||
|
|
|
|||
|
|
@ -8,78 +8,46 @@ The format is based on [Keep a Changelog].
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Implement project sorting by creation date `#2941`
|
||||
- Build-time option to disable auto updater `#2759`
|
||||
- Please read README for new build instruction.
|
||||
|
||||
### 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`
|
||||
- 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` `#2828`
|
||||
- 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`
|
||||
- I hope this prevents users unexpectedly adding prerelease packages
|
||||
- Path for unitypackage on Template Editor now can be reselected `#2635`
|
||||
- ALCOM now refuses launching project if project is on noexec mount points `#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`
|
||||
- Extended some timeouts to 1 minute `#2826`
|
||||
- Prevents timeouts in slow DNS environments
|
||||
- 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
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
### 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)
|
||||
- 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 `{}`
|
||||
- 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)
|
||||
- ALCOM cannot detect per-user flatpak installation of unity hub `#2812`
|
||||
- Unabled to import some untypackages `#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.
|
||||
- Panic when resolving projects where dependency packages depend on newer versions of locked packages `#2822`
|
||||
- Missing glibc and libgcc_s dependency notation in .deb / .rpm distributon `#2828`
|
||||
- Unclear error message for invalid version name or version range `#2842`
|
||||
|
||||
### 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)
|
||||
|
|
@ -703,8 +671,7 @@ 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.6...HEAD
|
||||
[1.1.6]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.5...gui-v1.1.6
|
||||
[Unreleased]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.5...HEAD
|
||||
[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
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ The format is based on [Keep a Changelog].
|
|||
- 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
|
||||
|
||||
|
|
@ -29,13 +27,8 @@ The format is based on [Keep a Changelog].
|
|||
- 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
|
||||
|
|
|
|||
543
Cargo.lock
generated
543
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
462
docs/vpm-spec.md
Normal file
462
docs/vpm-spec.md
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
# The VRChat Package Manager and vrc-get extensions.
|
||||
|
||||
## Abstract
|
||||
|
||||
The VRChat Package Manager (VPM) is a package manager architecture for UPM Packages
|
||||
developed by VRChat to distribute VRChat SDK based on Hypertext Transfer Protocol (HTTP).
|
||||
This document describes the overall architecture of VPM,
|
||||
how the VPM Client should work, and how the package repository should provide packages
|
||||
based on vrc-get developers' research.
|
||||
In addition, this document describes extensions of the VPM implemented by vrc-get.
|
||||
Please note that this document is not provided by VRChat, is provided by vrc-get developers, a third party VPM Client developers.
|
||||
|
||||
## 1. Introduction
|
||||
### 1.1. Purpose
|
||||
The VRChat Package Manager is a package manager developed by VRChat and widely used by the VRChat community.
|
||||
However, the VRChat Creator Companion (VCC), the official VPM implementation, has several bugs that make it hard to know the VPM Client should work.
|
||||
Therefore, vrc-get developers assume the best VPM behavior based on the behavior of VCC, and [[VCC Docs]].
|
||||
This document describes the vrc-get developers' understanding of the VPM and vrc-get extensions
|
||||
for future VPM Client developers and VPM Repository developers.
|
||||
|
||||
### 1.2. Core Concepts
|
||||
The VPM provides a simple way to distribute UPM packages.
|
||||
|
||||
The VPM Repository provides a list of packages and their metadata, and the link to the package archive file, in JSON document hosted on the HTTP server.
|
||||
The VPM Repository can authorize VPM Clients by using Header-based authentication, including downloading the package archive file.
|
||||
|
||||
The VPM Client is the software that manages the VPM Packages in the Unity project.
|
||||
The VPM Client fetches the package information from Package Repositories and collects packages to install based on requested packages, version constraints, and dependencies.
|
||||
The VPM Client then downloads the package archives from the URL provided by the Repository and extracts the package archive into `Packages` folder in the Unity project.
|
||||
The VPM Client also writes the managing package information to `Packages/vpm-manifest.json` to manage the installed packages.
|
||||
The VPM Client can re-install the package by using the information.
|
||||
This information is usually used to install packages after cloning the project with Version Control System.
|
||||
VPM Clients can share the user-wide installed repositories list and local package cache for better user experience.
|
||||
|
||||
## 2. Conformance
|
||||
### 2.1. Syntax Notation
|
||||
This specification uses the Augmented Backus-Naur Form (ABNF) notation of [[RFC5234]].
|
||||
|
||||
The following core rules are included by reference, as defined in Appendix B.1 of [[RFC5234]]:
|
||||
ALPHA (letters), CR (carriage return), CRLF (CR LF), CTL (controls), DIGIT (decimal 0-9), DQUOTE (double quote),
|
||||
HEXDIG (hexadecimal 0-9/A-F/a-f), HTAB (horizontal tab), LF (line feed), OCTET (any 8-bit sequence of data), SP (space),
|
||||
and VCHAR (any visible US-ASCII character).
|
||||
|
||||
### 2.2. Requirements Notation
|
||||
The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**",
|
||||
"**SHOULD NOT**", "**RECOMMENDED**", "**NOT RECOMMENDED**", "**MAY**", and "**OPTIONAL**"
|
||||
in this document are to be interpreted as described in BCP 14 [[RFC2119]] [[RFC8174]] when,
|
||||
and only when, they appear in all capitals, as shown here.
|
||||
|
||||
## 3. Terminology and Core Semantics
|
||||
### 3.1. The UPM, UPM Package
|
||||
The UPM is the Unity Package Manager,
|
||||
which is the package manager developed by Unity Technologies and built into Unity Editor.
|
||||
The UPM provides the package management system for Unity Editor.
|
||||
|
||||
The UPM Package is the package recognized by the UPM and Unity Editor.
|
||||
There are several ways to install UPM Packages to a Unity Project,
|
||||
but for VPM, the embedded UPM Package is the most important.
|
||||
The embedded UPM Package is the package that is placed in the `Packages` folder in the Unity project.
|
||||
|
||||
All VPM Packages are recognized as embedded UPM Packages by Unity Editor.
|
||||
|
||||
### 3.2. VPM, VPM Client, VPM Repository, VPM Profile Folder, and VPM Package
|
||||
The term "VPM" refers to the VRChat Package Manager, which is the package manager architecture.
|
||||
The VPM consists of the VPM Client and the VPM Repository.
|
||||
|
||||
The VPM Client is the software that manages the VPM Packages in the Unity project.
|
||||
It fetches the package information from the VPM Repository and installs the package.
|
||||
|
||||
The VPM Clients usually have feature to manage VPM Repository installed for the VPM Manifest folder.
|
||||
The VPM Client **MAY** have feature to add VPM Repository from Web Browsers by using the `vcc:` URL scheme.
|
||||
|
||||
The VPM Repository is a [[JSON]] file on a HTTP(S) Server that provides package manifest and URL to the package archive.
|
||||
|
||||
As described above, the VPM Client and VPM Repository communicate with each other via HTTP [[HTTP]].
|
||||
|
||||
VPM Profile Folder is the folder that contains the configuration files and the cache files of the VPM Client.
|
||||
The configuration file for VPM Client should be shared among the VPM Clients because it contains the user-wide installed repositories list.
|
||||
However, the VPM Client **MAY** have different configuration files for some purposes, such as beta testing.
|
||||
Actually, the recent VCC Beta uses their own folder as the VPM Profile Folder.
|
||||
|
||||
The VPM Package is the package distributed by the VPM.
|
||||
The VPM Package will be recognized as a UPM Package by Unity Editor.
|
||||
Therefore, the VPM Package is a special type of UPM Package.
|
||||
|
||||
### 3.3. UPM Package Manifest, VPM Package Manifest, VPM Project Manifest
|
||||
The UPM Package Manifest is the JSON document that describes the package information.
|
||||
The UPM Package Manifest is located as `package.json` in the UPM Package and provides the package information,
|
||||
such as the package name, version, UPM dependencies.
|
||||
|
||||
The VPM Package Manifest is a UPM Package Manifest that may have information for VPM.
|
||||
In addition to the UPM Package Manifest,
|
||||
the VPM Package Manifest provides the vpm dependencies and other data used by VPM.
|
||||
|
||||
The VPM Project Manifest is the JSON document that describes the installed package information.
|
||||
The VPM Project Manifest is located as `Packages/vpm-manifest.json` in the Unity project and provides the installed package information,
|
||||
The VPM Project Manifest has two main sections, `dependencies` and `locked`.
|
||||
The `dependencies` section provides the requested package information.
|
||||
The `locked` section provides the all installed package information, including the dependencies of the requested packages.
|
||||
|
||||
### 3.4. Requested Package, Dependency Package, Locked Package, and Unlocked Package
|
||||
Requested packages are VPM Packages that the user requested to install.
|
||||
The VPM client will tries to install those packages.
|
||||
|
||||
Dependency packages are VPM packages that are required to install the requested packages.
|
||||
VPM Client automatically installs dependency packages when installing the requested package.
|
||||
|
||||
Locked packages are embedded UPM packages that are controlled by the VPM Client.
|
||||
VPM Client manages locked packages based on the VPM Manifest.
|
||||
All requested packages and Dependency Packages are locked packages.
|
||||
|
||||
Unlocked packages are the embedded UPM packages that are not controlled by the VPM Client.
|
||||
VPM Client does not modify unlocked packages.
|
||||
However, may read the package information from the UPM Package Manifest.
|
||||
|
||||
## 4. VPM Package and Manifest
|
||||
This section describes the overall structure of VPM Package and the structure of the VPM Package Manifest.
|
||||
|
||||
### 4.1. Overall Structure of VPM Package
|
||||
The VPM Package is a directory that contains the VPM Package Manifest file
|
||||
(named `package.json`) and the entries of the package.
|
||||
|
||||
VPM Package **MUST** include file named `package.json` in the root directory of the package.
|
||||
VPM Package consists of files and directories,
|
||||
and they will be recognized as contents of the UPM Package by Unity Editor.
|
||||
|
||||
VPM Package **SHOULD NOT** contain the file-system entries other than files and directories such as symbolic links and device files.
|
||||
|
||||
All entries of the VPM Package **MUST** name with Unicode [[UNICODE]] characters
|
||||
and **MUST NOT** include any characters that are not allowed in the file system.
|
||||
All entries of the VPM Package **SHOULD NOT** use characters other than ASCII Alphabets,
|
||||
Digits, Hyphen (`-`), Underscore (`_`), Space (` `), Dot (`.`), and Parentheses (`(`, `)`).
|
||||
Other characters can cause problems in some environments.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The rules about the file name are not provided in the [[VCC Docs]]
|
||||
>, and this is completely based on the vrc-get developers' research and opinion.
|
||||
>
|
||||
> The official VCC uses .NET, so they cannot handle file names with non-UNICODE characters.
|
||||
> Therefore, we wrote the rule that entry name MUST name with Unicode characters.
|
||||
>
|
||||
> For non-UTF-8 environments, Unity is unstable with non-ASCII characters.
|
||||
> Therefore, we wrote the rule that entry name SHOULD NOT use characters other than ASCII characters.
|
||||
>
|
||||
> For some OSes like Windows, some characters are not allowed in the file name.
|
||||
> Therefore, we wrote the rule that entry name SHOULD NOT use characters other than specified characters.
|
||||
|
||||
### 4.2. Structure of VPM Package Manifest
|
||||
VPM Package Manifest is a [[JSON]] document that describes the package information.
|
||||
The document **MUST** be named `package.json` and located in the root directory of the VPM Package.
|
||||
The document **MUST** be encoded in UTF-8 [[UNICODE]].
|
||||
VPM Package Manifest is a subset of the UPM Package Manifest.
|
||||
Some fields are required, and some fields are optional.
|
||||
Additional fields are allowed, so the VPM Client **SHOULD NOT** cause error with them.
|
||||
Some VPM Clients **MAY** use the additional fields for their purposes.
|
||||
|
||||
#### 4.2.1. Basic Fields
|
||||
The VPM Package Manifest **MUST** include the following two fields.
|
||||
|
||||
##### 4.2.1.1. `name` (string)
|
||||
The `name` field shows the globally unique ID of the package.
|
||||
The type of this field **MUST** be a string.
|
||||
|
||||
The package ID is used to identify single package, so this **MUST** be unique among all packages in the VPM Repository
|
||||
and **SHOULD** be unique among all packages in the VPM ecosystem.
|
||||
VPM client will treat packages with same ID as same package.
|
||||
|
||||
The Package ID **SHOULD** be prefixed with reverse domain name of the package author to be unique.
|
||||
|
||||
The package ID **SHOULD** be in lowercase and **MUST NOT** contain any characters other than ASCII Alphabets, Digits, Hyphen (`-`), and Dot (`.`).
|
||||
The VPM Client **MAY** allow the package name that does not follow the rule.
|
||||
|
||||
##### 4.2.2.2. `version` (string)
|
||||
The `version` field shows the version of the package.
|
||||
The type of this field **MUST** be a string.
|
||||
The value of this field **MUST** follow the Semantic Versioning 2.0.0 [[SEMVER]].
|
||||
Because the UPM doesn't support that, the `version` **MUST NOT** include the build metadata.
|
||||
The VPM Client **MAY** support a loose version like `"1.0"`, `" v1.0.0 "` or `"v1.0.0"`,
|
||||
however, some VPM Clients **MAY** ignore the packages with a loose version.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The VCC currently does support the loose versions.
|
||||
> This came from the underlying library of the VCC, `semver.net` by Adam Reeve (https://github.com/adamreeve/semver.net)
|
||||
> In the future version of `semver.net`, accepting a loose version is going to be removed
|
||||
> so that the VCC may remove support the loose versions in the future.
|
||||
>
|
||||
> On the other hand, the vrc-get does not support the loose versions.
|
||||
> This is originally came from the `semver` crate by David Tolnay.
|
||||
|
||||
#### 4.2.3. Package Resolution Fields
|
||||
The VPM Package Manifest **MAY** include the following fields.
|
||||
Those fields are used to determine the package is compatible with the Project and find dependency packages.
|
||||
|
||||
##### 4.2.3.1. `vpmDependencies` (object)
|
||||
The `vpmDependencies` field shows the dependencies of the package.
|
||||
The type of this field **MUST** be an object.
|
||||
|
||||
The key of this field **MUST** be the package name,
|
||||
and the value **MUST** be a string that describes the version constraint.
|
||||
The format of the version constraint will be described in [!TODO:Section Version Constraint in Package Resolving].
|
||||
|
||||
The VPM Client will resolve the dependencies of the package based on this field.
|
||||
More about the resolving process will be described in [!TODO:Section Package Resolving].
|
||||
|
||||
##### 4.2.3.2. `unity` (string)
|
||||
The `unity` field shows the minimum Unity version that the package is compatible with.
|
||||
The type of the this field **MUST** be a string.
|
||||
The value of this field **MUST** follow the Unity Version number described in [!TODO:Section Unity Version in Package Resolving].
|
||||
|
||||
The VPM Client will check the compatibility of the package with the Unity Project based on the `unity` field.
|
||||
If the package is not compatible with the Unity Project,
|
||||
the VPM Client **MUST** reject the installation of the package or show a warning to the user.
|
||||
|
||||
##### 4.2.3.3. `legacyPackages` (array)
|
||||
The `legacyPackages` field shows the list of legacy packages that the package name is replacing.
|
||||
The type of this field **MUST** be an array of strings.
|
||||
|
||||
The UdonSharp and ClientShim was provided as a separate package in the past
|
||||
and since VRCSDK 3.4.0, they are provided as a part of the VRCSDK.
|
||||
However, at that time, many packages are depending on the UdonSharp and ClientShim.
|
||||
Therefore, VRChat introduced the `legacyPackages` field to provide the compatibility with the old packages.
|
||||
|
||||
The VPM Client **MUST** guarantee
|
||||
that packages specified in the `legacyPackages` field are not installed when the package is installed.
|
||||
|
||||
#### 4.2.4. Installation Data Fields
|
||||
The VPM Package Manifest **MAY** include the following fields that are used to install the package.
|
||||
|
||||
##### 4.2.4.1. `url` (string)
|
||||
The `url` field shows the URL of the package archive file.
|
||||
The type of this field **MUST** be a string.
|
||||
The value of this field **MUST** be a valid URL that points to the package archive file.
|
||||
The VPM Package Manifest in the Remote Package Repository **MUST** include the `url` field with the valid URL.
|
||||
|
||||
The VPM Client will download the package archive file from the URL specified with this field.
|
||||
If the `url` field is not included in the VPM Package Manifest,
|
||||
the VPM Client **MUST** reject the installation of the package.
|
||||
|
||||
The VPM Repository **MUST NOT** change the contents of the URL this field points to. this is compared bitwisely.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Unfortunately, some known popular repository did change the contents of repository so it might be better to assume packages might change.
|
||||
|
||||
##### 4.2.4.2. `zipSHA256` (string)
|
||||
The `zipSHA256` field shows the SHA-256 hash of the package archive file.
|
||||
The type of the `zipSHA256` field **MUST** be a string.
|
||||
The `zipSHA256` field **MUST** be a valid HEXDIG encoded SHA-256 hash of the package archive file.
|
||||
Typically the output of sha256sum.
|
||||
|
||||
The VPM Client **MUST** use this hash to verify the integrity of the cached package archive file.
|
||||
and **MAY** use this hash to verify the integrity of the downloaded package archive file.
|
||||
|
||||
The VPM Client **MAY** reject the installation of the package
|
||||
if the downloaded package does not match the SHA-256 hash specified in the `zipSHA256` field.
|
||||
|
||||
##### 4.2.4.3. `headers` (object)
|
||||
The `headers` field shows the additional HTTP headers that is for fetching the package archive file.
|
||||
The type of the `headers` field **MUST** be an object, and the keys and values **MUST** be strings.
|
||||
|
||||
The VPM Client **MUST** add the headers specified in the `headers` field to fetch the package archive file.
|
||||
|
||||
#### 4.2.5. Human-readable Fields
|
||||
The VPM Package Manifest **MAY** include the following fields that are used to provide Human-readable information of the package.
|
||||
Those fields **MUST NOT** affect the behavior of the VPM Client except for errors on type mismatch.
|
||||
|
||||
##### 4.2.5.1. `displayName` (string)
|
||||
The `displayName` field shows the human-readable name of the package.
|
||||
The type of the `displayName` field **MUST** be a string.
|
||||
The `displayName` field should show the human-readable name of the package.
|
||||
The name is usually in English but may be in other languages.
|
||||
|
||||
##### 4.2.5.2. `description` (string)
|
||||
The `description` field shows the human-readable description of the package.
|
||||
The type of the `description` field **MUST** be a string.
|
||||
The `description` field should show the human-readable description of the package.
|
||||
The name is usually in English but may be in other languages.
|
||||
|
||||
##### 4.2.5.3. `changelogUrl` (string)
|
||||
The `changelogUrl` field shows the URL of the changelog of the package.
|
||||
The type of the `changelogUrl` field **MUST** be a string.
|
||||
The `changelogUrl` field **SHOULD** be a valid URL that points to the changelog of the package.
|
||||
The VPM Client **MAY** suggest users to open the URL with the Web Browser when the package is updated.
|
||||
|
||||
#### 4.2.A. `vrc-get` extension (object)
|
||||
The VPM Package Manifest **MAY** include the `vrc-get` object that is used to provide the additional information for the vrc-get.
|
||||
The `vrc-get` object **MUST** be an object.
|
||||
The VPM Client **MAY** ignore the `vrc-get` object.
|
||||
The `vrc-get` object **MAY** include the following fields.
|
||||
|
||||
##### 4.2.A.1. `vrc-get.yanked` (string or boolean)
|
||||
The `yanked` field shows the yanked status of the package.
|
||||
The type of the this field **MUST** be a non-empty string or boolean.
|
||||
If this field is a string, the package is yanked and the string **MUST** be the reason of the yanked.
|
||||
If this field is a boolean, the package is yanked if the value is `true` and not yanked if the value is `false`.
|
||||
|
||||
The VPM Clients that recognizes this field **SHOULD** show the warning
|
||||
if the package is yanked and the package is already installed
|
||||
and **MUST** deny installing yanked packages except for resolving the packages.
|
||||
|
||||
##### 4.2.A.2. `vrc-get.aliases` (array)
|
||||
The `vrc-get.aliases` field shows the list of the alternative package display names.
|
||||
The type of this field **MUST** be an array of strings.
|
||||
The VPM Client **MAY** use this field to better package search
|
||||
but this field **MUST NOT** affect the behavior of the VPM Client.
|
||||
|
||||
## 5. VPM Repository
|
||||
The VPM Repository is a JSON file on a HTTP server that provides package manifest and the URL to the VPM Package Archive.
|
||||
The user provides the URL and Headers information to the VPM Client to access the VPM Repository.
|
||||
|
||||
This section describes not only about contents of VPM Repository JSON file, but also about archive file VPM Repository points to.
|
||||
|
||||
### 5.1. VPM Repository JSON
|
||||
VPM Repository **MUST** be a JSON file encoded in UTF-8 [[UNICODE]] and **MUST** be an object that contains the following fields.
|
||||
|
||||
#### 5.1.1. `packages` (object)
|
||||
The `packages` field shows the list of packages in the VPM Repository.
|
||||
The type of this field **MUST** be an object.
|
||||
The key of the value of this field **MUST** be the package ID
|
||||
and the value **MUST** be an object that contains the following fields.
|
||||
|
||||
##### 5.1.1.1. `packages.<id>.versions` (object)
|
||||
The `packages.<id>.versions` field shows the list of versions of the package.
|
||||
The type of this field **MUST** be an object.
|
||||
The key of the value of this field **MUST** be the version of the package,
|
||||
which is a valid version name described in Semantic Versioning 2.0.0 [[SEMVER]].
|
||||
|
||||
The value **MUST** be an object that contains the VPM Package Manifest,
|
||||
which is described in [Section 4.2](#42-structure-of-vpm-package-manifest).
|
||||
|
||||
The VPM Package Manifest **MUST** have same package ID as the key of `packages` object,
|
||||
and same version as the key of `packages.<id>.versions` object.
|
||||
The VPM Package Manifest **MUST** have the `url` field that points to the package archive file.
|
||||
|
||||
#### 5.1.2. `url` (string)
|
||||
The `url` field shows the URL of the VPM Repository JSON.
|
||||
The type of this field **MUST** be a string.
|
||||
The value of this field **MUST** be a valid URL that points to the VPM Repository JSON.
|
||||
|
||||
According to The [[VCC Docs]], It should point to the VPM Repository JSON.
|
||||
However, for Some existing VPM Repositories,
|
||||
the `url` field is used to point some related pages like booth page or the Author's Website.
|
||||
Therefore, VPM Client **SHOULD NOT** use the `url` field to fetch the VPM Repository JSON.
|
||||
|
||||
#### 5.1.3. `id` (string)
|
||||
The `id` field shows the unique identifier of the VPM Repository.
|
||||
The type of this field **MUST** be a string.
|
||||
VPM Repository **SHOULD** provide the `id` field to identify the VPM Repository.
|
||||
If the `id` field is not provided, the VPM Client use the URL of the VPM Repository as the `id` field.
|
||||
|
||||
The `id` field **MUST** be unique among all VPM Repositories.
|
||||
To the `id` field be unique among all repositories in the VPM ecosystem,
|
||||
the `id` **SHOULD** be prefixed with reverse domain name of the VPM Repository author.
|
||||
|
||||
#### 5.1.4. `name` (string)
|
||||
The `name` field shows the human-readable name of the VPM Repository.
|
||||
The type of the `name` field **MUST** be a string.
|
||||
The `name` field should show the human-readable name of the VPM Repository.
|
||||
|
||||
#### 5.1.5. `author` (string)
|
||||
The `author` field shows the human-readable author of the VPM Repository.
|
||||
The type of the `author` field **MUST** be a string.
|
||||
The `author` field should show the human-readable author of the VPM Repository.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> In the VRChat Official Repository at <https://packages.vrchat.com/official?download> and other popular repositories
|
||||
> the `author` field shows the name of the organization or the group that manages the repository.
|
||||
>
|
||||
> On the other hand, in the Example Repository in the [[VCC Docs]],
|
||||
> the `author` field shows the email address of the author.
|
||||
>
|
||||
> We don't know the reason for this difference, but We think the `author` field should show the human-readable name of the author.
|
||||
> It's more user-friendly and easier to understand.
|
||||
|
||||
### 5.2. Package Archive File
|
||||
The Package Archive File is a Zip archive file that contains the package contents.
|
||||
The Package Manifest inside the Package Repository JSON **MUST** have the `url` field
|
||||
that points to the Package Archive File.
|
||||
|
||||
The paths in the Package Archive File **MUST** be encoded in UTF-8 [[UNICODE]]. It's not necessary to set Language Encoding flag in zip.
|
||||
|
||||
The root directory of the Package Archive File is the root directory of the VPM Package.
|
||||
In other words, `package.json` of the VPM Package will be located as `package.json` in the Package Archive File,
|
||||
not as `package/package.json` or `root/package.json`.
|
||||
|
||||
The VPM Package **MUST** use `/` as path separator, **MUST NOT** include paths starting with `/` (absolute path) or `../` (path traversal), or contains `\\` (Windows path separator), `/../` (path traversal), `/./` (path identity in archive), or `:` (Windows drive letter which can cause absolute path).
|
||||
|
||||
The VPM Client **MUST** support compression method 0 (stored) and 8 (deflate). Package Archive Files **SHOULD NOT** use compression Methods other than 0 or 8.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Notes for the VPM Package Developers
|
||||
>
|
||||
> In the recent OSes, when we compress a directory, they create root directory that contains the directory.
|
||||
> Therefore, please be careful when you compress your VPM Package.
|
||||
>
|
||||
> In addition, you should not use Windows Explorer to create zip files since it may use deflate64, which will cause compatibility problems.
|
||||
|
||||
> [!NOTE]
|
||||
> Notes About Deflate64 Compression Method
|
||||
>
|
||||
> For better compatibility, the VPM Archive File should not use the compression method other than Deflate.
|
||||
>
|
||||
> Most implementation of Zip file only supports the Deflate compression method or store uncompressed bytes.
|
||||
> For example, the standard library of go, python,
|
||||
> and java only supports the Deflate compression method and uncompressed bytes.
|
||||
>
|
||||
> However, the Microsoft Windows built-in Zip file compressor may use the Deflate64 compression method.
|
||||
> The Deflate64 compression method is not supported by most of the Zip file implementations.
|
||||
> As a result, some existing VPM Packages may use the Deflate64 compression method in their Package Archive File.
|
||||
> Therefore, it might be better for VPM Clients to support the Deflate64 compression method.
|
||||
>
|
||||
> Side Note: the Deflate64 compression method is not publicly documented.
|
||||
> In the APPNOTE.TXT by pkware, deflate64 is described as "Deflate64(tm) is supported by the Deflate extractor.",
|
||||
> but it requires larger window size and no literal to code mapping on document, so it's impossible to decompress with existing Deflate implementation.
|
||||
> In addition, the Deflate64 is not documented in any public documents,
|
||||
> so there are very limited deflate 64 implementations.
|
||||
> Here's an incomplete list of known open-source implementations:
|
||||
> - [.NET System.IO.Compression](https://github.com/dotnet/runtime/blob/2f08fcbfece0c09319f237a6aee6f74c4a9e14e8/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateManaged/) by Microsoft, inc. under the MIT License
|
||||
> - [7zip](https://sourceforge.net/projects/sevenzip/files/7-Zip/) by Igor Pavlov under the GNU LGPL 2.1
|
||||
> - [deflate64-rs](https://github.com/anatawa12/deflate64-rs) by anatawa12 based which is reimplementation of the .NET System.IO.Compression under the MIT License
|
||||
> - [Info Zip](https://sourceforge.net/projects/infozip/) by Info-ZIP under the Info-ZIP License
|
||||
> - [Apache Commons Compress](https://commons.apache.org/proper/commons-compress/) by Apache Software Foundation under the Apache License 2.0
|
||||
|
||||
## References
|
||||
### \[VCC Docs]
|
||||
VRChat provides documentation for VCC at https://vcc.docs.vrchat.com/.
|
||||
The document is also published on GitHub at https://github.com/vrchat-community/creator-companion.
|
||||
This spec is based on commit [2f09cfe](https://github.com/vrchat-community/creator-companion/tree/2f09cfef3734b34e6e2cf4d8107c955c4f123322).
|
||||
|
||||
### \[HTTP]
|
||||
Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Semantics", STD 97, RFC 9110, DOI 10.17487/RFC9110, June 2022, <<https://www.rfc-editor.org/info/rfc9110>>.
|
||||
|
||||
### \[JSON]
|
||||
Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, December 2017, <<https://www.rfc-editor.org/info/rfc8259>>.
|
||||
|
||||
### \[UNICODE]
|
||||
Unicode Consortium, "The Unicode Standard", <<https://www.unicode.org/versions/latest/>>.
|
||||
|
||||
### \[SEMVER]
|
||||
Preston-Werner, T., "Semantic Versioning 2.0.0", <<https://semver.org/spec/v2.0.0.html>>.
|
||||
|
||||
### \[RFC2119]
|
||||
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, <<http://www.rfc-editor.org/info/rfc2119>>.
|
||||
|
||||
### \[RFC5234]
|
||||
Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", STD 68, RFC 5234, DOI 10.17487/RFC5234, January 2008, <<https://www.rfc-editor.org/info/rfc5234>>.
|
||||
|
||||
### \[RFC8174]
|
||||
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, <<https://www.rfc-editor.org/info/rfc8174>>.
|
||||
|
||||
[VCC Docs]: #vcc-docs
|
||||
[HTTP]: #http
|
||||
[JSON]: #json
|
||||
[SEMVER]: #semver
|
||||
[UNICODE]: #unicode
|
||||
[RFC5234]: #rfc5234
|
||||
[RFC2119]: #rfc2119
|
||||
[RFC8174]: #rfc8174
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vrc-get-gui"
|
||||
version = "1.1.7-beta.0"
|
||||
version = "1.1.6-beta.0"
|
||||
description = "A fast open-source alternative of VRChat Creator Companion"
|
||||
|
||||
homepage.workspace = true
|
||||
|
|
@ -22,7 +22,7 @@ tauri-build = { version = "2", features = [ "config-toml" ] }
|
|||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_with = { version = "3", features = ["base64"] }
|
||||
tauri = { version = "=2.11.2", features = [ "config-toml" ] } # = for sync version between npm and cargo
|
||||
tauri = { version = "=2.10.3", 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.13", features = ["gzip", "brotli", "json"] }
|
||||
specta = { version = "2.0.0-rc.24", features = [ "chrono", "url", "indexmap" ] }
|
||||
|
|
@ -61,7 +61,7 @@ yoke = { version = "0.8", features = ["derive"] }
|
|||
atomicbox = "0.4"
|
||||
stable_deref_trait = "1"
|
||||
itertools = "0.14"
|
||||
sysinfo = "0.39.3"
|
||||
sysinfo = "0.38.4"
|
||||
|
||||
[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"] }
|
||||
|
|
|
|||
|
|
@ -6,20 +6,15 @@ import globalInfo from "@/lib/global-info";
|
|||
export default function ErrorPage({
|
||||
error,
|
||||
}: {
|
||||
error: object;
|
||||
error: Error;
|
||||
reset?: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
// 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 errorMessage = `${error}`;
|
||||
const errorStack = `${error.stack}`;
|
||||
|
||||
const openIssue = () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -18,13 +18,7 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import {
|
||||
toastError,
|
||||
toastInfo,
|
||||
toastNormal,
|
||||
toastSuccess,
|
||||
toastWarning,
|
||||
} from "@/lib/toast";
|
||||
import { toastError, toastInfo, toastNormal, toastSuccess } from "@/lib/toast";
|
||||
|
||||
export const Route = createFileRoute("/_main/dev-palette/")({
|
||||
component: Page,
|
||||
|
|
@ -119,12 +113,6 @@ function Page() {
|
|||
>
|
||||
Error
|
||||
</Button>
|
||||
<Button
|
||||
variant={"warning"}
|
||||
onClick={() => toastWarning("Warning Toast Body")}
|
||||
>
|
||||
Warning
|
||||
</Button>
|
||||
<Button
|
||||
variant={"success"}
|
||||
onClick={() => toastSuccess("Success Toast Body")}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,5 @@
|
|||
"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,
|
||||
|
|
@ -28,16 +7,8 @@ import {
|
|||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ChevronDown, CircleX, GripVertical } from "lucide-react";
|
||||
import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ChevronDown, CircleX } from "lucide-react";
|
||||
import { Suspense, useCallback, useEffect, useId, useMemo } from "react";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -70,8 +41,6 @@ export const Route = createFileRoute("/_main/packages/repositories/")({
|
|||
component: Page,
|
||||
});
|
||||
|
||||
type UserRepoWithListId = TauriUserRepository & { listId: string };
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
|
|
@ -80,99 +49,11 @@ 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);
|
||||
|
||||
|
|
@ -214,110 +95,140 @@ function PageBody() {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const guiAnimation = useQuery({
|
||||
queryKey: ["environmentGuiAnimation"],
|
||||
queryFn: commands.environmentGuiAnimation,
|
||||
initialData: true,
|
||||
}).data;
|
||||
const bodyAnimation = usePrevPathName().startsWith("/packages")
|
||||
? "slide-right"
|
||||
: "";
|
||||
|
||||
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),
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setOrderedListIds(augmentedUserRepos.map((r) => r.listId));
|
||||
}, [augmentedUserRepos]);
|
||||
function RepositoryTableBody({
|
||||
userRepos,
|
||||
hiddenUserRepos,
|
||||
}: {
|
||||
userRepos: TauriUserRepository[];
|
||||
hiddenUserRepos: Set<string>;
|
||||
}) {
|
||||
const TABLE_HEAD = [
|
||||
"", // checkbox
|
||||
"general:name",
|
||||
"vpm repositories:url",
|
||||
"", // actions
|
||||
];
|
||||
|
||||
const userRepoByListId = useMemo(
|
||||
() => new Map(augmentedUserRepos.map((r) => [r.listId, r])),
|
||||
[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 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],
|
||||
);
|
||||
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 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) {
|
||||
|
|
@ -362,483 +273,72 @@ function PageBody() {
|
|||
},
|
||||
});
|
||||
|
||||
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
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
|
||||
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 (args: { index: number; id: string }) =>
|
||||
await commands.environmentRemoveRepository(args.index, args.id),
|
||||
onMutate: async ({ index }) => {
|
||||
mutationFn: async (id: string) =>
|
||||
await commands.environmentRemoveRepository(id),
|
||||
onMutate: async (id) => {
|
||||
await queryClient.cancelQueries(environmentRepositoriesInfo);
|
||||
const data = queryClient.getQueryData(
|
||||
environmentRepositoriesInfo.queryKey,
|
||||
|
|
@ -846,18 +346,10 @@ function RemoveRepositoryDialog({
|
|||
if (data !== undefined) {
|
||||
queryClient.setQueryData(environmentRepositoriesInfo.queryKey, {
|
||||
...data,
|
||||
user_repositories: data.user_repositories.filter(
|
||||
(x) => x.index !== index,
|
||||
),
|
||||
user_repositories: data.user_repositories.filter((x) => x.id !== id),
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
onError: (e, _args, ctx) => {
|
||||
queryClient.setQueryData(environmentRepositoriesInfo.queryKey, ctx);
|
||||
toastThrownError(e);
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries(environmentRepositoriesInfo),
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -877,7 +369,7 @@ function RemoveRepositoryDialog({
|
|||
<Button
|
||||
onClick={() => {
|
||||
dialog.close();
|
||||
removeRepository.mutate({ index, id });
|
||||
removeRepository.mutate(id);
|
||||
}}
|
||||
className={"ml-2"}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -316,10 +316,8 @@ function UnityVersion({
|
|||
function CreatingProject() {
|
||||
return (
|
||||
<DialogBase>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<RefreshCw className={"w-5 h-5 animate-spin"} />
|
||||
<p>{tc("projects:creating project...")}</p>
|
||||
</div>
|
||||
<RefreshCw className={"w-5 h-5 animate-spin"} />
|
||||
<p>{tc("projects:creating project...")}</p>
|
||||
</DialogBase>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,7 @@ import {
|
|||
} from "@/components/ui/tooltip";
|
||||
import type { TauriProject } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import {
|
||||
dateToString,
|
||||
dayToString,
|
||||
formatDateOffset,
|
||||
} from "@/lib/dateToString";
|
||||
import { dateToString, formatDateOffset } from "@/lib/dateToString";
|
||||
import { openSingleDialog } from "@/lib/dialog";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastThrownError } from "@/lib/toast";
|
||||
|
|
@ -49,7 +45,7 @@ export function ProjectGridItem({
|
|||
|
||||
const typeIconClass = "w-5 h-5";
|
||||
|
||||
const { projectTypeKind, displayType, isLegacy, createdAt, lastModified } =
|
||||
const { projectTypeKind, displayType, isLegacy, lastModified } =
|
||||
getProjectDisplayInfo(project);
|
||||
|
||||
const removed = !project.is_exists;
|
||||
|
|
@ -169,44 +165,22 @@ export function ProjectGridItem({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</time>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.last_modified)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex flex-wrap gap-2 justify-end compact:gap-1">
|
||||
|
|
|
|||
|
|
@ -29,11 +29,7 @@ import {
|
|||
import { assertNever } from "@/lib/assert-never";
|
||||
import type { TauriProject, TauriProjectType } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import {
|
||||
dateToString,
|
||||
dayToString,
|
||||
formatDateOffset,
|
||||
} from "@/lib/dateToString";
|
||||
import { dateToString, formatDateOffset } from "@/lib/dateToString";
|
||||
import { type DialogContext, openSingleDialog, showDialog } from "@/lib/dialog";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { router } from "@/lib/main";
|
||||
|
|
@ -82,7 +78,7 @@ export function ProjectRow({
|
|||
const noGrowCellClass = `${cellClass} w-1`;
|
||||
const typeIconClass = "w-5 h-5";
|
||||
|
||||
const { projectTypeKind, displayType, isLegacy, createdAt, lastModified } =
|
||||
const { projectTypeKind, displayType, isLegacy, lastModified } =
|
||||
getProjectDisplayInfo(project);
|
||||
|
||||
const openProjectFolder = () =>
|
||||
|
|
@ -109,7 +105,7 @@ export function ProjectRow({
|
|||
<tr
|
||||
className={`group even:bg-secondary/30 ${removed || loading || !(project.is_valid ?? true) ? "opacity-50" : ""}`}
|
||||
>
|
||||
<td className={noGrowCellClass}>
|
||||
<td className={`${cellClass} w-3`}>
|
||||
<div className={"relative flex"}>
|
||||
<FavoriteStarToggleButton
|
||||
favorite={project.favorite}
|
||||
|
|
@ -151,7 +147,7 @@ export function ProjectRow({
|
|||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className={noGrowCellClass}>
|
||||
<td className={`${cellClass} w-[8em] min-w-[8em]`}>
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="flex items-center">
|
||||
{projectTypeKind === "avatars" ? (
|
||||
|
|
@ -175,22 +171,6 @@ 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>
|
||||
|
|
@ -521,14 +501,12 @@ 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,7 +30,6 @@ 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,13 +11,7 @@ import { toastThrownError } from "@/lib/toast";
|
|||
import { compareUnityVersionString } from "@/lib/version";
|
||||
import { ProjectRow } from "./-project-row";
|
||||
|
||||
export const sortings = [
|
||||
"createdAt",
|
||||
"lastModified",
|
||||
"name",
|
||||
"unity",
|
||||
"type",
|
||||
] as const;
|
||||
export const sortings = ["lastModified", "name", "unity", "type"] as const;
|
||||
|
||||
type SimpleSorting = (typeof sortings)[number];
|
||||
type Sorting = SimpleSorting | `${SimpleSorting}Reversed`;
|
||||
|
|
@ -164,18 +158,6 @@ 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"
|
||||
|
|
@ -212,12 +194,6 @@ 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,7 +44,6 @@ 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;
|
||||
|
|
@ -93,9 +92,7 @@ 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;
|
||||
|
|
@ -110,19 +107,13 @@ export function combinePackagesAndProjectDetails(
|
|||
let packages: TauriPackage[];
|
||||
// check the repository is visible
|
||||
if (pkg.source === "LocalUser") {
|
||||
if (hideLocalUserPackages) {
|
||||
packages = hiddenUserPackages;
|
||||
} else {
|
||||
packages = userPackages;
|
||||
}
|
||||
if (hideLocalUserPackages) continue;
|
||||
packages = userPackages;
|
||||
} else if ("Remote" in pkg.source) {
|
||||
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);
|
||||
}
|
||||
if (hiddenRepositoriesSet.has(pkg.source.Remote.id)) continue;
|
||||
|
||||
packages = packagesPerRepository.get(pkg.source.Remote.id) ?? [];
|
||||
packagesPerRepository.set(pkg.source.Remote.id, packages);
|
||||
} else {
|
||||
assertNever(pkg.source);
|
||||
}
|
||||
|
|
@ -147,7 +138,6 @@ export function combinePackagesAndProjectDetails(
|
|||
unityIncompatible: new Map(),
|
||||
sources: new Set(),
|
||||
isThereSource: false,
|
||||
visibleSources: new Set(),
|
||||
installed: null,
|
||||
latest: { status: "none" },
|
||||
stableLatest: { status: "none" },
|
||||
|
|
@ -188,14 +178,8 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,13 +187,6 @@ 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");
|
||||
|
||||
|
|
@ -224,17 +201,6 @@ 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(
|
||||
|
|
@ -333,7 +299,7 @@ export function combinePackagesAndProjectDetails(
|
|||
pkg.is_yanked ||
|
||||
yankedVersions.has(`${pkg.name}:${toVersionString(pkg.version)}`),
|
||||
};
|
||||
packageRowInfo.isThereSource = true;
|
||||
packageRowInfo.isThereSource = knownPackages.has(pkg.name);
|
||||
|
||||
// if we have the latest version, check if it's upgradable
|
||||
if (packageRowInfo.latest.status !== "none") {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import {
|
|||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleArrowUp,
|
||||
CircleMinus,
|
||||
CirclePlus,
|
||||
|
|
@ -93,7 +91,6 @@ 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));
|
||||
|
|
@ -137,22 +134,6 @@ 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],
|
||||
|
|
@ -245,79 +226,26 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
))}
|
||||
</tbody>
|
||||
</ScrollableCardTable>
|
||||
</CardContent>
|
||||
|
|
@ -955,7 +883,7 @@ const PackageRow = memo(function PackageRow({
|
|||
return (
|
||||
<>
|
||||
<td className={`${cellClass} w-1 compact:px-2`}>
|
||||
<div className={"flex items-center justify-center aspect-square"}>
|
||||
<div className={"flex content-center aspect-square"}>
|
||||
<CheckboxDisabledIfLoading
|
||||
checked={bulkUpdateSelected}
|
||||
onCheckedChange={onClickBulkUpdate}
|
||||
|
|
@ -993,29 +921,27 @@ const PackageRow = memo(function PackageRow({
|
|||
<LatestPackageInfo info={pkg.latest} />
|
||||
</td>
|
||||
<td className={`${noGrowCellClass} max-w-32 overflow-hidden`}>
|
||||
{pkg.visibleSources.size === 0 ? (
|
||||
{pkg.sources.size === 0 ? (
|
||||
pkg.isThereSource ? (
|
||||
<p>{tc("projects:manage:source not selected")}</p>
|
||||
) : (
|
||||
<p>{tc("projects:manage:none")}</p>
|
||||
)
|
||||
) : pkg.visibleSources.size === 1 ? (
|
||||
) : pkg.sources.size === 1 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p className="overflow-hidden text-ellipsis">
|
||||
{[...pkg.visibleSources][0]}
|
||||
{[...pkg.sources][0]}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{[...pkg.visibleSources][0]}</TooltipContent>
|
||||
<TooltipContent>{[...pkg.sources][0]}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p>{tc("projects:manage:multiple sources")}</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{[...pkg.visibleSources].join(", ")}
|
||||
</TooltipContent>
|
||||
<TooltipContent>{[...pkg.sources].join(", ")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
|
|
@ -1052,24 +978,9 @@ const PackageRow = memo(function PackageRow({
|
|||
</ButtonDisabledIfLoading>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{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")
|
||||
)}
|
||||
{!latestVersion
|
||||
? tc("projects:manage:tooltip:incompatible with unity")
|
||||
: tc("projects:manage:tooltip:add package")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
body {
|
||||
:root {
|
||||
--toastify-font-family: var(--font-sans);
|
||||
--toastify-color-light: var(--background);
|
||||
/*--toastify-color-info: #3498db;*/
|
||||
|
|
@ -303,17 +303,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
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
|
||||
10
vrc-get-gui/bundle/deb-control
Normal file
10
vrc-get-gui/bundle/deb-control
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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, libc6 (>= {{libc_version}}), libc6 (>= {{libgcc_version}})
|
||||
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
7
vrc-get-gui/bundle/debian/.gitignore
vendored
|
|
@ -1,7 +0,0 @@
|
|||
/*-build-stamp
|
||||
/*.substvars
|
||||
/.debhelper
|
||||
/files
|
||||
/cargo_home
|
||||
/npm_cache
|
||||
alcom/
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
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/
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
#!/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 +0,0 @@
|
|||
3.0 (quilt)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# 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>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# 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@
|
||||
|
|
@ -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, useEffectEvent } from "react";
|
||||
import { Suspense, useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import Loading from "@/app/-loading";
|
||||
|
|
@ -16,37 +16,19 @@ 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,
|
||||
toastWarning,
|
||||
} from "@/lib/toast";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { useTauriListen } from "@/lib/use-tauri-listen";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const showToastForLog = useEffectEvent((entry: LogEntry) => {
|
||||
if (entry.level === "Error" && (entry.gui_toast ?? true)) {
|
||||
useTauriListen<LogEntry>("log", (event) => {
|
||||
const entry = event.payload as LogEntry;
|
||||
if (entry.level === "Error" && entry.gui_toast) {
|
||||
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" });
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ export const commands = {
|
|||
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 }),
|
||||
environmentRemoveRepository: (id: string) => __TAURI_INVOKE<null>("environment_remove_repository", { id }),
|
||||
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 }),
|
||||
|
|
@ -93,7 +92,6 @@ export const commands = {
|
|||
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<{
|
||||
|
|
@ -167,7 +165,7 @@ export type LogEntry_Deserialize = {
|
|||
level: LogLevel,
|
||||
target: string,
|
||||
message: string,
|
||||
gui_toast: boolean | null,
|
||||
gui_toast: boolean,
|
||||
};
|
||||
|
||||
export type LogEntry_Serialize = {
|
||||
|
|
@ -175,7 +173,7 @@ export type LogEntry_Serialize = {
|
|||
level: LogLevel,
|
||||
target: string,
|
||||
message: string,
|
||||
gui_toast: boolean | null,
|
||||
gui_toast: boolean,
|
||||
};
|
||||
|
||||
export type LogLevel = "Error" | "Warn" | "Info" | "Debug" | "Trace";
|
||||
|
|
@ -406,17 +404,11 @@ export type TauriUserPackage = {
|
|||
};
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,6 @@
|
|||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ function UnityInstallWindow({
|
|||
dialog: DialogContext<void>;
|
||||
}) {
|
||||
const openUnityHub = async () => {
|
||||
await commands.utilOpenUrlNocheck(installWithUnityHubLink);
|
||||
await commands.utilOpenUrl(installWithUnityHubLink);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -14,12 +14,6 @@ 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,11 +25,10 @@
|
|||
"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:toast:invalid file": "Ungültige Datei 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: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...",
|
||||
|
|
@ -114,7 +113,6 @@
|
|||
"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.",
|
||||
|
||||
|
|
@ -218,7 +216,6 @@
|
|||
"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)",
|
||||
|
|
@ -235,7 +232,6 @@
|
|||
"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",
|
||||
|
|
@ -444,7 +440,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-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-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: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.",
|
||||
|
||||
|
|
@ -494,10 +490,6 @@
|
|||
"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.",
|
||||
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@
|
|||
"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",
|
||||
|
|
@ -220,7 +218,6 @@
|
|||
"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)",
|
||||
|
|
@ -237,7 +234,6 @@
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -6,11 +6,6 @@
|
|||
'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 !',
|
||||
|
|
@ -50,7 +45,6 @@
|
|||
'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',
|
||||
|
|
@ -107,7 +101,6 @@
|
|||
'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.',
|
||||
|
|
@ -166,7 +159,6 @@
|
|||
'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',
|
||||
|
|
@ -203,14 +195,13 @@
|
|||
'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...',
|
||||
|
|
@ -228,7 +219,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',
|
||||
|
|
@ -291,9 +282,6 @@
|
|||
'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.',
|
||||
|
|
@ -336,7 +324,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-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: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: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',
|
||||
|
|
@ -468,4 +456,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',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@
|
|||
"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}}分前",
|
||||
|
|
@ -214,7 +212,6 @@
|
|||
"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": "すべて更新(安定版)",
|
||||
|
|
@ -231,7 +228,6 @@
|
|||
"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と互換性がありません。",
|
||||
|
|
|
|||
|
|
@ -215,7 +215,6 @@
|
|||
"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,7 +231,6 @@
|
|||
"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와 호환되지 않습니다.",
|
||||
|
|
|
|||
|
|
@ -212,7 +212,6 @@
|
|||
"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,7 +228,6 @@
|
|||
"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 不兼容",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
"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": "未實現",
|
||||
|
|
@ -114,7 +113,6 @@
|
|||
"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": "已添加過此專案。",
|
||||
|
||||
|
|
@ -218,7 +216,6 @@
|
|||
"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": "升級全部(穩定版)",
|
||||
|
|
@ -235,7 +232,6 @@
|
|||
"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,7 +386,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": "套件移除成功。",
|
||||
|
|
@ -444,7 +440,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-app-data": "此路徑位於 AppData 資料夾中,即使在「重設此電腦」時選擇「保留我的檔案」,該資料夾仍會被刪除。<br>建議你將資料保存到其他位置。",
|
||||
"settings:warning:in-local-app-data": "這個位置在資料夾 LocalAppData 中,如果在電腦的設定中執行「重設此電腦」並選擇「保留我的檔案」,此資料夾將被刪除。<br>建議你將資料保存到其他位置。",
|
||||
"settings:warning:whitespace": "此位置含有空格。這可能導致 Unity 和其他工具出現問題。請考慮使用其他位置。",
|
||||
"settings:warning:non-ascii": "此位置含有非 ASCII 字元。這可能導致 Unity 出現問題。請考慮使用其他位置。(不要使用中文或全形字元)",
|
||||
|
||||
|
|
@ -484,7 +480,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": "套件",
|
||||
|
|
@ -494,10 +490,6 @@
|
|||
"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 版本過舊時可能無效。",
|
||||
|
||||
|
|
@ -525,17 +517,11 @@
|
|||
"check update:toast:no updates": "無可用更新。",
|
||||
|
||||
"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>但你目前使用的安裝方式無法自動更新。<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:new version description": "有可用的新版 ALCOM",
|
||||
"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 中...",
|
||||
|
|
|
|||
1062
vrc-get-gui/package-lock.json
generated
1062
vrc-get-gui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,9 +14,6 @@
|
|||
"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",
|
||||
|
|
@ -31,9 +28,9 @@
|
|||
"@radix-ui/react-slot": "^1",
|
||||
"@radix-ui/react-tooltip": "^1",
|
||||
"@tanstack/react-query": "^5",
|
||||
"@tanstack/react-router": "^1.170.10",
|
||||
"@tanstack/router-devtools": "^1.167.0",
|
||||
"@tauri-apps/api": "2.11.0",
|
||||
"@tanstack/react-router": "^1.168.25",
|
||||
"@tanstack/router-devtools": "^1.166.11",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@uidotdev/usehooks": "^2",
|
||||
"class-variance-authority": "^0.7",
|
||||
"classnames": "^2",
|
||||
|
|
@ -41,25 +38,25 @@
|
|||
"cmdk": "^1",
|
||||
"i18next": "^26",
|
||||
"lucide-react": "^1",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5",
|
||||
"react-i18next": "^17",
|
||||
"react-toastify": "^11",
|
||||
"tailwind-merge": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2",
|
||||
"@rollup/pluginutils": "^5.4.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@tanstack/router-plugin": "^1.168.13",
|
||||
"@tauri-apps/cli": "2.11.2",
|
||||
"@rollup/pluginutils": "^5.3.0",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@tanstack/router-plugin": "^1.167.28",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "19.2.16",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"json5": "^2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~6.0",
|
||||
"vite": "^8.0.16"
|
||||
"vite": "^8.0.10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -797,7 +797,7 @@ PlayerSettings:
|
|||
webGLMemoryGeometricGrowthCap: 96
|
||||
webGLPowerPreference: 2
|
||||
scriptingDefineSymbols:
|
||||
Android: VRC_SDK_VRCSDK3;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
Android: VRC_SDK_VRCSDK3;UDON;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;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
Standalone: VRC_SDK_VRCSDK3;UDON;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;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
iPhone: VRC_SDK_VRCSDK3;UDON;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;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
Android: VRC_SDK_VRCSDK3;UDON;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;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
Standalone: VRC_SDK_VRCSDK3;UDON;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;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
iPhone: VRC_SDK_VRCSDK3;UDON;UDONSHARP;UNITY_POST_PROCESSING_STACK_V2;VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
tvOS: UNITY_POST_PROCESSING_STACK_V2
|
||||
additionalCompilerArguments: {}
|
||||
platformArchitecture: {}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
|
@ -96,7 +97,6 @@ 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,
|
||||
|
|
@ -148,7 +148,6 @@ 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,
|
||||
|
|
@ -206,7 +205,6 @@ 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,
|
||||
|
|
@ -258,7 +256,6 @@ 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,
|
||||
|
|
@ -322,16 +319,11 @@ enum HandleableRustError {
|
|||
}
|
||||
|
||||
impl RustError {
|
||||
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 unrecoverable<T: Display>(value: T) -> Self {
|
||||
error!("{value}");
|
||||
Self::Unrecoverable {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handleable(message: String, body: HandleableRustError) -> Self {
|
||||
|
|
@ -339,25 +331,6 @@ 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)>,
|
||||
|
|
@ -388,18 +361,13 @@ 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 {
|
||||
|
|
@ -414,7 +382,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_str("failed to load the latest release")
|
||||
Self::unrecoverable("failed to load the latest release")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -443,7 +411,7 @@ impl From<ReinstalPackagesError> for RustError {
|
|||
ReinstalPackagesError::DependenciesNotFound { dependencies } => {
|
||||
RustError::handleable_missing_dependencies(message, dependencies)
|
||||
}
|
||||
_ => RustError::unrecoverable(value),
|
||||
_ => RustError::unrecoverable(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -455,7 +423,7 @@ impl From<AddPackageErr> for RustError {
|
|||
AddPackageErr::DependenciesNotFound { dependencies } => {
|
||||
RustError::handleable_missing_dependencies(message, dependencies)
|
||||
}
|
||||
_ => RustError::unrecoverable(value),
|
||||
_ => RustError::unrecoverable(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -467,7 +435,7 @@ impl From<ResolvePackageErr> for RustError {
|
|||
ResolvePackageErr::DependenciesNotFound { dependencies } => {
|
||||
RustError::handleable_missing_dependencies(message, dependencies)
|
||||
}
|
||||
_ => RustError::unrecoverable(value),
|
||||
_ => RustError::unrecoverable(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -508,10 +476,6 @@ 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 {
|
||||
|
|
@ -523,14 +487,8 @@ impl TauriBasePackageInfo {
|
|||
.collect(),
|
||||
version: package.version().into(),
|
||||
unity: package.unity().map(|v| (v.major(), v.minor())),
|
||||
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()),
|
||||
changelog_url: package.changelog_url().map(|v| v.to_string()),
|
||||
documentation_url: package.documentation_url().map(|v| v.to_string()),
|
||||
vpm_dependencies: package
|
||||
.vpm_dependencies()
|
||||
.keys()
|
||||
|
|
@ -592,7 +550,7 @@ impl IntoPathBuf for tauri_plugin_dialog::FilePath {
|
|||
match self {
|
||||
Self::Url(url) => url
|
||||
.to_file_path()
|
||||
.map_err(|_| RustError::unrecoverable_str("internal error: bad file url")),
|
||||
.map_err(|_| RustError::unrecoverable("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::try_join_all;
|
||||
use futures::future::{join_all, 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, UserRepoSetting, VersionSelector};
|
||||
use vrc_get_vpm::{HttpClient, VersionSelector};
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
|
@ -58,7 +58,6 @@ pub async fn environment_packages(
|
|||
|
||||
#[derive(Serialize, specta::Type)]
|
||||
struct TauriUserRepository {
|
||||
index: usize,
|
||||
id: String,
|
||||
url: Option<String>,
|
||||
display_name: String,
|
||||
|
|
@ -88,11 +87,9 @@ pub async fn environment_repositories_info(
|
|||
let user_repositories = settings
|
||||
.get_user_repos()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, x)| {
|
||||
.map(|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(),
|
||||
|
|
@ -348,48 +345,24 @@ 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>,
|
||||
index: usize,
|
||||
expected_id: String,
|
||||
id: String,
|
||||
) -> Result<(), RustError> {
|
||||
let mut settings = settings.load_mut(io.inner()).await?;
|
||||
|
||||
verify_repo_at_index(settings.get_user_repos(), index, &expected_id)?;
|
||||
let removed = settings.remove_repo(|r| r.id() == Some(id.as_str()));
|
||||
|
||||
let removed = settings.remove_repo_at_index(index);
|
||||
|
||||
if let Some(repo) = &removed {
|
||||
io.remove_file(repo.local_path()).await.ok();
|
||||
}
|
||||
join_all(
|
||||
removed
|
||||
.iter()
|
||||
.map(|x| async { io.remove_file(x.local_path()).await.ok() }),
|
||||
)
|
||||
.await;
|
||||
|
||||
settings.save().await?;
|
||||
|
||||
|
|
@ -417,35 +390,6 @@ 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(
|
||||
|
|
@ -593,7 +537,7 @@ pub async fn environment_export_repositories(
|
|||
.file()
|
||||
.set_parent(&window)
|
||||
.add_filter("Text", &["txt"])
|
||||
.set_file_name("repositories.txt")
|
||||
.set_file_name("repositories")
|
||||
.blocking_save_file()
|
||||
.map(|x| x.into_path_buf())
|
||||
.transpose()?
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ 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,
|
||||
|
|
@ -350,7 +349,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_str("project not found"));
|
||||
return Err(RustError::unrecoverable("project not found"));
|
||||
};
|
||||
connection.remove_project(&project);
|
||||
connection.save(io.inner()).await?;
|
||||
|
|
@ -442,7 +441,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_str(
|
||||
return Err(RustError::unrecoverable(
|
||||
"failed to create a new folder for migration",
|
||||
));
|
||||
};
|
||||
|
|
@ -465,7 +464,6 @@ where
|
|||
proceed: AtomicUsize,
|
||||
total_files: usize,
|
||||
new_path: &'a Path,
|
||||
semaphore: Semaphore,
|
||||
ctx: &'a AsyncCommandContext<TauriCopyProjectProgress>,
|
||||
}
|
||||
|
||||
|
|
@ -489,19 +487,15 @@ 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);
|
||||
}
|
||||
|
|
@ -510,17 +504,10 @@ 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)
|
||||
|
|
@ -558,7 +545,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_str("project not found"));
|
||||
return Err(RustError::unrecoverable("project not found"));
|
||||
};
|
||||
project.set_favorite(favorite);
|
||||
connection.update_project(&project);
|
||||
|
|
@ -732,10 +719,10 @@ pub async fn environment_create_project(
|
|||
|
||||
let templates = templates
|
||||
.get_versioned(template_version)
|
||||
.ok_or_else(|| RustError::unrecoverable_str("Templates info version mismatch (bug)"))?;
|
||||
.ok_or_else(|| RustError::unrecoverable("Templates info version mismatch (bug)"))?;
|
||||
|
||||
let unity_version = UnityVersion::parse(&unity_version)
|
||||
.ok_or_else(|| RustError::unrecoverable_str("Bad Unity Version (unparsable)"))?;
|
||||
.ok_or_else(|| RustError::unrecoverable("Bad Unity Version (unparsable)"))?;
|
||||
|
||||
let base_path = Path::new(&base_path);
|
||||
let base_path = {
|
||||
|
|
|
|||
|
|
@ -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_str(
|
||||
return Err(RustError::unrecoverable(
|
||||
"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(format!("{}.alcomtemplate", &template.display_name))
|
||||
.set_file_name(&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_str(
|
||||
None => Err(RustError::unrecoverable(
|
||||
"Template with such id not found (this is bug)",
|
||||
)),
|
||||
Some(template) => Ok(template.into()),
|
||||
|
|
@ -174,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_str(format!("Bad Unity Version Range ({unity_range}): {x}"))
|
||||
RustError::unrecoverable(format!("Bad Unity Version Range ({unity_range}): {x}"))
|
||||
})?),
|
||||
vpm_dependencies: vpm_packages
|
||||
.into_iter()
|
||||
|
|
@ -182,7 +182,7 @@ pub async fn environment_save_template(
|
|||
Ok::<_, RustError>((
|
||||
pkg,
|
||||
VersionRange::from_str(&range).map_err(|x| {
|
||||
RustError::unrecoverable_str(format!("Bad Version Range ({range}): {x}"))
|
||||
RustError::unrecoverable(format!("Bad Version Range ({range}): {x}"))
|
||||
})?,
|
||||
))
|
||||
})
|
||||
|
|
@ -191,7 +191,7 @@ pub async fn environment_save_template(
|
|||
};
|
||||
|
||||
let template = serialize_alcom_template(template)
|
||||
.map_err(|x| RustError::unrecoverable_str(format!("Failed to serialize template: {x}")))?;
|
||||
.map_err(|x| RustError::unrecoverable(format!("Failed to serialize template: {x}")))?;
|
||||
|
||||
if let Some(id) = id {
|
||||
// There is id; overwrite existing one
|
||||
|
|
@ -201,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_str(
|
||||
return Err(RustError::unrecoverable(
|
||||
"Template with such id not found (this is bug)",
|
||||
));
|
||||
};
|
||||
|
|
@ -297,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_str(
|
||||
None => Err(RustError::unrecoverable(
|
||||
"Template with such id not found (this is bug)",
|
||||
)),
|
||||
Some(template) => {
|
||||
|
|
|
|||
|
|
@ -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_str(
|
||||
return Err(RustError::unrecoverable(
|
||||
"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_str("bad version file"));
|
||||
return Err(RustError::unrecoverable("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_str("some packages not found"));
|
||||
return Err(RustError::unrecoverable("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_str("changes version mismatch"));
|
||||
return Err(RustError::unrecoverable("changes version mismatch"));
|
||||
};
|
||||
|
||||
let changes = changes.take_changes();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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};
|
||||
|
|
@ -27,7 +26,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_str("Path does not exist"));
|
||||
return Err(RustError::unrecoverable("Path does not exist"));
|
||||
}
|
||||
OpenOptions::CreateFolderIfNotExists => {
|
||||
super::create_dir_all_with_err(&path).await?;
|
||||
|
|
@ -43,19 +42,9 @@ 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(())
|
||||
}
|
||||
|
|
@ -76,11 +65,7 @@ pub async fn check_for_update(
|
|||
app_handle: AppHandle,
|
||||
stable: bool,
|
||||
) -> updater::Result<Option<Update>> {
|
||||
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 {
|
||||
let endpoint = 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()
|
||||
|
|
@ -148,11 +133,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_str("No update response found"));
|
||||
return Err(RustError::unrecoverable("No update response found"));
|
||||
};
|
||||
|
||||
if response.version() != version {
|
||||
return Err(RustError::unrecoverable_str("Update data version mismatch"));
|
||||
return Err(RustError::unrecoverable("Update data version mismatch"));
|
||||
}
|
||||
|
||||
With::<InstallUpgradeProgress>::continue_async(move |ctx| async move {
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ pub(crate) struct LogEntry {
|
|||
level: LogLevel,
|
||||
target: String,
|
||||
message: String,
|
||||
gui_toast: Option<bool>,
|
||||
gui_toast: bool,
|
||||
}
|
||||
|
||||
fn to_rfc3339_micros<S>(
|
||||
|
|
@ -265,7 +265,8 @@ impl LogEntry {
|
|||
let gui_toast = record
|
||||
.key_values()
|
||||
.get("gui_toast".into())
|
||||
.and_then(|x| x.to_bool());
|
||||
.and_then(|x| x.to_bool())
|
||||
.unwrap_or(true);
|
||||
LogEntry {
|
||||
time: chrono::Local::now(),
|
||||
level: record.level().into(),
|
||||
|
|
|
|||
|
|
@ -115,14 +115,6 @@ 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()))?;
|
||||
|
|
@ -270,18 +262,6 @@ 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>();
|
||||
|
||||
|
|
@ -813,8 +793,7 @@ mod windows {
|
|||
let params = build_updater_args(&self.platform.args, self.current_install);
|
||||
|
||||
tempfile.disable_cleanup(true);
|
||||
drop(tempfile);
|
||||
start_installer(op, file, params)?;
|
||||
start_installer(op, file, params);
|
||||
|
||||
// For windows install, we need to quit app immediately.
|
||||
std::process::exit(0);
|
||||
|
|
@ -921,14 +900,14 @@ mod windows {
|
|||
|
||||
// os specific call
|
||||
#[cfg(windows)]
|
||||
fn start_installer(op: Vec<u16>, file: Vec<u16>, params: Vec<u16>) -> Result<()> {
|
||||
fn start_installer(op: Vec<u16>, file: Vec<u16>, params: Vec<u16>) {
|
||||
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
|
||||
let response = ShellExecuteW(
|
||||
ShellExecuteW(
|
||||
None,
|
||||
PCWSTR(op.as_ptr()),
|
||||
PCWSTR(file.as_ptr()),
|
||||
|
|
@ -936,19 +915,11 @@ 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>) -> Result<()> {
|
||||
fn start_installer(_op: Vec<u16>, _file: Vec<u16>, _params: Vec<u16>) {
|
||||
unreachable!("install_windows_impl called on a non-Windows platform")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,6 @@ export default defineConfig({
|
|||
"@/build": path.join(__dirname, "./build"),
|
||||
},
|
||||
},
|
||||
oxc: {
|
||||
target: "es2025",
|
||||
},
|
||||
build: {
|
||||
outDir: "out",
|
||||
chunkSizeWarningLimit: Number.POSITIVE_INFINITY,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ windows-sys = {
|
|||
"Win32_System_Threading",
|
||||
"Win32_Security",
|
||||
"Win32_System_IO",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_System_Console",
|
||||
],
|
||||
}
|
||||
[build-dependencies]
|
||||
embed-manifest = "1.5.0"
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
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,7 +18,6 @@ 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"));
|
||||
|
|
@ -99,9 +98,6 @@ 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-rc.0"
|
||||
version = "0.0.16-beta.0"
|
||||
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::{HttpClient, PackageInfo, PackageManifest, io};
|
|||
use futures::prelude::*;
|
||||
use hex::FromHex;
|
||||
use indexmap::IndexMap;
|
||||
use log::debug;
|
||||
use log::{debug, error};
|
||||
use std::io::SeekFrom;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::pin;
|
||||
|
|
@ -136,16 +136,12 @@ async fn get_package<T: HttpClient>(
|
|||
.and_then(|x| <[u8; 256 / 8] as FromHex>::from_hex(x).ok())
|
||||
&& repo_hash != zip_hash
|
||||
{
|
||||
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()
|
||||
),
|
||||
));
|
||||
error!(
|
||||
"Package hash mismatched! This will be hard error in the future!: {} v{}",
|
||||
package.name(),
|
||||
package.version()
|
||||
);
|
||||
//return None;
|
||||
}
|
||||
|
||||
Ok(zip_file)
|
||||
|
|
|
|||
|
|
@ -23,18 +23,7 @@ pub struct Settings {
|
|||
|
||||
impl Settings {
|
||||
pub async fn load(io: &DefaultEnvironmentIo) -> io::Result<Self> {
|
||||
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 settings = VpmSettings::load(io).await?;
|
||||
let vrc_get_settings = VrcGetSettings::load(io).await?;
|
||||
|
||||
Ok(Self {
|
||||
|
|
@ -269,14 +258,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ use crate::UserRepoSetting;
|
|||
use crate::environment::PackageCollection;
|
||||
use crate::io;
|
||||
use crate::io::DefaultEnvironmentIo;
|
||||
use crate::utils::{save_json, try_load_json};
|
||||
use crate::utils::{load_json_or_default, save_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, Default)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AsJson {
|
||||
#[serde(default)]
|
||||
|
|
@ -78,39 +78,45 @@ struct AsJson {
|
|||
rest: JsonObject,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
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)]
|
||||
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<Option<Self>> {
|
||||
Self::load_inner(io, JSON_PATH).await
|
||||
}
|
||||
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_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 }))
|
||||
Ok(Self { parsed })
|
||||
}
|
||||
|
||||
pub(crate) fn user_repos(&self) -> &[UserRepoSetting] {
|
||||
|
|
@ -155,32 +161,6 @@ 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);
|
||||
}
|
||||
|
|
@ -218,8 +198,7 @@ impl VpmSettings {
|
|||
}
|
||||
|
||||
pub async fn save(&self, io: &DefaultEnvironmentIo) -> io::Result<()> {
|
||||
save_json(io, JSON_PATH.as_ref(), &self.parsed).await?;
|
||||
save_json(io, ALT_JSON_PATH.as_ref(), &self.parsed).await
|
||||
save_json(io, JSON_PATH.as_ref(), &self.parsed).await
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ 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])*
|
||||
|
|
@ -85,9 +84,9 @@ macro_rules! package_json_struct {
|
|||
$(#[$optional])?
|
||||
$optional_vis headers: indexmap::IndexMap<Box<str>, Box<str>>,
|
||||
|
||||
$(#[$optional_url])?
|
||||
$(#[$optional])?
|
||||
$optional_vis changelog_url: Option<Url>,
|
||||
$(#[$optional_url])?
|
||||
$(#[$optional])?
|
||||
$optional_vis documentation_url: Option<Url>,
|
||||
|
||||
$(#[$optional])?
|
||||
|
|
@ -120,22 +119,10 @@ 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)]
|
||||
|
|
@ -271,7 +258,6 @@ 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)]
|
||||
|
|
@ -348,31 +334,3 @@ 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ 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, PartialEq, Eq, Hash)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ChipArchitecture {
|
||||
X86_64,
|
||||
ARM64,
|
||||
|
|
@ -48,27 +47,7 @@ pub async fn load_unity_by_loading_unity_hub_files() -> Result<Vec<UnityEditorIn
|
|||
)
|
||||
.await?;
|
||||
|
||||
// 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)
|
||||
Ok(a.into_iter().chain(b).chain(c).collect())
|
||||
}
|
||||
|
||||
async fn get_custom_install_location(local_settings: &LocalSettings) -> Option<PathBuf> {
|
||||
|
|
@ -192,9 +171,7 @@ async fn load_located_editors(local_settings: &LocalSettings) -> Vec<UnityEditor
|
|||
#[serde(with = "either::serde_untagged")]
|
||||
location: Either<String, Vec<String>>,
|
||||
version: String,
|
||||
// architecture may be null or absent in older editors-v2.json entries
|
||||
#[serde(default)]
|
||||
architecture: Option<String>,
|
||||
architecture: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct EditorsV2 {
|
||||
|
|
@ -215,9 +192,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_deref() {
|
||||
Some("x86_64") | None => Some(ChipArchitecture::X86_64),
|
||||
Some("arm64") => Some(ChipArchitecture::ARM64),
|
||||
let architecture = match editor.architecture.as_str() {
|
||||
"x86_64" => Some(ChipArchitecture::X86_64),
|
||||
"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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ 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(
|
||||
|
|
@ -24,8 +23,6 @@ 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,
|
||||
|
|
@ -49,20 +46,6 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::version::Version;
|
|||
use serde::de::Unexpected;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct UnityVersion {
|
||||
// major version such as 2019, 2022, and 6
|
||||
// note: 5 < 2017 < 2023 < 6 < 7 ...
|
||||
|
|
@ -263,20 +263,6 @@ 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))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vrc-get"
|
||||
version = "1.9.2-rc.0"
|
||||
version = "1.9.2-beta.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
|
@ -30,7 +30,7 @@ serde_json = { version = "1", features = ["preserve_order"] }
|
|||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] }
|
||||
|
||||
[dependencies.vrc-get-vpm]
|
||||
version = "0.0.16-rc.0"
|
||||
version = "0.0.16-beta.0"
|
||||
path = "../vrc-get-vpm"
|
||||
default-features = false
|
||||
|
||||
|
|
|
|||
|
|
@ -13,16 +13,17 @@ anyhow = { version = "1.0.102", features = ["backtrace"] }
|
|||
cargo_metadata = "0.23.1"
|
||||
clap = { version = "4.6.0", features = ["derive", "env"] }
|
||||
itertools = "0.14.0"
|
||||
object = { version = "0.39.1", features = ["read_core", "elf", "macho", "pe"], default-features = false }
|
||||
object = { version = "0.39.0", features = ["read_core", "elf", "macho", "pe"], default-features = false }
|
||||
chrono = { version = "0.4.44", features = ["serde", "now"], default-features = false }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
indexmap = { version = "2.13.0", features = ["serde"] }
|
||||
serde_json = "1.0.150"
|
||||
serde_json = "1.0.149"
|
||||
ureq = { version = "3.3.0", features = ["gzip", "native-tls"], default-features = false }
|
||||
flate2 = "1.1.1"
|
||||
tar = { version = "0.4.46", features = [], default-features = false }
|
||||
plist = "1.9.0"
|
||||
tar = { version = "0.4.43", features = [], default-features = false }
|
||||
plist = "1.8.0"
|
||||
rpm = { version = "0.22.0", default-features = false, features = ["gzip-compression"] }
|
||||
ar = "0.9.0"
|
||||
fs_extra = "1.3.0"
|
||||
base64 = "0.22.1"
|
||||
minisign = "0.9.1"
|
||||
zip = { version = "8.6.0", default-features = false, features = ["deflate-flate2"] }
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ struct UpdaterJson<'a> {
|
|||
struct Platform {
|
||||
signature: String,
|
||||
url: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn create_alcom_updater_json(assets_dir: &Path, version: &str, out_path: &Path) -> Result<()> {
|
||||
|
|
@ -66,27 +65,9 @@ pub fn create_alcom_updater_json(assets_dir: &Path, version: &str, out_path: &Pa
|
|||
.with_context(|| sig_name.clone())?;
|
||||
|
||||
let url = format!("{base_url}/{file_name}");
|
||||
platforms.insert(
|
||||
platform.to_string(),
|
||||
Platform {
|
||||
signature,
|
||||
url,
|
||||
args: vec![],
|
||||
},
|
||||
);
|
||||
platforms.insert(platform.to_string(), Platform { signature, url });
|
||||
}
|
||||
|
||||
platforms["windows-x86_64"].args = [
|
||||
"/SP-",
|
||||
"/SILENT",
|
||||
"/NOICONS",
|
||||
"!peruser:/CURRENTUSER",
|
||||
"!machine:/ALLUSERS",
|
||||
]
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect();
|
||||
|
||||
let is_beta = version.contains('-');
|
||||
let notes = if is_beta {
|
||||
// https://github.com/vrc-get/vrc-get/blob/master/CHANGELOG-gui.md#unreleased
|
||||
|
|
|
|||
|
|
@ -170,10 +170,6 @@ fn build_cargo(
|
|||
}
|
||||
}
|
||||
|
||||
if config.devtools {
|
||||
features.push("devtools");
|
||||
}
|
||||
|
||||
cmd.arg("--features").arg(features.iter().join(","));
|
||||
|
||||
if let Some(target) = target_triple {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
use crate::utils::{self, build_dir, build_target, target_os};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod app;
|
||||
mod appimage;
|
||||
mod deb;
|
||||
mod dmg;
|
||||
mod linux;
|
||||
mod rpm;
|
||||
mod setup_exe;
|
||||
|
||||
/// Individual bundle artifact that can be produced.
|
||||
|
|
@ -15,21 +17,15 @@ mod setup_exe;
|
|||
/// If `--bundles` is not specified, all artifacts for the target platform are produced.
|
||||
///
|
||||
/// **macOS** artifacts:
|
||||
/// - `app` - `ALCOM.app` application bundle
|
||||
/// - `dmg` - `ALCOM_<version>_<arch>.dmg` disk image
|
||||
/// - `app-updater` - `ALCOM.app.tar.gz` updater payload
|
||||
/// - `app` — `ALCOM.app` application bundle
|
||||
/// - `dmg` — `ALCOM_<version>_<arch>.dmg` disk image
|
||||
/// - `app-updater` — `ALCOM.app.tar.gz` updater payload
|
||||
///
|
||||
/// **Linux** artifacts:
|
||||
/// - `app-image` - `ALCOM_<version>_<arch>.AppImage`
|
||||
/// - `app-image-updater` - `ALCOM_<version>_<arch>.AppImage.tar.gz` updater payload
|
||||
/// - `deb` - `ALCOM_<version>_<arch>.deb` Debian package
|
||||
/// - `rpm` - `ALCOM-<version>-1.<arch>.rpm` RPM package
|
||||
/// - `buildroot` - The package manager independent buildroot for external package managers.
|
||||
///
|
||||
/// **Windows** artifacts:
|
||||
/// - `setup-exe` - `-setup.exe` for first-time installation
|
||||
/// - `setup-exe-zip` - `-setup.exe.zip` to workaround warning from browsers
|
||||
/// - `exe-updater` - `-updater.exe` for the updater. This includes
|
||||
/// - `app-image` — `ALCOM_<version>_<arch>.AppImage`
|
||||
/// - `app-image-updater` — `ALCOM_<version>_<arch>.AppImage.tar.gz` updater payload
|
||||
/// - `deb` — `ALCOM_<version>_<arch>.deb` Debian package
|
||||
/// - `rpm` — `ALCOM-<version>-1.<arch>.rpm` RPM package
|
||||
#[derive(clap::ValueEnum, Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum BundleKind {
|
||||
// --- macOS ---
|
||||
|
|
@ -50,18 +46,14 @@ pub(crate) enum BundleKind {
|
|||
///
|
||||
/// Unlike dmg depends on app, deb/rpm doesn't depend on this bundle.
|
||||
Buildroot,
|
||||
/// Debian package
|
||||
Deb,
|
||||
/// RPM package
|
||||
Rpm,
|
||||
/// Windows setup.exe
|
||||
SetupExe,
|
||||
/// Windows setup.exe in zip (requires setup.exe to already exist in bundle dir)
|
||||
SetupExeZip,
|
||||
/// Windows setup.exe for updater
|
||||
ExeUpdater,
|
||||
|
||||
// deleted
|
||||
#[value(hide = true)]
|
||||
Deb,
|
||||
#[value(hide = true)]
|
||||
Rpm,
|
||||
}
|
||||
|
||||
/// Bundles the ALCOM application for the target platform.
|
||||
|
|
@ -95,7 +87,7 @@ pub(super) struct Command {
|
|||
/// Specific bundle artifacts to produce (comma-separated or repeated).
|
||||
///
|
||||
/// When not specified, all artifacts for the target platform are produced.
|
||||
/// Use this to split the bundling process - e.g. produce only `app` first,
|
||||
/// Use this to split the bundling process — e.g. produce only `app` first,
|
||||
/// then sign it, then produce `dmg` and `app-updater`.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
bundles: Vec<BundleKind>,
|
||||
|
|
@ -113,12 +105,6 @@ impl crate::Command for Command {
|
|||
|
||||
let bundles = self.bundles.as_slice();
|
||||
|
||||
if bundles.contains(&BundleKind::Deb) || bundles.contains(&BundleKind::Rpm) {
|
||||
bail!(
|
||||
"--bundles deb and --bundles rpm are removed. Please use native packaging configuration at vrc-get-gui/bundles"
|
||||
)
|
||||
}
|
||||
|
||||
if bundles.is_empty() {
|
||||
println!("Note: no bundles are specified");
|
||||
}
|
||||
|
|
@ -147,12 +133,16 @@ impl crate::Command for Command {
|
|||
linux::create_install_build_root(&ctx, self.buildroot.as_deref())?;
|
||||
}
|
||||
|
||||
if bundles.contains(&BundleKind::SetupExe) {
|
||||
setup_exe::create_setup_exe(&ctx)?;
|
||||
if bundles.contains(&BundleKind::Deb) {
|
||||
deb::create_deb(&ctx)?;
|
||||
}
|
||||
|
||||
if bundles.contains(&BundleKind::SetupExeZip) {
|
||||
setup_exe::create_setup_exe_zip(&ctx)?;
|
||||
if bundles.contains(&BundleKind::Rpm) {
|
||||
rpm::create_rpm(&ctx)?;
|
||||
}
|
||||
|
||||
if bundles.contains(&BundleKind::SetupExe) {
|
||||
setup_exe::create_setup_exe(&ctx)?;
|
||||
}
|
||||
|
||||
if bundles.contains(&BundleKind::ExeUpdater) {
|
||||
|
|
@ -212,6 +202,14 @@ impl<'a> BundleContext<'a> {
|
|||
self.version.as_str()
|
||||
}
|
||||
|
||||
pub fn short_description(&self) -> &str {
|
||||
"ALCOM - Alternative Creator Companion"
|
||||
}
|
||||
|
||||
pub fn long_description(&self) -> &str {
|
||||
"ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri."
|
||||
}
|
||||
|
||||
/// Binary name without extension (e.g. `ALCOM`).
|
||||
pub fn binary_name(&self) -> &str {
|
||||
"ALCOM"
|
||||
|
|
|
|||
108
xtask/src/bundle_alcom/deb.rs
Normal file
108
xtask/src/bundle_alcom/deb.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use crate::bundle_alcom::BundleContext;
|
||||
use crate::bundle_alcom::linux::*;
|
||||
use crate::utils::tar::TarBuilderExt;
|
||||
use crate::utils::{CountingIo, tar, target_arch};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use flate2::Compression;
|
||||
use flate2::write::GzEncoder;
|
||||
use std::io::Write;
|
||||
use std::{fs, io};
|
||||
|
||||
fn deb_arch(triple: &str) -> Result<&str> {
|
||||
match target_arch(triple) {
|
||||
"aarch64" => Ok("arm64"),
|
||||
"x86_64" => Ok("amd64"),
|
||||
_ => {
|
||||
bail!(
|
||||
"unsupported architecture in target triple for deb: {}",
|
||||
triple
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_deb(ctx: &BundleContext<'_>) -> Result<()> {
|
||||
let arch = deb_arch(ctx.target_tuple)?;
|
||||
let pkg_name = format!("alcom_{}-1_{arch}", ctx.version());
|
||||
|
||||
let (estimated_size, data_tar_gz) = {
|
||||
let gz = GzEncoder::new(Vec::new(), Compression::default());
|
||||
let mut tar = tar::Builder::new(CountingIo::new(gz));
|
||||
|
||||
create_install_build_root_impl(ctx, &mut tar).context("creating data.tar.gz")?;
|
||||
|
||||
let finished_gz_count = tar.into_inner()?;
|
||||
let estimated_size = finished_gz_count.count();
|
||||
let finished_gz = finished_gz_count.into_inner();
|
||||
let data_tar_gz = finished_gz.finish().context("finishing data.tar.gz")?;
|
||||
|
||||
(estimated_size, data_tar_gz)
|
||||
};
|
||||
|
||||
let library = detect_library_versions(&ctx.binary_path())?;
|
||||
|
||||
// Build control.tar.gz
|
||||
let control_tar_gz = {
|
||||
let mut control_tar_gz = Vec::new();
|
||||
let gz = GzEncoder::new(&mut control_tar_gz, Compression::best());
|
||||
let mut tar = tar::Builder::new(gz);
|
||||
|
||||
let control = {
|
||||
let template_path = ctx.gui_dir.join("bundle/deb-control");
|
||||
fs::read_to_string(&template_path)
|
||||
.with_context(|| format!("reading {}", template_path.display()))?
|
||||
.replace("{{version}}", ctx.version())
|
||||
.replace("{{arch}}", arch)
|
||||
.replace("{{estimated_size}}", &(estimated_size / 1024).to_string())
|
||||
.replace("{{libc_version}}", &library.libc)
|
||||
.replace("{{libgcc_version}}", &library.libgcc)
|
||||
};
|
||||
|
||||
tar.append_file_data(0o644, "control", io::Cursor::new(control.as_bytes()))
|
||||
.context("appending control file")?;
|
||||
|
||||
let gz = tar.into_inner().context("finishing control tar")?;
|
||||
gz.finish().context("finishing control gzip")?;
|
||||
|
||||
control_tar_gz
|
||||
};
|
||||
|
||||
// Assemble .deb as an ar archive.
|
||||
let deb_dir = ctx.bundle_dir.join("deb");
|
||||
fs::create_dir_all(&deb_dir)?;
|
||||
let deb_name = format!("{pkg_name}.deb");
|
||||
let deb_out = deb_dir.join(&deb_name);
|
||||
|
||||
{
|
||||
let deb_file = fs::File::create(&deb_out)
|
||||
.with_context(|| format!("creating {}", deb_out.display()))?;
|
||||
let mut builder = ar::Builder::new(deb_file);
|
||||
|
||||
// debian-binary
|
||||
let debian_binary = b"2.0\n";
|
||||
let mut header = ar::Header::new(b"debian-binary".to_vec(), debian_binary.len() as u64);
|
||||
header.set_mode(0o100644);
|
||||
builder
|
||||
.append(&header, &mut debian_binary.as_slice())
|
||||
.context("appending debian-binary")?;
|
||||
|
||||
// control.tar.gz
|
||||
let mut header = ar::Header::new(b"control.tar.gz".to_vec(), control_tar_gz.len() as u64);
|
||||
header.set_mode(0o100644);
|
||||
builder
|
||||
.append(&header, &mut control_tar_gz.as_slice())
|
||||
.context("appending control.tar.gz")?;
|
||||
|
||||
// data.tar.gz
|
||||
let mut header = ar::Header::new(b"data.tar.gz".to_vec(), data_tar_gz.len() as u64);
|
||||
header.set_mode(0o100644);
|
||||
builder
|
||||
.append(&header, &mut data_tar_gz.as_slice())
|
||||
.context("appending data.tar.gz")?;
|
||||
|
||||
builder.into_inner()?.flush()?;
|
||||
}
|
||||
|
||||
println!("created: {}", deb_out.display());
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
use super::BundleContext;
|
||||
use anyhow::{Context, Result};
|
||||
use crate::utils::tar::TarBuilderExt;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::{fs, io};
|
||||
|
||||
|
|
@ -112,7 +114,6 @@ impl<'a> BuildRootFs for RealBuildRootFs<'a> {
|
|||
}
|
||||
|
||||
fn create_file(&mut self, mode: u32, relative: &str, data: &mut dyn io::Read) -> Result<()> {
|
||||
let _ = mode; // suppress warning on windows
|
||||
let path = &self.0.join(relative);
|
||||
std::io::copy(
|
||||
data,
|
||||
|
|
@ -131,6 +132,37 @@ impl<'a> BuildRootFs for RealBuildRootFs<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<W: io::Write> BuildRootFs for tar::Builder<W> {
|
||||
fn create_dir(&mut self, path: &str) -> Result<()> {
|
||||
self.append_directory(path)
|
||||
}
|
||||
|
||||
fn create_file(&mut self, mode: u32, path: &str, data: &mut dyn io::Read) -> Result<()> {
|
||||
self.append_file_data(mode, path, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildRootFs for rpm::PackageBuilder {
|
||||
fn create_dir(&mut self, path: &str) -> Result<()> {
|
||||
self.with_dir_entry(rpm::FileOptions::dir(format!("/{path}")))
|
||||
.map(|_| ())
|
||||
.with_context(|| format!("creating directory {}", path))
|
||||
}
|
||||
|
||||
fn create_file(&mut self, mode: u32, path: &str, data: &mut dyn io::Read) -> Result<()> {
|
||||
let mut contents = vec![];
|
||||
data.read_to_end(&mut contents)
|
||||
.with_context(|| format!("reading data for {}", path))?;
|
||||
|
||||
self.with_file_contents(
|
||||
contents,
|
||||
rpm::FileOptions::new(format!("/{path}")).permissions(mode as u16),
|
||||
)
|
||||
.map(|_| ())
|
||||
.with_context(|| format!("creating file {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the desktop file template from `alcom.desktop`.
|
||||
///
|
||||
/// The template uses `{{key}}` placeholders as in the tauri bundler.
|
||||
|
|
@ -145,3 +177,89 @@ pub fn render_desktop_file(ctx: &BundleContext<'_>, exec: &str) -> Result<String
|
|||
pub static LINUX_ICON_RESOLUTIONS: &[&str] = &["32x32", "64x64", "128x128"];
|
||||
|
||||
pub static LINUX_ICON_NAME: &str = "alcom"; // keep in sync with alcom.desktop template
|
||||
|
||||
pub struct LibraryVersions {
|
||||
pub libc: String,
|
||||
pub libgcc: String,
|
||||
}
|
||||
|
||||
pub fn detect_library_versions(path: &Path) -> Result<LibraryVersions> {
|
||||
use object::read::elf::ElfFile64;
|
||||
use object::{Endianness, Object, ObjectSymbol};
|
||||
|
||||
let binary = fs::read(path).context("Reading binary")?;
|
||||
|
||||
let elf = ElfFile64::<Endianness>::parse(&binary).context("failed to parse binary")?;
|
||||
|
||||
let Some(versions) = elf.elf_section_table().versions(elf.endian(), elf.data())? else {
|
||||
bail!("no version table found");
|
||||
};
|
||||
let versions = elf
|
||||
.dynamic_symbols()
|
||||
.map(|s| versions.version_index(elf.endian(), s.index()))
|
||||
.flat_map(|i| versions.version(i).transpose())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let mut by_lib = HashMap::new();
|
||||
for version in versions.into_iter() {
|
||||
let lib = version.name().split(|&x| x == b'_').next().unwrap();
|
||||
let version = VersionNumber::try_from(version.name())?;
|
||||
|
||||
let existing = by_lib.entry(lib).or_insert(VersionNumber::MIN);
|
||||
if *existing < version {
|
||||
*existing = version;
|
||||
}
|
||||
}
|
||||
|
||||
//for (lib, version) in &by_lib {
|
||||
// let lib = std::str::from_utf8(lib)?;
|
||||
// println!("{lib}: {version}");
|
||||
//}
|
||||
|
||||
return Ok(LibraryVersions {
|
||||
libc: by_lib[&b"GLIBC"[..]].to_string(),
|
||||
libgcc: by_lib[&b"GCC"[..]].to_string(),
|
||||
});
|
||||
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq)]
|
||||
struct VersionNumber(Vec<u32>);
|
||||
|
||||
impl VersionNumber {
|
||||
pub const MIN: VersionNumber = VersionNumber(Vec::new());
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a [u8]> for VersionNumber {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
|
||||
let value = if let Some(index) = value.iter().position(|&x| x == b'_') {
|
||||
value.split_at(index + 1).1
|
||||
} else {
|
||||
value
|
||||
};
|
||||
let value = std::str::from_utf8(value)?;
|
||||
let components = value
|
||||
.split('.')
|
||||
.map(std::str::FromStr::from_str)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Self(components))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VersionNumber {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut iter = self.0.iter();
|
||||
f.write_fmt(format_args!("{}", iter.next().unwrap()))?;
|
||||
for x in iter {
|
||||
f.write_fmt(format_args!(".{}", x))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VersionNumber {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
xtask/src/bundle_alcom/rpm.rs
Normal file
63
xtask/src/bundle_alcom/rpm.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use super::BundleContext;
|
||||
use crate::bundle_alcom::linux::*;
|
||||
use anyhow::{Context, Result};
|
||||
use rpm::Dependency;
|
||||
use std::fs;
|
||||
|
||||
pub fn create_rpm(ctx: &BundleContext<'_>) -> Result<()> {
|
||||
let arch = rpm_arch(ctx.target_tuple);
|
||||
let rpm_name = format!("alcom-{}-1.{arch}.rpm", ctx.version());
|
||||
let rpm_dir = ctx.bundle_dir.join("rpm");
|
||||
fs::create_dir_all(&rpm_dir)?;
|
||||
let rpm_out = rpm_dir.join(&rpm_name);
|
||||
|
||||
let library = detect_library_versions(&ctx.binary_path())?;
|
||||
|
||||
let mut builder = rpm::PackageBuilder::new(
|
||||
"alcom",
|
||||
// RPM doesn't support '-' in their version name.
|
||||
// It's recommended to use '~' instead.
|
||||
// https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/#_handling_non_sorting_versions_with_tilde_dot_and_caret
|
||||
&ctx.version().replace('-', "~"),
|
||||
"MIT",
|
||||
arch,
|
||||
ctx.short_description(),
|
||||
);
|
||||
builder.release("1").description(ctx.long_description());
|
||||
|
||||
builder.requires(Dependency::any(format!(
|
||||
"libgcc_s.so.1(GCC_{})(64bit)",
|
||||
library.libgcc
|
||||
)));
|
||||
builder.requires(Dependency::any(format!(
|
||||
"libc.so.6(GLIBC_{})(64bit)",
|
||||
library.libc
|
||||
)));
|
||||
builder.requires(Dependency::any("libgtk-3.so.0()(64bit)"));
|
||||
builder.requires(Dependency::any("libwebkit2gtk-4.1.so.0()(64bit)"));
|
||||
|
||||
// Binary.
|
||||
create_install_build_root_impl(ctx, &mut builder).context("adding files to rpm")?;
|
||||
|
||||
let pkg = builder.build().context("building rpm package")?;
|
||||
|
||||
pkg.write_file(&rpm_out)
|
||||
.with_context(|| format!("writing {}", rpm_out.display()))?;
|
||||
|
||||
println!("created: {}", rpm_out.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// RPM architecture string.
|
||||
fn rpm_arch(triple: &str) -> &str {
|
||||
if triple.starts_with("aarch64") {
|
||||
"aarch64"
|
||||
} else if triple.starts_with("x86_64") {
|
||||
"x86_64"
|
||||
} else {
|
||||
panic!(
|
||||
"unsupported architecture in target triple for rpm: {}",
|
||||
triple
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
use crate::bundle_alcom::BundleContext;
|
||||
use crate::utils::command::{CommandExt, WineRunner};
|
||||
use crate::utils::{cargo, download_file_cached, target_abi};
|
||||
use crate::utils::{download_file_cached, target_abi};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command as ProcessCommand;
|
||||
use zip::write::FileOptions;
|
||||
|
||||
const WEBVIEW2_URL: &str = "https://go.microsoft.com/fwlink/?linkid=2124703";
|
||||
const INNO_SETUP_VERSION: &str = "6.7.1";
|
||||
|
|
@ -29,26 +28,6 @@ pub fn create_setup_exe(ctx: &BundleContext<'_>) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_setup_exe_zip(ctx: &BundleContext<'_>) -> Result<()> {
|
||||
let wrapper_in_bundle = ctx.bundle_dir.join("setup/alcom-setup.exe");
|
||||
let zip = ctx.bundle_dir.join("setup/alcom-setup.exe.zip");
|
||||
|
||||
let mut zip = zip::write::ZipWriter::new(fs::File::create(&zip).context("creating zip file")?);
|
||||
zip.start_file(
|
||||
format!("ALCOM-{}-x86_64-setup.exe", cargo::gui_version()),
|
||||
FileOptions::DEFAULT,
|
||||
)
|
||||
.context("adding file to zip")?;
|
||||
std::io::copy(
|
||||
&mut std::io::BufReader::new(
|
||||
fs::File::open(&wrapper_in_bundle).context("opening alcom-setup.exe")?,
|
||||
),
|
||||
&mut zip,
|
||||
)
|
||||
.context("copying file to zip")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_updater_exe(ctx: &BundleContext<'_>) -> Result<()> {
|
||||
let iss_setup = ctx.bundle_dir.join("setup/alcom-setup.exe");
|
||||
|
||||
|
|
|
|||
|
|
@ -150,7 +150,6 @@ fn process_pe_64(binary: &[u8]) -> Result<bool> {
|
|||
| b"crypt32.dll" // since Windows NT 4.0/XP era
|
||||
| b"bcryptprimitives.dll" // since Windows 7/Server 2008 R2
|
||||
| b"combase.dll" // since Windows 8 / Server 2012
|
||||
| b"shlwapi.dll" // since Windows 98
|
||||
| b"api-ms-win-core-synch-l1-2-0.dll" // since Windows 8 / Server 2012
|
||||
=> {
|
||||
println!(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use cargo_metadata::Metadata;
|
||||
use cargo_metadata::semver::Version;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
@ -11,12 +10,3 @@ pub fn cargo_metadata() -> &'static Metadata {
|
|||
.expect("cargo metadata failed")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gui_version() -> &'static Version {
|
||||
cargo_metadata()
|
||||
.packages
|
||||
.iter()
|
||||
.find(|p| p.name == "vrc-get-gui")
|
||||
.map(|p| &p.version)
|
||||
.expect("vrc-get-gui metadata not found")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue