Compare commits

..

1 Commits

Author SHA1 Message Date
Louka
472f6d8fc0 add accessories wip 2023-07-29 02:19:08 -04:00
814 changed files with 50224 additions and 83524 deletions

10
.envrc
View File

@@ -1,9 +1 @@
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 package.json
if ! use flake . --impure
then
echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
fi
use flake

5
.gitattributes vendored
View File

@@ -1,5 +0,0 @@
* text=auto
*.png binary
*.webp binary
*.gif binary

67
.github/CODEOWNERS vendored
View File

@@ -1,37 +1,30 @@
# Global code owner
* @Eirenliel
# Make everyone be able to approve SolarXR submodule changes
/solarxr-protocol @ButterscotchV @Erimelowo @loucass003
# Make Loucass the owner of all GUI stuff
/gui/ @loucass003
/pnpm-lock.yaml @loucass003
/pnpm-workspace.yaml @loucass003
# loucass003 and Erimel responsible for i18n
/gui/public/i18n/ @loucass003 @Erimelowo @ImSapphire
/gui/src/i18n/ @loucass003 @Erimelowo
/l10n.toml @loucass003 @Erimelowo
/gui/src/components/settings/ @Erimelowo @loucass003
# Rust part of the GUI
/gui/electron/ @loucass003
# Some server code~
/server/ @ButterscotchV @Eirenliel @Erimelowo
/server/src/main/java/dev/slimevr/autobone/ @ButterscotchV
/server/src/main/java/dev/slimevr/poserecorder/ @ButterscotchV
/server/src/main/java/dev/slimevr/posestreamer/ @ButterscotchV
/server/src/main/java/dev/slimevr/osc/ @Erimelowo
/server/src/main/java/dev/slimevr/tracking/processor/ @Erimelowo
/server/src/main/java/dev/slimevr/filtering/ @Erimelowo
# Linux files
*.nix @loucass003
/flake.lock @loucass003
/dev.slimevr.SlimeVR.metainfo.xml @loucass003
/.envrc @loucass003
# Global code owner
* @Eirenliel
# Make Loucas and Uriel the owners of all GUI stuff
/gui/ @loucass003 @ImUrX
/package-lock.json @loucass003 @ImUrX
# Uriel and Erimel responsible for i18n
/gui/public/i18n/ @ImUrX @Louka3000
/gui/src/i18n/ @ImUrX @Louka3000
/l10n.toml @ImUrX @Louka3000
/gui/src/components/settings/ @Louka3000 @loucass003
# Rust part of the GUI
/gui/src-tauri/ @ImUrX @TheButlah
/Cargo.lock @ImUrX @TheButlah
# Some server code~
/server/ @ButterscotchV @Eirenliel @Louka3000
/server/src/main/java/dev/slimevr/autobone/ @ButterscotchV
/server/src/main/java/dev/slimevr/poserecorder/ @ButterscotchV
/server/src/main/java/dev/slimevr/posestreamer/ @ButterscotchV
/server/src/main/java/dev/slimevr/osc/ @Louka3000
/server/src/main/java/dev/slimevr/tracking/processor/ @Louka3000
/server/src/main/java/dev/slimevr/filtering/ @Louka3000
server/src/main/java/dev/slimevr/config/ @loucass003

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: SlimeVR

View File

@@ -1,8 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"

37
.github/labeler.yml vendored
View File

@@ -1,37 +0,0 @@
"Area: Continuous Integration":
- changed-files:
- any-glob-to-any-file: ".github/**"
"Area: Application Protocol":
- changed-files:
- any-glob-to-any-file: ["solarxr-protocol"]
"Area: AutoBone":
- changed-files:
- any-glob-to-any-file: "server/core/src/main/java/dev/slimevr/autobone/**"
"Area: Documentation":
- changed-files:
- any-glob-to-any-file: "**/*.md"
"Area: GUI":
- all:
- changed-files:
- all-globs-to-any-file: ["gui/**/*", "!gui/public/i18n/**/*"]
"Area: Hardware Protocol":
- changed-files:
- any-glob-to-any-file: "server/core/src/main/java/dev/slimevr/tracking/trackers/udp/**"
"Area: Server":
- changed-files:
- any-glob-to-any-file: ["server/**", "*gradle*", "gradle/**"]
"Area: Skeletal Model":
- changed-files:
- any-glob-to-any-file: "server/core/src/main/java/dev/slimevr/tracking/processor/**"
"Area: SteamVR Driver":
- changed-files:
- any-glob-to-any-file: "server/desktop/src/main/java/dev/slimevr/desktop/platform/**"
"Area: Translation":
- changed-files:
- any-glob-to-any-file: "gui/public/i18n/**"
"Area: VMC":
- changed-files:
- any-glob-to-any-file: ["server/core/src/main/java/dev/slimevr/osc/Unity*", "server/core/src/main/java/dev/slimevr/osc/VMC*", "server/core/src/main/java/dev/slimevr/osc/VRM*"]
"Area: VRCOSC":
- changed-files:
- any-glob-to-any-file: ["server/core/src/main/java/dev/slimevr/osc/VRC*"]

110
.github/workflows/build-gui.yml vendored Normal file
View File

@@ -0,0 +1,110 @@
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@v3
with:
submodules: recursive
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'npm'
- name: Build
run: |
npm ci
cd gui
npm run lint
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
env:
# Don't mark warnings as errors
CI: false
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- if: matrix.os == 'ubuntu-20.04'
name: Set up Linux dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
# Increment to invalidate the cache
version: 1.0
# 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: Cache cargo dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "${{ matrix.os }}"
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'npm'
- name: Build
run: |
npm ci
npm run skipbundler
- if: matrix.os == 'windows-latest'
name: Upload a Build Artifact (Windows)
uses: actions/upload-artifact@v3
with:
# Artifact name
name: SlimeVR-GUI-Windows
# A file, directory or wildcard pattern that describes what to upload
path: target/release/slimevr.exe
- if: matrix.os == 'ubuntu-20.04'
name: Upload a Build Artifact (Linux)
uses: actions/upload-artifact@v3
with:
# Artifact name
name: SlimeVR-GUI-Linux
# 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@v3
with:
# Artifact name
name: SlimeVR-GUI-macOS
# A file, directory or wildcard pattern that describes what to upload
path: target/release/slimevr

View File

@@ -1,291 +0,0 @@
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: Get tags
run: git fetch --tags origin --recurse-submodules=no --force
- 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: 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 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
server/core/resources
build-gui-frontend:
name: Build GUI Assets
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no --force
- 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: Get tags
run: git fetch --tags origin --recurse-submodules=no --force
- 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
- 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/*.zip "$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: 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: 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
- 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/*
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-*
path: release-out
merge-multiple: true
- name: Download Server Jar
uses: actions/download-artifact@v6
with:
name: server-jar
path: server
- 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

View File

@@ -1,34 +0,0 @@
# This workflow will build the update manifest for the updater and update a GitHub release
name: Generate update manifest
on:
workflow_dispatch:
release:
types: [released]
jobs:
generate-manifest:
runs-on: ubuntu-22.04
steps:
- uses: actions/setup-node@v6
with:
node-version: '22.x'
- name: Generate update-manifest.json
run: |
npx @slimevr/update-manifest-generator@latest
- uses: actions/upload-artifact@v6
with:
name: "update-manifest.json"
path: ./update-manifest.json
- name: Upload update-manifest.json to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ secrets.UPDATE_MANIFEST_RELEASE_TAG }}
files: ./update-manifest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

291
.github/workflows/gradle.yaml vendored Normal file
View File

@@ -0,0 +1,291 @@
# 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@v3
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "adopt"
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- 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@v3
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "adopt"
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Build with Gradle
run: ./gradlew shadowJar
- name: Upload the Server JAR as a Build Artifact
uses: actions/upload-artifact@v3
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/*
- name: Upload to draft release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true
generate_release_notes: true
files: |
server/desktop/build/libs/*
bundle-linux:
runs-on: ubuntu-20.04
needs: [build, test]
if: contains(fromJSON('["workflow_dispatch", "create"]'), github.event_name)
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/download-artifact@v3
with:
name: "SlimeVR-Server"
path: server/desktop/build/libs/
- name: Set up Linux dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libfuse2
# Increment to invalidate the cache
version: 1.0
# 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: Cache cargo dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "ubuntu-20.04"
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'npm'
- name: Build
run: |
npm ci
npm run tauri build
- uses: actions/upload-artifact@v3.1.0
with:
name: SlimeVR-GUI-Deb
path: target/release/bundle/deb/slimevr*.deb
- name: Install appimage-builder
run: |
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod a+x appimagetool-x86_64.AppImage
sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool
- name: Modify and Build AppImage
run: |
cd target/release/bundle/appimage
chmod a+x slimevr*.AppImage
./slimevr*.AppImage --appimage-extract
cp $( git rev-parse --show-toplevel )/server/desktop/build/libs/slimevr.jar squashfs-root/slimevr.jar
chmod 644 squashfs-root/slimevr.jar
appimagetool squashfs-root slimevr*.AppImage
- uses: actions/upload-artifact@v3.1.0
with:
name: SlimeVR-GUI-AppImage
path: target/release/bundle/appimage/slimevr*.AppImage
- name: Prepare for release
if: startsWith(github.ref, 'refs/tags/')
run: |
cp target/release/bundle/appimage/slimevr*.AppImage ./SlimeVR-amd64.appimage
cp target/release/bundle/deb/slimevr*.deb ./SlimeVR-amd64.deb
- name: Upload to draft release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true
generate_release_notes: true
files: |
./SlimeVR-amd64.appimage
./SlimeVR-amd64.deb
bundle-mac:
runs-on: macos-latest
needs: [build, test]
if: contains(fromJSON('["workflow_dispatch", "create"]'), github.event_name)
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/download-artifact@v3
with:
name: "SlimeVR-Server"
path: server/desktop/build/libs/
- name: Cache cargo dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "macos-latest"
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'npm'
- name: Build
run: |
npm ci
npm run tauri build -- "--target universal-apple-darwin"
- name: Modify Application
run: |
cd target/release/bundle/macos/slimevr.app/Contents/MacOS
cp $( git rev-parse --show-toplevel )/server/desktop/build/libs/slimevr.jar ./
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@v3.1.0
with:
name: SlimeVR-GUI-MacApp
path: target/release/bundle/macos/slimevr*.app
- uses: actions/upload-artifact@v3.1.0
with:
name: SlimeVR-GUI-MacDmg
path: target/release/bundle/dmg/slimevr.dmg
- name: Prepare for release
if: startsWith(github.ref, 'refs/tags/')
run: |
cp target/release/bundle/dmg/slimevr.dmg ./SlimeVR-mac.dmg
- name: Upload to draft release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true
generate_release_notes: true
files: |
./SlimeVR-mac.dmg
bundle-windows:
runs-on: windows-latest
needs: [build, test]
if: contains(fromJSON('["workflow_dispatch", "create"]'), github.event_name)
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/download-artifact@v3
with:
name: "SlimeVR-Server"
path: server/desktop/build/libs/
- name: Cache cargo dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "windows-latest"
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'npm'
- name: Build
run: |
npm ci
npm run skipbundler
- 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-win64.zip ./SlimeVR/
mv ./SlimeVR/slimevr.exe ./SlimeVR/slimevr-ui.exe
7z a -tzip SlimeVR.zip ./SlimeVR/
- uses: actions/upload-artifact@v3.1.0
with:
name: SlimeVR-GUI-Windows
path: ./SlimeVR*.zip
- name: Upload to draft release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true
generate_release_notes: true
files: |
./SlimeVR-win64.zip
./SlimeVR.zip

View File

@@ -1,22 +0,0 @@
# This workflow will triage pull requests and apply a label based on the
# paths that are modified in the pull request.
#
# To use this workflow, you will need to set up a .github/labeler.yml
# file with configuration. For more information, see:
# https://github.com/actions/labeler
name: Labeler
on: [pull_request_target]
jobs:
label:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@v6
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -3,19 +3,22 @@ on:
push:
branches:
- pontoon
jobs:
pull_request:
if: ${{ github.repository == 'SlimeVR/SlimeVR-Server' }}
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
with:
submodules: recursive
- name: pull-request
env:
GH_TOKEN: ${{ secrets.PONTOON_BOT_KEY }}
run: |
gh_pr_up() { gh pr create "$@" --label "Area: Translation" --base main || gh pr edit "$@"; }
gh_pr_up --title "New Pontoon translations" --body "Please don't squash me 🥺"
- uses: repo-sync/pull-request@v2
with:
destination_branch: "main"
pr_title: "New Pontoon translations"
pr_body: "Please don't squash me 🥺"
pr_label: "Area: Translation"
github_token: ${{ secrets.PONTOON_BOT_KEY }}

View File

@@ -15,7 +15,7 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
with:
ref: pontoon
submodules: recursive
@@ -24,7 +24,7 @@ jobs:
- name: Rebase
run: |
git config --local user.name "slimevr-bot"
git config --local user.email 'pantoon@slimevr.dev'
git config --local user.email pantoon@slimevr.dev
git fetch origin main
git rebase origin/main
git submodule update
@@ -36,4 +36,3 @@ jobs:
commit-message: "update"
force-push: "true"
name: "slimevr-bot"
email: "pantoon@slimevr.dev"

14
.gitignore vendored
View File

@@ -17,7 +17,6 @@
# VSCode stuff
/.vscode/settings.json
/.vscode/launch.json
# Ignore eclipse stuff
.project
@@ -28,22 +27,15 @@
/node_modules
.husky
# kotlin stuff
/.kotlin
# ignore gradle build folder
build/
# Rust build artifacts
/target
# direnv has been claimed for Nix usage
.direnv/
.devenv
# Ignore Android local properties
local.properties
# Ignore temporary config
vrconfig.yml.tmp
# Nixos
.bin/

View File

@@ -1,33 +0,0 @@
YELLOW="\033[1;33m"
GREEN="\033[1;32m"
RESET="\033[0m"
if git rev-parse -q --verify MERGE_HEAD; then
echo -e "${YELLOW}Skipping precommit hook because of merge${RESET}"
exit 0
fi
APP_PRE_COMMIT_OPTIONS="$(dirname "$0")/_/pre-commit.options"
if ! [ -f "$APP_PRE_COMMIT_OPTIONS" ]; then
echo -e "${YELLOW}\nSkipping pre-commit hook."
echo -e "If you want to use pre-commit for lint-staged, run:\n"
echo -e " ${GREEN}echo -e 'APP_LINT=true;' > ${APP_PRE_COMMIT_OPTIONS}${RESET}"
echo -e "${YELLOW}\nIt will add some delay before committing!\n${RESET}"
exit 0
fi
source $APP_PRE_COMMIT_OPTIONS
if [ -n "${APP_LINT}" ] && [ "${APP_LINT}" == "true" ]; then
echo -e "${GREEN}[husky] [pre-commit] [lint-staged]${RESET}"
case "$(uname -sr)" in
CYGWIN*|MINGW*|MINGW32*|MSYS*)
npx.cmd lint-staged
;;
*)
npx lint-staged
;;
esac
fi

View File

@@ -1,9 +0,0 @@
export default {
'server/**/*.{java,kt,kts}': (filenames) =>
filenames.map(
(filename) =>
`./gradlew${
process.platform === 'win32' ? '.bat' : ''
} spotlessApply "-PspotlessIdeHook=${filename}"`
),
};

View File

@@ -1 +1 @@
22.17.0
18.12.1

1
.npmrc
View File

@@ -1 +0,0 @@
update-notifier=false

View File

@@ -7,10 +7,10 @@
"gaborv.flatbuffers",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"rust-lang.rust-analyzer",
"bradlc.vscode-tailwindcss",
"EditorConfig.EditorConfig",
"macabeus.vscode-fluent",
"redhat.vscode-yaml"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []

View File

@@ -6,7 +6,9 @@ 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)
- [Node.js v16+](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.
@@ -30,13 +32,12 @@ 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.)
### Electron (gui)
### Tauri (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 package:build`. The result
will be at `dist/artifacts/` content will change depending of the platform.
- Run `npm install` in your IDE's terminal to download and install dependencies.
- To launch the GUI in dev mode, run `npm run gui`.
- Finally, to compile for production, run `npm run tauri build`. The result
will be at `target/release/slimevr.exe`.
## Code style
@@ -82,12 +83,12 @@ 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.
### Electron (gui)
### Tauri (gui)
We use ESLint and Prettier to format GUI code.
- First, go into the GUI's directory with your terminal by running `cd gui`.
- To check code formatting, run `pnpm run lint`.
- To fix code formatting, run `pnpm run lint:fix` and `pnpm run format`
- To check code formatting, run `npm run lint`.
- To fix code formatting, run `npm run lint:fix` and `npm run format`
Don't forget to run `cd ..` to return to the root directory.
@@ -103,7 +104,6 @@ When touching SolarXR:
- After editing files, you should run `cd solarxr-protocol`, then either run
`./generate-flatbuffer.ps1` (Windows) or `./generate-flatbuffer.sh` (Linux/OSX)
- Make sure to commit your changes inside the submodule.
- To make sure the gui use the latest generated code, run `pnpm i`.
## Code Licensing
SlimeVR uses dual MIT and Apache-2.0 license. Be sure that any code that you reference,
@@ -114,9 +114,3 @@ licensed under `GPL-v3`.
## Discord
We use discord *a lot* to coordinate and discuss development. Come join us at
https://discord.gg/SlimeVR!
## Use of AI
We DO NOT accept contributions that are generated with AI (for example, "vibe-coding").
If you do use AI, and you believe your usage of AI is reasonable, you must clearly disclose
how you used AI in your submission.

4221
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[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.65" # This version stabilized GATs and let-else
repository = "https://github.com/SlimeVR/SlimeVR-Server"
[profile.release]
lto = "thin"
strip = "debuginfo" # Only affects Unix binaries with DWARF

View File

@@ -47,11 +47,6 @@ on SlimeVR, you need to be aware of them:**
Please refer to the [LICENSE-MIT] and [LICENSE-APACHE] files if you are at any point
uncertain what the exact requirements are.
## Trademark and Logo use
**SlimeVR is a trademark or a registered trademark of SlimeVR B.V. Usage of SlimeVR software, hardware, or other intellectual property in this or other repositories does not grant you the right to use SlimeVR trademark as your own.**
For more information, please refer to the [TRADEMARK].
## Contributions
Any contributions submitted for inclusion in this repository will be dual-licensed under
either:
@@ -69,7 +64,3 @@ okay with this and that you are authorized to provide the above licenses.
[LICENSE-MIT]: LICENSE-MIT
[LICENSE-APACHE]: LICENSE-APACHE
[TRADEMARK]: TRADEMARK.md
*if you read this, u cute*

View File

@@ -1,33 +0,0 @@
## SlimeVR is a trademark or a registered trademark of SlimeVR B.V.
**Usage of SlimeVR software, hardware, or other intellectual property in this or other repositories does not grant you the right to use SlimeVR trademark as your own.**
The purpose of a trademark is to remove uncertainty for users and customers regarding the product's manufacturer or endorsement. You're not allowed to market your product using SlimeVR name, and your usage of the name should be only factual and descriptive. For example, calling original SlimeVR products SlimeVR or describing compatibility of other products or derivatives. This applies to all products, including software, and hardware including non-official Full-Body Trackers.
**Here are a few _acceptable_ uses of SlimeVR name when selling unofficial Slime trackers:**
* SlimeVR-compatible trackers
* Unofficial SlimeVR trackers / Non-official SlimeVR trackers
* DIY SlimeVR trackers
* Third-party SlimeVR Trackers
* Custom SlimeVR-compatible trackers
* < Your Brand > Slime Trackers
* Using "SlimeVR" as a search tag
**_Unacceptable_ uses include, but are not limited to:**
* SlimeVR store
* Buy SlimeVR
* SlimeVR Trackers
* Original SlimeVR
* Official SlimeVR
* SlimeVR BMI270 (or any other IMU model along with SlimeVR name)
* < Your brand > SlimeVR / < your brand > SlimeVR Trackers
Use of the SlimeVR name that can cause confusion is not allowed in any part of the listing, including, but not limited to: product title, product description, product metadata, site title, site name, site metadata, site texts, social media posts, or other advertisement.
Also, please ensure you use the correct spelling and capitalization: only **"SlimeVR" is acceptable**. Not "Slimevr", "slimevr", or "Slime VR". You're allowed to use the word "slime" as you wish, it's not a trademark.
Please understand that we have an obligation to reduce confusion for the customers, and we believe that our usage terms are generous compared to many other companies and products. This applies to all sellers or derivative products, we do not make exceptions.
---
If you have any questions about SlimeVR trademark or copyrighted materials, you can reach out to us at *tm[at]slimevr.dev*.

View File

@@ -1,3 +1,3 @@
plugins {
id("org.ajoberstar.grgit")
id("org.ajoberstar.grgit") version "5.2.0"
}

View File

@@ -0,0 +1,84 @@
<?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>An app for facilitating full-body tracking in virtual reality</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="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">#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>
<screenshots>
<screenshot type="default">
<caption>The onboarding for the GUI</caption>
<image>https://raw.githubusercontent.com/SlimeVR/SlimeVR-Server/main/assets/img/onboarding.png</image>
</screenshot>
</screenshots>
<releases>
<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>

118
flake.lock generated
View File

@@ -1,58 +1,126 @@
{
"nodes": {
"flake-parts": {
"flake-utils": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
"systems": "systems"
},
"locked": {
"lastModified": 1762980239,
"narHash": "sha256-8oNVE8TrD19ulHinjaqONf9QWCKK+w4url56cdStMpM=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "52a2caecc898d0b46b2b905f058ccc5081f842da",
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1756787288,
"narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=",
"lastModified": 1686412476,
"narHash": "sha256-inl9SVk6o5h75XKC79qrDCAobTD1Jxh6kVYTZKHzewA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1",
"rev": "21951114383770f96ae528d0ae68824557768e81",
"type": "github"
},
"original": {
"owner": "NixOS",
"id": "nixpkgs",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
"type": "indirect"
}
},
"nixpkgs-lib": {
"nixpkgs_2": {
"locked": {
"lastModified": 1761765539,
"narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1686364106,
"narHash": "sha256-h4gCQg+jizmAbdg6UPlhxQVk4A7Ar/zoLa0wx3wBya0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "ba011dd1c5028dbb880bc3b0f427e0ff689e6203",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},

125
flake.nix
View File

@@ -1,50 +1,87 @@
{
description = "SlimeVR Server & GUI";
description = "Server app for SlimeVR ecosystem";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
};
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = inputs@{ self, nixpkgs, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" ];
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
perSystem = { pkgs, ... }:
let
runtimeLibs = pkgs: (with pkgs; [
jdk17
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
wineWow64Packages.stable
zlib squashfsTools fakeroot libarchive icu
nodejs_22 pnpm pkg-config python3 gcc gnumake binutils git
pkgs.nodePackages.node-gyp-build
]);
slimeShell = pkgs.buildFHSEnv {
name = "slimevr-env";
targetPkgs = runtimeLibs;
profile = ''
export JAVA_HOME=${pkgs.jdk17}
export PATH="${pkgs.jdk17}/bin:$PATH"
# 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;
outputs = {
self,
nixpkgs,
flake-utils,
rust-overlay,
}:
flake-utils.lib.eachDefaultSystem
(
system: let
overlays = [(import rust-overlay)];
pkgs = import nixpkgs {
inherit system overlays;
};
};
rustTarget = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
nativeBuildInputs = with pkgs; [
curl
gcc
openssl
pkgconfig
which
zlib
freetype
expat
];
buildInputs = with pkgs; [
appimagekit
atk
cairo
dbus
dbus.lib
dprint
gdk-pixbuf
glib.out
gobject-introspection
gtk3
harfbuzz
libayatana-appindicator-gtk3
libffi
libsoup
openssl.out
pango
pkg-config
treefmt
webkitgtk
zlib
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
# Some nice things to have
exa
fd
jdk17 # JDK17
nodejs
gradle
];
in {
devShells.default = pkgs.mkShell {
nativeBuildInputs =
nativeBuildInputs
++ [
];
buildInputs =
buildInputs
++ [
rustTarget
];
shellHook = ''
alias ls=exa
alias find=fd
'';
};
}
);
}

View File

@@ -13,10 +13,7 @@ android.useAndroidX=true
android.nonTransitiveRClass=true
org.gradle.unsafe.configuration-cache=false
kotlinVersion=2.3.10
spotlessVersion=8.2.1
shadowJarVersion=9.3.1
buildconfigVersion=6.0.7
# We should probably stop using grgit, see:
# https://andrewoberstar.com/posts/2024-04-02-dont-commit-to-grgit/
grgitVersion=5.3.3
kotlinVersion=1.9.0-RC
spotlessVersion=6.12.0
shadowJarVersion=8.1.1
buildconfigVersion=3.1.0

Binary file not shown.

View File

@@ -1,8 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
distributionSha256Sum=f1771298a70f6db5a29daf62378c4e18a17fc33c9ba6b14362e0cdf40610380d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

34
gradlew vendored
View File

@@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -85,9 +83,10 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -134,13 +133,10 @@ location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@@ -148,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -156,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -201,15 +197,11 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored Executable file → Normal file
View File

@@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
@@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail

View File

@@ -1,8 +0,0 @@
VITE_FIRMWARE_TOOL_URL=https://fw-tool-api-v2.slimevr.io
VITE_FIRMWARE_TOOL_S3_URL=https://fw-tool-bucket-v2.slimevr.io
FIRMWARE_TOOL_SCHEMA_URL=https://fw-tool-api-v2.slimevr.io/api-json
# VITE_FIRMWARE_TOOL_URL=http://localhost:3000
# VITE_FIRMWARE_TOOL_S3_URL=http://localhost:9099
# FIRMWARE_TOOL_SCHEMA_URL=http://localhost:3000/api-json

43
gui/.eslintrc.json Normal file
View File

@@ -0,0 +1,43 @@
{
"env": {
"browser": true,
"es2021": true,
"jest": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react-hooks", "@typescript-eslint"],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"spaced-comment": "error",
"quotes": ["error", "single"],
"no-duplicate-imports": "error",
"no-inline-styles": "off",
"@typescript-eslint/no-explicit-any": "off",
"react/no-unescaped-entities": "off",
"camelcase": "error",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
]
},
"settings": {
"import/resolver": {
"typescript": {}
},
"react": {
"version": "detect"
}
}
}

11
gui/.gitignore vendored
View File

@@ -28,14 +28,3 @@ yarn-error.log*
# vite
/dist
/stats.html
vite.config.ts.timestamp*
electron.vite.config.*.mjs
# eslint
.eslintcache
# Sentry Config File
.env.sentry-build-plugin
# electron
out/

View File

@@ -1,5 +0,0 @@
export default {
'**/*.{ts,tsx}': () => 'tsc -p tsconfig.json --noEmit',
'src/**/*.{js,jsx,ts,tsx}': 'eslint --max-warnings=0 --no-warn-ignored --cache --fix',
'**/*.{js,jsx,ts,tsx,css,scss,md,json}': 'prettier --write',
};

View File

@@ -1,70 +0,0 @@
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: zip
icon: "./electron/resources/icons/icon.ico"
extraFiles:
- from: "../server/desktop/build/libs/slimevr.jar"
to: "."
- from: "../server/core/resources"
to: "."
filter: ["**/*"]
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: "Resources/slimevr.jar"

View File

@@ -1,47 +0,0 @@
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')
}
}
}
})

View File

@@ -1 +0,0 @@
resources/java-version/JavaVersion.class

View File

@@ -1,12 +0,0 @@
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();

View File

@@ -1,477 +0,0 @@
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/Square30x30Logo.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 { closeLogger, 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';
// Fixes colors looking washed on linux
// Might affect hdr
if (process.platform === 'linux') {
app.commandLine.appendSwitch('disable-features', 'WaylandWpColorManagerV1');
app.commandLine.appendSwitch('force-color-profile', 'srgb');
}
app.setPath('userData', getGuiDataFolder());
app.setPath('sessionData', join(getGuiDataFolder(), 'electron'));
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 'hide':
mainWindow?.hide();
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.quit();
}
};
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.quit()
return;
}
logger.info({ javaBin, serverJar }, 'Found Java and server jar');
const platform = getPlatform();
const serverWorkdir = getServerDataFolder()
const serverProcess = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run'], {
cwd: serverWorkdir,
shell: false,
env:
platform === 'windows'
? {
...process.env,
APPDATA: app.getPath('appData'),
LOCALAPPDATA: process.env['USERPROFILE'] ? path.join(process.env['USERPROFILE'], 'AppData', 'Local') : undefined,
}
: undefined,
});
const sendToWindow = (event: ServerStatusEvent) => {
if (mainWindow && !mainWindow.webContents.isDestroyed()) {
mainWindow.webContents.send(IPC_CHANNELS.SERVER_STATUS, event);
}
};
serverProcess.stdout?.on('data', (message) => {
sendToWindow({ message: message.toString(), type: 'stdout' });
});
serverProcess.stderr?.on('data', (message) => {
sendToWindow({ message: message.toString(), type: 'stderr' });
});
serverProcess.on('error', (err) => {
logger.info({ err }, 'Error launching the java server');
if (!isQuitting) app.quit();
})
serverProcess.on('exit', () => {
logger.info('Server process exiting');
})
const exited = new Promise<void>((resolve) => serverProcess.once('exit', resolve));
return {
process: serverProcess,
close: () => serverProcess.kill(),
waitForExit: () => exited,
};
};
let isQuitting = false;
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', () => {
app.quit();
});
app.on('before-quit', async (event) => {
if (isQuitting) return;
isQuitting = true;
event.preventDefault();
logger.info('App quitting, saving...');
server?.close();
await server?.waitForExit();
stores.settings.save();
stores.cache.save();
discordPresence.destroy();
await saveWindowState();
await closeLogger();
app.exit(0);
});
});

View File

@@ -1,34 +0,0 @@
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);
export const closeLogger = () =>
new Promise<void>((resolve) => {
logger.flush(() => {
transport.once('close', resolve);
transport.end();
});
});

View File

@@ -1,120 +0,0 @@
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,
app.isPackaged ? path.resolve(process.resourcesPath) : 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));
};

View File

@@ -1,49 +0,0 @@
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();

View File

@@ -1,76 +0,0 @@
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),
};

View File

@@ -1,50 +0,0 @@
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);
});
}

View File

@@ -1,40 +0,0 @@
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'),
hide: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'hide'),
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);

View File

@@ -1,65 +0,0 @@
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;
hide: () => 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;
}
}

View File

@@ -1,46 +0,0 @@
# Copyright 2025 Eiren Rain and SlimeVR Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
## QinHeng
# CH340
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="7522", MODE="0660", TAG+="uaccess"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="7523", MODE="0660", TAG+="uaccess"
# CH341
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="5523", MODE="0660", TAG+="uaccess"
# CH343
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="55D3", MODE="0660", TAG+="uaccess"
# CH9102x
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1A86", ATTRS{idProduct}=="55D4", MODE="0660", TAG+="uaccess"
## Silabs
# CP210x
SUBSYSTEMS=="usb", ATTRS{idVendor}=="10C4", ATTRS{idProduct}=="EA60", MODE="0660", TAG+="uaccess"
## Espressif
# ESP32-S3 / ESP32-C3 / ESP32-C5 / ESP32-C6 / ESP32-C61 / ESP32-H2 / ESP32-P4
SUBSYSTEMS=="usb", ATTRS{idVendor}=="303A", ATTRS{idProduct}=="1001", MODE="0660", TAG+="uaccess"
# ESP32-S2
SUBSYSTEMS=="usb", ATTRS{idVendor}=="303A", ATTRS{idProduct}=="0002", MODE="0660", TAG+="uaccess"
## FTDI
# FT232BM/L/Q, FT245BM/L/Q
# FT232RL/Q, FT245RL/Q
# VNC1L with VDPS Firmware
# VNC2 with FT232Slave
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0660", TAG+="uaccess"
## SlimeVR
# smol slime dongle
SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="7690", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="7690", MODE="0660", TAG+="uaccess"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-miterlimit="10" clip-rule="evenodd" version="1.1" viewBox="0 0 380 380" xml:space="preserve"><rect id="bg" width="380" height="380" fill="#663499" stroke-width="1" rx="76" /><g id="logo" fill="none" stroke="#fff"><path id="left" stroke-width="13.62" d="m72.867 191.74 37-39 39 36"/><path id="right" stroke-width="13.62" d="m208.87 187.74 38-35 36 38"/><path id="outer" stroke-linecap="square" stroke-width="17" d="m56.867 253.74s130.61-31.182 248 5c13.45 4.146 20.244 2.975 20-8s1.909-126.06-46-131"/></g></svg>

Before

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,26 +0,0 @@
#!/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

View File

@@ -1,8 +0,0 @@
#!/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

View File

@@ -1,49 +0,0 @@
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' | 'hide') => 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;
}

View File

@@ -1,82 +0,0 @@
import { FlatCompat } from '@eslint/eslintrc';
import eslint from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import stylistic from '@stylistic/eslint-plugin';
const compat = new FlatCompat();
export const gui = [
eslint.configs.recommended,
...tseslint.configs.recommended,
...compat.extends('plugin:@dword-design/import-alias/recommended'),
...compat.plugins('eslint-plugin-react-hooks'),
// Add import-alias rule inside compat because plugin doesn't like flat configs
...compat.config({
rules: {
'@dword-design/import-alias/prefer-alias': [
'error',
{
alias: {
'@': './src/',
},
},
],
},
}),
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: tseslint.parser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
globals: {
...globals.browser,
...globals.jest,
},
},
files: ['src/**/*.{js,jsx,ts,tsx,json}'],
plugins: {
'@typescript-eslint': tseslint.plugin,
'@stylistic': stylistic,
},
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'spaced-comment': 'error',
quotes: ['error', 'single'],
'no-duplicate-imports': 'error',
'no-inline-styles': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'react/no-unescaped-entities': 'off',
camelcase: 'error',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@stylistic/jsx-self-closing-comp': 'error',
},
settings: {
'import/resolver': {
typescript: {},
},
react: {
version: 'detect',
},
},
},
// Global ignore
{
ignores: ['**/firmware-tool-api/'],
},
];
export default gui;

View File

@@ -10,7 +10,7 @@
<link rel="manifest" href="/manifest.json" />
<title>SlimeVR GUI</title>
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,28 +0,0 @@
import {
generateSchemaTypes,
generateReactQueryComponents,
} from '@openapi-codegen/typescript';
import { defineConfig } from '@openapi-codegen/cli';
import dotenv from 'dotenv';
dotenv.config()
export default defineConfig({
firmwareTool: {
from: {
source: 'url',
url: process.env.FIRMWARE_TOOL_SCHEMA_URL ?? 'http://localhost:3000/api-json',
},
outputDir: 'src/firmware-tool-api',
to: async (context) => {
const filenamePrefix = 'firmwareTool';
const { schemasFiles } = await generateSchemaTypes(context, {
filenamePrefix,
});
await generateReactQueryComponents(context, {
filenamePrefix,
schemasFiles,
});
},
},
});

View File

@@ -1,113 +1,94 @@
{
"name": "slimevr",
"version": "0.0.0",
"author": "SlimeVR Team <contact@slimevr.dev>",
"homepage": "https://slimevr.dev",
"type": "module",
"name": "slimevr-ui",
"version": "0.5.1",
"private": true,
"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"
"@fluent/bundle": "^0.17.1",
"@fluent/react": "^0.14.1",
"@fontsource/poppins": "^4.5.8",
"@formatjs/intl-localematcher": "^0.2.32",
"@react-three/fiber": "^8.10.0",
"@tauri-apps/api": "^1.4.0",
"@vitejs/plugin-react": "^3.0.0",
"browserslist": "^4.18.1",
"classnames": "^2.3.1",
"eslint-config-react-app": "^7.0.0",
"file-saver": "^2.0.5",
"flatbuffers": "^22.10.26",
"identity-obj-proxy": "^3.0.0",
"intl-pluralrules": "^1.3.1",
"ip-num": "^1.4.1",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.0.1",
"prompts": "^2.4.2",
"react": "^18.0.0",
"react-dev-utils": "^12.0.0",
"react-dom": "^18.0.0",
"react-hook-form": "^7.29.0",
"react-modal": "3.15.1",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.2.2",
"semver": "^7.5.3",
"solarxr-protocol": "file:../solarxr-protocol",
"three": "^0.148.0",
"ts-pattern": "^5.0.1",
"typescript": "^5.1.6"
},
"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"
"build": "vite build",
"dev": "tauri dev",
"skipbundler": "tauri build -b none",
"tauri": "tauri",
"lint": "eslint --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && prettier --check \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
"lint:fix": "eslint --fix \"src/**/*.{js,jsx,ts,tsx,json}\"",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
"preview-vite": "vite preview",
"javaversion-build": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"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",
"@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",
"@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",
"@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11",
"@types/react-modal": "3.16.3",
"@types/semver": "^7.5.8",
"@types/three": "^0.163.0",
"@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",
"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",
"use-double-tap": "^1.3.6",
"uuid": "^13.0.0",
"vite": "^5.4.8",
"yup": "^1.4.0"
},
"main": "./out/main/index.js"
"@tailwindcss/forms": "^0.5.3",
"@tauri-apps/cli": "^1.4.0",
"@types/file-saver": "^2.0.5",
"@types/react": "18.0.25",
"@types/react-dom": "^18.0.5",
"@types/react-modal": "3.13.1",
"@types/three": "^0.148.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"autoprefixer": "^10.4.4",
"cross-env": "^7.0.3",
"eslint": "^8.44.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.24",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"rollup-plugin-visualizer": "^5.9.2",
"tailwind-gradient-mask-image": "^1.0.0",
"tailwindcss": "^3.3.2",
"vite": "^4.3.9"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,13 +7,8 @@
## Websocket (server) status
websocket-connecting = جاري التحميل...
websocket-connection_lost = تعطل الخادم!
websocket-connection_lost-desc = يبدو أن خادم SlimeVR تعطل. تحقق من السجلات وأعد تشغيل البرنامج
websocket-timedout = تعذر الاتصال بالخادم
websocket-timedout-desc = يبدو أن خادم SlimeVR قد تعطل أو انتهت مهلته. تحقق من السجلات وأعد تشغيل البرنامج
websocket-error-close = الخروج من SlimeVR
websocket-error-logs = افتح مجلد السجلات
websocket-connecting = جاري التوصيل بالسيرفر
websocket-connection_lost = انقطع الاتصال بالسيرفر. يتم إعادة التوصيل...
## Update notification
@@ -28,11 +23,6 @@ tips-find_tracker = لست متأكد من أجهزة التعقب؟ قم بتح
tips-do_not_move_heels = يرجى عدم تحريك كاحليك أثناء التسجيل!
tips-file_select = اسحب الملفات وأفلتها لاستخدامها أو <u> تصفح </ u>
tips-tap_setup = يمكنك النقر ببطء مرتين على جهاز التعقب لاختياره بدلاً من تحديده من القائمة.
tips-turn_on_tracker = هل تستخدم أجهزة تعقب SlimeVR الرسمية؟ تذكر <b><em> أن تشغل أجهزة التعقب </em></b> بعد توصيلها بالكمبيوتر!
tips-failed_webgl = فشل تهيئة WebGL.
## Units
## Body parts
@@ -57,72 +47,12 @@ body_part-LEFT_HAND = اليد اليسرى
body_part-LEFT_UPPER_LEG = الفخذ الأيسر
body_part-LEFT_LOWER_LEG = الكاحل الأيسر
body_part-LEFT_FOOT = القدم اليسرى
body_part-LEFT_THUMB_METACARPAL = عظم مشط الإبهام الأيسر
body_part-LEFT_THUMB_PROXIMAL = الإبهام الأيسر القريب
body_part-LEFT_THUMB_DISTAL = الإبهام الأيسر البعيد
body_part-LEFT_INDEX_PROXIMAL = السبابة اليسرى القريبة
body_part-LEFT_INDEX_INTERMEDIATE = السبابة اليسرى المتوسطة
body_part-LEFT_INDEX_DISTAL = السبابة اليسرى البعيدة
body_part-LEFT_MIDDLE_PROXIMAL = الجزء الأوسط الأيسر القريب
body_part-LEFT_MIDDLE_INTERMEDIATE = الوسط الأيسر المتوسط
body_part-LEFT_MIDDLE_DISTAL = الجزء الأوسط الأيسر البعيد
body_part-LEFT_RING_PROXIMAL = الحلقة اليسرى القريبة
body_part-LEFT_RING_INTERMEDIATE = الحلقة اليسرى المتوسطة
body_part-LEFT_RING_DISTAL = الحلقة اليسرى البعيدة
body_part-LEFT_LITTLE_PROXIMAL = الجزء الأيسر الصغير القريب
body_part-LEFT_LITTLE_INTERMEDIATE = اليسار الصغير المتوسط
body_part-LEFT_LITTLE_DISTAL = الجزء البعيد الأيسر الصغير
body_part-RIGHT_THUMB_METACARPAL = مشط الإبهام الأيمن
body_part-RIGHT_THUMB_PROXIMAL = الإبهام الأيمن القريب
body_part-RIGHT_THUMB_DISTAL = الإبهام الأيمن البعيد
body_part-RIGHT_INDEX_PROXIMAL = السبابة اليمنى القريبة
body_part-RIGHT_INDEX_INTERMEDIATE = السبابة اليمنى المتوسطة
body_part-RIGHT_INDEX_DISTAL = السبابة اليمنى البعيدة
body_part-RIGHT_MIDDLE_PROXIMAL = منتصف اليمين القريب
body_part-RIGHT_MIDDLE_INTERMEDIATE = الوسط الأيمن المتوسط
body_part-RIGHT_MIDDLE_DISTAL = منتصف اليمين البعيد
body_part-RIGHT_RING_PROXIMAL = الحلقة اليمنى القريبة
body_part-RIGHT_RING_INTERMEDIATE = الحلقة اليمنى المتوسطة
body_part-RIGHT_RING_DISTAL = الحلقة اليمنى البعيدة
body_part-RIGHT_LITTLE_PROXIMAL = الجزء القريب الأيمن الصغير
body_part-RIGHT_LITTLE_INTERMEDIATE = المتوسط ​​الصغير الأيمن
body_part-RIGHT_LITTLE_DISTAL = الجزء البعيد الصغير الأيمن
## BoardType
board_type-UNKNOWN = مجهول
board_type-NODEMCU = NodeMCU
board_type-CUSTOM = لوحة مخصصة
board_type-WROOM32 = WROOM32
board_type-WEMOSD1MINI = Wemos D1 Mini
board_type-TTGO_TBASE = TTGO T-Base
board_type-ESP01 = ESP-01
board_type-SLIMEVR = سلايم في آر
board_type-LOLIN_C3_MINI = Lolin C3 Mini
board_type-BEETLE32C3 = Beetle ESP32-C3
board_type-ESP32C3DEVKITM1 = Espressif ESP32-C3 DevKitM-1
board_type-OWOTRACK = owoTrack
board_type-WRANGLER = رانجلر جويكونز
board_type-MOCOPI = سوني موكوبي (Mocopi)
board_type-WEMOSWROOM02 = Wemos Wroom-02 D1 Mini
board_type-XIAO_ESP32C3 = Seeed Studio XIAO ESP32C3
board_type-HARITORA = Haritora
board_type-ESP32C6DEVKITC1 = Espressif ESP32-C6 DevKitC-1
board_type-GLOVE_IMU_SLIMEVR_DEV = قفاز SlimeVR تطوير IMU
## Proportions
skeleton_bone-NONE = غير محدد
skeleton_bone-HEAD = إمالة الرأس
skeleton_bone-HEAD-desc =
هذه هي المسافة من سماعة الرأس إلى منتصف رأسك.
لضبط المسافة ، هز رأسك من اليسار إلى اليمين كما لو كنت لا توافق وقم بتعديله
حتى أي حركة في أجهزة التتبع الأخرى لا تكاد تذكر.
skeleton_bone-NECK = طول العنق
skeleton_bone-NECK-desc =
هذه هي المسافة من منتصف رأسك إلى قاعدة رقبتك.
لضبطها ، حرك رأسك لأعلى ولأسفل كما لو كنت تومئ رأسك أو تميل رأسك
إلى اليسار واليمين وقم بتعديله حتى تصبح أي حركة في أجهزة التتبع الأخرى ضئيلة.
skeleton_bone-torso_group = طول الجذع
skeleton_bone-UPPER_CHEST = طول أعلى الصدر
skeleton_bone-CHEST_OFFSET = درجة تشريد الصدر
@@ -149,14 +79,6 @@ skeleton_bone-ELBOW_OFFSET = درجة تشريد الكوع
## Tracker reset buttons
reset-reset_all = إعادة تعيين جميع النسب
reset-reset_all_warning-v2 =
<b>تحذير:</b> ستتم إعادة تعيين النسب إلى الإعدادات الافتراضية التي تم تحجيمها إلى الارتفاع الذي تم تكوينه.
هل أنت متأكد من أنك تريد القيام بذلك؟
reset-reset_all_warning-reset = إعادة تعيين النسب
reset-reset_all_warning-cancel = إلغاء
reset-reset_all_warning_default-v2 =
<b>تحذير:</b> لم يتم ضبط طولك، وسيتم إعادة تعيين نسبك إلى الإعدادات الافتراضية مع الارتفاع الافتراضي.
هل أنت متأكد من أنك تريد القيام بذلك؟
reset-full = اعاده تعيين
reset-mounting = إعادة تعيين التركيب
reset-yaw = إعادة تعيين الانعراج
@@ -200,10 +122,6 @@ widget-overlay-is_mirrored_label = عكس تراكب الشاشة
widget-drift_compensation-clear = حذف تعويض الانجراف
## Widget: Clear Mounting calibration
widget-clear_mounting = مسح إعادة تعيين التركيب
## Widget: Developer settings
widget-developer_mode = وضع المطوّر
@@ -218,17 +136,9 @@ widget-developer_mode-more_info = المزيد
## Widget: IMU Visualizer
widget-imu_visualizer = دوران
widget-imu_visualizer-preview = عرض
widget-imu_visualizer-hide = إخفاء
widget-imu_visualizer-rotation_raw = صافي
widget-imu_visualizer-rotation_preview = عرض مسبق
widget-imu_visualizer-acceleration = التسارع
widget-imu_visualizer-position = الموضع
## Widget: Skeleton Visualizer
widget-skeleton_visualizer-preview = إظهار الهيكل العظمي
widget-skeleton_visualizer-hide = إخفاء
widget-imu_visualizer-rotation_hide = إخفاء
## Tracker status
@@ -238,7 +148,6 @@ tracker-status-error = خطأ
tracker-status-disconnected = فقد الاتصال
tracker-status-occluded = محجوب
tracker-status-ok = حسنًا
tracker-status-timed_out = انتهت المهله
## Tracker status columns
@@ -256,15 +165,9 @@ tracker-table-column-url = عنوان URL
## Tracker rotation
tracker-rotation-front = المقدمة
tracker-rotation-front_left = أمامي-يسار
tracker-rotation-front_right = أمامي -يمين
tracker-rotation-left = اليسار
tracker-rotation-right = اليمين
tracker-rotation-back = الخلف
tracker-rotation-back_left = الخلف اليسار
tracker-rotation-back_right = الخلف الأيمن
tracker-rotation-custom = مخصص
tracker-rotation-overriden = (تم تجاوزه عن طريق إعادة الضبط المتصاعد)
## Tracker information
@@ -275,17 +178,8 @@ tracker-infos-url = عنوان URL لجهاز التعقب
tracker-infos-version = إصدار البرنامج الثابت
tracker-infos-hardware_rev = مراجعة الأجهزة
tracker-infos-hardware_identifier = معرف الجهاز
tracker-infos-data_support = دعم البيانات
tracker-infos-imu = مستشعر IMU
tracker-infos-board_type = اللوحة الرئيسية
tracker-infos-network_version = نسخة البروتوكول
tracker-infos-magnetometer = المقياس المغناطيسي
tracker-infos-magnetometer-status-v1 =
{ $status ->
[DISABLED] ملغي
[ENABLED] فعال
*[NOT_SUPPORTED] غير مدعوم
}
## Tracker settings
@@ -300,25 +194,11 @@ tracker-settings-mounting_section-edit = تعديل التركيب
tracker-settings-drift_compensation_section = السماح بتعويض الانجراف
tracker-settings-drift_compensation_section-description = هل يجب أن يعوض جهاز التعقب عن انحرافه عند تمكين تعويض الانجراف؟
tracker-settings-drift_compensation_section-edit = السماح بتعويض الانجراف
tracker-settings-use_mag = اسمح بالمقياس المغناطيسي على هذا المتتبع
# Multiline!
tracker-settings-use_mag-description =
هل يجب أن يستخدم هذا المتتبع مقياس المغناطيسية لتقليل الانجراف عند السماح باستخدام المقياس المغناطيسي؟ <b>من فضلا لا تغلق جهاز التتبع الخاص بك أثناء تبديل هذا!</b>
تحتاج إلى السماح باستخدام مقياس المغناطيسية أولا ، <magSetting>انقر هنا للذهاب إلى الإعداد</magSetting>.
tracker-settings-use_mag-label = السماح بالمقياس المغناطيسي
# The .<name> means it's an attribute and it's related to the top key.
# In this case that is the settings for the assignment section.
tracker-settings-name_section = اسم جهاز التعقب
tracker-settings-name_section-description = أعطها لقب لطيف :)
tracker-settings-name_section-placeholder = ساق نايتي بيست اليسرى
tracker-settings-name_section-label = اسم جهاز التعقب
tracker-settings-forget = انسي جهاز التعقب
tracker-settings-forget-description = يزيل جهاز التعقب من خادم SlimeVR ويمنعه من الاتصال به حتى يتم إعادة تشغيل الخادم. لن تضيع تكوين جهاز التعقب.
tracker-settings-forget-label = ننسى جهاز التعقب
tracker-settings-update-up_to_date = حديث
tracker-settings-update = التحديث الآن
tracker-settings-update-title = إصدار البرنامج الثابت
## Tracker part card info
@@ -392,11 +272,6 @@ settings-sidebar-osc_router = راوتر أوه أس سي
settings-sidebar-osc_trackers = أجهزة تعقب في ار تشات أوه أس سي
settings-sidebar-utils = الأدوات المساعدة
settings-sidebar-serial = وحدة التحكم التسلسلية
settings-sidebar-appearance = مظهر
settings-sidebar-notifications = إشعارات
settings-sidebar-behavior = سلوك
settings-sidebar-firmware-tool = أداة برامج الجهاز المصنوع بنفسك
settings-sidebar-advanced = متقدم
## SteamVR settings
@@ -411,22 +286,10 @@ settings-general-steamvr-description =
مفيد فقط للألعاب أو التطبيقات التي تدعم أجهزة تعقب معينة.
settings-general-steamvr-trackers-waist = الخصر
settings-general-steamvr-trackers-chest = الصدر
settings-general-steamvr-trackers-left_foot = القدم اليسرى
settings-general-steamvr-trackers-right_foot = القدم اليمنى
settings-general-steamvr-trackers-left_knee = الركبة اليسرى
settings-general-steamvr-trackers-right_knee = الركبة اليمنى
settings-general-steamvr-trackers-left_elbow = الكوع الأيسر
settings-general-steamvr-trackers-right_elbow = الكوع الأيمن
settings-general-steamvr-trackers-left_hand = اليد اليسرى
settings-general-steamvr-trackers-right_hand = اليد اليمنى
settings-general-steamvr-trackers-tracker_toggling = تعيين جهاز التعقب تلقائي
settings-general-steamvr-trackers-tracker_toggling-description = يتعامل تلقائيا مع تبديل أجهزة تعقب SteamVR أو إيقاف تشغيلها اعتمادا على مهام التعقب الحالية
settings-general-steamvr-trackers-tracker_toggling-label = تعيين جهاز التعقب التلقائي
settings-general-steamvr-trackers-hands-warning =
<b>تحذير:</b> ستتجاوز أجهزة تعقب اليد وحدات التحكم الخاصة بك.
هل أنت متأكد؟
settings-general-steamvr-trackers-hands-warning-cancel = إلغاء
settings-general-steamvr-trackers-hands-warning-done = نعم
settings-general-steamvr-trackers-feet = القدمين
settings-general-steamvr-trackers-knees = الركبتين
settings-general-steamvr-trackers-elbows = الكوعين
settings-general-steamvr-trackers-hands = اليدين
## Tracker mechanics
@@ -444,39 +307,14 @@ settings-general-tracker_mechanics-filtering-type-smoothing-description = ينع
settings-general-tracker_mechanics-filtering-type-prediction = التنبؤ
settings-general-tracker_mechanics-filtering-type-prediction-description = يقلل من وقت الإستجابة ويجعل الحركات أكثر سرعة ، ولكنه قد يزيد من التوتر.
settings-general-tracker_mechanics-filtering-amount = المبلغ
settings-general-tracker_mechanics-yaw-reset-smooth-time = إعادة ضبط الانعراج على نحو سلس (يتعطل التنعيم 0 ثوان)
settings-general-tracker_mechanics-drift_compensation = تعويض الانجراف
# This cares about multilines
settings-general-tracker_mechanics-drift_compensation-description =
يعوض عن انجراف انعراج وحدة IMU بتطبيق دوران عكسي.
قم بتغيير كمية التعويض وعدد عمليات إعادة التعيين التي يتم أخذها في الاعتبار.
settings-general-tracker_mechanics-drift_compensation-enabled-label = تعويض الانجراف
settings-general-tracker_mechanics-drift_compensation-prediction = التنبؤ بتعويض الانحراف
# This cares about multilines
settings-general-tracker_mechanics-drift_compensation-prediction-description =
يتنبأ بتعويض انحراف الانعراج خارج النطاق المقاس مسبقا.
قم بتمكين هذا إذا كانت أجهزة التتبع تدور باستمرار على محور الانعراج.
settings-general-tracker_mechanics-drift_compensation-prediction-label = التنبؤ بتعويض الانحراف
settings-general-tracker_mechanics-drift_compensation_warning =
<b>تحذير:</b> استخدم تعويض الانحراف فقط إذا كنت بحاجة إلى إعادة الضبط
في كثير من الأحيان (كل ~ 5-10 دقائق).
تتضمن بعض وحدات IMU المعرضة لإعادة الضبط المتكرر ما يلي:
Joy-Cons و owoTrack و MPUs (بدون برامج ثابتة حديثة).
settings-general-tracker_mechanics-drift_compensation_warning-cancel = إلغاء
settings-general-tracker_mechanics-drift_compensation_warning-done = أتفهم
settings-general-tracker_mechanics-drift_compensation-amount-label = مبلغ التعويض
settings-general-tracker_mechanics-drift_compensation-max_resets-label = استخدام ما يصل إلى x عمليات إعادة التعيين الأخيرة
settings-general-tracker_mechanics-save_mounting_reset = حفظ معايرة إعادة ضبط التركيب التلقائي
settings-general-tracker_mechanics-save_mounting_reset-description =
يحفظ معايرة إعادة تعيين التثبيت التلقائي لأجهزة التتبع بين عمليات إعادة التشغيل. مفيد
عند ارتداء بدلة حيث لا تتحرك المتتبعات بين الجلسات. <b>لا ينصح به للمستخدمين العاديين!</b>
settings-general-tracker_mechanics-save_mounting_reset-enabled-label = حفظ إعادة تعيين التركيب
settings-general-tracker_mechanics-use_mag_on_all_trackers = استخدم المقياس المغناطيسي على جميع أجهزة تتبع IMU التي تدعمه
settings-general-tracker_mechanics-use_mag_on_all_trackers-description =
يستخدم المقياس المغناطيسي على جميع أجهزة التعقب التي تحتوي على برامج ثابتة متوافقة له ، مما يقلل من الانحراف في البيئات المغناطيسية المستقرة.
يمكن تعطيله لكل جهاز تعقب في إعدادات التعقب. <b>من فضلك لا تغلق أيا من أجهزة التعقب أثناء تبديل هذا!</b>
settings-general-tracker_mechanics-use_mag_on_all_trackers-label = استخدم المقياس المغناطيسي على أجهزة التعقب
## FK/Tracking settings
@@ -498,42 +336,17 @@ settings-general-fk_settings-leg_tweak-floor_clip-description = يمكن أن ي
settings-general-fk_settings-leg_tweak-toe_snap-description = الانجذاب إلى أصابع القدم يحاول تخمين دوران قدميك إذا لم تكن أجهزة تعقب القدم قيد الاستخدام.
settings-general-fk_settings-leg_tweak-foot_plant-description = تثبيت اصبع القدم يحاول تخمين دوران قدميك إذا لم تكن أجهزة تعقب القدم قيد الاستخدام.
settings-general-fk_settings-leg_fk = تعقب الساق
settings-general-fk_settings-enforce_joint_constraints = حدود الهيكل العظمي
settings-general-fk_settings-enforce_joint_constraints-enforce_constraints = فرض القيود
settings-general-fk_settings-enforce_joint_constraints-enforce_constraints-description = منع المفاصل من الدوران إلى ما بعد الحد الأقصى
settings-general-fk_settings-enforce_joint_constraints-correct_constraints = التصحيح مع قيود
settings-general-fk_settings-enforce_joint_constraints-correct_constraints-description = تصحيح دوران المفاصل عندما تتجاوز حدودها
settings-general-fk_settings-arm_fk = تعقب الذراع
settings-general-fk_settings-arm_fk-description = تغيير طريقة تعقب الذراعين.
settings-general-fk_settings-arm_fk-force_arms = إجبار الذراعين من ايتش أم دي
settings-general-fk_settings-reset_settings = إعادة تعيين الإعدادات
settings-general-fk_settings-reset_settings-reset_hmd_pitch-description = أعد تعيين درجة جهاز الرأس (الدوران الرأسي) عند إجراء إعادة تعيين كاملة. مفيد في حالة ارتداء جهاز الرأس على الجبهة ل VTubing أو mocap. لا تقم بتمكين لVR.
settings-general-fk_settings-reset_settings-reset_hmd_pitch = إعادة تعيين درجة جهاز الرأس
settings-general-fk_settings-arm_fk-reset_mode-description = قم بتغيير وضع الذراع المتوقع لإعادة ضبط المتصاعد.
settings-general-fk_settings-arm_fk-back = العودة
settings-general-fk_settings-arm_fk-back-description = الوضع الافتراضي، مع وضع الذراعين العلويين إلى الخلف والساعدين للأمام.
settings-general-fk_settings-arm_fk-tpose_up = تي بوز (أعلى)
settings-general-fk_settings-arm_fk-tpose_up-description = يتوقع أن تكون ذراعيك لأسفل على الجانبين أثناء إعادة الضبط الكامل ، و 90 درجة حتى الجانبين أثناء إعادة ضبط التركيب.
settings-general-fk_settings-arm_fk-tpose_down = تي بوز (لأسفل)
settings-general-fk_settings-arm_fk-tpose_down-description = يتوقع أن تكون ذراعيك 90 درجة لأعلى على الجانبين أثناء إعادة الضبط الكامل ، ولأسفل على الجانبين أثناء إعادة ضبط التركيب.
settings-general-fk_settings-arm_fk-forward = أمامي
settings-general-fk_settings-arm_fk-forward-description = يتوقع أن تكون ذراعيك 90 درجة للأمام. مفيد ل VTubing.
settings-general-fk_settings-skeleton_settings-toggles = تبديل الهيكل العظمي
settings-general-fk_settings-skeleton_settings = إعدادات الهيكل العظمي
settings-general-fk_settings-skeleton_settings-description = تبديل إعدادات الهيكل العظمي أو إيقافه. يوصى بتركها شغالة.
settings-general-fk_settings-skeleton_settings-extended_spine_model = نموذج العمود الفقري الممتد
settings-general-fk_settings-skeleton_settings-extended_pelvis_model = نموذج الحوض الممتد
settings-general-fk_settings-skeleton_settings-extended_knees_model = نموذج الركبة الممتدة
settings-general-fk_settings-skeleton_settings-ratios = نسب الهيكل العظمي
settings-general-fk_settings-skeleton_settings-ratios-description = تغيير قيم إعدادات الهيكل العظمي. قد تحتاج إلى ضبط النسب الخاصة بك بعد تغييرها.
settings-general-fk_settings-skeleton_settings-impute_waist_from_chest_hip = خصص الخصر من الصدر إلى الورك
settings-general-fk_settings-skeleton_settings-impute_waist_from_chest_legs = خصص الخصر من الصدر إلى الساقين
settings-general-fk_settings-skeleton_settings-impute_hip_from_chest_legs = خصص الورك من الصدر إلى الساقين
settings-general-fk_settings-skeleton_settings-impute_hip_from_waist_legs = خصص الورك من الخصر إلى الساقين
settings-general-fk_settings-skeleton_settings-interp_hip_legs = متوسط انعراج الفخذ وتدحرج مع الساقين'
settings-general-fk_settings-skeleton_settings-interp_knee_tracker_ankle = متوسط الانحراف وتدحرج مع الكاحلين
settings-general-fk_settings-skeleton_settings-interp_knee_ankle = متوسط انحراف الركبتين ولفة مع الكاحلين
settings-general-fk_settings-self_localization-title = وضع Mocap
settings-general-fk_settings-self_localization-description = يسمح وضع Mocap للهيكل العظمي بتعقب موضعه تقريبا بدون سماعة رأس أو أجهزة تعقب أخرى. لاحظ أن هذا يتطلب أجهزة تعقب القدمين والرأس للعمل ولا تزال تجريبية.
settings-general-fk_settings-skeleton_settings-extended_spine = العمود الفقري الممتد
settings-general-fk_settings-skeleton_settings-extended_pelvis = الحوض الممتد
settings-general-fk_settings-skeleton_settings-extended_knees = الركبة الممتدة
settings-general-fk_settings-vive_emulation-title = محاكاة فايف
settings-general-fk_settings-vive_emulation-description = محاكاة مشاكل تعقب الخصر التي تعاني منها أجهزة تعقب فايف. هذه مزحة وتجعل التتبع أسوأ.
settings-general-fk_settings-vive_emulation-label = تمكين محاكاة فايف
## Gesture control settings (tracker tapping)
@@ -551,17 +364,6 @@ settings-general-gesture_control-taps =
[many] { $amount } نقرات
*[other] { $amount } نقرات
}
# This is a unit: 3 trackers, 2 trackers, 1 tracker
# $amount (Number) - Amount of trackers
settings-general-gesture_control-trackers =
{ $amount ->
[zero] لا أجهزة تعقب
[one] جهاز تعقب واحد
[two] جهازي تعقب
[few] { "" }
[many] { "" }
*[other] { $amount } أجهزة تعقب
}
settings-general-gesture_control-yawResetEnabled = تمكين النقر لإعادة التعيين الانعراج
settings-general-gesture_control-yawResetDelay = إعادة تعيين التأخير الانعراج
settings-general-gesture_control-yawResetTaps = عدد النقرات لإعادة تعيين الانعراج
@@ -571,38 +373,13 @@ settings-general-gesture_control-fullResetTaps = عدد النقرات لإعا
settings-general-gesture_control-mountingResetEnabled = تمكين النقر لإعادة تعيين التركيب
settings-general-gesture_control-mountingResetDelay = تأخير إعادة تعيين التركيب
settings-general-gesture_control-mountingResetTaps = نقرات لإعادة تعيين التركيب
# The number of trackers that can have higher acceleration before a tap is rejected
settings-general-gesture_control-numberTrackersOverThreshold = أجهزة تعقب فوق قيمة القطع
settings-general-gesture_control-numberTrackersOverThreshold-description = قم بزيادة هذه القيمة إذا كان اكتشاف النقر لا يعمل. لا تقم بزيادته فوق ما هو مطلوب لجعل اكتشاف النقر يعمل لأنه قد يتسبب في المزيد من الإيجابيات الخاطئة.
## Appearance settings
## Interface settings
settings-interface-appearance = مظهر
settings-general-interface = واجهة المستخدم
settings-general-interface-dev_mode = وضع المطوّر
settings-general-interface-dev_mode-description = يمكن أن يكون هذا الوضع مفيدًا إذا كنت بحاجة إلى بيانات متعمقة أو للتفاعل مع أجهزة التعقب المتصلة على مستوى أكثر تقدمًا.
settings-general-interface-dev_mode-label = وضع المطوّر
settings-general-interface-theme = موضوع اللون
settings-general-interface-show-navbar-onboarding = إظهار "{ navbar-onboarding }" على قائمة التنقل
settings-general-interface-show-navbar-onboarding-description = يغير هذا إذا ظهر الزر "{ navbar-onboarding }" على قائمة التنقل.
settings-general-interface-show-navbar-onboarding-label = إظهار "{ navbar-onboarding }"
settings-general-interface-lang = اختر اللغة
settings-general-interface-lang-description = قم بتغيير اللغة الافتراضية التي تريد استخدامها.
settings-general-interface-lang-placeholder = اختر اللغة التي تريد استخدامها
# Keep the font name untranslated
settings-interface-appearance-font = خط واجهة المستخدم الرسومية
settings-interface-appearance-font-description = هذا يغير الخط المستخدم من قبل الواجهة.
settings-interface-appearance-font-placeholder = الخط الافتراضي
settings-interface-appearance-font-os_font = خط نظام التشغيل
settings-interface-appearance-font-slime_font = الخط الافتراضي
settings-interface-appearance-font_size = قياس الخط الأساسي
settings-interface-appearance-font_size-description = يؤثر هذا على حجم خط الواجهة بأكملها باستثناء لوحة الإعدادات هذه.
settings-interface-appearance-decorations = استخدم الزخارف الأصلية للنظام
settings-interface-appearance-decorations-description = لن يؤدي هذا إلى عرض الشريط الأعلى للواجهة وسيستخدم نظام التشغيل بدلا من ذلك.
settings-interface-appearance-decorations-label = استخدم الزخارف الأصلية
## Notification settings
settings-interface-notifications = إشعارات
settings-general-interface-serial_detection = الكشف عن جهاز تسلسلي
settings-general-interface-serial_detection-description = سيعرض هذا الخيار نافذة منبثقة في كل مرة تقوم فيها بتوصيل جهاز تسلسلي جديد يمكن أن يكون جهاز تعقب. يساعد في تحسين عملية إعداد جهاز التعقب.
settings-general-interface-serial_detection-label = الكشف عن جهاز تسلسلي
@@ -610,37 +387,10 @@ settings-general-interface-feedback_sound = صوت ردود الفعل
settings-general-interface-feedback_sound-description = سيصدر هذا الخيار صوتًا عند تشغيل إعادة الضبط
settings-general-interface-feedback_sound-label = صوت ردود الفعل
settings-general-interface-feedback_sound-volume = حجم صوت ردود الفعل
settings-general-interface-connected_trackers_warning = تحذير عن أجهزة التعقب المتصلة
settings-general-interface-connected_trackers_warning-description = سيعرض هذا الخيار نافذة كل مرة تحاول فيها الخروج من SlimeVR أثناء وجود جهاز أو أكثر من أجهزة التعقب المتصلة. سيذكرك بإيقاف تشغيل أجهزة التعقب عند الانتهاء للحفاظ على عمر البطارية.
settings-general-interface-connected_trackers_warning-label = تحذير عن أجهزة التعقب المتصلة عند الخروج
## Behavior settings
settings-interface-behavior = السلوك
settings-general-interface-use_tray = تصغير إلى علبة النظام
settings-general-interface-use_tray-description = يتيح لك إغلاق النافذة دون إغلاق خادم SlimeVR حتى تتمكن من الاستمرار في استخدامه دون إزعاجك من واجهة المستخدم الرسومية.
settings-general-interface-use_tray-label = تصغير إلى علبة النظام
settings-general-interface-discord_presence = مشاركة النشاط على Discord
settings-general-interface-discord_presence-description = يخبر عميل Discord الخاص بك أنك تستخدم SlimeVR جنبا إلى جنب مع عدد أجهزة تعقب IMU التي تستخدمها.
settings-general-interface-discord_presence-label = مشاركة النشاط على Discord
settings-general-interface-discord_presence-message =
{ $amount ->
[0] صفر
[zero] صفر
[one] واحد
[two] اثنان
[few] قليل
[many] كثيرة
*[other] أخرى
}
settings-interface-behavior-error_tracking = جمع الأخطاء عبر Sentry.io
settings-interface-behavior-error_tracking-description_v2 =
<h1>هل توافق على جمع بيانات الخطأ مجهولة المصدر؟</h1>
<b>نحن لا نجمع معلومات شخصية</b> مثل عنوان IP الخاص بك أو بيانات الاعتماد اللاسلكية. يقدر SlimeVR خصوصيتك!
لتوفير أفضل تجربة للمستخدم، نقوم بجمع تقارير الأخطاء ومقاييس الأداء ومعلومات نظام التشغيل مجهولة المصدر. يساعدنا هذا في اكتشاف الأخطاء والمشكلات المتعلقة ب SlimeVR. يتم جمع هذه المقاييس عبر Sentry.io.
settings-interface-behavior-error_tracking-label = إرسال الأخطاء إلى المطورين
settings-general-interface-theme = موضوع اللون
settings-general-interface-lang = اختر اللغة
settings-general-interface-lang-description = قم بتغيير اللغة الافتراضية التي تريد استخدامها.
settings-general-interface-lang-placeholder = اختر اللغة التي تريد استخدامها
## Serial settings
@@ -659,11 +409,9 @@ settings-serial-factory_reset-warning =
مما يعني أن إعدادات واي فاي والمعايرة <b>ستفقد جميعا!</b>
settings-serial-factory_reset-warning-ok = أنا أعرف ماذا أفعل
settings-serial-factory_reset-warning-cancel = إلغاء
settings-serial-get_infos = احصل على معلومات
settings-serial-serial_select = اختر منفذ تسلسلي
settings-serial-auto_dropdown_item = تلقائي
settings-serial-get_wifi_scan = احصل على فحص WiFi
settings-serial-file_type = نص عادي
settings-serial-save_logs = حفظ في ملف
## OSC router settings
@@ -694,22 +442,14 @@ settings-osc-router-network-address-placeholder = عنوان آي بي في 4
settings-osc-vrchat = أجهزة تعقب "في ار تشات أوه أس سي"
# This cares about multilines
settings-osc-vrchat-description-v1 =
تغيير الإعدادات الخاصة بمعيار أجهزة تعقب OSC المستخدم لإرسال
بيانات التعقب إلى التطبيقات التي لا تحتوي على SteamVR (مثل Quest المستقل).
تأكد من تمكين OSC في VRChat عبر قائمة الإجراءات ضمن OSC > ممكن.
settings-osc-vrchat-description =
قم بتغيير الإعدادات الخاصة ب في ار تشات لتلقي بيانات ايتش أم دي وإرسالها
بيانات أجهزة تعقب لتعقب الجسم (يعمل على كوست مستقل).
settings-osc-vrchat-enable = تمكين
settings-osc-vrchat-enable-description = بتبديل إرسال واستقبال البيانات.
settings-osc-vrchat-enable-label = تمكين
settings-osc-vrchat-oscqueryEnabled = تمكين OSCQuery
settings-osc-vrchat-oscqueryEnabled-description =
يكتشف OSCQuery تلقائيا مثيلات VRChat قيد التشغيل ويرسل البيانات إليها.
يمكنه أيضا الإعلان عن نفسه لهم من أجل تلقي بيانات HMD ووحدة التحكم.
للسماح بتلقي بيانات HMD ووحدة التحكم من VRChat ، انتقل إلى إعدادات القائمة الرئيسية
ضمن "التتبع و IK (الحركة العكسية)" وتمكين "السماح بإرسال بيانات OSC لتتبع الرأس والمعصم".
settings-osc-vrchat-oscqueryEnabled-label = تمكين OSCQuery
settings-osc-vrchat-network = منافذ الشبكة
settings-osc-vrchat-network-description-v1 = ضبط المنافذ الاستماع إلى البيانات وإرسالها. يمكن تركها دون أن تمس ل VRChat.
settings-osc-vrchat-network-description = قم بتعيين المنافذ للاستماع وإرسال البيانات إلى في ار تشات
settings-osc-vrchat-network-port_in =
.label = منفذ الدخول
.placeholder = منفذ الدخول (الإفتراضي: 9001)
@@ -717,7 +457,7 @@ settings-osc-vrchat-network-port_out =
.label = منفذ الخروج
.placeholder = منفذ الخروج (الإفتراضي: 9000)
settings-osc-vrchat-network-address = عنوان الشبكة
settings-osc-vrchat-network-address-description-v1 = اختر العنوان الذي تريد إرسال البيانات إليه. يمكن تركها دون أن تمس ل VRChat.
settings-osc-vrchat-network-address-description = اختر العنوان الذي تريد إرسال البيانات إلى في ار تشات (تحقق من إعدادات واي فاي على جهازك)
settings-osc-vrchat-network-address-placeholder = عنوان آي بي الخاص بفي ار تشات
settings-osc-vrchat-network-trackers = أجهزة التعقب
settings-osc-vrchat-network-trackers-description = تبديل إرسال أجهزة تتبع محددة عبر أوه أس سي.
@@ -750,54 +490,16 @@ settings-osc-vmc-network-address-description = قم بتعيين العنوان
settings-osc-vmc-network-address-placeholder = عنوان آي بي في 4
settings-osc-vmc-vrm = نموذج في ار إم
settings-osc-vmc-vrm-description = قم بتحميل نموذج في ار إم للسماح بتركيز الرأس وتمكين توافق أعلى مع تطبيقات الأخرى
settings-osc-vmc-vrm-untitled_model = نموذج بدون عنوان
settings-osc-vmc-vrm-model_unloaded = لم يتم تحميل أي نموذج
settings-osc-vmc-vrm-model_loaded =
{ $titled ->
[true] تحميل النموذج: { $name }
*[other] تم تحميل نموذج بدون عنوان
}
settings-osc-vmc-vrm-file_select = اسحب نموذج وأفلته لاستخدامه أو <u> تصفح </ u>
settings-osc-vmc-anchor_hip = ثبت في الوركين
settings-osc-vmc-anchor_hip-description = ثبت التعقب في الوركين، هو مفيد إن كنت تيوبنغ جالسًا. في حالة التعطيل، قم بتحميل نموذج في ار إم.
settings-osc-vmc-anchor_hip-label = ثبت في الوركين
settings-osc-vmc-mirror_tracking = اعكس التعقب
settings-osc-vmc-mirror_tracking-description = اعكس التعقب أفقيا.
settings-osc-vmc-mirror_tracking-label = اعكس التعقب
## Common OSC settings
## Advanced settings
settings-utils-advanced = متقدم
settings-utils-advanced-reset-gui = إعادة تعيين إعدادات واجهة المستخدم الرسومية (GUI)
settings-utils-advanced-reset-gui-description = قم باستعادة الإعدادات الافتراضية للواجهة.
settings-utils-advanced-reset-gui-label = إعادة تعيين واجهة المستخدم الرسومية
settings-utils-advanced-reset-server = إعادة تعيين إعدادات التعقب
settings-utils-advanced-reset-server-description = استعادة الإعدادات الافتراضية للتعقب.
settings-utils-advanced-reset-server-label = إعادة تعيين التعقب
settings-utils-advanced-reset-all = إعادة تعيين جميع الإعدادات
settings-utils-advanced-reset-all-description = قم باستعادة الإعدادات الافتراضية لكل من الواجهة و التعقب.
settings-utils-advanced-reset-all-label = إعادة تعيين الكل
settings-utils-advanced-reset_warning =
{ $type ->
[gui]
<b>تحذير:</b> سيؤدي هذا إلى إعادة تعيين جميع الإعدادات الخاصة بك إلى الإعدادات الافتراضية.
هل أنت متأكد من أنك تريد القيام بذلك؟
[server] <b>تحذير:</b> سيؤدي هذا إلى إعادة تعيين إعدادات التعقب إلى الإعدادات الافتراضية. هل أنت متأكد من أنك تريد القيام بذلك؟
*[all]
<b>تحذير:</b> سيؤدي هذا إلى إعادة تعيين جميع الإعدادات الخاصة بك إلى الإعدادات الافتراضية.
هل أنت متأكد من أنك تريد القيام بذلك؟
}
settings-utils-advanced-reset_warning-reset = إعادة تعيين الإعدادات
settings-utils-advanced-reset_warning-cancel = إلغاء
settings-utils-advanced-open_data-v1 = مجلد التكوين
settings-utils-advanced-open_data-description-v1 = فتح مجلد إعدادات SlimeVR في مستكشف الملفات ، والذي يحتوي على الإعدادات
settings-utils-advanced-open_data-label = فتح المجلد
settings-utils-advanced-open_logs = مجلد السجلات
settings-utils-advanced-open_logs-description = افتح مجلد سجلات SlimeVR في مستكشف الملفات ، والذي يحتوي على سجلات التطبيق
settings-utils-advanced-open_logs-label = فتح المجلد
## Home Screen
## Tracking Checlist
## Setup/onboarding menu
@@ -814,12 +516,16 @@ onboarding-setup_warning-cancel = متابعة الإعداد
## Wi-Fi setup
onboarding-wifi_creds-back = العودة إلى المقدمة
onboarding-wifi_creds = إدخل بيانات اعتماد واي فاي
# This cares about multilines
onboarding-wifi_creds-description =
ستستخدم أجهزة التعقب بيانات الاعتماد هذه للاتصال لاسلكيًا.
الرجاء استخدام بيانات الاعتماد التي تتصل بها حاليًا.
onboarding-wifi_creds-skip = تخطى إعدادات واي فاي
onboarding-wifi_creds-submit = إرسال!
onboarding-wifi_creds-ssid =
.label = اسم الواي فاي
.placeholder = أدخل اسم الواي فاي
onboarding-wifi_creds-ssid-required = مطلوب اسم Wi-Fi
onboarding-wifi_creds-password =
.label = كلمة السر
.placeholder = أدخل كلمة السر
@@ -854,6 +560,13 @@ onboarding-reset_tutorial-2 =
onboarding-home = مرحبا بكم في سلايم في ار
onboarding-home-start = هيا نتجهز!
## Enter VR part of setup
onboarding-enter_vr-back = العودة إلى تعيين أجهزة التعقب
onboarding-enter_vr-title = حان وقت دخول في ار!
onboarding-enter_vr-description = ضع كل أجهزة التعقب ثم أدخل في ار!
onboarding-enter_vr-ready = أنا جاهز
## Setup done
onboarding-done-title = أنت جاهز تمامًا!
@@ -864,13 +577,12 @@ onboarding-done-close = إغلاق الدليل
onboarding-connect_tracker-back = العودة إلى بيانات اعتماد الواي فاي
onboarding-connect_tracker-title = ربط أجهزة التعقب
onboarding-connect_tracker-description-p0-v1 = ننتقل الآن إلى الجزء الممتع ، ربط أجهزة التعقب!
onboarding-connect_tracker-description-p1-v1 = قم بتوصيل كل جهاز تعقب واحدا تلو الآخر من خلال منفذ USB.
onboarding-connect_tracker-description-p0 = ننتقل الآن إلى الجزء الممتع ، ربط جميع أجهزة التعقب!
onboarding-connect_tracker-description-p1 = ما عليك سوى توصيل كل ما لم يتم توصيله بعد من خلال منفذ يو أس بي.
onboarding-connect_tracker-issue-serial = أواجه مشكلة في الاتصال!
onboarding-connect_tracker-usb = جهاز تعقب يو أس بي
onboarding-connect_tracker-connection_status-none = نبحث عن أجهزة التعقب
onboarding-connect_tracker-connection_status-serial_init = نتواصل بجهاز التسلسلي
onboarding-connect_tracker-connection_status-obtaining_mac_address = الحصول على عنوان mac الخاص بجهاز التعقب
onboarding-connect_tracker-connection_status-provisioning = نرسل بيانات اعتماد واي فاي
onboarding-connect_tracker-connection_status-connecting = جارٍ إرسال بيانات اعتماد الواي فاي
onboarding-connect_tracker-connection_status-looking_for_server = نبحث عن السرفر
@@ -898,12 +610,12 @@ onboarding-connect_tracker-next = لقد قمت بتوصيل جميع أجهزة
onboarding-calibration_tutorial = برنامج تعليم معايرة IMU
onboarding-calibration_tutorial-subtitle = سوف يساعد هذا في تقليل الانجراف التعقب!
onboarding-calibration_tutorial-description = كل مرة تقوم بتشغيل أجهزة التعقب، يجب أن تستريح للحظة على سطح مستوٍ للمعايرة. لنفعل الشيء نفسه بالنقر فوق الزر "{ onboarding-calibration_tutorial-calibrate }" ، <b>لا تحركها!</b>
onboarding-calibration_tutorial-calibrate = وضعت أجهزة التعقب على الطاولة
onboarding-calibration_tutorial-status-waiting = بانتظارك
onboarding-calibration_tutorial-status-calibrating = جاري المعايرة
onboarding-calibration_tutorial-status-success = رائع!
onboarding-calibration_tutorial-status-error = تم نقل جهاز التعقب
onboarding-calibration_tutorial-skip = تخطي البرنامج التعليمي
## Tracker assignment tutorial
@@ -911,8 +623,8 @@ onboarding-assignment_tutorial = كيفية تحضير جهاز تعقب Slime
onboarding-assignment_tutorial-first_step = 1. ضع ملصق جزء الجسم (إذا كان لديك واحد) على جهاز التعقب وفقا لاختيارك
# This text has a character limit of around 11 characters, so please keep it short
onboarding-assignment_tutorial-sticker = ملصق
onboarding-assignment_tutorial-second_step-v2 = 2. قم بتوصيل الشريط بجهاز التعقب، مع الحفاظ على جانب الفيلكرو من الشريط في نفس اتجاه وجه السلايم لجهاز التعقب:
onboarding-assignment_tutorial-second_step-continuation-v2 = يجب أن يكون جانب الفيلكرو للامتداد متجها للأعلى مثل الصورة التالية:
onboarding-assignment_tutorial-second_step = 2. قم بتوصيل الشريط بجهاز التعقب الخاص بك ، مع الحفاظ على جانب الخطاف والحلقة من وجه الشريط في الاتجاه التالي:
onboarding-assignment_tutorial-second_step-continuation = يجب أن يكون جانب الخطاف والحلقة للامتداد في هذا الاتجاه:
onboarding-assignment_tutorial-done = وضعت الملصقات والأشرطة!
## Tracker assignment setup
@@ -934,32 +646,6 @@ onboarding-assign_trackers-assigned =
}
onboarding-assign_trackers-advanced = إظهار مواقع التعيين المتقدمة
onboarding-assign_trackers-next = لقد عينت جميع أجهزة التعقب
onboarding-assign_trackers-mirror_view = عرض المرآة
onboarding-assign_trackers-option-amount =
{ $trackersCount ->
[zero] صفر
[one] واحد
[two] اثنان
[few] قليلة
[many] كثيرة
*[other] أخرى
}
onboarding-assign_trackers-option-label =
{ $mode ->
[lower-body] الجسم السفلي
[core] أساس الجسم
[enhanced-core] الأساس المحسن
[full-body] الجسم الكامل
*[all] الكل
}
onboarding-assign_trackers-option-description =
{ $mode ->
[lower-body] الحد الأدنى لتعقب الجسم الكامل في الواقع الافتراضي
[core] + تحسين تعقب العمود الفقري
[enhanced-core] + دوران القدم
[full-body] + تعقب الكوع
*[all] جميع مهام التعقب المتاحة
}
## Tracker assignment warnings
@@ -1035,20 +721,13 @@ onboarding-choose_mounting = ما طريقة معايرة التركيب الم
# Multiline text
onboarding-choose_mounting-description = اتجاه التركيب يصحح وضع أجهزة التعقب على جسمك.
onboarding-choose_mounting-auto_mounting = التركيب التلقائي
# Italicized text
onboarding-choose_mounting-auto_mounting-label-v2 = الموصى به
# Italized text
onboarding-choose_mounting-auto_mounting-label = تجريبي
onboarding-choose_mounting-auto_mounting-description = سيكتشف هذا تلقائيًا اتجاهات التركيب لجميع أجهزة التعقب من وضعين
onboarding-choose_mounting-manual_mounting = التركيب اليدوي
# Italicized text
onboarding-choose_mounting-manual_mounting-label-v2 = قد لا تكون الدقة كافية
# Italized text
onboarding-choose_mounting-manual_mounting-label = المستحسن
onboarding-choose_mounting-manual_mounting-description = سيسمح لك باختيار اتجاه التثبيت يدويًا لكل جهاز تعقب
# Multiline text
onboarding-choose_mounting-manual_modal-title =
هل أنت متأكد من أنك تريد
معايرة التركيب التلقائي؟
onboarding-choose_mounting-manual_modal-description = <b>يوصى بمعايرة التركيب اليدوي للمستخدمين الجدد</b> ، حيث قد يكون من الصعب الحصول على أوضاع معايرة التركيب التلقائي الصحيحة من اول مرة وقد تتطلب بعض التمرين.
onboarding-choose_mounting-manual_modal-confirm = أنا أعرف ماذا أفعل
onboarding-choose_mounting-manual_modal-cancel = إلغاء
## Tracker manual mounting setup
@@ -1073,18 +752,37 @@ onboarding-automatic_mounting-mounting_reset-title = إعادة تعيين ال
onboarding-automatic_mounting-mounting_reset-step-0 = 1. قرفص في وضع "التزلج" مع ثني ساقيك ، وإمالة الجزء العلوي من جسمك إلى الأمام ، وثني ذراعيك.
onboarding-automatic_mounting-mounting_reset-step-1 = 2. اضغط على زر "إعادة تعيين التركيب" وانتظر لمدة 3 ثوان قبل إعادة تعيين دوران تركيب أجهزة التعقب.
onboarding-automatic_mounting-preparation-title = التحضير
onboarding-automatic_mounting-preparation-step-0 = 1. قف بشكل مستقيم مع ذراعيك على جانبيك.
onboarding-automatic_mounting-preparation-step-1 = اضغط على زر "إعادة ضبط" و انتظر لمدة 3 ثوانٍ قبل إعادة تعيين أجهزة التعقب.
onboarding-automatic_mounting-put_trackers_on-title = ارتدي أجهزة التعقب
onboarding-automatic_mounting-put_trackers_on-description = لمعايرة دوران التركيب، سنستخدم أجهزة التعقب التي قمت بتعيينها. ارتدي جميع أجهزة التعقب، يمكنك معرفة أي منها في المستند على اليمين.
onboarding-automatic_mounting-put_trackers_on-next = ارتديت جميع أجهزة التعقب.
## Tracker manual proportions setupa
## Tracker proportions method choose
onboarding-choose_proportions = ما هي طريقة معايرة النسب التي يجب استخدامها؟
# Multiline string
onboarding-choose_proportions-description =
تستخدم نسب الجسم لمعرفة قياسات جسمك. هم مطلوبون لحساب مواقع أجهزة التعقب.
عندما لا تتطابق نسب جسمك مع تلك المحفوظة ، ستكون دقة التتبع لديك أسوأ وستلاحظ أشياء مثل التزلج أو الانزلاق ، أو أن جسمك لا يتطابق مع الصورة الرمزية بشكل جيد.
onboarding-choose_proportions-auto_proportions = النسب التلقائية
# Italized text
onboarding-choose_proportions-auto_proportions-subtitle = الموصى به
onboarding-choose_proportions-auto_proportions-description = سيقدر هذا النسب الخاصة بك عن طريق تسجيل عينة من حركاتك وتمريرها من خلال برنامج
onboarding-choose_proportions-manual_proportions = النسب اليدوية
# Italized text
onboarding-choose_proportions-manual_proportions-subtitle = للمسات الصغيرة
onboarding-choose_proportions-manual_proportions-description = سيسمح لك بتعديل النسب يدويًا عن طريق تعديلها مباشرة
onboarding-choose_proportions-export = تصدير النسب
onboarding-choose_proportions-file_type = ملف نسب الجسم
## Tracker manual proportions setup
onboarding-manual_proportions-back = العودة إلى برنامج تعليم إعادة التعيين
onboarding-manual_proportions-title = نسب الجسم اليدوية
onboarding-manual_proportions-fine_tuning_button = ضبط النسب تلقائيا
onboarding-manual_proportions-fine_tuning_button-disabled-tooltip = يرجى توصيل سماعة رأس VR لاستخدام الضبط الدقيق التلقائي
onboarding-manual_proportions-export = تصدير النسب
onboarding-manual_proportions-import = استيراد النسب
onboarding-manual_proportions-file_type = ملف نسب الجسم
onboarding-manual_proportions-precision = ضبط الدقة
onboarding-manual_proportions-auto = المعايرة التلقائية
onboarding-manual_proportions-ratio = اضبط حسب مجموعات النسب
## Tracker automatic proportions setup
@@ -1098,40 +796,14 @@ onboarding-automatic_proportions-put_trackers_on-description = لمعايرة ن
onboarding-automatic_proportions-put_trackers_on-next = ارتديت جميع أجهزة التعقب.
onboarding-automatic_proportions-requirements-title = المتطلبات
# Each line of text is a different list item
onboarding-automatic_proportions-requirements-descriptionv2 =
لديك على الأقل ما يكفي من أجهزة التعقب لتتبع قدميك (بشكل عام 5 أجهزة تعقب).
لديك أجهزة التعقب وجهاز الواقع الافتراضي الخاص بك وترتديهم.
أجهزة التعقب وجهاز الواقع الافتراضي متصلة بخادم SlimeVR وتعمل بشكل صحيح (مثلاً، لا يوجد تأتأة أو قطع اتصال ، إلخ).
يقوم جهاز الواقع الافتراضي بالإبلاغ عن البيانات الموضعية إلى خادم SlimeVR (وهذا يعني عموما تشغيل SteamVR وتوصيله ب SlimeVR باستخدام برنامج تشغيل SteamVR الخاص ب SlimeVR).
يعمل التتبع الخاص بك ويمثل تحركاتك بدقة (على سبيل المثال ، لقد أجريت إعادة تعيين كاملة وتتحرك في الاتجاه الصحيح عند الركل, الانحناء, الجلوس, إلخ).
onboarding-automatic_proportions-requirements-description =
لديك على الأقل ما يكفي من أجهزة تعقب لتعقب قدميك (بشكل عام 5 أجهزة تعقب).
ارتديت أجهزة التعقب وسماعة الرأس.
شغلت أجهزة التعقب وسماعة الرأس.
أجهزة التعقب وسماعات الرأس متصلة بسرفر سلايم في ار.
تعمل أجهزة التتبع وسماعات الرأس بشكل صحيح داخل سرفر سلايم في ار .
تقوم سماعة الرأس الخاصة بك بالإبلاغ عن بيانات الموقع إلى سرفر سلايم في ار (وهذا يعني بشكل عام تشغيل سلايم في ار وتوصيله بـ سلايم في ار باستخدام برنامج تشغيل ستيم في ار من سلايم في ار ).
onboarding-automatic_proportions-requirements-next = لقد قرأت المتطلبات
onboarding-automatic_proportions-check_height-title-v3 = قم بقياس ارتفاع سماعة الرأس
onboarding-automatic_proportions-check_height-description-v2 = يجب أن يكون ارتفاع سماعة الرأس (HMD) أقل قليلا من طولك الكامل ، حيث تقيس سماعات الرأس ارتفاع عينيك. سيتم استخدام هذا القياس كخط أساس لنسب جسمك.
# All the text is in bold!
onboarding-automatic_proportions-check_height-calculation_warning-v3 = ابدأ في القياس أثناء الوقوف <u>في وضع مستقيم</u> لقياس طولك. احرص على عدم رفع يديك أعلى من سماعة الرأس ، لأنها قد تؤثر على القياس!
onboarding-automatic_proportions-check_height-guardian_tip =
إذا كنت تستخدم سماعة رأس VR مستقلة ، فتأكد من تشغيل حدود الحارس /
لكي يكون طولك صحيحا!
# Context is that the height is unknown
onboarding-automatic_proportions-check_height-unknown = مجهول
# Shows an element below it
onboarding-automatic_proportions-check_height-hmd_height2 = ارتفاع سماعة الرأس هو:
onboarding-automatic_proportions-check_height-measure-start = ابدأ القياس
onboarding-automatic_proportions-check_height-measure-stop = توقف عن القياس
onboarding-automatic_proportions-check_height-measure-reset = إعادة محاولة القياس
onboarding-automatic_proportions-check_height-next_step = انهم بخير
onboarding-automatic_proportions-check_floor_height-title = قم بقياس الارتفاع عن الأرض (اختياري)
onboarding-automatic_proportions-check_floor_height-description = في بعض الحالات، قد لا يتم ضبط الارتفاع عن الأرض بشكل صحيح بواسطة سماعة الرأس، مما يتسبب في قياس ارتفاع سماعة الرأس على أنه أعلى مما ينبغي. يمكنك قياس "الارتفاع" عن الأرض لتصحيح ارتفاع سماعة الرأس.
# All the text is in bold!
onboarding-automatic_proportions-check_floor_height-calculation_warning-v2 = ابدأ بقياس و ضع وحدة التحكم على الأرض لقياس ارتفاعها. إذا كنت متأكدا من صحة الارتفاع عن الأرض ، فيمكنك تخطي هذه الخطوة.
# Shows an element below it
onboarding-automatic_proportions-check_floor_height-floor_height = الارتفاع عن الأرض هو:
onboarding-automatic_proportions-check_floor_height-full_height = طولك الكامل المقدر هو:
onboarding-automatic_proportions-check_floor_height-measure-start = ابدأ القياس
onboarding-automatic_proportions-check_floor_height-measure-stop = توقف عن القياس
onboarding-automatic_proportions-check_floor_height-measure-reset = إعد محاولة القياس
onboarding-automatic_proportions-check_floor_height-skip_step = تخطي الخطوة وحفظ
onboarding-automatic_proportions-check_floor_height-next_step = استخدم الارتفاع عن الأرض وحفظه
onboarding-automatic_proportions-start_recording-title = استعد للتحرك
onboarding-automatic_proportions-start_recording-description = سنقوم الآن بتسجيل بعض الوضعيات والحركات المحددة. ستتم مطالبتك بذلك في الشاشة التالية. كن مستعدا للبدء عند الضغط على الزر!
onboarding-automatic_proportions-start_recording-next = بدء التسجيل
@@ -1165,35 +837,11 @@ onboarding-automatic_proportions-verify_results-redo = إعادة التسجيل
onboarding-automatic_proportions-verify_results-confirm = تبدو صحيحة
onboarding-automatic_proportions-done-title = تم قياس الجسم و حفظه.
onboarding-automatic_proportions-done-description = اكتملت معايرة نسب جسمك!
onboarding-automatic_proportions-error_modal-v2 =
<b>تحذير:</b> حدث خطأ أثناء تقدير النسب!
من المحتمل أن تكون هذه مشكلة معايرة التركيب. تأكد من أن التعقب يعمل بشكل صحيح قبل المحاولة مرة أخرى.
يرجى <docs>التحقق من التعليمات</docs> أو الانضمام إلى <discord>Discord</discord> للحصول على المساعدة ^_^
onboarding-automatic_proportions-error_modal-confirm = مفهوم!
onboarding-automatic_proportions-smol_warning =
الارتفاع الذي تم تكوينه هو { $height } و هو أصغر من الحد الأدنى للارتفاع المقبول البالغ { $minHeight }.
<b>يرجى إعادة القياسات والتأكد من صحتها.</b>
onboarding-automatic_proportions-smol_warning-cancel = الرجوع
## User height calibration
## Stay Aligned setup
## Home
home-no_trackers = لم يتم الكشف أو تعيين عن أي جهاز تعقب
## Trackers Still On notification
trackers_still_on-modal-title = أجهزة التعقب لا تزال قيد التشغيل
trackers_still_on-modal-description =
لا يزال واحد أو أكثر من أجهزة التعقب قيد التشغيل.
هل مازلت تريد الخروج من SlimeVR؟
trackers_still_on-modal-confirm = الخروج من SlimeVR
trackers_still_on-modal-cancel = انتظر...
## Status system
status_system-StatusTrackerReset = يوصى بإجراء إعادة تعيين كاملة نظرًا لعدم تعديل واحد أو أكثر من أجهزة التعقب.
@@ -1203,67 +851,3 @@ status_system-StatusSteamVRDisconnected =
*[other] حاليًا غير متصل بـ SteamVR عبر برنامج تشغيل SlimeVR.
}
status_system-StatusTrackerError = يحتوي جهاز التعقب { $trackerName } على خطأ.
status_system-StatusUnassignedHMD = يجب تعيين سماعة رأس VR كجهاز تعقب للرأس.
## Firmware tool globals
firmware_tool-next_step = الخطوة التالية
firmware_tool-previous_step = الخطوة السابقة
firmware_tool-ok = تبدو جيدة
firmware_tool-retry = اعادة المحاولة
firmware_tool-loading = تحميل...
## Firmware tool Steps
firmware_tool = أداة البرامج الثابتة DIY
firmware_tool-description = يسمح لك بتكوين و لتحديث أجهزة التعقب DIY الخاصة بك
firmware_tool-not_available = عفوا ، أداة البرامج الثابتة غير متوفرة في الوقت الحالي. عد لاحقا!
firmware_tool-not_compatible = أداة البرنامج الثابت غير متوافقة مع هذا الإصدار من الخادم. يرجى تحديث الخادم الخاص بك!
firmware_tool-flash_method_step = طريقة التثبيت
firmware_tool-flash_method_step-description = الرجاء حدد طريقة التثبيت التي تريد استخدامها
firmware_tool-flashbtn_step = اضغط على زر التمهيد
firmware_tool-flashbtn_step-description = قبل الانتقال إل الخطوة التالية، هناك بعض الأشياء التي عليك القيام بها
firmware_tool-flashbtn_step-board_SLIMEVR = أوقف تشغيل جهاز التعقب، قم بإزالة العلبة (إن وجدت)، وقم بتوصيل كابل USB بهذا الكمبيوتر ، ثم قم بإحدى الخطوات التالية وفقا لمراجعة لوحة SlimeVR:
## firmware tool build status
## Firmware update status
## Dedicated Firmware Update Page
## Tray Menu
tray_menu-show = عرض
tray_menu-hide = إخفاء
tray_menu-quit = انهاء
## First exit modal
tray_or_exit_modal-title = ماذا يجب أن يفعل زر الإغلاق؟
# Multiline text
tray_or_exit_modal-description =
يتيح لك ذلك اختيار ما إذا كنت تريد الخروج من الخادم أو تصغيره إلى علبة النظام عند الضغط على زر الإغلاق.
يمكنك تغيير هذا لاحقا في إعدادات الواجهة!
tray_or_exit_modal-radio-exit = الخروج عند الإغلاق
tray_or_exit_modal-radio-tray = تصغير إلى علبة النظام
tray_or_exit_modal-submit = احفظ
tray_or_exit_modal-cancel = إلغاء
## Unknown device modal
unknown_device-modal-title = تم العثور على جهاز تعقب جديد!
unknown_device-modal-description =
هناك جهاز تعقب جديد مع عنوان MAC <b>{ $deviceId }</b>.
هل تريد توصيله ب SlimeVR؟
unknown_device-modal-confirm = أكيد
unknown_device-modal-forget = تجاهلها
## Error collection consent modal
## Tracking checklist section

Some files were not shown because too many files have changed in this diff Show More