Freya rewrite

This is v2 of OneClient. Removed all web-related stuff.

Co-Authored-By: Julian Chang <wyvest@polyfrost.org>
This commit is contained in:
LynithDev 2026-07-05 16:36:12 -04:00 • committed by Julian Chang
parent a10378e9d7
commit 9ab0a0ecdd
No known key found for this signature in database
1103 changed files with 53832 additions and 80740 deletions

View file

@ -1,34 +0,0 @@
{{#isMacOS}}
[target.x86_64-apple-darwin]
rustflags = ["-Csplit-debuginfo=unpacked"]
[target.aarch64-apple-darwin]
rustflags = ["-Csplit-debuginfo=unpacked"]
{{/isMacOS}}
{{#isWin}}
[target.'cfg(windows)']
rustflags = [
"-C",
"link-args=/STACK:16777220"
]
[target.x86_64-pc-windows-msvc]
{{#hasLLD}}
linker = "lld-link.exe"
{{/hasLLD}}
{{/isWin}}
{{#isLinux}}
[target.x86_64-unknown-linux-gnu]
{{#hasLLD}}
linker = "clang"
rustflags = ["-C", "link-args=-fuse-ld={{{linker}}}"]
{{/hasLLD}}
[target.aarch64-unknown-linux-gnu]
{{#hasLLD}}
linker = "clang"
rustflags = ["-C", "link-args=-fuse-ld={{{linker}}}"]
{{/hasLLD}}
{{/isLinux}}

View file

@ -1 +0,0 @@
cognitive-complexity-threshold = 30

View file

@ -1,3 +0,0 @@
# Used by sea-orm-cli
# DOES NOT AFFECT THE CODE
DATABASE_URL=sqlite:///path/to/user_data.db?mode=rwc

View file

@ -1,4 +0,0 @@
code_review:
comment_severity_threshold: LOW
ignore_patterns:
- '**/*.gen.ts'

View file

@ -1,14 +0,0 @@
name: Publish Artifacts
description: Publish Artifacts
inputs:
target:
description: Target triples for build artifact
profile:
description: '''debug'' or ''release'''
os:
description: '''darwin'', ''windows'', or ''linux'''
arch:
description: '''x86_64'' or ''aarch64'''
runs:
using: node20
main: dist/index.js

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,26 +0,0 @@
{
"name": "@onelauncher/action-publish-artifacts",
"version": "1.1.7",
"private": true,
"description": "Next-generation open source Minecraft launcher",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git@github.com:polyfrost/onelauncher",
"directory": ".github/actions/publish-artifacts"
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {
"build": "rimraf dist && ncc build src/index.ts -m -q --license ../../LICENSE"
},
"dependencies": {
"@actions/artifact": "^2.1.9",
"@actions/core": "^1.10.1",
"@actions/glob": "^0.5.0",
"@actions/io": "^1.1.3"
},
"devDependencies": {
"@vercel/ncc": "^0.38.2"
}
}

View file

@ -1,147 +0,0 @@
import process from 'node:process';
import client from '@actions/artifact';
import core from '@actions/core';
import glob from '@actions/glob';
import io from '@actions/io';
import { exists } from '@actions/io/lib/io-util';
type OS = 'darwin' | 'windows' | 'linux';
type Arch = 'x64' | 'arm64';
type Profile = 'debug' | 'release' | 'dev' | 'dev-debug';
interface Updater {
bundle: string;
bundleExt: string;
archiveExt: string;
}
interface TargetConfig {
bundle: string;
ext: string;
}
interface BuildTarget {
updater: false | Updater;
standalone: Array<TargetConfig>;
}
const osTargets = {
darwin: {
updater: {
bundle: 'macos',
bundleExt: 'app',
archiveExt: 'tar.gz',
},
standalone: [
{ ext: 'dmg', bundle: 'dmg' },
],
},
windows: {
updater: {
bundle: 'msi',
bundleExt: 'msi',
archiveExt: 'zip',
},
standalone: [
{ ext: 'msi', bundle: 'msi' },
],
},
linux: {
updater: false, // TODO
standalone: [
{ ext: 'deb', bundle: 'deb' },
],
},
} satisfies Record<OS, BuildTarget>;
const getInput = <T = string>(name: string): T => core.getInput(name) as T;
const os: OS = getInput('os');
const arch: Arch = getInput('arch');
const profile: Profile = getInput('profile');
const target = getInput('target');
const binaryName = getInput<string | undefined>('binary');
const targetDir = `target/${target}`;
const bundleDir = `${targetDir}/${profile}/bundle`;
const artifactsDir = '.artifacts';
const artifactBase = `OneLauncher-${os}-${arch}`;
const frontendBundle = 'apps/frontend/dist.tar.xz';
const updaterName = `OneLauncher-Updater-${os}-${arch}`;
const frontendName = `OneLauncher-Frontend-${os}-${arch}`;
const globFiles = async (pattern: string): Promise<Array<string>> => await (await glob.create(pattern)).glob();
async function uploadFrontend() {
if (!(await exists(frontendBundle))) {
console.error('frontend archive missing!');
return;
}
const artifactName = `${frontendName}.tar.xz`;
const artifactPath = `${artifactsDir}/${artifactName}`;
await io.cp(frontendBundle, artifactPath);
await client.uploadArtifact(artifactName, [artifactPath], artifactsDir);
}
async function uploadUpdater(updater: BuildTarget['updater']) {
if (!updater)
return;
const { bundle, bundleExt, archiveExt } = updater;
const fullExt = `${bundleExt}.${archiveExt}`;
const files = await globFiles(`${bundleDir}/${bundle}/*.${fullExt}*`);
const updaterPath = files.find(f => f.endsWith(fullExt));
if (!updaterPath)
throw new Error(`failed to find updater path in ${files.join(',')}`);
const artifactPath = `${artifactsDir}/${updaterName}.${archiveExt}`;
await io.cp(updaterPath, artifactPath);
await io.cp(`${updaterPath}.sig`, `${artifactPath}.sig`);
await client.uploadArtifact(updaterName, [artifactPath, `${artifactPath}.sig`], artifactsDir);
}
async function uploadStandalone({ bundle, ext }: TargetConfig) {
const files = await globFiles(`${bundleDir}/${bundle}/*.${ext}*`);
const standalonePath = files.find(f => f.endsWith(ext));
if (!standalonePath)
throw new Error(`failed to find standalone path in ${files.join(',')}`);
const artifactName = `${artifactBase}.${ext}`;
const artifactPath = `${artifactsDir}/${artifactName}`;
await io.cp(standalonePath, artifactPath, { recursive: true });
await client.uploadArtifact(artifactName, [artifactPath], artifactsDir);
}
async function uploadBinary() {
if (typeof binaryName !== 'string')
return;
const file = binaryName || 'onelauncher_gui';
const binaryPath = `${targetDir}/${file}`;
if (!(await exists(binaryPath))) {
console.error('binary missing!');
return;
}
await io.cp(binaryPath, `${artifactsDir}/${file}`);
await client.uploadArtifact(file, [`${artifactsDir}/${file}`], artifactsDir);
}
async function run() {
await io.mkdirP(artifactsDir);
const { updater, standalone } = osTargets[os];
await Promise.all([
uploadUpdater(updater),
uploadFrontend(),
uploadBinary(),
...standalone.map(uploadStandalone),
]);
}
run().catch((error) => {
console.error(error);
process.exit(1);
});

View file

@ -1,9 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"allowImportingTsExtensions": false,
"noEmit": true,
"skipLibCheck": true
},
"exclude": ["**/dist/**"]
}

View file

@ -1,69 +0,0 @@
name: Setup System and Rust
description: Setup System and Rust
inputs:
token:
description: GitHub token
required: false
default: ''
setup-arg:
description: Argument for the system setup script
required: false
default: ''
runs:
using: composite
steps:
- name: (macOS) Install bash@latest
shell: bash
if: ${{ runner.os == 'macOS' }}
run: brew install bash
- name: (Linux) Install mold@latest
shell: bash
if: ${{ runner.os == 'Linux' }}
run: |
curl -L# 'https://github.com/rui314/mold/releases/download/v2.33.0/mold-2.33.0-x86_64-linux.tar.gz' \
| sudo tar -xzf- -C /usr/local
- name: (Linux) Cleanup Runner
shell: bash
if: ${{ runner.os == 'Linux' }}
run: |
set -eux
if dpkg -l | grep i386; then
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386" || true
sudo dpkg --remove-architecture i386 || true
fi
sudo apt-get remove libunwind-* || true
- name: (Linux/macOS) - Setup System
shell: bash
if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }}
run: ./packages/scripts/setup.sh ${{ inputs.setup-arg }}
- name: (Windows) - Setup System
if: ${{ runner.os == 'Windows' }}
shell: powershell
run: ./packages/scripts/setup.ps1
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm
with:
token: ${{ github.token }}
- name: Setup Shared Libraries
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
NODE_ENV: debug
run: pnpm prep
- name: Override linuxdeploy version (fixes AppImage)
shell: bash
if: matrix.platform == 'ubuntu-22.04'
run: |
# Pin linuxdeploy version to prevent @tauri-apps/cli-linux-x64-gnu from pulling in an outdated version
TAURI_TOOLKIT_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
mkdir -p "$TAURI_TOOLKIT_PATH"
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage -O "$TAURI_TOOLKIT_PATH/linuxdeploy-x86_64.AppImage"
chmod +x "$TAURI_TOOLKIT_PATH/linuxdeploy-x86_64.AppImage"

View file

@ -1,29 +0,0 @@
name: Setup Pnpm
description: Setup Pnpm
inputs:
token:
description: GitHub token
required: false
default: ''
runs:
using: composite
steps:
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v4
with:
token: ${{ inputs.token }}
check-latest: true
node-version-file: .nvmrc
cache: pnpm
- name: Install dependencies
shell: ${{ runner.os == 'Windows' && 'powershell' || 'bash' }}
env:
NODE_ENV: debug
GITHUB_TOKEN: ${{ inputs.token }}
run: pnpm install --frozen-lockfile

View file

@ -1,42 +0,0 @@
name: Setup Rust
description: Setup Rust
inputs:
target:
description: Toolchain target triple
required: false
save-cache:
description: Whether or not to save the rust cache
required: false
default: 'false'
restore-cache:
description: Whether or not to restore the rust cache
required: false
default: 'true'
runs:
using: composite
steps:
# - name: Install Rust Stable
# uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a
# with:
# toolchain: stable
# targets: ${{ inputs.target }}
# components: clippy, rustfmt
- name: Install Rust Nightly
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-07-23
targets: ${{ inputs.target }}
components: cargo, clippy, rustc, rustfmt
- name: Cache Rust
if: ${{ inputs.restore-cache == 'true' }}
uses: Swatinem/rust-cache@master
with:
key: ${{ inputs.target }}
save-if: ${{ inputs.save-cache }}
shared-key: stable-cache
- name: Prepare Cargo
shell: bash
run: echo '{}' | npx -y mustache - .cargo/config.toml.mustache .cargo/config.toml

View file

@ -1,104 +0,0 @@
name: Publish AUR Packages
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g. onelauncher-1.0.3)'
required: true
jobs:
resolve:
runs-on: ubuntu-latest
outputs:
app: ${{ steps.parse.outputs.app }}
version: ${{ steps.parse.outputs.version }}
steps:
- name: Parse release tag
id: parse
run: |
TAG="${{ github.event.release.tag_name || inputs.tag }}"
if [[ "$TAG" =~ ^onelauncher-(.+)$ ]]; then
echo "app=onelauncher" >> "$GITHUB_OUTPUT"
echo "version=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT"
elif [[ "$TAG" =~ ^oneclient-(.+)$ ]]; then
echo "app=oneclient" >> "$GITHUB_OUTPUT"
echo "version=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT"
else
echo "::error::Tag '$TAG' does not match onelauncher-* or oneclient-*"
exit 1
fi
publish-source:
needs: resolve
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Compute source sha256
id: hash
env:
APP: ${{ needs.resolve.outputs.app }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
URL="https://github.com/${{ github.repository }}/archive/refs/tags/${APP}-${VERSION}.tar.gz"
SHA=$(curl -sL "$URL" | sha256sum | cut -d' ' -f1)
echo "sha256=$SHA" >> "$GITHUB_OUTPUT"
- name: Prepare PKGBUILD
env:
APP: ${{ needs.resolve.outputs.app }}
VERSION: ${{ needs.resolve.outputs.version }}
SHA256: ${{ steps.hash.outputs.sha256 }}
run: |
PKGBUILD="apps/${APP}/distribution/aur/source/PKGBUILD"
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD"
sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" "$PKGBUILD"
- name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3
with:
pkgname: ${{ needs.resolve.outputs.app }}
pkgbuild: apps/${{ needs.resolve.outputs.app }}/distribution/aur/source/PKGBUILD
commit_username: ${{ secrets.AUR_USERNAME }}
commit_email: ${{ secrets.AUR_EMAIL }}
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
publish-bin:
needs: resolve
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Compute .deb sha256
id: hash
env:
APP: ${{ needs.resolve.outputs.app }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
URL="https://github.com/${{ github.repository }}/releases/download/${APP}-${VERSION}/${APP}_${VERSION}_amd64.deb"
SHA=$(curl -sL "$URL" | sha256sum | cut -d' ' -f1)
echo "sha256=$SHA" >> "$GITHUB_OUTPUT"
- name: Prepare PKGBUILD
env:
APP: ${{ needs.resolve.outputs.app }}
VERSION: ${{ needs.resolve.outputs.version }}
SHA256: ${{ steps.hash.outputs.sha256 }}
run: |
PKGBUILD="apps/${APP}/distribution/aur/bin/PKGBUILD"
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD"
sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" "$PKGBUILD"
- name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3
with:
pkgname: ${{ needs.resolve.outputs.app }}-bin
pkgbuild: apps/${{ needs.resolve.outputs.app }}/distribution/aur/bin/PKGBUILD
commit_username: ${{ secrets.AUR_USERNAME }}
commit_email: ${{ secrets.AUR_EMAIL }}
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}

View file

@ -1,52 +0,0 @@
name: Build and Lint
on:
push:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
es:
name: ES Checks
runs-on: ubuntu-22.04
timeout-minutes: 7
steps:
- name: Prepare Target
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1))" >> "${GITHUB_ENV}"
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pnpm
uses: ./.github/actions/setup-pnpm
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Linting
run: pnpm lint:es
clippy:
name: Rust Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
save-cache: true
restore-cache: true
- name: Setup Packages
uses: ./.github/actions/setup-packages
- name: Create Dummy Dist Directories
run: |
mkdir -p apps/onelauncher/frontend/dist
mkdir -p apps/oneclient/frontend/dist
- name: Run Clippy
run: cargo clippy --all-targets --all-features -- -D warnings

View file

@ -1,201 +0,0 @@
name: OneClient Release Build
on:
workflow_dispatch:
jobs:
publish-tauri:
permissions:
contents: write
# Required so SignPath can download the uploaded build artifact.
actions: read
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest # ARM
args: --target aarch64-apple-darwin
- platform: macos-latest # Intel
args: --target x86_64-apple-darwin
- platform: ubuntu-22.04
args: ''
- platform: windows-latest
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Resolve release version
shell: bash
run: |
VERSION=$(node -p "require('./apps/oneclient/desktop/package.json').version")
echo "RELEASE_VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
target: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
save-cache: true
restore-cache: true
- name: Add x86_64-apple-darwin target
if: contains(matrix.args, 'x86_64-apple-darwin')
run: rustup target add x86_64-apple-darwin
- name: Setup Packages
uses: ./.github/actions/setup-packages
- name: import Apple Developer Certificate
if: startsWith(matrix.platform, 'macos')
# Prevents keychain from locking automatically for 3600 seconds.
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain
- name: verify certificate
if: startsWith(matrix.platform, 'macos')
run: |
IDENTITIES=$(security find-identity -v -p codesigning build.keychain || true)
echo "$IDENTITIES"
CERT_ID=$(echo "$IDENTITIES" | awk -F'"' '/Developer ID Application/ {print $2; exit}')
if [ -z "$CERT_ID" ]; then
CERT_ID=$(echo "$IDENTITIES" | awk -F'"' '/^[[:space:]]*[0-9]+\)/ && $2 != "" {print $2; exit}')
if [ -n "$CERT_ID" ]; then
echo "::warning::Developer ID Application certificate not found. Falling back to: $CERT_ID"
fi
fi
if [ -z "$CERT_ID" ]; then
echo "::error::No code-signing certificate was found in build.keychain. Check APPLE_CERTIFICATE/APPLE_CERTIFICATE_PASSWORD and ensure the cert includes a private key."
exit 1
fi
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported."
- name: Build App (macOS)
if: startsWith(matrix.platform, 'macos')
uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
with:
tagName: oneclient-${{ env.RELEASE_VERSION }}
releaseName: OneClient ${{ env.RELEASE_VERSION }}
releaseDraft: true
prerelease: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-alpha') || false }}
retryAttempts: 3
tauriScript: pnpm -w oneclient:desktop
args: ${{ matrix.args }}
projectPath: ./apps/oneclient/desktop/
uploadUpdaterJson: true
- name: Build App (non-macOS)
if: startsWith(matrix.platform, 'macos') == false
uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: oneclient-${{ env.RELEASE_VERSION }}
releaseName: OneClient ${{ env.RELEASE_VERSION }}
releaseDraft: true
prerelease: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-alpha') || false }}
retryAttempts: 3
tauriScript: pnpm -w oneclient:desktop
args: ${{ matrix.args }}
projectPath: ./apps/oneclient/desktop/
uploadUpdaterJson: true
# ---- Windows Authenticode signing via SignPath (windows-latest only) ----
# The installer must be Authenticode-signed BEFORE its Tauri updater signature is
# computed, otherwise auto-update breaks: the .sig covers the *unsigned* bytes and no
# longer verifies once SignPath alters the exe. tauri-action already built and uploaded
# the unsigned exe + latest.json, so here we: SignPath-sign -> recompute the updater
# .sig over the SIGNED exe -> replace the release exe and patch latest.json's signature.
- name: Locate Windows installer
if: matrix.platform == 'windows-latest'
id: installer
shell: bash
run: echo "path=$(ls target/release/bundle/nsis/*-setup.exe | head -n1)" >> "$GITHUB_OUTPUT"
- name: Upload unsigned installer
if: matrix.platform == 'windows-latest'
id: upload-unsigned
uses: actions/upload-artifact@v4
with:
name: oneclient-windows-installer-unsigned
path: ${{ steps.installer.outputs.path }}
- name: Submit SignPath signing request
if: matrix.platform == 'windows-latest'
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: onelauncher
signing-policy-slug: test-signing
artifact-configuration-slug: onelauncher-installer
github-artifact-id: ${{ steps.upload-unsigned.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: signed
- name: Recompute updater signature over signed installer
if: matrix.platform == 'windows-latest'
id: resign
shell: bash
run: |
signed="$(ls signed/*-setup.exe | head -n1)"
echo "signed=$signed" >> "$GITHUB_OUTPUT"
signed_abs="$(cygpath -w "$(pwd)/$signed")"
pnpm -w oneclient:desktop tauri signer sign \
--private-key "${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}" \
--password "${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}" \
"$signed_abs"
- name: Publish signed installer and patch updater manifest
if: matrix.platform == 'windows-latest'
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: oneclient-${{ env.RELEASE_VERSION }}
run: |
signed="${{ steps.resign.outputs.signed }}"
# Replace the release installer with the Authenticode-signed one (same filename).
gh release upload "$TAG" "$signed" --clobber
# Patch latest.json: swap the windows-x86_64 signature for the recomputed one.
gh release download "$TAG" -p latest.json -D manifest --clobber
SIG="$(cat "$signed.sig")" python3 - <<'PY'
import json, os
p = "manifest/latest.json"
with open(p) as f:
data = json.load(f)
for key, plat in data.get("platforms", {}).items():
if key.startswith("windows-"):
plat["signature"] = os.environ["SIG"]
with open(p, "w") as f:
json.dump(data, f, indent=2)
PY
gh release upload "$TAG" manifest/latest.json --clobber

View file

@ -1,48 +0,0 @@
name: OneLauncher Release Build
on:
workflow_dispatch:
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest # ARM
args: --target aarch64-apple-darwin
- platform: macos-latest # Intel
args: --target x86_64-apple-darwin
- platform: ubuntu-22.04
args: ''
- platform: windows-latest
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
target: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
save-cache: true
restore-cache: true
- name: Setup Packages
uses: ./.github/actions/setup-packages
- uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: onelauncher-__VERSION__
releaseName: OneLauncher __VERSION__
releaseDraft: true
prerelease: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-alpha') || false }}
tauriScript: pnpm -w onelauncher:desktop
args: ${{ matrix.args }}
projectPath: ./apps/onelauncher/desktop/

519
.gitignore vendored
View file

@ -1,519 +1,2 @@
dev.db
dev.db-journal
.cargo/config.toml
.deps/*
vite.config.ts.timestamp*
# Created by https://www.toptal.com/developers/gitignore/api/node,rust,yarn,linux,macos,windows,intellij+all,rust-analyzer,visualstudiocode,intellij,intellij+iml,git
# Edit at https://www.toptal.com/developers/gitignore?templates=node,rust,yarn,linux,macos,windows,intellij+all,rust-analyzer,visualstudiocode,intellij,intellij+iml,git
### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
### Intellij+iml ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### Intellij+iml Patch ###
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
/target
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
### rust-analyzer ###
# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules)
rust-project.json
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### yarn ###
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
.yarn/*
!.yarn/releases
!.yarn/patches
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
# if you are NOT using Zero-installs, then:
# comment the following lines
!.yarn/cache
# and uncomment the following lines
# .pnp.*
# End of https://www.toptal.com/developers/gitignore/api/node,rust,yarn,linux,macos,windows,intellij+all,rust-analyzer,visualstudiocode,intellij,intellij+iml,git

View file

@ -1 +0,0 @@
pnpm lint-staged

9
.npmrc
View file

@ -1,9 +0,0 @@
; make all engine requirements (e.g. node version) strictly kept
engine-strict=true
dedupe-peer-dependents=true
# strict-peer-dependencies=true
auto-install-peers=true
max-old-space-size=4096
enable-pre-post-scripts=true
package-manager-strict=true
package-manager-strict-version=true

1
.nvmrc
View file

@ -1 +0,0 @@
24

View file

@ -1 +0,0 @@
*

View file

@ -1,23 +0,0 @@
max_width = 100
hard_tabs = true
newline_style = "Unix"
use_small_heuristics = "Default"
reorder_imports = true
imports_granularity = "Module"
reorder_modules = true
remove_nested_parens = true
edition = "2024"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
unstable_features = true
deprecated = "warn"
deprecated_in_future = "forbid"
rust_2018_idioms = { level = "warn", priority = -1 }
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_allocation = "warn"
unused_import_braces = "warn"
unused_must_use = "deny"
unused_qualifications = "warn"

View file

@ -1,10 +0,0 @@
{
"recommendations": [
"tauri-apps.tauri-vscode", // Provides Tauri framework support.
"rust-lang.rust-analyzer", // Provides Rust language support.
"vadimcn.vscode-lldb", // Provides Rust debugging support.
"bradlc.vscode-tailwindcss", // Provides UnoCSS intellisense.
"EditorConfig.EditorConfig", // Maintains consistent coding styles.
"dbaeumer.vscode-eslint" // Code formatting using ESLint.
]
}

57
.vscode/launch.json vendored
View file

@ -1,57 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Tauri Development",
"cargo": {
"args": [
"build",
"--profile=dev-debug",
"--manifest-path=./apps/onelauncher/desktop/Cargo.toml",
"--no-default-features"
],
"problemMatcher": "$rustc"
},
"env": {
"RUST_BACKTRACE": "short"
// "RUST_LOG": "onelauncher=trace"
},
"sourceLanguages": ["rust"],
"preLaunchTask": "frontend:dev"
},
{
"type": "lldb",
"request": "launch",
"name": "Tauri Production",
"cargo": {
"args": [
"build",
"--release",
"--manifest-path=./apps/desktop/Cargo.toml"
],
"problemMatcher": "$rustc"
},
"sourceLanguages": ["rust"],
"preLaunchTask": "frontend:build"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug Core",
"cargo": {
"args": ["test", "--no-run", "--lib", "--package=onelauncher"],
"filter": {
"name": "onelauncher",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

201
.vscode/settings.json vendored
View file

@ -1,201 +0,0 @@
{
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
},
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"files.readonlyInclude": {
"**/node_modules/**": true,
"**/*.gen.ts": true
},
"files.watcherExclude": {
"**/_build": true,
"**/.classpath": true,
"**/.flatpak-builder": true,
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/.idea": true,
"**/.project": true,
"**/.settings": true,
"**/.venv": true,
"**/node_modules": true,
"**/.rustup": true,
"**/.cargo": true,
"**/.crates": true,
"**/*.gen.ts": true
},
"rust-analyzer.procMacro.enable": true,
"rust-analyzer.procMacro.attributes.enable": true,
"rust-analyzer.diagnostics.disabled": [
"macro-error" // TODO: Remove this once taurpc upgrades to latest specta
],
"rust-analyzer.assist.preferSelf": true,
"rust-analyzer.procMacro.ignored": {
"async-trait": ["async_trait"]
},
"rust-analyzer.check.command": "check",
"rust-analyzer.diagnostics.experimental.enable": false,
"rust-analyzer.linkedProjects": [],
"rust-analyzer.cargo.extraEnv": {},
"rust-analyzer.check.targets": null,
"rust-analyzer.showUnlinkedFileNotification": false,
// Disable the default formatter
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.runtime": "node",
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"astro",
"toml",
"graphql",
"gql",
"xml",
"css",
"less",
"scss",
"pcss",
"postcss"
],
"eslint.options": {
"flags": ["unstable_ts_config"]
},
// Tailwind CSS intellisense
"tailwindCSS.classFunctions": ["tv"],
// updated 2024-08-26 15:46
// https://github.com/antfu/vscode-file-nesting-config
"explorer.fileNesting.enabled": false,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
".clang-tidy": ".clang-format, .clangd, compile_commands.json",
".env": "*.env, .env.*, .envrc, env.d.ts",
".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*",
".project": ".classpath",
"+layout.svelte": "+layout.ts,+layout.ts,+layout.js,+layout.server.ts,+layout.server.js,+layout.gql",
"+page.svelte": "+page.server.ts,+page.server.js,+page.ts,+page.js,+page.gql",
"ansible.cfg": "ansible.cfg, .ansible-lint, requirements.yml",
"app.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"artisan": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, server.php, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, webpack.mix.js, windi.config.*",
"astro.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"BUILD.bazel": "*.bzl, *.bazel, *.bazelrc, bazel.rc, .bazelignore, .bazelproject, WORKSPACE",
"Cargo.toml": ".clippy.toml, .rustfmt.toml, cargo.lock, clippy.toml, cross.toml, rust-toolchain.toml, rustfmt.toml",
"CMakeLists.txt": "*.cmake, *.cmake.in, .cmake-format.yaml, CMakePresets.json, CMakeCache.txt",
"composer.json": ".php*.cache, composer.lock, phpunit.xml*, psalm*.xml",
"default.nix": "shell.nix",
"deno.json*": "*.env, .env.*, .envrc, api-extractor.json, deno.lock, env.d.ts, import-map.json, import_map.json, jsconfig.*, tsconfig.*, tsdoc.*",
"Dockerfile": "*.dockerfile, .devcontainer.*, .dockerignore, captain-definition, compose.*, docker-compose.*, dockerfile*",
"flake.nix": "flake.lock",
"gatsby-config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, gatsby-browser.*, gatsby-node.*, gatsby-ssr.*, gatsby-transformer.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"gemfile": ".ruby-version, gemfile.lock",
"go.mod": ".air*, go.sum",
"go.work": "go.work.sum",
"hatch.toml": ".editorconfig, .flake8, .isort.cfg, .python-version, hatch.toml, requirements*.in, requirements*.pip, requirements*.txt, tox.ini",
"I*.cs": "$(capture).cs",
"Makefile": "*.mk",
"mix.exs": ".credo.exs, .dialyzer_ignore.exs, .formatter.exs, .iex.exs, .tool-versions, mix.lock",
"next.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, next-env.d.ts, next-i18next.config.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"nuxt.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .nuxtignore, .nuxtrc, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"package.json": "*.code-workspace, .browserslist*, .circleci*, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitmojirc.json, .gitpod*, .huskyrc*, .jslint*, .knip.*, .lintstagedrc*, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .pylintrc, .release-please*.json, .releaserc*, .ruff.toml, .sentry*, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, biome.json*, bower.json, build.config.*, bun.lockb, bunfig.toml, commitlint*, crowdin*, dangerfile*, dlint.json, dprint.json*, electron-builder.*, eslint*, firebase.json, grunt*, gulp*, jenkins*, knip.*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-please*.json, release-tasks.sh, release.config.*, renovate*, rollup.config.*, rspack*, ruff.toml, simple-git-hooks*, sonar-project.properties, stylelint*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, wrangler.toml, xo.config.*, yarn*",
"Pipfile": ".editorconfig, .flake8, .isort.cfg, .python-version, Pipfile, Pipfile.lock, requirements*.in, requirements*.pip, requirements*.txt, tox.ini",
"pubspec.yaml": ".metadata, .packages, all_lint_rules.yaml, analysis_options.yaml, build.yaml, pubspec.lock, pubspec_overrides.yaml",
"pyproject.toml": ".commitlint*, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .flake8, .flowconfig, .isort.cfg, .jslint*, .lintstagedrc*, .markdownlint*, .pdm-python, .pdm.toml, .prettier*, .pylintrc, .python-version, .ruff.toml, .stylelint*, .textlint*, .xo-config*, .yamllint*, MANIFEST.in, Pipfile, Pipfile.lock, biome.json*, commitlint*, dangerfile*, dlint.json, dprint.json*, eslint*, hatch.toml, lint-staged*, pdm.lock, phpcs.xml, poetry.lock, poetry.toml, prettier*, pyproject.toml, pyrightconfig.json, requirements*.in, requirements*.pip, requirements*.txt, ruff.toml, setup.cfg, setup.py, stylelint*, tox.ini, tslint*, xo.config.*",
"quasar.conf.js": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, quasar.extensions.json, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"readme*": "AUTHORS, Authors, BACKERS*, Backers*, CHANGELOG*, CITATION*, CODEOWNERS, CODE_OF_CONDUCT*, CONTRIBUTING*, CONTRIBUTORS, COPYING*, CREDITS, Changelog*, Citation*, Code_Of_Conduct*, Codeowners, Contributing*, Contributors, Copying*, Credits, GOVERNANCE.MD, Governance.md, HISTORY.MD, History.md, LICENSE*, License*, MAINTAINERS, Maintainers, README-*, README_*, RELEASE_NOTES*, Readme-*, Readme_*, Release_Notes*, SECURITY.MD, SPONSORS*, Security.md, Sponsors*, authors, backers*, changelog*, citation*, code_of_conduct*, codeowners, contributing*, contributors, copying*, credits, governance.md, history.md, license*, maintainers, readme-*, readme_*, release_notes*, security.md, sponsors*",
"Readme*": "AUTHORS, Authors, BACKERS*, Backers*, CHANGELOG*, CITATION*, CODEOWNERS, CODE_OF_CONDUCT*, CONTRIBUTING*, CONTRIBUTORS, COPYING*, CREDITS, Changelog*, Citation*, Code_Of_Conduct*, Codeowners, Contributing*, Contributors, Copying*, Credits, GOVERNANCE.MD, Governance.md, HISTORY.MD, History.md, LICENSE*, License*, MAINTAINERS, Maintainers, README-*, README_*, RELEASE_NOTES*, Readme-*, Readme_*, Release_Notes*, SECURITY.MD, SPONSORS*, Security.md, Sponsors*, authors, backers*, changelog*, citation*, code_of_conduct*, codeowners, contributing*, contributors, copying*, credits, governance.md, history.md, license*, maintainers, readme-*, readme_*, release_notes*, security.md, sponsors*",
"README*": "AUTHORS, Authors, BACKERS*, Backers*, CHANGELOG*, CITATION*, CODEOWNERS, CODE_OF_CONDUCT*, CONTRIBUTING*, CONTRIBUTORS, COPYING*, CREDITS, Changelog*, Citation*, Code_Of_Conduct*, Codeowners, Contributing*, Contributors, Copying*, Credits, GOVERNANCE.MD, Governance.md, HISTORY.MD, History.md, LICENSE*, License*, MAINTAINERS, Maintainers, README-*, README_*, RELEASE_NOTES*, Readme-*, Readme_*, Release_Notes*, SECURITY.MD, SPONSORS*, Security.md, Sponsors*, authors, backers*, changelog*, citation*, code_of_conduct*, codeowners, contributing*, contributors, copying*, credits, governance.md, history.md, license*, maintainers, readme-*, readme_*, release_notes*, security.md, sponsors*",
"remix.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, remix.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"requirements.txt": ".editorconfig, .flake8, .isort.cfg, .python-version, requirements*.in, requirements*.pip, requirements*.txt, tox.ini",
"rush.json": "*.code-workspace, .browserslist*, .circleci*, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitmojirc.json, .gitpod*, .huskyrc*, .jslint*, .knip.*, .lintstagedrc*, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .pylintrc, .release-please*.json, .releaserc*, .ruff.toml, .sentry*, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, biome.json*, bower.json, build.config.*, bun.lockb, bunfig.toml, commitlint*, crowdin*, dangerfile*, dlint.json, dprint.json*, electron-builder.*, eslint*, firebase.json, grunt*, gulp*, jenkins*, knip.*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-please*.json, release-tasks.sh, release.config.*, renovate*, rollup.config.*, rspack*, ruff.toml, simple-git-hooks*, sonar-project.properties, stylelint*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, wrangler.toml, xo.config.*, yarn*",
"sanity.config.*": "sanity.cli.*, sanity.types.ts, schema.json",
"setup.cfg": ".editorconfig, .flake8, .isort.cfg, .python-version, MANIFEST.in, requirements*.in, requirements*.pip, requirements*.txt, setup.cfg, tox.ini",
"setup.py": ".editorconfig, .flake8, .isort.cfg, .python-version, MANIFEST.in, requirements*.in, requirements*.pip, requirements*.txt, setup.cfg, setup.py, tox.ini",
"shims.d.ts": "*.d.ts",
"svelte.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, houdini.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, mdsvex.config.js, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vite.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"vite.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"vue.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"*.asax": "$(capture).*.cs, $(capture).*.vb",
"*.ascx": "$(capture).*.cs, $(capture).*.vb",
"*.ashx": "$(capture).*.cs, $(capture).*.vb",
"*.aspx": "$(capture).*.cs, $(capture).*.vb",
"*.axaml": "$(capture).axaml.cs",
"*.bloc.dart": "$(capture).event.dart, $(capture).state.dart",
"*.c": "$(capture).h",
"*.cc": "$(capture).hpp, $(capture).h, $(capture).hxx, $(capture).hh",
"*.cjs": "$(capture).cjs.map, $(capture).*.cjs, $(capture)_*.cjs",
"*.component.ts": "$(capture).component.html, $(capture).component.spec.ts, $(capture).component.css, $(capture).component.scss, $(capture).component.sass, $(capture).component.less",
"*.cpp": "$(capture).hpp, $(capture).h, $(capture).hxx, $(capture).hh",
"*.cs": "$(capture).*.cs",
"*.cshtml": "$(capture).cshtml.cs",
"*.csproj": "*.config, *proj.user, appsettings.*, bundleconfig.json",
"*.css": "$(capture).css.map, $(capture).*.css",
"*.cxx": "$(capture).hpp, $(capture).h, $(capture).hxx, $(capture).hh",
"*.dart": "$(capture).freezed.dart, $(capture).g.dart",
"*.db": "*.db-shm, *.db-wal",
"*.ex": "$(capture).html.eex, $(capture).html.heex, $(capture).html.leex",
"*.fs": "$(capture).fs.js, $(capture).fs.js.map, $(capture).fs.jsx, $(capture).fs.ts, $(capture).fs.tsx, $(capture).fs.rs, $(capture).fs.php, $(capture).fs.dart",
"*.go": "$(capture)_test.go",
"*.java": "$(capture).class",
"*.js": "$(capture).js.map, $(capture).*.js, $(capture)_*.js",
"*.jsx": "$(capture).js, $(capture).*.jsx, $(capture)_*.js, $(capture)_*.jsx, $(capture).less, $(capture).module.less, $(capture).module.less.d.ts, $(capture).scss, $(capture).module.scss, $(capture).module.scss.d.ts",
"*.master": "$(capture).*.cs, $(capture).*.vb",
"*.md": "$(capture).*",
"*.mjs": "$(capture).mjs.map, $(capture).*.mjs, $(capture)_*.mjs",
"*.module.ts": "$(capture).resolver.ts, $(capture).controller.ts, $(capture).service.ts",
"*.mts": "$(capture).mts.map, $(capture).*.mts, $(capture)_*.mts",
"*.pubxml": "$(capture).pubxml.user",
"*.py": "$(capture).pyi",
"*.razor": "$(capture).razor.cs, $(capture).razor.css, $(capture).razor.scss",
"*.resx": "$(capture).*.resx, $(capture).designer.cs, $(capture).designer.vb",
"*.tex": "$(capture).acn, $(capture).acr, $(capture).alg, $(capture).aux, $(capture).bbl, $(capture).blg, $(capture).fdb_latexmk, $(capture).fls, $(capture).glg, $(capture).glo, $(capture).gls, $(capture).idx, $(capture).ind, $(capture).ist, $(capture).lof, $(capture).log, $(capture).lot, $(capture).out, $(capture).pdf, $(capture).synctex.gz, $(capture).toc, $(capture).xdv",
"*.ts": "$(capture).js, $(capture).d.ts.map, $(capture).*.ts, $(capture)_*.js, $(capture)_*.ts",
"*.tsx": "$(capture).ts, $(capture).*.tsx, $(capture)_*.ts, $(capture)_*.tsx, $(capture).less, $(capture).module.less, $(capture).module.less.d.ts, $(capture).scss, $(capture).module.scss, $(capture).module.scss.d.ts, $(capture).css.ts",
"*.vbproj": "*.config, *proj.user, appsettings.*, bundleconfig.json",
"*.vue": "$(capture).*.ts, $(capture).*.js, $(capture).story.vue",
"*.w": "$(capture).*.w, I$(capture).w",
"*.xaml": "$(capture).xaml.cs"
},
"typescript.tsdk": "node_modules/typescript/lib",
"svg.preview.background": "editor"
}

83
.vscode/tasks.json vendored
View file

@ -1,83 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "cargo",
"command": "clippy",
"problemMatcher": ["$rustc"],
"group": {
"kind": "build",
"isDefault": true
},
"label": "rust:clippy",
"args": ["--all-targets", "--all-features", "--all"]
},
{
"type": "npm",
"script": "prepare",
"label": "pnpm:prep",
"group": "none",
"problemMatcher": ["$rustc"]
},
{
"type": "shell",
"label": "start",
"command": "sh",
"args": ["-c", "'pnpm i && pnpm prepare'"],
"problemMatcher": ["$tsc-watch", "$rustc"]
},
{
"type": "shell",
"label": "frontend:dev",
"problemMatcher": {
"base": "$tsc-watch",
"background": {
"activeOnStart": true,
"beginsPattern": "VITE v",
"endsPattern": "http://localhost:8001/"
}
},
"isBackground": true,
"command": "pnpm",
"args": ["onelauncher:frontend", "dev", "--mode=development"],
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "shell",
"label": "frontend:build",
"problemMatcher": "$tsc",
"command": "pnpm",
"args": ["frontend", "build"]
},
{
"type": "cargo",
"command": "run",
"args": ["--manifest-path=./apps/desktop/Cargo.toml", "--no-default-features"],
"env": {
"RUST_BACKTRACE": "short"
// "RUST_LOG": "onelauncher=trace"
},
"problemMatcher": ["$rustc"],
"group": "build",
"label": "rust:run",
"dependsOn": ["frontend:dev"]
},
{
"type": "cargo",
"command": "run",
"args": ["--manifest-path=./apps/desktop/Cargo.toml", "--release"],
"env": {
"RUST_BACKTRACE": "short"
// "RUST_LOG": "onelauncher=trace"
},
"problemMatcher": ["$rustc"],
"group": "build",
"label": "rust:run release",
"dependsOn": ["frontend:build"]
}
]
}

8663
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,264 +1,126 @@
[workspace]
members = [
"apps/oneclient/desktop",
"apps/onelauncher/desktop",
"packages/core",
"packages/entity",
"packages/gamemode",
"packages/macro",
"packages/migration",
"packages/serde_json_path_to_error",
"packages/*",
]
resolver = "2" # virtual workspace resolver so we don't need a root package
resolver = "3"
[workspace.package]
authors = [
"Polyfrost",
"Polyfrost Contributors"
"Polyfrost",
"Polyfrost Contributors",
]
documentation = "https://docs.polyfrost.org/onelauncher"
documentation = "https://github.com/Polyfrost/OneLauncher"
edition = "2024"
homepage = "https://polyfrost.org/projects/onelauncher"
homepage = "https://polyfrost.org/projects/oneclient"
license = "GPL-3.0-only"
readme = "README.md"
repository = "https://github.com/Polyfrost/OneLauncher"
version = "1.1.7"
version = "2.0.0"
[workspace.dependencies]
# workspace
onelauncher_core = { path = "./packages/core" }
onelauncher_entity = { path = "./packages/entity" }
onelauncher_gamemode = { path = "./packages/gamemode" }
onelauncher_macro = { path = "./packages/macro" }
onelauncher_migration = { path = "./packages/migration" }
serde_json_path_to_error = { path = "./packages/serde_json_path_to_error" }
# core
interfrost = { version = "=1.3.2", features = [ "specta" ] }
polyio = { path = "./packages/polyio" }
oneclient_core = { path = "./packages/oneclient_core" }
oneclient_db = { path = "./packages/oneclient_db" }
oneclient_macro = { path = "./packages/oneclient_macro" }
# tauri
tauri = { version = "=2.5.1", features = [
"rustls-tls",
"tracing",
"specta",
"uuid",
"protocol-asset",
] }
tauri-build = { version = "=2.2.0" }
tauri-plugin-clipboard-manager = { version = "=2.2.2" }
tauri-plugin-deep-link = { version = "=2.2.1" }
tauri-plugin-dialog = { version = "=2.2.1" }
tauri-plugin-fs = { version = "=2.2.1", features = ["watch"] }
tauri-plugin-os = { version = "=2.2.1" }
tauri-plugin-global-shortcut = { version = "=2.2.1" }
tauri-plugin-single-instance = { version = "=2.2.3" }
tauri-plugin-updater = { version = "=2.7.1" }
tauri-plugin-window-state = { version = "=2.2.2" }
tauri-plugin-opener = { version = "=2.3.0" }
# code gen
strum = { version = "=0.27.1", features = [ "derive" ] }
specta = { version = "=2.0.0-rc.22", features = [
"chrono",
"uuid",
"serde",
"serde_json",
"function",
"derive",
"tokio",
] }
specta-typescript = { version = "=0.0.9" }
taurpc = { version = "=0.5.0" }
# core (extra)
gamemode_rs = {path = "./packages/gamemode_rs"}
interfrost = {version = "=1.3.2"}
# logging
tracing = { version = "0.1" }
tracing-appender = { version = "0.2" }
tracing-error = { version = "0.2" }
tracing-subscriber = { version = "0.3", features = [
"chrono",
"env-filter"
] }
tracing = {version = "=0.1.44"}
tracing-futures = {version = "=0.2.5"}
tracing-subscriber = {version = "=0.3.23", features = ["env-filter", "fmt", "ansi", "json", "parking_lot", "registry"]}
# serialization & text
base64 = { version = "=0.22.1" }
byteorder = { version = "=1.5.0" }
chrono = { version = "0.4", features = [ "serde" ] }
nom = { version = "=8.0.0" }
rand = { version = "0.8" }
regex = { version = "1.11" }
serde = { version = "1.0", features = [ "derive" ] }
serde_json = { version = "=1.0" }
serde_with = { version = "=3.12.0", features = [
"chrono_0_4",
"json",
"macros"
] }
serde_repr = { version = "=0.1.20" }
toml = { version = "0.8" }
url = { version = "2.5" }
uuid = { version = "1.16", features = [
"serde",
"v4"
] }
# database
sqlx = {version = "=0.9.0", default-features = false, features = [
"runtime-tokio",
"tls-rustls-ring-native-roots",
"sqlite",
"derive",
"macros",
"migrate",
"json",
"uuid",
"chrono",
]}
# network
reqwest = { version = "0.12", default-features = false, features = [
"json",
"stream",
"multipart",
"blocking",
"rustls-tls",
"charset",
"http2",
"macos-system-configuration",
"deflate",
"gzip",
"brotli",
] }
reqwest-websocket = { version = "0.5", default-features = false }
webbrowser = { version = "=1.0.4" }
# io
async-compression = { version = "=0.4.23", features = [
"flate2",
"tokio"
] }
async_zip = { version = "=0.0.17", features = [ "full" ] }
bytes = { version = "=1.10.1" }
dirs = { version = "=6.0.0" }
dunce = { version = "=1.0.5" }
flate2 = { version = "=1.1.1" }
sanitize-filename = { version = "=0.6.0" }
async-tempfile = { version = "=0.7.0" }
tokio = { version = "1", features = [ "full" ] }
tokio-stream = { version = "0.1", features = [ "fs" ] }
tokio-util = { version = "0.7", features = [ "compat" ] }
governor = { version = "0.10.4" }
encoding_rs = { version = "0.8.35" }
# crypto
digest = { version = "0.10" }
md-5 = { version = "0.10" }
murmur2 = { version = "=0.1.0" }
sha1 = { version = "0.10" }
sha2 = { version = "0.10" }
p256 = { version = "0.13", features = [ "ecdsa" ] }
# db
sea-orm = { version = "1.1", features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
"with-chrono",
"with-json",
"with-uuid",
"sqlite-use-returning-for-3_35",
] }
sea-orm-migration = { version = "1.1", features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
"with-chrono",
"with-json",
"with-uuid",
] }
# utils
anyhow = { version = "1" }
async-trait = { version = "=0.1.88" }
# async
async-trait = {version = "=0.1.89"}
futures-util = {version = "=0.3.32", features = ["tokio-io"]}
futures-lite = { version = "=2.6.1" }
reqwest = {version = "=0.13.3", default-features = false, features = [
"json",
"stream",
"multipart",
"blocking",
"rustls",
"charset",
"http2",
"deflate",
"gzip",
"brotli",
"form",
"hickory-dns",
]}
tokio = {version = "=1.52.3", features = ["full"]}
tokio-stream = {version = "=0.1.18", features = ["full"]}
tokio-util = {version = "=0.7.18", features = ["full"]}
async_zip = { version = "=0.0.18", features = ["full"]}
astral-tokio-tar = { version = "=0.6.2" }
async-tempfile = { version = "=0.7.0", features = ["uuid"] }
async-stream = { version = "=0.3.6" }
cc = { version = "1" }
discord-rich-presence = { version = "=0.2.5" }
futures = { version = "0.3" }
futures-lite = { version = "2.6" }
indicatif = { version = "0.17" }
libc = { version = "0.2" }
merge = { version = "=0.2.0" }
sysinfo = { version = "=0.34.2", features = [ "serde" ] }
thiserror = { version = "2" }
whoami = { version = "=1.6.0" }
opener = { version = "=0.8.2" }
shadow-rs = { version = "1.5.0", default-features = false }
async-compression = { version = "=0.4.42", features = ["all"] }
# macro
paste = { version = "1" }
quote = { version = "1" }
syn = { version = "2" }
proc-macro2 = { version = "1" }
# crypto (content hashing)
digest = { version = "=0.10.7", features = ["std"] }
sha1 = { version = "=0.10.6", default-features = false }
sha2 = { version = "=0.10.9", default-features = false }
md-5 = { version = "=0.10.6", default-features = false }
p256 = { version = "=0.13.2", features = ["ecdsa", "pkcs8"] }
byteorder = { version = "=1.5.0" }
[patch.crates-io]
# tauri-specta = { git = "https://github.com/specta-rs/tauri-specta", rev = "6fe9532a44b8b683b4dacd178b0120ee585bea23" }
specta = { git = "https://github.com/specta-rs/specta", rev = "5758221bc9a11d6e827851273fd665662e90ceef" }
specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "5758221bc9a11d6e827851273fd665662e90ceef" }
# util
base64 = {version = "=0.22.1"}
chrono = {version = "=0.4.44", features = ["serde"]}
directories = {version = "=6.0.0"}
rand = {version = "=0.10.1"}
serde = {version = "=1.0.228"}
serde_json = {version = "=1.0.150"}
url = {version = "=2.5.8", features = ["serde"]}
uuid = {version = "=1.23.1", features = ["serde", "v4"]}
sanitize-filename = { version = "=0.6.0" }
regex = { version = "=1.12.3" }
parking_lot = { version = "=0.12.5" }
dunce = { version = "=1.0.5" }
bytes = { version = "=1.11.1" }
indicatif = { version = "=0.18.4", features = [ "tokio" ]}
sysinfo = { version = "=0.39.5" }
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(feature, values("used_linker"))' ] }
# util (codegen/macro)
proc-macro2 = {version = "=1.0.106"}
quote = {version = "=1.0.45"}
thiserror = {version = "=2.0.18" }
anyhow = {version = "=1.0.102"}
strum = {version = "=0.28.0", features = ["derive"]}
syn = {version = "=2.0.117"}
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
cast_lossless = "warn"
cast_precision_loss = "allow"
complexity = { level = "warn", priority = -1 }
correctness = { level = "warn", priority = -1 }
dbg_macro = "warn"
deprecated_cfg_attr = "warn"
future_not_send = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
needless_return = "allow"
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
perf = { level = "warn", priority = -1 }
separated_literal_suffix = "warn"
significant_drop_tightening = "allow" # tmp
struct_field_names = "allow"
style = { level = "warn", priority = -1 }
suspicious = { level = "warn", priority = -1 }
unnecessary_cast = "warn"
image = {version = "=0.25.10", default-features = false, features = ["png", "jpeg"]}
arboard = {version = "=3.6.1"}
trash = {version = "=5.2.6"}
[profile.dev]
# add debug symbols/logic and optimize for reduced build time (parallel)
codegen-units = 256 # make parallel incremental builds faster
debug = true # add debug info
incremental = true # enable incremental compilation
lto = false # disable link time optimizations, reduce compile time
opt-level = 0 # reduce optimizations
panic = "unwind" # add unwind panic logic because some deps require it
rpath = false # disable rpath
split-debuginfo = "unpacked" # unpack debug info
strip = "none" # don't remove debug symbols
# add debug symbols/logic and optimize for reduced build time (parallel)
[profile.dev-debug]
codegen-units = 256 # make parallel incremental builds faster
debug = "full" # add full debug info
incremental = true # enable incremental compilation
inherits = "dev" # extend the dev profile
lto = "off" # disable link time optimizations, reduce compile time
opt-level = 0 # reduce optimizations
split-debuginfo = "none" # don't split debug info
strip = "none" # don't remove debug symbols
# set the settings for build scripts and proc-macros.
[profile.dev.build-override]
opt-level = 3 # optimize build
# set the default for dependencies, except workspace members.
[profile.dev.package."*"]
incremental = false # disable incremental compilation
opt-level = 3 # optimize build
# set the default for dependencies, except workspace members.
[profile.dev-debug.package."*"]
debug = "full" # add full debug info
incremental = false # disable incremental compilation
inherits = "dev" # extend the dev profile
opt-level = 3 # optimize build
# optimize for speed and reduce size for release
[profile.release]
codegen-units = 1 # compile crate one after another for better optimization
lto = true # enable link time optimization
opt-level = "s" # optimize binary
panic = "abort" # strip panic cleanup logic
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true
panic = "abort"
debug = false
[profile.release-profiling]
inherits = "release"
strip = false
debug = true

View file

@ -1,3 +0,0 @@
# Generated by Tauri
.cargo/
/gen

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- allows us to load minecraft libraries -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- allows us to set DYLD_FALLBACK_LIBRARY_PATH set by cargo which breaks minecraft -->
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<!-- allows us to request microphone permissions (for things like voice chat) -->
<key>com.apple.security.device.audio-input</key>
<true/>
<!-- allows us to request camera permissions (for things like voice chat) -->
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>

View file

@ -1,97 +0,0 @@
[package]
name = "oneclient_gui"
default-run = "oneclient_gui"
description = "Next-generation open source Minecraft launcher"
version = { workspace = true }
license = { workspace = true }
edition = { workspace = true }
repository = { workspace = true }
documentation = { workspace = true }
readme = { workspace = true }
homepage = { workspace = true }
authors = { workspace = true }
[lib]
name = "oneclient_gui"
crate-type = [
"staticlib",
"cdylib",
"lib"
]
[lints]
workspace = true
[build-dependencies]
tauri-build = { workspace = true }
[dependencies]
# core
onelauncher_core = { workspace = true, features = [ "tauri" ] }
interfrost = { workspace = true }
# tauri
tauri-plugin-window-state = { workspace = true }
tauri-plugin-single-instance = { workspace = true }
tauri-plugin-updater = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-fs = { workspace = true }
tauri-plugin-os = { workspace = true }
tauri-plugin-global-shortcut = { workspace = true }
tauri-plugin-deep-link = { workspace = true }
# code gen
taurpc = { workspace = true }
specta = { workspace = true }
specta-typescript = { workspace = true }
strum = { workspace = true }
# logging
tracing = { workspace = true }
tracing-error = { workspace = true }
# serialization & text
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
# io
tokio = { workspace = true }
# utils
thiserror = { workspace = true }
anyhow = { workspace = true }
reqwest = { workspace = true }
[dependencies.tauri]
workspace = true
features = [
"macos-private-api",
"image-ico",
"image-png",
"protocol-asset",
"specta"
]
[dev-dependencies]
sea-orm = { workspace = true }
[features]
default = [
"custom-protocol",
"devtools"
]
devtools = [ "tauri/devtools" ]
custom-protocol = [ "tauri/custom-protocol" ]
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.3"
objc2-app-kit = { version = "0.3.2", features = [
"NSWindow",
"NSButton",
"NSControl",
"NSView"
] }
objc2-foundation = { version = "0.3.2", features = [ "NSObject" ] }

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- registers our uri handler on macos -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>OneClient</string>
<key>CFBundleURLSchemes</key>
<array>
<string>oneclient</string>
</array>
</dict>
</array>
<!-- allows us to request camera permissions (for things like voice chat) -->
<key>NSCameraUsageDescription</key>
<string>A Minecraft mod is requesting access to your camera.</string>
<!-- allows us to request microphone permissions (for things like voice chat) -->
<key>NSMicrophoneUsageDescription</key>
<string>A Minecraft mod is requesting access to your microphone.</string>
</dict>
</plist>

View file

@ -1,3 +0,0 @@
fn main() {
tauri_build::build();
}

View file

@ -1,55 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "capabilities for the main window",
"windows": ["*"],
"permissions": [
"core:app:default",
"core:webview:default",
"core:image:default",
"core:menu:default",
"core:path:default",
"core:resources:default",
"core:tray:default",
"core:window:default",
"core:window:allow-close",
"core:window:allow-create",
"core:window:allow-destroy",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-toggle-maximize",
"core:window:allow-internal-toggle-maximize",
"core:window:allow-set-decorations",
"core:window:allow-set-title-bar-style",
"core:webview:allow-internal-toggle-devtools",
"core:event:default",
"clipboard-manager:allow-write-text",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-confirm",
"deep-link:default",
"updater:default",
"fs:allow-download-read",
"fs:allow-download-write",
"fs:allow-data-read-recursive",
"fs:allow-data-write-recursive",
"fs:allow-watch",
"os:deny-hostname",
"os:allow-locale",
"os:allow-os-type",
"os:allow-platform",
"os:allow-version",
"os:allow-arch",
"os:deny-exe-extension",
"os:allow-family",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister"
]
}

View file

@ -1,28 +0,0 @@
{
"name": "@oneclient/desktop",
"type": "module",
"version": "1.1.7",
"private": true,
"description": "Next-generation open source Minecraft launcher",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git@github.com:polyfrost/onelauncher",
"directory": "apps/desktop"
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {
"build": "tauri build",
"dev": "tauri dev",
"tauri": "tauri",
"tauri-ext": "pnpm --filter @onelauncher/scripts -- tauri",
"cargo": "cargo"
},
"dependencies": {
"@oneclient/frontend": "workspace:*"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.4"
}
}

View file

@ -1,383 +0,0 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::Context;
use onelauncher_core::api;
use onelauncher_core::api::cluster::dao::ClusterId;
use onelauncher_core::api::packages::modpack::data::{ModpackArchive, ModpackFileKind};
use onelauncher_core::entity::{clusters, packages};
use onelauncher_core::error::LauncherResult;
use crate::ext::updater::Update;
use crate::oneclient::bundle_updates::ApplyBundleUpdatesResult;
use crate::oneclient::bundles::BundlesManager;
use crate::oneclient::clusters::{OnlineClusterManifest, get_data_storage_versions};
use tauri::{AppHandle, Runtime};
const BUNDLE_DIRS: &[&str] = &["mods", "resourcepacks", "shaderpacks"];
/// Recursively copy all files from `src` into `dst`, creating directories as needed.
async fn copy_dir_all(src: &Path, dst: &Path) -> onelauncher_core::error::LauncherResult<()> {
tokio::fs::create_dir_all(dst)
.await
.map_err(anyhow::Error::from)?;
let mut entries = tokio::fs::read_dir(src)
.await
.map_err(anyhow::Error::from)?;
while let Some(entry) = entries.next_entry().await.map_err(anyhow::Error::from)? {
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
Box::pin(copy_dir_all(&src_path, &dst_path)).await?;
} else {
tokio::fs::copy(&src_path, &dst_path)
.await
.map_err(anyhow::Error::from)?;
}
}
Ok(())
}
#[taurpc::procedures(path = "oneclient", export_to = "../frontend/src/bindings.gen.ts")]
pub trait OneClientApi {
#[taurpc(alias = "getClustersGroupedByMajor")]
async fn get_clusters_grouped_by_major() -> LauncherResult<HashMap<u32, Vec<clusters::Model>>>;
#[taurpc(alias = "getBundlesFor")]
async fn get_bundles_for(cluster_id: ClusterId) -> LauncherResult<Vec<ModpackArchive>>;
#[taurpc(alias = "getVersions")]
async fn get_versions() -> LauncherResult<OnlineClusterManifest>;
#[taurpc(alias = "extractBundleOverrides")]
async fn extract_bundle_overrides(
bundle_path: PathBuf,
cluster_id: ClusterId,
) -> LauncherResult<()>;
#[taurpc(alias = "checkForUpdate")]
async fn check_for_update<R: Runtime>(
app_handle: AppHandle<R>,
) -> LauncherResult<Option<Update>>;
#[taurpc(alias = "installUpdate")]
async fn install_update<R: Runtime>(app_handle: AppHandle<R>) -> LauncherResult<()>;
#[taurpc(alias = "downloadPackageFromBundle")]
async fn download_package_from_bundle(
package: ModpackFileKind,
cluster_id: ClusterId,
bundle_name: String,
skip_compatibility: Option<bool>,
) -> LauncherResult<packages::Model>;
#[taurpc(alias = "updateBundlePackages")]
async fn update_bundle_packages(
cluster_id: ClusterId,
) -> LauncherResult<ApplyBundleUpdatesResult>;
#[taurpc(alias = "isBundleSyncing")]
async fn is_bundle_syncing() -> LauncherResult<bool>;
#[taurpc(alias = "cacheArt")]
async fn cache_art(path: String) -> LauncherResult<String>;
#[taurpc(alias = "refreshArt")]
async fn refresh_art(path: String) -> LauncherResult<()>;
#[taurpc(alias = "copyClusterContent")]
async fn copy_cluster_content(
source_id: ClusterId,
target_id: ClusterId,
) -> LauncherResult<CopyClusterResult>;
}
#[taurpc::ipc_type]
pub struct CopyClusterResult {
/// Files that had no compatible provider version and were copied verbatim from the source.
pub fallback_files: Vec<String>,
}
#[taurpc::ipc_type]
pub struct OneClientApiImpl;
#[taurpc::resolvers]
impl OneClientApi for OneClientApiImpl {
async fn get_clusters_grouped_by_major(
self,
) -> LauncherResult<HashMap<u32, Vec<clusters::Model>>> {
let clusters = onelauncher_core::api::cluster::dao::get_all_clusters().await?;
let mut mapped: HashMap<u32, Vec<clusters::Model>> = HashMap::new();
for cluster in clusters {
let mut parts = cluster.mc_version.splitn(3, '.');
let major: u32 = match parts.next() {
// Old format: 1.X[.Y]
Some("1") => match parts.next().and_then(|v| v.parse().ok()) {
Some(v) => v,
None => continue,
},
// New format: YY.N[.P]
Some(year) => match year.parse() {
Ok(v) => v,
Err(_) => continue,
},
None => continue,
};
mapped.entry(major).or_default().push(cluster);
}
Ok(mapped)
}
async fn get_bundles_for(self, cluster_id: ClusterId) -> LauncherResult<Vec<ModpackArchive>> {
let cluster = onelauncher_core::api::cluster::dao::get_cluster_by_id(cluster_id)
.await?
.ok_or_else(|| {
onelauncher_core::error::LauncherError::from(anyhow::anyhow!(
"cluster with id {} not found",
cluster_id
))
})?;
let bundles = BundlesManager::get()
.await
.get_bundles_for(&cluster.mc_version, cluster.mc_loader)
.await?;
Ok(bundles)
}
async fn get_versions(self) -> LauncherResult<OnlineClusterManifest> {
get_data_storage_versions().await
}
async fn extract_bundle_overrides(
self,
bundle_path: PathBuf,
cluster_id: ClusterId,
) -> LauncherResult<()> {
let cluster = onelauncher_core::api::cluster::dao::get_cluster_by_id(cluster_id)
.await?
.ok_or_else(|| {
onelauncher_core::error::LauncherError::from(anyhow::anyhow!(
"cluster with id {} not found",
cluster_id
))
})?;
onelauncher_core::api::packages::modpack::mrpack::copy_overrides_folder(
&cluster,
&bundle_path,
&None,
)
.await?;
Ok(())
}
async fn check_for_update<R: Runtime>(
self,
app_handle: AppHandle<R>,
) -> LauncherResult<Option<Update>> {
crate::ext::updater::check_for_update(app_handle)
.await
.map_err(|e| onelauncher_core::error::LauncherError::from(anyhow::anyhow!(e)))
}
async fn install_update<R: Runtime>(self, app_handle: AppHandle<R>) -> LauncherResult<()> {
crate::ext::updater::install_update(app_handle)
.await
.map_err(|e| onelauncher_core::error::LauncherError::from(anyhow::anyhow!(e)))
}
async fn download_package_from_bundle(
self,
package: ModpackFileKind,
cluster_id: ClusterId,
bundle_name: String,
skip_compatibility: Option<bool>,
) -> LauncherResult<packages::Model> {
let cluster = api::cluster::dao::get_cluster_by_id(cluster_id)
.await?
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", cluster_id))?;
match package {
ModpackFileKind::Managed(box_) => {
let (pkg, version) = *box_;
let model = api::packages::download_package(&pkg, &version, None, None).await?;
api::packages::link_package(&model, &cluster, skip_compatibility).await?;
api::packages::bundle_dao::track_bundle_package(
&cluster,
&model,
&bundle_name,
&version.version_id,
)
.await?;
Ok(model)
}
ModpackFileKind::External(ext_package) => {
let model = api::packages::download_external_package(
&ext_package,
&cluster,
None,
skip_compatibility.or(Some(true)),
None,
)
.await?
.ok_or_else(|| anyhow::anyhow!("Failed to download external package"))?;
api::packages::link_package(&model, &cluster, skip_compatibility.or(Some(true)))
.await?;
api::packages::bundle_dao::track_bundle_package(
&cluster,
&model,
&bundle_name,
&ext_package.sha1,
)
.await?;
Ok(model)
}
}
}
async fn update_bundle_packages(
self,
cluster_id: ClusterId,
) -> LauncherResult<ApplyBundleUpdatesResult> {
// A manual update check must re-fetch the manifest and re-validate bundles rather than
// serving the process-cached copy, otherwise updates published after launch are missed.
crate::oneclient::bundles::BundlesManager::get()
.await
.refresh()
.await;
crate::oneclient::bundle_updates::apply_bundle_updates(cluster_id).await
}
async fn is_bundle_syncing(self) -> LauncherResult<bool> {
Ok(crate::oneclient::is_bundle_syncing())
}
async fn cache_art(self, path: String) -> LauncherResult<String> {
crate::oneclient::clusters::cache_art_image(&path).await
}
async fn refresh_art(self, path: String) -> LauncherResult<()> {
tokio::spawn(async move {
crate::oneclient::clusters::refresh_art_cache(&path).await;
});
Ok(())
}
async fn copy_cluster_content(
self,
source_id: ClusterId,
target_id: ClusterId,
) -> LauncherResult<CopyClusterResult> {
let source = api::cluster::dao::get_cluster_by_id(source_id)
.await?
.ok_or_else(|| anyhow::anyhow!("source cluster {} not found", source_id))?;
let target = api::cluster::dao::get_cluster_by_id(target_id)
.await?
.ok_or_else(|| anyhow::anyhow!("target cluster {} not found", target_id))?;
let clusters_dir = onelauncher_core::store::Dirs::get_clusters_dir().await?;
let source_path = clusters_dir.join(&source.folder_name);
let target_path = clusters_dir.join(&target.folder_name);
// Mods, resource packs, and shader packs are version-dependent and bundle-managed.
// Detect the source cluster's bundle subscriptions, map them to the target version,
// copy overrides, and install the correctly-versioned packages into the target.
crate::oneclient::bundle_updates::migrate_bundles_to_cluster(source_id, target_id).await?;
// Migrate non-bundle (custom) packages: update via provider for the target MC version
// where possible, and get back a list of files that had no compatible version and
// need to be copied verbatim.
let fallback_files: std::collections::HashSet<String> =
crate::oneclient::bundle_updates::migrate_non_bundle_packages(source_id, target_id)
.await?
.into_iter()
.collect();
// File-copy everything from the source folder. For bundle-managed dirs, only copy
// files in the fallback list (everything else was handled version-correctly above).
if source_path.exists() {
let mut entries = tokio::fs::read_dir(&source_path)
.await
.map_err(anyhow::Error::from)?;
while let Some(entry) = entries.next_entry().await.map_err(anyhow::Error::from)? {
let name = entry.file_name();
let name_str = name.to_string_lossy();
let src = entry.path();
let dst = target_path.join(&name);
if BUNDLE_DIRS.contains(&name_str.as_ref()) {
// Only copy files that weren't handled by migrate_non_bundle_packages
// or migrate_bundles_to_cluster (i.e., provider lookup failed).
if src.is_dir() {
let mut sub = tokio::fs::read_dir(&src)
.await
.map_err(anyhow::Error::from)?;
while let Some(sub_entry) =
sub.next_entry().await.map_err(anyhow::Error::from)?
{
let sub_name = sub_entry.file_name();
if !fallback_files.contains(sub_name.to_string_lossy().as_ref()) {
continue;
}
tokio::fs::create_dir_all(&dst)
.await
.map_err(anyhow::Error::from)?;
let sub_src = sub_entry.path();
// Copy as disabled — no compatible version exists for the new MC
// version, so mark it disabled rather than silently breaking the game.
let disabled_name = {
let s = sub_name.to_string_lossy();
if s.ends_with(".disabled") {
sub_name.clone()
} else {
std::ffi::OsString::from(format!("{s}.disabled"))
}
};
let sub_dst = dst.join(&disabled_name);
if sub_src.is_dir() {
copy_dir_all(&sub_src, &sub_dst).await?;
} else {
tokio::fs::copy(&sub_src, &sub_dst)
.await
.map_err(anyhow::Error::from)?;
}
}
}
} else if src.is_dir() {
copy_dir_all(&src, &dst).await?;
} else {
tokio::fs::copy(&src, &dst)
.await
.map_err(anyhow::Error::from)?;
}
}
}
onelauncher_core::api::cluster::sync_cluster_by_id(target_id).await?;
// Stamp the target cluster's created_at with the actual migration time so the
// home page can correctly identify it as a newly-migrated cluster.
onelauncher_core::api::cluster::dao::touch_cluster_created_at(target_id)
.await
.with_context(|| {
format!(
"failed to stamp migrated cluster created_at for target cluster {target_id}"
)
})?;
Ok(CopyClusterResult {
fallback_files: fallback_files.into_iter().collect(),
})
}
}

View file

@ -1,2 +0,0 @@
pub mod commands;
pub mod statics;

View file

@ -1,26 +0,0 @@
use onelauncher_core::constants::{NATIVE_ARCH, TARGET_OS};
use onelauncher_core::store::Core;
use serde::Serialize;
use specta::Type;
#[derive(Serialize, Type)]
pub struct ProgramInfo {
launcher_version: String,
webview_version: String,
tauri_version: String,
dev_build: bool,
platform: String,
arch: String,
}
#[must_use]
pub fn get_program_info() -> ProgramInfo {
ProgramInfo {
launcher_version: Core::get().launcher_version.clone(),
webview_version: tauri::webview_version().unwrap_or_else(|_| "UNKNOWN".into()),
tauri_version: tauri::VERSION.into(),
dev_build: tauri::is_dev(),
platform: TARGET_OS.into(),
arch: NATIVE_ARCH.into(),
}
}

View file

@ -1,15 +0,0 @@
// === API ===
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
pub const DISCORD_CLIENT_ID: &str = "1426999264633946334";
pub const PLUS_BACKEND_URL: &str = match option_env!("ONECLIENT_PLUS_BACKEND_URL") {
Some(url) => url,
#[cfg(debug_assertions)]
None => "https://plus-staging.polyfrost.org",
#[cfg(not(debug_assertions))]
None => "https://plus.polyfrost.org",
};
// === Meta ===
// Cannot contain trailing slash
pub const META_URL_BASE: &str =
"https://raw.githubusercontent.com/Polyfrost/DataStorage/refs/heads/main/oneclient";

View file

@ -1,2 +0,0 @@
pub mod specta;
pub mod updater;

View file

@ -1,23 +0,0 @@
use std::io;
use std::path::Path;
use std::process::Command;
pub fn formatter(file: &Path) -> io::Result<()> {
let result = Command::new("pnpm")
.arg("eslint")
.arg("--flag")
.arg("unstable_ts_config")
.arg("--fix")
.arg(file)
.output();
match result {
Ok(_) => Ok(()),
Err(e) => {
eprintln!("error running formatter on {}: {}", file.display(), e);
Ok(())
}
}
}
const _: specta_typescript::FormatterFn = formatter;

View file

@ -1,90 +0,0 @@
use onelauncher_core::store::Core;
use tauri::{Manager, Runtime};
use tauri_plugin_updater::{Update as TauriPluginUpdate, UpdaterExt};
use tokio::sync::Mutex;
#[derive(Debug, Clone, serde::Serialize, specta::Type)]
pub struct Update {
pub version: String,
}
impl Update {
fn new(update: &TauriPluginUpdate) -> Self {
Self {
version: update.version.clone(),
}
}
}
#[derive(Default)]
pub struct UpdaterState {
install_lock: Mutex<()>,
}
async fn get_update<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<Option<TauriPluginUpdate>, String> {
app.updater()
.map_err(|e| e.to_string())?
.check()
.await
.map_err(|e| e.to_string())
}
#[derive(Debug, Clone, serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase", tag = "status")]
pub enum UpdateEvent {
Loading,
Error { error: String },
UpdateAvailable { update: Update },
NoUpdateAvailable,
Installing,
}
pub async fn check_for_update<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<Option<Update>, String> {
get_update(app)
.await
.map(|update| update.map(|u| Update::new(&u)))
}
pub async fn install_update<R: Runtime>(app: tauri::AppHandle<R>) -> Result<(), String> {
let state = app.state::<UpdaterState>();
let Ok(lock) = state.install_lock.try_lock() else {
return Err("Update already installing".into());
};
get_update(app.clone())
.await?
.ok_or_else(|| "No update required".to_string())?
.download_and_install(|_, _| {}, || {})
.await
.map_err(|e| e.to_string())?;
drop(lock);
app.restart();
}
pub fn init<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
#[cfg(target_os = "linux")]
let updater_available = false;
#[cfg(not(target_os = "linux"))]
let updater_available = true;
if updater_available && let Some(window) = app.get_webview_window("main") {
window
.eval("window.__LAUNCHER_UPDATER__ = true;")
.expect("Failed to inject updater JS");
window
.eval(format!(
r#"window.__ONECLIENT_VERSION__ = "{}";"#,
Core::get().launcher_version,
))
.expect("Failed to inject version JS");
}
Ok(())
}

View file

@ -1,200 +0,0 @@
use onelauncher_core::api::proxy::ProxyTauri;
use onelauncher_core::api::tauri::{TauRPCLauncherExt, TauriLauncherDebugApi};
use onelauncher_core::error::LauncherResult;
use onelauncher_core::store::proxy::ProxyState;
use onelauncher_core::store::semaphore::SemaphoreStore;
use onelauncher_core::store::{Core, CoreOptions, Dirs, State};
use tauri::{Emitter, Manager};
use crate::api::commands::OneClientApi;
use crate::oneclient::initialize_oneclient;
pub mod api;
pub mod constants;
pub mod ext;
pub mod oneclient;
#[derive(Clone, serde::Serialize)]
pub struct SingleInstancePayload {
args: Vec<String>,
cwd: String,
}
#[tracing::instrument]
async fn initialize_core() -> LauncherResult<()> {
let level = if cfg!(debug_assertions) {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
let opts = CoreOptions {
curseforge_api_key: Some(constants::CURSEFORGE_API_KEY.to_string()),
launcher_name: "OneClient".to_string(),
launcher_version: env!("CARGO_PKG_VERSION").to_string(),
launcher_website: "https://polyfrost.org/".to_string(),
discord_client_id: Some(constants::DISCORD_CLIENT_ID.to_string()),
plus_backend_url: Some(constants::PLUS_BACKEND_URL.to_string()),
fetch_attempts: 3,
logger_filter: Some(format!(
"{}={level},onelauncher_core={level}",
env!("CARGO_PKG_NAME")
)),
..Default::default()
};
Core::initialize(opts).await?;
Dirs::get().await?;
onelauncher_core::start_logger().await;
SemaphoreStore::get().await;
tracing::info!("initialized core modules");
let debug_info = onelauncher_core::api::tauri::TauriLauncherDebugApiImpl
.get_full_debug_info_parsed_string()
.await;
tracing::info!("\n{}", debug_info);
Ok(())
}
#[tracing::instrument(skip_all)]
#[allow(clippy::large_stack_frames)]
async fn initialize_tauri(builder: tauri::Builder<tauri::Wry>) -> LauncherResult<tauri::App> {
let router = taurpc::Router::new()
.export_config(
specta_typescript::Typescript::default()
.bigint(specta_typescript::BigIntExportBehavior::Number)
.formatter(ext::specta::formatter)
.header("// @ts-nocheck\n"),
)
.merge(api::commands::OneClientApiImpl.into_handler())
.use_launcher_api();
let builder = builder
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
println!("{}, {argv:?}, {cwd}", app.package_info().name);
app.emit("single-instance", SingleInstancePayload { args: argv, cwd })
.unwrap();
}))
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_os::init());
#[cfg(not(target_os = "linux"))]
let builder = builder.menu(tauri::menu::Menu::default);
let builder = builder
.invoke_handler(router.into_handler())
.setup(move |app| {
app.manage(ext::updater::UpdaterState::default());
setup_window(app.handle()).expect("failed to setup main window");
#[cfg(desktop)]
app.handle()
.plugin(tauri_plugin_global_shortcut::Builder::new().build())?;
Ok(())
});
let app = builder
.build(tauri::generate_context!())
.expect("failed to build tauri application");
Ok(app)
}
#[tracing::instrument(skip_all)]
async fn initialize_state(handle: &tauri::AppHandle) -> LauncherResult<()> {
let proxy = ProxyTauri::new(handle.clone());
ProxyState::initialize(proxy).await?;
State::get().await?;
if let Err(err) = onelauncher_core::api::credentials::refresh_accounts().await {
tracing::warn!("failed to refresh accounts on startup: {err}");
}
onelauncher_core::store::credentials::spawn_plus_playtime_tracking();
tracing::info!("initialized launcher successfully");
Ok(())
}
pub async fn run() {
initialize_core().await.expect("failed to initialize core");
let app = initialize_tauri(tauri::Builder::default())
.await
.expect("failed to initialize tauri");
initialize_state(app.handle())
.await
.expect("failed to initialize state");
initialize_oneclient().await;
app.run(|_, _| {});
}
fn setup_window(handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
let win = handle
.get_webview_window("main")
.ok_or_else(|| anyhow::anyhow!("no window called main was found"))?;
ext::updater::init(handle)?;
let win_clone = win;
#[allow(unused_variables)]
let app_handle = handle.clone();
tokio::task::spawn(async move {
if State::get().await.is_ok() {
// native_window_frame=true means use native decorations
// native_window_frame=false means use custom frame (no decorations)
#[cfg(target_os = "macos")]
{
win_clone.set_decorations(true).ok();
let win_weak = win_clone.clone();
app_handle
.run_on_main_thread(move || {
#[cfg(target_os = "macos")]
{
use objc2_app_kit::{NSWindow, NSWindowButton};
if let Ok(ns_window_ptr) = win_weak.ns_window() {
let ns_window = ns_window_ptr.cast::<NSWindow>();
unsafe {
let ns_window = &*ns_window;
if let Some(btn) =
ns_window.standardWindowButton(NSWindowButton::CloseButton)
{
btn.setHidden(true);
}
if let Some(btn) = ns_window
.standardWindowButton(NSWindowButton::MiniaturizeButton)
{
btn.setHidden(true);
}
if let Some(btn) =
ns_window.standardWindowButton(NSWindowButton::ZoomButton)
{
btn.setHidden(true);
}
}
}
}
})
.ok();
}
#[cfg(not(target_os = "macos"))]
win_clone.set_decorations(false).ok();
}
win_clone.show().ok();
});
Ok(())
}

View file

@ -1,7 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[tokio::main]
async fn main() {
oneclient_gui::run().await;
}

File diff suppressed because it is too large Load diff

View file

@ -1,584 +0,0 @@
#[cfg(test)]
mod tests {
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::store::{Core, CoreOptions};
use tokio::fs;
use crate::oneclient::bundle_updates::{apply_bundle_updates, check_bundle_updates};
use crate::oneclient::bundles::BundlesManager;
use onelauncher_core::api::cluster::{create_cluster, dao};
use onelauncher_core::utils::DatabaseModelExt;
// We only run this if we ignore it by default, since it hits the network and modifies DB
// but we can run it explicitly.
#[tokio::test]
#[ignore = "touches true files and takes long"]
#[allow(clippy::too_many_lines, clippy::items_after_statements)]
async fn test_bundle_updates_e2e() {
// 1. Initialize DB and paths
Core::initialize(CoreOptions::default()).await.unwrap();
// 2. Create a fresh cluster
let cluster_name = format!(
"Bundle Test {}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let cluster = create_cluster(&cluster_name, "1.21.1", GameLoader::Fabric, None, None)
.await
.unwrap();
let cluster_dir = cluster.path().await.unwrap();
// Ensure BundlesManager is initialized and fetches remote bundles
println!("Loaded BundlesManager: {:#?}", BundlesManager::get().await);
let retrieved_bundles = BundlesManager::get()
.await
.get_bundles_for(&cluster.mc_version, cluster.mc_loader)
.await
.unwrap();
println!(
"get_bundles_for manually returned: {} bundles",
retrieved_bundles.len()
);
for b in &retrieved_bundles {
println!(
"Bundle '{}' has {} files",
b.manifest.name,
b.manifest.files.len()
);
}
// Subscribe the cluster to one of the bundles manually to trigger additions
use sea_orm::{ActiveModelTrait, Set};
let dummy_hash = uuid::Uuid::new_v4().to_string();
let dummy_base_pkg = onelauncher_core::entity::packages::ActiveModel {
hash: Set(dummy_hash.clone()),
file_name: Set("dummy.jar".to_string()),
version_id: Set("dummy_version".to_string()),
display_name: Set("Dummy".to_string()),
display_version: Set("1.0.0".to_string()),
package_type: Set(onelauncher_core::entity::package::PackageType::Mod),
provider: Set(onelauncher_core::entity::package::Provider::Modrinth),
package_id: Set("dummy_id".to_string()),
mc_versions: Set(onelauncher_core::entity::utility::DbVec(vec![
"1.21.1".to_string(),
])),
mc_loader: Set(onelauncher_core::entity::utility::DbVec(vec![
onelauncher_core::entity::loader::GameLoader::Fabric,
])),
published_at: Set(chrono::Utc::now()),
..Default::default()
};
dummy_base_pkg
.insert(&onelauncher_core::store::State::get().await.unwrap().db)
.await
.unwrap();
let dummy_pkg = onelauncher_core::entity::cluster_packages::ActiveModel {
cluster_id: Set(cluster.id),
package_hash: Set(dummy_hash.clone()),
bundle_name: Set(Some("OneClient 1.21.1 Fabric [Performance]".to_string())),
..Default::default()
};
dummy_pkg
.insert(&onelauncher_core::store::State::get().await.unwrap().db)
.await
.unwrap();
// 3. First apply: should detect additions for all bundle packages
let check1 = check_bundle_updates(cluster.id).await.unwrap();
assert!(
!check1.additions_available.is_empty(),
"Expected fresh cluster to have bundle additions available"
);
assert!(check1.updates_available.is_empty(), "Expected 0 updates");
assert!(check1.removals_available.is_empty(), "Expected 0 removals");
let applied1 = apply_bundle_updates(cluster.id).await.unwrap();
assert_eq!(
applied1.additions_applied.len(),
check1.additions_available.len()
);
let cluster_after_apply = dao::get_cluster_by_id(cluster.id)
.await
.unwrap()
.expect("cluster should still exist after bundle apply");
assert!(
cluster_after_apply.mc_loader_version.is_some(),
"Expected bundle apply to sync cluster mc_loader_version"
);
// 4. Verify overrides were extracted
// The 1.21.1 Fabric bundles usually include some config overrides (e.g. options.txt or config/...)
// We'll write a custom file in the cluster to simulate a user creating a config
let user_config = cluster_dir.join("config").join("user_custom.txt");
fs::create_dir_all(user_config.parent().unwrap())
.await
.unwrap();
fs::write(&user_config, b"user content").await.unwrap();
// We will modify an *existing* extracted override (if one exists), or we just let it be.
// For the sake of the test, let's artificially remove one package from the DB so the updater runs again
let installed_pkgs =
onelauncher_core::api::packages::bundle_dao::get_bundle_packages_for_cluster(
cluster.id,
)
.await
.unwrap();
let hash_to_remove = installed_pkgs
.into_iter()
.find(|p| {
p.package_hash != dummy_hash
&& p.bundle_name.as_deref() == Some("OneClient 1.21.1 Fabric [Performance]")
})
.unwrap()
.package_hash;
// Remove it from the database and filesystem so the updater thinks it's missing.
// record_override=false: this simulates a missing package, not a deliberate user removal.
println!("Removing package hash: {hash_to_remove}");
onelauncher_core::api::packages::remove_package(cluster.id, hash_to_remove, false)
.await
.unwrap();
// 5. Second apply: should detect 1 addition (the one we removed from DB)
let check2 = check_bundle_updates(cluster.id).await.unwrap();
println!(
"check2 additions count: {}",
check2.additions_available.len()
);
if check2.additions_available.len() != 1 {
println!(
"check2 additions: {:#?}",
check2
.additions_available
.iter()
.map(|a| &a.new_file.kind)
.collect::<Vec<_>>()
);
}
assert_eq!(
check2.additions_available.len(),
1,
"Expected exactly 1 addition available after DB deletion"
);
assert_eq!(check2.updates_available.len(), 0);
assert_eq!(check2.removals_available.len(), 0);
// Record modification time of the user_config
let mdata_before = fs::metadata(&user_config).await.unwrap();
let mtime_before = mdata_before.modified().unwrap();
let applied2 = apply_bundle_updates(cluster.id).await.unwrap();
assert_eq!(applied2.additions_applied.len(), 1);
// 6. Verify user config was NOT overwritten or wiped (no_overwrite preserved it)
let mdata_after = fs::metadata(&user_config).await.unwrap();
let mtime_after = mdata_after.modified().unwrap();
assert_eq!(
mtime_before, mtime_after,
"User config was modified during bundle update!"
);
let content = fs::read_to_string(&user_config).await.unwrap();
assert_eq!(content, "user content");
// Clean up
let _ = onelauncher_core::api::packages::dao::delete_package_by_id(dummy_hash).await;
let _ = dao::delete_cluster_by_id(cluster.id).await;
let _ = fs::remove_dir_all(cluster_dir).await;
println!("Bundle updates E2E test passed successfully");
}
#[tokio::test]
#[ignore = "touches true files and takes long"]
#[allow(clippy::too_many_lines, clippy::items_after_statements)]
async fn test_bundle_user_overrides() {
// 1. Initialize DB and paths
Core::initialize(CoreOptions::default()).await.unwrap();
// 2. Create a fresh cluster
let cluster_name = format!(
"Bundle Overrides Test {}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let cluster = create_cluster(&cluster_name, "1.21.1", GameLoader::Fabric, None, None)
.await
.unwrap();
let bundles = BundlesManager::get()
.await
.get_bundles_for(&cluster.mc_version, cluster.mc_loader)
.await
.unwrap();
let target_bundle = bundles
.into_iter()
.find(|b| b.manifest.name.contains("Performance"))
.expect("Expected to find a Performance bundle");
// Subscribe cluster to bundle to trigger initial fetch
use sea_orm::{ActiveModelTrait, Set};
let dummy_hash = uuid::Uuid::new_v4().to_string();
let dummy_base_pkg = onelauncher_core::entity::packages::ActiveModel {
hash: Set(dummy_hash.clone()),
file_name: Set("dummy.jar".to_string()),
version_id: Set("dummy_version".to_string()),
display_name: Set("Dummy".to_string()),
display_version: Set("1.0.0".to_string()),
package_type: Set(onelauncher_core::entity::package::PackageType::Mod),
provider: Set(onelauncher_core::entity::package::Provider::Modrinth),
package_id: Set("dummy_id".to_string()),
mc_versions: Set(onelauncher_core::entity::utility::DbVec(vec![
"1.21.1".to_string(),
])),
mc_loader: Set(onelauncher_core::entity::utility::DbVec(vec![
onelauncher_core::entity::loader::GameLoader::Fabric,
])),
published_at: Set(chrono::Utc::now()),
..Default::default()
};
dummy_base_pkg
.insert(&onelauncher_core::store::State::get().await.unwrap().db)
.await
.unwrap();
let dummy_pkg = onelauncher_core::entity::cluster_packages::ActiveModel {
cluster_id: Set(cluster.id),
package_hash: Set(dummy_hash.clone()),
bundle_name: Set(Some(target_bundle.manifest.name.clone())),
..Default::default()
};
dummy_pkg
.insert(&onelauncher_core::store::State::get().await.unwrap().db)
.await
.unwrap();
// 3. First apply: installs real bundle packages
let applied1 = apply_bundle_updates(cluster.id).await.unwrap();
assert!(
!applied1.additions_applied.is_empty(),
"Expected initial additions from bundle"
);
let cluster_after_apply = dao::get_cluster_by_id(cluster.id)
.await
.unwrap()
.expect("cluster should still exist after bundle apply");
assert!(
cluster_after_apply.mc_loader_version.is_some(),
"Expected bundle apply to sync cluster mc_loader_version"
);
// Get all installed packages mapping to the bundle
let installed_pkgs =
onelauncher_core::api::packages::bundle_dao::get_bundle_packages_for_cluster(
cluster.id,
)
.await
.unwrap();
// Find two packages to test the two override types (Disabled, Removed)
let mut real_packages = installed_pkgs
.into_iter()
.filter(|p| p.package_hash != dummy_hash && p.package_id.is_some());
let disabled_target = real_packages
.next()
.expect("Expected at least 1 real package installed");
let removed_target = real_packages
.next()
.expect("Expected at least 2 real packages installed");
// 4. Test "Removed" override behavior
println!(
"Testing Removed override on package: {}",
removed_target.package_id.as_ref().unwrap()
);
// Unlink/remove package, which should internally trigger the Overrides logic.
// record_override=true: this is an explicit user-initiated removal.
onelauncher_core::api::packages::remove_package(
cluster.id,
removed_target.package_hash.clone(),
true,
)
.await
.unwrap();
// Check updates. The package should NOT appear as an addition since the user explicitly removed it.
let check_removed = check_bundle_updates(cluster.id).await.unwrap();
let is_offered_as_addition = check_removed.additions_available.iter().any(|a| {
let file_id = match &a.new_file.kind {
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed(box_) => {
box_.0.id.clone()
}
onelauncher_core::api::packages::modpack::data::ModpackFileKind::External(ext) => {
ext.sha1.clone()
}
};
&file_id == removed_target.package_id.as_ref().unwrap()
});
assert!(
!is_offered_as_addition,
"Package was removed by user but offered as an addition during bundle check!"
);
// 5. Test "Disabled" override behavior
println!(
"Testing Disabled override on package: {}",
disabled_target.package_id.as_ref().unwrap()
);
// Toggle package to disabled, should trigger Overrides logic
let is_now_enabled = onelauncher_core::api::packages::toggle_package(
cluster.id,
disabled_target.package_hash.clone(),
)
.await
.unwrap();
assert!(!is_now_enabled, "Package was not disabled successfully");
// Let's fake an update for the disabled package by altering its installed version ID
let dummy_pkg = onelauncher_core::entity::cluster_packages::ActiveModel {
cluster_id: Set(disabled_target.cluster_id),
package_hash: Set(disabled_target.package_hash.clone()),
bundle_version_id: Set(Some("old_fake_version_trigger_update".to_string())),
..disabled_target.clone().into()
};
// Ignore warnings about updating Primary Key fields
dummy_pkg
.update(&onelauncher_core::store::State::get().await.unwrap().db)
.await
.unwrap();
// Apply updates. The disabled package should be updated, BUT should remain disabled.
let _pre_update_path = onelauncher_core::api::packages::dao::get_package_by_hash(
disabled_target.package_hash.clone(),
)
.await
.unwrap()
.unwrap()
.path()
.await
.unwrap();
let check_disabled = check_bundle_updates(cluster.id).await.unwrap();
assert!(
check_disabled
.updates_available
.iter()
.any(|u| u.installed_package_hash == disabled_target.package_hash),
"Disabled package should still be marked for update"
);
let applied2 = apply_bundle_updates(cluster.id).await.unwrap();
let updated_pkg_hash = applied2
.updates_applied
.into_iter()
.find(|u| u.installed_package_hash == disabled_target.package_hash)
.expect("Expected the disabled package to be updated")
.new_file;
let file_id = match &updated_pkg_hash.kind {
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed(box_) => {
box_.0.id.clone()
}
onelauncher_core::api::packages::modpack::data::ModpackFileKind::External(ext) => {
ext.sha1.clone()
}
};
// Check the new package database entry and filesystem to ensure it's still disabled
let new_bundle_mapping =
onelauncher_core::api::packages::bundle_dao::get_bundle_package_by_package_id(
cluster.id, &file_id,
)
.await
.unwrap()
.unwrap();
let new_pkg_data = onelauncher_core::api::packages::dao::get_package_by_hash(
new_bundle_mapping.package_hash,
)
.await
.unwrap()
.unwrap();
assert!(
new_pkg_data.file_name.ends_with(".disabled"),
"Updated bundle package was re-enabled! It should have kept the .disabled extension."
);
// Clean up
let _ = dao::delete_cluster_by_id(cluster.id).await;
let _ = fs::remove_dir_all(cluster.path().await.unwrap()).await;
}
/// Regression test for the bundle-override bug: override files (config JSONs, etc.) must
/// be re-extracted on a sync EVEN WHEN no package was added/updated/removed.
///
/// Previously override extraction was gated behind package-level changes, so an
/// override-only update (e.g. a JSON making an already-installed mod compatible with a
/// new MC version) was silently skipped for users who already had the mods installed.
#[tokio::test]
#[ignore = "touches true files and takes long"]
#[allow(clippy::too_many_lines, clippy::items_after_statements)]
async fn test_overrides_reextracted_without_package_changes() {
use sea_orm::{ActiveModelTrait, Set};
Core::initialize(CoreOptions::default()).await.unwrap();
let cluster_name = format!(
"Bundle Override-Only Test {}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let cluster = create_cluster(&cluster_name, "1.21.1", GameLoader::Fabric, None, None)
.await
.unwrap();
let cluster_dir = cluster.path().await.unwrap();
let bundles = BundlesManager::get()
.await
.get_bundles_for(&cluster.mc_version, cluster.mc_loader)
.await
.unwrap();
let target_bundle = bundles
.into_iter()
.find(|b| b.manifest.name.contains("Performance"))
.expect("Expected to find a Performance bundle");
// Find an override file shipped in the bundle archive so we have something to assert on.
let override_rel_path = first_override_entry(&target_bundle.path)
.await
.expect("bundle has no override files to test against; cannot run this regression");
// Subscribe the cluster to the bundle.
let dummy_hash = uuid::Uuid::new_v4().to_string();
onelauncher_core::entity::packages::ActiveModel {
hash: Set(dummy_hash.clone()),
file_name: Set("dummy.jar".to_string()),
version_id: Set("dummy_version".to_string()),
display_name: Set("Dummy".to_string()),
display_version: Set("1.0.0".to_string()),
package_type: Set(onelauncher_core::entity::package::PackageType::Mod),
provider: Set(onelauncher_core::entity::package::Provider::Modrinth),
package_id: Set("dummy_id".to_string()),
mc_versions: Set(onelauncher_core::entity::utility::DbVec(vec![
"1.21.1".to_string(),
])),
mc_loader: Set(onelauncher_core::entity::utility::DbVec(vec![
GameLoader::Fabric,
])),
published_at: Set(chrono::Utc::now()),
..Default::default()
}
.insert(&onelauncher_core::store::State::get().await.unwrap().db)
.await
.unwrap();
onelauncher_core::entity::cluster_packages::ActiveModel {
cluster_id: Set(cluster.id),
package_hash: Set(dummy_hash.clone()),
bundle_name: Set(Some(target_bundle.manifest.name.clone())),
..Default::default()
}
.insert(&onelauncher_core::store::State::get().await.unwrap().db)
.await
.unwrap();
// First apply: installs packages AND extracts overrides (has_changes == true).
let applied1 = apply_bundle_updates(cluster.id).await.unwrap();
assert!(
!applied1.additions_applied.is_empty(),
"Expected initial additions from bundle"
);
let extracted = cluster_dir.join(&override_rel_path);
assert!(
extracted.exists(),
"override file should be extracted on first apply"
);
// Simulate the override file being missing (e.g. it was only just added to the bundle,
// so existing users never had it). Delete it from disk.
fs::remove_file(&extracted).await.unwrap();
assert!(!extracted.exists());
// Second apply: nothing about the packages changed, so has_changes == false.
// The override file MUST still be re-extracted (this is the bug being regression-tested).
let applied2 = apply_bundle_updates(cluster.id).await.unwrap();
assert!(
applied2.updates_applied.is_empty()
&& applied2.additions_applied.is_empty()
&& applied2.removals_applied.is_empty(),
"second apply should have no package-level changes"
);
assert!(
extracted.exists(),
"override file must be re-extracted even when no package changed (Bug 2 regression)"
);
// Clean up
let _ = onelauncher_core::api::packages::dao::delete_package_by_id(dummy_hash).await;
let _ = dao::delete_cluster_by_id(cluster.id).await;
let _ = fs::remove_dir_all(cluster_dir).await;
}
/// Returns the first file path under `overrides/` in a `.mrpack`/zip archive, relative to
/// the cluster root (i.e. with the `overrides/` prefix stripped).
///
/// Extracts the archive's overrides into a throwaway temp dir using the same public helper
/// the real sync uses, then walks it — avoiding a direct zip-crate dependency in this test.
async fn first_override_entry(archive_path: &std::path::Path) -> Option<String> {
let probe_dir = std::env::temp_dir().join(format!(
"oneclient_override_probe_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&probe_dir).await.ok()?;
onelauncher_core::api::packages::modpack::mrpack::extract_overrides_no_overwrite(
&archive_path.to_path_buf(),
&probe_dir,
)
.await
.ok()?;
let relative = first_file_relative(&probe_dir, &probe_dir);
let _ = fs::remove_dir_all(&probe_dir).await;
relative
}
/// Recursively finds the first regular file under `dir`, returning its path relative to
/// `root` with forward slashes.
fn first_file_relative(dir: &std::path::Path, root: &std::path::Path) -> Option<String> {
let mut entries: Vec<_> = std::fs::read_dir(dir).ok()?.flatten().collect();
entries.sort_by_key(std::fs::DirEntry::path);
for entry in entries {
let path = entry.path();
if path.is_dir() {
if let Some(found) = first_file_relative(&path, root) {
return Some(found);
}
} else if let Ok(rel) = path.strip_prefix(root) {
return Some(rel.to_string_lossy().replace('\\', "/"));
}
}
None
}
}

View file

@ -1,274 +0,0 @@
use std::collections::HashMap;
use std::path::PathBuf;
use onelauncher_core::api::packages::modpack::data::ModpackArchive;
use onelauncher_core::api::packages::modpack::{InstallableModpackFormatExt, ModpackFormat};
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::error::LauncherResult;
use onelauncher_core::send_error;
use onelauncher_core::store::Dirs;
use onelauncher_core::utils::{http, io};
use reqwest::{Method, header};
use tokio::sync::{OnceCell, RwLock};
/// e.g.
/// ```json
/// {
/// "versions": {
/// "1.21.5": {
/// "fabric": ["/generated/hud-fabric-1.21.5.mrpack"],
/// "forge": ["/generated/hud-forge-1.21.5.mrpack"]
/// }
/// },
/// }
/// ```
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
struct BundleManifest {
pub versions: HashMap<String, HashMap<String, Vec<String>>>,
}
static BUNDLES_STATE: OnceCell<BundlesManager> = OnceCell::const_new();
#[derive(Debug)]
pub struct BundlesManager {
manifest: RwLock<BundleManifest>,
bundles: RwLock<HashMap<String, HashMap<GameLoader, Vec<ModpackArchive>>>>,
}
impl BundlesManager {
pub async fn get() -> &'static Self {
BUNDLES_STATE
.get_or_init(|| async {
let manifest = Self::fetch_cached().await;
Self {
manifest: RwLock::new(manifest),
bundles: RwLock::new(HashMap::new()),
}
})
.await
}
#[tracing::instrument(skip(self))]
pub async fn get_bundles_for(
&self,
mc_version: &str,
loader: onelauncher_core::entity::loader::GameLoader,
) -> LauncherResult<Vec<ModpackArchive>> {
let manifest = self.manifest.read().await;
// Fast path: check cache under read lock.
let bundles_lock = self.bundles.read().await;
if let Some(entry) = bundles_lock.get(mc_version)
&& let Some(bundles) = entry.get(&loader)
{
return Ok(bundles.clone());
}
drop(bundles_lock);
// Slow path: acquire write lock and re-check before populating (double-checked locking).
// This closes the TOCTOU gap between dropping the read lock and acquiring the write lock.
let mut bundles_lock = self.bundles.write().await;
if let Some(entry) = bundles_lock.get(mc_version)
&& let Some(bundles) = entry.get(&loader)
{
return Ok(bundles.clone());
}
let mut found = Vec::new();
for (version, loaders) in &manifest.versions {
if version != mc_version {
continue;
}
let Some(paths) = loaders.get(&loader.get_format_name()) else {
continue;
};
// we will be first checking the disk cache, if that fails we fetch from remote
for path in paths {
let Some(file_name) = path.split('/').next_back() else {
tracing::error!("no bundle name was found in path: {path}");
continue;
};
let disk_path = BundlesManager::dir().await.join("bundles").join(file_name);
let modpack = match download_and_load_bundle(path, &disk_path).await {
Ok(modpack) => modpack,
Err(e) => {
tracing::error!("failed to load bundle from {path}: {e}");
continue;
}
};
let manifest = match modpack.manifest().await {
Ok(manifest) => manifest,
Err(e) => {
tracing::error!("failed to load modpack manifest from {path}: {e}");
continue;
}
}
.clone();
found.push(ModpackArchive {
manifest,
path: disk_path,
format: modpack.kind(),
});
}
}
bundles_lock
.entry(mc_version.to_string())
.or_default()
.insert(loader, found.clone());
Ok(found.clone())
}
/// Re-fetches the bundles manifest from remote and clears the in-memory parsed-bundle
/// cache so the next [`get_bundles_for`](Self::get_bundles_for) call reloads everything.
///
/// The on-disk `.mrpack` cache is still consulted (and validated via ETag) by
/// `download_and_load_bundle`, so this only forces re-validation, not blind re-downloads.
///
/// Without this, the manifest is fetched exactly once per process at startup and the
/// parsed bundles are cached for the process lifetime — meaning a long-running session
/// (or a manual "check for updates") would never observe a bundle the publisher updated
/// after launch. Mods would silently stop updating until a full restart.
pub async fn refresh(&self) {
let manifest = Self::fetch_cached().await;
*self.manifest.write().await = manifest;
self.bundles.write().await.clear();
}
/// Fetches the bundles manifest from remote, falling back to a saved copy on disk if available
#[tracing::instrument]
pub async fn fetch_cached() -> BundleManifest {
let url = format!("{}/bundles.json", crate::constants::META_URL_BASE);
let manifest_path = Self::dir().await.join("bundles.json");
match http::fetch_json::<BundleManifest>(Method::GET, &url, None, None).await {
Ok(manifest) => {
io::create_dir_all(manifest_path.parent().unwrap())
.await
.unwrap_or_else(|e| {
tracing::error!("failed to create bundles dir: {e}");
});
if let Err(e) = io::write_json(&manifest_path, &manifest).await {
send_error!("failed to cache bundles manifest to disk: {e}");
}
manifest
}
Err(e) if manifest_path.exists() => {
tracing::debug!(
"falling back to cached bundles manifest, due to error fetching remote: {e}"
);
match io::read_json::<BundleManifest>(&manifest_path).await {
Ok(manifest) => manifest,
Err(e) => {
tracing::error!("failed to read cached bundles manifest: {e}");
BundleManifest::default()
}
}
}
Err(e) => {
tracing::error!("failed to fetch bundles manifest from remote: {e}");
BundleManifest::default()
}
}
}
/// returns the directory for everything bundle related
pub async fn dir() -> std::path::PathBuf {
Dirs::get_caches_dir()
.await
.expect("failed to get caches dir")
.join("oneclient")
.join("bundles")
}
}
#[tracing::instrument]
async fn download_and_load_bundle(
url_path: &str,
disk_path: &PathBuf,
) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
let url = format!("{}{}", crate::constants::META_URL_BASE, url_path);
// Sidecar file that stores the server ETag for cache freshness checks.
let etag_path = PathBuf::from(format!("{}.etag", disk_path.display()));
if disk_path.exists() {
// Check if the remote file differs from the local cached copy.
// If the validation request itself fails, trust the local cache instead of
// treating the bundle as unavailable.
match http::request(Method::HEAD, &url).await {
Ok(res) if res.status().is_success() => {
let server_etag = res
.headers()
.get(reqwest::header::ETAG)
.and_then(|v| v.to_str().ok())
.map(std::string::ToString::to_string);
let stored_etag: Option<String> = tokio::fs::read_to_string(&etag_path).await.ok();
if let (Some(server), Some(stored)) = (&server_etag, &stored_etag) {
// ETag comparison: most reliable cache validation method.
if server == stored {
tracing::debug!("bundle cache hit via ETag for: {url}");
return ModpackFormat::from_file(disk_path).await;
}
} else {
// Fall back to Content-Length comparison when no ETag is available.
let content_length = res
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok());
if let Some(length) = content_length {
let file_size = io::stat(disk_path).await.map(|m| m.len()).unwrap_or(0);
if length == file_size {
return ModpackFormat::from_file(disk_path).await;
}
}
// If neither ETag nor Content-Length are available, fall through to re-download.
}
}
Ok(res) => {
tracing::warn!(
status = %res.status(),
"failed to validate remote bundle cache for {url}; using cached local bundle"
);
return ModpackFormat::from_file(disk_path).await;
}
Err(e) => {
tracing::warn!(
"failed to validate remote bundle cache for {url}: {e}; using cached local bundle"
);
return ModpackFormat::from_file(disk_path).await;
}
}
}
tracing::debug!("downloading bundle from remote: {url}");
http::download(Method::GET, &url, disk_path, None, None).await?;
// After a fresh download, persist the server ETag for next time.
if let Ok(head_res) = http::request(Method::HEAD, &url).await
&& let Some(etag) = head_res
.headers()
.get(reqwest::header::ETAG)
.and_then(|v| v.to_str().ok())
{
let _ = tokio::fs::write(&etag_path, etag).await;
}
ModpackFormat::from_file(disk_path).await
}

View file

@ -1,218 +0,0 @@
use std::path::Path;
use onelauncher_core::entity::clusters::Model as ClusterModel;
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::error::LauncherResult;
use onelauncher_core::send_error;
use onelauncher_core::store::Dirs;
use onelauncher_core::utils::http::{fetch, fetch_json};
use reqwest::Method;
use serde::{Deserialize, Serialize};
///
/// e.g.
/// ```json
/// {
/// "clusters": [
/// {
/// "major_version": 21,
/// "name": "Tricky Trials",
/// "art": "/versions/art/Tricky_Trials.png",
/// "entries": [
/// {
/// "minor_version": 5,
/// "loader": "fabric",
/// "tags": ["PvP", "Survival"]
/// },
/// {
/// "minor_version": 5,
/// "loader": "forge",
/// "tags": ["PvP", "Survival"]
/// }
/// ]
/// },
/// {
/// "major_version": 20,
/// "name": "Trails & Tales",
/// "art": "/versions/art/Trails_Tales.png",
/// "entries": [
/// {
/// "minor_version": 5,
/// "loader": "fabric",
/// "tags": ["PvP", "Survival"]
/// }
/// ]
/// }
/// ]
/// }
/// ```
#[derive(specta::Type, Deserialize, Serialize)]
pub struct OnlineClusterManifest {
clusters: Vec<OnlineCluster>,
}
#[derive(specta::Type, Deserialize, Serialize)]
pub struct OnlineCluster {
major_version: u8,
name: String,
art: String,
entries: Vec<OnlineClusterEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
long_description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tags: Vec<String>,
}
#[derive(specta::Type, Deserialize, Serialize)]
pub struct OnlineClusterEntry {
minor_version: u8,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
art: Option<String>,
loader: GameLoader,
tags: Vec<String>,
}
/// Load the versions manifest, preferring a local override file if one exists.
///
/// Place a `versions_override.json` in the launcher's data directory
/// (the path logged as "using base directory '...'" on startup) to use it
/// instead of fetching from the remote `DataStorage` URL. Useful for testing
/// new versions locally before pushing to `DataStorage`.
async fn load_versions_manifest() -> LauncherResult<OnlineClusterManifest> {
if let Ok(base_dir) = onelauncher_core::store::Dirs::get()
.await
.map(|d| d.base_dir().clone())
{
let override_path = base_dir.join("versions_override.json");
if override_path.exists() {
tracing::info!("using local versions override: {}", override_path.display());
let bytes = tokio::fs::read(&override_path)
.await
.map_err(anyhow::Error::from)?;
let manifest: OnlineClusterManifest =
serde_json::from_slice(&bytes).map_err(anyhow::Error::from)?;
return Ok(manifest);
}
}
fetch_json::<OnlineClusterManifest>(
Method::GET,
&format!("{}/versions/versions.json", crate::constants::META_URL_BASE),
None,
None,
)
.await
}
pub async fn get_data_storage_versions() -> LauncherResult<OnlineClusterManifest> {
match load_versions_manifest().await {
Ok(m) => Ok(m),
Err(e) => {
send_error!("failed to fetch clusters manifest: {}", e);
Err(e)
}
}
}
/// Download an art image (e.g. `/versions/art/Foo.png`) from the data storage CDN and
/// save it to the local cache directory.
///
/// Returns the OS-native path to the cached file,
/// which the frontend converts to an `asset://` URL via `convertFileSrc`.
/// If the file is already cached it is returned immediately without a network request.
pub async fn cache_art_image(path: &str) -> LauncherResult<String> {
let art_dir = Dirs::get_caches_dir().await?.join("oneclient").join("art");
tokio::fs::create_dir_all(&art_dir)
.await
.map_err(anyhow::Error::from)?;
// Flatten "/versions/art/Foo.png" → "versions_art_Foo.png"
let filename = path.trim_start_matches('/').replace('/', "_");
let cached_path = art_dir.join(&filename);
if cached_path.exists() {
return Ok(cached_path.to_string_lossy().into_owned());
}
let url = format!("{}{}", crate::constants::META_URL_BASE, path);
let bytes = fetch(Method::GET, &url).await?;
tokio::fs::write(&cached_path, bytes)
.await
.map_err(anyhow::Error::from)?;
Ok(cached_path.to_string_lossy().into_owned())
}
/// Re-download an art image and overwrite the on-disk cache.
///
/// Errors are silently ignored — the existing cached version is kept on failure.
/// Call this from `tokio::spawn` so it runs fully in the background.
pub async fn refresh_art_cache(path: &str) {
let Ok(caches_dir) = Dirs::get_caches_dir().await else {
return;
};
let art_dir = caches_dir.join("oneclient").join("art");
let filename = path.trim_start_matches('/').replace('/', "_");
let cached_path = art_dir.join(&filename);
let url = format!("{}{}", crate::constants::META_URL_BASE, path);
if let Ok(bytes) = fetch(Method::GET, &url).await {
let _ = tokio::fs::write(&cached_path, bytes).await;
}
}
pub async fn init_clusters() -> LauncherResult<()> {
let manifest = match load_versions_manifest().await {
Ok(m) => m,
Err(e) => {
send_error!("failed to fetch clusters manifest: {}", e);
return Err(e);
}
};
for cluster in manifest.clusters {
for entry in cluster.entries {
let mc_version = if cluster.major_version >= 26 {
format!("{}.{}", cluster.major_version, entry.minor_version)
} else {
format!("1.{}.{}", cluster.major_version, entry.minor_version)
};
if let Err(e) = create_cluster_if_not_exist(&mc_version, entry.loader, None).await {
send_error!("failed to create cluster for {}: {}", mc_version, e);
}
}
}
Ok(())
}
fn cluster_name(mc_version: &str, loader: GameLoader) -> String {
format!("{mc_version} {loader}")
}
async fn create_cluster_if_not_exist(
mc_version: &str,
mc_loader: GameLoader,
mc_loader_version: Option<&str>,
) -> LauncherResult<Option<ClusterModel>> {
let name = cluster_name(mc_version, mc_loader);
let cluster =
onelauncher_core::api::cluster::dao::get_cluster_by_folder_name(Path::new(&name)).await?;
if cluster.is_some() {
return Ok(cluster);
}
onelauncher_core::api::cluster::create_cluster(
&name,
mc_version,
mc_loader,
mc_loader_version,
None,
)
.await
.map(Some)
}

View file

@ -1,180 +0,0 @@
pub mod bundle_updates;
pub mod bundles;
pub mod clusters;
use std::sync::atomic::{AtomicBool, Ordering};
use onelauncher_core::api::cluster::dao::get_all_clusters;
use onelauncher_core::send_info;
/// Global flag indicating whether a bundle sync is currently in progress.
/// Used by the frontend FS watcher to skip `syncCluster` calls during updates.
static BUNDLE_SYNCING: AtomicBool = AtomicBool::new(false);
pub fn is_bundle_syncing() -> bool {
BUNDLE_SYNCING.load(Ordering::Relaxed)
}
pub async fn initialize_oneclient() {
if let Err(err) = clusters::init_clusters().await {
tracing::error!("failed to initialize clusters: {err}");
}
if let Err(err) = onelauncher_core::api::cluster::sync_clusters().await {
tracing::error!("failed to sync clusters: {err}");
}
bundles::BundlesManager::get().await;
tokio::spawn(async {
check_and_apply_all_bundle_updates().await;
});
}
#[allow(clippy::too_many_lines)]
async fn check_and_apply_all_bundle_updates() {
BUNDLE_SYNCING.store(true, Ordering::Relaxed);
tracing::info!("checking for bundle updates...");
// Force a fresh manifest fetch + cache clear so we observe bundles the publisher updated
// since this process started (or since the last sync). The on-disk .mrpack cache is still
// ETag-validated, so unchanged bundles are not re-downloaded.
bundles::BundlesManager::get().await.refresh().await;
let clusters = match get_all_clusters().await {
Ok(clusters) => clusters,
Err(err) => {
tracing::error!("failed to get clusters for bundle update check: {err}");
BUNDLE_SYNCING.store(false, Ordering::Relaxed);
return;
}
};
let mut total_updates_applied = 0;
let mut total_removals_applied = 0;
let mut total_additions_applied = 0;
let mut total_clusters_failed = 0;
for cluster in clusters {
tracing::debug!(
cluster_id = %cluster.id,
cluster_name = %cluster.name,
"Checking and applying bundle updates for cluster"
);
match bundle_updates::apply_bundle_updates(cluster.id).await {
Ok(result) => {
let update_count = result.updates_applied.len();
let removal_count = result.removals_applied.len();
let addition_count = result.additions_applied.len();
let failure_count = result.updates_failed.len()
+ result.removals_failed.len()
+ result.additions_failed.len();
if failure_count > 0 {
tracing::warn!(
"{failure_count} bundle operation(s) failed for cluster '{}'",
cluster.name
);
}
if update_count > 0 || removal_count > 0 || addition_count > 0 {
total_updates_applied += update_count;
total_removals_applied += removal_count;
total_additions_applied += addition_count;
if update_count > 0 {
tracing::info!(
"applied {} bundle update(s) for cluster '{}' (id: {})",
update_count,
cluster.name,
cluster.id
);
for update in &result.updates_applied {
tracing::info!(
" - updated package from bundle '{}': {} -> {}",
update.bundle_name,
update.installed_version_id,
update.new_version_id
);
}
}
if removal_count > 0 {
tracing::info!(
"removed {} package(s) no longer in bundles for cluster '{}' (id: {})",
removal_count,
cluster.name,
cluster.id
);
for removal in &result.removals_applied {
tracing::info!(
" - removed package '{}' (was from bundle '{}')",
removal.package_id,
removal.bundle_name
);
}
}
if addition_count > 0 {
tracing::info!(
"installed {} new package(s) from bundles for cluster '{}' (id: {})",
addition_count,
cluster.name,
cluster.id
);
for addition in &result.additions_applied {
let file_id = match &addition.new_file.kind {
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed(box_) => box_.0.id.clone(),
onelauncher_core::api::packages::modpack::data::ModpackFileKind::External(ext) => ext.sha1.clone(),
};
tracing::info!(
" - installed new package '{}' from bundle '{}'",
file_id,
addition.bundle_name
);
}
}
} else {
tracing::debug!("no bundle updates needed for cluster '{}'", cluster.name);
}
}
Err(err) => {
total_clusters_failed += 1;
tracing::warn!(
"failed to apply bundle updates for cluster '{}': {err}",
cluster.name
);
}
}
}
if total_updates_applied > 0 || total_removals_applied > 0 || total_additions_applied > 0 {
let mut message_parts = Vec::new();
if total_updates_applied > 0 {
message_parts.push(format!("{total_updates_applied} mod(s) updated"));
}
if total_removals_applied > 0 {
message_parts.push(format!("{total_removals_applied} mod(s) removed"));
}
if total_additions_applied > 0 {
message_parts.push(format!("{total_additions_applied} mod(s) added"));
}
send_info!("Bundle sync: {}", message_parts.join(", "));
tracing::info!(
"bundle sync complete: {} updates applied, {} removals applied, {} additions applied",
total_updates_applied,
total_removals_applied,
total_additions_applied
);
} else if total_clusters_failed == 0 {
tracing::info!("all bundle packages are up to date");
}
if total_clusters_failed > 0 {
tracing::warn!("failed to apply updates for {total_clusters_failed} cluster(s)");
}
BUNDLE_SYNCING.store(false, Ordering::Relaxed);
}

View file

@ -1,124 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
"productName": "OneClient",
"identifier": "org.polyfrost.oneclient",
"app": {
"withGlobalTauri": true,
"macOSPrivateApi": true,
"security": {
"csp": null,
"assetProtocol": {
"enable": true,
"scope": {
"allow": [
"$DATA/OneClient/**",
"$RESOURCE/**"
]
}
},
"capabilities": []
},
"windows": [
{
"title": "OneClient",
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"width": 1280,
"height": 802,
"minWidth": 1020,
"minHeight": 600,
"resizable": true,
"center": true,
"decorations": false,
"fullscreen": false,
"shadow": true,
"visible": false
}
]
},
"build": {
"beforeDevCommand": "pnpm --filter @oneclient/frontend run dev",
"beforeBuildCommand": "pnpm --filter @oneclient/frontend run build",
"frontendDist": "../frontend/dist",
"devUrl": "http://localhost:8001"
},
"bundle": {
"active": true,
"createUpdaterArtifacts": true,
"targets": [
"app",
"dmg",
"deb",
"rpm",
"appimage",
"nsis"
],
"category": "Game",
"icon": [
"../distribution/icons/32x32.png",
"../distribution/icons/128x128.png",
"../distribution/icons/128x128@2x.png",
"../distribution/icons/icon.icns",
"../distribution/icons/icon.ico"
],
"copyright": "© 2026 Polyfrost Inc.",
"publisher": "Polyfrost",
"license": "GPL-3.0-only",
"homepage": "https://polyfrost.org/",
"longDescription": "Next-generation open source Minecraft launcher",
"resources": [],
"externalBin": [],
"linux": {
"deb": {
"depends": [],
"desktopTemplate": "../distribution/linux/org.polyfrost.oneclient.template"
},
"rpm": {
"desktopTemplate": "../distribution/linux/org.polyfrost.oneclient.template"
},
"appimage": {
}
},
"shortDescription": "OneClient",
"macOS": {
"entitlements": "App.entitlements",
"exceptionDomain": null,
"frameworks": [],
"dmg": {
"appPosition": {
"x": 190,
"y": 190
},
"applicationFolderPosition": {
"x": 470,
"y": 190
}
},
"providerShortName": null,
"signingIdentity": null,
"minimumSystemVersion": "10.15"
},
"windows": {
"certificateThumbprint": null,
"webviewInstallMode": {
"silent": true,
"type": "downloadBootstrapper"
},
"digestAlgorithm": "sha256",
"timestampUrl": "https://timestamp.digicert.com"
}
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFGODk3MkMyMjg0MjFDMDUKUldRRkhFSW93bktKSHpkWjNEMXNzaDVINVpCTU8xSnhuK2RnV0dTZ2FkcFJWbG1zUkhGYTNjaUkK",
"endpoints": ["https://github.com/Polyfrost/OneLauncher/releases/latest/download/latest.json"]
},
"deep-link": {
"mobile": [],
"desktop": {
"schemes": ["oneclient"]
}
}
}
}

View file

@ -1,21 +0,0 @@
{
"app": {
"windows": [
{
"title": "OneClient",
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"width": 1280,
"height": 802,
"minWidth": 1020,
"minHeight": 600,
"resizable": true,
"center": true,
"decorations": true,
"fullscreen": false,
"shadow": true,
"visible": false
}
]
}
}

View file

@ -1,25 +0,0 @@
pkgname=oneclient-bin
pkgver=1.0.3
pkgrel=1
pkgdesc='Next-generation open source Minecraft launcher that downloads all the mods you would ever want'
url='https://polyfrost.org/projects/oneclient'
arch=('x86_64')
license=('GPL-3.0-only')
depends=(
'openssl' 'dbus' 'gtk3' 'libayatana-appindicator' 'librsvg' 'webkit2gtk-4.1'
'libgl' 'libpulse' 'libx11' 'libxcursor' 'libxext' 'libxxf86vm'
)
provides=('oneclient')
conflicts=('oneclient')
source=("https://github.com/Polyfrost/OneLauncher/releases/download/oneclient-${pkgver}/oneclient_${pkgver}_amd64.deb")
sha256sums=('SKIP')
build() {
cd "$srcdir/"
tar xf data.tar.gz
}
package() {
cp -r "${srcdir}/usr/" "${pkgdir}"
}

View file

@ -1,46 +0,0 @@
pkgname=oneclient
pkgver=1.0.3
pkgrel=1
pkgdesc='Next-generation open source Minecraft launcher that downloads all the mods you would ever want'
url='https://polyfrost.org/projects/oneclient'
arch=('x86_64')
license=('GPL-3.0-only')
makedepends=('rust' 'pnpm' 'nodejs' 'clang' 'nasm' 'perl')
depends=(
'openssl' 'dbus' 'gtk3' 'libayatana-appindicator' 'librsvg' 'webkit2gtk-4.1'
'libgl' 'libpulse' 'libx11' 'libxcursor' 'libxext' 'libxxf86vm'
)
conflicts=('oneclient-bin')
source=("$pkgname-$pkgver.tar.gz::https://github.com/Polyfrost/OneLauncher/archive/refs/tags/oneclient-${pkgver}.tar.gz")
sha256sums=('SKIP')
options=('!lto')
prepare() {
cd "OneLauncher-oneclient-${pkgver}"
export RUSTUP_TOOLCHAIN=stable
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
pnpm install --frozen-lockfile
pnpm prep
}
build() {
cd "OneLauncher-oneclient-${pkgver}"
export RUSTUP_TOOLCHAIN=stable
pnpm --filter @oneclient/frontend build
cargo build --frozen --release -p oneclient_gui
}
check() {
cd "OneLauncher-oneclient-${pkgver}"
export RUSTUP_TOOLCHAIN=stable
cargo test --frozen -p oneclient_gui
}
package() {
cd "OneLauncher-oneclient-${pkgver}"
install -Dm755 "target/release/oneclient_gui" "$pkgdir/usr/bin/oneclient"
install -Dm644 "apps/oneclient/distribution/icons/128x128.png" "$pkgdir/usr/share/icons/hicolor/128x128/apps/org.polyfrost.oneclient.png"
install -Dm644 "apps/oneclient/distribution/icons/512x512.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/org.polyfrost.oneclient.png"
install -Dm644 "apps/oneclient/distribution/flatpak/org.polyfrost.oneclient" "$pkgdir/usr/share/applications/org.polyfrost.oneclient.desktop"
}

View file

@ -1,28 +0,0 @@
cask "oneclient" do
version "0.1.0"
sha256 ""
url "https://oneclient.polyfrost.org/versions/#{version}/macos/OneClient_#{version}_universal.dmg"
name "OneClient"
desc "Next-generation open source Minecraft launcher"
homepage "https://polyfrost.org"
livecheck do
url "https://polyfrost.org/projects/oneclient"
regex("/OneClient[._-]v(\d+(?:\.\d+)+)[._-]universal\.dmg/i")
end
auto_updated true
depends_on macos: ">= :high_sierra"
app "OneClient.app"
uninstall quit: "OneClient"
zap trash: [
"~/Library/Application Support/OneClient",
"~/Library/Caches/OneClient",
"~/Library/Saved Application State/OneClient.savedState",
"~/Library/WebKit/OneClient",
]
end

View file

@ -1,12 +0,0 @@
[Desktop Entry]
Name=OneClient
Comment=Next-generation open source Minecraft launcher
Exec=oneclient
Icon=org.polyfrost.client
Type=Application
Terminal=false
StartupNotify=true
Categories=Game;
Keywords=game;launcher;mc;minecraft;
StartupWMClass=OneClient
Version=1.4

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
</component>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -1,12 +0,0 @@
[Desktop Entry]
Name=OneClient
Comment=Next-generation open source Minecraft launcher
Exec={{exec}}
Icon={{icon}}
Type=Application
Terminal=false
StartupNotify=true
Categories=Game;ActionGame;AdventureGame;Simulation;
Keywords=mc;minecraft;game
StartupWMClass={{exec}}
Version=1.4

View file

@ -1,28 +0,0 @@
{
"version": "0.1.0",
"homepage": "https://polyfrost.org/projects/oneclient",
"description": "Next-generation open source Minecraft launcher",
"license": "GPL-3.0-only",
"architecture": {
"64bit": {
"url": "https://oneclient.polyfrost.org/versions/0.1.0/windows/OneClient_0.1.0_x64_en-US.msi",
"hash": "hhjgskdfghskfdlafjdshjadlk"
}
},
"shortcuts": [
[
"PFiles\\OneClient\\OneClient.exe",
"OneClient"
]
],
"checkver": {
"github": "https://github.com/polyfrost/onelauncher"
},
"autoupdat": {
"architecture": {
"64bit": {
"url": "https://oneclient.polyfrost.org/versions/$version/windows/OneClient_$version_x64_en-US.msi"
}
}
}
}

View file

@ -1,13 +0,0 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.5.0.schema.json
PackageIdentifier: Polyfrost.OneClient
PackageVersion: 0.1.0
InstallerLocale: en-US
InstallerType: wix
ProductCode: '{}'
Installers:
- Architecture: x64
InstallerUrl: https://oneclient.polyfrost.org/versions/0.1.0/windows/OneClient_0.1.0_x64_en-US.msi
InstallerSha256: '1111111111111111111111111111318274823726424624666666666666666666666666666666661111E1235666'
ManifestType: installer
ManifestVersion: 1.5.0

View file

@ -1,11 +0,0 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.5.0.schema.json
PackageIdentifier: Polyfrost.OneClient
PackageVersion: 0.1.0
PackageLocale: en-US
Publisher: Polyfrost
PackageName: OneClient
License: GPL-3.0
ShortDescription: Next-generation open source Minecraft launcher
ManifestType: defaultLocale
ManifestVersion: 1.5.0

View file

@ -1,7 +0,0 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.5.0.schema.json
PackageIdentifier: Polyfrost.OneClient
PackageVersion: 0.1.0
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.5.0

View file

@ -1,8 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
# **/*.gen.ts
routeTree.gen.ts

View file

@ -1,3 +0,0 @@
package-lock.json
pnpm-lock.yaml
yarn.lock

View file

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-tsrouter-app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>OneClient</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,64 +0,0 @@
{
"name": "@oneclient/frontend",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"start": "vite",
"build": "vite build && tsc",
"serve": "vite preview",
"lint": "eslint",
"check": "eslint --fix"
},
"dependencies": {
"@fontsource/poppins": "catalog:",
"@formatjs/intl-durationformat": "catalog:",
"@onelauncher/common": "workspace:*",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "catalog:",
"@tanstack/match-sorter-utils": "catalog:",
"@tanstack/react-devtools": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-query-devtools": "catalog:",
"@tanstack/react-router": "catalog:",
"@tanstack/react-router-devtools": "catalog:",
"@tanstack/react-table": "catalog:",
"@tanstack/react-virtual": "catalog:",
"@tanstack/router-plugin": "catalog:",
"@tauri-apps/api": "catalog:",
"@tauri-apps/plugin-clipboard-manager": "catalog:",
"@tauri-apps/plugin-dialog": "catalog:",
"@tauri-apps/plugin-fs": "catalog:",
"@tauri-apps/plugin-global-shortcut": "catalog:",
"@untitled-theme/icons-react": "catalog:",
"fuse.js": "^7.1.0",
"motion": "catalog:",
"overlayscrollbars": "catalog:",
"overlayscrollbars-react": "catalog:",
"react": "catalog:",
"react-aria-components": "catalog:",
"react-dom": "catalog:",
"react-just-parallax": "catalog:",
"react-markdown": "^10.1.0",
"react-toastify": "catalog:",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"skinview3d": "catalog:",
"tailwind-merge": "catalog:",
"tailwind-variants": "catalog:",
"tailwindcss": "catalog:",
"tailwindcss-animated": "catalog:",
"tailwindcss-react-aria-components": "catalog:",
"taurpc": "catalog:",
"zustand": "catalog:"
},
"devDependencies": {
"@tanstack/eslint-config": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-svgr": "catalog:"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 MiB

View file

@ -1,38 +0,0 @@
<svg width="319" height="277" viewBox="0 0 319 277" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.043 246.222C190.998 246.222 246.086 191.104 246.086 123.111C246.086 55.1187 190.998 0 123.043 0C55.0882 0 0 55.1187 0 123.111C0 191.104 55.0882 246.222 123.043 246.222Z" fill="url(#paint0_linear_3343_6724)"/>
<mask id="mask0_3343_6724" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="247" height="247">
<path d="M123.043 246.222C190.998 246.222 246.086 191.104 246.086 123.111C246.086 55.1187 190.998 0 123.043 0C55.0882 0 0 55.1187 0 123.111C0 191.104 55.0882 246.222 123.043 246.222Z" fill="white"/>
</mask>
<g mask="url(#mask0_3343_6724)">
<path opacity="0.77" d="M206.791 198.326C203.4 187.661 211.376 176.767 203.959 159.122C196.542 141.478 190.524 145.731 184.325 134.945C178.126 124.159 185.887 120.568 179.165 115.976C175.679 113.595 172.615 115.463 169.434 117.33C166.478 119.064 163.422 120.799 159.83 119.132C152.369 115.668 153.317 106.251 147.467 100.365C141.616 94.4799 107.864 101.061 109.737 86.285C111.61 71.5094 161.369 52.1559 168.568 52.1604C175.768 52.165 179.291 64.7588 191.038 66.9406C196.782 68.0075 203.408 65.3447 209.862 62.6739C216.61 59.8868 223.172 57.1008 228.346 58.577C238.469 61.4656 318.417 137.044 318.417 137.044L233.795 216.383C217.533 201.777 210.185 208.99 206.791 198.326ZM89.7868 209.651C81.1954 208.398 67.3428 194.749 59.86 181.648C52.3772 168.547 64.3032 163.962 59.86 157.254C55.4168 150.546 41.7636 140.598 39.9088 136.735C38.0541 132.873 45.6577 126.269 39.9088 118.155C34.16 110.041 25.776 118.292 16.9089 104.291C8.04187 90.2907 4.43945 62.1518 4.43945 62.1518L67.6197 -12.4319L79.4945 6.62298L96.3616 18.6457C96.3616 18.6457 120.111 31.7969 119.16 40.8023C118.209 49.8076 94.4248 52.8979 92.5586 54.6637C90.6925 56.4294 92.9004 63.8138 84.2419 71.2996C75.5833 78.7855 65.0563 75.3201 57.9198 84.607C57.9198 84.607 56.8386 90.2679 56.2849 96.2342C55.7312 102.201 55.7073 108.452 55.7073 108.452C57.7979 111.522 60.5891 110.014 64.2223 108.504C67.8134 107.014 72.2247 105.522 77.5975 108.452C88.4037 114.344 98.9341 132.02 98.9341 132.02C98.9341 132.02 123.043 133.197 125.814 140.613C128.586 148.03 111.874 158.585 110.016 161.686C108.158 164.787 111.47 169.527 107.522 175.765C103.574 182.003 99.2782 177.835 94.222 186.639C89.3595 195.105 97.6911 209.721 90.6993 209.722C90.3938 209.721 90.0889 209.697 89.7868 209.651Z" fill="url(#paint1_linear_3343_6724)" fill-opacity="0.8"/>
</g>
<g filter="url(#filter0_d_3343_6724)">
<path d="M284.821 104.872H173.171C158.7 104.872 146.968 116.611 146.968 131.09V242.802C146.968 257.282 158.7 269.02 173.171 269.02H284.821C299.293 269.02 311.025 257.282 311.025 242.802V131.09C311.025 116.611 299.293 104.872 284.821 104.872Z" fill="url(#paint2_linear_3343_6724)"/>
<path d="M284.821 105.442H173.171C159.014 105.442 147.537 116.925 147.537 131.091V242.802C147.537 256.968 159.014 268.451 173.171 268.451H284.821C298.978 268.451 310.455 256.968 310.455 242.802V131.091C310.455 116.925 298.978 105.442 284.821 105.442Z" stroke="white" stroke-opacity="0.5"/>
</g>
<path d="M266.21 220.077L240.946 150.478C239.852 147.628 238.43 146.867 235.258 146.867H222.79C219.618 146.867 218.196 147.524 217.103 150.697L191.947 220.077C190.853 223.032 191.514 224.564 194.9 224.564H206.384C211.853 224.564 213.275 223.36 214.368 219.968L216.884 212.088H240.503L243.019 219.968C243.899 223.36 245.539 224.564 251.226 224.564H263.039C266.32 224.564 267.304 223.032 266.21 220.077ZM231.54 182.104L235.586 194.908H221.915L225.962 181.995C227.325 177.039 228.024 171.924 228.04 166.784H229.133C229.356 171.965 230.163 177.104 231.54 182.104Z" fill="#2B4BFF" fill-opacity="0.76"/>
<defs>
<filter id="filter0_d_3343_6724" x="138.968" y="96.8723" width="176.058" height="176.148" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="-2" dy="-2"/>
<feGaussianBlur stdDeviation="3"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.078 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3343_6724"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3343_6724" result="shape"/>
</filter>
<linearGradient id="paint0_linear_3343_6724" x1="63.244" y1="2.78205e-06" x2="195.749" y2="233.109" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A64FF"/>
<stop offset="0.481" stop-color="#415EFF"/>
<stop offset="1" stop-color="#2B4BFF"/>
</linearGradient>
<linearGradient id="paint1_linear_3343_6724" x1="102.401" y1="-12.4319" x2="134.26" y2="220.105" gradientUnits="userSpaceOnUse">
<stop stop-color="#EAF6FF"/>
<stop offset="1" stop-color="#C6E1F5"/>
</linearGradient>
<linearGradient id="paint2_linear_3343_6724" x1="190.607" y1="102.082" x2="254.159" y2="268.997" gradientUnits="userSpaceOnUse">
<stop stop-color="#C8D6F5"/>
<stop offset="1" stop-color="#B6CCFC"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5 KiB

View file

@ -1,9 +0,0 @@
<svg width="153" height="32" viewBox="0 0 153 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.4648 25.3793V7.44458H25.2079L26.3799 11.0264V25.3793H22.4648ZM34.6091 25.3793L24.2353 11.8393L25.2079 7.44458L35.5818 20.9846L34.6091 25.3793ZM34.6091 25.3793L33.5616 21.7975V7.44458H37.4768V25.3793H34.6091ZM42.3714 25.3793V7.44458H46.2866V25.3793H42.3714ZM45.364 25.3793V21.9245H55.0144V25.3793H45.364ZM45.364 17.8854V14.5575H54.1416V17.8854H45.364ZM45.364 10.874V7.44458H54.8898V10.874H45.364Z" fill="#D5DBFF" fill-opacity="0.85"/>
<path d="M67.2479 25.6588C65.9345 25.6588 64.7124 25.4302 63.5821 24.9729C62.4683 24.4987 61.4875 23.8382 60.6396 22.9915C59.8083 22.1447 59.16 21.1624 58.6945 20.0447C58.229 18.91 57.9961 17.6906 57.9961 16.3866C57.9961 15.0825 58.229 13.8716 58.6945 12.7539C59.16 11.6192 59.8083 10.6369 60.6396 9.80712C61.4707 8.97727 62.4434 8.33371 63.5572 7.87644C64.6875 7.40227 65.9096 7.16516 67.2229 7.16516C68.6192 7.16516 69.8578 7.3938 70.9385 7.85104C72.0355 8.30831 72.9914 8.93493 73.8063 9.7309L71.1877 12.4236C70.7222 11.9156 70.157 11.5176 69.4922 11.2297C68.8437 10.9418 68.0873 10.7979 67.2229 10.7979C66.4582 10.7979 65.7514 10.9333 65.1031 11.2043C64.4714 11.4583 63.9227 11.8309 63.4572 12.322C63.0085 12.8132 62.6511 13.4059 62.3852 14.1003C62.1358 14.7946 62.0109 15.5567 62.0109 16.3866C62.0109 17.2334 62.1358 18.0039 62.3852 18.6983C62.6511 19.3926 63.0085 19.9854 63.4572 20.4765C63.9227 20.9676 64.4714 21.3487 65.1031 21.6197C65.7514 21.8907 66.4582 22.0261 67.2229 22.0261C68.1206 22.0261 68.9019 21.8822 69.567 21.5943C70.2318 21.3064 70.8053 20.9084 71.2876 20.4003L73.9308 23.0931C73.083 23.889 72.1105 24.5156 71.0133 24.9729C69.9325 25.4302 68.6774 25.6588 67.2479 25.6588ZM77.5831 25.3794V7.44461H81.498V25.3794H77.5831ZM80.6003 25.3794V21.8737H89.3031V25.3794H80.6003ZM93.0575 25.3794V7.44461H96.9726V25.3794H93.0575ZM101.859 25.3794V7.44461H105.774V25.3794H101.859ZM104.852 25.3794V21.9245H114.502V25.3794H104.852ZM104.852 17.8854V14.5575H113.63V17.8854H104.852ZM104.852 10.874V7.44461H114.378V10.874H104.852ZM118.698 25.3794V7.44461H121.441L122.613 11.0265V25.3794H118.698ZM130.842 25.3794L120.468 11.8394L121.441 7.44461L131.814 20.9846L130.842 25.3794ZM130.842 25.3794L129.795 21.7975V7.44461H133.71V25.3794H130.842ZM142.918 25.3794V7.69862H146.833V25.3794H142.918ZM137.407 10.9249V7.44461H152.345V10.9249H137.407Z" fill="#D5DBFF"/>
<path d="M7.90642 7.17236H6.63633C5.1788 7.17236 3.83197 7.95838 3.1032 9.23429L0.259 14.2139C0.161603 14.3844 0.0772228 14.5602 0.00585938 14.7399L4.04665 13.9875C5.67828 13.6836 6.95495 12.3931 7.25554 10.7439L7.90642 7.17236Z" fill="#D5DBFF"/>
<path d="M0 17.7972C0.072567 17.982 0.158862 18.1627 0.258886 18.3378L3.10308 23.3174C3.83185 24.5933 5.17868 25.3793 6.63622 25.3793H7.90877L7.25542 21.7944C6.95484 20.1451 5.67817 18.8546 4.04654 18.5508L0 17.7972Z" fill="#D5DBFF"/>
<path d="M11.1172 25.3793H12.3254C13.7829 25.3793 15.1297 24.5933 15.8585 23.3174L18.7027 18.3378C18.8004 18.1668 18.885 17.9904 18.9565 17.8102L14.9794 18.5508C13.3478 18.8546 12.0711 20.1451 11.7705 21.7944L11.1172 25.3793Z" fill="#D5DBFF"/>
<path d="M18.9502 14.727C18.8799 14.5518 18.7973 14.3803 18.7022 14.2139L15.858 9.23429C15.1292 7.95835 13.7824 7.17236 12.3249 7.17236H11.1191L11.7701 10.7439C12.0706 12.3931 13.3473 13.6836 14.9789 13.9875L18.9502 14.727Z" fill="#D5DBFF"/>
<path d="M9.47318 7.52967C9.47851 7.48181 9.54735 7.48181 9.55268 7.52967L9.86729 10.3575C10.1879 13.2396 12.4348 15.5162 15.2853 15.8472L18.5731 16.229C18.6203 16.2345 18.6203 16.3038 18.5731 16.3093L15.2853 16.6911C12.4348 17.0221 10.1879 19.2987 9.86729 22.1808L9.55268 25.0086C9.54735 25.0565 9.47851 25.0565 9.47318 25.0086L9.15908 22.1852C8.83818 19.3011 6.58846 17.0236 3.73565 16.6949L0.388983 16.3093C0.341693 16.3039 0.341693 16.2344 0.388983 16.229L3.73565 15.8434C6.58846 15.5147 8.83818 13.2372 9.15908 10.3531L9.47318 7.52967Z" fill="#D5DBFF" fill-opacity="0.85"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -1,9 +0,0 @@
<svg width="565" height="116" viewBox="0 0 565 116" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M82.9609 92V26.9865H93.0904L97.4185 39.9707V92H82.9609ZM127.807 92L89.499 42.9175L93.0904 26.9865L131.399 76.0689L127.807 92ZM127.807 92L123.939 79.0157V26.9865H138.397V92H127.807ZM156.472 92V26.9865H170.93V92H156.472ZM167.523 92V79.4761H203.16V92H167.523ZM167.523 64.8343V52.7709H199.937V64.8343H167.523ZM167.523 39.4182V26.9865H202.7V39.4182H167.523Z" fill="#0A5BE8"/>
<path d="M248.333 93.013C243.483 93.013 238.97 92.1842 234.796 90.5266C230.683 88.8076 227.061 86.4134 223.93 83.3438C220.86 80.2742 218.466 76.7135 216.747 72.6617C215.028 68.5485 214.168 64.1283 214.168 59.4011C214.168 54.674 215.028 50.2845 216.747 46.2327C218.466 42.1194 220.86 38.5587 223.93 35.5506C226.999 32.5424 230.591 30.2095 234.704 28.5519C238.878 26.833 243.391 25.9735 248.241 25.9735C253.397 25.9735 257.971 26.8023 261.962 28.4598C266.013 30.1174 269.543 32.3889 272.552 35.2743L262.882 45.0355C261.163 43.1938 259.076 41.7511 256.621 40.7074C254.226 39.6638 251.433 39.142 248.241 39.142C245.417 39.142 242.807 39.6331 240.413 40.6154C238.08 41.5362 236.054 42.8868 234.335 44.6672C232.678 46.4475 231.358 48.5962 230.376 51.1133C229.455 53.6303 228.994 56.393 228.994 59.4011C228.994 62.4707 229.455 65.264 230.376 67.7811C231.358 70.2981 232.678 72.4468 234.335 74.2272C236.054 76.0075 238.08 77.3888 240.413 78.3711C242.807 79.3534 245.417 79.8445 248.241 79.8445C251.556 79.8445 254.441 79.3227 256.897 78.279C259.352 77.2354 261.47 75.7927 263.251 73.9509L273.012 83.7122C269.881 86.5976 266.29 88.869 262.238 90.5266C258.247 92.1842 253.612 93.013 248.333 93.013ZM286.499 92V26.9865H300.956V92H286.499ZM297.641 92V79.292H329.779V92H297.641ZM343.643 92V26.9865H358.101V92H343.643ZM376.147 92V26.9865H390.604V92H376.147ZM387.197 92V79.4761H422.835V92H387.197ZM387.197 64.8343V52.7709H419.612V64.8343H387.197ZM387.197 39.4182V26.9865H422.375V39.4182H387.197ZM438.327 92V26.9865H448.457L452.785 39.9707V92H438.327ZM483.174 92L444.865 42.9175L448.457 26.9865L486.765 76.0689L483.174 92ZM483.174 92L479.306 79.0157V26.9865H493.764V92H483.174ZM527.77 92V27.9073H542.227V92H527.77ZM507.418 39.6024V26.9865H562.579V39.6024H507.418Z" fill="#0048C5"/>
<path d="M29.1968 26H24.5066C19.1242 26 14.1506 28.8493 11.4594 33.4745L0.956285 51.5255C0.596616 52.1437 0.285016 52.781 0.0214844 53.4323L14.9434 50.7047C20.9687 49.6033 25.6832 44.9253 26.7932 38.9467L29.1968 26Z" fill="#0048C5"/>
<path d="M0 64.5149C0.267976 65.1846 0.586648 65.8397 0.956016 66.4745L11.4591 84.5255C14.1503 89.1507 19.1239 92 24.5063 92H29.2056L26.7929 79.0045C25.6829 73.0259 20.9684 68.3479 14.9431 67.2465L0 64.5149Z" fill="#0048C5"/>
<path d="M41.0508 92H45.5124C50.8948 92 55.8683 89.1507 58.5595 84.5255L69.0627 66.4745C69.4234 65.8545 69.7358 65.2152 69.9998 64.5618L55.3133 67.2465C49.288 68.3479 44.5735 73.0259 43.4635 79.0045L41.0508 92Z" fill="#0048C5"/>
<path d="M69.9782 53.3854C69.7187 52.7504 69.4135 52.1288 69.0625 51.5255L58.5593 33.4745C55.8681 28.8492 50.8946 26 45.5122 26H41.0596L43.4633 38.9467C44.5733 44.9253 49.2878 49.6033 55.3131 50.7047L69.9782 53.3854Z" fill="#0048C5"/>
<path d="M34.9819 27.295C35.0016 27.1215 35.2558 27.1215 35.2755 27.295L36.4373 37.5459C37.6214 47.9936 45.9187 56.2463 56.445 57.4461L68.5861 58.83C68.7606 58.8499 68.7606 59.1013 68.5861 59.1212L56.445 60.5051C45.9187 61.7049 37.6214 69.9576 36.4373 80.4053L35.2755 90.6561C35.2558 90.8297 35.0016 90.8297 34.9819 90.6561L33.822 80.4214C32.637 69.9663 24.3292 61.7104 13.7943 60.5189L1.43566 59.1212C1.26103 59.1014 1.26103 58.8497 1.43566 58.83L13.7943 57.4322C24.3292 56.2407 32.637 47.9848 33.822 37.5298L34.9819 27.295Z" fill="#0A5BE8"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -1,419 +0,0 @@
// @ts-nocheck
// This file has been generated by Specta. DO NOT EDIT.
import { createTauRPCProxy as createProxy, type InferCommandOutput } from 'taurpc'
type TAURI_CHANNEL<T> = (response: T) => void
export type ApplyBundleUpdatesResult = { updates_applied: BundlePackageUpdate[]; removals_applied: BundlePackageRemoval[]; additions_applied: BundlePackageAddition[]; updates_failed: string[]; removals_failed: string[]; additions_failed: string[] }
export type BundlePackageAddition = { cluster_id: number; bundle_name: string; new_file: ModpackFile }
export type BundlePackageRemoval = { cluster_id: number; package_hash: string; package_id: string; bundle_name: string; installed_at: string }
export type BundlePackageUpdate = { cluster_id: number; installed_package_hash: string; installed_version_id: string; bundle_name: string; new_version_id: string; new_file: ModpackFile; installed_at: string }
export type ClusterError = { type: "InvalidVersion"; data: string } | { type: "MismatchedVersion"; data: string } | { type: "MismatchedLoader"; data: string } | { type: "MissingJavaVersion"; data: string } | { type: "ClusterDownloading"; data: string } | { type: "ClusterAlreadyRunning"; data: string }
export type ClusterModel = { id: number; folder_name: string; stage: ClusterStage; created_at: string; group_id: number | null; name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; last_played: string | null; overall_played: number | null; icon_url: Icon | null; setting_profile_name: string | null; linked_modpack_hash: string | null }
export type ClusterStage = "notready" | "downloading" | "repairing" | "ready"
export type ClusterUpdate = { name: string | null; icon_url: Icon | null; setting_profile_name: string | null }
export type CopyClusterResult = {
/**
* Files that had no compatible provider version and were copied verbatim from the source.
*/
fallback_files: string[] }
export type CreateCluster = { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon: Icon | null }
export type CryptoError = { type: "InvalidHash"; data: string } | { type: "InvalidAlgorithm"; data: string }
export type DaoError = { type: "NotFound"; data: string } | { type: "AlreadyExists"; data: string } | { type: "InvalidValue"; data: string }
export type DbVec = T[]
export type DebugInfoData = { inDev: boolean; platform: string; arch: string; family: string; locale: string; osType: string; osVersion: string; osDistro: string; commitHash: string; buildTimestamp: string; buildVersion: string; hasDevSettings: boolean }
export type DebugInfoParsedLine = { title: string; value: string }
export type DirectoryError = { type: "BaseDir"; data: string }
export type DiscordError = { type: "MissingClientId"; data: string } | { type: "ConnectError"; data: string }
export type ExternalPackage = { name: string; url: string; sha1: string; size: number; package_type: PackageType }
export type Filters = { game_versions: string[] | null; loaders: GameLoader[] | null; categories: PackageCategories | null; package_type: PackageType | null }
export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric"
export type IOError = { type: "InvalidAbsolutePath"; data: string } | { type: "FileNotFoundInZip"; data: string } | { type: "IOErrorWrapper"; data: string } | { type: "IOError"; data: string } | { type: "DeserializeError"; data: string } | { type: "AsyncZipError"; data: string } | { type: "TempFileError"; data: string }
export type Icon = string
export type IncompatiblePackageType = { type: "McVersion"; data: string } | { type: "Loader"; data: string }
export type IngressError = { type: "NotFound"; data: string }
export type IngressPayload = { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number }
export type IngressType = { Download: { file_name: string } } | "JavaPrepare" | "JavaCheck" | "JavaLocate" | "MinecraftDownload" | { PrepareCluster: { cluster_name: string } }
export type JavaError = { type: "ParseVersion"; data: string } | { type: "Execute"; data: string } | { type: "MissingJava"; data: string }
export type LauncherError = { type: "DirError"; data: DirectoryError } | { type: "IOError"; data: IOError } | { type: "IngressError"; data: IngressError } | { type: "JavaError"; data: JavaError } | { type: "CryptoError"; data: CryptoError } | { type: "DiscordError"; data: DiscordError } | { type: "MetadataError"; data: MetadataError } | { type: "ClusterError"; data: ClusterError } | { type: "MinecraftAuthError"; data: MinecraftAuthError } | { type: "ProcessError"; data: ProcessError } | { type: "PackageError"; data: PackageError } | { type: "DaoError"; data: DaoError } | { type: "SerdeError"; data: string } | { type: "AnyhowError"; data: string } | { type: "DbError"; data: string } | { type: "ReqwestError"; data: string } | { type: "InterfrostError"; data: string } | { type: "RegexError"; data: string } | { type: "SemaphoreError"; data: string } | { type: "UrlError"; data: string } | { type: "OpenerError"; data: string } | { type: "TauriError"; data: string }
export type ManagedPackage = { id: string; slug: string; provider: Provider; package_type: PackageType; name: string; short_desc: string; body: ManagedPackageBody;
/**
* Won't have all versions for some providers, like `CurseForge`.
* Try making a request to get the versions if needed
*/
version_ids: string[]; mc_versions: string[]; loaders: GameLoader[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; categories: PackageCategories; license: PackageLicense | null; author: PackageAuthor; links: PackageLinks; status: PackageStatus; downloads: number; gallery: PackageGallery[] }
export type ManagedPackageBody = { Url: string } | { Raw: string }
export type ManagedUser = { id: string; username: string; url?: string | null; avatar_url?: string | null; bio?: string | null; is_organization_user?: boolean; role?: string | null }
export type ManagedVersion = { version_id: string; project_id: string; display_name: string; display_version: string; changelog: string | null; dependencies: ManagedVersionDependency[]; mc_versions: string[]; release_type: PackageReleaseType; loaders: GameLoader[]; published: string; downloads: number; files: ManagedVersionFile[] }
export type ManagedVersionDependency = { version_id: string | null; project_id: string | null; file_name: string | null; dependency_type: PackageDependencyType }
export type ManagedVersionFile = { sha1: string; url: string; file_name: string; primary: boolean; size: number }
export type MessageLevel = "Info" | "Warn" | "Error"
export type MessagePayload = { level: MessageLevel; message: string }
export type MetadataError = { type: "FetchError"; data: string } | { type: "NotModdedManifest"; data: string } | { type: "NotVanillaManifest"; data: string } | { type: "ParseError"; data: string } | { type: "NoMatchingLoader"; data: string } | { type: "RequestedLoaderVersionNotFound"; data: string } | { type: "NoMatchingVersion"; data: string }
export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string } | { type: "XboxError"; data: string }
/**
* A structure of all needed Minecraft credentials for logging in and account management.
*/
export type MinecraftCredentials = {
/**
* The uuid of the credentials.
*/
id: string;
/**
* The username of the Minecraft account.
*/
username: string;
/**
* The access token as a String.
*/
access_token: string;
/**
* The refresh token as a string for [`MinecraftState#refresh`].
*/
refresh_token: string;
/**
* The time that the access token expires as a [`DateTime<Utc>`].
*/
expires: string }
export type ModpackArchive = { manifest: ModpackManifest; path: string; format: ModpackFormat }
export type ModpackFile = { enabled: boolean; hidden?: boolean; kind: ModpackFileKind; overrides?: PackageOverrides | null }
export type ModpackFileKind = { Managed: [ManagedPackage, ManagedVersion] } | { External: ExternalPackage }
export type ModpackFormat = "CurseForge" | "MrPack" | "PolyMrPack"
export type ModpackManifest = { name: string; version: string; loader: GameLoader; loader_version: string; mc_version: string; enabled?: boolean; files: ModpackFile[] }
export type MojangCape = { id: string; state: string; url: string; alias: string }
export type MojangFullPlayerProfile = { id: string; username: string; skins: MojangSkin[]; capes: MojangCape[] }
export type MojangPlayerProfile = { uuid: string; username: string; is_slim: boolean; skin_url: string | null; cape_url: string | null }
export type MojangSkin = { id: string; state: string; url: string; variant: SkinVariant }
export type MowojangProfile = { id: string; username: string }
export type OnlineCluster = { major_version: number; name: string; art: string; entries: OnlineClusterEntry[]; long_description?: string | null; tags: string[] }
export type OnlineClusterEntry = { minor_version: number; name?: string | null; art?: string | null; loader: GameLoader; tags: string[] }
/**
* e.g.
* ```json
* {
* "clusters": [
* {
* "major_version": 21,
* "name": "Tricky Trials",
* "art": "/versions/art/Tricky_Trials.png",
* "entries": [
* {
* "minor_version": 5,
* "loader": "fabric",
* "tags": ["PvP", "Survival"]
* },
* {
* "minor_version": 5,
* "loader": "forge",
* "tags": ["PvP", "Survival"]
* }
* ]
* },
* {
* "major_version": 20,
* "name": "Trails & Tales",
* "art": "/versions/art/Trails_Tales.png",
* "entries": [
* {
* "minor_version": 5,
* "loader": "fabric",
* "tags": ["PvP", "Survival"]
* }
* ]
* }
* ]
* }
* ```
*/
export type OnlineClusterManifest = { clusters: OnlineCluster[] }
export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] }
export type PackageCategories = { Mod: PackageModCategory[] } | { ResourcePack: PackageResourcePackCategory[] } | { Shader: PackageShaderCategory[] } | { DataPack: PackageModCategory[] } | { ModPack: PackageModPackCategory[] }
export type PackageDependencyType = "required" | "optional" | "embedded" | "incompatible"
/**
* <https://api.modrinth.com/v2/tag/donation_platform>
*/
export type PackageDonationPlatform = "patreon" | "buymeacoffee" | "paypal" | "github" | "kofi" | "other"
export type PackageDonationUrl = { id: PackageDonationPlatform; url: string }
export type PackageError = { type: "NoPrimaryFile"; data: string } | { type: "IsNotModPack"; data: string } | { type: "PartialModpackInstall"; data: string } | { type: "Incompatible"; data: IncompatiblePackageType } | { type: "MissingApiKey"; data: string } | { type: "UnsupportedBodyType"; data: string } | { type: "UnsupportedAuthorType"; data: string } | { type: "UnsupportedModpackFormat"; data: string }
export type PackageGallery = { url: string; thumbnail_url: string; title: string | null; description: string | null; featured: boolean | null }
/**
* <https://spdx.org/licenses/>
*/
export type PackageLicense = { id: string; name: string; url: string | null }
/**
* <https://docs.curseforge.com/rest-api/#tocS_ModLinks>
*/
export type PackageLinks = { website: string | null; issues: string | null; source: string | null; wiki: string | null; donation: PackageDonationUrl[] | null; discord: string | null }
export type PackageModCategory = "Adventure" | "Library" | "Equipment" | "Patches" | "Cosmetic" | "Food" | "Magic" | "Information" | "Misc" | "Performance" | "Redstone" | "ServerUtil" | "Storage" | "Technology" | "Farming" | "Automation" | "Transport" | "Utility" | "QoL" | "WorldGen" | "Mobs" | "Economy" | "Social"
export type PackageModPackCategory = "Technology" | "Quests" | "Optimization" | "Multiplayer" | "Magic" | "LightWeight" | "Combat" | "Challenging" | "Adventure"
export type PackageModel = { hash: string; file_name: string; version_id: string; published_at: string; display_name: string; display_version: string; package_type: PackageType; provider: Provider; package_id: string; mc_versions: DbVec<string>; mc_loader: DbVec<GameLoader>; icon: Icon | null }
export type PackageOverrides = { icon: string | null; name: string | null; authors: string[] | null; description: string | null }
export type PackageReleaseType = "release" | "beta" | "alpha"
export type PackageResourcePackCategory = "X8" | "X16" | "X32" | "X48" | "X64" | "X128" | "X256" | "X512" | "VanillaLike" | "Utility" | "Tweaks" | "Themed" | "Simplistic" | "Realistic" | "Modded" | "Decoration" | "Cursed" | "Combat" | "Audio" | "Blocks" | "CoreShaders" | "Gui" | "Fonts" | "Equipment" | "Environment" | "Entities" | "Items" | "Locale" | "Models"
export type PackageShaderCategory = "VanillaLike" | "SemiRealistic" | "Realistic" | "Fantasy" | "Cursed" | "Cartoon" | "Bloom" | "Atmosphere" | "Reflections" | "Shadows" | "PBR" | "PathTracing" | "Foliage" | "ColoredLightning" | "Potato" | "Low" | "Medium" | "High" | "Ultra"
export type PackageSide = "unknown" | "required" | "optional" | "unsupported"
export type PackageStatus = "active" | "abandoned"
export type PackageType = "mod" | "resourcepack" | "shader" | "datapack" | "modpack"
export type Paginated<T> = { total: number; offset: number; limit: number; items: T[] }
export type Process = { pid: number; started_at: string; cluster_id: number; post_hook: string | null; account_id: string }
export type ProcessError = { type: "HookUnsuccessful"; data: string } | { type: "NoPid"; data: string }
export type ProcessLogTail = { content: string; truncated: boolean }
export type ProcessPayload = { cluster_id: number; kind: ProcessPayloadKind }
export type ProcessPayloadKind = { type: "Starting"; command: string } | { type: "Started"; pid: number; started_at: string; post_hook: string | null; account_id: string } | { type: "Stopped"; pid: number; exit_code: number } | { type: "Output"; pid: number; output: string }
export type ProfileUpdate = { res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null }
export type Provider = "Modrinth" | "CurseForge" | "SkyClient" | "Local"
export type Resolution = { width: number; height: number }
export type SearchQuery = { query: string | null; offset: number | null; limit: number | null; sort: Sort | null; filters: Filters | null }
export type SearchResult = { project_id: string; package_type: PackageType; slug: string; author: string; title: string; description: string; categories: PackageCategories; loaders: GameLoader[]; mc_versions: string[]; downloads: number; icon_url: string; date_created: string; date_modified: string; latest_version: string; license: string | null; client_side: PackageSide; server_side: PackageSide;
/**
* List of URLs to images
*/
gallery: string[] }
export type SettingProfileModel = { name: string; java_id: number | null; res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null; os_extra: SettingsOsExtra | null }
export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; seen_onboarding: boolean; mod_list_use_grid: boolean; max_concurrent_requests: number; settings_version: number; native_window_frame: boolean; log_debug_info: boolean; show_tanstack_dev_tools: boolean; toast_position: ToastPosition; toast_duration: number; toast_auto_close: boolean; seen_versions: string[] }
export type SettingsOsExtra = Record<string, never>
export type SkinVariant = "classic" | "slim"
export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated"
export type ToastPosition = "top-right" | "top-left" | "bottom-right" | "bottom-left"
export type Update = { version: string }
/**
* A game version of Minecraft
*/
export type Version = {
/**
* A unique identifier of the version
*/
id: string;
/**
* The release type of the version
*/
type: VersionType;
/**
* A link to additional information about the version
*/
url: string;
/**
* The latest time a file in this version was updated
*/
time: string;
/**
* The time this version was released
*/
releaseTime: string;
/**
* The SHA1 hash of the additional information about the version
*/
sha1: string;
/**
* Whether the version supports the latest player safety features
*/
complianceLevel: number;
/**
* The SHA1 hash of the original unmodified Minecraft version's JSON
* This is only available when using the Polyfrost/Pulseflow mirror
*/
originalSha1?: string | null }
/**
* The version type of Minecraft
*/
export type VersionType =
/**
* A major version, which is stable for all players to use
*/
"release" |
/**
* An experimental version, which is unstable and used for feature previews and beta testing
*/
"snapshot" |
/**
* The oldest versions before the game was released
*/
"old_alpha" |
/**
* Early versions of the game
*/
"old_beta"
const ARGS_MAP = { 'oneclient':'{"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"copyClusterContent":["source_id","target_id"],"getVersions":[],"isBundleSyncing":[],"installUpdate":[],"cacheArt":["path"],"getBundlesFor":["cluster_id"],"getClustersGroupedByMajor":[],"refreshArt":["path"],"extractBundleOverrides":["bundle_path","cluster_id"],"checkForUpdate":[],"updateBundlePackages":["cluster_id"]}', 'core':'{"getProfileOrDefault":["name"],"getWorlds":["id"],"createCluster":["options"],"setDefaultUser":["uuid"],"refreshAccounts":[],"getPackage":["provider","slug"],"getMultiplePackages":["provider","slugs"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"changeSkin":["access_token","skin_url","skin_variant"],"changeCape":["access_token","cape_uuid"],"installModpack":["modpack","cluster_id"],"open":["input"],"isClusterRunning":["cluster_id"],"killProcess":["pid"],"createSettingsProfile":["name"],"updateClusterProfile":["name","profile"],"getDefaultUser":["fallback"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"syncCluster":["cluster_id"],"fetchMinecraftProfile":["uuid"],"searchPackages":["provider","query"],"removeCape":["access_token"],"getLogs":["id"],"fetchLoggedInProfile":["access_token"],"getClusterById":["id"],"getScreenshots":["id"],"removePackage":["cluster_id","package_hash"],"setClusterStage":["id","stage"],"readSettings":[],"getUsersFromAuthor":["provider","author"],"getUser":["uuid"],"getUsers":[],"updateClusterById":["id","request"],"getPackageBody":["provider","body"],"launchCluster":["id","uuid","search_for_java"],"writeSettings":["setting"],"getLinkedPackages":["cluster_id"],"togglePackage":["cluster_id","package_hash"],"refreshAccount":["uuid"],"getRunningProcesses":[],"getClusters":[],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"removeCluster":["id"],"removeUser":["uuid"],"getLogByName":["id","name"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"convertUsernameUUID":["username_uuid"],"setDiscordRPCMessage":["message"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getGameVersions":[],"getGlobalProfile":[],"getProcessLogTail":["id","max_lines"],"getRunningProcessesByClusterId":["cluster_id"]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getOsDistro":[],"getPackageVersion":[],"getArch":[],"getOsVersion":[],"hasDevSettings":[],"getLocale":[],"getFamily":[],"getGitCommitHash":[],"getFullDebugInfoParsedString":[],"getFullDebugInfo":[],"getFullDebugInfoParsed":[],"openDevTools":[],"isInDev":[],"getPlatform":[],"getBuildTimestamp":[],"getType":[]}' }
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
getClusterById: (id: number) => Promise<ClusterModel | null>,
removeCluster: (id: number) => Promise<null>,
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
launchCluster: (id: number, uuid: string | null, searchForJava: boolean | null) => Promise<null>,
updateClusterById: (id: number, request: ClusterUpdate) => Promise<null>,
setClusterStage: (id: number, stage: ClusterStage) => Promise<null>,
getScreenshots: (id: number) => Promise<string[]>,
getWorlds: (id: number) => Promise<string[]>,
getLogs: (id: number) => Promise<string[]>,
getLogByName: (id: number, name: string) => Promise<string | null>,
getProcessLogTail: (id: number, maxLines: number) => Promise<ProcessLogTail | null>,
getRunningProcesses: () => Promise<Process[]>,
getRunningProcessesByClusterId: (clusterId: number) => Promise<Process[]>,
isClusterRunning: (clusterId: number) => Promise<boolean>,
killProcess: (pid: number) => Promise<null>,
getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>,
getGlobalProfile: () => Promise<SettingProfileModel>,
updateClusterProfile: (name: string, profile: ProfileUpdate) => Promise<SettingProfileModel>,
createSettingsProfile: (name: string) => Promise<SettingProfileModel>,
getGameVersions: () => Promise<Version[]>,
getLoadersForVersion: (mcVersion: string) => Promise<GameLoader[]>,
getUsers: () => Promise<MinecraftCredentials[]>,
getUser: (uuid: string) => Promise<MinecraftCredentials | null>,
removeUser: (uuid: string) => Promise<null>,
getDefaultUser: (fallback: boolean | null) => Promise<MinecraftCredentials | null>,
setDefaultUser: (uuid: string | null) => Promise<null>,
openMsaLogin: () => Promise<MinecraftCredentials | null>,
refreshAccounts: () => Promise<MinecraftCredentials[]>,
refreshAccount: (uuid: string) => Promise<MinecraftCredentials | null>,
readSettings: () => Promise<Settings>,
writeSettings: (setting: Settings) => Promise<null>,
searchPackages: (provider: Provider, query: SearchQuery) => Promise<Paginated<SearchResult>>,
getPackage: (provider: Provider, slug: string) => Promise<ManagedPackage>,
getPackageBody: (provider: Provider, body: ManagedPackageBody) => Promise<string>,
getMultiplePackages: (provider: Provider, slugs: string[]) => Promise<ManagedPackage[]>,
getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: number, limit: number) => Promise<Paginated<ManagedVersion>>,
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise<PackageModel>,
downloadExternalPackage: (package: ExternalPackage, clusterId: number, force: boolean | null, skipCompatibility: boolean | null) => Promise<PackageModel | null>,
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]>,
syncCluster: (clusterId: number) => Promise<null>,
getLinkedPackages: (clusterId: number) => Promise<PackageModel[]>,
removePackage: (clusterId: number, packageHash: string) => Promise<null>,
togglePackage: (clusterId: number, packageHash: string) => Promise<boolean>,
installModpack: (modpack: ModpackArchive, clusterId: number) => Promise<null>,
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile>,
fetchLoggedInProfile: (accessToken: string) => Promise<MojangFullPlayerProfile>,
uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, skinVariant: SkinVariant) => Promise<MojangSkin>,
changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise<MojangSkin>,
changeCape: (accessToken: string, capeUuid: string) => Promise<MojangFullPlayerProfile>,
removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
setDiscordRPCMessage: (message: string) => Promise<null>,
open: (input: string) => Promise<null> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest>,
extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise<null>,
checkForUpdate: () => Promise<Update | null>,
installUpdate: () => Promise<null>,
downloadPackageFromBundle: (package: ModpackFileKind, clusterId: number, bundleName: string, skipCompatibility: boolean | null) => Promise<PackageModel>,
updateBundlePackages: (clusterId: number) => Promise<ApplyBundleUpdatesResult>,
isBundleSyncing: () => Promise<boolean>,
cacheArt: (path: string) => Promise<string>,
refreshArt: (path: string) => Promise<null>,
copyClusterContent: (sourceId: number, targetId: number) => Promise<CopyClusterResult> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,
getFamily: () => Promise<string>,
getLocale: () => Promise<string>,
getType: () => Promise<string>,
getPlatform: () => Promise<string>,
getOsVersion: () => Promise<string>,
getOsDistro: () => Promise<string>,
getGitCommitHash: () => Promise<string>,
getBuildTimestamp: () => Promise<string>,
getPackageVersion: () => Promise<string>,
getFullDebugInfo: () => Promise<DebugInfoData>,
getFullDebugInfoParsed: () => Promise<DebugInfoParsedLine[]>,
getFullDebugInfoParsedString: () => Promise<string>,
hasDevSettings: () => Promise<boolean> } };
export type { InferCommandOutput }
export const createTauRPCProxy = () => createProxy<Router>(ARGS_MAP)

View file

@ -1,35 +0,0 @@
import type { ImgHTMLAttributes } from 'react';
import { usePlayerProfile } from '@/hooks/usePlayerProfile';
import { getSkinUrl } from '@/utils/minecraft';
import { renderHeadToDataUrl } from '@onelauncher/common';
import { useEffect, useState } from 'react';
export interface AccountAvatarProps extends ImgHTMLAttributes<HTMLImageElement> {
uuid: string | undefined | null;
}
export function AccountAvatar({
uuid,
alt,
style,
...rest
}: AccountAvatarProps) {
const { data: profile } = usePlayerProfile(uuid);
const [headUrl, setHeadUrl] = useState<string | null>(null);
useEffect(() => {
renderHeadToDataUrl(getSkinUrl(profile?.skin_url)).then(setHeadUrl);
}, [profile]);
return (
<img
alt={alt || `${profile?.username}'s avatars`}
src={headUrl || undefined}
style={{
...style,
imageRendering: 'pixelated',
}}
{...rest}
/>
);
}

View file

@ -1,66 +0,0 @@
import type { Provider, SearchResult } from '@/bindings.gen';
import { Link } from '@tanstack/react-router';
export function PackageGrid({ items, provider, clusterId }: { items: Array<SearchResult>; provider: Provider; clusterId: number }) {
return (
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5! gap-4">
{items.map(item => (
<PackageItem
key={item.project_id}
{...item}
clusterId={clusterId}
provider={provider}
/>
))}
</div>
);
}
export function PackageItem({ provider, clusterId, ...item }: SearchResult & { provider: Provider; clusterId: number }) {
return (
<Link
className="h-full min-w-50 overflow-hidden rounded-lg bg-component-bg hover:bg-component-bg-hover grid grid-rows-[7rem_auto] max-h-74 min-h-74"
preloadDelay={750}
search={{ provider, packageId: item.project_id, clusterId }}
tabIndex={0}
to="/app/cluster/browser/package"
>
<div
className="relative flex items-center justify-center overflow-hidden w-full h-28"
>
{item.icon_url
? (
<>
<img alt={`Icon for ${item.title}`} className="absolute z-0 max-w-none w-7/6 opacity-50 blur-xl" src={item.icon_url} />
<img
alt={`Icon for ${item.title}`}
className="relative z-1 aspect-ratio-square rounded-lg image-render-auto h-5/6"
src={item.icon_url}
/>
</>
)
: (
<div
className="aspect-ratio-square rounded-md bg-border/05 w-2/5"
/>
)}
</div>
<div className="flex flex-1 flex-col gap-2 p-3">
<div className="flex flex-col gap-2">
<h4 className="text-fg-primary font-medium line-height-normal">{item.title}</h4>
<p className="text-xs text-fg-secondary">
By
{' '}
<span className="text-fg-primary">{item.author}</span>
{' '}
on
{' '}
{provider}
</p>
</div>
<p className="max-h-22 flex-1 overflow-hidden text-sm text-fg-secondary line-height-snug">{item.description}</p>
</div>
</Link>
);
}

View file

@ -1,28 +0,0 @@
import type { ClusterModel, ModpackFile } from '@/bindings.gen';
import { ModCard, useModCardContext } from '@/components';
import { useSettings } from '@/hooks/useSettings';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { twMerge } from 'tailwind-merge';
interface BundleProps {
files: Array<ModpackFile>;
cluster: ClusterModel;
}
export function Bundle({ files, cluster }: BundleProps) {
const { useVerticalGridLayout } = useModCardContext();
const { setting } = useSettings();
const useGridLayout = setting('mod_list_use_grid');
const visibleFiles = files.filter(file => !file.hidden);
return (
<OverlayScrollbarsComponent className="bg-page-elevated rounded-lg">
<div className={twMerge('grid gap-2 p-1', useGridLayout || useVerticalGridLayout ? 'grid-cols-3' : 'grid-cols-1', useVerticalGridLayout ? 'max-h-112' : 'max-h-148')}>
{visibleFiles.map(file => (
<ModCard cluster={cluster} file={file} key={'Managed' in file.kind ? file.kind.Managed[0].id : file.kind.External.url} />
))}
<div className="h-1" />
</div>
</OverlayScrollbarsComponent>
);
}

View file

@ -1,47 +0,0 @@
import type { ClusterModel, ModpackFile } from '@/bindings.gen';
import { useSettings } from '@/hooks/useSettings';
import { bindings } from '@/main';
import { useCommandMut } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
import { Download01Icon } from '@untitled-theme/icons-react';
import { twMerge } from 'tailwind-merge';
export function DownloadModButton({ cluster, file }: { cluster: ClusterModel; file: ModpackFile }) {
const download = useCommandMut(async () => {
if ('Managed' in file.kind) {
const [pkg, version] = file.kind.Managed;
if (version.dependencies.length > 0)
for (const dependency of version.dependencies)
if (dependency.dependency_type === 'required') {
const slug = dependency.project_id ?? '';
const versions = await bindings.core.getPackageVersions(pkg.provider, slug, cluster.mc_version, cluster.mc_loader, 0, 1);
await bindings.core.downloadPackage(pkg.provider, slug, versions.items[0].version_id, cluster.id, null);
}
await bindings.core.downloadPackage(pkg.provider, pkg.id, version.version_id, cluster.id, null);
}
else {
await bindings.core.downloadExternalPackage(file.kind.External, cluster.id, null, null);
}
});
const handleDownload = () => {
(async () => {
await download.mutateAsync();
})();
};
const { setting } = useSettings();
const useGridLayout = setting('mod_list_use_grid');
return (
<Button
className={twMerge('flex flex-col items-center justify-center', useGridLayout ? 'w-full' : '')}
color="primary"
onClick={handleDownload}
size={useGridLayout ? 'large' : 'iconLarge'}
>
<Download01Icon />
</Button>
);
}

View file

@ -1,283 +0,0 @@
import type { ClusterModel, ModpackFile, ModpackFileKind, PackageModel } from '@/bindings.gen';
import MissingLogo from '@/assets/misc/missingLogo.svg';
import { ModTag } from '@/components';
import { useSettings } from '@/hooks/useSettings';
import { bindings } from '@/main';
import { useCommandMut } from '@onelauncher/common';
import { useQueryClient } from '@tanstack/react-query';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface ModInfo {
name: string;
description: string | null;
author: string | null;
iconURL: string | null;
url: string | null;
managed: boolean;
packageSlug: string | null;
}
async function getModAuthor(kind: ModpackFileKind, useVerticalGridLayout?: boolean): Promise<string | null> {
if ('External' in kind)
return null;
if (!('Managed' in kind))
return null;
const authors = await bindings.core.getUsersFromAuthor(kind.Managed[0].provider, kind.Managed[0].author);
return parseAuthors(authors.map(author => author.username), useVerticalGridLayout);
}
function parseAuthors(usernames: Array<string>, useVerticalGridLayout?: boolean): string | null {
if (usernames.length === 0)
return null;
if (useVerticalGridLayout) {
let visibleCount = usernames.length;
while (visibleCount >= 1) {
const hiddenUsers = usernames.length - visibleCount;
let string = usernames.slice(0, visibleCount).join(', ');
if (hiddenUsers > 0)
string += ` and ${hiddenUsers} more`;
if (string.length <= 21)
return string;
visibleCount--;
}
return `${usernames[0]} and ${usernames.length - 1} more`;
}
return usernames.join(', ');
}
export function getModMetaDataName(file: ModpackFile): string {
if ('External' in file.kind)
return file.overrides?.name ?? file.kind.External.name.replaceAll('.jar', '');
if ('Managed' in file.kind)
return file.overrides?.name ?? file.kind.Managed[0].name;
else return 'UNKNOWN';
}
function getFileKey(file: ModpackFile): string {
if ('Managed' in file.kind)
return `managed:${file.kind.Managed[0].id}`;
if ('External' in file.kind)
return `external:${file.kind.External.sha1 || file.kind.External.url || file.kind.External.name}`;
return 'unknown';
}
const modMetadataCache: Map<string, Promise<ModInfo>> = new Map();
function fetchModMetaData(file: ModpackFile, useVerticalGridLayout?: boolean): Promise<ModInfo> {
const key = getFileKey(file);
const existing = modMetadataCache.get(key);
if (existing)
return existing;
const promise = getModMetaData(file, useVerticalGridLayout).catch((err) => {
modMetadataCache.delete(key);
throw err;
});
modMetadataCache.set(key, promise);
return promise;
}
async function getModMetaData(file: ModpackFile, useVerticalGridLayout?: boolean): Promise<ModInfo> {
if ('External' in file.kind)
return {
name: getModMetaDataName(file),
description: file.overrides?.description ?? null,
author: parseAuthors(file.overrides?.authors ?? [], useVerticalGridLayout),
iconURL: file.overrides?.icon ?? null,
url: null,
managed: false,
packageSlug: null,
};
return {
name: getModMetaDataName(file),
description: file.overrides?.description ?? file.kind.Managed[0].short_desc,
author: (file.overrides?.authors ?? []).length > 1 ? parseAuthors(file.overrides?.authors ?? []) : await getModAuthor(file.kind, useVerticalGridLayout),
iconURL: file.overrides?.icon ?? file.kind.Managed[0].icon_url,
url: `https://modrinth.com/project/${file.kind.Managed[0].slug}`,
managed: true,
packageSlug: file.kind.Managed[0].id,
};
}
interface ModCardProps {
file: ModpackFile;
cluster: ClusterModel;
}
export type onClickOnMod = (file: ModpackFile) => void;
export interface ModCardContextApi {
enableClickToDownload?: boolean;
onClickOnMod?: onClickOnMod;
useVerticalGridLayout?: boolean;
mods?: Array<ModpackFile>;
installedPackages?: Array<PackageModel>;
useToggleMode?: boolean;
skipCompatibilityChecks?: boolean;
}
const DEFAULT_MOD_CARD_CONTEXT: ModCardContextApi = {};
export const ModCardContext = createContext<ModCardContextApi | null>(null);
export function useModCardContext() {
return useContext(ModCardContext) ?? DEFAULT_MOD_CARD_CONTEXT;
}
export function ModCard({ file, cluster }: ModCardProps) {
const { enableClickToDownload, onClickOnMod, useVerticalGridLayout, mods, installedPackages, useToggleMode, skipCompatibilityChecks = null } = useModCardContext();
const queryClient = useQueryClient();
const [modMetadata, setModMetadata] = useState<ModInfo>({ name: getModMetaDataName(file), description: null, author: null, iconURL: null, url: null, managed: false, packageSlug: null });
useEffect(() => {
let cancelled = false;
fetchModMetaData(file, useVerticalGridLayout).then((meta) => {
if (!cancelled)
setModMetadata(meta);
}).catch(() => {});
return () => {
cancelled = true;
};
}, [file, useVerticalGridLayout]);
const kind = file.kind;
// Directory-based packages (folders dropped into resourcepacks/shaderpacks/datapacks)
// cannot be toggled via .disabled rename — skip toggle mode for them.
const isDirectory = useMemo(() =>
'External' in kind && !kind.External.name.includes('.'), [kind]);
const effectiveToggleMode = useToggleMode && !isDirectory;
const isInstalled = useMemo(() => {
if (installedPackages)
if ('Managed' in kind) {
const [pkg, _] = kind.Managed;
return installedPackages.some(p => p.package_id === pkg.id && p.provider === pkg.provider);
}
else {
return installedPackages.some(p => p.hash === kind.External.sha1);
}
return mods?.includes(file) ?? false;
}, [installedPackages, kind, mods, file]);
// In toggle mode, "selected" = enabled (file_name doesn't end with .disabled)
const isFileEnabled = useMemo(() => {
if (!effectiveToggleMode)
return true;
if ('Managed' in kind) {
const primary = kind.Managed[1].files.find(f => f.primary) ?? kind.Managed[1].files[0];
return !primary.file_name.endsWith('.disabled');
}
return !kind.External.name.endsWith('.disabled');
}, [effectiveToggleMode, kind]);
const [isSelected, setSelected] = useState(effectiveToggleMode ? isFileEnabled : isInstalled);
useEffect(() => {
setSelected(effectiveToggleMode ? isFileEnabled : isInstalled);
}, [isInstalled, effectiveToggleMode, isFileEnabled]);
const download = useCommandMut(async () => {
if ('Managed' in kind) {
const [pkg, version] = kind.Managed;
if (version.dependencies.length > 0)
for (const dependency of version.dependencies)
if (dependency.dependency_type === 'required') {
const slug = dependency.project_id ?? '';
const versions = await bindings.core.getPackageVersions(pkg.provider, slug, cluster.mc_version, cluster.mc_loader, 0, 1);
await bindings.core.downloadPackage(pkg.provider, slug, versions.items[0].version_id, cluster.id, skipCompatibilityChecks);
}
await bindings.core.downloadPackage(pkg.provider, pkg.id, version.version_id, cluster.id, skipCompatibilityChecks);
}
else {
await bindings.core.downloadExternalPackage(kind.External, cluster.id, null, skipCompatibilityChecks);
}
});
const remove = useCommandMut(async () => {
let hash: string | undefined;
if ('Managed' in kind) {
const [_, version] = kind.Managed;
const primary = version.files.find(f => f.primary) ?? version.files[0];
hash = primary.sha1;
}
else {
hash = kind.External.sha1;
}
if (hash)
await bindings.core.removePackage(cluster.id, hash);
});
const toggle = useCommandMut(async () => {
let hash: string | undefined;
if ('Managed' in kind) {
const [_, version] = kind.Managed;
const primary = version.files.find(f => f.primary) ?? version.files[0];
hash = primary.sha1;
}
else {
hash = kind.External.sha1;
}
if (hash)
await bindings.core.togglePackage(cluster.id, hash);
});
const handleOnClick = () => {
// Folder-based packages (resource packs, shader packs, data packs as directories)
// cannot be toggled or removed — treat them as entirely non-interactive.
if (isDirectory)
return;
if (effectiveToggleMode)
(async () => {
await toggle.mutateAsync();
await queryClient.invalidateQueries({ queryKey: ['getLinkedPackages', cluster.id] });
})();
else if (enableClickToDownload)
(async () => {
if (isInstalled)
await remove.mutateAsync();
else
await download.mutateAsync();
await queryClient.invalidateQueries({ queryKey: ['getLinkedPackages', cluster.id] });
})();
else if (onClickOnMod)
onClickOnMod(file);
};
const { setting } = useSettings();
let useGridLayout = setting('mod_list_use_grid');
if (useVerticalGridLayout)
useGridLayout = true;
return (
<div className={twMerge('rounded-lg m-1 break-inside-avoid flex bg-component-bg border border-gray-100/5 p-2 gap-2 outline-2 outline-component-bg', useVerticalGridLayout && useGridLayout ? '' : 'justify-between', useGridLayout ? 'flex-col' : 'flex-row', isSelected ? 'outline-brand bg-brand/20' : '')} onClick={handleOnClick}>
<div className="flex flex-row gap-2">
<div className={twMerge('flex flex-col items-center justify-center', useGridLayout ? (useVerticalGridLayout ? 'size-14' : 'size-20') : 'size-18')}>
<div className={twMerge('rounded-lg bg-component-bg-disabled border border-gray-100/5', useGridLayout ? (useVerticalGridLayout ? 'size-14' : 'size-20') : 'size-18')}>
<img className={twMerge('rounded-lg', modMetadata.iconURL === null ? 'hidden' : '', useGridLayout ? (useVerticalGridLayout ? 'size-14' : 'size-20') : 'size-18')} src={modMetadata.iconURL ?? MissingLogo} />
</div>
</div>
<div className={twMerge('flex flex-col', useVerticalGridLayout && useGridLayout ? 'justify-center' : '')}>
<div className="flex flex-row flex-wrap">
<p className={twMerge('text-fg-primary break-words', useGridLayout ? 'text-lg' : 'text-xl', useGridLayout && !useVerticalGridLayout ? 'max-w-3/5' : '')}>{modMetadata.name}</p>
{useVerticalGridLayout !== true && <ModTag cluster={cluster} modData={modMetadata} />}
</div>
<p className={twMerge(modMetadata.description === null ? 'text-fg-secondary/25' : 'text-fg-secondary', useGridLayout ? 'text-sm' : 'text-base')}>
by
{' '}
<span className="font-semibold">{modMetadata.author ?? 'UNKNOWN'}</span>
</p>
{useVerticalGridLayout !== true && <p className={twMerge('font-normal', modMetadata.description === null ? 'text-fg-secondary/25' : 'text-fg-secondary', useGridLayout ? 'text-sm' : 'text-base')}>{modMetadata.description ?? 'No Description'}</p>}
</div>
</div>
{useVerticalGridLayout === true && modMetadata.description !== null && <p className={twMerge('font-normal text-fg-secondary', useGridLayout ? 'text-sm' : 'text-base')}>{modMetadata.description}</p>}
</div>
);
}

View file

@ -1,106 +0,0 @@
import type { ClusterModel, ModpackArchive } from '@/bindings.gen';
import { Bundle, getModMetaDataName, ModCardContext } from '@/components';
import { useSettings } from '@/hooks/useSettings';
import { Button, Tab, TabContent, TabList, TabPanel, Tabs, TextField } from '@onelauncher/common/components';
import { SearchMdIcon } from '@untitled-theme/icons-react';
import Fuse from 'fuse.js';
import { useContext, useMemo, useState } from 'react';
function getBundleName(name: string): string {
return (name.match(/\[(.*?)\]/)?.[1]) ?? 'Loading...';
}
interface ModListProps {
bundles: Array<ModpackArchive>;
cluster: ClusterModel;
selectedTab?: string;
onTabChange?: (value: string) => void;
toggleBundlePaths?: Set<string>;
}
export function ModList({ bundles, cluster, selectedTab, onTabChange, toggleBundlePaths }: ModListProps) {
const ctx = useContext(ModCardContext);
const useVerticalGridLayout = ctx?.useVerticalGridLayout ?? false;
const { createSetting } = useSettings();
const [useGridLayout, setUseGrid] = createSetting('mod_list_use_grid');
const [searchQuery, setSearchQuery] = useState('');
const nonEmptyBundles = bundles
.map(bundle => ({
...bundle,
manifest: {
...bundle.manifest,
files: bundle.manifest.files.filter(file => !file.hidden),
},
}))
.filter(b => b.manifest.files.length > 0);
const isSearching = searchQuery.trim().length > 0;
const searchResults = useMemo(() => {
if (!isSearching)
return [];
const allFiles = nonEmptyBundles.flatMap(b => b.manifest.files);
const fuse = new Fuse(allFiles, {
keys: [{ name: 'name', getFn: file => getModMetaDataName(file) }],
threshold: 0.4,
distance: 100,
minMatchCharLength: 1,
});
return fuse.search(searchQuery.trim()).map(r => r.item);
}, [isSearching, searchQuery, nonEmptyBundles]);
return (
<Tabs defaultValue={selectedTab ?? getBundleName(nonEmptyBundles[0]?.manifest.name ?? bundles[0].manifest.name)} onTabChange={onTabChange}>
<TabList className="justify-between px-4 py-3" height={false}>
{!isSearching && (
<div className="flex flex-row gap-6 px-2">
{nonEmptyBundles.map(bundle => <Tab key={bundle.path} value={getBundleName(bundle.manifest.name)}>{getBundleName(bundle.manifest.name)}</Tab>)}
</div>
)}
{isSearching && <div />}
<div className="flex flex-row items-center gap-2">
<TextField
className="w-48"
iconLeft={<SearchMdIcon className="scale-75" />}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search..."
value={searchQuery}
/>
{!useVerticalGridLayout && (
<Button onPress={() => setUseGrid(!useGridLayout)}>
Toggle {useGridLayout ? 'Row' : 'Grid'}
</Button>
)}
</div>
</TabList>
{isSearching
? (
<div className={useVerticalGridLayout ? 'pt-0' : 'p-2'}>
<Bundle cluster={cluster} files={searchResults} />
</div>
)
: (
<TabContent className={useVerticalGridLayout ? 'pt-0' : ''}>
{nonEmptyBundles.map((bundleData) => {
const isToggle = toggleBundlePaths?.has(bundleData.path) ?? false;
const content = <Bundle cluster={cluster} files={bundleData.manifest.files} />;
return (
<TabPanel className={useVerticalGridLayout ? 'max-w-192' : ''} key={getBundleName(bundleData.manifest.name)} value={getBundleName(bundleData.manifest.name)}>
{isToggle
? (
<ModCardContext.Provider value={{ ...ctx, useToggleMode: true }}>
{content}
</ModCardContext.Provider>
)
: content}
</TabPanel>
);
})}
</TabContent>
)}
</Tabs>
);
}

View file

@ -1,16 +0,0 @@
import type { ClusterModel } from '@/bindings.gen';
import type { ModInfo } from '@/components';
import { useSettings } from '@/hooks/useSettings';
import { Link } from '@tanstack/react-router';
import { twMerge } from 'tailwind-merge';
export function ModTag({ modData, cluster }: { modData: ModInfo; cluster: ClusterModel }) {
const { setting } = useSettings();
const useGridLayout = setting('mod_list_use_grid');
return (
<Link className={twMerge('flex flex-row items-center justify-center px-4 rounded-full font-normal bg-component-bg border border-gray-100/5 h-8 scale-90', useGridLayout ? '-my-1' : '')} search={{ provider: 'Modrinth', packageId: modData.packageSlug ?? '', clusterId: cluster.id }} to="/app/cluster/browser/package">
<p>{modData.managed ? 'Modrinth' : 'External'}</p>
</Link>
);
}

View file

@ -1,5 +0,0 @@
export * from './Bundle';
export * from './DownloadModButton';
export * from './ModCard';
export * from './ModList';
export * from './ModTag';

View file

@ -1,18 +0,0 @@
import type { MinecraftCredentials } from '@/bindings.gen';
import { Overlay, RemoveAccountModal } from '@/components';
import { Button } from '@onelauncher/common/components';
import { Trash01Icon } from '@untitled-theme/icons-react';
export function DeleteAccountButton({ profile, onPress }: { profile: MinecraftCredentials; onPress: () => void }) {
return (
<Overlay.Trigger>
<Button className="group w-8 h-8" color="ghost" size="icon">
<Trash01Icon className="group-hover:stroke-danger" />
</Button>
<Overlay>
<RemoveAccountModal onPress={onPress} profile={profile} />
</Overlay>
</Overlay.Trigger>
);
}

View file

@ -1,297 +0,0 @@
import type { ExternalPackage, ManagedVersionDependency, ModpackFile, ModpackFileKind, Provider } from '@/bindings.gen';
import { getModMetaDataName, Overlay } from '@/components';
import { bindings } from '@/main';
import { useCommandMut } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
import { useNavigate } from '@tanstack/react-router';
import { useEffect, useImperativeHandle, useState } from 'react';
import { DialogTrigger } from 'react-aria-components';
export interface DownloadModsRef {
openDownloadDialog: (nextPath?: string) => void;
}
export interface BaseModData {
name: string;
clusterId: number;
managed: boolean;
bundleName: string | null;
fileKind: ModpackFileKind;
}
export interface ManagedModData extends BaseModData {
provider: Provider;
id: string;
versionId: string;
dependencies: Array<ManagedVersionDependency>;
}
export interface ExternalModData extends BaseModData {
package: ExternalPackage;
}
export type ModData = ManagedModData | ExternalModData;
export type ModDataArray = Array<ModData>;
export type DownloadModsParallelResult =
| {
ok: true;
index: number;
mod: ModData;
}
| {
ok: false;
index: number;
mod: ModData;
error: unknown;
};
export function isManagedMod(mod: ModData): mod is ManagedModData {
return mod.managed === true;
}
export interface ModWithBundle {
file: ModpackFile;
bundleName: string;
}
export function DownloadMods({ modsPerCluster, ref }: { modsPerCluster: Record<string, Array<ModWithBundle>>; ref: React.Ref<DownloadModsRef> }) {
const navigate = useNavigate();
const [isOpen, setOpen] = useState<boolean>(false);
const [mods, setMods] = useState<ModDataArray>([]);
const [nextPath, setNextPath] = useState<string>('/app');
useEffect(() => {
const modsList: ModDataArray = [];
for (const [clusterId, modsWithBundle] of Object.entries(modsPerCluster))
for (const { file: mod, bundleName } of modsWithBundle) {
if ('External' in mod.kind)
modsList.push({
name: getModMetaDataName(mod),
clusterId: Number(clusterId),
managed: false,
package: mod.kind.External,
bundleName,
fileKind: mod.kind,
});
if ('Managed' in mod.kind) {
const [pkg, version] = mod.kind.Managed;
modsList.push({
name: getModMetaDataName(mod),
clusterId: Number(clusterId),
managed: true,
provider: pkg.provider,
id: pkg.id,
versionId: version.version_id,
dependencies: version.dependencies,
bundleName,
fileKind: mod.kind,
});
}
}
setMods(modsList);
}, [modsPerCluster]);
useImperativeHandle(ref, () => {
return {
openDownloadDialog(nextPath?: string) {
if (mods.length !== 0) {
setOpen(true);
setNextPath(nextPath ?? '/app');
}
else {
navigate({ to: nextPath ?? '/app' });
}
},
};
}, [mods.length, navigate]);
return (
<DialogTrigger>
<Button className="mb-4" isDisabled={mods.length === 0} onPress={() => setOpen(prev => !prev)}>Download Mods</Button>
<Overlay isDismissable={false} isOpen={isOpen}>
<DownloadingMods mods={mods} nextPath={nextPath} setOpen={setOpen} />
</Overlay>
</DialogTrigger>
);
}
export function buildModDataArray(modsPerCluster: Record<string, Array<ModWithBundle>>): ModDataArray {
const modsList: ModDataArray = [];
for (const [clusterId, modsWithBundle] of Object.entries(modsPerCluster))
for (const { file: mod, bundleName } of modsWithBundle) {
if ('External' in mod.kind)
modsList.push({
name: getModMetaDataName(mod),
clusterId: Number(clusterId),
managed: false,
package: mod.kind.External,
bundleName,
fileKind: mod.kind,
});
if ('Managed' in mod.kind) {
const [pkg, version] = mod.kind.Managed;
modsList.push({
name: getModMetaDataName(mod),
clusterId: Number(clusterId),
managed: true,
provider: pkg.provider,
id: pkg.id,
versionId: version.version_id,
dependencies: version.dependencies,
bundleName,
fileKind: mod.kind,
});
}
}
return modsList;
}
export async function downloadModsParallel(
items: ModDataArray,
limit: number,
fn: (mod: ModData, index: number) => Promise<void>,
): Promise<Array<DownloadModsParallelResult>> {
if (items.length === 0)
return [];
let index = 0;
const workerCount = Math.max(1, Math.min(limit, items.length));
const results: Array<DownloadModsParallelResult> = [];
const workers = Array.from({ length: workerCount }).fill(null).map(async () => {
while (index < items.length) {
const i = index;
index += 1;
if (i >= items.length)
return;
const mod = items[i];
try {
await fn(mod, i);
results.push({
ok: true,
index: i,
mod,
});
}
catch (error) {
results.push({
ok: false,
index: i,
mod,
error,
});
}
}
});
await Promise.all(workers);
results.sort((left, right) => left.index - right.index);
return results;
}
function DownloadingMods({ mods, setOpen, nextPath }: { mods: ModDataArray; setOpen: React.Dispatch<React.SetStateAction<boolean>>; nextPath: string }) {
const navigate = useNavigate();
const [downloadedMods, setDownloadedMods] = useState(0);
const [modName, setModName] = useState<string | null>(null);
const download = useCommandMut(async (mod: ModData) => {
if (isManagedMod(mod)) {
if (mod.dependencies.length > 0)
for (const dependency of mod.dependencies) {
const cluster = await bindings.core.getClusterById(mod.clusterId);
if (!cluster)
continue;
if (dependency.dependency_type === 'required') {
const slug = dependency.project_id ?? '';
const versions = await bindings.core.getPackageVersions(mod.provider, slug, cluster.mc_version, cluster.mc_loader, 0, 1);
if (versions.items.length !== 0)
await bindings.core.downloadPackage(mod.provider, slug, versions.items[0].version_id, cluster.id, null);
}
}
if (mod.bundleName)
await bindings.oneclient.downloadPackageFromBundle(
mod.fileKind,
mod.clusterId,
mod.bundleName,
true,
);
else
await bindings.core.downloadPackage(mod.provider, mod.id, mod.versionId, mod.clusterId, true);
}
else {
if (mod.bundleName)
await bindings.oneclient.downloadPackageFromBundle(
mod.fileKind,
mod.clusterId,
mod.bundleName,
true,
);
else
await bindings.core.downloadExternalPackage(mod.package, mod.clusterId, null, null);
}
});
useEffect(() => {
const downloadAll = async () => {
const results = await downloadModsParallel(mods, 10, async (mod) => {
setModName(mod.name);
try {
await download.mutateAsync(mod);
}
finally {
setDownloadedMods(prev => prev + 1);
}
});
const failed = results.filter(result => !result.ok);
if (failed.length > 0)
console.error('[DownloadMods] Some mods failed to download:', {
failed: failed.length,
total: mods.length,
entries: failed.map(result => ({
index: result.index,
name: result.mod.name,
error: result.error,
})),
});
};
void downloadAll();
// eslint-disable-next-line react-hooks/exhaustive-deps -- download is not stable, adding it would cause infinite loops
}, [mods]);
useEffect(() => {
if (downloadedMods >= mods.length) {
setOpen(false);
navigate({ to: nextPath });
}
}, [downloadedMods, mods, navigate, nextPath, setOpen]);
return (
<Overlay.Dialog isDismissable={false}>
<Overlay.Title>Downloading Mods</Overlay.Title>
<div className="w-full flex flex-col items-center gap-2">
<p>Downloaded {downloadedMods} / {mods.length}</p>
<div className="w-1/2 h-4 bg-component-bg-disabled rounded-full outline-2 outline-ghost-overlay">
<div
className="h-full bg-brand rounded-full transition-all duration-300"
style={{ width: mods.length > 0 ? `${(downloadedMods / mods.length) * 100}%` : '0%' }}
>
</div>
</div>
{modName !== null ? <p>Downloading {modName}</p> : <></>}
</div>
</Overlay.Dialog>
);
}

View file

@ -1,87 +0,0 @@
import type React from 'react';
import type { HTMLProps } from 'react';
import { Overlay } from '@/components';
import { bindings } from '@/main';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { LinkExternal01Icon } from '@untitled-theme/icons-react';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface ExternalLinkProps extends HTMLProps<HTMLAnchorElement> {
href?: string | undefined;
children?: React.ReactNode;
includeIcon?: boolean | undefined;
showModal?: boolean | undefined;
}
export function ExternalLink({
href,
children,
includeIcon,
showModal = true,
className,
...rest
}: ExternalLinkProps) {
const [isOpen, setIsOpen] = useState(false);
// done like this so that "image" links don't have an icon
const withIcon = !!(includeIcon ?? typeof children === 'string');
return (
<Overlay.Trigger isOpen={isOpen} onOpenChange={setIsOpen}>
<a
className={twMerge('text-fg-primary hover:opacity-80 underline', className)}
href={href}
onClick={(e) => {
e.preventDefault();
if (!href)
return;
if (showModal)
setIsOpen(true);
else
bindings.core.open(href);
}}
role="button"
{...rest}
>
{children}
{withIcon && <LinkExternal01Icon className="inline w-3 ml-0.5 h-3" />}
</a>
<Overlay>
<Overlay.Dialog>
<Overlay.Title>External Link</Overlay.Title>
<p>Are you sure you want to open this link in your browser?</p>
<code className="w-full text-center bg-component-bg py-2 rounded-md overflow-auto max-w-100 text-nowrap">{href}</code>
<Overlay.Buttons
buttons={[
{
color: 'secondary',
children: 'Copy Link',
onClick: () => {
if (href)
writeText(href);
setIsOpen(false);
},
},
{
color: 'primary',
children: 'Open Link',
onClick: () => {
if (href)
bindings.core.open(href);
setIsOpen(false);
},
},
]}
/>
</Overlay.Dialog>
</Overlay>
</Overlay.Trigger>
);
}

View file

@ -1,31 +0,0 @@
/* eslint-disable perfectionist/sort-imports -- nah */
import type { CSSProperties } from 'react';
import { twMerge } from 'tailwind-merge';
import CavesAndCliffs from '@/assets/backgrounds/CavesAndCliffs.jpg';
import HypixelSkyblockHub from '@/assets/backgrounds/HypixelSkyblockHub.png';
const GameBackgrounds = {
HypixelSkyblockHub,
CavesAndCliffs,
MinecraftBuilding: HypixelSkyblockHub,
};
export type GameBackgroundName = keyof typeof GameBackgrounds;
interface GameBackgroundProps {
name: GameBackgroundName;
className?: string;
style?: CSSProperties;
}
export function GameBackground({ name, className = '', style = undefined }: GameBackgroundProps) {
const BackgroundImage = GameBackgrounds[name];
return (
<div
className={twMerge(`pointer-events-none inset-0 bg-cover bg-center bg-no-repeat`, className)}
style={{ backgroundImage: `url(${BackgroundImage})`, ...style }}
/>
);
}

View file

@ -1,128 +0,0 @@
import type { ClusterModel } from '@/bindings.gen';
import type { ButtonProps } from '@onelauncher/common/components';
import { KillMinecraft, NoAccountPopup, Overlay } from '@/components';
import { useIsRunning } from '@/hooks/useClusters';
import { useLaunchCluster } from '@/hooks/useLaunchCluster';
import { bindings } from '@/main';
import { useCommandSuspense } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
import { useNavigate } from '@tanstack/react-router';
import { useState } from 'react';
import { tv } from 'tailwind-variants';
export type LaunchButtonProps = Omit<ButtonProps, 'children' | 'onClick'> & {
cluster: ClusterModel;
};
const launchButtonVariants = tv({
variants: {
isRunning: {
true: 'bg-success hover:bg-success-hover pressed:bg-success-pressed disabled:bg-success-disabled pending:bg-success-disabled',
},
},
});
function PromptForOnboarding({ launch, setSkipPackagesCheck, cluster }: { launch: () => void; setSkipPackagesCheck: React.Dispatch<React.SetStateAction<boolean>>; cluster: ClusterModel }) {
const navigate = useNavigate();
const onClickYes = () => {
localStorage.setItem('selectedClusters', JSON.stringify([{ mc_version: cluster.mc_version, mc_loader: cluster.mc_loader }]));
navigate({ to: `/onboarding/preferences/versionCategory` });
};
const onClickLaunch = () => {
setSkipPackagesCheck(true);
launch();
};
return (
<Overlay.Dialog>
<Overlay.Title>Not Setup</Overlay.Title>
<div className="flex flex-col items-center">
<p className="max-w-sm text-fg-secondary">It seems like you haven't setup this version</p>
<p className="max-w-sm text-fg-secondary">Do you want to setup this version?</p>
</div>
<Overlay.Buttons
buttons={[
{ color: 'primary', key: 'Yes', children: 'Yes', size: 'normal', onClick: onClickYes },
{
color: 'secondary',
key: 'anyways',
children: 'Launch Anyways',
size: 'normal',
onClick: onClickLaunch,
},
]}
/>
</Overlay.Dialog>
);
}
export function LaunchButton({
cluster,
isDisabled,
className,
...rest
}: LaunchButtonProps) {
const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true));
const { data: installedPackages } = useCommandSuspense(['getLinkedPackages', cluster.id], () => bindings.core.getLinkedPackages(cluster.id));
const { launch: launchCluster, showDownloadWarning, setShowDownloadWarning, forceLaunch } = useLaunchCluster(cluster.id);
const isRunning = useIsRunning(cluster.id);
const [reason, setReason] = useState<'account' | 'packages' | 'kill' | null>(null);
const [skipPackagesCheck, setSkipPackagesCheck] = useState(false);
const launch = () => {
if (isRunning)
return setReason('kill');
if (!currentAccount)
return setReason('account');
if (installedPackages.length === 0 && !skipPackagesCheck)
return setReason('packages');
launchCluster();
setSkipPackagesCheck(false);
setReason(null);
};
return (
<>
<Overlay.Trigger isOpen={reason !== null} onOpenChange={open => !open && setReason(null)}>
<Button
className={launchButtonVariants({ isRunning, className })}
isDisabled={isDisabled}
onPress={launch}
{...rest}
>
{isRunning ? 'Running' : 'Launch'}
</Button>
<Overlay>
{reason === 'kill' && <KillMinecraft setOpen={() => setReason(null)} />}
{reason === 'packages' && <PromptForOnboarding cluster={cluster} launch={launch} setSkipPackagesCheck={setSkipPackagesCheck} />}
{reason === 'account' && <NoAccountPopup />}
</Overlay>
</Overlay.Trigger>
<Overlay isOpen={showDownloadWarning} onOpenChange={setShowDownloadWarning}>
<Overlay.Dialog>
<Overlay.Title>Cluster is downloading</Overlay.Title>
<div className="flex flex-col items-center">
<p className="max-w-sm text-fg-secondary">The cluster is currently downloading. Launching it now might cause issues or corruption. Are you sure you want to proceed?</p>
</div>
<Overlay.Buttons
buttons={[
{ color: 'secondary', key: 'Cancel', children: 'Cancel', size: 'normal', onClick: () => setShowDownloadWarning(false) },
{
color: 'primary',
key: 'Proceed',
children: 'Proceed Anyways',
size: 'normal',
onClick: forceLaunch,
},
]}
/>
</Overlay.Dialog>
</Overlay>
</>
);
}

View file

@ -1,68 +0,0 @@
import type { SpinnerProps } from '@/components';
import type { HTMLAttributes, ReactNode } from 'react';
import { Spinner } from '@/components';
import { motion } from 'motion/react';
import { Suspense } from 'react';
import { twMerge } from 'tailwind-merge';
interface LoaderProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
spinner?: SpinnerProps;
}
export function LoaderContainer({
loading = false,
children,
spinner,
className,
}: LoaderProps & {
loading?: boolean;
}) {
return loading ? <Fallback className={className} spinner={spinner} /> : <LoadedChildren className={className}>{children}</LoadedChildren>;
}
export function LoaderSuspense({
children,
spinner,
className,
}: LoaderProps) {
return (
<Suspense fallback={<Fallback className={className} spinner={spinner} />}>
<LoadedChildren className={className}>
{children}
</LoadedChildren>
</Suspense>
);
}
function Fallback({
spinner,
className,
}: {
spinner?: SpinnerProps;
className?: string;
}) {
return (
<div className={twMerge('w-full h-full flex justify-center items-center', className)}>
<Spinner {...spinner} />
</div>
);
}
function LoadedChildren({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<motion.div
animate={{ opacity: 1 }}
className={twMerge('w-full h-full', className)}
initial={{ opacity: 0 }}
>
{children}
</motion.div>
);
}

View file

@ -1,56 +0,0 @@
@reference "../styles/global.css";
.log_container {
@apply relative w-full whitespace-pre text-base font-mono bg-component-bg rounded-xl;
}
.log {
@apply absolute top-0 left-0 w-full whitespace-pre text-base font-mono;
}
.log * {
white-space: pre !important;
user-select: text !important;
-webkit-user-select: text !important;
}
.log > span {
@apply box-content left-0 top-0 absolute h-full w-full rounded-md hover:bg-component-bg-hover;
}
.log > span[data-level=INFO] {
@apply text-code-info;
}
.log > span[data-level=WARN] {
@apply text-code-warn;
}
.log > span[data-level=ERROR] {
@apply text-code-error;
}
.log > span[data-level=DEBUG] {
@apply text-code-debug;
}
.log > span[data-level=TRACE] {
@apply text-code-trace;
}
.log > span > span:nth-child(1) {
@apply relative text-transparent;
}
.log > span > span:nth-child(1):before {
content: attr(data-time);
@apply absolute h-full w-full text-center select-text text-fg-secondary opacity-100;
}
.log > span > span:nth-child(2) {
@apply font-bold;
}
.log > span > span[data-chat="true"] {
@apply text-white;
}

View file

@ -1,198 +0,0 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import styles from './LogViewer.module.css';
export interface LogViewerProps {
content: string;
scrollRef: React.RefObject<HTMLElement | null>;
autoScroll?: boolean;
maxLines?: number;
onTrimmedLines?: (trimmedLines: number) => void;
ref?: React.Ref<LogViewerRef>;
}
export interface LogViewerRef {
push: (line: string) => void;
scrollToEnd: () => void;
}
const LINE_HEIGHT = 16;
const REGEX_PATTERN = /\[((?:\S+ )?\d+:\d+:\d+(?:\.\d+)?)\] \[(.[^(\n\r/\u2028\u2029]*)\/(\w+)\]:? (?:\[(CHAT)\])?/;
function normalizeLineLimit(maxLines?: number): number {
if (maxLines === undefined || !Number.isFinite(maxLines))
return Number.POSITIVE_INFINITY;
return Math.max(1, Math.floor(maxLines));
}
interface TrimResult {
lines: Array<string>;
trimmedCount: number;
}
function trimLines(
lines: Array<string>,
lineLimit: number,
): TrimResult {
if (!Number.isFinite(lineLimit) || lines.length <= lineLimit)
return {
lines,
trimmedCount: 0,
};
const trimmedCount = lines.length - lineLimit;
return {
lines: lines.slice(trimmedCount),
trimmedCount,
};
}
export function LogViewer({
content,
scrollRef,
autoScroll = false,
maxLines,
onTrimmedLines,
ref,
}: LogViewerProps) {
const lineLimit = normalizeLineLimit(maxLines);
const containerRef = useRef<HTMLDivElement>(null);
const pendingLinesRef = useRef<Array<string>>([]);
const frameRef = useRef<number | null>(null);
const [lines, setLines] = useState<Array<string>>(() => trimLines(content.split('\n'), lineLimit).lines);
const linesRef = useRef<Array<string>>(lines);
const virtualizer = useVirtualizer({
count: lines.length,
estimateSize: () => LINE_HEIGHT,
getScrollElement: () => scrollRef.current ?? null,
paddingStart: 4,
paddingEnd: 4,
gap: 0,
overscan: 8,
scrollMargin: containerRef.current ? containerRef.current.offsetTop + 75 : 0,
});
const scrollToEnd = useCallback(() => {
requestAnimationFrame(() => scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }));
}, [scrollRef]);
useEffect(() => {
pendingLinesRef.current = [];
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current);
frameRef.current = null;
}
const nextState = trimLines(content.split('\n'), lineLimit);
linesRef.current = nextState.lines;
setLines(nextState.lines);
if (nextState.trimmedCount > 0)
onTrimmedLines?.(nextState.trimmedCount);
}, [content, lineLimit, onTrimmedLines]);
useEffect(() => {
return () => {
if (frameRef.current !== null)
cancelAnimationFrame(frameRef.current);
};
}, []);
const appendLine = useCallback((line: string) => {
pendingLinesRef.current.push(line);
if (frameRef.current !== null)
return;
frameRef.current = requestAnimationFrame(() => {
frameRef.current = null;
if (pendingLinesRef.current.length === 0)
return;
const pending = pendingLinesRef.current;
pendingLinesRef.current = [];
const nextState = trimLines(linesRef.current.concat(pending), lineLimit);
linesRef.current = nextState.lines;
setLines(nextState.lines);
if (nextState.trimmedCount > 0)
onTrimmedLines?.(nextState.trimmedCount);
if (autoScroll)
scrollToEnd();
});
}, [autoScroll, lineLimit, onTrimmedLines, scrollToEnd]);
useImperativeHandle(ref, () => ({
push: appendLine,
scrollToEnd,
}), [appendLine, scrollToEnd]);
return (
<div
ref={containerRef}
style={{
height: `${virtualizer.getTotalSize()}px`,
}}
>
<OverlayScrollbarsComponent
className={styles.log_container}
options={{
overflow: {
x: 'scroll',
y: 'hidden',
},
}}
style={{
height: '100%',
}}
>
<code className={styles.log}>
{virtualizer.getVirtualItems().map(virtualRow => (
<LogLine
key={virtualRow.key}
line={lines[virtualRow.index]}
style={{
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start - virtualizer.options.scrollMargin}px)`,
}}
/>
))}
</code>
</OverlayScrollbarsComponent>
</div>
);
}
function LogLine({
line,
style,
}: {
line: string;
style: React.CSSProperties;
}) {
line = line.replace(/§./g, ''); // remove color codes
const groups = line.match(REGEX_PATTERN);
if (groups === null)
return (
<span style={style}>
{line}
<br />
</span>
);
const [matched, time, threadName, level, isChat] = groups as [string, string, string, string, string | undefined];
return (
<span data-level={level} style={style}>
<span data-time={time}>[{time}]</span>
<span>[{threadName}/{level}]</span>
<span data-chat={isChat ? 'true' : undefined}>{isChat ? ' [CHAT]' : ' '}{line.slice(matched.length)}</span>
<br />
</span>
);
}

View file

@ -1,22 +0,0 @@
import { useCopyDebugInfo } from '@/hooks/useDebugInfo';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
export function MadeBy() {
const { data: version } = useCommand(['getPackageVersion'], () => bindings.debug.getPackageVersion());
const { data: isInDev } = useCommand(['isInDev'], () => bindings.debug.isInDev());
const copyDebugInfo = useCopyDebugInfo();
return (
<Button
className="flex flex-col items-start p-4 text-xs text-fg-secondary"
color="ghost"
onClick={copyDebugInfo}
>
<p>OneClient by Polyfrost</p>
<p>Version {version}</p>
<p>{isInDev ? 'Development' : 'Release'} Build</p>
</Button>
);
}

View file

@ -1,21 +0,0 @@
import type { MinecraftCredentials } from '@/bindings.gen';
import { Button } from '@onelauncher/common/components';
import { useNavigate } from '@tanstack/react-router';
import { Pencil01Icon } from '@untitled-theme/icons-react';
import { useCallback } from 'react';
export function ManageSkinButton({ profile }: { profile: MinecraftCredentials }) {
const navigate = useNavigate();
const manageSkin = useCallback(() => navigate({ to: `/app/account/skins`, search: { profile, search: false } }), [profile, navigate]);
return (
<Button
className="group w-8 h-8"
color="ghost"
onPress={manageSkin}
size="icon"
>
<Pencil01Icon className="group-hover:stroke-brand-hover" />
</Button>
);
}

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