Switch to Electron (#1747)
Co-authored-by: Hannah Lindrob <hannahlindrob@ourlook.com> Co-authored-by: Sapphire <imsapphire0@gmail.com>
1
.envrc
@@ -2,7 +2,6 @@ if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
|
||||
fi
|
||||
|
||||
nix_direnv_watch_file rust-toolchain.toml
|
||||
nix_direnv_watch_file package.json
|
||||
if ! use flake . --impure
|
||||
then
|
||||
|
||||
3
.github/CODEOWNERS
vendored
@@ -17,8 +17,7 @@
|
||||
/gui/src/components/settings/ @Erimelowo @loucass003
|
||||
|
||||
# Rust part of the GUI
|
||||
/gui/src-tauri/ @loucass003
|
||||
/Cargo.lock @loucass003
|
||||
/gui/electron/ @loucass003
|
||||
|
||||
# Some server code~
|
||||
/server/ @ButterscotchV @Eirenliel @Erimelowo
|
||||
|
||||
129
.github/workflows/build-gui.yml
vendored
@@ -1,129 +0,0 @@
|
||||
name: Build GUI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/build-gui.yml
|
||||
- gui/**
|
||||
- package*.json
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/build-gui.yml
|
||||
- gui/**
|
||||
- package*.json
|
||||
workflow_dispatch:
|
||||
create:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
pnpm i
|
||||
cd gui
|
||||
pnpm run lint
|
||||
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
[
|
||||
ubuntu-22.04,
|
||||
windows-latest,
|
||||
macos-latest,
|
||||
ubuntu-22.04-arm,
|
||||
windows-11-arm,
|
||||
]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
env:
|
||||
# Don't mark warnings as errors
|
||||
CI: false
|
||||
BUILD_ARCH: ${{ endsWith(matrix.os, 'arm') && 'aarch64' || 'amd64' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- if: startsWith(matrix.os, 'ubuntu')
|
||||
name: Set up Linux dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
|
||||
with:
|
||||
packages: libgtk-3-dev webkit2gtk-4.1 libappindicator3-dev librsvg2-dev patchelf
|
||||
# Increment to invalidate the cache
|
||||
version: ${{ format('v1.0-{0}', env.BUILD_ARCH) }}
|
||||
# Enables a workaround to attempt to run pre and post install scripts
|
||||
execute_install_scripts: true
|
||||
# Disables uploading logs as a build artifact
|
||||
debug: false
|
||||
|
||||
- if: matrix.os == 'windows-11-arm'
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache: false
|
||||
|
||||
- name: Cache cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm i
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
NODE_OPTIONS: ${{ matrix.os == 'macos-latest' && '--max-old-space-size=4096' || '' }}
|
||||
run: pnpm run skipbundler --config $( ./gui/scripts/gitversion.mjs )
|
||||
|
||||
- if: startsWith(matrix.os, 'windows')
|
||||
name: Upload a Build Artifact (Windows)
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
# Artifact name
|
||||
name: ${{ format('SlimeVR-GUI-Windows-{0}', env.BUILD_ARCH) }}
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: target/release/slimevr.exe
|
||||
|
||||
- if: startsWith(matrix.os, 'ubuntu')
|
||||
name: Upload a Build Artifact (Linux)
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
# Artifact name
|
||||
name: ${{ format('SlimeVR-GUI-Linux-{0}', env.BUILD_ARCH) }}
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: target/release/slimevr
|
||||
|
||||
- if: matrix.os == 'macos-latest'
|
||||
name: Upload a Build Artifact (macOS)
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
# Artifact name
|
||||
name: SlimeVR-GUI-macOS
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: target/release/slimevr
|
||||
257
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
name: SlimeVR Full Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
setup-matrix:
|
||||
name: Configure Build Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- id: set-matrix
|
||||
shell: bash
|
||||
run: |
|
||||
BASE='[{"os":"ubuntu-22.04","platform":"linux"},{"os":"windows-latest","platform":"windows"},{"os":"macos-latest","platform":"macos"}]'
|
||||
EXTRA='[{"os":"ubuntu-22.04-arm","platform":"linux"},{"os":"windows-11-arm","platform":"windows"}]'
|
||||
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* || "${{ github.ref }}" == refs/heads/main || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
RESULT=$(echo "$BASE $EXTRA" | jq -c -s 'add')
|
||||
else
|
||||
RESULT=$(echo "$BASE" | jq -c '.')
|
||||
fi
|
||||
echo "matrix={\"include\":$RESULT}" >> "$GITHUB_OUTPUT"
|
||||
gui-checks:
|
||||
name: Gui Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- name: GUI Lint
|
||||
run: pnpm i && cd gui && pnpm run lint
|
||||
|
||||
java-checks:
|
||||
name: Java Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
- name: Java Spotless Check
|
||||
run: ./gradlew spotlessCheck --build-cache
|
||||
|
||||
|
||||
build-server-jar:
|
||||
name: Build Desktop Server
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
- name: Build ShadowJar
|
||||
run: ./gradlew :server:desktop:shadowJar --build-cache
|
||||
- name: Test with Gradle
|
||||
run: ./gradlew :server:desktop:test
|
||||
- name: Upload Server Jar
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: server-jar
|
||||
path: server/desktop/build/libs/slimevr.jar
|
||||
|
||||
build-gui-frontend:
|
||||
name: Build GUI Assets
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- name: Build JS
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: pnpm i && cd gui && pnpm run build
|
||||
- name: Tar GUI Dist
|
||||
run: tar -czf slimevr-gui-dist.tar.gz -C gui/out .
|
||||
- name: Upload GUI Dist
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: gui-dist
|
||||
path: slimevr-gui-dist.tar.gz
|
||||
|
||||
package-desktop:
|
||||
name: Package Desktop (${{ matrix.platform }} - ${{ matrix.os }})
|
||||
needs: [setup-matrix, build-server-jar, build-gui-frontend]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- name: Download Server Jar
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: server-jar
|
||||
path: server/desktop/build/libs/
|
||||
- name: Download GUI Dist
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: gui-dist
|
||||
- name: Extract GUI for Electron
|
||||
shell: bash
|
||||
run: mkdir -p gui/out && tar -xzf slimevr-gui-dist.tar.gz -C gui/out
|
||||
- name: Run Electron Builder
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: |
|
||||
mkdir -p gui/dist/artifacts/linux/ \
|
||||
gui/dist/artifacts/win \
|
||||
gui/dist/artifacts/mac
|
||||
cd gui
|
||||
pnpm i
|
||||
pnpm exec electron-builder --${{ matrix.platform }} ${{ matrix.platform == 'macos' && '--universal' || '' }} --publish never
|
||||
- name: Collect and Rename Artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
SRC_DIR="${{ github.workspace }}/gui/dist/artifacts"
|
||||
DEST_DIR="${{ github.workspace }}/release-out"
|
||||
mkdir -p "$DEST_DIR"
|
||||
|
||||
if [ "${{ matrix.platform }}" == "windows" ]; then
|
||||
[[ "${{ matrix.os }}" == *"arm"* ]] && SUFFIX="win-aarch64" || SUFFIX="win64"
|
||||
cp "$SRC_DIR"/win/*.exe "$DEST_DIR/SlimeVR-$SUFFIX.zip"
|
||||
|
||||
elif [ "${{ matrix.platform }}" == "linux" ]; then
|
||||
for f in "$SRC_DIR"/linux/*; do
|
||||
[ -d "$f" ] && continue
|
||||
BASE=$(basename "$f")
|
||||
NEW_NAME=$(echo "$BASE" | sed -e 's/x86_64/amd64/g' -e 's/arm64/aarch64/g')
|
||||
cp "$f" "$DEST_DIR/$NEW_NAME"
|
||||
done
|
||||
|
||||
elif [ "${{ matrix.platform }}" == "macos" ]; then
|
||||
cp "$SRC_DIR"/mac/*.dmg "$DEST_DIR/SlimeVR-mac.dmg"
|
||||
fi
|
||||
|
||||
echo "Collected files:"
|
||||
ls -lh "$DEST_DIR"
|
||||
- name: Upload For Release
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-${{ matrix.platform }}-${{ matrix.os }}
|
||||
path: release-out/*
|
||||
|
||||
bundle-android:
|
||||
name: Build Android APK
|
||||
needs: [build-gui-frontend]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
- name: Download GUI Dist
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: gui-dist
|
||||
- name: Extract GUI for Android
|
||||
run: mkdir -p gui/out && tar -xzf slimevr-gui-dist.tar.gz -C gui/out
|
||||
- name: Build APK
|
||||
run: ./gradlew :server:android:build --build-cache
|
||||
env:
|
||||
ANDROID_STORE_FILE: ${{ secrets.ANDROID_STORE_FILE }}
|
||||
ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_STORE_PASSWD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_KEY_PASSWD }}
|
||||
- name: Test with Gradle
|
||||
run: ./gradlew test
|
||||
- name: Prepare APK
|
||||
run: cp server/android/build/outputs/apk/release/*.apk ./SlimeVR-android.apk
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-android
|
||||
path: SlimeVR-android.apk
|
||||
|
||||
create-release:
|
||||
name: Finalize Release Draft
|
||||
needs: [package-desktop, bundle-android, build-server-jar, build-gui-frontend]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download All Release Artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: release-*
|
||||
merge-multiple: true
|
||||
- name: Download Server Jar
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: server-jar
|
||||
- name: Download GUI Dist
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: gui-dist
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
release-out/*
|
||||
server/desktop/build/libs/slimevr.jar
|
||||
slimevr-gui-dist.tar.gz
|
||||
413
.github/workflows/gradle.yaml
vendored
@@ -1,413 +0,0 @@
|
||||
# This workflow will build a Java project with Gradle
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
|
||||
|
||||
name: SlimeVR Server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
create:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin --recurse-submodules=no --force
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- run: mkdir ./gui/dist && touch ./gui/dist/somefile
|
||||
shell: bash
|
||||
|
||||
- name: Check code formatting
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
- name: Test with Gradle
|
||||
run: ./gradlew test
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin --recurse-submodules=no --force
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew :server:desktop:shadowJar
|
||||
|
||||
- name: Upload the Server JAR as a Build Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
# Artifact name
|
||||
name: 'SlimeVR-Server' # optional, default is artifact
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: server/desktop/build/libs/slimevr.jar
|
||||
|
||||
- name: Upload to draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
server/desktop/build/libs/slimevr.jar
|
||||
|
||||
bundle-android:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin --recurse-submodules=no --force
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Build GUI
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: cd gui && pnpm run build
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew :server:android:build
|
||||
env:
|
||||
ANDROID_STORE_FILE: ${{ secrets.ANDROID_STORE_FILE }}
|
||||
ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_STORE_PASSWD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_KEY_PASSWD }}
|
||||
|
||||
- name: Upload the Android build artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
# Artifact name
|
||||
name: 'SlimeVR-Android' # optional, default is artifact
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: server/android/build/outputs/apk/*
|
||||
|
||||
- name: Prepare for release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
cp server/android/build/outputs/apk/release/android-release.apk ./SlimeVR-android.apk
|
||||
|
||||
- name: Upload to draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
./SlimeVR-android.apk
|
||||
|
||||
- name: Build Google Play release bundle
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: ./gradlew :server:android:bundleRelease
|
||||
env:
|
||||
ANDROID_STORE_FILE: ${{ secrets.ANDROID_GPLAY_STORE_FILE }}
|
||||
ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_GPLAY_STORE_PASSWD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_GPLAY_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_GPLAY_KEY_PASSWD }}
|
||||
|
||||
- name: Upload the Google Play artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
# Artifact name
|
||||
name: 'SlimeVR-Android-GPDev' # optional, default is artifact
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: server/android/build/outputs/bundle/release/*
|
||||
|
||||
bundle-linux:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [build, test]
|
||||
if: contains(fromJSON('["workflow_dispatch", "create"]'), github.event_name)
|
||||
|
||||
env:
|
||||
BUILD_ARCH: ${{ endsWith(matrix.os, 'arm') && 'aarch64' || 'amd64' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: 'SlimeVR-Server'
|
||||
path: server/desktop/build/libs/
|
||||
|
||||
- name: Set up Linux dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
|
||||
with:
|
||||
packages: |
|
||||
build-essential curl wget file libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev xdg-utils
|
||||
# Increment to invalidate the cache
|
||||
version: ${{ format('v1.0-{0}', env.BUILD_ARCH) }}
|
||||
# Enables a workaround to attempt to run pre and post install scripts
|
||||
execute_install_scripts: true
|
||||
# Disables uploading logs as a build artifact
|
||||
debug: false
|
||||
|
||||
- name: Set up specific Linux versioned dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-0=2.44.0-2 \
|
||||
libwebkit2gtk-4.1-dev=2.44.0-2 \
|
||||
libjavascriptcoregtk-4.1-0=2.44.0-2 \
|
||||
libjavascriptcoregtk-4.1-dev=2.44.0-2 \
|
||||
gir1.2-javascriptcoregtk-4.1=2.44.0-2 \
|
||||
gir1.2-webkit2-4.1=2.44.0-2;
|
||||
|
||||
- name: Cache cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: pnpm run tauri build --config $( ./gui/scripts/gitversion.mjs )
|
||||
|
||||
- name: Make GUI tarball
|
||||
run: |
|
||||
tar czf slimevr-gui-dist.tar.gz -C gui/dist/ .
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
name: SlimeVR-GUI-Dist
|
||||
path: ./slimevr-gui-dist.tar.gz
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ format('SlimeVR-GUI-Deb-{0}', env.BUILD_ARCH) }}
|
||||
path: target/release/bundle/deb/slimevr*.deb
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ format('SlimeVR-GUI-AppImage-{0}', env.BUILD_ARCH) }}
|
||||
path: target/release/bundle/appimage/slimevr*.AppImage
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ format('SlimeVR-GUI-RPM-{0}', env.BUILD_ARCH) }}
|
||||
path: target/release/bundle/rpm/slimevr*.rpm
|
||||
|
||||
- name: Prepare for release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
cp target/release/bundle/appimage/slimevr*.AppImage "./SlimeVR-$BUILD_ARCH.appimage"
|
||||
cp target/release/bundle/deb/slimevr*.deb "./SlimeVR-$BUILD_ARCH.deb"
|
||||
cp target/release/bundle/rpm/slimevr*.rpm "./SlimeVR-$BUILD_ARCH.rpm"
|
||||
|
||||
- name: Upload to draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
./slimevr-gui-dist.tar.gz
|
||||
./SlimeVR-*.appimage
|
||||
./SlimeVR-*.deb
|
||||
./SlimeVR-*.rpm
|
||||
|
||||
bundle-mac:
|
||||
runs-on: macos-latest
|
||||
needs: [build, test]
|
||||
if: contains(fromJSON('["workflow_dispatch", "create"]'), github.event_name)
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: 'SlimeVR-Server'
|
||||
path: server/desktop/build/libs/
|
||||
|
||||
- name: Cache cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
pnpm i
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
run: pnpm run tauri build --target universal-apple-darwin --config $( ./gui/scripts/gitversion.mjs )
|
||||
|
||||
- name: Modify Application
|
||||
run: |
|
||||
cd target/universal-apple-darwin/release/bundle/macos/slimevr.app/Contents/MacOS
|
||||
cp $( git rev-parse --show-toplevel )/server/desktop/build/libs/slimevr.jar ./
|
||||
cd ../../../
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName SlimeVR" slimevr.app/Contents/Info.plist
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleName SlimeVR" slimevr.app/Contents/Info.plist
|
||||
codesign --sign - --deep --force slimevr.app
|
||||
mv slimevr.app SlimeVR.app
|
||||
cd ../dmg/
|
||||
./bundle_dmg.sh --volname SlimeVR --icon slimevr 180 170 --app-drop-link 480 170 \
|
||||
--window-size 660 400 --hide-extension ../macos/SlimeVR.app \
|
||||
--volicon ../macos/SlimeVR.app/Contents/Resources/icon.icns --skip-jenkins \
|
||||
--eula ../../../../../LICENSE-MIT slimevr.dmg ../macos/SlimeVR.app
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: SlimeVR-GUI-MacApp
|
||||
path: target/universal-apple-darwin/release/bundle/macos/SlimeVR*.app
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: SlimeVR-GUI-MacDmg
|
||||
path: target/universal-apple-darwin/release/bundle/dmg/slimevr.dmg
|
||||
|
||||
- name: Prepare for release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
cp target/universal-apple-darwin/release/bundle/dmg/slimevr.dmg ./SlimeVR-mac.dmg
|
||||
|
||||
- name: Upload to draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
./SlimeVR-mac.dmg
|
||||
|
||||
bundle-windows:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, windows-11-arm]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [build, test]
|
||||
if: contains(fromJSON('["workflow_dispatch", "create"]'), github.event_name)
|
||||
|
||||
env:
|
||||
BUILD_ARCH: ${{ endsWith(matrix.os, 'arm') && 'win-aarch64' || 'win64' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: 'SlimeVR-Server'
|
||||
path: server/desktop/build/libs/
|
||||
|
||||
- if: matrix.os == 'windows-11-arm'
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache: false
|
||||
|
||||
- name: Cache cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm i
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: pnpm run skipbundler --config $( ./gui/scripts/gitversion.mjs )
|
||||
|
||||
- name: Bundle to zips
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir SlimeVR
|
||||
cp gui/src-tauri/icons/icon.ico ./SlimeVR/run.ico
|
||||
cp server/desktop/build/libs/slimevr.jar ./SlimeVR/slimevr.jar
|
||||
cp server/core/resources/* ./SlimeVR/
|
||||
cp target/release/slimevr.exe ./SlimeVR/
|
||||
7z a -tzip "SlimeVR-$BUILD_ARCH.zip" ./SlimeVR/
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ format('SlimeVR-GUI-Windows-{0}', env.BUILD_ARCH) }}
|
||||
path: ./SlimeVR*.zip
|
||||
|
||||
- name: Upload to draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: ./SlimeVR-*.zip
|
||||
7
.gitignore
vendored
@@ -34,9 +34,6 @@
|
||||
# ignore gradle build folder
|
||||
build/
|
||||
|
||||
# Rust build artifacts
|
||||
/target
|
||||
|
||||
# direnv has been claimed for Nix usage
|
||||
.direnv/
|
||||
.devenv
|
||||
@@ -46,3 +43,7 @@ local.properties
|
||||
|
||||
# Ignore temporary config
|
||||
vrconfig.yml.tmp
|
||||
|
||||
|
||||
# Nixos
|
||||
.bin/
|
||||
|
||||
1
.vscode/extensions.json
vendored
@@ -7,7 +7,6 @@
|
||||
"gaborv.flatbuffers",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"rust-lang.rust-analyzer",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"EditorConfig.EditorConfig",
|
||||
"macabeus.vscode-fluent",
|
||||
|
||||
@@ -7,8 +7,6 @@ This document describes essential knowledge required to contribute to the SlimeV
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [Java v17+](https://adoptium.net/temurin/releases/)
|
||||
- [Node.js v16.9+](https://nodejs.org) (We recommend the use of `nvm` instead of installing Node.js directly)
|
||||
- [Microsoft Edge WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) or `webkit2gtk` for Linux
|
||||
- [Rust](https://rustup.rs)
|
||||
|
||||
## Cloning the code
|
||||
First, clone the codebase using git in a terminal in the folder you want.
|
||||
@@ -32,13 +30,13 @@ be at `server/build/libs/slimevr.jar` (you can ignore `server.jar`).
|
||||
|
||||
(Note: Your IDE may be able to do all of the above for you.)
|
||||
|
||||
### Tauri (gui)
|
||||
### Electron (gui)
|
||||
|
||||
- Activate corepack (included with Node.JS) via `corepack enable` (might require administrator permissions)
|
||||
- Run `pnpm i` in your IDE's terminal to download and install dependencies.
|
||||
- To launch the GUI in dev mode, run `pnpm gui`.
|
||||
- Finally, to compile for production, run `pnpm run tauri build`. The result
|
||||
will be at `target/release/slimevr.exe`.
|
||||
- Finally, to compile for production, run `pnpm package:build`. The result
|
||||
will be at `dist/artifacts/` content will change depending of the platform.
|
||||
|
||||
## Code style
|
||||
|
||||
@@ -84,7 +82,7 @@ Import the formatting settings defined in `spotless.xml`, like this:
|
||||
Eclipse will only do a subset of the checks in `spotless`, so you may still want to do
|
||||
`./gradlew spotlessApply` if you ever see an error from spotless.
|
||||
|
||||
### Tauri (gui)
|
||||
### Electron (gui)
|
||||
|
||||
We use ESLint and Prettier to format GUI code.
|
||||
- First, go into the GUI's directory with your terminal by running `cd gui`.
|
||||
|
||||
6898
Cargo.lock
generated
16
Cargo.toml
@@ -1,16 +0,0 @@
|
||||
[workspace]
|
||||
# Use 2021 edition resolver, better resolves crate features.
|
||||
resolver = "2"
|
||||
|
||||
# A list of all rust crates in the workspace.
|
||||
members = ["gui/src-tauri"]
|
||||
|
||||
# These settings can be inherited by workspace members
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.82" # Tauri's MSRV
|
||||
repository = "https://github.com/SlimeVR/SlimeVR-Server"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
242
deny.toml
@@ -1,242 +0,0 @@
|
||||
# This template contains all of the possible sections and their default values
|
||||
|
||||
# Note that all fields that take a lint level have these possible values:
|
||||
# * deny - An error will be produced and the check will fail
|
||||
# * warn - A warning will be produced, but the check will not fail
|
||||
# * allow - No warning or error will be produced, though in some cases a note
|
||||
# will be
|
||||
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
# dependency, such as, for example, the `nix` crate only being used via the
|
||||
# `target_family = "unix"` configuration, that only having windows targets in
|
||||
# this list would mean the nix crate, as well as any of its exclusive
|
||||
# dependencies not shared by any other crates, would be ignored, as the target
|
||||
# list here is effectively saying which targets you are building for.
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"MIT-0",
|
||||
"ISC",
|
||||
"BSD-3-Clause",
|
||||
"Zlib",
|
||||
"MPL-2.0",
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.8
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
github = [""]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
gitlab = [""]
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
bitbucket = [""]
|
||||
39
flake.lock
generated
@@ -1,26 +1,5 @@
|
||||
{
|
||||
"nodes": {
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756795219,
|
||||
"narHash": "sha256-tKBQtz1JLKWrCJUxVkHKR+YKmVpm0KZdJdPWmR2slQ8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "80dbdab137f2809e3c823ed027e1665ce2502d74",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
@@ -72,27 +51,9 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"fenix": "fenix",
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1756597274,
|
||||
"narHash": "sha256-wfaKRKsEVQDB7pQtAt04vRgFphkVscGRpSx3wG1l50E=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "21614ed2d3279a9aa1f15c88d293e65a98991b30",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
86
flake.nix
@@ -1,74 +1,50 @@
|
||||
{
|
||||
description = "Affordable full-body tracking for VR!";
|
||||
description = "SlimeVR Server & GUI";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs@{ self, nixpkgs, flake-parts, fenix, ... }:
|
||||
outputs = inputs@{ self, nixpkgs, flake-parts, ... }:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||
systems = [ "x86_64-linux" ];
|
||||
|
||||
perSystem = { system, lib, ... }:
|
||||
perSystem = { pkgs, ... }:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
runtimeLibs = pkgs: (with pkgs; [
|
||||
jdk17
|
||||
|
||||
rust_toolchain = lib.importTOML ./rust-toolchain.toml;
|
||||
fenixPkgs = fenix.packages.${system};
|
||||
alsa-lib at-spi2-atk at-spi2-core cairo cups dbus expat
|
||||
gdk-pixbuf glib gtk3 libdrm libgbm libglvnd libnotify
|
||||
libxkbcommon mesa nspr nss pango systemd vulkan-loader
|
||||
wayland xorg.libX11 xorg.libXcomposite xorg.libXdamage
|
||||
xorg.libXext xorg.libXfixes xorg.libXrandr xorg.libxcb
|
||||
xorg.libxshmfence libusb1 udev libxcrypt-legacy
|
||||
rpm fpm
|
||||
|
||||
rustToolchainSet = fenixPkgs.fromToolchainName {
|
||||
name = rust_toolchain.toolchain.channel;
|
||||
sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE=";
|
||||
};
|
||||
in {
|
||||
wine
|
||||
zlib squashfsTools fakeroot libarchive icu
|
||||
nodejs_22 pnpm pkg-config python3 gcc gnumake binutils git
|
||||
pkgs.nodePackages.node-gyp-build
|
||||
]);
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
name = "slimevr";
|
||||
slimeShell = pkgs.buildFHSEnv {
|
||||
name = "slimevr-env";
|
||||
targetPkgs = runtimeLibs;
|
||||
profile = ''
|
||||
export JAVA_HOME=${pkgs.jdk17}
|
||||
export PATH="${pkgs.jdk17}/bin:$PATH"
|
||||
|
||||
buildInputs =
|
||||
(with pkgs; [
|
||||
cacert
|
||||
]) ++ lib.optionals pkgs.stdenv.isLinux (with pkgs; [
|
||||
atk cairo dbus dbus.lib dprint gdk-pixbuf glib.out glib-networking
|
||||
gobject-introspection gtk3 harfbuzz libffi libsoup_3 openssl.dev pango
|
||||
pkg-config treefmt webkitgtk_4_1 zlib
|
||||
gst_all_1.gstreamer gst_all_1.gst-plugins-base
|
||||
gst_all_1.gst-plugins-good gst_all_1.gst-plugins-bad
|
||||
librsvg freetype expat libayatana-appindicator udev libusb1
|
||||
]) ++ lib.optionals pkgs.stdenv.isDarwin [
|
||||
pkgs.darwin.apple_sdk.frameworks.Security
|
||||
] ++ [
|
||||
pkgs.jdk17
|
||||
pkgs.kotlin
|
||||
rustToolchainSet.rustc
|
||||
rustToolchainSet.cargo
|
||||
rustToolchainSet.rustfmt
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [ pnpm nodejs_22 gradle ];
|
||||
|
||||
RUST_BACKTRACE = 1;
|
||||
GIO_EXTRA_MODULES = "${pkgs.glib-networking}/lib/gio/modules:${pkgs.dconf.lib}/lib/gio/modules";
|
||||
|
||||
shellHook = ''
|
||||
export SLIMEVR_RUST_LD_LIBRARY_PATH="$LD_LIBRARY_PATH"
|
||||
export LD_LIBRARY_PATH="${pkgs.udev}/lib:${pkgs.libayatana-appindicator}/lib:$LD_LIBRARY_PATH"
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0="${pkgs.gst_all_1.gstreamer.out}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-base}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-good}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-bad}/lib/gstreamer-1.0"
|
||||
|
||||
# Force linker and pkg-config to use udev from nixpkgs so libgudev/hidapi
|
||||
# resolve against the correct libudev implementation at link time.
|
||||
export PKG_CONFIG_PATH="${pkgs.udev}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export LIBRARY_PATH="${pkgs.udev}/lib:$LIBRARY_PATH"
|
||||
export LD_RUN_PATH="${pkgs.udev}/lib:$LD_RUN_PATH"
|
||||
export NIX_LDFLAGS="-L${pkgs.udev}/lib -ludev $NIX_LDFLAGS"
|
||||
export LDFLAGS="-L${pkgs.udev}/lib -Wl,-rpath,${pkgs.udev}/lib -ludev $LDFLAGS"
|
||||
# Tell electron-builder to use system tools instead of downloading them
|
||||
export USE_SYSTEM_FPM=true
|
||||
export USE_SYSTEM_MKSQUASHFS=true
|
||||
'';
|
||||
runScript = "bash";
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = slimeShell.env;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
4
gui/.gitignore
vendored
@@ -29,9 +29,13 @@ yarn-error.log*
|
||||
/dist
|
||||
/stats.html
|
||||
vite.config.ts.timestamp*
|
||||
electron.vite.config.*.mjs
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
# electron
|
||||
out/
|
||||
|
||||
64
gui/electron-builder.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
appId: dev.slimevr.SlimeVR
|
||||
productName: SlimeVR
|
||||
# Global naming pattern
|
||||
artifactName: "${productName}-${version}-${os}-${arch}.${ext}"
|
||||
|
||||
directories:
|
||||
output: dist/artifacts/${os}
|
||||
|
||||
asar: true
|
||||
asarUnpack:
|
||||
- out/main/chunks/*.jar
|
||||
|
||||
electronLanguages:
|
||||
- en-US
|
||||
|
||||
files:
|
||||
- out/**/*
|
||||
- index.html
|
||||
- package.json
|
||||
- node_modules/**
|
||||
- "!node_modules/*/{README,readme,README.md,readme.md,CHANGELOG,CHANGELOG.md,changelog.md}"
|
||||
- "!node_modules/*/{test,tests,__tests__,docs,doc,example,examples}"
|
||||
- "!node_modules/*/.{git,github,vscode,editorconfig,eslintrc,prettierrc}"
|
||||
- "!node_modules/**/*.{map,ts,tsx,d.ts}"
|
||||
- "!**/.DS_Store"
|
||||
|
||||
|
||||
linux:
|
||||
category: Game
|
||||
artifactName: "${productName}-${arch}.${ext}"
|
||||
target:
|
||||
- target: AppImage
|
||||
- target: deb
|
||||
- target: rpm
|
||||
extraFiles:
|
||||
- from: "../server/desktop/build/libs/slimevr.jar"
|
||||
to: "."
|
||||
- from: "./electron/resources/69-slimevr-devices.rules"
|
||||
to: "."
|
||||
icon: "./electron/resources/icons"
|
||||
|
||||
deb:
|
||||
depends: [openjdk-17-jre-headless, udev]
|
||||
afterInstall: "./electron/resources/scripts/postinstall.sh"
|
||||
afterRemove: "./electron/resources/scripts/postremove.sh"
|
||||
|
||||
rpm:
|
||||
depends: [java-latest-openjdk, udev]
|
||||
afterInstall: "./electron/resources/scripts/postinstall.sh"
|
||||
afterRemove: "./electron/resources/scripts/postremove.sh"
|
||||
|
||||
win:
|
||||
artifactName: "${productName}-${os}-${arch}.${ext}"
|
||||
target: [target: portable]
|
||||
icon: "./electron/resources/icons/icon.ico"
|
||||
|
||||
mac:
|
||||
target: dmg
|
||||
artifactName: "SlimeVR-mac.${ext}"
|
||||
x64ArchFiles: "**/register-protocol-handler.node"
|
||||
icon: "./electron/resources/icons/icon.icns"
|
||||
extraFiles:
|
||||
- from: "../server/desktop/build/libs/slimevr.jar"
|
||||
to: "."
|
||||
47
gui/electron.vite.config.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { defineConfig } from 'electron-vite'
|
||||
import { resolve } from 'path'
|
||||
import rendererConfig from './vite.config' // Import your existing React config
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: resolve(__dirname, 'electron/main/index.ts'),
|
||||
external: [
|
||||
'pino',
|
||||
'pino-pretty',
|
||||
'pino-roll',
|
||||
'commander',
|
||||
'open'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
preload: {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: resolve(__dirname, 'electron/preload/index.ts'),
|
||||
output: {
|
||||
format: 'cjs', // Force CJS for the preload
|
||||
entryFileNames: 'index.js' // Change back to .js
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer: {
|
||||
...rendererConfig,
|
||||
root: '.',
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
// Force Rollup to treat the protocol directory as CommonJS
|
||||
// even though it's not in node_modules
|
||||
include: [/solarxr-protocol/, /node_modules/],
|
||||
// Required for Flatbuffers/Generated code interop
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
input: resolve(__dirname, 'index.html')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
1
gui/electron/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
resources/java-version/JavaVersion.class
|
||||
12
gui/electron/main/cli.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { program } from "commander";
|
||||
|
||||
program
|
||||
.option('-p --path <path>', 'set launch path')
|
||||
.option(
|
||||
'--skip-server-if-running',
|
||||
'gui will not launch the server if it is already running'
|
||||
)
|
||||
.allowUnknownOption();
|
||||
|
||||
program.parse(process.argv);
|
||||
export const options = program.opts();
|
||||
444
gui/electron/main/index.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
Menu,
|
||||
nativeImage,
|
||||
net,
|
||||
protocol,
|
||||
screen,
|
||||
shell,
|
||||
Tray,
|
||||
} from 'electron';
|
||||
import { IPC_CHANNELS } from '../shared';
|
||||
import path, { dirname, join } from 'path';
|
||||
import open from 'open';
|
||||
import trayIcon from '../resources/icons/icon.png?asset';
|
||||
import appleTrayIcon from '../resources/icons/appleTrayIcon.png?asset';
|
||||
import { readFile, stat } from 'fs/promises';
|
||||
import { getPlatform, handleIpc, isPortAvailable } from './utils';
|
||||
import {
|
||||
findServerJar,
|
||||
findSystemJRE,
|
||||
getGuiDataFolder,
|
||||
getLogsFolder,
|
||||
getServerDataFolder,
|
||||
getWindowStateFile,
|
||||
} from './paths';
|
||||
import { stores } from './store';
|
||||
import { logger } from './logger';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { discordPresence } from './presence';
|
||||
import { options } from './cli';
|
||||
import { ServerStatusEvent } from 'electron/preload/interface';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { MenuItem } from 'electron/main';
|
||||
|
||||
// Register custom protocol to handle asset paths with leading slashes
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: 'app',
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
handleIpc(IPC_CHANNELS.GH_FETCH, async (e, options) => {
|
||||
if (options.type === 'fw-releases') {
|
||||
return fetch(
|
||||
'https://api.github.com/repos/SlimeVR/SlimeVR-Tracker-ESP/releases'
|
||||
).then((res) => res.json());
|
||||
}
|
||||
if (options.type === 'asset') {
|
||||
if (
|
||||
!options.url.startsWith(
|
||||
'https://github.com/SlimeVR/SlimeVR-Tracker-ESP/releases/download'
|
||||
)
|
||||
)
|
||||
return null;
|
||||
return fetch(options.url).then((res) => res.json());
|
||||
}
|
||||
});
|
||||
|
||||
handleIpc(IPC_CHANNELS.OS_STATS, async () => {
|
||||
return {
|
||||
type: getPlatform(),
|
||||
};
|
||||
});
|
||||
|
||||
handleIpc(IPC_CHANNELS.I18N_OVERRIDE, async () => {
|
||||
const overridefile = join(getServerDataFolder(), 'override.ftl');
|
||||
const exists = await stat(overridefile)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!exists) return false;
|
||||
return readFile(overridefile, { encoding: 'utf-8' });
|
||||
});
|
||||
|
||||
handleIpc(IPC_CHANNELS.LOG, (e, type, ...args) => {
|
||||
let payload: Record<string, unknown> = {};
|
||||
const messageParts: unknown[] = [];
|
||||
|
||||
args.forEach((arg) => {
|
||||
if (arg instanceof Error) {
|
||||
payload.err = arg;
|
||||
} else if (typeof arg === 'object' && arg !== null) {
|
||||
payload = { ...payload, ...arg };
|
||||
} else {
|
||||
messageParts.push(arg);
|
||||
}
|
||||
});
|
||||
|
||||
const msg = messageParts.join(' ');
|
||||
|
||||
switch (type) {
|
||||
case 'error':
|
||||
logger.error(payload, msg);
|
||||
break;
|
||||
case 'warn':
|
||||
logger.warn(payload, msg);
|
||||
break;
|
||||
default:
|
||||
logger.info(payload, msg);
|
||||
}
|
||||
});
|
||||
|
||||
handleIpc(IPC_CHANNELS.OPEN_URL, (e, url) => {
|
||||
const allowed_urls = [
|
||||
/steam:\/\/.*/,
|
||||
/ms-settings:network$/,
|
||||
/https:\/\/.*\.slimevr\.dev.*/,
|
||||
/https:\/\/github\.com\/.*/,
|
||||
/https:\/\/discord\.gg\/slimevr$/,
|
||||
];
|
||||
if (allowed_urls.find((a) => url.match(a))) open(url);
|
||||
else logger.error({ url }, 'attempted to open non-whitelisted URL');
|
||||
});
|
||||
|
||||
handleIpc(IPC_CHANNELS.STORAGE, async (e, { type, method, key, value }) => {
|
||||
const store = stores[type];
|
||||
if (!store) throw new Error(`Storage type ${type} not found`);
|
||||
|
||||
switch (method) {
|
||||
case 'get':
|
||||
return store.get(key!);
|
||||
case 'set':
|
||||
return store.set(key!, value);
|
||||
case 'delete':
|
||||
return store.delete(key!);
|
||||
case 'save':
|
||||
return store.save();
|
||||
}
|
||||
});
|
||||
|
||||
handleIpc(IPC_CHANNELS.DISCORD_PRESENCE, async (e, options) => {
|
||||
if (options.enable && !discordPresence.state.ready) {
|
||||
await discordPresence.connect();
|
||||
discordPresence.updateActivity(options.activity);
|
||||
} else if (!options.enable && discordPresence.state.ready) {
|
||||
discordPresence.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
handleIpc(IPC_CHANNELS.OPEN_FILE, (e, folder) => {
|
||||
const requestedPath = path.resolve(folder);
|
||||
|
||||
const isAllowed = [getServerDataFolder(), getGuiDataFolder(), getLogsFolder()].some(
|
||||
(parent) => {
|
||||
const absoluteParent = path.resolve(parent);
|
||||
const relative = path.relative(absoluteParent, requestedPath);
|
||||
return !relative.includes('..') && !path.isAbsolute(relative);
|
||||
}
|
||||
);
|
||||
|
||||
if (isAllowed) {
|
||||
shell.openPath(requestedPath);
|
||||
} else {
|
||||
logger.error({ path: requestedPath }, 'Blocked unauthorized path');
|
||||
}
|
||||
});
|
||||
|
||||
handleIpc(IPC_CHANNELS.GET_FOLDER, (e, folder) => {
|
||||
switch (folder) {
|
||||
case 'config':
|
||||
return getGuiDataFolder();
|
||||
case 'logs':
|
||||
return getLogsFolder();
|
||||
}
|
||||
});
|
||||
|
||||
const windowStateFile = await readFile(getWindowStateFile(), {
|
||||
encoding: 'utf-8',
|
||||
}).catch(() => null);
|
||||
|
||||
const defaultWindowState: {
|
||||
width: number;
|
||||
height: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
} = {
|
||||
width: 1289.0,
|
||||
height: 709.0,
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
};
|
||||
const windowState = windowStateFile ? JSON.parse(windowStateFile) : defaultWindowState;
|
||||
|
||||
const MIN_WIDTH = 393;
|
||||
const MIN_HEIGHT = 667;
|
||||
|
||||
function validateWindowState(state: typeof defaultWindowState) {
|
||||
if (state.x === undefined || state.y === undefined) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const displays = screen.getAllDisplays();
|
||||
|
||||
const isVisible = displays.some((display) => {
|
||||
return (
|
||||
state.x! >= display.bounds.x &&
|
||||
state.y! >= display.bounds.y &&
|
||||
state.x! + state.width <= display.bounds.x + display.bounds.width &&
|
||||
state.y! + state.height <= display.bounds.y + display.bounds.height
|
||||
);
|
||||
});
|
||||
|
||||
const minWidth = MIN_WIDTH;
|
||||
const minHeight = MIN_HEIGHT;
|
||||
|
||||
if (!isVisible || state.width < minWidth || state.height < minHeight) {
|
||||
return defaultWindowState;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
const saveWindowState = async () => {
|
||||
await mkdir(dirname(getWindowStateFile()), { recursive: true });
|
||||
writeFileSync(getWindowStateFile(), JSON.stringify(windowState));
|
||||
};
|
||||
|
||||
function createWindow() {
|
||||
const validatedState = validateWindowState(windowState);
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: validatedState.width,
|
||||
height: validatedState.height,
|
||||
x: validatedState.x,
|
||||
y: validatedState.y,
|
||||
minHeight: MIN_HEIGHT,
|
||||
minWidth: MIN_WIDTH,
|
||||
movable: true,
|
||||
frame: false,
|
||||
roundedCorners: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
devTools: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
mainWindow.loadURL('app://./index.html');
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
handleIpc('window-actions', (e, action) => {
|
||||
switch (action) {
|
||||
case 'close':
|
||||
mainWindow?.close();
|
||||
break;
|
||||
case 'minimize':
|
||||
mainWindow?.minimize();
|
||||
break;
|
||||
case 'maximize':
|
||||
mainWindow?.maximize();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
handleIpc('open-dialog', (e, options) => dialog.showOpenDialog(options));
|
||||
handleIpc('save-dialog', (e, options) => dialog.showSaveDialog(options));
|
||||
|
||||
const icon = nativeImage.createFromPath(
|
||||
getPlatform() === 'macos' ? appleTrayIcon : trayIcon
|
||||
);
|
||||
const tray = new Tray(icon);
|
||||
tray.setToolTip('SlimeVR');
|
||||
tray.on('click', () => {
|
||||
mainWindow?.show();
|
||||
});
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show',
|
||||
click: () => {
|
||||
mainWindow?.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide',
|
||||
click: () => {
|
||||
mainWindow?.hide();
|
||||
},
|
||||
},
|
||||
{ role: 'quit' },
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
const updateWindowState = () => {
|
||||
if (!mainWindow) return;
|
||||
|
||||
windowState.minimized = mainWindow.isMinimized();
|
||||
if (!mainWindow.isMinimized() && !mainWindow.isMaximized()) {
|
||||
const bounds = mainWindow.getBounds();
|
||||
windowState.width = bounds.width;
|
||||
windowState.height = bounds.height;
|
||||
windowState.x = bounds.x;
|
||||
windowState.y = bounds.y;
|
||||
}
|
||||
};
|
||||
|
||||
mainWindow.on('move', updateWindowState);
|
||||
mainWindow.on('resize', updateWindowState);
|
||||
mainWindow.on('minimize', updateWindowState);
|
||||
mainWindow.on('maximize', updateWindowState);
|
||||
|
||||
mainWindow.webContents.on('context-menu', (event, params) => {
|
||||
const menu = new Menu();
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: 'Inspect Element',
|
||||
click: () => {
|
||||
mainWindow?.webContents.inspectElement(params.x, params.y);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
menu.append(new MenuItem({ label: 'Copy', role: 'copy' }));
|
||||
menu.append(new MenuItem({ label: 'Paste', role: 'paste' }));
|
||||
|
||||
if (mainWindow)
|
||||
menu.popup({ window: mainWindow });
|
||||
});
|
||||
}
|
||||
|
||||
const checkEnvironmentVariables = () => {
|
||||
const to_check = ['_JAVA_OPTIONS', 'JAVA_TOOL_OPTIONS'];
|
||||
|
||||
const set = to_check.filter((env) => !!process.env[env]);
|
||||
if (set.length > 0) {
|
||||
dialog.showErrorBox(
|
||||
'SlimeVR',
|
||||
`You have environment variables ${set.join(', ')} set, which may cause the SlimeVR Server to fail to launch properly.`
|
||||
);
|
||||
app.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
const isServerRunning = async () => !(await isPortAvailable(21110));
|
||||
|
||||
const spawnServer = async () => {
|
||||
if (options.skipServerIfRunning && (await isServerRunning())) {
|
||||
logger.info(
|
||||
{ skipServerIfRunning: options.skipServerIfRunning },
|
||||
'Server is already running, skipping server start'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const serverJar = findServerJar();
|
||||
if (!serverJar) {
|
||||
logger.info('server jar not found, skipping');
|
||||
return;
|
||||
}
|
||||
const sharedDir = dirname(serverJar);
|
||||
const javaBin = await findSystemJRE(sharedDir);
|
||||
if (!javaBin) {
|
||||
dialog.showErrorBox(
|
||||
'SlimeVR',
|
||||
`Couldn't find a compatible Java version, please download Java 17 or higher`
|
||||
);
|
||||
app.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({ javaBin, serverJar }, 'Found Java and server jar');
|
||||
|
||||
const process = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run']);
|
||||
|
||||
process.stdout?.on('data', (message) => {
|
||||
mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, {
|
||||
message: message.toString(),
|
||||
type: 'stdout',
|
||||
} satisfies ServerStatusEvent);
|
||||
});
|
||||
|
||||
process.stderr?.on('data', (message) => {
|
||||
mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, {
|
||||
message: message.toString(),
|
||||
type: 'stderr',
|
||||
} satisfies ServerStatusEvent);
|
||||
});
|
||||
|
||||
return {
|
||||
process: process,
|
||||
close: () => {
|
||||
process.kill('SIGTERM');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Register protocol handler for app:// scheme to handle assets with leading slashes
|
||||
protocol.handle('app', (request) => {
|
||||
const url = request.url.slice('app://'.length);
|
||||
const filePath = path.normalize(join(__dirname, '../renderer', url));
|
||||
return net.fetch('file://' + filePath);
|
||||
});
|
||||
|
||||
checkEnvironmentVariables();
|
||||
const server = await spawnServer();
|
||||
|
||||
createWindow();
|
||||
|
||||
logger.info('SlimeVR started!');
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
process.on('exit', () => {
|
||||
server?.close();
|
||||
});
|
||||
|
||||
app.on('before-quit', async () => {
|
||||
logger.info('App quitting, saving...');
|
||||
server?.close();
|
||||
stores.settings.save();
|
||||
stores.cache.save();
|
||||
|
||||
discordPresence.destroy();
|
||||
|
||||
await saveWindowState();
|
||||
});
|
||||
});
|
||||
26
gui/electron/main/logger.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import pino from 'pino';
|
||||
import { getLogsFolder } from './paths';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const transport = pino.transport({
|
||||
targets: [
|
||||
{
|
||||
target: 'pino-roll',
|
||||
options: {
|
||||
file: join(getLogsFolder(), 'slimevr-gui.log'),
|
||||
frequency: 'daily',
|
||||
size: '10m',
|
||||
mkdir: true,
|
||||
limit: { count: 7 },
|
||||
},
|
||||
level: 'info',
|
||||
},
|
||||
{
|
||||
target: 'pino-pretty',
|
||||
options: { colorize: true },
|
||||
level: 'debug',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const logger = pino(transport);
|
||||
120
gui/electron/main/paths.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { app } from 'electron';
|
||||
import path, { join } from 'node:path';
|
||||
import { getPlatform } from './utils';
|
||||
import { glob } from 'glob';
|
||||
import { spawn } from 'node:child_process';
|
||||
import javaVersionJar from '../resources/java-version/JavaVersion.jar?asset&asarUnpack';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { options } from './cli';
|
||||
|
||||
const javaBin = getPlatform() === 'windows' ? 'java.exe' : 'java';
|
||||
export const CONFIG_IDENTIFIER = 'dev.slimevr.SlimeVR';
|
||||
|
||||
export const getGuiDataFolder = () => {
|
||||
const platform = getPlatform();
|
||||
|
||||
switch (platform) {
|
||||
case 'linux':
|
||||
if (process.env['XDG_DATA_HOME'])
|
||||
return join(process.env['XDG_DATA_HOME'], CONFIG_IDENTIFIER);
|
||||
return join(app.getPath('home'), '.local/share', CONFIG_IDENTIFIER);
|
||||
case 'windows':
|
||||
return join(app.getPath('appData'), CONFIG_IDENTIFIER);
|
||||
case 'macos':
|
||||
return join(
|
||||
app.getPath('home'),
|
||||
'Library/Application Support',
|
||||
CONFIG_IDENTIFIER
|
||||
);
|
||||
case 'unknown':
|
||||
throw 'error';
|
||||
}
|
||||
};
|
||||
|
||||
export const getServerDataFolder = () => {
|
||||
const platform = getPlatform();
|
||||
|
||||
switch (platform) {
|
||||
case 'linux':
|
||||
case 'windows':
|
||||
case 'macos':
|
||||
return join(app.getPath('appData'), CONFIG_IDENTIFIER);
|
||||
case 'unknown':
|
||||
throw 'error';
|
||||
}
|
||||
};
|
||||
|
||||
export const getLogsFolder = () => {
|
||||
return join(getGuiDataFolder(), 'logs');
|
||||
};
|
||||
|
||||
export const getWindowStateFile = () =>
|
||||
join(getServerDataFolder(), '.window-state.json');
|
||||
|
||||
const localJavaBin = (sharedDir: string) => {
|
||||
const jre = join(sharedDir, 'jre/bin', javaBin);
|
||||
return jre;
|
||||
};
|
||||
|
||||
const javaHomeBin = () => {
|
||||
const javaHome = process.env['JAVA_HOME'];
|
||||
if (!javaHome) return null;
|
||||
const javaHomeJre = join(javaHome, 'bin', javaBin);
|
||||
return javaHomeJre;
|
||||
};
|
||||
|
||||
export const findSystemJRE = async (sharedDir: string) => {
|
||||
const paths = [
|
||||
localJavaBin(sharedDir),
|
||||
javaHomeBin(),
|
||||
...(await glob('/usr/lib/jvm/*/bin/' + javaBin)),
|
||||
...(await glob('/Library/Java/JavaVirtualMachines/*/Contents/Home/bin/' + javaBin)),
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
if (!path) continue;
|
||||
|
||||
const version = await new Promise<number | null>((resolve) => {
|
||||
const process = spawn(path, ['-jar', javaVersionJar], {});
|
||||
|
||||
let version: number | null = null;
|
||||
|
||||
process.stdout?.once('data', (data) => {
|
||||
try {
|
||||
version = parseFloat(data.toString());
|
||||
} catch {
|
||||
version = null;
|
||||
}
|
||||
});
|
||||
|
||||
process.on('error', () => {
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
process.on('exit', () => {
|
||||
resolve(version);
|
||||
});
|
||||
});
|
||||
if (version && version >= 17) return path;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const findServerJar = () => {
|
||||
const paths = [
|
||||
options.path ? path.resolve(options.path) : undefined,
|
||||
// AppImage passes the fakeroot in `APPDIR` env var.
|
||||
process.env['APPDIR']
|
||||
? path.resolve(join(process.env['APPDIR'], 'usr/share/slimevr/'))
|
||||
: undefined,
|
||||
path.dirname(app.getPath('exe')),
|
||||
|
||||
// For flatpack container
|
||||
path.resolve('/app/share/slimevr/'),
|
||||
path.resolve('/usr/share/slimevr/'),
|
||||
];
|
||||
return paths
|
||||
.filter((p) => !!p)
|
||||
.map((p) => join(p!, 'slimevr.jar'))
|
||||
.find((p) => existsSync(p));
|
||||
};
|
||||
49
gui/electron/main/presence.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Client } from '@xhayper/discord-rpc';
|
||||
import { logger } from './logger';
|
||||
|
||||
export const richPresence = () => {
|
||||
const initialState = () => ({ ready: false, start: Date.now() });
|
||||
|
||||
const state = initialState();
|
||||
|
||||
const client = new Client({
|
||||
clientId: '1237970689009647639',
|
||||
transport: { type: 'ipc' },
|
||||
});
|
||||
client.on('ready', () => {
|
||||
state.ready = true;
|
||||
});
|
||||
|
||||
client.on('disconnected', () => {
|
||||
state.ready = false;
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
connect: async () => {
|
||||
try {
|
||||
await client.login();
|
||||
} catch (e) {
|
||||
logger.error(e, 'unable to connect to discord rpc');
|
||||
}
|
||||
},
|
||||
updateActivity: (content: string) => {
|
||||
if (!state.ready) return;
|
||||
client.user
|
||||
?.setActivity({
|
||||
state: content,
|
||||
largeImageKey: 'icon',
|
||||
startTimestamp: state.start,
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error(e, 'unable to update rpc activity');
|
||||
});
|
||||
},
|
||||
destroy: () => {
|
||||
client.destroy();
|
||||
Object.assign(state, initialState());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const discordPresence = richPresence();
|
||||
76
gui/electron/main/store.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { logger } from "./logger";
|
||||
import { getGuiDataFolder } from "./paths";
|
||||
|
||||
|
||||
export class CustomStore {
|
||||
private data: Record<string, unknown> = {};
|
||||
private saveTimeout: NodeJS.Timeout | null = null;
|
||||
private filePath: string;
|
||||
private debounceMs: number;
|
||||
|
||||
constructor(filePath: string, debounceMs: number = 2000) {
|
||||
this.filePath = filePath;
|
||||
this.debounceMs = debounceMs;
|
||||
this.load();
|
||||
}
|
||||
|
||||
private load() {
|
||||
try {
|
||||
if (existsSync(this.filePath)) {
|
||||
const raw = readFileSync(this.filePath, 'utf-8');
|
||||
this.data = JSON.parse(raw);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err, `Failed to load store at ${this.filePath}`);
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Set a key and trigger the debounced auto-save */
|
||||
set(key: string, value: unknown) {
|
||||
this.data[key] = value;
|
||||
this.triggerAutoSave();
|
||||
}
|
||||
|
||||
get<T>(key: string): T | undefined {
|
||||
return this.data[key] as T;
|
||||
}
|
||||
|
||||
delete(key: string): boolean {
|
||||
if (key in this.data) {
|
||||
delete this.data[key];
|
||||
this.triggerAutoSave();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private triggerAutoSave() {
|
||||
if (this.saveTimeout) clearTimeout(this.saveTimeout);
|
||||
this.saveTimeout = setTimeout(() => {
|
||||
this.save();
|
||||
}, this.debounceMs);
|
||||
}
|
||||
|
||||
save(): boolean {
|
||||
try {
|
||||
if (this.saveTimeout) clearTimeout(this.saveTimeout);
|
||||
|
||||
const dir = dirname(this.filePath);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
|
||||
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8');
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error(err, 'Save failed', this.filePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const stores = {
|
||||
settings: new CustomStore(join(getGuiDataFolder(), 'gui-settings.dat'), 1000),
|
||||
cache: new CustomStore(join(getGuiDataFolder(), 'gui-cache.dat'), 100),
|
||||
};
|
||||
50
gui/electron/main/utils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import os from 'os'
|
||||
import { OSStats } from "../preload/interface";
|
||||
import { ipcMain, IpcMainInvokeEvent } from 'electron';
|
||||
import { IpcInvokeMap } from '../shared';
|
||||
import net from 'net'
|
||||
|
||||
export const getPlatform = (): OSStats['type'] => {
|
||||
switch (os.platform()) {
|
||||
case 'darwin':
|
||||
return 'macos';
|
||||
case 'win32':
|
||||
return 'windows';
|
||||
case 'linux':
|
||||
return 'linux';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const isPortAvailable = (port: number) => {
|
||||
return new Promise((resolve) => {
|
||||
const s = net.createServer();
|
||||
s.once('error', (err) => {
|
||||
s.close();
|
||||
if ("code" in err && err["code"] == "EADDRINUSE") {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
s.once('listening', () => {
|
||||
resolve(true);
|
||||
s.close();
|
||||
});
|
||||
s.listen(port);
|
||||
});
|
||||
};
|
||||
|
||||
export function handleIpc<K extends keyof IpcInvokeMap>(
|
||||
channel: K,
|
||||
handler: (
|
||||
event: IpcMainInvokeEvent,
|
||||
...args: Parameters<IpcInvokeMap[K]>
|
||||
) => ReturnType<IpcInvokeMap[K]>
|
||||
) {
|
||||
ipcMain.handle(channel, (event, ...args) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return handler(event, ...args as any);
|
||||
});
|
||||
}
|
||||
39
gui/electron/preload/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { IElectronAPI, ServerStatusEvent } from './interface';
|
||||
import { IPC_CHANNELS } from '../shared';
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
onServerStatus: (callback) => {
|
||||
const subscription = (_event: IpcRendererEvent, value: ServerStatusEvent) =>
|
||||
callback(value);
|
||||
ipcRenderer.on(IPC_CHANNELS.SERVER_STATUS, subscription);
|
||||
return () => ipcRenderer.removeListener(IPC_CHANNELS.SERVER_STATUS, subscription);
|
||||
},
|
||||
openUrl: (url) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_URL, url),
|
||||
osStats: () => ipcRenderer.invoke(IPC_CHANNELS.OS_STATS),
|
||||
close: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'close'),
|
||||
minimize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'minimize'),
|
||||
maximize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'maximize'),
|
||||
getStorage: async (type) => {
|
||||
return {
|
||||
get: (key) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.STORAGE, { type, method: 'get', key }),
|
||||
set: (key, value) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.STORAGE, { type, method: 'set', key, value }),
|
||||
delete: (key) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.STORAGE, { type, method: 'delete', key }),
|
||||
save: () => ipcRenderer.invoke(IPC_CHANNELS.STORAGE, { type, method: 'save' }),
|
||||
};
|
||||
},
|
||||
log: (type, ...args) => ipcRenderer.invoke(IPC_CHANNELS.LOG, type, ...args),
|
||||
i18nOverride: async () => ipcRenderer.invoke(IPC_CHANNELS.I18N_OVERRIDE),
|
||||
showDecorations: () => {},
|
||||
setTranslations: () => {},
|
||||
openDialog: (options) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_DIALOG, options),
|
||||
saveDialog: (options) => ipcRenderer.invoke(IPC_CHANNELS.SAVE_DIALOG, options),
|
||||
openConfigFolder: async () => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'config')),
|
||||
openLogsFolder: async () => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'logs')),
|
||||
openFile: (path) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, path),
|
||||
ghGet: (req) => ipcRenderer.invoke(IPC_CHANNELS.GH_FETCH, req),
|
||||
setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options)
|
||||
} satisfies IElectronAPI);
|
||||
64
gui/electron/preload/interface.d.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
OpenDialogOptions,
|
||||
OpenDialogReturnValue,
|
||||
SaveDialogOptions,
|
||||
SaveDialogReturnValue,
|
||||
} from 'electron';
|
||||
|
||||
export type ServerStatusEvent = {
|
||||
type: 'stdout' | 'stderr' | 'error' | 'terminated' | 'other';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type OSStats = {
|
||||
type: 'linux' | 'windows' | 'macos' | 'unknown';
|
||||
};
|
||||
|
||||
export interface CrossStorage {
|
||||
set(key: string, value: unknown): Promise<void>;
|
||||
get<T>(key: string): Promise<T | undefined>;
|
||||
delete(key: string): Promise<boolean>;
|
||||
save(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export type GHGet = { type: 'fw-releases' } | { type: 'asset'; url: string };
|
||||
export type GHReturn = {
|
||||
asset: [number, string][] | null;
|
||||
['fw-releases']:
|
||||
| {
|
||||
assets: { browser_download_url: string; name: string; digest: string }[];
|
||||
prerelease: boolean;
|
||||
tag_name: string;
|
||||
body: string;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
|
||||
export type DiscordPresence = { enable: false } | { enable: true, activity: string }
|
||||
|
||||
export interface IElectronAPI {
|
||||
onServerStatus: (cb: (data: ServerStatusEvent) => void) => () => void;
|
||||
openUrl: (url: string) => Promise<void>;
|
||||
osStats: () => Promise<OSStats>;
|
||||
openLogsFolder: () => Promise<void>;
|
||||
openConfigFolder: () => Promise<void>;
|
||||
close: () => void;
|
||||
minimize: () => void;
|
||||
maximize: () => void;
|
||||
showDecorations: (decorations: boolean) => void;
|
||||
setTranslations: (translations: Record<string, string>) => void;
|
||||
i18nOverride: () => Promise<string | false>;
|
||||
getStorage: (type: 'settings' | 'cache') => Promise<CrossStorage>;
|
||||
openDialog: (options: OpenDialogOptions) => Promise<OpenDialogReturnValue>;
|
||||
saveDialog: (options: SaveDialogOptions) => Promise<SaveDialogReturnValue>;
|
||||
log: (type: 'info' | 'error' | 'warn', ...args: unknown[]) => void;
|
||||
openFile: (path: string) => void;
|
||||
ghGet: <T extends GHGet>(options: T) => Promise<GHReturn[T['type']]>;
|
||||
setPresence: (options: DiscordPresence) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: IElectronAPI;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 747 B After Width: | Height: | Size: 747 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1008 B After Width: | Height: | Size: 1008 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 922 B After Width: | Height: | Size: 922 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 579 B After Width: | Height: | Size: 579 B |
|
Before Width: | Height: | Size: 555 B After Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 645 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
gui/electron/resources/java-version/JavaVersion.jar
Normal file
@@ -3,6 +3,6 @@ public class JavaVersion {
|
||||
public static void main(String[] args)
|
||||
{
|
||||
var version = Runtime.version().version().get(0);
|
||||
System.exit(version);
|
||||
System.out.println(version);
|
||||
}
|
||||
}
|
||||
26
gui/electron/resources/scripts/postinstall.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
SRC="/opt/SlimeVR/69-slimevr-devices.rules"
|
||||
DESTDIRS=("/lib" "/usr/lib")
|
||||
|
||||
if [[ ! -f "$SRC" ]]; then
|
||||
echo "SlimeVR udev rules not found, serial console and dongles may not work" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Configuring SlimeVR udev rules..."
|
||||
|
||||
for DIR in "${DESTDIRS[@]}"; do
|
||||
if [[ -d "$DIR" && ! -h "$DIR" ]]; then
|
||||
echo "Copying rules to $DIR"
|
||||
install -Dm644 "$SRC" "$DIR/udev/rules.d/69-slimevr-devices.rules"
|
||||
|
||||
if command -v udevadm >/dev/null 2>&1; then
|
||||
udevadm control --reload-rules
|
||||
udevadm trigger
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Couldn't copy SlimeVR udev rules, serial console and dongles may not work" >&2
|
||||
8
gui/electron/resources/scripts/postremove.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
echo "Removing SlimeVR udev rules..."
|
||||
rm -f "/lib/udev/rules.d/69-slimevr-devices.rules"
|
||||
rm -f "/usr/lib/udev/rules.d/69-slimevr-devices.rules"
|
||||
|
||||
if command -v udevadm >/dev/null 2>&1; then
|
||||
udevadm control --reload-rules
|
||||
fi
|
||||
49
gui/electron/shared.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
OpenDialogOptions,
|
||||
OpenDialogReturnValue,
|
||||
SaveDialogOptions,
|
||||
SaveDialogReturnValue,
|
||||
} from 'electron';
|
||||
import { DiscordPresence, GHGet, GHReturn, OSStats } from './preload/interface';
|
||||
|
||||
export const IPC_CHANNELS = {
|
||||
SERVER_STATUS: 'server-status',
|
||||
OPEN_URL: 'open-url',
|
||||
OS_STATS: 'os-stats',
|
||||
WINDOW_ACTIONS: 'window-actions',
|
||||
LOG: 'log',
|
||||
STORAGE: 'storage',
|
||||
OPEN_DIALOG: 'open-dialog',
|
||||
SAVE_DIALOG: 'save-dialog',
|
||||
I18N_OVERRIDE: 'i18n-override',
|
||||
OPEN_FILE: 'open-file',
|
||||
GET_FOLDER: 'get-folder',
|
||||
GH_FETCH: 'gh-fetch',
|
||||
DISCORD_PRESENCE: 'discord-presence'
|
||||
} as const;
|
||||
|
||||
export interface IpcInvokeMap {
|
||||
[IPC_CHANNELS.OPEN_URL]: (url: string) => void;
|
||||
[IPC_CHANNELS.OS_STATS]: () => Promise<OSStats>;
|
||||
[IPC_CHANNELS.WINDOW_ACTIONS]: (action: 'close' | 'minimize' | 'maximize') => void;
|
||||
[IPC_CHANNELS.LOG]: (type: 'info' | 'error' | 'warn', ...args: unknown[]) => void;
|
||||
[IPC_CHANNELS.OPEN_DIALOG]: (
|
||||
options: OpenDialogOptions
|
||||
) => Promise<OpenDialogReturnValue>;
|
||||
[IPC_CHANNELS.SAVE_DIALOG]: (
|
||||
options: SaveDialogOptions
|
||||
) => Promise<SaveDialogReturnValue>;
|
||||
[IPC_CHANNELS.I18N_OVERRIDE]: () => Promise<string | false>;
|
||||
[IPC_CHANNELS.STORAGE]: (args: {
|
||||
type: 'settings' | 'cache';
|
||||
method: 'get' | 'set' | 'delete' | 'save';
|
||||
key?: string;
|
||||
value?: unknown;
|
||||
}) => Promise<unknown>;
|
||||
[IPC_CHANNELS.OPEN_FILE]: (path: string) => void;
|
||||
[IPC_CHANNELS.GET_FOLDER]: (folder: 'config' | 'logs') => string;
|
||||
[IPC_CHANNELS.GH_FETCH]: <T extends GHGet>(
|
||||
options: T
|
||||
) => Promise<GHReturn[T['type']]>;
|
||||
[IPC_CHANNELS.DISCORD_PRESENCE]: (options: DiscordPresence) => void;
|
||||
}
|
||||
143
gui/package.json
@@ -1,82 +1,54 @@
|
||||
{
|
||||
"name": "slimevr-ui",
|
||||
"version": "0.5.1",
|
||||
"private": true,
|
||||
"name": "slimevr",
|
||||
"version": "0.0.0",
|
||||
"author": "SlimeVR Team <contact@slimevr.dev>",
|
||||
"homepage": "https://slimevr.dev",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@ryuziii/discord-rpc": "1.0.1-rc.1",
|
||||
"@xhayper/discord-rpc": "^1.3.0",
|
||||
"commander": "^14.0.3",
|
||||
"discord-rich-presence": "^0.0.8",
|
||||
"glob": "^13.0.3",
|
||||
"open": "^11.0.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"pino-roll": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite --force",
|
||||
"gui": "electron-vite dev --config electron.vite.config.ts --watch",
|
||||
"build": "electron-vite build --config electron.vite.config.ts",
|
||||
"package": "electron-builder",
|
||||
"package:build": "pnpm build && pnpm package",
|
||||
"preview": "electron-vite preview --config electron.vite.config.ts",
|
||||
"skipbundler": "vite build",
|
||||
"lint": "tsc --noEmit && eslint --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && prettier --check \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"",
|
||||
"lint:fix": "tsc --noEmit && eslint --fix --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && pnpm run format",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"",
|
||||
"javaversion-build": "cd electron/resources/java-version/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class",
|
||||
"gen:javaversion": "cd electron/resources/java-version/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class",
|
||||
"gen:firmware-tool": "openapi-codegen gen firmwareTool"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dword-design/eslint-plugin-import-alias": "^4.0.9",
|
||||
"@electron/asar": "^4.0.1",
|
||||
"@fluent/bundle": "^0.18.0",
|
||||
"@fluent/react": "^0.15.2",
|
||||
"@fontsource/poppins": "^5.1.0",
|
||||
"@formatjs/intl-localematcher": "^0.2.32",
|
||||
"@hookform/resolvers": "^3.6.0",
|
||||
"@react-hookz/deep-equal": "^3.0.3",
|
||||
"@react-three/drei": "^9.114.3",
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"@sentry/react": "10.29.0",
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/react-query": "^5.48.0",
|
||||
"@tauri-apps/api": "^2.0.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-fs": "2.4.1",
|
||||
"@tauri-apps/plugin-http": "^2.5.0",
|
||||
"@tauri-apps/plugin-log": "~2",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||
"@tauri-apps/plugin-store": "^2.4.1",
|
||||
"@tweenjs/tween.js": "^25.0.0",
|
||||
"@twemoji/svg": "^15.0.0",
|
||||
"ajv": "^8.17.1",
|
||||
"browser-fs-access": "^0.35.0",
|
||||
"classnames": "^2.5.1",
|
||||
"convert": "^5.12.0",
|
||||
"flatbuffers": "22.10.26",
|
||||
"intl-pluralrules": "^2.0.1",
|
||||
"ip-num": "^1.5.1",
|
||||
"jotai": "^2.12.2",
|
||||
"prompts": "^2.4.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.63.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"semver": "^7.6.3",
|
||||
"solarxr-protocol": "file:../solarxr-protocol",
|
||||
"three": "^0.163.0",
|
||||
"ts-pattern": "^5.4.0",
|
||||
"typescript": "^5.6.3",
|
||||
"use-double-tap": "^1.3.6",
|
||||
"uuid": "^13.0.0",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite --force",
|
||||
"build": "vite build",
|
||||
"dev": "tauri dev",
|
||||
"skipbundler": "tauri build --no-bundle",
|
||||
"tauri": "tauri",
|
||||
"lint": "tsc --noEmit && eslint --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && prettier --check \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"",
|
||||
"lint:fix": "tsc --noEmit && eslint --fix --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && pnpm run format",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"",
|
||||
"preview-vite": "vite preview",
|
||||
"javaversion-build": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class",
|
||||
"gen:javaversion": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class",
|
||||
"gen:firmware-tool": "openapi-codegen gen firmwareTool",
|
||||
"gen:icons": "tauri icon --ios-color '#663499' src-tauri/icons/icon.svg"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dword-design/eslint-plugin-import-alias": "^4.0.9",
|
||||
"@openapi-codegen/cli": "^3.1.0",
|
||||
"@openapi-codegen/typescript": "^8.0.2",
|
||||
"@react-hookz/deep-equal": "^3.0.3",
|
||||
"@sentry/react": "10.29.0",
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"@stylistic/eslint-plugin": "^5.5.0",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tauri-apps/cli": "~2",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/react-query": "^5.48.0",
|
||||
"@tweenjs/tween.js": "^25.0.0",
|
||||
"@twemoji/svg": "^15.0.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^18.3.11",
|
||||
@@ -88,23 +60,54 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"ajv": "^8.17.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cross-env": "^7.0.3",
|
||||
"browser-fs-access": "^0.35.0",
|
||||
"classnames": "^2.5.1",
|
||||
"convert": "^5.12.0",
|
||||
"dmg-license": "^1.0.11",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^40.3.0",
|
||||
"electron-builder": "^26.7.0",
|
||||
"electron-vite": "^5.0.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-import-resolver-typescript": "^3.10.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"flatbuffers": "22.10.26",
|
||||
"globals": "^15.10.0",
|
||||
"intl-pluralrules": "^2.0.1",
|
||||
"ip-num": "^1.5.1",
|
||||
"jotai": "^2.12.2",
|
||||
"prettier": "^3.3.3",
|
||||
"prompts": "^2.4.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.63.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.79.4",
|
||||
"semver": "^7.6.3",
|
||||
"solarxr-protocol": "file:../solarxr-protocol",
|
||||
"spdx-satisfies": "^5.0.1",
|
||||
"tailwind-gradient-mask-image": "^1.2.0",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"three": "^0.163.0",
|
||||
"ts-pattern": "^5.4.0",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
"use-double-tap": "^1.3.6",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^5.4.8",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"main": "./out/main/index.js"
|
||||
}
|
||||
|
||||
BIN
gui/public/fonts/noto-sans-v42-latin-regular.woff2
Normal file
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
6
gui/src-tauri/.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
WixTools
|
||||
src/JavaVersion.class
|
||||
/gen/schemas
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
'**/*.rs': 'cargo fmt --',
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
[package]
|
||||
name = "slimevr"
|
||||
version = "0.0.0"
|
||||
|
||||
description = "SlimeVR GUI Application"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
default-run = "slimevr"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = ["custom-protocol"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0", features = [] }
|
||||
cfg_aliases = "0.2"
|
||||
shadow-rs = "0.35"
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "2.0", features = ["devtools", "tray-icon", "image-png", "rustls-tls"] }
|
||||
tauri-runtime = "2.0"
|
||||
tauri-plugin-dialog = "2.0"
|
||||
tauri-plugin-fs = "2.4.1"
|
||||
tauri-plugin-os = "2.0"
|
||||
tauri-plugin-shell = "2.3.0"
|
||||
tauri-plugin-store = "2.0"
|
||||
flexi_logger = "0.29"
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
log = "0.4"
|
||||
clap = { version = "4.0.29", features = ["derive"] }
|
||||
clap-verbosity-flag = "2"
|
||||
rand = "0.8.5"
|
||||
tempfile = "3"
|
||||
which = "6.0"
|
||||
glob = "0.3"
|
||||
open = "5"
|
||||
shadow-rs = { version = "0.35", default-features = false }
|
||||
const_format = "0.2.30"
|
||||
cfg-if = "1"
|
||||
color-eyre = "0.6"
|
||||
rfd = { version = "0.15", features = ["gtk3"], default-features = false }
|
||||
dirs-next = "2.0.0"
|
||||
discord-sdk = "0.3.6"
|
||||
tokio = { version = "1.37.0", features = ["time"] }
|
||||
itertools = "0.13.0"
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
tauri-plugin-http = "2.5.0"
|
||||
tauri-plugin-log = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
win32job = "1"
|
||||
winreg = "0.52"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libloading = "0.8"
|
||||
@@ -1,15 +0,0 @@
|
||||
use cfg_aliases::cfg_aliases;
|
||||
|
||||
fn main() -> shadow_rs::SdResult<()> {
|
||||
// Bypass for Nix script having libudev-zero and Tauri not liking it
|
||||
if let Some(path) = option_env!("SLIMEVR_RUST_LD_LIBRARY_PATH") {
|
||||
println!("cargo:rustc-env=LD_LIBRARY_PATH={path}");
|
||||
}
|
||||
|
||||
tauri_build::build();
|
||||
cfg_aliases! {
|
||||
mobile: { any(target_os = "ios", target_os = "android") },
|
||||
desktop: { not(any(target_os = "ios", target_os = "android")) }
|
||||
}
|
||||
shadow_rs::new()
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"identifier": "migrated",
|
||||
"description": "permissions that were migrated from v1",
|
||||
"local": true,
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-destroy",
|
||||
"core:window:allow-request-user-attention",
|
||||
"core:window:allow-set-decorations",
|
||||
"store:default",
|
||||
"os:allow-os-type",
|
||||
"os:allow-hostname",
|
||||
"os:allow-locale",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"shell:allow-open",
|
||||
"store:allow-get",
|
||||
"store:allow-set",
|
||||
"store:allow-save",
|
||||
"fs:allow-write-text-file",
|
||||
"fs:allow-read-text-file",
|
||||
"fs:allow-exists",
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$APPDATA"
|
||||
},
|
||||
{
|
||||
"path": "$APPDATA/**"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "opener:allow-open-url",
|
||||
"allow": [
|
||||
{
|
||||
"url": "steam:*"
|
||||
},
|
||||
{
|
||||
"url": "ms-settings:network"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://github.com/SlimeVR/SlimeVR-Tracker-ESP/releases/download/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"opener:default",
|
||||
"log:default"
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.5
|
||||
Categories=Game;GTK;
|
||||
Exec={{exec}}
|
||||
Icon={{icon}}
|
||||
|
||||
Name=SlimeVR
|
||||
GenericName=Full-body tracking
|
||||
Comment=An app for facilitating full-body tracking in virtual reality
|
||||
Keywords=FBT;VR;Steam;VRChat;IMU
|
||||
|
||||
Terminal=false
|
||||
Type=Application
|
||||
@@ -1,139 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
dev.slimevr.SlimeVR.metainfo.xml by SlimeVR contributors
|
||||
|
||||
To the extent possible under law, the person who associated CC0 with
|
||||
dev.slimevr.SlimeVR.metainfo.xml has waived all copyright and related or neighboring rights
|
||||
to dev.slimevr.SlimeVR.metainfo.xml.
|
||||
|
||||
You should have received a copy of the CC0 legalcode along with this
|
||||
work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
-->
|
||||
<component type="desktop-application">
|
||||
<id>dev.slimevr.SlimeVR</id>
|
||||
|
||||
<name>SlimeVR</name>
|
||||
<summary>Accessible full-body tracking in VR</summary>
|
||||
<developer_name>SlimeVR Team</developer_name>
|
||||
|
||||
<!-- CC0 so attribution is not required -->
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>MIT OR Apache-2.0</project_license>
|
||||
|
||||
<content_rating type="oars-1.1" />
|
||||
<url type="homepage">https://slimevr.dev/</url>
|
||||
<url type="bugtracker">https://github.com/SlimeVR/SlimeVR-Server/issues</url>
|
||||
<url type="faq">https://docs.slimevr.dev/slimevr101.html</url>
|
||||
<url type="donation">https://github.com/sponsors/SlimeVR</url>
|
||||
<url type="vcs-browser">https://github.com/SlimeVR/SlimeVR-Server</url>
|
||||
<url type="translate">https://i18n.slimevr.dev</url>
|
||||
<url type="help">https://docs.slimevr.dev/server-setup/slimevr-setup.html</url>
|
||||
<url type="contribute">https://github.com/SlimeVR/SlimeVR-Server/blob/main/CONTRIBUTING.md</url>
|
||||
<url type="contact">https://discord.gg/SlimeVR</url>
|
||||
<recommends>
|
||||
<display_length compare="ge">300</display_length>
|
||||
</recommends>
|
||||
<supports>
|
||||
<control>pointing</control>
|
||||
<control>keyboard</control>
|
||||
<control>touch</control>
|
||||
</supports>
|
||||
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#BB8AE5</color>
|
||||
<color type="primary" scheme_preference="dark">#663499</color>
|
||||
</branding>
|
||||
|
||||
<description>
|
||||
<p>
|
||||
SlimeVR is a set of open hardware sensors and open source software that facilitates full-body
|
||||
tracking (FBT) in virtual reality. With no base station required, SlimeVR makes wireless
|
||||
VR FBT affordable and comfortable.
|
||||
</p>
|
||||
</description>
|
||||
|
||||
<launchable type="desktop-id">dev.slimevr.SlimeVR.desktop</launchable>
|
||||
<launchable type="desktop-id">safe-mode.dev.slimevr.SlimeVR.desktop</launchable>
|
||||
<screenshots>
|
||||
<screenshot type="default" xml:lang="en">
|
||||
<caption>The onboarding for the GUI</caption>
|
||||
<image>https://raw.githubusercontent.com/SlimeVR/SlimeVR-Server/main/assets/img/onboarding.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<provides>
|
||||
<binary>slimevr</binary>
|
||||
</provides>
|
||||
|
||||
<releases>
|
||||
<release version="0.16.2" date="2025-08-01"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.2</url></release>
|
||||
<release version="0.16.1" date="2025-07-27"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1</url></release>
|
||||
<release version="0.16.1~rc.2" type="development" date="2025-07-17"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1-rc.2</url></release>
|
||||
<release version="0.16.1~rc.1" type="development" date="2025-07-04"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.1-rc.1</url></release>
|
||||
<release version="0.16.0" date="2025-07-01"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0</url></release>
|
||||
<release version="0.16.0~rc.2" type="development" date="2025-06-20"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0-rc.2</url></release>
|
||||
<release version="0.16.0~rc.1" type="development" date="2025-05-27"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.16.0-rc.1</url></release>
|
||||
<release version="0.15.0" date="2025-05-19"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.15.0</url></release>
|
||||
<release version="0.15.0~rc.4" type="development" date="2025-05-12"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.15.0-rc.4</url></release>
|
||||
<release version="0.15.0~rc.3" type="development" date="2025-04-28"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.15.0-rc.3</url></release>
|
||||
<release version="0.15.0~rc.2" type="development" date="2025-04-25"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.15.0-rc.2</url></release>
|
||||
<release version="0.15.0~rc.1" type="development" date="2025-04-23"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.15.0-rc.1</url></release>
|
||||
<release version="0.14.1" date="2025-04-15"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.14.1</url></release>
|
||||
<release version="0.14.0" date="2025-04-10"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.14.0</url></release>
|
||||
<release version="0.14.0~rc.2" type="development" date="2025-03-25"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.14.0-rc.2</url></release>
|
||||
<release version="0.14.0~rc.1" type="development" date="2025-02-12"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.14.0-rc1</url></release>
|
||||
<release version="0.13.2" date="2024-11-06"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.2</url></release>
|
||||
<release version="0.13.1" date="2024-11-05"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1</url></release>
|
||||
<release version="0.13.1~rc.3" type="development" date="2024-10-31"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.3</url></release>
|
||||
<release version="0.13.1~rc.2" type="development" date="2024-10-26"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.2</url></release>
|
||||
<release version="0.13.1~rc.1" type="development" date="2024-10-16"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.1</url></release>
|
||||
<release version="0.13.0" date="2024-09-20"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.0</url></release>
|
||||
<release version="0.13.0~rc.4" type="development" date="2024-09-13"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.0-rc.4</url></release>
|
||||
<release version="0.13.0~rc.3" type="development" date="2024-08-14"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.0-rc.3</url></release>
|
||||
<release version="0.13.0~rc.2" type="development" date="2024-08-08"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.0-rc.2</url></release>
|
||||
<release version="0.13.0~rc.1" type="development" date="2024-08-02"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.0-rc.1</url></release>
|
||||
<release version="0.12.1" date="2024-04-29"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.12.1</url></release>
|
||||
<release version="0.12.0" date="2024-04-26"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.12.0</url></release>
|
||||
<release version="0.12.0~rc.4" type="development" date="2024-04-21"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.12.0-rc.4</url></release>
|
||||
<release version="0.12.0~rc.3" type="development" date="2024-04-14"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.12.0-rc.3</url></release>
|
||||
<release version="0.12.0~rc.2" type="development" date="2024-04-09"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.12.0-rc.2</url></release>
|
||||
<release version="0.12.0~rc.1" type="development" date="2024-04-04"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.12.0-rc.1</url></release>
|
||||
<release version="0.11.0" date="2023-12-23"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.11.0</url></release>
|
||||
<release version="0.11.0~rc.2" type="development" date="2023-12-08"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.11.0-rc.2</url></release>
|
||||
<release version="0.11.0~rc.1" type="development" date="2023-11-23"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.11.0-rc.1</url></release>
|
||||
<release version="0.10.1" date="2023-09-30"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.10.1</url></release>
|
||||
<release version="0.10.1~rc.1" type="development" date="2023-09-29"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.10.1-rc.1</url></release>
|
||||
<release version="0.10.0" date="2023-09-22"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.10.0</url></release>
|
||||
<release version="0.10.0~rc.2" type="development" date="2023-09-15"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.10.0-rc.2</url></release>
|
||||
<release version="0.10.0~rc.1" type="development" date="2023-09-02"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.10.0-rc.1</url></release>
|
||||
<release version="0.9.1" date="2023-08-30"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.9.1</url></release>
|
||||
<release version="0.9.1~rc.4" type="development" date="2023-08-28"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.9.1-rc.4</url></release>
|
||||
<release version="0.9.1~rc.3" type="development" date="2023-08-19"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.9.1-rc.3</url></release>
|
||||
<release version="0.9.1~rc.2" type="development" date="2023-08-15"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.9.1-rc.2</url></release>
|
||||
<release version="0.9.1~rc.1" type="development" date="2023-08-13"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.9.1-rc.1</url></release>
|
||||
<release version="0.9.0" date="2023-08-05"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.9.0</url></release>
|
||||
<release version="0.9.0~rc.2" type="development" date="2023-08-02"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.9.0-rc.2</url></release>
|
||||
<release version="0.9.0~rc.1" type="development" date="2023-07-31"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.9.0-rc.1</url></release>
|
||||
<release version="0.8.3" date="2023-07-09"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.3</url></release>
|
||||
<release version="0.8.2" date="2023-07-09"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.2</url></release>
|
||||
<release version="0.8.2~rc.1" type="development" date="2023-07-07"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.2-rc.1</url></release>
|
||||
<release version="0.8.1" date="2023-07-04"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.1</url></release>
|
||||
<release version="0.8.0" date="2023-06-22"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.0</url></release>
|
||||
<release version="0.8.0~rc.3" type="development" date="2023-06-20"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.0-rc.3</url></release>
|
||||
<release version="0.8.0~rc.2" type="development" date="2023-06-15"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.0-rc.2</url></release>
|
||||
<release version="0.8.0~rc.1" type="development" date="2023-06-01"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.0-rc.1</url></release>
|
||||
<release version="0.7.1" date="2023-04-14"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.7.1</url></release>
|
||||
<release version="0.7.0" date="2023-04-11"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.7.0</url></release>
|
||||
<release version="0.6.3" date="2023-02-22"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.6.3</url></release>
|
||||
<release version="0.6.2" date="2023-02-17"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.6.2</url></release>
|
||||
<release version="0.6.1" date="2023-02-12"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.6.1</url></release>
|
||||
<release version="0.6.0" date="2023-01-05"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.6.0</url></release>
|
||||
<release version="0.5.1" date="2022-12-12"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.5.1</url></release>
|
||||
<release version="0.5.0" date="2022-12-07"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.5.0</url></release>
|
||||
<release version="0.4.0" date="2022-11-24"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.4.0</url></release>
|
||||
<release version="0.3.1" date="2022-11-22"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.3.1</url></release>
|
||||
<release version="0.3.0" date="2022-11-16"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.3.0</url></release>
|
||||
<release version="0.2.1" date="2022-08-24"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.2.1</url></release>
|
||||
<release version="0.2.0" date="2022-06-28"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.2.0</url></release>
|
||||
</releases>
|
||||
</component>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,11 +0,0 @@
|
||||
@echo off
|
||||
setlocal enableextensions
|
||||
|
||||
echo "TEST"
|
||||
|
||||
cd /d "C:\Program Files (x86)\SlimeVR Server"
|
||||
|
||||
jre\bin\java.exe -Xmx512M -jar slimevr.jar --no-gui
|
||||
if %errorlevel% NEQ 0 (
|
||||
pause
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.5
|
||||
Categories=Game;GTK;
|
||||
Exec=bash -c "WEBKIT_DISABLE_DMABUF_RENDERER=1 WAYLAND_DISPLAY=0 slimevr"
|
||||
Icon=slimevr
|
||||
|
||||
Name=SlimeVR (safe mode)
|
||||
GenericName=Full-body tracking
|
||||
Comment=An app for facilitating full-body tracking in virtual reality
|
||||
Keywords=FBT;VR;Steam;VRChat;IMU
|
||||
|
||||
Terminal=false
|
||||
Type=Application
|
||||
@@ -1,443 +0,0 @@
|
||||
#![cfg_attr(all(not(debug_assertions), windows), windows_subsystem = "windows")]
|
||||
use std::env;
|
||||
use std::panic;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use state::WindowState;
|
||||
use tauri::Emitter;
|
||||
use tauri::WindowEvent;
|
||||
use tauri::{Manager, RunEvent};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use util::get_log_dir;
|
||||
|
||||
use crate::util::{show_error, valid_java_paths, Cli, JAVA_BIN, MINIMUM_JAVA_VERSION};
|
||||
|
||||
mod presence;
|
||||
mod state;
|
||||
mod tray;
|
||||
mod util;
|
||||
|
||||
#[tauri::command]
|
||||
fn update_window_state(
|
||||
window: tauri::Window,
|
||||
state: tauri::State<Mutex<WindowState>>,
|
||||
) -> Result<(), String> {
|
||||
let mut lock = state.lock().unwrap();
|
||||
lock.update_state(&window, false)
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
if window.is_maximized().map_err(|e| e.to_string())? {
|
||||
window.unmaximize().map_err(|e| e.to_string())?;
|
||||
lock.update_state(&window, true)
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_config_folder(app_handle: tauri::AppHandle) {
|
||||
let path = app_handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.unwrap_or_else(|_| Path::new(".").to_path_buf());
|
||||
|
||||
if let Err(err) = open::that(path) {
|
||||
log::error!("Failed to open config folder: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_logs_folder(app_handle: tauri::AppHandle) {
|
||||
#[cfg(windows)]
|
||||
let path = app_handle.path().app_data_dir().map(|dir| dir.join("logs"));
|
||||
|
||||
#[cfg(unix)]
|
||||
let path = app_handle.path().app_log_dir();
|
||||
|
||||
if let Err(err) =
|
||||
open::that(path.unwrap_or_else(|_| Path::new("./logs/").to_path_buf()))
|
||||
{
|
||||
log::error!("Failed to open logs folder: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
log_panics::init();
|
||||
let hook = panic::take_hook();
|
||||
// Make an error dialog box when panicking
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
show_error(&panic_info.to_string());
|
||||
hook(panic_info);
|
||||
}));
|
||||
|
||||
let cli = Cli::parse();
|
||||
let tauri_context = tauri::generate_context!();
|
||||
|
||||
// Ensure child processes die when spawned on windows
|
||||
// and then check for WebView2's existence
|
||||
#[cfg(windows)]
|
||||
setup_webview2()?;
|
||||
|
||||
// Check for environment variables that can affect the server, and if so, warn in log and GUI
|
||||
check_environment_variables();
|
||||
|
||||
// Spawn server process
|
||||
let exit_flag = Arc::new(AtomicBool::new(false));
|
||||
let backend = Arc::new(Mutex::new(Option::<CommandChild>::None));
|
||||
let build_result =
|
||||
setup_tauri(cli, tauri_context, exit_flag.clone(), backend.clone());
|
||||
|
||||
tauri_build_result(build_result, exit_flag, backend);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn setup_webview2() -> Result<()> {
|
||||
use crate::util::webview2_exists;
|
||||
use win32job::{ExtendedLimitInfo, Job};
|
||||
|
||||
let mut info = ExtendedLimitInfo::new();
|
||||
info.limit_kill_on_job_close();
|
||||
let job = Job::create_with_limit_info(&mut info).expect("Failed to create Job");
|
||||
job.assign_current_process()
|
||||
.expect("Failed to assign current process to Job");
|
||||
|
||||
// We don't do anything with the job anymore, but we shouldn't drop it because that would
|
||||
// terminate our process tree. So we intentionally leak it instead.
|
||||
std::mem::forget(job);
|
||||
|
||||
if !webview2_exists() {
|
||||
// This makes a dialog appear which let's you press Ok or Cancel
|
||||
// If you press Ok it will open the SlimeVR installer documentation
|
||||
use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel};
|
||||
|
||||
let confirm = MessageDialog::new()
|
||||
.set_title("SlimeVR")
|
||||
.set_description("Couldn't find WebView2 installed. You can install it with the SlimeVR installer")
|
||||
.set_buttons(MessageButtons::OkCancel)
|
||||
.set_level(MessageLevel::Error)
|
||||
.show();
|
||||
if confirm == MessageDialogResult::Ok {
|
||||
open::that("https://docs.slimevr.dev/server-setup/installing-and-connecting.html#install-the-latest-slimevr-installer").unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_environment_variables() {
|
||||
use itertools::Itertools;
|
||||
const ENVS_TO_CHECK: &[&str] = &["_JAVA_OPTIONS", "JAVA_TOOL_OPTIONS"];
|
||||
let checked_envs = ENVS_TO_CHECK
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
let Ok(data) = env::var(e) else {
|
||||
return None;
|
||||
};
|
||||
log::warn!("{e} is set to: {data}");
|
||||
Some(e)
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
if !checked_envs.is_empty() {
|
||||
rfd::MessageDialog::new()
|
||||
.set_title("SlimeVR")
|
||||
.set_description(format!("You have environment variables {} set, which may cause the SlimeVR Server to fail to launch properly.", checked_envs))
|
||||
.set_level(rfd::MessageLevel::Warning)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
fn find_java_bin<P: AsRef<Path>>(shared_dir: P) -> Option<PathBuf> {
|
||||
let shared_dir = shared_dir.as_ref();
|
||||
// Check if any Java already installed is compatible
|
||||
let jre = shared_dir.join("jre/bin").join(JAVA_BIN);
|
||||
let java_bin = jre
|
||||
.exists()
|
||||
.then(|| jre.into_os_string())
|
||||
.or_else(|| valid_java_paths().first().map(|x| x.0.to_owned()))?;
|
||||
Some(PathBuf::from(java_bin))
|
||||
}
|
||||
|
||||
fn find_server_jar(cli: &Cli) -> Option<PathBuf> {
|
||||
let paths = [
|
||||
cli.launch_from_path.clone(),
|
||||
// AppImage passes the fakeroot in `APPDIR` env var.
|
||||
env::var_os("APPDIR").map(|a| PathBuf::from(a).join("usr/share/slimevr/")),
|
||||
env::current_dir().ok(),
|
||||
// getcwd in Mac can't be trusted, so let's get the executable's path
|
||||
env::current_exe()
|
||||
.map(|mut f| {
|
||||
f.pop();
|
||||
f
|
||||
})
|
||||
.ok(),
|
||||
// For development
|
||||
#[cfg(debug_assertions)]
|
||||
Some(PathBuf::from(env!("CARGO_MANIFEST_DIR"))),
|
||||
// For flatpak container
|
||||
Some(PathBuf::from("/app/share/slimevr/")),
|
||||
Some(PathBuf::from("/usr/share/slimevr/")),
|
||||
];
|
||||
|
||||
paths
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|x| x.join("slimevr.jar"))
|
||||
.find(|x| x.exists())
|
||||
}
|
||||
|
||||
fn server_running() -> bool {
|
||||
std::net::TcpListener::bind("127.0.0.1:21110").is_err()
|
||||
}
|
||||
|
||||
fn setup_tauri(
|
||||
cli: Cli,
|
||||
context: tauri::Context,
|
||||
exit_flag: Arc<AtomicBool>,
|
||||
backend: Arc<Mutex<Option<CommandChild>>>,
|
||||
) -> Result<tauri::App, tauri::Error> {
|
||||
let exit_flag_terminated = exit_flag.clone();
|
||||
tauri::Builder::default()
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.target(tauri_plugin_log::Target::new(
|
||||
tauri_plugin_log::TargetKind::Folder {
|
||||
path: get_log_dir(&context)?,
|
||||
file_name: Some("slimevr".to_string()),
|
||||
},
|
||||
))
|
||||
.max_file_size(30_000 /* bytes */)
|
||||
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepSome(3))
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
update_window_state,
|
||||
open_config_folder,
|
||||
open_logs_folder,
|
||||
tray::update_translations,
|
||||
tray::update_tray_text,
|
||||
tray::is_tray_available,
|
||||
presence::discord_client_exists,
|
||||
presence::update_presence,
|
||||
presence::clear_presence,
|
||||
presence::create_discord_client,
|
||||
])
|
||||
.setup(move |app| {
|
||||
log::info!("SlimeVR started!");
|
||||
let window_state =
|
||||
WindowState::open_state(app.path().app_config_dir().unwrap())
|
||||
.unwrap_or_default();
|
||||
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
app,
|
||||
"main",
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("SlimeVR")
|
||||
.inner_size(1289.0, 709.0)
|
||||
.min_inner_size(util::MIN_WINDOW_SIZE_WIDTH, util::MIN_WINDOW_SIZE_HEIGHT)
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.decorations(false)
|
||||
.fullscreen(false)
|
||||
// This allows drag & drop via HTML5 for Windows
|
||||
.disable_drag_drop_handler()
|
||||
.build()?;
|
||||
if window_state.is_old() {
|
||||
window_state.update_window(&window.as_ref().window(), false)?;
|
||||
}
|
||||
|
||||
if cfg!(desktop) {
|
||||
let handle = app.handle();
|
||||
tray::create_tray(handle)?;
|
||||
presence::create_presence(handle)?;
|
||||
} else {
|
||||
app.manage(tray::TrayAvailable(false));
|
||||
}
|
||||
|
||||
app.manage(Mutex::new(window_state));
|
||||
|
||||
if cli.skip_server_start_if_running && server_running() {
|
||||
log::info!("Skipping server start: server is already running.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
struct ServerStartInfo {
|
||||
server_jar: PathBuf,
|
||||
java_bin: PathBuf,
|
||||
}
|
||||
|
||||
let start_server = move |start_info: ServerStartInfo| {
|
||||
let app_handle = app.app_handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
use tauri_plugin_shell::{process::CommandEvent, ShellExt};
|
||||
|
||||
let (mut rx, child) = app_handle
|
||||
.shell()
|
||||
.command(start_info.java_bin.to_str().unwrap())
|
||||
.current_dir(start_info.server_jar.parent().unwrap())
|
||||
.args([
|
||||
"-Xmx128M",
|
||||
"-jar",
|
||||
start_info.server_jar.to_str().unwrap(),
|
||||
"run",
|
||||
])
|
||||
.spawn()
|
||||
.expect("Unable to start the server jar");
|
||||
|
||||
{
|
||||
let mut lock = backend.lock().unwrap();
|
||||
*lock = Some(child)
|
||||
}
|
||||
|
||||
while let Some(cmd_event) = rx.recv().await {
|
||||
let emit_me = match cmd_event {
|
||||
CommandEvent::Stderr(v) => {
|
||||
("stderr", String::from_utf8(v).unwrap_or_default())
|
||||
}
|
||||
CommandEvent::Stdout(v) => {
|
||||
("stdout", String::from_utf8(v).unwrap_or_default())
|
||||
}
|
||||
CommandEvent::Error(s) => ("error", s),
|
||||
CommandEvent::Terminated(s) => {
|
||||
exit_flag_terminated.store(true, Ordering::Relaxed);
|
||||
("terminated", format!("{s:?}"))
|
||||
}
|
||||
_ => ("other", "".to_string()),
|
||||
};
|
||||
app_handle
|
||||
.emit("server-status", emit_me)
|
||||
.expect("Check server log files. \nFailed to emit");
|
||||
}
|
||||
log::error!("Java server receiver died");
|
||||
app_handle
|
||||
.emit("server-status", ("other", "receiver cancelled"))
|
||||
.expect("Failed to emit");
|
||||
});
|
||||
};
|
||||
|
||||
let find_server = || -> Option<ServerStartInfo> {
|
||||
use const_format::formatcp;
|
||||
|
||||
let server_jar = find_server_jar(&cli)?;
|
||||
log::info!("Server found on path: {}", server_jar.to_str().unwrap());
|
||||
|
||||
let shared_dir = server_jar.parent().unwrap();
|
||||
let Some(java_bin) = find_java_bin(shared_dir) else {
|
||||
show_error(formatcp!(
|
||||
"Couldn't find a compatible Java version, please download Java {} or higher",
|
||||
MINIMUM_JAVA_VERSION
|
||||
));
|
||||
return None;
|
||||
};
|
||||
log::info!("Using Java binary: {:?}", java_bin);
|
||||
|
||||
Some(ServerStartInfo {
|
||||
server_jar,
|
||||
java_bin,
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(start_info) = find_server() {
|
||||
start_server(start_info);
|
||||
} else {
|
||||
log::warn!("No server found. We will not start the server.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|w, e| match e {
|
||||
WindowEvent::CloseRequested { .. } => {
|
||||
let window_state = w.state::<Mutex<WindowState>>();
|
||||
if let Err(e) = update_window_state(w.clone(), window_state) {
|
||||
log::error!("failed to update window state {}", e)
|
||||
}
|
||||
}
|
||||
// See https://github.com/tauri-apps/tauri/issues/4012#issuecomment-1449499149
|
||||
// #[cfg(windows)]
|
||||
// WindowEvent::Resized(_) => std::thread::sleep(std::time::Duration::from_nanos(1)),
|
||||
_ => (),
|
||||
})
|
||||
.build(context)
|
||||
}
|
||||
|
||||
fn tauri_build_result(
|
||||
build_result: Result<tauri::App, tauri::Error>,
|
||||
exit_flag: Arc<AtomicBool>,
|
||||
backend: Arc<Mutex<Option<CommandChild>>>,
|
||||
) {
|
||||
match build_result {
|
||||
Ok(app) => {
|
||||
app.run(move |app_handle, event| match event {
|
||||
RunEvent::Exit => {
|
||||
let window_state = app_handle.state::<Mutex<WindowState>>();
|
||||
let lock = window_state.lock().unwrap();
|
||||
let config_dir = app_handle.path().app_config_dir().unwrap();
|
||||
let window_state_res = lock.save_state(config_dir);
|
||||
match window_state_res {
|
||||
Ok(()) => log::info!("saved window state"),
|
||||
Err(e) => log::error!("failed to save window state: {}", e),
|
||||
}
|
||||
|
||||
let mut lock = backend.lock().unwrap();
|
||||
let Some(ref mut child) = *lock else { return };
|
||||
let write_result = child.write(b"exit\n");
|
||||
match write_result {
|
||||
Ok(()) => log::info!("send exit to backend"),
|
||||
Err(_) => log::error!("fail to send exit to backend"),
|
||||
}
|
||||
let ten_seconds = Duration::from_secs(10);
|
||||
let start_time = Instant::now();
|
||||
while start_time.elapsed() < ten_seconds {
|
||||
if exit_flag.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
#[cfg(windows)]
|
||||
// Often triggered when the user doesn't have webview2 installed
|
||||
Err(tauri::Error::Runtime(tauri_runtime::Error::CreateWebview(error))) => {
|
||||
// I should log this anyways, don't want to dig a grave by not logging the error.
|
||||
log::error!("CreateWebview error {}", error);
|
||||
|
||||
use rfd::{
|
||||
MessageButtons, MessageDialog, MessageDialogResult, MessageLevel,
|
||||
};
|
||||
|
||||
let confirm = MessageDialog::new()
|
||||
.set_title("SlimeVR")
|
||||
.set_description("You seem to have a faulty installation of WebView2. You can check a guide on how to fix that in the docs!")
|
||||
.set_buttons(MessageButtons::OkCancel)
|
||||
.set_level(MessageLevel::Error)
|
||||
.show();
|
||||
if confirm == MessageDialogResult::Ok {
|
||||
open::that("https://docs.slimevr.dev/common-issues.html#webview2-is-missing--slimevr-gui-crashes-immediately--panicked-at--webview2error").unwrap();
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("tauri build error {}", error);
|
||||
show_error(&error.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||